diff --git a/audit/divergences/data_table.yml b/audit/divergences/data_table.yml index c8bbf97da..79a0228e3 100644 --- a/audit/divergences/data_table.yml +++ b/audit/divergences/data_table.yml @@ -10,3 +10,17 @@ shadcn: "dashboard-01 block: Select onValueChange calls TanStack Table's setPageSize on the client" reason: "RubyUI's DataTable is server-driven, as for pagination: the page size is a query param the server reads, so the list comes back from the server and the URL can be shared. The Select itself (open, arrows, typeahead, Enter, Esc) behaves as shadcn's." since: "2026-11-05" +- component: data_table + dimension: Look + match: {part: DataTableRowCheckbox, property: display, rubyui: "block", shadcn: "flex"} + rubyui: "row checkbox is RubyUI::Checkbox, a native with no children; as a flex item of the cell's div.flex it computes display: block" + shadcn: "Base UI Checkbox is a that centers its Indicator child" + reason: "flex only lays out children, and the native input has none (its check mark is an absolutely positioned ::before), so block and flex render the same box. Adding flex to the input would change nothing on screen." + since: "2026-11-06" +- component: data_table + dimension: Look + match: {part: DataTableRowCheckbox, property: text-align, rubyui: "start", shadcn: "left"} + rubyui: "select-all checkbox (header cell, text-left) is a native input: the browser's form-control defaults reset text-align to start instead of inheriting left" + shadcn: "the Base UI inherits text-align: left from the header cell (TableHead text-left); row checkboxes in body cells are start on both sides" + reason: "In a left-to-right document start resolves to left, and the input has no text to align either way." + since: "2026-11-06" diff --git a/audit/fixtures/data_table/rubyui.rb b/audit/fixtures/data_table/rubyui.rb index 40a07c810..49754118b 100644 --- a/audit/fixtures/data_table/rubyui.rb +++ b/audit/fixtures/data_table/rubyui.rb @@ -44,8 +44,15 @@ def view_template TableRow do TableCell(class: "w-8") { div(class: "flex items-center justify-center") { DataTableRowCheckbox(value: id, label: "Select row") } } TableCell { header } - TableCell(data: {column: "type"}) { Badge(variant: :outline, size: :sm) { type } } - TableCell(data: {column: "status"}) { Badge(variant: :outline, size: :sm) { status } } + TableCell(data: {column: "type"}) do + div(class: "w-32") { Badge(variant: :outline, class: "px-1.5 text-muted-foreground") { type } } + end + TableCell(data: {column: "status"}) do + Badge(variant: :outline, class: "px-1.5 text-muted-foreground") do + (status == "Done") ? circle_check_icon : loader_icon + plain status + end + end TableCell(data: {column: "reviewer"}) { reviewer } end end @@ -64,5 +71,25 @@ def view_template end end end + + private + + # lucide circle-check, as the block's status cell for "Done" + def circle_check_icon + 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", aria_hidden: "true", + class: "fill-green-500 dark:fill-green-400") do |s| + s.circle(cx: "12", cy: "12", r: "10") + s.path(d: "m16 9-5.5 5.5L8 12") + end + end + + # lucide loader, as the block's status cell otherwise + def loader_icon + 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", aria_hidden: "true") do |s| + ["M12 2v4", "m16.2 7.8 2.9-2.9", "M18 12h4", "m16.2 16.2 2.9 2.9", "M12 18v4", "m4.9 19.1 2.9-2.9", "M2 12h4", "m4.9 4.9 2.9 2.9"].each { |d| s.path(d:) } + end + end end end diff --git a/audit/fixtures/data_table/shadcn.tsx b/audit/fixtures/data_table/shadcn.tsx index 28c6f4665..52592af3f 100644 --- a/audit/fixtures/data_table/shadcn.tsx +++ b/audit/fixtures/data_table/shadcn.tsx @@ -4,7 +4,9 @@ import { ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon, + CircleCheckIcon, Columns3Icon, + LoaderIcon, } from "lucide-react" import { useState } from "react" @@ -35,7 +37,8 @@ import { TableRow, } from "@/components/ui/table" -// JSX and classes from bases/base/blocks/dashboard-01/components/data-table.tsx, +// JSX and classes from bases/base/blocks/dashboard-01/components/data-table.tsx +// (status icons: the lucide picks of its IconPlaceholder), // without drag-and-drop, tabs, the drawer and the inline target/limit inputs. const rows = [ { id: 1, header: "Cover page", type: "Cover page", status: "In Process", reviewer: "Eddie Lake" }, @@ -125,6 +128,11 @@ export default function DataTableFixture() { {visible.Status && ( + {row.status === "Done" ? ( + + ) : ( + + )} {row.status} diff --git a/gem/lib/ruby_ui/data_table/data_table_column_toggle.rb b/gem/lib/ruby_ui/data_table/data_table_column_toggle.rb index 340a2d9fe..b3edf4900 100644 --- a/gem/lib/ruby_ui/data_table/data_table_column_toggle.rb +++ b/gem/lib/ruby_ui/data_table/data_table_column_toggle.rb @@ -10,7 +10,9 @@ def initialize(columns:, label: "Columns", **attrs) def view_template div(**attrs) do - render RubyUI::DropdownMenu.new do + # Both wrappers are display: contents so the button is a flex item of + # the toolbar, as shadcn's trigger (inline-flex blockifies to flex). + render RubyUI::DropdownMenu.new(class: "contents") do render RubyUI::DropdownMenuTrigger.new do render RubyUI::Button.new(variant: :outline, size: :sm) do columns_icon @@ -55,7 +57,7 @@ def chevron_down_icon def default_attrs { - class: "relative", + class: "contents", data: { controller: "ruby-ui--data-table-column-visibility", action: "ruby-ui--dropdown-menu:change->ruby-ui--data-table-column-visibility#toggle" diff --git a/gem/lib/ruby_ui/data_table/data_table_toolbar.rb b/gem/lib/ruby_ui/data_table/data_table_toolbar.rb index e94867a2a..76099ed65 100644 --- a/gem/lib/ruby_ui/data_table/data_table_toolbar.rb +++ b/gem/lib/ruby_ui/data_table/data_table_toolbar.rb @@ -9,7 +9,10 @@ def view_template(&) private def default_attrs - {class: "flex items-center justify-between gap-2"} + # mb-6 overrides DataTable's space-y-4 (zero specificity) below the + # toolbar: the dashboard-01 block puts 24px (gap-6) between the toolbar + # and the table, 16px between the table and the pagination. + {class: "mb-6 flex items-center justify-between gap-2"} end end end diff --git a/gem/test/ruby_ui/data_table_column_toggle_test.rb b/gem/test/ruby_ui/data_table_column_toggle_test.rb index cc990be5a..fd4c05206 100644 --- a/gem/test/ruby_ui/data_table_column_toggle_test.rb +++ b/gem/test/ruby_ui/data_table_column_toggle_test.rb @@ -44,4 +44,10 @@ def test_trigger_icons_carry_data_icon assert_match(/]*data-icon="inline-start"/, output) assert_match(/]*data-icon="inline-end"/, output) end + + def test_wrappers_stay_out_of_layout + output = phlex { RubyUI.DataTableColumnToggle(columns: [{key: :email, label: "Email"}]) } + assert_match(/
]*class="contents"[^>]*data-slot="dropdown-menu"|]*data-slot="dropdown-menu"[^>]*class="contents"/, output) + end end diff --git a/gem/test/ruby_ui/data_table_toolbar_test.rb b/gem/test/ruby_ui/data_table_toolbar_test.rb index e78b9f9d9..17ae288f5 100644 --- a/gem/test/ruby_ui/data_table_toolbar_test.rb +++ b/gem/test/ruby_ui/data_table_toolbar_test.rb @@ -8,4 +8,9 @@ def test_renders_div_with_flex_layout_and_children assert_match(/]*class="[^"]*flex[^"]*"/, output) assert_match(/INNER/, output) end + + def test_leaves_24px_before_the_table + output = phlex { RubyUI.DataTableToolbar { "INNER" } } + assert_match(/]*class="[^"]*\bmb-6\b[^"]*"/, output) + end end diff --git a/mcp/data/registry/components/data_table.json b/mcp/data/registry/components/data_table.json index d763f01d6..861359fde 100644 --- a/mcp/data/registry/components/data_table.json +++ b/mcp/data/registry/components/data_table.json @@ -12,7 +12,7 @@ }, { "path": "data_table_column_toggle.rb", - "content": "# frozen_string_literal: true\n\nmodule RubyUI\n class DataTableColumnToggle < Base\n def initialize(columns:, label: \"Columns\", **attrs)\n @columns = columns\n @label = label\n super(**attrs)\n end\n\n def view_template\n div(**attrs) do\n render RubyUI::DropdownMenu.new do\n render RubyUI::DropdownMenuTrigger.new do\n render RubyUI::Button.new(variant: :outline, size: :sm) do\n columns_icon\n plain @label\n chevron_down_icon\n end\n end\n render RubyUI::DropdownMenuContent.new(align: :end, class: \"w-32\") do\n @columns.each do |col|\n render RubyUI::DropdownMenuCheckboxItem.new(\n checked: col.fetch(:visible, true),\n class: \"capitalize\",\n data: {column_key: col[:key].to_s}\n ) { plain col[:label] }\n end\n end\n end\n end\n end\n\n private\n\n # lucide columns-3, as shadcn's trigger (data-icon tightens the button's padding)\n def columns_icon\n svg(xmlns: \"http://www.w3.org/2000/svg\", viewBox: \"0 0 24 24\", fill: \"none\", stroke: \"currentColor\",\n stroke_width: \"2\", stroke_linecap: \"round\", stroke_linejoin: \"round\", aria_hidden: \"true\",\n data: {icon: \"inline-start\"}) do |s|\n s.rect(width: \"18\", height: \"18\", x: \"3\", y: \"3\", rx: \"2\")\n s.path(d: \"M9 3v18\")\n s.path(d: \"M15 3v18\")\n end\n end\n\n # lucide chevron-down\n def chevron_down_icon\n svg(xmlns: \"http://www.w3.org/2000/svg\", viewBox: \"0 0 24 24\", fill: \"none\", stroke: \"currentColor\",\n stroke_width: \"2\", stroke_linecap: \"round\", stroke_linejoin: \"round\", aria_hidden: \"true\",\n data: {icon: \"inline-end\"}) do |s|\n s.path(d: \"m6 9 6 6 6-6\")\n end\n end\n\n def default_attrs\n {\n class: \"relative\",\n data: {\n controller: \"ruby-ui--data-table-column-visibility\",\n action: \"ruby-ui--dropdown-menu:change->ruby-ui--data-table-column-visibility#toggle\"\n }\n }\n end\n end\nend\n" + "content": "# frozen_string_literal: true\n\nmodule RubyUI\n class DataTableColumnToggle < Base\n def initialize(columns:, label: \"Columns\", **attrs)\n @columns = columns\n @label = label\n super(**attrs)\n end\n\n def view_template\n div(**attrs) do\n # Both wrappers are display: contents so the button is a flex item of\n # the toolbar, as shadcn's trigger (inline-flex blockifies to flex).\n render RubyUI::DropdownMenu.new(class: \"contents\") do\n render RubyUI::DropdownMenuTrigger.new do\n render RubyUI::Button.new(variant: :outline, size: :sm) do\n columns_icon\n plain @label\n chevron_down_icon\n end\n end\n render RubyUI::DropdownMenuContent.new(align: :end, class: \"w-32\") do\n @columns.each do |col|\n render RubyUI::DropdownMenuCheckboxItem.new(\n checked: col.fetch(:visible, true),\n class: \"capitalize\",\n data: {column_key: col[:key].to_s}\n ) { plain col[:label] }\n end\n end\n end\n end\n end\n\n private\n\n # lucide columns-3, as shadcn's trigger (data-icon tightens the button's padding)\n def columns_icon\n svg(xmlns: \"http://www.w3.org/2000/svg\", viewBox: \"0 0 24 24\", fill: \"none\", stroke: \"currentColor\",\n stroke_width: \"2\", stroke_linecap: \"round\", stroke_linejoin: \"round\", aria_hidden: \"true\",\n data: {icon: \"inline-start\"}) do |s|\n s.rect(width: \"18\", height: \"18\", x: \"3\", y: \"3\", rx: \"2\")\n s.path(d: \"M9 3v18\")\n s.path(d: \"M15 3v18\")\n end\n end\n\n # lucide chevron-down\n def chevron_down_icon\n svg(xmlns: \"http://www.w3.org/2000/svg\", viewBox: \"0 0 24 24\", fill: \"none\", stroke: \"currentColor\",\n stroke_width: \"2\", stroke_linecap: \"round\", stroke_linejoin: \"round\", aria_hidden: \"true\",\n data: {icon: \"inline-end\"}) do |s|\n s.path(d: \"m6 9 6 6 6-6\")\n end\n end\n\n def default_attrs\n {\n class: \"contents\",\n data: {\n controller: \"ruby-ui--data-table-column-visibility\",\n action: \"ruby-ui--dropdown-menu:change->ruby-ui--data-table-column-visibility#toggle\"\n }\n }\n end\n end\nend\n" }, { "path": "data_table_column_visibility_controller.js", @@ -84,7 +84,7 @@ }, { "path": "data_table_toolbar.rb", - "content": "# frozen_string_literal: true\n\nmodule RubyUI\n class DataTableToolbar < Base\n def view_template(&)\n div(**attrs, &)\n end\n\n private\n\n def default_attrs\n {class: \"flex items-center justify-between gap-2\"}\n end\n end\nend\n" + "content": "# frozen_string_literal: true\n\nmodule RubyUI\n class DataTableToolbar < Base\n def view_template(&)\n div(**attrs, &)\n end\n\n private\n\n def default_attrs\n # mb-6 overrides DataTable's space-y-4 (zero specificity) below the\n # toolbar: the dashboard-01 block puts 24px (gap-6) between the toolbar\n # and the table, 16px between the table and the pagination.\n {class: \"mb-6 flex items-center justify-between gap-2\"}\n end\n end\nend\n" } ], "dependencies": {