From fe6496aa6fe935e160a645f53be073206c8cac05 Mon Sep 17 00:00:00 2001 From: Lucas Sousa Date: Tue, 20 May 2025 17:59:34 -0300 Subject: [PATCH 1/6] Update ruby_ui --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index a4e1114..5ed6974 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -14,7 +14,7 @@ GIT GIT remote: https://github.com/ruby-ui/ruby_ui.git - revision: 1a21bdb6fd38d2bd08e684a82b71e1637a727597 + revision: 95dc9a28ba5c4d460fa2845cba308d518bb87431 branch: main specs: ruby_ui (1.0.1) From 937a03ebad599fdc049db477b3b10fa6e1727064 Mon Sep 17 00:00:00 2001 From: Lucas Sousa Date: Tue, 20 May 2025 17:59:40 -0300 Subject: [PATCH 2/6] Install the sidebar component --- .../ruby_ui/sidebar/collapsible_sidebar.rb | 99 +++++++++++++++++++ .../ruby_ui/sidebar/mobile_sidebar.rb | 45 +++++++++ .../sidebar/non_collapsible_sidebar.rb | 17 ++++ app/components/ruby_ui/sidebar/sidebar.rb | 29 ++++++ .../ruby_ui/sidebar/sidebar_content.rb | 20 ++++ .../ruby_ui/sidebar/sidebar_footer.rb | 20 ++++ .../ruby_ui/sidebar/sidebar_group.rb | 20 ++++ .../ruby_ui/sidebar/sidebar_group_action.rb | 33 +++++++ .../ruby_ui/sidebar/sidebar_group_content.rb | 20 ++++ .../ruby_ui/sidebar/sidebar_group_label.rb | 26 +++++ .../ruby_ui/sidebar/sidebar_header.rb | 20 ++++ .../ruby_ui/sidebar/sidebar_input.rb | 20 ++++ .../ruby_ui/sidebar/sidebar_inset.rb | 23 +++++ .../ruby_ui/sidebar/sidebar_menu.rb | 20 ++++ .../ruby_ui/sidebar/sidebar_menu_action.rb | 48 +++++++++ .../ruby_ui/sidebar/sidebar_menu_badge.rb | 30 ++++++ .../ruby_ui/sidebar/sidebar_menu_button.rb | 63 ++++++++++++ .../ruby_ui/sidebar/sidebar_menu_item.rb | 20 ++++ .../ruby_ui/sidebar/sidebar_menu_skeleton.rb | 36 +++++++ .../ruby_ui/sidebar/sidebar_menu_sub.rb | 24 +++++ .../sidebar/sidebar_menu_sub_button.rb | 50 ++++++++++ .../ruby_ui/sidebar/sidebar_menu_sub_item.rb | 9 ++ .../ruby_ui/sidebar/sidebar_rail.rb | 36 +++++++ .../ruby_ui/sidebar/sidebar_separator.rb | 20 ++++ .../ruby_ui/sidebar/sidebar_trigger.rb | 42 ++++++++ .../ruby_ui/sidebar/sidebar_wrapper.rb | 24 +++++ app/javascript/controllers/index.js | 3 + .../controllers/ruby_ui/sidebar_controller.js | 67 +++++++++++++ 28 files changed, 884 insertions(+) create mode 100644 app/components/ruby_ui/sidebar/collapsible_sidebar.rb create mode 100644 app/components/ruby_ui/sidebar/mobile_sidebar.rb create mode 100644 app/components/ruby_ui/sidebar/non_collapsible_sidebar.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar_content.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar_footer.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar_group.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar_group_action.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar_group_content.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar_group_label.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar_header.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar_input.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar_inset.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar_menu.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar_menu_action.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar_menu_badge.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar_menu_button.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar_menu_item.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar_menu_skeleton.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar_menu_sub.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar_menu_sub_button.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar_menu_sub_item.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar_rail.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar_separator.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar_trigger.rb create mode 100644 app/components/ruby_ui/sidebar/sidebar_wrapper.rb create mode 100644 app/javascript/controllers/ruby_ui/sidebar_controller.js diff --git a/app/components/ruby_ui/sidebar/collapsible_sidebar.rb b/app/components/ruby_ui/sidebar/collapsible_sidebar.rb new file mode 100644 index 0000000..12b7172 --- /dev/null +++ b/app/components/ruby_ui/sidebar/collapsible_sidebar.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +module RubyUI + class CollapsibleSidebar < Base + def initialize(side: :left, variant: :sidebar, collapsible: :offcanvas, open: true, **attrs) + @side = side + @variant = variant + @collapsible = collapsible + @open = open + super(**attrs) + end + + def view_template(&) + MobileSidebar(side: @side, **attrs, &) + div(**mix(sidebar_attrs, attrs)) do + div(**gap_element_attrs) + div(**content_wrapper_attrs) do + div(**content_attrs, &) + end + end + end + + private + + def sidebar_attrs + { + class: "group peer hidden text-sidebar-foreground md:block", + data: { + state: @open ? "expanded" : "collapsed", + collapsible: @open ? "" : @collapsible, + variant: @variant, + side: @side, + collapsible_kind: @collapsible, + ruby_ui__sidebar_target: "sidebar" + } + } + end + + def gap_element_attrs + { + class: [ + "relative w-[var(--sidebar-width)] bg-transparent transition-[width]", + "duration-200 ease-linear", + "group-data-[collapsible=offcanvas]:w-0", + "group-data-[side=right]:rotate-180", + variant_classes + ] + } + end + + def content_wrapper_attrs + { + class: [ + "fixed inset-y-0 z-10 hidden h-svh w-[var(--sidebar-width)]", + "transition-[left,right,width] duration-200 ease-linear md:flex", + content_wrapper_side_classes, + content_wrapper_variant_classes + ] + } + end + + def content_attrs + { + class: [ + "flex h-full w-full flex-col bg-sidebar", + "group-data-[variant=floating]:rounded-lg", + "group-data-[variant=floating]:border", + "group-data-[variant=floating]:border-sidebar-border", + "group-data-[variant=floating]:shadow" + ], + data: { + sidebar: "sidebar" + } + } + end + + def variant_classes + if %i[floating inset].include?(@variant) + "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]" + else + "group-data-[collapsible=icon]:w-[var(--sidebar-width-icon)]" + end + end + + def content_wrapper_side_classes + return "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]" if @side == :left + + "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]" + end + + def content_wrapper_variant_classes + if %i[floating inset].include?(@variant) + "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]" + else + "group-data-[collapsible=icon]:w-[var(--sidebar-width-icon)] group-data-[side=left]:border-r group-data-[side=right]:border-l" + end + end + end +end diff --git a/app/components/ruby_ui/sidebar/mobile_sidebar.rb b/app/components/ruby_ui/sidebar/mobile_sidebar.rb new file mode 100644 index 0000000..3ecc697 --- /dev/null +++ b/app/components/ruby_ui/sidebar/mobile_sidebar.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module RubyUI + class MobileSidebar < Base + SIDEBAR_WIDTH_MOBILE = "18rem" + + def initialize(side: :left, **attrs) + @side = side + super(**attrs) + end + + def view_template(&) + Sheet(**attrs) do + SheetContent( + side: @side, + class: "w-[var(--sidebar-width)] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden", + style: { + "--sidebar-width": SIDEBAR_WIDTH_MOBILE + }, + data: { + sidebar: "sidebar", + mobile: "true" + } + ) do + SheetHeader(class: "sr-only") do + SheetTitle { "Sidebar" } + SheetDescription { "Displays the mobile sidebar." } + end + div(class: "flex h-full w-full flex-col", &) + end + end + end + + private + + def default_attrs + { + data: { + ruby_ui__sidebar_target: "mobileSidebar", + action: "ruby--ui-sidebar:open->ruby-ui--sheet#open:self" + } + } + end + end +end diff --git a/app/components/ruby_ui/sidebar/non_collapsible_sidebar.rb b/app/components/ruby_ui/sidebar/non_collapsible_sidebar.rb new file mode 100644 index 0000000..b68a12b --- /dev/null +++ b/app/components/ruby_ui/sidebar/non_collapsible_sidebar.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RubyUI + class NonCollapsibleSidebar < Base + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + class: "flex h-full w-[var(--sidebar-width)] flex-col bg-sidebar text-sidebar-foreground" + } + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar.rb b/app/components/ruby_ui/sidebar/sidebar.rb new file mode 100644 index 0000000..be5423e --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module RubyUI + class Sidebar < Base + SIDES = %i[left right].freeze + VARIANTS = %i[sidebar floating inset].freeze + COLLAPSIBLES = %i[offcanvas icon none].freeze + + def initialize(side: :left, variant: :sidebar, collapsible: :offcanvas, open: true, **attrs) + raise ArgumentError, "Invalid side: #{side}." unless SIDES.include?(side.to_sym) + raise ArgumentError "Invalid variant: #{variant}." unless VARIANTS.include?(variant.to_sym) + raise ArgumentError, "Invalid collapsible: #{collapsible}." unless COLLAPSIBLES.include?(collapsible.to_sym) + + @side = side.to_sym + @variant = variant.to_sym + @collapsible = collapsible.to_sym + @open = open + super(**attrs) + end + + def view_template(&) + if @collapsible == :none + NonCollapsibleSidebar(**attrs, &) + else + CollapsibleSidebar(side: @side, variant: @variant, collapsible: @collapsible, open: @open, **attrs, &) + end + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar_content.rb b/app/components/ruby_ui/sidebar/sidebar_content.rb new file mode 100644 index 0000000..0874ce7 --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar_content.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module RubyUI + class SidebarContent < Base + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + class: "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden", + data: { + sidebar: "content" + } + } + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar_footer.rb b/app/components/ruby_ui/sidebar/sidebar_footer.rb new file mode 100644 index 0000000..3df63e2 --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar_footer.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module RubyUI + class SidebarFooter < Base + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + class: "flex flex-col gap-2 p-2", + data: { + sidebar: "footer" + } + } + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar_group.rb b/app/components/ruby_ui/sidebar/sidebar_group.rb new file mode 100644 index 0000000..54e71ec --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar_group.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module RubyUI + class SidebarGroup < Base + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + class: "relative flex w-full min-w-0 flex-col p-2", + data: { + sidebar: "group" + } + } + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar_group_action.rb b/app/components/ruby_ui/sidebar/sidebar_group_action.rb new file mode 100644 index 0000000..b6833d5 --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar_group_action.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module RubyUI + class SidebarGroupAction < Base + def initialize(as: :button, **attrs) + @as = as + super(**attrs) + end + + def view_template(&) + tag(@as, **attrs, &) + end + + private + + def default_attrs + { + class: [ + "absolute right-3 top-3.5 flex aspect-square w-5 items-center", + "justify-center rounded-md p-0 text-sidebar-foreground", + "outline-none ring-sidebar-ring transition-transform", + "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", + "focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", + "after:absolute after:-inset-2 after:md:hidden", + "group-data-[collapsible=icon]:hidden" + ], + data: { + sidebar: "group-action" + } + } + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar_group_content.rb b/app/components/ruby_ui/sidebar/sidebar_group_content.rb new file mode 100644 index 0000000..d4c8de7 --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar_group_content.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module RubyUI + class SidebarGroupContent < Base + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + class: "w-full text-sm", + data: { + sidebar: "group-content" + } + } + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar_group_label.rb b/app/components/ruby_ui/sidebar/sidebar_group_label.rb new file mode 100644 index 0000000..77ab53a --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar_group_label.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module RubyUI + class SidebarGroupLabel < Base + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + class: [ + "flex h-8 shrink-0 items-center rounded-md px-2 text-xs", + "font-medium text-sidebar-foreground/70 outline-none", + "ring-sidebar-ring transition-[margin,opacity] duration-200", + "ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", + "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0" + ], + data: { + sidebar: "group-label" + } + } + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar_header.rb b/app/components/ruby_ui/sidebar/sidebar_header.rb new file mode 100644 index 0000000..ca2d160 --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar_header.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module RubyUI + class SidebarHeader < Base + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + class: "flex flex-col gap-2 p-2", + data: { + sidebar: "header" + } + } + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar_input.rb b/app/components/ruby_ui/sidebar/sidebar_input.rb new file mode 100644 index 0000000..89bfaed --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar_input.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module RubyUI + class SidebarInput < Base + def view_template(&) + Input(**attrs, &) + end + + private + + def default_attrs + { + class: "h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring", + data: { + sidebar: "input" + } + } + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar_inset.rb b/app/components/ruby_ui/sidebar/sidebar_inset.rb new file mode 100644 index 0000000..e03d3fc --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar_inset.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module RubyUI + class SidebarInset < Base + def view_template(&) + main(**attrs, &) + end + + private + + def default_attrs + { + class: [ + "relative flex w-full flex-1 flex-col bg-background", + "md:peer-data-[variant=inset]:m-2", + "md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2", + "md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl", + "md:peer-data-[variant=inset]:shadow" + ] + } + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar_menu.rb b/app/components/ruby_ui/sidebar/sidebar_menu.rb new file mode 100644 index 0000000..959db3e --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar_menu.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module RubyUI + class SidebarMenu < Base + def view_template(&) + ul(**attrs, &) + end + + private + + def default_attrs + { + class: "flex w-full min-w-0 flex-col gap-1", + data: { + sidebar: "menu" + } + } + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar_menu_action.rb b/app/components/ruby_ui/sidebar/sidebar_menu_action.rb new file mode 100644 index 0000000..21c48d0 --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar_menu_action.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module RubyUI + class SidebarMenuAction < Base + def initialize(as: :button, show_on_hover: false, **attrs) + @as = as + super(**attrs) + end + + def view_template(&) + tag(@as, **attrs, &) + end + + private + + def default_attrs + { + class: [ + "absolute right-1 top-1.5 flex aspect-square w-5 items-center", + "justify-center rounded-md p-0 text-sidebar-foreground outline-none", + "ring-sidebar-ring transition-transform hover:bg-sidebar-accent", + "hover:text-sidebar-accent-foreground focus-visible:ring-2", + "peer-hover/menu-button:text-sidebar-accent-foreground", + "[&>svg]:size-4 [&>svg]:shrink-0", + "after:absolute after:-inset-2 after:md:hidden", + "peer-data-[size=sm]/menu-button:top-1", + "peer-data-[size=default]/menu-button:top-1.5", + "peer-data-[size=lg]/menu-button:top-2.5", + "group-data-[collapsible=icon]:hidden", + show_on_hover_classes + ], + data: { + sidebar: "menu-action" + } + } + end + + def show_on_hover_classes + return unless @show_on_hover + + [ + "group-focus-within/menu-item:opacity-100", + "group-hover/menu-item:opacity-100 data-[state=open]:opacity-100", + "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0" + ].join(" ") + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar_menu_badge.rb b/app/components/ruby_ui/sidebar/sidebar_menu_badge.rb new file mode 100644 index 0000000..101b0bb --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar_menu_badge.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module RubyUI + class SidebarMenuBadge < Base + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + class: [ + "pointer-events-none absolute right-1 flex h-5 min-w-5 select-none", + "items-center justify-center rounded-md px-1 text-xs font-medium", + "tabular-nums text-sidebar-foreground", + "peer-hover/menu-button:text-sidebar-accent-foreground", + "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground", + "peer-data-[size=sm]/menu-button:top-1", + "peer-data-[size=default]/menu-button:top-1.5", + "peer-data-[size=lg]/menu-button:top-2.5", + "group-data-[collapsible=icon]:hidden" + ], + data: { + sidebar: "menu-badge" + } + } + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar_menu_button.rb b/app/components/ruby_ui/sidebar/sidebar_menu_button.rb new file mode 100644 index 0000000..5480c27 --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar_menu_button.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module RubyUI + class SidebarMenuButton < Base + VARIANT_CLASSES = { + default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", + outline: + "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]" + }.freeze + + SIZE_CLASSES = { + default: "h-8 text-sm", + sm: "h-7 text-xs", + lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0" + }.freeze + + def initialize(as: :button, variant: :default, size: :default, active: false, **attrs) + raise ArgumentError, "Invalid variant: #{variant}" unless VARIANT_CLASSES.key?(variant) + raise ArgumentError, "Invalid size: #{size}" unless SIZE_CLASSES.key?(size) + + @as = as + @variant = variant + @size = size + @active = active + super(**attrs) + end + + def view_template(&) + tag(@as, **attrs, &) + end + + private + + def default_attrs + { + class: [ + "peer/menu-button flex w-full items-center gap-2 overflow-hidden", + "rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring", + "transition-[width,height,padding] hover:bg-sidebar-accent", + "hover:text-sidebar-accent-foreground focus-visible:ring-2", + "active:bg-sidebar-accent active:text-sidebar-accent-foreground", + "disabled:pointer-events-none disabled:opacity-50", + "group-has-[[data-sidebar=menu-action]]/menu-item:pr-8", + "aria-disabled:pointer-events-none aria-disabled:opacity-50", + "data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium", + "data-[active=true]:text-sidebar-accent-foreground", + "data-[state=open]:hover:bg-sidebar-accent", + "data-[state=open]:hover:text-sidebar-accent-foreground", + "group-data-[collapsible=icon]:!size-8", + "group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate", + "[&>svg]:size-4 [&>svg]:shrink-0", + VARIANT_CLASSES[@variant], + SIZE_CLASSES[@size] + ], + data: { + sidebar: "menu-button", + size: @size, + active: @active.to_s + } + } + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar_menu_item.rb b/app/components/ruby_ui/sidebar/sidebar_menu_item.rb new file mode 100644 index 0000000..4412c73 --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar_menu_item.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module RubyUI + class SidebarMenuItem < Base + def view_template(&) + ul(**attrs, &) + end + + private + + def default_attrs + { + class: "group/menu-item relative", + data: { + sidebar: "menu-item" + } + } + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar_menu_skeleton.rb b/app/components/ruby_ui/sidebar/sidebar_menu_skeleton.rb new file mode 100644 index 0000000..acc4285 --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar_menu_skeleton.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module RubyUI + class SidebarMenuSkeleton < Base + def initialize(show_icon: false, **attrs) + @show_icon = show_icon + super(**attrs) + end + + def view_template(&) + div(**attrs) do + Skeleton(class: "size-4 rounded-md", data: {sidebar: "menu-skeleton-icon"}) if @show_icon + Skeleton( + class: "h-4 max-w-[var(--skeleton-width)] flex-1", + data: {sidebar: "menu-skeleton-text"}, + style: {"--skeleton-width" => "#{skeleton_width}%"} + ) + end + end + + private + + def default_attrs + { + class: "flex h-8 items-center gap-2 rounded-md px-2", + data: { + sidebar: "menu-skeleton" + } + } + end + + def skeleton_width + @_skeleton_width ||= rand(50..89) + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar_menu_sub.rb b/app/components/ruby_ui/sidebar/sidebar_menu_sub.rb new file mode 100644 index 0000000..573ae85 --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar_menu_sub.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module RubyUI + class SidebarMenuSub < Base + def view_template(&) + ul(**attrs, &) + end + + private + + def default_attrs + { + class: [ + "mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l", + "border-sidebar-border px-2.5 py-0.5", + "group-data-[collapsible=icon]:hidden" + ], + data: { + sidebar: "menu-sub" + } + } + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar_menu_sub_button.rb b/app/components/ruby_ui/sidebar/sidebar_menu_sub_button.rb new file mode 100644 index 0000000..1c4abef --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar_menu_sub_button.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module RubyUI + class SidebarMenuSubButton < Base + SIZE_CLASSES = { + sm: "text-xs", + md: "text-sm" + }.freeze + + def initialize(as: :button, size: :md, active: false, **attrs) + raise ArgumentError, "Invalid size: #{size}" unless SIZE_CLASSES.key?(size) + + @as = as + @size = size + @active = active + super(**attrs) + end + + def view_template(&) + tag(@as, **attrs, &) + end + + private + + def default_attrs + { + class: [ + "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden", + "rounded-md px-2 text-sidebar-foreground outline-none", + "ring-sidebar-ring hover:bg-sidebar-accent", + "hover:text-sidebar-accent-foreground focus-visible:ring-2", + "active:bg-sidebar-accent active:text-sidebar-accent-foreground", + "disabled:pointer-events-none disabled:opacity-50", + "aria-disabled:pointer-events-none aria-disabled:opacity-50", + "[&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + "[&>svg]:text-sidebar-accent-foreground", + "data-[active=true]:bg-sidebar-accent", + "data-[active=true]:text-sidebar-accent-foreground", + "group-data-[collapsible=icon]:hidden", + SIZE_CLASSES[@size] + ], + data: { + sidebar: "menu-sub-button", + size: @size, + active: @active.to_s + } + } + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar_menu_sub_item.rb b/app/components/ruby_ui/sidebar/sidebar_menu_sub_item.rb new file mode 100644 index 0000000..424ed78 --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar_menu_sub_item.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module RubyUI + class SidebarMenuSubItem < Base + def view_template(&) + li(**attrs, &) + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar_rail.rb b/app/components/ruby_ui/sidebar/sidebar_rail.rb new file mode 100644 index 0000000..c4e64f2 --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar_rail.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module RubyUI + class SidebarRail < Base + def view_template(&) + button(**attrs, &) + end + + private + + def default_attrs + { + class: [ + "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all", + "ease-linear after:absolute after:inset-y-0 after:left-1/2", + "after:w-[2px] hover:after:bg-sidebar-border", + "group-data-[side=left]:-right-4 group-data-[side=right]:left-0", + "sm:flex [[data-side=left]_&]:cursor-w-resize", + "[[data-side=right]_&]:cursor-e-resize", + "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize", + "[[data-side=right][data-state=collapsed]_&]:cursor-w-resize", + "group-data-[collapsible=offcanvas]:translate-x-0", + "group-data-[collapsible=offcanvas]:after:left-full", + "group-data-[collapsible=offcanvas]:hover:bg-sidebar", + "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", + "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2" + ], + data: { + sidebar: "rail", + tabindex: "-1", + action: "click->ruby-ui--sidebar#toggle" + } + } + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar_separator.rb b/app/components/ruby_ui/sidebar/sidebar_separator.rb new file mode 100644 index 0000000..fb00e60 --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar_separator.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module RubyUI + class SidebarSeparator < Base + def view_template(&) + Separator(**attrs, &) + end + + private + + def default_attrs + { + class: "mx-2 w-auto bg-sidebar-border", + data: { + sidebar: "separator" + } + } + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar_trigger.rb b/app/components/ruby_ui/sidebar/sidebar_trigger.rb new file mode 100644 index 0000000..64dfaf7 --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar_trigger.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module RubyUI + class SidebarTrigger < Base + def view_template(&) + Button(variant: :ghost, size: :icon, **attrs) do + panel_left_icon + span(class: "sr-only") { "Toggle Sidebar" } + end + end + + private + + def default_attrs + { + class: "h-7 w-7 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + data: { + sidebar: "trigger", + action: "click->ruby-ui--sidebar#toggle" + } + } + end + + def panel_left_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-panel-left" + ) do |s| + s.rect(width: "18", height: "18", x: "3", y: "3", rx: "2") + s.path(d: "M9 3v18") + end + end + end +end diff --git a/app/components/ruby_ui/sidebar/sidebar_wrapper.rb b/app/components/ruby_ui/sidebar/sidebar_wrapper.rb new file mode 100644 index 0000000..f964860 --- /dev/null +++ b/app/components/ruby_ui/sidebar/sidebar_wrapper.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module RubyUI + class SidebarWrapper < Base + SIDEBAR_WIDTH = "16rem" + SIDEBAR_WIDTH_ICON = "3rem" + + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + class: "group/sidebar-wrapper [&:has([data-variant=inset])]:bg-sidebar flex min-h-svh w-full", + style: "--sidebar-width: #{SIDEBAR_WIDTH}; --sidebar-width-icon: #{SIDEBAR_WIDTH_ICON};", + data: { + controller: "ruby-ui--sidebar" + } + } + end + end +end diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index 3b30743..c5db664 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -70,6 +70,9 @@ application.register("ruby-ui--sheet-content", RubyUi__SheetContentController) import RubyUi__SheetController from "./ruby_ui/sheet_controller" application.register("ruby-ui--sheet", RubyUi__SheetController) +import RubyUi__SidebarController from "./ruby_ui/sidebar_controller" +application.register("ruby-ui--sidebar", RubyUi__SidebarController) + import RubyUi__TabsController from "./ruby_ui/tabs_controller" application.register("ruby-ui--tabs", RubyUi__TabsController) diff --git a/app/javascript/controllers/ruby_ui/sidebar_controller.js b/app/javascript/controllers/ruby_ui/sidebar_controller.js new file mode 100644 index 0000000..c789438 --- /dev/null +++ b/app/javascript/controllers/ruby_ui/sidebar_controller.js @@ -0,0 +1,67 @@ +import { Controller } from "@hotwired/stimulus"; + +const SIDEBAR_COOKIE_NAME = "sidebar_state"; +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +const State = { + EXPANDED: "expanded", + COLLAPSED: "collapsed", +}; +const MOBILE_BREAKPOINT = 768; + +export default class extends Controller { + static targets = ["sidebar", "mobileSidebar"]; + + sidebarTargetConnected() { + const { state, collapsibleKind } = this.sidebarTarget.dataset; + + this.open = state === State.EXPANDED; + this.collapsibleKind = collapsibleKind; + } + + toggle(e) { + e.preventDefault(); + + if (this.#isMobile()) { + this.#openMobileSidebar(); + + return; + } + + this.open = !this.open; + this.onToggle(); + } + + onToggle() { + this.#updateSidebarState(); + this.#persistSidebarState(); + } + + #updateSidebarState() { + if (!this.hasSidebarTarget) { + return; + } + + const { dataset } = this.sidebarTarget; + + dataset.state = this.open ? State.EXPANDED : State.COLLAPSED; + dataset.collapsible = this.open ? "" : this.collapsibleKind; + } + + #persistSidebarState() { + document.cookie = `${SIDEBAR_COOKIE_NAME}=${this.open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; + } + + #isMobile() { + return window.innerWidth < MOBILE_BREAKPOINT; + } + + #openMobileSidebar() { + if (!this.hasMobileSidebarTarget) { + return; + } + + this.mobileSidebarTarget.dispatchEvent( + new CustomEvent("ruby--ui-sidebar:open"), + ); + } +} From 70beb82214dd6e6f856cae5e48cfa93044fd5bae Mon Sep 17 00:00:00 2001 From: Lucas Sousa Date: Tue, 20 May 2025 18:01:12 -0300 Subject: [PATCH 3/6] Update the visual code component to support iframes --- app/components/docs/visual_code_example.rb | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/app/components/docs/visual_code_example.rb b/app/components/docs/visual_code_example.rb index 7d559cf..c934705 100644 --- a/app/components/docs/visual_code_example.rb +++ b/app/components/docs/visual_code_example.rb @@ -13,9 +13,10 @@ def self.reset_collected_code @@collected_code = [] end - def initialize(title: nil, description: nil, context: nil) + def initialize(title: nil, description: nil, src: nil, context: nil) @title = title @description = description + @src = src @context = context end @@ -43,7 +44,7 @@ def view_template(&) def render_header div do if @title - div(class: "flex items-center gap-x-2 mb-1") do + div do Components.Heading(level: 4) { @title.capitalize } end end @@ -71,6 +72,20 @@ def render_tab_contents(&) end def render_preview_tab(&block) + return iframe_preview if @src + + raw_preview + end + + def iframe_preview + div(class: "relative aspect-[4/2.5] w-full overflow-hidden rounded-md border") do + div(class: "absolute inset-0 hidden w-[1600px] bg-background md:block") do + iframe(src: @src, class: "size-full") + end + end + end + + def raw_preview 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) From c3b0a894cdaaab6b6d3488f0eb2bed8ae1421ba4 Mon Sep 17 00:00:00 2001 From: Lucas Sousa Date: Tue, 20 May 2025 18:01:29 -0300 Subject: [PATCH 4/6] Add the sidebar examples --- app/components/shared/menu.rb | 1 + app/controllers/docs/sidebar_controller.rb | 23 ++ app/controllers/docs_controller.rb | 4 + app/views/docs/sidebar.rb | 52 ++++ app/views/docs/sidebar/dialog_example.rb | 198 ++++++++++++++ app/views/docs/sidebar/example.rb | 286 +++++++++++++++++++++ app/views/docs/sidebar/inset_example.rb | 286 +++++++++++++++++++++ app/views/layouts/examples_layout.rb | 22 ++ config/routes.rb | 4 + 9 files changed, 876 insertions(+) create mode 100644 app/controllers/docs/sidebar_controller.rb create mode 100644 app/views/docs/sidebar.rb create mode 100644 app/views/docs/sidebar/dialog_example.rb create mode 100644 app/views/docs/sidebar/example.rb create mode 100644 app/views/docs/sidebar/inset_example.rb create mode 100644 app/views/layouts/examples_layout.rb diff --git a/app/components/shared/menu.rb b/app/components/shared/menu.rb index 3b49fe2..3cb1150 100644 --- a/app/components/shared/menu.rb +++ b/app/components/shared/menu.rb @@ -99,6 +99,7 @@ def components {name: "Separator", path: docs_separator_path, badge: "New"}, {name: "Sheet", path: docs_sheet_path}, {name: "Shortcut Key", path: docs_shortcut_key_path}, + {name: "Sidebar", path: docs_sidebar_path, badge: "New"}, {name: "Skeleton", path: docs_skeleton_path, badge: "New"}, {name: "Switch", path: docs_switch_path}, {name: "Table", path: docs_table_path}, diff --git a/app/controllers/docs/sidebar_controller.rb b/app/controllers/docs/sidebar_controller.rb new file mode 100644 index 0000000..c09c912 --- /dev/null +++ b/app/controllers/docs/sidebar_controller.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class Docs::SidebarController < ApplicationController + layout -> { Views::Layouts::ExamplesLayout } + + def example + sidebar_state = cookies.fetch(:sidebar_state, "true") == "true" + + render Views::Docs::Sidebar::Example.new(sidebar_state:) + end + + def inset_example + sidebar_state = cookies.fetch(:sidebar_state, "true") == "true" + + render Views::Docs::Sidebar::InsetExample.new(sidebar_state:) + end + + def dialog_example + sidebar_state = cookies.fetch(:sidebar_state, "true") == "true" + + render Views::Docs::Sidebar::DialogExample.new(sidebar_state:) + end +end diff --git a/app/controllers/docs_controller.rb b/app/controllers/docs_controller.rb index c670c4f..92382c6 100644 --- a/app/controllers/docs_controller.rb +++ b/app/controllers/docs_controller.rb @@ -178,6 +178,10 @@ def shortcut_key render Views::Docs::ShortcutKey.new end + def sidebar + render Views::Docs::Sidebar.new + end + def skeleton render Views::Docs::Skeleton.new end diff --git a/app/views/docs/sidebar.rb b/app/views/docs/sidebar.rb new file mode 100644 index 0000000..93d34f7 --- /dev/null +++ b/app/views/docs/sidebar.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +class Views::Docs::Sidebar < Views::Base + def view_template + component = "Sidebar" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Sidebar", description: "A composable, themeable and customizable sidebar component.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", src: "/docs/sidebar/example", context: self) do + Views::Docs::Sidebar::Example::CODE + end + + render Docs::VisualCodeExample.new(title: "Inset variant", src: "/docs/sidebar/inset", context: self) do + Views::Docs::Sidebar::InsetExample::CODE + end + + render div do + div do + Components.Heading(level: 4) { "Dialog variant" } + + Tabs(default_value: "preview") do + TabsList do + TabsTrigger(value: "preview") do + span { "Preview" } + end + TabsTrigger(value: "code") do + span { "Code" } + end + end + + TabsContent(value: "preview") do + Link(href: "/docs/sidebar/dialog", target: :_blank, variant: :primary) { "Open in another tab" } + end + + TabsContent(value: "code") do + 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(Views::Docs::Sidebar::DialogExample::CODE, syntax: :ruby, class: "-m-px") + end + end + end + end + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/app/views/docs/sidebar/dialog_example.rb b/app/views/docs/sidebar/dialog_example.rb new file mode 100644 index 0000000..3b2aa21 --- /dev/null +++ b/app/views/docs/sidebar/dialog_example.rb @@ -0,0 +1,198 @@ +# frozen_string_literal: true + +class Views::Docs::Sidebar::DialogExample < Views::Base + def initialize(sidebar_state:) + @sidebar_state = sidebar_state + end + + CODE = <<~RUBY + Dialog(data: {action: "ruby-ui--dialog:connect->ruby-ui--dialog#open"}) do + DialogTrigger do + Button { "Open Dialog" } + end + DialogContent(class: "grid overflow-hidden p-0 md:max-h-[500px] md:max-w-[700px] lg:max-w-[800px]") do + SidebarWrapper(class: "items-start") do + Sidebar(collapsible: :none, class: "hidden md:flex") do + SidebarContent do + SidebarGroup do + SidebarGroupContent 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" } + end + end + end + end + end + end + end + main(class: "flex h-[480px] flex-1 flex-col overflow-hidden") do + end + 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/views/docs/sidebar/example.rb b/app/views/docs/sidebar/example.rb new file mode 100644 index 0000000..10927d2 --- /dev/null +++ b/app/views/docs/sidebar/example.rb @@ -0,0 +1,286 @@ +# frozen_string_literal: true + +class Views::Docs::Sidebar::Example < 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() 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(class: "z-40") 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(class: "z-40") 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/views/docs/sidebar/inset_example.rb b/app/views/docs/sidebar/inset_example.rb new file mode 100644 index 0000000..4f0e91a --- /dev/null +++ b/app/views/docs/sidebar/inset_example.rb @@ -0,0 +1,286 @@ +# frozen_string_literal: true + +class Views::Docs::Sidebar::InsetExample < 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(variant: :inset) 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() 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(class: "z-40") 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(class: "z-40") 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/views/layouts/examples_layout.rb b/app/views/layouts/examples_layout.rb new file mode 100644 index 0000000..e79a2dd --- /dev/null +++ b/app/views/layouts/examples_layout.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Views + module Layouts + class ExamplesLayout < Views::Base + include Phlex::Rails::Layout + + def view_template(&block) + doctype + + html do + render Shared::Head.new + + body do + block.call + render Shared::Flashes.new(notice: flash[:notice], alert: flash[:alert]) + end + end + end + end + end +end diff --git a/config/routes.rb b/config/routes.rb index ecfc518..fea4d64 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -50,6 +50,10 @@ get "separator", to: "docs#separator", as: :docs_separator 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 "sidebar/dialog", to: "docs/sidebar#dialog_example", as: :docs_sidebar_dialog get "skeleton", to: "docs#skeleton", as: :docs_skeleton get "switch", to: "docs#switch", as: :docs_switch get "table", to: "docs#table", as: :docs_table From be3b4425cf14b0c8b7755446113f087555885301 Mon Sep 17 00:00:00 2001 From: Lucas Sousa Date: Tue, 20 May 2025 18:14:12 -0300 Subject: [PATCH 5/6] Use the default visual code example for the dialog --- app/controllers/docs/sidebar_controller.rb | 6 - app/views/docs/sidebar.rb | 113 +++++++++--- app/views/docs/sidebar/dialog_example.rb | 198 --------------------- app/views/docs/sidebar/example.rb | 4 +- app/views/docs/sidebar/inset_example.rb | 4 +- config/routes.rb | 1 - 6 files changed, 96 insertions(+), 230 deletions(-) delete mode 100644 app/views/docs/sidebar/dialog_example.rb diff --git a/app/controllers/docs/sidebar_controller.rb b/app/controllers/docs/sidebar_controller.rb index c09c912..d6ffc46 100644 --- a/app/controllers/docs/sidebar_controller.rb +++ b/app/controllers/docs/sidebar_controller.rb @@ -14,10 +14,4 @@ def inset_example render Views::Docs::Sidebar::InsetExample.new(sidebar_state:) end - - def dialog_example - sidebar_state = cookies.fetch(:sidebar_state, "true") == "true" - - render Views::Docs::Sidebar::DialogExample.new(sidebar_state:) - end end diff --git a/app/views/docs/sidebar.rb b/app/views/docs/sidebar.rb index 93d34f7..d75438d 100644 --- a/app/views/docs/sidebar.rb +++ b/app/views/docs/sidebar.rb @@ -17,31 +17,48 @@ def view_template Views::Docs::Sidebar::InsetExample::CODE end - render div do - div do - Components.Heading(level: 4) { "Dialog variant" } - - Tabs(default_value: "preview") do - TabsList do - TabsTrigger(value: "preview") do - span { "Preview" } - end - TabsTrigger(value: "code") do - span { "Code" } - end - end - - TabsContent(value: "preview") do - Link(href: "/docs/sidebar/dialog", target: :_blank, variant: :primary) { "Open in another tab" } + render Docs::VisualCodeExample.new(title: "Dialog variant", context: self) do + <<~RUBY + Dialog(data: {action: "ruby-ui--dialog:connect->ruby-ui--dialog#open"}) do + DialogTrigger do + Button { "Open Dialog" } end - - TabsContent(value: "code") do - 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(Views::Docs::Sidebar::DialogExample::CODE, syntax: :ruby, class: "-m-px") + DialogContent(class: "grid overflow-hidden p-0 md:max-h-[500px] md:max-w-[700px] lg:max-w-[800px]") do + SidebarWrapper(class: "items-start") do + Sidebar(collapsible: :none, class: "hidden md:flex") do + SidebarContent do + SidebarGroup do + SidebarGroupContent 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" } + end + end + end + end + end + end + end + main(class: "flex h-[480px] flex-1 flex-col overflow-hidden") do + end end end end - end + RUBY end render Components::ComponentSetup::Tabs.new(component_name: component) @@ -49,4 +66,58 @@ def view_template render Docs::ComponentsTable.new(component_files(component)) 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 end diff --git a/app/views/docs/sidebar/dialog_example.rb b/app/views/docs/sidebar/dialog_example.rb deleted file mode 100644 index 3b2aa21..0000000 --- a/app/views/docs/sidebar/dialog_example.rb +++ /dev/null @@ -1,198 +0,0 @@ -# frozen_string_literal: true - -class Views::Docs::Sidebar::DialogExample < Views::Base - def initialize(sidebar_state:) - @sidebar_state = sidebar_state - end - - CODE = <<~RUBY - Dialog(data: {action: "ruby-ui--dialog:connect->ruby-ui--dialog#open"}) do - DialogTrigger do - Button { "Open Dialog" } - end - DialogContent(class: "grid overflow-hidden p-0 md:max-h-[500px] md:max-w-[700px] lg:max-w-[800px]") do - SidebarWrapper(class: "items-start") do - Sidebar(collapsible: :none, class: "hidden md:flex") do - SidebarContent do - SidebarGroup do - SidebarGroupContent 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" } - end - end - end - end - end - end - end - main(class: "flex h-[480px] flex-1 flex-col overflow-hidden") do - end - 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/views/docs/sidebar/example.rb b/app/views/docs/sidebar/example.rb index 10927d2..315c0ca 100644 --- a/app/views/docs/sidebar/example.rb +++ b/app/views/docs/sidebar/example.rb @@ -69,7 +69,7 @@ def initialize(sidebar_state:) ellipsis_icon() span(class: "sr-only") { "More" } end - DropdownMenuContent(class: "z-40") do + DropdownMenuContent do DropdownMenuItem(href: '#') { "Profile" } DropdownMenuItem(href: '#') { "Billing" } DropdownMenuItem(href: '#') { "Team" } @@ -99,7 +99,7 @@ def initialize(sidebar_state:) ellipsis_icon() span(class: "sr-only") { "More" } end - DropdownMenuContent(class: "z-40") do + DropdownMenuContent do DropdownMenuItem(href: '#') { "Profile" } DropdownMenuItem(href: '#') { "Billing" } DropdownMenuItem(href: '#') { "Team" } diff --git a/app/views/docs/sidebar/inset_example.rb b/app/views/docs/sidebar/inset_example.rb index 4f0e91a..3747097 100644 --- a/app/views/docs/sidebar/inset_example.rb +++ b/app/views/docs/sidebar/inset_example.rb @@ -69,7 +69,7 @@ def initialize(sidebar_state:) ellipsis_icon() span(class: "sr-only") { "More" } end - DropdownMenuContent(class: "z-40") do + DropdownMenuContent do DropdownMenuItem(href: '#') { "Profile" } DropdownMenuItem(href: '#') { "Billing" } DropdownMenuItem(href: '#') { "Team" } @@ -99,7 +99,7 @@ def initialize(sidebar_state:) ellipsis_icon() span(class: "sr-only") { "More" } end - DropdownMenuContent(class: "z-40") do + DropdownMenuContent do DropdownMenuItem(href: '#') { "Profile" } DropdownMenuItem(href: '#') { "Billing" } DropdownMenuItem(href: '#') { "Team" } diff --git a/config/routes.rb b/config/routes.rb index fea4d64..a6da0ff 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -53,7 +53,6 @@ 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 "sidebar/dialog", to: "docs/sidebar#dialog_example", as: :docs_sidebar_dialog get "skeleton", to: "docs#skeleton", as: :docs_skeleton get "switch", to: "docs#switch", as: :docs_switch get "table", to: "docs#table", as: :docs_table From ad242bd4483899655c89f7d2a513efbbb5c43d9e Mon Sep 17 00:00:00 2001 From: Lucas Sousa Date: Tue, 20 May 2025 18:26:55 -0300 Subject: [PATCH 6/6] Add a alert to list the sidebar requirements --- app/views/docs/sidebar.rb | 53 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/app/views/docs/sidebar.rb b/app/views/docs/sidebar.rb index d75438d..165872f 100644 --- a/app/views/docs/sidebar.rb +++ b/app/views/docs/sidebar.rb @@ -9,6 +9,26 @@ def view_template Heading(level: 2) { "Usage" } + Alert do + info_icon + AlertTitle { "Requirements" } + AlertDescription { "The sidebar component depends on the following components:" } + ul(class: "list-disc list-inside") do + li do + InlineLink(href: docs_sheet_path, target: "_blank", class: "inline-flex items-center gap-2") do + span { "Sheet" } + external_icon_link + end + end + li do + div(class: "inline-flex items-center gap-2") do + InlineLink(href: docs_separator_path, target: "_blank") { "Separator" } + external_icon_link + end + end + end + end + render Docs::VisualCodeExample.new(title: "Example", src: "/docs/sidebar/example", context: self) do Views::Docs::Sidebar::Example::CODE end @@ -120,4 +140,37 @@ def inbox_icon 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