From 6c5e4ee876df983a4636b5c328ffe2d349cafa76 Mon Sep 17 00:00:00 2001 From: Jakob Skjerning Date: Sat, 21 Feb 2026 20:23:25 +0100 Subject: [PATCH 1/5] Demo flowbite-components 0.2.1 --- demo/Gemfile.lock | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/demo/Gemfile.lock b/demo/Gemfile.lock index 345eda2..966c7d0 100644 --- a/demo/Gemfile.lock +++ b/demo/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: ../.. specs: - flowbite-components (0.2.0) + flowbite-components (0.2.1) view_component (>= 4.0.0) GEM @@ -81,11 +81,11 @@ GEM addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) base64 (0.3.0) - benchmark (0.4.1) - bigdecimal (3.2.2) + benchmark (0.5.0) + bigdecimal (4.0.1) builder (3.3.0) - concurrent-ruby (1.3.5) - connection_pool (2.5.3) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) crass (1.0.6) css_parser (1.21.1) addressable @@ -118,7 +118,7 @@ GEM herb (0.8.10-x86_64-linux-musl) htmlbeautifier (1.4.3) htmlentities (4.3.4) - i18n (1.14.7) + i18n (1.14.8) concurrent-ruby (~> 1.0) io-console (0.8.1) irb (1.15.2) @@ -155,7 +155,6 @@ GEM net-smtp marcel (1.0.4) mini_mime (1.1.5) - mini_portile2 (2.8.9) minitest (6.0.1) prism (~> 1.5) net-imap (0.5.7) @@ -257,8 +256,6 @@ GEM rexml (3.4.4) rouge (4.5.2) securerandom (0.4.1) - sqlite3 (2.6.0) - mini_portile2 (~> 2.8.0) sqlite3 (2.6.0-aarch64-linux-gnu) sqlite3 (2.6.0-aarch64-linux-musl) sqlite3 (2.6.0-arm-linux-gnu) @@ -282,10 +279,11 @@ GEM timeout (0.4.3) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - uri (1.0.4) + uri (1.1.1) useragent (0.16.11) - view_component (4.0.2) - activesupport (>= 7.1.0, < 8.1) + view_component (4.4.0) + actionview (>= 7.1.0) + activesupport (>= 7.1.0) concurrent-ruby (~> 1) websocket-driver (0.7.7) base64 From 3064557e44c8fade68631440eb22aed8ff04f2fb Mon Sep 17 00:00:00 2001 From: Jakob Skjerning Date: Sat, 21 Feb 2026 20:34:18 +0100 Subject: [PATCH 2/5] Add Sidebar component with Item subcomponent --- app/components/flowbite/sidebar.rb | 78 ++++++++++++ app/components/flowbite/sidebar/item.rb | 68 +++++++++++ demo/.yardoc/checksums | 2 + demo/.yardoc/object_types | Bin 9370 -> 9947 bytes demo/.yardoc/objects/root.dat | Bin 133017 -> 142774 bytes .../components/previews/sidebar_preview.rb | 75 ++++++++++++ test/components/flowbite/sidebar_test.rb | 111 ++++++++++++++++++ 7 files changed, 334 insertions(+) create mode 100644 app/components/flowbite/sidebar.rb create mode 100644 app/components/flowbite/sidebar/item.rb create mode 100644 demo/test/components/previews/sidebar_preview.rb create mode 100644 test/components/flowbite/sidebar_test.rb diff --git a/app/components/flowbite/sidebar.rb b/app/components/flowbite/sidebar.rb new file mode 100644 index 0000000..ed7256d --- /dev/null +++ b/app/components/flowbite/sidebar.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module Flowbite + # Renders a sidebar navigation component. + # + # Use {Flowbite::Sidebar} and the child {Flowbite::Sidebar::Item} components + # to create a vertical navigation sidebar. + # + # @example Usage + # <%= render(Flowbite::Sidebar.new) do |sidebar| %> + # <% sidebar.with_item do %> + # <%= render(Flowbite::Sidebar::Item.new(href: "/dashboard")) do |item| %> + # <% item.with_icon do %> + # + # <% end %> + # Dashboard + # <% end %> + # <% end %> + # <% end %> + # + # @viewcomponent_slot items The navigation items rendered in the sidebar. + # + # @see https://flowbite.com/docs/components/sidebar/ + # @lookbook_embed SidebarPreview + class Sidebar < ViewComponent::Base + renders_many :items + + class << self + def classes + [ + "fixed", "top-0", "left-0", "z-40", "w-64", "h-screen", + "transition-transform", "-translate-x-full", "sm:translate-x-0" + ] + end + end + + # @param class [Array] Additional CSS classes for the sidebar + # container. + # @param options [Hash] Additional HTML options for the sidebar container. + def initialize(class: nil, **options) + super() + @class = Array.wrap(binding.local_variable_get(:class)) + @options = options + end + + def call + content_tag(:aside, aside_options) do + content_tag(:div, class: wrapper_classes) do + content_tag(:ul, class: list_classes) do + items.each do |item| + concat(item) + end + end + end + end + end + + private + + def aside_classes + self.class.classes + @class + end + + def aside_options + {class: aside_classes, "aria-label": "Sidebar"}.merge(@options) + end + + def list_classes + ["space-y-2", "font-medium"] + end + + def wrapper_classes + ["h-full", "px-3", "py-4", "overflow-y-auto", "bg-neutral-primary-soft"] + end + end +end diff --git a/app/components/flowbite/sidebar/item.rb b/app/components/flowbite/sidebar/item.rb new file mode 100644 index 0000000..5372648 --- /dev/null +++ b/app/components/flowbite/sidebar/item.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Flowbite + class Sidebar + # Renders a sidebar navigation item. + # + # Each item renders as a list item containing a link. Optionally, an icon + # can be provided using the +icon+ slot, which will be displayed before the + # label text. + # + # @example Basic item + # <%= render Flowbite::Sidebar::Item.new(href: "/dashboard") { "Dashboard" } %> + # + # @example Item with icon + # <%= render(Flowbite::Sidebar::Item.new(href: "/dashboard")) do |item| %> + # <% item.with_icon do %> + # ... + # <% end %> + # Dashboard + # <% end %> + # + # @viewcomponent_slot icon An optional icon displayed before the label text. + class Item < ViewComponent::Base + renders_one :icon + + attr_reader :href, :options + + class << self + def classes + [ + "flex", "items-center", "px-2", "py-1.5", "text-body", + "rounded-base", "hover:bg-neutral-tertiary", "hover:text-fg-brand", "group" + ] + end + end + + # @param class [Array] Additional CSS classes for the link element. + # @param href [String] The URL for the navigation link. + # @param options [Hash] Additional HTML attributes for the link element. + def initialize(href:, class: nil, **options) + super() + @class = Array.wrap(binding.local_variable_get(:class)) + @href = href + @options = options + end + + def call + content_tag(:li) do + link_options = {class: link_classes}.merge(options) + content_tag(:a, href: href, **link_options) do + concat(icon) if icon? + concat(content_tag(:span, content, class: label_classes)) + end + end + end + + private + + def label_classes + "ms-3" if icon? + end + + def link_classes + self.class.classes + @class + end + end + end +end diff --git a/demo/.yardoc/checksums b/demo/.yardoc/checksums index 269055f..c0b997b 100644 --- a/demo/.yardoc/checksums +++ b/demo/.yardoc/checksums @@ -5,6 +5,7 @@ app/components/flowbite/style.rb ef063360cc99cd7a6b8e67a7693326bb5dfb0e42 app/components/flowbite/toast.rb 6b822405dd55d87d56979e6cfba55e8f73965047 app/components/flowbite/button.rb 6ae7681d3b842d73aa99cddfa5a9b107ede7fea4 app/components/flowbite/styles.rb 929c42e428ba5a8e16efacaae0f35380e2f5f95c +app/components/flowbite/sidebar.rb 0c0838341eb3c40225c03389f854b4573715ad25 app/components/flowbite/input/url.rb f1046824f9b06c8df8e0f567979321b82baac6fa app/components/flowbite/breadcrumb.rb c69ffb465b6e7f2489d4ac9a928e08bdf252fe99 app/components/flowbite/card/title.rb 8067aa1e027c725896b063b67364aecfbf2f7d4e @@ -19,6 +20,7 @@ app/components/flowbite/input/phone.rb 0dfe3e9a83c4fb9f558405a20601649c7b08922a app/components/flowbite/input_field.rb 8ef98ace7d4ccb4f474d3063cf48cc5c83cd8068 app/components/flowbite/input/number.rb a33580788ad91308b85955fdb44d37883329dd4e app/components/flowbite/input/select.rb 9f1a6406efdda2e29d479117a35c2a924bd888c2 +app/components/flowbite/sidebar/item.rb 11361327fa3ce132ebb8da08dd09405f00aa2f30 app/components/flowbite/button/outline.rb 2829cf352a03c00dd99a56a05181c4e1a6794d18 app/components/flowbite/input/checkbox.rb 500f109206a47997bf2bc0732399297a92005dc0 app/components/flowbite/input/password.rb 39a4c2bb2684a0a310175307bd1bdfd9c99c6cd1 diff --git a/demo/.yardoc/object_types b/demo/.yardoc/object_types index dbe993c5ba46c7ba9a433a869c29811aa0419625..f2e3a01af12124e7eb19e6fa781289ec277e3558 100644 GIT binary patch delta 513 zcmbQ`dE0k_0i)SO!{dTtZaMkoNtq?7R#w58DXB?`8$V8Al#oFeEwZxmEJ@AXT+h_Z z%y?uoGn*rul^lj5z2uz4;^Nd|Hfwe_PbEbRdF9N!%#zH+oXLvZA^{NVijk76t?Rg>okOY*=B&B-h-nXE4)DGn1TFG?&ZNG*aHwb_oxoUQ1QPh%nJY(~}5| d3FVx`q|_XkcOY)n#HltjFB__KbDpRUKLG1#uIB&% delta 42 ycmccZJIix|0i(%8!{d|Pm;^Q}FxE0|-py3Y%(!i{C7UDL=3-t$&duAzRrvuxs}0Bi diff --git a/demo/.yardoc/objects/root.dat b/demo/.yardoc/objects/root.dat index 0a4f2411d91cb8763f6f9a3931e7b7042a84f8ea..42fce65b76ef49335d3791b21faf5ec0d89dad3f 100644 GIT binary patch delta 11909 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 E0?KgHIRF3v delta 6258 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'.html_safe + end + item.with_content("Dashboard") + end + end + + def inbox_item + Flowbite::Sidebar::Item.new(href: "#").tap do |item| + item.with_icon do + ''.html_safe + end + item.with_content("Inbox") + end + end + + def kanban_item + Flowbite::Sidebar::Item.new(href: "#").tap do |item| + item.with_icon do + ''.html_safe + end + item.with_content("Kanban") + end + end + + def products_item + Flowbite::Sidebar::Item.new(href: "#").tap do |item| + item.with_icon do + ''.html_safe + end + item.with_content("Products") + end + end + + def render_to_string(component) + ApplicationController.render(component, layout: false) + end + + def users_item + Flowbite::Sidebar::Item.new(href: "#").tap do |item| + item.with_icon do + ''.html_safe + end + item.with_content("Users") + end + end +end diff --git a/test/components/flowbite/sidebar_test.rb b/test/components/flowbite/sidebar_test.rb new file mode 100644 index 0000000..3e83098 --- /dev/null +++ b/test/components/flowbite/sidebar_test.rb @@ -0,0 +1,111 @@ +require "test_helper" + +class Flowbite::SidebarTest < Minitest::Test + include ViewComponent::TestHelpers + + def test_render_component + render_inline(Flowbite::Sidebar.new) + + assert_component_rendered + assert_selector("aside[aria-label='Sidebar']") + end + + def test_renders_aside_with_proper_classes + render_inline(Flowbite::Sidebar.new) + + assert_selector("aside.fixed.top-0.left-0.z-40.w-64.h-screen") + end + + def test_renders_wrapper_div_with_proper_classes + render_inline(Flowbite::Sidebar.new) + + assert_selector("aside div.h-full.px-3.py-4.overflow-y-auto.bg-neutral-primary-soft") + end + + def test_renders_unordered_list + render_inline(Flowbite::Sidebar.new) + + assert_selector("ul.space-y-2.font-medium") + end + + def test_renders_with_items + render_inline(Flowbite::Sidebar.new) do |sidebar| + sidebar.with_item { "

  • Dashboard
  • ".html_safe } + sidebar.with_item { "
  • Settings
  • ".html_safe } + end + + assert_selector("li", count: 2) + end + + def test_adds_classes_to_the_default_classes + render_inline(Flowbite::Sidebar.new(class: "custom-class")) + + assert_selector("aside.fixed.top-0.left-0.custom-class") + end + + def test_passes_options_as_attributes + render_inline(Flowbite::Sidebar.new(id: "my-sidebar")) + + assert_selector("aside#my-sidebar") + end +end + +class Flowbite::Sidebar::ItemTest < Minitest::Test + include ViewComponent::TestHelpers + + def test_render_component + render_inline(Flowbite::Sidebar::Item.new(href: "/dashboard")) { "Dashboard" } + + assert_component_rendered + assert_selector("li") + end + + def test_renders_link_with_href + render_inline(Flowbite::Sidebar::Item.new(href: "/dashboard")) { "Dashboard" } + + assert_selector("a[href='/dashboard']", text: "Dashboard") + end + + def test_renders_link_with_proper_classes + render_inline(Flowbite::Sidebar::Item.new(href: "/dashboard")) { "Dashboard" } + + assert_selector("a.flex.items-center.px-2.py-1\\.5.text-body.rounded-base.group") + end + + def test_renders_text_without_margin_when_no_icon + render_inline(Flowbite::Sidebar::Item.new(href: "/dashboard")) { "Dashboard" } + + assert_selector("span", text: "Dashboard") + assert_no_selector("span.ms-3") + end + + def test_renders_icon_slot + render_inline(Flowbite::Sidebar::Item.new(href: "/dashboard")) do |item| + item.with_icon { "".html_safe } + "Dashboard" + end + + assert_selector("svg.w-5.h-5") + end + + def test_renders_text_with_margin_when_icon_is_present + render_inline(Flowbite::Sidebar::Item.new(href: "/dashboard")) do |item| + item.with_icon { "".html_safe } + "Dashboard" + end + + assert_selector("span.ms-3", text: "Dashboard") + end + + def test_adds_classes_to_the_default_classes + render_inline(Flowbite::Sidebar::Item.new(href: "/", class: "custom-class")) { "Home" } + + assert_selector("a.flex.items-center.px-2.py-1\\.5.text-body.rounded-base.group.custom-class") + end + + def test_passes_options_as_attributes + render_inline(Flowbite::Sidebar::Item.new(href: "/", id: "home-link")) { "Home" } + + assert_selector("a[id='home-link']") + end +end From b3216acefe65de778f42cada3d163cb85c60c503 Mon Sep 17 00:00:00 2001 From: Jakob Skjerning Date: Sat, 21 Feb 2026 22:20:05 +0100 Subject: [PATCH 3/5] Render the sidebar items on their own This allows us to render the navigation items in the sidebar without needing to render the entire sidebar component, which is useful for cases like the documentation sidebar where we want to reuse the navigation structure but not the fixed positioning or background styles of the full sidebar. --- app/components/flowbite/sidebar.rb | 34 +++------- app/components/flowbite/sidebar/navigation.rb | 62 ++++++++++++++++++ demo/.yardoc/checksums | 5 +- demo/.yardoc/object_types | Bin 9947 -> 10176 bytes demo/.yardoc/objects/root.dat | Bin 142774 -> 145737 bytes demo/app/views/docs/components/show.html.erb | 26 +++----- demo/app/views/docs/pages/show.html.erb | 26 +++----- .../components/previews/sidebar_preview.rb | 32 ++++----- test/components/flowbite/sidebar_test.rb | 59 +++++++++++++---- 9 files changed, 157 insertions(+), 87 deletions(-) create mode 100644 app/components/flowbite/sidebar/navigation.rb diff --git a/app/components/flowbite/sidebar.rb b/app/components/flowbite/sidebar.rb index ed7256d..766682c 100644 --- a/app/components/flowbite/sidebar.rb +++ b/app/components/flowbite/sidebar.rb @@ -1,32 +1,24 @@ # frozen_string_literal: true module Flowbite - # Renders a sidebar navigation component. + # Renders a fixed-position sidebar container. # - # Use {Flowbite::Sidebar} and the child {Flowbite::Sidebar::Item} components - # to create a vertical navigation sidebar. + # Use {Flowbite::Sidebar} as the outer shell and + # {Flowbite::Sidebar::Navigation} inside it to render the list of + # navigation items. # # @example Usage - # <%= render(Flowbite::Sidebar.new) do |sidebar| %> - # <% sidebar.with_item do %> - # <%= render(Flowbite::Sidebar::Item.new(href: "/dashboard")) do |item| %> - # <% item.with_icon do %> - # - # <% end %> - # Dashboard + # <%= render(Flowbite::Sidebar.new) do %> + # <%= render(Flowbite::Sidebar::Navigation.new) do |nav| %> + # <% nav.with_item do %> + # <%= render(Flowbite::Sidebar::Item.new(href: "/dashboard")) { "Dashboard" } %> # <% end %> # <% end %> # <% end %> # - # @viewcomponent_slot items The navigation items rendered in the sidebar. - # # @see https://flowbite.com/docs/components/sidebar/ # @lookbook_embed SidebarPreview class Sidebar < ViewComponent::Base - renders_many :items - class << self def classes [ @@ -48,11 +40,7 @@ def initialize(class: nil, **options) def call content_tag(:aside, aside_options) do content_tag(:div, class: wrapper_classes) do - content_tag(:ul, class: list_classes) do - items.each do |item| - concat(item) - end - end + content end end end @@ -67,10 +55,6 @@ def aside_options {class: aside_classes, "aria-label": "Sidebar"}.merge(@options) end - def list_classes - ["space-y-2", "font-medium"] - end - def wrapper_classes ["h-full", "px-3", "py-4", "overflow-y-auto", "bg-neutral-primary-soft"] end diff --git a/app/components/flowbite/sidebar/navigation.rb b/app/components/flowbite/sidebar/navigation.rb new file mode 100644 index 0000000..5b15721 --- /dev/null +++ b/app/components/flowbite/sidebar/navigation.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Flowbite + class Sidebar + # Renders the navigation list for a sidebar. + # + # This component renders a +
      + with navigation items. It can be used + # inside a {Flowbite::Sidebar} for a fixed-position sidebar, or standalone + # in any layout that needs sidebar-style navigation. + # + # @example Inside a Sidebar + # <%= render(Flowbite::Sidebar.new) do %> + # <%= render(Flowbite::Sidebar::Navigation.new) do |nav| %> + # <% nav.with_item do %> + # <%= render(Flowbite::Sidebar::Item.new(href: "/")) { "Home" } %> + # <% end %> + # <% end %> + # <% end %> + # + # @example Standalone + # <%= render(Flowbite::Sidebar::Navigation.new) do |nav| %> + # <% nav.with_item do %> + # <%= render(Flowbite::Sidebar::Item.new(href: "/")) { "Home" } %> + # <% end %> + # <% end %> + class Navigation < ViewComponent::Base + renders_many :items + + class << self + def classes + ["space-y-2", "font-medium"] + end + end + + # @param class [Array] Additional CSS classes for the list element. + # @param options [Hash] Additional HTML options for the list element. + def initialize(class: nil, **options) + super() + @class = Array.wrap(binding.local_variable_get(:class)) + @options = options + end + + def call + content_tag(:ul, list_options) do + items.each do |item| + concat(item) + end + end + end + + private + + def list_classes + self.class.classes + @class + end + + def list_options + {class: list_classes}.merge(@options) + end + end + end +end diff --git a/demo/.yardoc/checksums b/demo/.yardoc/checksums index c0b997b..6194637 100644 --- a/demo/.yardoc/checksums +++ b/demo/.yardoc/checksums @@ -5,7 +5,7 @@ app/components/flowbite/style.rb ef063360cc99cd7a6b8e67a7693326bb5dfb0e42 app/components/flowbite/toast.rb 6b822405dd55d87d56979e6cfba55e8f73965047 app/components/flowbite/button.rb 6ae7681d3b842d73aa99cddfa5a9b107ede7fea4 app/components/flowbite/styles.rb 929c42e428ba5a8e16efacaae0f35380e2f5f95c -app/components/flowbite/sidebar.rb 0c0838341eb3c40225c03389f854b4573715ad25 +app/components/flowbite/sidebar.rb eeac5c328d63c05728c994a80e485d9b91ad83a3 app/components/flowbite/input/url.rb f1046824f9b06c8df8e0f567979321b82baac6fa app/components/flowbite/breadcrumb.rb c69ffb465b6e7f2489d4ac9a928e08bdf252fe99 app/components/flowbite/card/title.rb 8067aa1e027c725896b063b67364aecfbf2f7d4e @@ -20,7 +20,7 @@ app/components/flowbite/input/phone.rb 0dfe3e9a83c4fb9f558405a20601649c7b08922a app/components/flowbite/input_field.rb 8ef98ace7d4ccb4f474d3063cf48cc5c83cd8068 app/components/flowbite/input/number.rb a33580788ad91308b85955fdb44d37883329dd4e app/components/flowbite/input/select.rb 9f1a6406efdda2e29d479117a35c2a924bd888c2 -app/components/flowbite/sidebar/item.rb 11361327fa3ce132ebb8da08dd09405f00aa2f30 +app/components/flowbite/sidebar/item.rb 8dc762357988fdf6e9308de129bd7c89b6700472 app/components/flowbite/button/outline.rb 2829cf352a03c00dd99a56a05181c4e1a6794d18 app/components/flowbite/input/checkbox.rb 500f109206a47997bf2bc0732399297a92005dc0 app/components/flowbite/input/password.rb 39a4c2bb2684a0a310175307bd1bdfd9c99c6cd1 @@ -36,6 +36,7 @@ app/components/flowbite/input_field/phone.rb 6a3da98a2dded98b1c4d14c2419077fc9de app/components/flowbite/input/radio_button.rb 60ccac6862b459b89f9f3335246a51d83ae4af63 app/components/flowbite/input_field/number.rb 32dce4e2bb586f64229dc78b541700df829068c7 app/components/flowbite/input_field/select.rb 1f9788b5fff4be2b65184e73e739da8fa9de0264 +app/components/flowbite/sidebar/navigation.rb 0f79a28928a40c222210352eaad86eb111e3a401 app/components/flowbite/breadcrumb/home_icon.rb bd2e47a31a793a79f20bd792a2a7e2dd8094fc5d app/components/flowbite/input_field/checkbox.rb 4cbc6f541842838234189b383f813b164a06379b app/components/flowbite/input_field/password.rb 37e592f55f258cc4b81859d48763b9c0eff12f61 diff --git a/demo/.yardoc/object_types b/demo/.yardoc/object_types index f2e3a01af12124e7eb19e6fa781289ec277e3558..a5bb2e9de0b2f2868bf711fb8808b6621db0322b 100644 GIT binary patch delta 263 zcmccZd%%B!0i*dw!)g{)CAXaX@}$g?R4c3C%#_rm#3Cyzzr?c4^u&_P{5&>mb~jHY z<;jk$Vv}v;I5wYV$z^0bvDuRCE!$>q-dh45T6neVCFdj-7pE3OjM2rTcCw_!Bdg^9w*;DxSn9yV*u=J_i7K C=37Mo delta 68 zcmV-K0K5OdPuoq9Fab8PF?$5F&;*PD0mQRi2JHqHBSvg*cVcODWjZ=jX=G($VR9pE aX>)XwT@x9zRSnM&0VtEv1Rk>~AD0Mj{}@gH diff --git a/demo/.yardoc/objects/root.dat b/demo/.yardoc/objects/root.dat index 42fce65b76ef49335d3791b21faf5ec0d89dad3f..a52ce9c023a5a6c6798ac35e7afe87ed4761a2d5 100644 GIT binary patch delta 9185 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 aTm>7Wvm`hR`IJsZ{%#Omm9%4IB>Xpq4e^}- diff --git a/demo/app/views/docs/components/show.html.erb b/demo/app/views/docs/components/show.html.erb index 29fec6d..e594021 100644 --- a/demo/app/views/docs/components/show.html.erb +++ b/demo/app/views/docs/components/show.html.erb @@ -1,30 +1,24 @@
      -
      <%= render(Docs::Headline::H1.new) { @code_object.path } %> diff --git a/demo/app/views/docs/pages/show.html.erb b/demo/app/views/docs/pages/show.html.erb index 9a317ac..b587b90 100644 --- a/demo/app/views/docs/pages/show.html.erb +++ b/demo/app/views/docs/pages/show.html.erb @@ -1,30 +1,24 @@
      -

      <%= @page.title %>

      diff --git a/demo/test/components/previews/sidebar_preview.rb b/demo/test/components/previews/sidebar_preview.rb index 7fb2a5b..fa166b1 100644 --- a/demo/test/components/previews/sidebar_preview.rb +++ b/demo/test/components/previews/sidebar_preview.rb @@ -3,27 +3,23 @@ class SidebarPreview < Lookbook::Preview # Use the sidebar component to show a list of navigation items. def example - render(Flowbite::Sidebar.new) do |sidebar| - sidebar.with_item do - render_to_string(dashboard_item) - end - sidebar.with_item do - render_to_string(kanban_item) - end - sidebar.with_item do - render_to_string(inbox_item) - end - sidebar.with_item do - render_to_string(users_item) - end - sidebar.with_item do - render_to_string(products_item) - end + render(Flowbite::Sidebar.new) do + render_to_string(navigation) end end private + def navigation + Flowbite::Sidebar::Navigation.new.tap do |nav| + nav.with_item { render_item(dashboard_item) } + nav.with_item { render_item(kanban_item) } + nav.with_item { render_item(inbox_item) } + nav.with_item { render_item(users_item) } + nav.with_item { render_item(products_item) } + end + end + def dashboard_item Flowbite::Sidebar::Item.new(href: "#").tap do |item| item.with_icon do @@ -60,6 +56,10 @@ def products_item end end + def render_item(component) + ApplicationController.render(component, layout: false) + end + def render_to_string(component) ApplicationController.render(component, layout: false) end diff --git a/test/components/flowbite/sidebar_test.rb b/test/components/flowbite/sidebar_test.rb index 3e83098..5152aca 100644 --- a/test/components/flowbite/sidebar_test.rb +++ b/test/components/flowbite/sidebar_test.rb @@ -4,49 +4,84 @@ class Flowbite::SidebarTest < Minitest::Test include ViewComponent::TestHelpers def test_render_component - render_inline(Flowbite::Sidebar.new) + render_inline(Flowbite::Sidebar.new) { "content" } assert_component_rendered assert_selector("aside[aria-label='Sidebar']") end def test_renders_aside_with_proper_classes - render_inline(Flowbite::Sidebar.new) + render_inline(Flowbite::Sidebar.new) { "content" } assert_selector("aside.fixed.top-0.left-0.z-40.w-64.h-screen") end + def test_renders_responsive_transform_classes + render_inline(Flowbite::Sidebar.new) { "content" } + + assert_selector("aside.transition-transform.-translate-x-full.sm\\:translate-x-0") + end + def test_renders_wrapper_div_with_proper_classes - render_inline(Flowbite::Sidebar.new) + render_inline(Flowbite::Sidebar.new) { "content" } assert_selector("aside div.h-full.px-3.py-4.overflow-y-auto.bg-neutral-primary-soft") end - def test_renders_unordered_list - render_inline(Flowbite::Sidebar.new) + def test_renders_content + render_inline(Flowbite::Sidebar.new) { "sidebar content" } + + assert_text("sidebar content") + end + + def test_adds_classes_to_the_default_classes + render_inline(Flowbite::Sidebar.new(class: "custom-class")) { "content" } + + assert_selector("aside.fixed.top-0.left-0.custom-class") + end + + def test_passes_options_as_attributes + render_inline(Flowbite::Sidebar.new(id: "my-sidebar")) { "content" } + + assert_selector("aside#my-sidebar") + end +end + +class Flowbite::Sidebar::NavigationTest < Minitest::Test + include ViewComponent::TestHelpers + + def test_render_component + render_inline(Flowbite::Sidebar::Navigation.new) + + assert_component_rendered + assert_selector("ul") + end + + def test_renders_list_with_proper_classes + render_inline(Flowbite::Sidebar::Navigation.new) assert_selector("ul.space-y-2.font-medium") end def test_renders_with_items - render_inline(Flowbite::Sidebar.new) do |sidebar| - sidebar.with_item { "
    • Dashboard
    • ".html_safe } - sidebar.with_item { "
    • Settings
    • ".html_safe } + render_inline(Flowbite::Sidebar::Navigation.new) do |nav| + nav.with_item { "
    • Dashboard
    • ".html_safe } + nav.with_item { "
    • Settings
    • ".html_safe } end assert_selector("li", count: 2) end def test_adds_classes_to_the_default_classes - render_inline(Flowbite::Sidebar.new(class: "custom-class")) + render_inline(Flowbite::Sidebar::Navigation.new(class: "custom-class")) - assert_selector("aside.fixed.top-0.left-0.custom-class") + assert_selector("ul.space-y-2.font-medium.custom-class") end def test_passes_options_as_attributes - render_inline(Flowbite::Sidebar.new(id: "my-sidebar")) + render_inline(Flowbite::Sidebar::Navigation.new(id: "my-nav")) - assert_selector("aside#my-sidebar") + assert_selector("ul#my-nav") end end From 7e1185aabe0d1243504c87737ea4afd1e8651ab4 Mon Sep 17 00:00:00 2001 From: Jakob Skjerning Date: Sun, 22 Feb 2026 12:19:06 +0100 Subject: [PATCH 4/5] Clarify that we render Sidebar::Item components --- app/components/flowbite/sidebar.rb | 2 +- demo/.yardoc/checksums | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/flowbite/sidebar.rb b/app/components/flowbite/sidebar.rb index 766682c..298c79f 100644 --- a/app/components/flowbite/sidebar.rb +++ b/app/components/flowbite/sidebar.rb @@ -5,7 +5,7 @@ module Flowbite # # Use {Flowbite::Sidebar} as the outer shell and # {Flowbite::Sidebar::Navigation} inside it to render the list of - # navigation items. + # {Flowbite::Sidebar::Item}s. # # @example Usage # <%= render(Flowbite::Sidebar.new) do %> diff --git a/demo/.yardoc/checksums b/demo/.yardoc/checksums index 6194637..a5792e2 100644 --- a/demo/.yardoc/checksums +++ b/demo/.yardoc/checksums @@ -5,7 +5,7 @@ app/components/flowbite/style.rb ef063360cc99cd7a6b8e67a7693326bb5dfb0e42 app/components/flowbite/toast.rb 6b822405dd55d87d56979e6cfba55e8f73965047 app/components/flowbite/button.rb 6ae7681d3b842d73aa99cddfa5a9b107ede7fea4 app/components/flowbite/styles.rb 929c42e428ba5a8e16efacaae0f35380e2f5f95c -app/components/flowbite/sidebar.rb eeac5c328d63c05728c994a80e485d9b91ad83a3 +app/components/flowbite/sidebar.rb 85033b602a098f3334b9b3e180239ef20a1b6f90 app/components/flowbite/input/url.rb f1046824f9b06c8df8e0f567979321b82baac6fa app/components/flowbite/breadcrumb.rb c69ffb465b6e7f2489d4ac9a928e08bdf252fe99 app/components/flowbite/card/title.rb 8067aa1e027c725896b063b67364aecfbf2f7d4e From 94cf21d0be12f45c5bca65486e3be592421b2b4a Mon Sep 17 00:00:00 2001 From: Jakob Skjerning Date: Sun, 22 Feb 2026 12:37:57 +0100 Subject: [PATCH 5/5] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad7bd1d..3946e73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added +* Sidebar component. + ### Changed ### Fixed