diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index 8735fe66..40fc5ccf 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -49,6 +49,15 @@ --warning-foreground: hsl(0 0% 100%); --success: hsl(87 100% 37%); --success-foreground: hsl(0 0% 100%); + + /* code colors */ + --code: oklch(0.97 0 0); + --code-foreground: oklch(0.145 0 0); + --code-number: oklch(0.556 0 0); + + /* spacing and text */ + --spacing: 0.25rem; + --text-sm: 0.875rem; } .dark { @@ -90,6 +99,11 @@ --warning-foreground: hsl(0 0% 100%); --success: hsl(84 81% 44%); --success-foreground: hsl(0 0% 100%); + + /* code colors */ + --code: oklch(0.205 0 0); + --code-foreground: oklch(0.985 0 0); + --code-number: oklch(0.708 0 0); } @theme inline { @@ -135,6 +149,9 @@ --color-warning-foreground: var(--warning-foreground); --color-success: var(--success); --color-success-foreground: var(--success-foreground); + --color-code: var(--code); + --color-code-foreground: var(--code-foreground); + --color-code-number: var(--code-number); } /* Container settings */ @@ -152,3 +169,28 @@ @apply bg-background text-foreground; } } + +/* Shiki line numbers */ +code[data-line-numbers] { + counter-reset: line; + font-size: var(--text-sm); +} + +[data-line-numbers] [data-line]::before { + font-size: var(--text-sm); + counter-increment: line; + content: counter(line); + width: calc(var(--spacing) * 16); + padding-right: calc(var(--spacing) * 6); + text-align: right; + color: var(--color-code-number); + background-color: var(--color-code); + display: inline-block; + position: sticky; + left: 0; +} + +[data-line-numbers] [data-line] { + display: inline-block; + width: 100%; +} diff --git a/app/blocks/sidebar_01.rb b/app/blocks/sidebar_01.rb new file mode 100644 index 00000000..906686ad --- /dev/null +++ b/app/blocks/sidebar_01.rb @@ -0,0 +1,287 @@ +# frozen_string_literal: true + +# class Blocks::Sidebar01 < RubyUI::Block +class Blocks::Sidebar01 < Views::Base + FAVORITES = [ + {name: "Project Management & Task Tracking", emoji: "📊"}, + {name: "Movies & TV Shows", emoji: "🎬"}, + {name: "Books & Articles", emoji: "📚"}, + {name: "Recipes & Meal Planning", emoji: "🍽️"}, + {name: "Travel & Places", emoji: "🌍"}, + {name: "Health & Fitness", emoji: "🏋️"} + ].freeze + + WORKSPACES = [ + {name: "Personal Life Management", emoji: "🏡"}, + {name: "Work & Projects", emoji: "💼"}, + {name: "Side Projects", emoji: "🚀"}, + {name: "Learning & Courses", emoji: "📚"}, + {name: "Writing & Blogging", emoji: "📝"}, + {name: "Design & Development", emoji: "🎨"} + ].freeze + + def initialize(sidebar_state:) + @sidebar_state = sidebar_state + end + + CODE = <<~RUBY + SidebarWrapper do + Sidebar(collapsible: :icon) do + SidebarHeader do + SidebarMenu do + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + search_icon() + span { "Search" } + end + end + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#", active: true) do + home_icon() + span { "Home" } + end + end + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + inbox_icon() + span { "Inbox" } + SidebarMenuBadge { 4 } + end + end + end + end + SidebarContent do + SidebarGroup do + SidebarGroupLabel { "Favorites" } + SidebarMenu do + FAVORITES.each do |item| + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + span { item[:emoji] } + span { item[:name] } + end + DropdownMenu(options: { strategy: "fixed", placement: "right-start" }) do + SidebarMenuAction( + data: { + ruby_ui__dropdown_menu_target: "trigger", + action: "click->ruby-ui--dropdown-menu#toggle" + } + ) do + ellipsis_icon() + span(class: "sr-only") { "More" } + end + DropdownMenuContent do + DropdownMenuItem(href: '#') { "Profile" } + DropdownMenuItem(href: '#') { "Billing" } + DropdownMenuItem(href: '#') { "Team" } + DropdownMenuItem(href: '#') { "Subscription" } + end + end + end + end + end + end + SidebarGroup do + SidebarGroupLabel { "Workspaces" } + SidebarMenu do + WORKSPACES.each do |item| + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + span { item[:emoji] } + span { item[:name] } + end + DropdownMenu() do + SidebarMenuAction( + data: { + ruby_ui__dropdown_menu_target: "trigger", + action: "click->ruby-ui--dropdown-menu#toggle" + } + ) do + ellipsis_icon() + span(class: "sr-only") { "More" } + end + DropdownMenuContent do + DropdownMenuItem(href: '#') { "Profile" } + DropdownMenuItem(href: '#') { "Billing" } + DropdownMenuItem(href: '#') { "Team" } + DropdownMenuItem(href: '#') { "Subscription" } + end + end + end + end + end + end + SidebarGroup(class: "mt-auto") do + SidebarGroupContent do + SidebarMenu do + nav_secondary.each do |item| + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + item[:icon].call + span { item[:label] } + end + end + end + end + end + end + end + SidebarRail() + end + SidebarInset do + header(class: "flex h-16 shrink-0 items-center gap-2 border-b px-4") do + SidebarTrigger(class: "-ml-1") + end + end + end + RUBY + + def view_template + decoded_code = CGI.unescapeHTML(CODE) + instance_eval(decoded_code) + end + + private + + def nav_secondary + [ + {label: "Settings", icon: -> { settings_icon }}, + {label: "Help & Support", icon: -> { message_circle_question }} + ] + end + + def home_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-house" + ) do |s| + s.path(d: "M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8") + s.path(d: "M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z") + end + end + + def inbox_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-inbox" + ) do |s| + s.polyline(points: "22 12 16 12 14 15 10 15 8 12 2 12") + s.path(d: "M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z") + end + end + + def calendar_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-calendar" + ) do |s| + s.path(d: "M8 2v4") + s.path(d: "M16 2v4") + s.rect(width: "18", height: "18", x: "3", y: "4", rx: "2") + s.path(d: "M3 10h18") + end + end + + def search_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-search" + ) do |s| + s.circle(cx: "11", cy: "11", r: "8") + s.path(d: "M21 21L16.7 16.7") + end + end + + def settings_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-settings" + ) do |s| + s.path(d: "M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73 + l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z") + s.circle(cx: "12", cy: "12", r: "3") + end + end + + def plus_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-plus" + ) do |s| + s.path(d: "M5 12h14") + s.path(d: "M12 5v14") + end + end + + def gallery_vertical_end + svg(xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", view_box: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", stroke_linecap: "round", stroke_linejoin: "round", class: "lucide lucide-gallery-vertical-end size-4") do |s| + s.path d: "M7 2h10" + s.path d: "M5 6h14" + s.rect width: "18", height: "12", x: "3", y: "10", rx: "2" + end + end + + def ellipsis_icon + svg(xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", stroke_linecap: "round", stroke_line_join: "round", class: "lucide lucide-ellipsis-icon lucide-ellipsis") do |s| + s.circle cx: "12", cy: "12", r: "1" + s.circle cx: "19", cy: "12", r: "1" + s.circle cx: "5", cy: "12", r: "1" + end + end + + def message_circle_question + svg(xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", stroke_linecap: "round", stroke_line_join: "round", class: "lucide lucide-message-circle-question-icon lucide-message-circle-question") do |s| + s.path d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z" + s.path d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" + s.path d: "M12 17h.01" + end + end +end diff --git a/app/blocks/sidebar_02.rb b/app/blocks/sidebar_02.rb new file mode 100644 index 00000000..24907358 --- /dev/null +++ b/app/blocks/sidebar_02.rb @@ -0,0 +1,278 @@ +# frozen_string_literal: true + +# class Blocks::Sidebar01 < RubyUI::Block +class Blocks::Sidebar02 < Views::Base + FAVORITES = [ + {name: "Project Management & Task Tracking", emoji: "📊"}, + {name: "Movies & TV Shows", emoji: "🎬"}, + {name: "Books & Articles", emoji: "📚"}, + {name: "Recipes & Meal Planning", emoji: "🍽️"}, + {name: "Travel & Places", emoji: "🌍"}, + {name: "Health & Fitness", emoji: "🏋️"} + ].freeze + + WORKSPACES = [ + {name: "Personal Life Management", emoji: "🏡"}, + {name: "Work & Projects", emoji: "💼"}, + {name: "Side Projects", emoji: "🚀"}, + {name: "Learning & Courses", emoji: "📚"}, + {name: "Writing & Blogging", emoji: "📝"}, + {name: "Design & Development", emoji: "🎨"} + ].freeze + + def view_template + SidebarWrapper do + Sidebar(collapsible: :icon) do + SidebarHeader do + SidebarMenu do + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + search_icon + span { "Search" } + end + end + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#", active: true) do + home_icon + span { "Home" } + end + end + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + inbox_icon + span { "Inbox" } + SidebarMenuBadge { 4 } + end + end + end + end + SidebarContent do + SidebarGroup do + SidebarGroupLabel { "Favorites" } + SidebarMenu do + FAVORITES.each do |item| + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + span { item[:emoji] } + span { item[:name] } + end + DropdownMenu(options: {strategy: "fixed", placement: "right-start"}) do + SidebarMenuAction( + data: { + ruby_ui__dropdown_menu_target: "trigger", + action: "click->ruby-ui--dropdown-menu#toggle" + } + ) do + ellipsis_icon + span(class: "sr-only") { "More" } + end + DropdownMenuContent do + DropdownMenuItem(href: "#") { "Profile" } + DropdownMenuItem(href: "#") { "Billing" } + DropdownMenuItem(href: "#") { "Team" } + DropdownMenuItem(href: "#") { "Subscription" } + end + end + end + end + end + end + SidebarGroup do + SidebarGroupLabel { "Workspaces" } + SidebarMenu do + WORKSPACES.each do |item| + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + span { item[:emoji] } + span { item[:name] } + end + DropdownMenu() do + SidebarMenuAction( + data: { + ruby_ui__dropdown_menu_target: "trigger", + action: "click->ruby-ui--dropdown-menu#toggle" + } + ) do + ellipsis_icon + span(class: "sr-only") { "More" } + end + DropdownMenuContent do + DropdownMenuItem(href: "#") { "Profile" } + DropdownMenuItem(href: "#") { "Billing" } + DropdownMenuItem(href: "#") { "Team" } + DropdownMenuItem(href: "#") { "Subscription" } + end + end + end + end + end + end + SidebarGroup(class: "mt-auto") do + SidebarGroupContent do + SidebarMenu do + nav_secondary.each do |item| + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + item[:icon].call + span { item[:label] } + end + end + end + end + end + end + end + SidebarRail() + end + SidebarInset do + header(class: "flex h-16 shrink-0 items-center gap-2 border-b px-4") do + SidebarTrigger(class: "-ml-1") + end + end + end + end + + private + + def nav_secondary + [ + {label: "Settings", icon: -> { settings_icon }}, + {label: "Help & Support", icon: -> { message_circle_question }} + ] + end + + def home_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-house" + ) do |s| + s.path(d: "M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8") + s.path(d: "M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z") + end + end + + def inbox_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-inbox" + ) do |s| + s.polyline(points: "22 12 16 12 14 15 10 15 8 12 2 12") + s.path(d: "M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z") + end + end + + def calendar_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-calendar" + ) do |s| + s.path(d: "M8 2v4") + s.path(d: "M16 2v4") + s.rect(width: "18", height: "18", x: "3", y: "4", rx: "2") + s.path(d: "M3 10h18") + end + end + + def search_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-search" + ) do |s| + s.circle(cx: "11", cy: "11", r: "8") + s.path(d: "M21 21L16.7 16.7") + end + end + + def settings_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-settings" + ) do |s| + s.path(d: "M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73 + l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z") + s.circle(cx: "12", cy: "12", r: "3") + end + end + + def plus_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-plus" + ) do |s| + s.path(d: "M5 12h14") + s.path(d: "M12 5v14") + end + end + + def gallery_vertical_end + svg(xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", view_box: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", stroke_linecap: "round", stroke_linejoin: "round", class: "lucide lucide-gallery-vertical-end size-4") do |s| + s.path d: "M7 2h10" + s.path d: "M5 6h14" + s.rect width: "18", height: "12", x: "3", y: "10", rx: "2" + end + end + + def ellipsis_icon + svg(xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", stroke_linecap: "round", stroke_line_join: "round", class: "lucide lucide-ellipsis-icon lucide-ellipsis") do |s| + s.circle cx: "12", cy: "12", r: "1" + s.circle cx: "19", cy: "12", r: "1" + s.circle cx: "5", cy: "12", r: "1" + end + end + + def message_circle_question + svg(xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", stroke_linecap: "round", stroke_line_join: "round", class: "lucide lucide-message-circle-question-icon lucide-message-circle-question") do |s| + s.path d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z" + s.path d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" + s.path d: "M12 17h.01" + end + end +end diff --git a/app/blocks/sidebar_02/app_sidebar.rb b/app/blocks/sidebar_02/app_sidebar.rb new file mode 100644 index 00000000..6b5e339f --- /dev/null +++ b/app/blocks/sidebar_02/app_sidebar.rb @@ -0,0 +1,217 @@ +# frozen_string_literal: true + +class Blocks::Sidebar02::AppSidebar < Views::Base + def view_template + Sidebar(collapsible: :icon) do + SidebarHeader do + SidebarMenu do + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + search_icon + span { "Search" } + end + end + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#", active: true) do + home_icon + span { "Home" } + end + end + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + inbox_icon + span { "Inbox" } + SidebarMenuBadge { 4 } + end + end + end + end + SidebarContent do + SidebarGroup do + SidebarGroupLabel { "Favorites" } + SidebarMenu do + Blocks::Sidebar02::Index::FAVORITES.each do |item| + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + span { item[:emoji] } + span { item[:name] } + end + DropdownMenu(options: {strategy: "fixed", placement: "right-start"}) do + SidebarMenuAction( + data: { + ruby_ui__dropdown_menu_target: "trigger", + action: "click->ruby-ui--dropdown-menu#toggle" + } + ) do + ellipsis_icon + span(class: "sr-only") { "More" } + end + DropdownMenuContent do + DropdownMenuItem(href: "#") { "Profile" } + DropdownMenuItem(href: "#") { "Billing" } + DropdownMenuItem(href: "#") { "Team" } + DropdownMenuItem(href: "#") { "Subscription" } + end + end + end + end + end + end + SidebarGroup do + SidebarGroupLabel { "Workspaces" } + SidebarMenu do + Blocks::Sidebar02::Index::WORKSPACES.each do |item| + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + span { item[:emoji] } + span { item[:name] } + end + DropdownMenu() do + SidebarMenuAction( + data: { + ruby_ui__dropdown_menu_target: "trigger", + action: "click->ruby-ui--dropdown-menu#toggle" + } + ) do + ellipsis_icon + span(class: "sr-only") { "More" } + end + DropdownMenuContent do + DropdownMenuItem(href: "#") { "Profile" } + DropdownMenuItem(href: "#") { "Billing" } + DropdownMenuItem(href: "#") { "Team" } + DropdownMenuItem(href: "#") { "Subscription" } + end + end + end + end + end + end + SidebarGroup(class: "mt-auto") do + SidebarGroupContent do + SidebarMenu do + nav_secondary.each do |item| + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + item[:icon].call + span { item[:label] } + end + end + end + end + end + end + end + SidebarRail() + end + end + + private + + def nav_secondary + [ + {label: "Settings", icon: -> { settings_icon }}, + {label: "Help & Support", icon: -> { message_circle_question }} + ] + end + + def search_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-search" + ) do |s| + s.circle(cx: "11", cy: "11", r: "8") + s.path(d: "M21 21L16.7 16.7") + end + end + + def home_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-house" + ) do |s| + s.path(d: "M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8") + s.path(d: "M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z") + end + end + + def inbox_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-inbox" + ) do |s| + s.polyline(points: "22 12 16 12 14 15 10 15 8 12 2 12") + s.path(d: "M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z") + end + end + + def settings_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-settings" + ) do |s| + s.path(d: "M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73 + l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z") + s.circle(cx: "12", cy: "12", r: "3") + end + end + + def message_circle_question + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_line_join: "round", + class: "lucide lucide-message-circle-question-icon lucide-message-circle-question" + ) do |s| + s.path d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z" + s.path d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" + s.path d: "M12 17h.01" + end + end + + def ellipsis_icon + svg(xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", stroke_linecap: "round", stroke_line_join: "round", class: "lucide lucide-ellipsis-icon lucide-ellipsis") do |s| + s.circle cx: "12", cy: "12", r: "1" + s.circle cx: "19", cy: "12", r: "1" + s.circle cx: "5", cy: "12", r: "1" + end + end +end diff --git a/app/blocks/sidebar_02/index.rb b/app/blocks/sidebar_02/index.rb new file mode 100644 index 00000000..b6efbe53 --- /dev/null +++ b/app/blocks/sidebar_02/index.rb @@ -0,0 +1,203 @@ +class Blocks::Sidebar02::Index < Views::Base + FAVORITES = [ + {name: "Project Management & Task Tracking", emoji: "📊"}, + {name: "Movies & TV Shows", emoji: "🎬"}, + {name: "Books & Articles", emoji: "📚"}, + {name: "Recipes & Meal Planning", emoji: "🍽️"}, + {name: "Travel & Places", emoji: "🌍"}, + {name: "Health & Fitness", emoji: "🏋️"} + ].freeze + + WORKSPACES = [ + {name: "Personal Life Management", emoji: "🏡"}, + {name: "Work & Projects", emoji: "💼"}, + {name: "Side Projects", emoji: "🚀"}, + {name: "Learning & Courses", emoji: "📚"}, + {name: "Writing & Blogging", emoji: "📝"}, + {name: "Design & Development", emoji: "🎨"} + ].freeze + + def initialize(sidebar_state:) + @sidebar_state = sidebar_state + end + + CODE = <<~RUBY + SidebarWrapper do + render Blocks::Sidebar02::AppSidebar.new + SidebarInset do + header(class: "flex h-16 shrink-0 items-center gap-2 border-b px-4") do + SidebarTrigger(class: "-ml-1") + Separator(orientation: :vertical, class: "mr-2 h-4") + Breadcrumb do + BreadcrumbList do + BreadcrumbItem(class: "hidden md:block") do + BreadcrumbLink(href: "#") { "Building Your Application" } + end + BreadcrumbSeparator(class: "hidden md:block") + BreadcrumbItem do + BreadcrumbPage { "Data Fetching" } + end + end + end + end + div(class: "flex flex-1 flex-col gap-4 p-4") do + div(class: "grid auto-rows-min gap-4 md:grid-cols-3") do + div(class: "aspect-video rounded-xl bg-muted/50") + div(class: "aspect-video rounded-xl bg-muted/50") + div(class: "aspect-video rounded-xl bg-muted/50") + end + div(class: "min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min") + end + end + end + RUBY + + def view_template + decoded_code = CGI.unescapeHTML(CODE) + instance_eval(decoded_code) + end + + private + + def nav_secondary + [ + {label: "Settings", icon: -> { settings_icon }}, + {label: "Help & Support", icon: -> { message_circle_question }} + ] + end + + def home_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-house" + ) do |s| + s.path(d: "M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8") + s.path(d: "M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z") + end + end + + def inbox_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-inbox" + ) do |s| + s.polyline(points: "22 12 16 12 14 15 10 15 8 12 2 12") + s.path(d: "M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z") + end + end + + def calendar_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-calendar" + ) do |s| + s.path(d: "M8 2v4") + s.path(d: "M16 2v4") + s.rect(width: "18", height: "18", x: "3", y: "4", rx: "2") + s.path(d: "M3 10h18") + end + end + + def search_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-search" + ) do |s| + s.circle(cx: "11", cy: "11", r: "8") + s.path(d: "M21 21L16.7 16.7") + end + end + + def settings_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-settings" + ) do |s| + s.path(d: "M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73 + l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z") + s.circle(cx: "12", cy: "12", r: "3") + end + end + + def plus_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-plus" + ) do |s| + s.path(d: "M5 12h14") + s.path(d: "M12 5v14") + end + end + + def gallery_vertical_end + svg(xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", view_box: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", stroke_linecap: "round", stroke_linejoin: "round", class: "lucide lucide-gallery-vertical-end size-4") do |s| + s.path d: "M7 2h10" + s.path d: "M5 6h14" + s.rect width: "18", height: "12", x: "3", y: "10", rx: "2" + end + end + + def ellipsis_icon + svg(xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", stroke_linecap: "round", stroke_line_join: "round", class: "lucide lucide-ellipsis-icon lucide-ellipsis") do |s| + s.circle cx: "12", cy: "12", r: "1" + s.circle cx: "19", cy: "12", r: "1" + s.circle cx: "5", cy: "12", r: "1" + end + end + + def message_circle_question + svg(xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", stroke_linecap: "round", stroke_line_join: "round", class: "lucide lucide-message-circle-question-icon lucide-message-circle-question") do |s| + s.path d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z" + s.path d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" + s.path d: "M12 17h.01" + end + end +end diff --git a/app/components/block_display.rb b/app/components/block_display.rb new file mode 100644 index 00000000..b521ec1c --- /dev/null +++ b/app/components/block_display.rb @@ -0,0 +1,443 @@ +class Components::BlockDisplay < Components::Base + def initialize(content:, description: nil) + @description = description + @content = content + @files = extract_files_from_block + end + + def view_template + div( + class: "group/block-view-wrapper", + data: { + controller: "custom-tabs block-code-viewer", + tab: "preview", + action: "tab-change->custom-tabs#setTab" + } + ) do + tool_bar + block_viewer_view + block_viewer_code + end + end + + def tool_bar + div(class: "hidden w-full items-center gap-2 pl-2 md:pr-6 lg:flex") do + Tabs(default: "preview") do + div(class: "flex justify-between items-end mb-4 gap-x-2") do + TabsList do + render_tab_trigger("preview", "Preview", method(:eye_icon)) + render_tab_trigger("code", "Code", method(:code_icon)) + end + Separator(orientation: :vertical) + + render_header + end + end + end + end + + def render_tool_bar + div(class: "hidden w-full items-center gap-2 pl-2 md:pr-6 lg:flex") do + Tabs(default: "preview") do + div(class: "flex justify-between items-end mb-4 gap-x-2") do + TabsList do + render_tab_trigger("preview", "Preview", method(:eye_icon)) + render_tab_trigger("code", "Code", method(:code_icon)) + end + end + end + end + end + + def block_viewer_view + div(class: "hidden group-data-[tab=code]/block-view-wrapper:hidden md:h-(--height) lg:flex") do + div(class: "relative grid w-full gap-4") do + div(class: "absolute inset-0 right-4 [background-image:radial-gradient(#d4d4d4_1px,transparent_1px)] [background-size:20px_20px] dark:[background-image:radial-gradient(#404040_1px,transparent_1px)]") + render_preview_tab + end + end + end + + def block_viewer_code + div(class: "bg-code text-code-foreground mr-[14px] flex overflow-hidden rounded-xl border group-data-[tab=preview]/block-view-wrapper:hidden h-[600px]") do + render_file_tree + render_code_content + end + end + + def render_file_tree + div(class: "w-72") do + div( + data_slot: "sidebar-wrapper", + style: "--sidebar-width:16rem;--sidebar-width-icon:3rem", + class: "group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar min-h-svh w-full flex !min-h-full flex-col border-r" + ) do + div( + data_slot: "sidebar", + class: "bg-sidebar text-sidebar-foreground flex h-full flex-col w-full flex-1" + ) do + SidebarGroupLabel(data_slot: "sidebar-group-label", class: "h-12 rounded-none border-b px-4 text-sm") { "Files" } + SidebarGroup(data_slot: "sidebar-group", class: "p-0") do + SidebarGroupContent(data_slot: "sidebar-group-content") do + SidebarMenu(data_slot: "sidebar-menu", class: "translate-x-0 gap-1.5") do + render_file_tree_items + end + end + end + end + end + end + end + + def render_file_tree_items + grouped_files = group_files_by_directory + grouped_files.each do |dir, files| + render_directory_item(dir, files) + end + end + + def render_directory_item(dir, files) + depth = dir.split("/").size - 1 + padding_left = "#{1 + (depth * 1.4)}rem" + + SidebarMenuItem(data_slot: "sidebar-menu-item") do + Collapsible( + open: true, + data_slot: "collapsible", + class: "group/collapsible" + ) do + CollapsibleTrigger(data_slot: "collapsible-trigger") do + SidebarMenuButton( + as: :button, + type: "button", + class: "rounded-none pl-(--index) whitespace-nowrap h-8 text-sm hover:bg-muted-foreground/15 focus:bg-muted-foreground/15 focus-visible:bg-muted-foreground/15 active:bg-muted-foreground/15 data-[active=true]:bg-muted-foreground/15", + style: "--index:#{padding_left}" + ) do + chevron_icon(collapsible: true) + folder_icon + span { dir.split("/").last } + end + end + CollapsibleContent(data_slot: "collapsible-content") do + SidebarMenuSub(data_slot: "sidebar-menu-sub", class: "m-0 w-full translate-x-0 border-none p-0") do + files.each_with_index do |file, index| + render_file_item(file, index, depth + 1) + end + end + end + end + end + end + + def render_file_item(file, index, depth) + padding_left = "#{1 + (depth * 1.4) + 0.5}rem" + + SidebarMenuSubItem(data_slot: "sidebar-menu-item") do + SidebarMenuButton( + active: index == 0, + as: :button, + data_slot: "sidebar-menu-button", + class: "rounded-none pl-(--index) whitespace-nowrap h-8 text-sm hover:bg-muted-foreground/15 focus:bg-muted-foreground/15 focus-visible:bg-muted-foreground/15 active:bg-muted-foreground/15 data-[active=true]:bg-muted-foreground/15", + style: "--index:#{padding_left}", + data: { + action: "click->block-code-viewer#selectFile", + file_path: file[:path], + block_code_viewer_target: "fileButton", + index: depth + } + ) do + chevron_icon(invisible: true) + file_icon + span { file[:name] } + end + end + end + + def group_files_by_directory + grouped = {} + @files.each do |file| + dir = File.dirname(file[:path]) + grouped[dir] ||= [] + grouped[dir] << file + end + grouped + end + + def render_code_content + figure(class: "!mx-0 mt-0 flex min-w-0 flex-1 flex-col rounded-xl border-none overflow-hidden") do + render_file_header + render_code_body + end + end + + def render_file_header + @files.each_with_index do |file, index| + figcaption( + class: "text-code-foreground [&_svg]:text-code-foreground relative flex h-12 shrink-0 items-center gap-2 border-b px-4 py-2 [&_svg]:size-4 [&_svg]:opacity-70 #{(index == 0) ? "" : "hidden"}", + data: { + file_path: file[:path], + block_code_viewer_target: "fileHeader" + } + ) do + file_icon + span(class: "text-sm font-light") { file[:path] } + + # Clipboard button + RubyUI.Clipboard(success: "Copied!", error: "Copy failed!", class: "absolute right-2") do + RubyUI.ClipboardSource do + pre(class: "hidden") { plain file[:code] } + end + RubyUI.ClipboardTrigger do + RubyUI.Button( + variant: :ghost, + size: :icon, + class: "size-7" + ) { clipboard_icon } + end + end + end + end + end + + def render_code_body + @files.each_with_index do |file, index| + div( + class: "overflow-y-auto flex-1 min-h-0 #{(index == 0) ? "" : "hidden"}", + data: { + file_path: file[:path], + block_code_viewer_target: "fileContent" + } + ) do + render CodeblockWithLineNumbers.new(code: file[:code], syntax: file[:syntax]) + end + end + end + + def render_tab_trigger(value, label, icon_method) + TabsTrigger(value: value) do + icon_method.call + span { label } + end + end + + def render_tab_contents + TabsContent(value: "preview") { render_preview_tab } + TabsContent(value: "code") { render_code_tab } + end + + def render_preview_tab + div(class: "relative aspect-[4/2.5] w-full overflow-hidden rounded-md border md:-mx-1", data: {controller: "iframe-theme"}) do + iframe(src: render_block_path(id: @content.to_s, attributes: @content_attributes), class: "size-full", data: {iframe_theme_target: "iframe"}) + end + end + + def render_code_tab + div(class: "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 relative rounded-md border") do + Codeblock("@display_code", syntax: :ruby, class: "-m-px") + end + end + + def render_header + div do + if @title + div do + Components.Heading(level: 4) { @title.capitalize } + end + end + p { @description } if @description + end + end + + def eye_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" + ) + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + end + + def code_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" + ) + end + end + + def chevron_icon(invisible: false, collapsible: false) + classes = ["lucide", "lucide-chevron-right", "transition-transform", "duration-200"] + classes << "invisible" if invisible + classes << "group-data-[state=open]/collapsible:rotate-90" if collapsible + + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: classes.join(" ") + ) do |s| + s.path(d: "m9 18 6-6-6-6") + end + end + + def folder_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-folder" + ) do |s| + s.path(d: "M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z") + end + end + + def file_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-file h-4 w-4" + ) do |s| + s.path(d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z") + s.path(d: "M14 2v4a2 2 0 0 0 2 2h4") + end + end + + def clipboard_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-clipboard" + ) do |s| + s.rect(width: "8", height: "4", x: "8", y: "2", rx: "1", ry: "1") + s.path(d: "M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2") + end + end + + private + + def extract_files_from_block + files = [] + + main_file_path = get_file_path_for_class(@content) + if File.exist?(main_file_path) + content = File.read(main_file_path) + files << { + name: File.basename(main_file_path), + path: relative_path_from_app(main_file_path), + code: content, + syntax: :ruby + } + end + + if @content.to_s.include?("::") + dir_path = File.dirname(main_file_path) + if File.directory?(dir_path) && dir_path != File.dirname(dir_path) + Dir.glob("#{dir_path}/*.rb").sort.each do |file_path| + next if file_path == main_file_path + content = File.read(file_path) + files << { + name: File.basename(file_path), + path: relative_path_from_app(file_path), + code: content, + syntax: :ruby + } + end + end + end + + files + end + + def get_file_path_for_class(klass) + parts = klass.to_s.split("::") + filename = parts.pop.underscore + path_parts = parts.map(&:underscore) + + Rails.root.join("app", *path_parts, "#{filename}.rb").to_s + end + + def relative_path_from_app(absolute_path) + absolute_path.sub(Rails.root.join("app").to_s + "/", "") + end + + # Inner component for rendering code with Shiki highlighting + class CodeblockWithLineNumbers < Components::Base + def initialize(code:, syntax:) + @code = code + @syntax = syntax + end + + def view_template + div( + class: "relative", + data: { + controller: "shiki-highlighter", + shiki_highlighter_language_value: @syntax.to_s + } + ) do + # Hidden code content for Shiki to process + pre( + class: "hidden", + data: {shiki_highlighter_target: "code"} + ) do + plain @code + end + + # Output container for Shiki-generated HTML + div( + class: "bg-code text-code-foreground overflow-y-auto", + data: {shiki_highlighter_target: "output"} + ) + end + end + end +end diff --git a/app/components/block_viewer.rb b/app/components/block_viewer.rb new file mode 100644 index 00000000..d3879ccc --- /dev/null +++ b/app/components/block_viewer.rb @@ -0,0 +1,23 @@ +class Components::BlockViewer < Components::Base + def view_template + render ComponentPreview.new(title: "Example", context: self) do + <<~RUBY + Button(disabled: true) { "Disabled" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Disabled", context: self) do + <<~RUBY + Button(disabled: true) { "Disabled" } + RUBY + end + + # render ComponentPreview.new( + # title: "Example", + # context: self, + # type: :block, + # content: Blocks::Sidebar02::Index, + # content_attributes: {sidebar_state: "open"} + # ) + end +end diff --git a/app/components/component_preview.rb b/app/components/component_preview.rb new file mode 100644 index 00000000..c2da35fb --- /dev/null +++ b/app/components/component_preview.rb @@ -0,0 +1,24 @@ +module Components + class ComponentPreview < Components::Base + def initialize(ruby_code: nil, title: nil, description: nil, src: nil, context: nil, type: :component, content: nil, content_attributes: nil) + @ruby_code = ruby_code + @title = title + @description = description + @src = src + @context = context + @type = type + @content = content + @content_attributes = content_attributes + end + + def view_template(&) + if @type == :block && @content + div(class: "relative aspect-[4/2.5] w-full overflow-hidden rounded-md border md:-mx-1", data: {controller: "iframe-theme"}) do + iframe(src: render_block_path(id: @content.to_s, attributes: @content_attributes), class: "size-full", data: {iframe_theme_target: "iframe"}) + end + else + render ComponentPreviewTabs.new(title: @title, description: @description, context: @context, ruby_code: @ruby_code) { capture(&) } + end + end + end +end diff --git a/app/components/component_preview_tabs.rb b/app/components/component_preview_tabs.rb new file mode 100644 index 00000000..230a6877 --- /dev/null +++ b/app/components/component_preview_tabs.rb @@ -0,0 +1,107 @@ +module Components + class ComponentPreviewTabs < Components::Base + def initialize(context:, ruby_code: nil, title: nil, description: nil) + @title = title + @description = description + @context = context + @ruby_code = ruby_code + end + + def view_template(&) + @display_code = @ruby_code || CGI.unescapeHTML(capture(&)) + div(id: @title) do + div(class: "relative") do + Tabs(default_value: "preview") do + div(class: "flex justify-between items-end mb-4 gap-x-2") do + render_header + TabsList do + render_tab_trigger("preview", "Preview", method(:eye_icon)) + render_tab_trigger("code", "Code", method(:code_icon)) + end + end + render_tab_contents(&) + end + end + end + # render ComponentRender.new(context: @context, ruby_code: @ruby_code) { capture(&) } + end + + def render_header + div do + if @title + div do + Components.Heading(level: 4) { @title.capitalize } + end + end + p { @description } if @description + end + end + + def render_tab_trigger(value, label, icon_method) + TabsTrigger(value: value) do + icon_method.call + span { label } + end + end + + def render_tab_contents(&) + TabsContent(value: "preview") { render_preview_tab(&) } + TabsContent(value: "code") { render_code_tab } + end + + def render_preview_tab(&) + div(class: "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 relative rounded-md border") do + div(class: "preview flex min-h-[350px] w-full justify-center p-10 items-center") do + decoded_code = CGI.unescapeHTML(@display_code) + @context.instance_eval(decoded_code) + end + end + end + + def render_code_tab + div(class: "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 relative rounded-md border") do + Codeblock(@display_code, syntax: :ruby, class: "-m-px") + end + end + + def eye_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" + ) + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + end + + def code_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" + ) + end + end + end +end diff --git a/app/components/component_render.rb b/app/components/component_render.rb new file mode 100644 index 00000000..a2f77b28 --- /dev/null +++ b/app/components/component_render.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Components + class ComponentRender < Components::Base + def initialize(context:, ruby_code: nil) + @ruby_code = ruby_code + @context = context + end + + def view_template(&) + @display_code = @ruby_code || CGI.unescapeHTML(capture(&)) + decoded_code = CGI.unescapeHTML(@display_code) + @context.instance_eval(decoded_code) + end + # standard:enable Style/ArgumentsForwarding + end +end diff --git a/app/components/docs/visual_code_example.rb b/app/components/docs/visual_code_example.rb index 9f8f2757..12aabd6b 100644 --- a/app/components/docs/visual_code_example.rb +++ b/app/components/docs/visual_code_example.rb @@ -13,15 +13,19 @@ def self.reset_collected_code @@collected_code = [] end - def initialize(title: nil, description: nil, src: nil, context: nil) + def initialize(ruby_code: nil, title: nil, description: nil, src: nil, context: nil, type: :component, content: nil, content_attributes: nil) + @ruby_code = ruby_code @title = title @description = description @src = src @context = context + @type = type + @content = content + @content_attributes = content_attributes end def view_template(&) - @display_code = CGI.unescapeHTML(capture(&)) + @display_code = @ruby_code || CGI.unescapeHTML(capture(&)) @@collected_code << @display_code div(id: @title) do @@ -55,7 +59,7 @@ def render_header def render_tab_triggers TabsList do render_tab_trigger("preview", "Preview", method(:eye_icon)) - render_tab_trigger("code", "Code", method(:code_icon)) + render_tab_trigger("code", "Code", method(:code_icon)) if @type == :component end end @@ -72,15 +76,25 @@ def render_tab_contents(&) end def render_preview_tab(&block) - return iframe_preview if @src + block_class_name = @content.to_s + + return iframe_preview(block_class_name) if @type == :block raw_preview end - def iframe_preview + def iframe_preview(block_name) div(class: "relative aspect-[4/2.5] w-full overflow-hidden rounded-md border", data: {controller: "iframe-theme"}) do div(class: "absolute inset-0 hidden w-[1600px] bg-background md:block") do - iframe(src: @src, class: "size-full", data: {iframe_theme_target: "iframe"}) + if @content + iframe(src: render_block_path(id: block_name, attributes: @content_attributes), class: "size-full", data: {iframe_theme_target: "iframe"}) + else + iframe(srcdoc: safe("
You cannot render a ruby block for a block preview
"), class: "size-full") + # TODO + # decoded_code = CGI.unescapeHTML(@display_code) + # html_content = render_block_to_html(decoded_code) + # iframe(srcdoc: safe(html_content), class: "size-full") + end end end end @@ -100,6 +114,14 @@ def render_code_tab end end + def render_block_to_html(code) + # Extract the component from "render ComponentName.new" pattern + # and evaluate it to generate standalone HTML + # component_code = code.strip.sub(/^render\s+/, "") + # component = eval(component_code) + # component.call + end + def eye_icon svg( xmlns: "http://www.w3.org/2000/svg", diff --git a/app/components/ruby_ui/codeblock/codeblock.rb b/app/components/ruby_ui/codeblock/codeblock.rb index 2e74f152..51f1900f 100644 --- a/app/components/ruby_ui/codeblock/codeblock.rb +++ b/app/components/ruby_ui/codeblock/codeblock.rb @@ -1,12 +1,7 @@ # frozen_string_literal: true -require "rouge" - module RubyUI class Codeblock < Base - FORMATTER = ::Rouge::Formatters::HTML.new - ROUGE_CSS = Rouge::Themes::Github.mode(:dark).render(scope: ".highlight") # See themes here: https://rouge-ruby.github.io/docs/Rouge/CSSTheme.html - def initialize(code, syntax:, clipboard: true, clipboard_success: "Copied!", clipboard_error: "Copy failed!", **attrs) @code = code @syntax = syntax.to_sym @@ -22,7 +17,6 @@ def initialize(code, syntax:, clipboard: true, clipboard_success: "Copied!", cli end def view_template - style { ROUGE_CSS } # For faster load times, move this to the head of your document. (Also move ROUGE_CSS value to head of document) if @clipboard with_clipboard else @@ -35,7 +29,7 @@ def view_template def default_attrs { style: {tab_size: 2}, - class: "highlight text-sm max-h-[350px] after:content-none flex font-mono overflow-auto overflow-x rounded-md border !bg-stone-900 [&_pre]:p-4" + class: "max-h-[350px] font-mono overflow-auto rounded-md border" } end @@ -54,16 +48,30 @@ def with_clipboard def codeblock div(**attrs) do - div(class: "after:content-none") do - pre { raw(safe(FORMATTER.format(lexer.lex(@code)))) } + div( + class: "relative", + data: { + controller: "shiki-highlighter", + shiki_highlighter_language_value: @syntax.to_s + } + ) do + # Hidden code content for Shiki to process + pre( + class: "hidden", + data: {shiki_highlighter_target: "code"} + ) do + plain @code + end + + # Output container for Shiki-generated HTML + div( + class: "overflow-auto", + data: {shiki_highlighter_target: "output"} + ) end end end - def lexer - Rouge::Lexer.find(@syntax) - end - def clipboard_icon svg( xmlns: "http://www.w3.org/2000/svg", diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 6ca7a4c0..8d6886d4 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -1,9 +1,21 @@ # frozen_string_literal: true class PagesController < ApplicationController - layout -> { Views::Layouts::PagesLayout } + layout false def home render Views::Pages::Home.new end + + def blocks + render Views::Pages::Blocks.new + end + + def render_block + self.class.layout -> { Views::Layouts::ExamplesLayout } + block_class_name = params[:id] + attributes = params[:attributes]&.permit!&.to_h&.symbolize_keys || {} + block_class = block_class_name.constantize + render block_class.new(**attributes) + end end diff --git a/app/javascript/controllers/block_code_viewer_controller.js b/app/javascript/controllers/block_code_viewer_controller.js new file mode 100644 index 00000000..0a3329c9 --- /dev/null +++ b/app/javascript/controllers/block_code_viewer_controller.js @@ -0,0 +1,32 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["fileButton", "fileHeader", "fileContent"] + + selectFile(event) { + const selectedPath = event.currentTarget.dataset.filePath + + // Update button states + this.fileButtonTargets.forEach(button => { + const isSelected = button.dataset.filePath === selectedPath + button.dataset.active = isSelected.toString() + button.classList.toggle("bg-accent", isSelected) + button.classList.toggle("text-accent-foreground", isSelected) + button.classList.toggle("text-muted-foreground", !isSelected) + }) + + // Update file headers + this.fileHeaderTargets.forEach(header => { + const isSelected = header.dataset.filePath === selectedPath + header.classList.toggle("hidden", !isSelected) + }) + + // Update file contents + this.fileContentTargets.forEach(content => { + const isSelected = content.dataset.filePath === selectedPath + content.classList.toggle("hidden", !isSelected) + }) + } +} + + diff --git a/app/javascript/controllers/custom_tabs_controller.js b/app/javascript/controllers/custom_tabs_controller.js new file mode 100644 index 00000000..770ab68c --- /dev/null +++ b/app/javascript/controllers/custom_tabs_controller.js @@ -0,0 +1,9 @@ +import { Controller } from "@hotwired/stimulus"; + +// Connects to data-controller="custom-tabs" +export default class extends Controller { + setTab(event) { + this.element.dataset.tab = event.detail.value; + } +} + diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index 9047cd23..34321fd1 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -87,3 +87,12 @@ application.register("iframe-theme", IframeThemeController) import SidebarMenuController from "./sidebar_menu_controller" application.register("sidebar-menu", SidebarMenuController) + +import CustomTabsController from "./custom_tabs_controller" +application.register("custom-tabs", CustomTabsController) + +import BlockCodeViewerController from "./block_code_viewer_controller" +application.register("block-code-viewer", BlockCodeViewerController) + +import ShikiHighlighterController from "./shiki_highlighter_controller" +application.register("shiki-highlighter", ShikiHighlighterController) diff --git a/app/javascript/controllers/ruby_ui/collapsible_controller.js b/app/javascript/controllers/ruby_ui/collapsible_controller.js index cb367da3..9d819fe9 100644 --- a/app/javascript/controllers/ruby_ui/collapsible_controller.js +++ b/app/javascript/controllers/ruby_ui/collapsible_controller.js @@ -33,6 +33,7 @@ export default class extends Controller { open() { if (this.hasContentTarget) { this.contentTarget.classList.remove('hidden') + this.element.dataset.state = 'open' this.openValue = true } } @@ -41,6 +42,7 @@ export default class extends Controller { close() { if (this.hasContentTarget) { this.contentTarget.classList.add('hidden') + this.element.dataset.state = 'closed' this.openValue = false } } diff --git a/app/javascript/controllers/ruby_ui/tabs_controller.js b/app/javascript/controllers/ruby_ui/tabs_controller.js index e46d69c4..cf5743bd 100644 --- a/app/javascript/controllers/ruby_ui/tabs_controller.js +++ b/app/javascript/controllers/ruby_ui/tabs_controller.js @@ -29,6 +29,14 @@ export default class extends Controller { this.activeContentTarget() && this.activeContentTarget().classList.remove("hidden"); this.activeTriggerTarget().dataset.state = "active"; + + // Dispatch custom event for external listeners + this.element.dispatchEvent( + new CustomEvent("tab-change", { + detail: { value: currentValue }, + bubbles: true + }) + ); } activeTriggerTarget() { diff --git a/app/javascript/controllers/shiki_highlighter_controller.js b/app/javascript/controllers/shiki_highlighter_controller.js new file mode 100644 index 00000000..c62e83fa --- /dev/null +++ b/app/javascript/controllers/shiki_highlighter_controller.js @@ -0,0 +1,51 @@ +import { Controller } from "@hotwired/stimulus" +import { codeToHtml } from "shiki" + +export default class extends Controller { + static targets = ["code", "output"] + static values = { + language: { type: String, default: "ruby" } + } + + async connect() { + await this.highlightCode() + } + + async highlightCode() { + if (!this.hasCodeTarget || !this.hasOutputTarget) return + + const code = this.codeTarget.textContent + const lang = this.languageValue + + const isDark = document.documentElement.classList.contains('dark') + const theme = isDark ? 'github-dark' : 'github-light' + + try { + const html = await codeToHtml(code, { + lang: lang, + theme: theme, + transformers: [ + { + pre(node) { + node.properties["class"] = "no-scrollbar min-w-0 overflow-x-auto px-4 py-3.5 outline-none has-[[data-highlighted-line]]:px-0 has-[[data-line-numbers]]:px-0 has-[[data-slot=tabs]]:p-0 !bg-transparent" + node.properties["data-line-numbers"] = "" + }, + code(node) { + node.properties["data-line-numbers"] = "" + }, + line(node, line) { + node.properties["data-line"] = line + this.addClassToHast(node, "line") + } + } + ] + }) + + this.outputTarget.innerHTML = html + } catch (error) { + console.error('Shiki highlighting error:', error) + this.outputTarget.textContent = code + } + } +} + diff --git a/app/views/docs/button.rb b/app/views/docs/button.rb index ee943b12..cc59bb42 100644 --- a/app/views/docs/button.rb +++ b/app/views/docs/button.rb @@ -45,11 +45,9 @@ def view_template RUBY end - render Docs::VisualCodeExample.new(title: "Link", context: self) do - <<~RUBY - Button(variant: :link) { "Link" } - RUBY - end + render Docs::VisualCodeExample.new(ruby_code: <<~RUBY, title: "Link", context: self) + Button(variant: :link) { "Link" } + RUBY render Docs::VisualCodeExample.new(title: "Disabled", context: self) do <<~RUBY diff --git a/app/views/docs/sidebar.rb b/app/views/docs/sidebar.rb index 165872fa..cc1e8638 100644 --- a/app/views/docs/sidebar.rb +++ b/app/views/docs/sidebar.rb @@ -29,13 +29,21 @@ def view_template end end - render Docs::VisualCodeExample.new(title: "Example", src: "/docs/sidebar/example", context: self) do - Views::Docs::Sidebar::Example::CODE - end + render Docs::VisualCodeExample.new( + title: "Example", + context: self, + type: :block, + content: Views::Docs::Sidebar::Example, + content_attributes: {sidebar_state: "open"} + ) - render Docs::VisualCodeExample.new(title: "Inset variant", src: "/docs/sidebar/inset", context: self) do - Views::Docs::Sidebar::InsetExample::CODE - end + render Docs::VisualCodeExample.new( + title: "Inset variant", + context: self, + type: :block, + content: Views::Docs::Sidebar::InsetExample, + content_attributes: {sidebar_state: "open"} + ) render Docs::VisualCodeExample.new(title: "Dialog variant", context: self) do <<~RUBY diff --git a/app/views/docs/tabs.rb b/app/views/docs/tabs.rb index 33b6ff34..34bc6688 100644 --- a/app/views/docs/tabs.rb +++ b/app/views/docs/tabs.rb @@ -129,6 +129,36 @@ def view_template RUBY end + render Docs::VisualCodeExample.new(title: "Custom Tabs", context: self) do + <<~RUBY + div( + class: "group/custom-tab", + data: { + controller: "custom-tabs", + tab: "first", + action: "tab-change->custom-tabs#setTab" + } + ) do + div(class: "block") do + Tabs(default: "first") do + TabsList do + TabsTrigger(value: "first") { "first" } + TabsTrigger(value: "second") { "second" } + end + end + end + + div(class: "hidden group-data-[tab=second]/custom-tab:hidden md:h-50 lg:flex") do + plain "first1" + end + + div(class: "bg-code text-code-foreground mr-[14px] flex overflow-hidden rounded-xl border group-data-[tab=first]/custom-tab:hidden md:h-50") do + plain "first2" + end + end + RUBY + end + render Components::ComponentSetup::Tabs.new(component_name: component) render Docs::ComponentsTable.new(component_files(component)) diff --git a/app/views/layouts/pages_layout.rb b/app/views/layouts/pages_layout.rb index 98bfc95e..9dfb5653 100644 --- a/app/views/layouts/pages_layout.rb +++ b/app/views/layouts/pages_layout.rb @@ -5,7 +5,12 @@ module Layouts class PagesLayout < Views::Base include Phlex::Rails::Layout - def view_template(&block) + def initialize(page_info = nil, **user_attrs) + @page_info = page_info + super(**user_attrs) + end + + def view_template doctype html do @@ -13,7 +18,7 @@ def view_template(&block) body do render Shared::Navbar.new - main(class: "relative", &block) + main(class: "relative") { yield } render Shared::Flashes.new(notice: flash[:notice], alert: flash[:alert]) end end diff --git a/app/views/pages/base.rb b/app/views/pages/base.rb new file mode 100644 index 00000000..f95a6624 --- /dev/null +++ b/app/views/pages/base.rb @@ -0,0 +1,15 @@ +class Views::Pages::Base < Views::Base + PageInfo = Data.define(:title) + + def around_template + render layout.new(page_info) do + super + end + end + + def page_info + PageInfo.new( + title: page_title + ) + end +end diff --git a/app/views/pages/blocks.rb b/app/views/pages/blocks.rb new file mode 100644 index 00000000..28832de2 --- /dev/null +++ b/app/views/pages/blocks.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +class Views::Pages::Blocks < Views::Pages::Base + def layout = Views::Layouts::PagesLayout + + def page_title = "Building Blocks for the Web" + + def view_template + div(class: "mx-auto w-full max-w-5xl md:max-w-6xl px-4 py-14 md:py-16 space-y-10") do + div(class: "text-center space-y-4") do + Components.Heading(level: 1, as: "h1", class: "text-4xl md:text-6xl font-bold tracking-tight") { "Building Blocks for the Web" } + p(class: "text-muted-foreground text-base md:text-lg") { "Clean, modern building blocks. Copy and paste into your apps." } + p(class: "text-muted-foreground text-base md:text-lg") { "Works with all React frameworks. Open Source. Free forever." } + div(class: "flex flex-col sm:flex-row items-center justify-center gap-3 pt-2") do + Link(variant: :primary, href: blocks_path, class: "px-5") { "Browse Blocks" } + Link(variant: :ghost, href: docs_introduction_path, class: "px-5") { "Add a block" } + end + end + + # render Components::BlockViewer.new + # render ComponentPreview.new(title: "Example", context: self, type: :block, content: Blocks::Sidebar02) + # + render BlockDisplay.new(description: "A dashboard with sidebar, charts and data table", content: Blocks::Sidebar02) + # render Docs::VisualCodeExample.new( + # title: "Example", + # context: self, + # type: :block, + # content: Blocks::Sidebar02, + # ) + end + end + + def search_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-search" + ) do |s| + s.circle(cx: "11", cy: "11", r: "8") + s.path(d: "M21 21L16.7 16.7") + end + end + + def home_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-house" + ) do |s| + s.path(d: "M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8") + s.path(d: "M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z") + end + end + + def inbox_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-inbox" + ) do |s| + s.polyline(points: "22 12 16 12 14 15 10 15 8 12 2 12") + s.path(d: "M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z") + end + end + + def external_icon_link + svg( + xmlns: "http://www.w3.org/2000/svg", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-external-link-icon lucide-external-link size-3" + ) do |s| + s.path(d: "M15 3h6v6") + s.path(d: "M10 14 21 3") + s.path(d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6") + end + end + + def info_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 24 24", + fill: "currentColor", + class: "w-5 h-5" + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm8.706-1.442c1.146-.573 2.437.463 2.126 1.706l-.709 2.836.042-.02a.75.75 0 01.67 1.34l-.04.022c-1.147.573-2.438-.463-2.127-1.706l.71-2.836-.042.02a.75.75 0 11-.671-1.34l.041-.022zM12 9a.75.75 0 100-1.5.75.75 0 000 1.5z", + clip_rule: "evenodd" + ) + end + end +end diff --git a/app/views/pages/home.rb b/app/views/pages/home.rb index 435b07d8..6dc93885 100644 --- a/app/views/pages/home.rb +++ b/app/views/pages/home.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true -class Views::Pages::Home < Views::Base +class Views::Pages::Home < Views::Pages::Base + def layout = Views::Layouts::PagesLayout + + def page_title = "RubyUI" + def view_template render HomeView::Banner.new do |banner| banner.cta do diff --git a/config/initializers/phlex.rb b/config/initializers/phlex.rb index 13fd15ae..7f30d9a7 100644 --- a/config/initializers/phlex.rb +++ b/config/initializers/phlex.rb @@ -7,6 +7,10 @@ module Components extend Phlex::Kit end +module Blocks + extend Phlex::Kit +end + Rails.autoloaders.main.push_dir( "#{Rails.root}/app/views", namespace: Views ) @@ -14,3 +18,7 @@ module Components Rails.autoloaders.main.push_dir( "#{Rails.root}/app/components", namespace: Components ) + +Rails.autoloaders.main.push_dir( + "#{Rails.root}/app/blocks", namespace: Blocks +) diff --git a/config/routes.rb b/config/routes.rb index a6da0ff2..73208daa 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -51,8 +51,6 @@ get "sheet", to: "docs#sheet", as: :docs_sheet get "shortcut_key", to: "docs#shortcut_key", as: :docs_shortcut_key get "sidebar", to: "docs#sidebar", as: :docs_sidebar - get "sidebar/example", to: "docs/sidebar#example", as: :docs_sidebar_example - get "sidebar/inset", to: "docs/sidebar#inset_example", as: :docs_sidebar_inset get "skeleton", to: "docs#skeleton", as: :docs_skeleton get "switch", to: "docs#switch", as: :docs_switch get "table", to: "docs#table", as: :docs_table @@ -63,6 +61,9 @@ get "typography", to: "docs#typography", as: :docs_typography end + get "blocks", to: "pages#blocks", as: :blocks + get "blocks/:id", to: "pages#render_block", as: :render_block + match "/404", to: "errors#not_found", via: :all match "/500", to: "errors#internal_server_error", via: :all diff --git a/package.json b/package.json index b323ba73..22219a44 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "maska": "3.2.0", "motion": "12.23.16", "mustache": "4.2.0", + "shiki": "^3.14.0", "tailwindcss": "4.1.14", "tippy.js": "6.3.7", "tw-animate-css": "1.3.8" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a147f925..84a8c5e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: mustache: specifier: 4.2.0 version: 4.2.0 + shiki: + specifier: ^3.14.0 + version: 3.14.0 tailwindcss: specifier: 4.1.14 version: 4.1.14 @@ -249,6 +252,27 @@ packages: '@rails/actioncable@8.0.200': resolution: {integrity: sha512-EDqWyxck22BHmv1e+mD8Kl6GmtNkhEPdRfGFT7kvsv1yoXd9iYrqHDVAaR8bKmU/syC5eEZ2I5aWWxtB73ukMw==} + '@shikijs/core@3.14.0': + resolution: {integrity: sha512-qRSeuP5vlYHCNUIrpEBQFO7vSkR7jn7Kv+5X3FO/zBKVDGQbcnlScD3XhkrHi/R8Ltz0kEjvFR9Szp/XMRbFMw==} + + '@shikijs/engine-javascript@3.14.0': + resolution: {integrity: sha512-3v1kAXI2TsWQuwv86cREH/+FK9Pjw3dorVEykzQDhwrZj0lwsHYlfyARaKmn6vr5Gasf8aeVpb8JkzeWspxOLQ==} + + '@shikijs/engine-oniguruma@3.14.0': + resolution: {integrity: sha512-TNcYTYMbJyy+ZjzWtt0bG5y4YyMIWC2nyePz+CFMWqm+HnZZyy9SWMgo8Z6KBJVIZnx8XUXS8U2afO6Y0g1Oug==} + + '@shikijs/langs@3.14.0': + resolution: {integrity: sha512-DIB2EQY7yPX1/ZH7lMcwrK5pl+ZkP/xoSpUzg9YC8R+evRCCiSQ7yyrvEyBsMnfZq4eBzLzBlugMyTAf13+pzg==} + + '@shikijs/themes@3.14.0': + resolution: {integrity: sha512-fAo/OnfWckNmv4uBoUu6dSlkcBc+SA1xzj5oUSaz5z3KqHtEbUypg/9xxgJARtM6+7RVm0Q6Xnty41xA1ma1IA==} + + '@shikijs/types@3.14.0': + resolution: {integrity: sha512-bQGgC6vrY8U/9ObG1Z/vTro+uclbjjD/uG58RvfxKZVD5p9Yc1ka3tVyEFy7BNJLzxuWyHH5NWynP9zZZS59eQ==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@tailwindcss/forms@0.5.10': resolution: {integrity: sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==} peerDependencies: @@ -259,6 +283,18 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + autoprefixer@10.4.21: resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} engines: {node: ^10 || ^12 || >=14} @@ -274,6 +310,15 @@ packages: caniuse-lite@1.0.30001713: resolution: {integrity: sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + chart.js@4.5.1: resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} engines: {pnpm: '>=8'} @@ -285,11 +330,21 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + electron-to-chromium@1.5.136: resolution: {integrity: sha512-kL4+wUTD7RSA5FHx5YwWtjDnEEkIIikFgWHR4P6fqjw1PPLlqYkxeOb++wAauAssat0YClCy8Y3C5SxgSkjibQ==} @@ -326,6 +381,15 @@ packages: resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==} engines: {node: '>=10'} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + lodash.castarray@4.4.0: resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} @@ -338,6 +402,24 @@ packages: maska@3.2.0: resolution: {integrity: sha512-zSmSgs5/q9vMSmrdZT3rKOv9uLznNWR/niuuAdBZDTvB3SMKOX9vhMtDijFyExz+B4UClu2rvksylUh/ea1bLA==} + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + mini-svg-data-uri@1.4.4: resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} hasBin: true @@ -378,6 +460,12 @@ packages: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} + oniguruma-parser@0.12.1: + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} + + oniguruma-to-es@4.3.3: + resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -392,22 +480,61 @@ packages: resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.0.1: + resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} + + shiki@3.14.0: + resolution: {integrity: sha512-J0yvpLI7LSig3Z3acIuDLouV5UCKQqu8qOArwMx+/yPVC3WRMgrP67beaG8F+j4xfEWE0eVC4GeBCIXeOPra1g==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + tailwindcss@4.1.14: resolution: {integrity: sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==} tippy.js@6.3.7: resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} tw-animate-css@1.3.8: resolution: {integrity: sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw==} + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + update-browserslist-db@1.1.3: resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true @@ -417,6 +544,15 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@esbuild/aix-ppc64@0.25.11': @@ -523,6 +659,39 @@ snapshots: '@rails/actioncable@8.0.200': {} + '@shikijs/core@3.14.0': + dependencies: + '@shikijs/types': 3.14.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@3.14.0': + dependencies: + '@shikijs/types': 3.14.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.3 + + '@shikijs/engine-oniguruma@3.14.0': + dependencies: + '@shikijs/types': 3.14.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.14.0': + dependencies: + '@shikijs/types': 3.14.0 + + '@shikijs/themes@3.14.0': + dependencies: + '@shikijs/types': 3.14.0 + + '@shikijs/types@3.14.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + '@tailwindcss/forms@0.5.10(tailwindcss@4.1.14)': dependencies: mini-svg-data-uri: 1.4.4 @@ -536,6 +705,18 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 4.1.14 + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/unist@3.0.3': {} + + '@ungap/structured-clone@1.3.0': {} + autoprefixer@10.4.21(postcss@8.5.3): dependencies: browserslist: 4.24.4 @@ -555,6 +736,12 @@ snapshots: caniuse-lite@1.0.30001713: {} + ccount@2.0.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + chart.js@4.5.1: dependencies: '@kurkle/color': 0.3.4 @@ -565,8 +752,16 @@ snapshots: clsx@2.1.1: {} + comma-separated-tokens@2.0.3: {} + cssesc@3.0.0: {} + dequal@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + electron-to-chromium@1.5.136: {} embla-carousel@8.6.0: {} @@ -612,6 +807,26 @@ snapshots: fuse.js@7.1.0: {} + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + html-void-elements@3.0.0: {} + lodash.castarray@4.4.0: {} lodash.isplainobject@4.0.6: {} @@ -620,6 +835,35 @@ snapshots: maska@3.2.0: {} + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + mini-svg-data-uri@1.4.4: {} motion-dom@12.23.12: @@ -641,6 +885,14 @@ snapshots: normalize-range@0.1.2: {} + oniguruma-parser@0.12.1: {} + + oniguruma-to-es@4.3.3: + dependencies: + oniguruma-parser: 0.12.1 + regex: 6.0.1 + regex-recursion: 6.0.2 + picocolors@1.1.1: {} postcss-selector-parser@6.0.10: @@ -656,18 +908,73 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + property-information@7.1.0: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.0.1: + dependencies: + regex-utilities: 2.3.0 + + shiki@3.14.0: + dependencies: + '@shikijs/core': 3.14.0 + '@shikijs/engine-javascript': 3.14.0 + '@shikijs/engine-oniguruma': 3.14.0 + '@shikijs/langs': 3.14.0 + '@shikijs/themes': 3.14.0 + '@shikijs/types': 3.14.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + source-map-js@1.2.1: {} + space-separated-tokens@2.0.2: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + tailwindcss@4.1.14: {} tippy.js@6.3.7: dependencies: '@popperjs/core': 2.11.8 + trim-lines@3.0.1: {} + tslib@2.8.1: {} tw-animate-css@1.3.8: {} + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + update-browserslist-db@1.1.3(browserslist@4.24.4): dependencies: browserslist: 4.24.4 @@ -675,3 +982,15 @@ snapshots: picocolors: 1.1.1 util-deprecate@1.0.2: {} + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + zwitch@2.0.4: {}