diff --git a/audit/component_map/table.yml b/audit/component_map/table.yml
new file mode 100644
index 000000000..9e3e60433
--- /dev/null
+++ b/audit/component_map/table.yml
@@ -0,0 +1,27 @@
+shadcn: table
+shadcn_sha: 8a3536678a528cc012b9e62484290e79d0be410f
+kind: static
+docs_view: table
+# RubyUI::Table renders the scroll container
(data-slot="table-container")
+# around the
, as shadcn's Table does.
+parts:
+ Table: table
+ TableContainer: table-container
+ TableHeader: table-header
+ TableBody: table-body
+ TableFooter: table-footer
+ TableRow: table-row
+ TableHead: table-head
+ TableCell: table-cell
+ TableCaption: table-caption
+selectors:
+ TableContainer: '[data-slot="table-container"]'
+ Table: '[data-slot="table"]'
+ TableHeader: '[data-slot="table-header"]'
+ TableBody: '[data-slot="table-body"]'
+ TableFooter: '[data-slot="table-footer"]'
+ TableRow: '[data-slot="table-row"]'
+ TableHead: '[data-slot="table-head"]'
+ TableCell: '[data-slot="table-cell"]'
+ TableCaption: '[data-slot="table-caption"]'
+props: {}
diff --git a/audit/fixtures/table/content.yml b/audit/fixtures/table/content.yml
new file mode 100644
index 000000000..f190bc6cd
--- /dev/null
+++ b/audit/fixtures/table/content.yml
@@ -0,0 +1,23 @@
+# The shadcn Base UI "With Footer" table example (bases/base/examples/table-example.tsx):
+# caption, header, three body rows, footer. The second body row is selected
+# (data-state="selected") and carries data-audit-trigger (static kind: only the
+# closed state is collected).
+caption: A list of your recent invoices.
+head_1: Invoice
+head_2: Status
+head_3: Method
+head_4: Amount
+row_1_invoice: INV001
+row_1_status: Paid
+row_1_method: Credit Card
+row_1_amount: $250.00
+row_2_invoice: INV002
+row_2_status: Pending
+row_2_method: PayPal
+row_2_amount: $150.00
+row_3_invoice: INV003
+row_3_status: Unpaid
+row_3_method: Bank Transfer
+row_3_amount: $350.00
+footer_label: Total
+footer_amount: $2,500.00
diff --git a/audit/fixtures/table/rubyui.rb b/audit/fixtures/table/rubyui.rb
new file mode 100644
index 000000000..c5fc651cc
--- /dev/null
+++ b/audit/fixtures/table/rubyui.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module ShadcnAuditFixtures
+ # RubyUI side of audit/fixtures/table (strings in content.yml).
+ class Table < Phlex::HTML
+ include RubyUI
+
+ INVOICES = [
+ ["INV001", "Paid", "Credit Card", "$250.00"],
+ ["INV002", "Pending", "PayPal", "$150.00"],
+ ["INV003", "Unpaid", "Bank Transfer", "$350.00"]
+ ].freeze
+
+ def view_template
+ div(class: "w-[640px]") do
+ Table do
+ TableCaption { "A list of your recent invoices." }
+ TableHeader do
+ TableRow do
+ TableHead(class: "w-[100px]") { "Invoice" }
+ TableHead { "Status" }
+ TableHead { "Method" }
+ TableHead(class: "text-right") { "Amount" }
+ end
+ end
+ TableBody do
+ INVOICES.each_with_index do |(invoice, status, method, amount), i|
+ row_attrs = (i == 1) ? {data: {state: "selected", audit_trigger: true}} : {}
+ TableRow(**row_attrs) do
+ TableCell(class: "font-medium") { invoice }
+ TableCell { status }
+ TableCell { method }
+ TableCell(class: "text-right") { amount }
+ end
+ end
+ end
+ TableFooter do
+ TableRow do
+ TableCell(colspan: 3) { "Total" }
+ TableCell(class: "text-right") { "$2,500.00" }
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/audit/fixtures/table/shadcn.tsx b/audit/fixtures/table/shadcn.tsx
new file mode 100644
index 000000000..c9f35505a
--- /dev/null
+++ b/audit/fixtures/table/shadcn.tsx
@@ -0,0 +1,53 @@
+import {
+ Table,
+ TableBody,
+ TableCaption,
+ TableCell,
+ TableFooter,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+
+const invoices = [
+ { invoice: "INV001", status: "Paid", method: "Credit Card", amount: "$250.00" },
+ { invoice: "INV002", status: "Pending", method: "PayPal", amount: "$150.00" },
+ { invoice: "INV003", status: "Unpaid", method: "Bank Transfer", amount: "$350.00" },
+]
+
+export default function TableFixture() {
+ return (
+
+
+ A list of your recent invoices.
+
+
+ Invoice
+ Status
+ Method
+ Amount
+
+
+
+ {invoices.map((row, i) => (
+
+ {row.invoice}
+ {row.status}
+ {row.method}
+ {row.amount}
+
+ ))}
+
+
+
+ Total
+ $2,500.00
+
+
+
+
+ )
+}
diff --git a/docs/app/views/docs/table.rb b/docs/app/views/docs/table.rb
index ad0d22c74..405c38130 100644
--- a/docs/app/views/docs/table.rb
+++ b/docs/app/views/docs/table.rb
@@ -2,33 +2,32 @@
class Views::Docs::Table < Views::Base
Invoice = Struct.new(:identifier, :status, :method, :amount)
- User = Struct.new(:avatar_url, :name, :username, :commits, :github_url)
def view_template
component = "Table"
- div(class: "max-w-2xl mx-auto w-full py-10 space-y-8") do
+ div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
render Docs::Header.new(title: "Table", description: "A responsive table component.")
Heading(level: 2) { "Usage" }
- render Docs::VisualCodeExample.new(title: "Without builder", context: self) do
+ render Docs::VisualCodeExample.new(title: "With footer", description: "Cells don't wrap: when the table is wider than its container, the container scrolls horizontally.", context: self) do
<<~RUBY
Table do
- TableCaption { "Employees at Acme inc." }
+ TableCaption { "A list of your recent invoices." }
TableHeader do
TableRow do
- TableHead { "Name" }
- TableHead { "Email" }
+ TableHead(class: "w-[100px]") { "Invoice" }
TableHead { "Status" }
- TableHead(class: "text-right") { "Role" }
+ TableHead { "Method" }
+ TableHead(class: "text-right") { "Amount" }
end
end
TableBody do
invoices.each do |invoice|
TableRow do
- TableCell(class: 'font-medium') { invoice.identifier }
- TableCell { render_status_badge(invoice.status) }
+ TableCell(class: "font-medium") { invoice.identifier }
+ TableCell { invoice.status }
TableCell { invoice.method }
TableCell(class: "text-right") { format_amount(invoice.amount) }
end
@@ -36,8 +35,69 @@ def view_template
end
TableFooter do
TableRow do
- TableHead(class: "font-medium", colspan: 3) { "Total" }
- TableHead(class: "font-medium text-right") { format_amount(invoices.sum(&:amount)) }
+ TableCell(colspan: 3) { "Total" }
+ TableCell(class: "text-right") { format_amount(invoices.sum(&:amount)) }
+ end
+ end
+ end
+ RUBY
+ end
+
+ render Docs::VisualCodeExample.new(title: "With badges", context: self) do
+ <<~RUBY
+ Table do
+ TableHeader do
+ TableRow do
+ TableHead { "Invoice" }
+ TableHead { "Status" }
+ TableHead(class: "text-right") { "Amount" }
+ end
+ end
+ TableBody do
+ invoices.each do |invoice|
+ TableRow do
+ TableCell(class: "font-medium") { invoice.identifier }
+ TableCell { render_status_badge(invoice.status) }
+ TableCell(class: "text-right") { format_amount(invoice.amount) }
+ end
+ end
+ end
+ end
+ RUBY
+ end
+
+ render Docs::VisualCodeExample.new(title: "With actions", description: "A row stays highlighted while a menu inside it is open (anything in it with aria-expanded=true).", context: self) do
+ <<~RUBY
+ Table do
+ TableHeader do
+ TableRow do
+ TableHead { "Product" }
+ TableHead { "Price" }
+ TableHead(class: "text-right") { "Actions" }
+ end
+ end
+ TableBody do
+ [["Wireless Mouse", "$29.99"], ["Mechanical Keyboard", "$129.99"], ["USB-C Hub", "$49.99"]].each do |product, price|
+ TableRow do
+ TableCell(class: "font-medium") { product }
+ TableCell { price }
+ TableCell(class: "text-right") do
+ DropdownMenu do
+ DropdownMenuTrigger do
+ Button(variant: :ghost, size: :icon, class: "size-8") do
+ more_icon
+ span(class: "sr-only") { "Open menu" }
+ end
+ end
+ DropdownMenuContent(align: :end) do
+ DropdownMenuItem { "Edit" }
+ DropdownMenuItem { "Duplicate" }
+ DropdownMenuSeparator()
+ DropdownMenuItem(variant: :destructive) { "Delete" }
+ end
+ end
+ end
+ end
end
end
end
@@ -54,49 +114,33 @@ def view_template
def invoices
[
- Invoice.new(identifier: "INV-0001", status: "Active", method: "Credit Card", amount: 100),
- Invoice.new(identifier: "INV-0002", status: "Active", method: "Bank Transfer", amount: 230),
- Invoice.new(identifier: "INV-0003", status: "Pending", method: "PayPal", amount: 350),
- Invoice.new(identifier: "INV-0004", status: "Inactive", method: "Credit Card", amount: 100)
- ]
- end
-
- def users
- [
- User.new(avatar_url: "https://avatars.githubusercontent.com/u/246692?v=4", name: "Joel Drapper", username: "joeldrapper", commits: 404),
- User.new(avatar_url: "https://avatars.githubusercontent.com/u/33979976?v=4", name: "Alexandre Ruban", username: "alexandreruban", commits: 16),
- User.new(avatar_url: "https://avatars.githubusercontent.com/u/77887?v=4", name: "Will Cosgrove", username: "willcosgrove", commits: 12),
- User.new(avatar_url: "https://avatars.githubusercontent.com/u/3025661?v=4", name: "Stephann V.", username: "stephannv", commits: 8),
- User.new(avatar_url: "https://avatars.githubusercontent.com/u/6411752?v=4", name: "Marco Roth", username: "marcoroth", commits: 8)
+ Invoice.new(identifier: "INV001", status: "Paid", method: "Credit Card", amount: 250),
+ Invoice.new(identifier: "INV002", status: "Pending", method: "PayPal", amount: 150),
+ Invoice.new(identifier: "INV003", status: "Unpaid", method: "Bank Transfer", amount: 350)
]
end
- def github_link(user)
- "https://github.com/#{user.username}"
- end
-
- def github_icon
- svg(viewbox: "0 0 438.549 438.549", class: "h-4 w-4") do |s|
- s.path(
- fill: "currentColor",
- d:
- "M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
- )
- end
- end
-
def format_amount(amount)
"$#{amount}.00"
end
def render_status_badge(status)
- case status.downcase
- when "active"
+ case status
+ when "Paid"
Badge(size: :sm, class: "bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300") { status }
- when "inactive"
- Badge(variant: :destructive, size: :sm) { status }
- when "pending"
+ when "Pending"
Badge(size: :sm, class: "bg-amber-50 text-amber-700 dark:bg-amber-950 dark:text-amber-300") { status }
+ else
+ Badge(variant: :destructive, size: :sm) { status }
+ end
+ end
+
+ def more_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|
+ s.circle(cx: "12", cy: "12", r: "1")
+ s.circle(cx: "19", cy: "12", r: "1")
+ s.circle(cx: "5", cy: "12", r: "1")
end
end
end
diff --git a/gem/lib/ruby_ui/table/table.rb b/gem/lib/ruby_ui/table/table.rb
index d3f2c3730..08fd9880d 100644
--- a/gem/lib/ruby_ui/table/table.rb
+++ b/gem/lib/ruby_ui/table/table.rb
@@ -1,9 +1,14 @@
# frozen_string_literal: true
module RubyUI
+ # A native inside a horizontally scrolling container, like shadcn's
+ # Table: wide tables scroll instead of wrapping their cells. Attributes go
+ # to the ; the wrapper is data-slot="table-container".
class Table < Base
+ include TableSlot
+
def view_template(&block)
- div(class: "relative w-full overflow-auto") do
+ div(data: {slot: "table-container"}, class: "relative w-full overflow-x-auto") do
table(**attrs, &block)
end
end
@@ -12,6 +17,7 @@ def view_template(&block)
def default_attrs
{
+ data: {slot: default_slot("table")}.compact,
class: "w-full caption-bottom text-sm"
}
end
diff --git a/gem/lib/ruby_ui/table/table_body.rb b/gem/lib/ruby_ui/table/table_body.rb
index 84891e205..1ebff8dbc 100644
--- a/gem/lib/ruby_ui/table/table_body.rb
+++ b/gem/lib/ruby_ui/table/table_body.rb
@@ -2,6 +2,8 @@
module RubyUI
class TableBody < Base
+ include TableSlot
+
def view_template(&)
tbody(**attrs, &)
end
@@ -10,6 +12,7 @@ def view_template(&)
def default_attrs
{
+ data: {slot: default_slot("table-body")}.compact,
class: "[&_tr:last-child]:border-0"
}
end
diff --git a/gem/lib/ruby_ui/table/table_caption.rb b/gem/lib/ruby_ui/table/table_caption.rb
index 21335a8b2..9611a3906 100644
--- a/gem/lib/ruby_ui/table/table_caption.rb
+++ b/gem/lib/ruby_ui/table/table_caption.rb
@@ -2,6 +2,8 @@
module RubyUI
class TableCaption < Base
+ include TableSlot
+
def view_template(&)
caption(**attrs, &)
end
@@ -10,6 +12,7 @@ def view_template(&)
def default_attrs
{
+ data: {slot: default_slot("table-caption")}.compact,
class: "mt-4 text-sm text-muted-foreground"
}
end
diff --git a/gem/lib/ruby_ui/table/table_cell.rb b/gem/lib/ruby_ui/table/table_cell.rb
index 6d7e35391..dd7bf6a7f 100644
--- a/gem/lib/ruby_ui/table/table_cell.rb
+++ b/gem/lib/ruby_ui/table/table_cell.rb
@@ -2,6 +2,8 @@
module RubyUI
class TableCell < Base
+ include TableSlot
+
def view_template(&)
td(**attrs, &)
end
@@ -10,7 +12,8 @@ def view_template(&)
def default_attrs
{
- class: "p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]"
+ data: {slot: default_slot("table-cell")}.compact,
+ class: "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&:has([data-slot=checkbox])]:pr-0"
}
end
end
diff --git a/gem/lib/ruby_ui/table/table_docs.rb b/gem/lib/ruby_ui/table/table_docs.rb
index ad0d22c74..405c38130 100644
--- a/gem/lib/ruby_ui/table/table_docs.rb
+++ b/gem/lib/ruby_ui/table/table_docs.rb
@@ -2,33 +2,32 @@
class Views::Docs::Table < Views::Base
Invoice = Struct.new(:identifier, :status, :method, :amount)
- User = Struct.new(:avatar_url, :name, :username, :commits, :github_url)
def view_template
component = "Table"
- div(class: "max-w-2xl mx-auto w-full py-10 space-y-8") do
+ div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
render Docs::Header.new(title: "Table", description: "A responsive table component.")
Heading(level: 2) { "Usage" }
- render Docs::VisualCodeExample.new(title: "Without builder", context: self) do
+ render Docs::VisualCodeExample.new(title: "With footer", description: "Cells don't wrap: when the table is wider than its container, the container scrolls horizontally.", context: self) do
<<~RUBY
Table do
- TableCaption { "Employees at Acme inc." }
+ TableCaption { "A list of your recent invoices." }
TableHeader do
TableRow do
- TableHead { "Name" }
- TableHead { "Email" }
+ TableHead(class: "w-[100px]") { "Invoice" }
TableHead { "Status" }
- TableHead(class: "text-right") { "Role" }
+ TableHead { "Method" }
+ TableHead(class: "text-right") { "Amount" }
end
end
TableBody do
invoices.each do |invoice|
TableRow do
- TableCell(class: 'font-medium') { invoice.identifier }
- TableCell { render_status_badge(invoice.status) }
+ TableCell(class: "font-medium") { invoice.identifier }
+ TableCell { invoice.status }
TableCell { invoice.method }
TableCell(class: "text-right") { format_amount(invoice.amount) }
end
@@ -36,8 +35,69 @@ def view_template
end
TableFooter do
TableRow do
- TableHead(class: "font-medium", colspan: 3) { "Total" }
- TableHead(class: "font-medium text-right") { format_amount(invoices.sum(&:amount)) }
+ TableCell(colspan: 3) { "Total" }
+ TableCell(class: "text-right") { format_amount(invoices.sum(&:amount)) }
+ end
+ end
+ end
+ RUBY
+ end
+
+ render Docs::VisualCodeExample.new(title: "With badges", context: self) do
+ <<~RUBY
+ Table do
+ TableHeader do
+ TableRow do
+ TableHead { "Invoice" }
+ TableHead { "Status" }
+ TableHead(class: "text-right") { "Amount" }
+ end
+ end
+ TableBody do
+ invoices.each do |invoice|
+ TableRow do
+ TableCell(class: "font-medium") { invoice.identifier }
+ TableCell { render_status_badge(invoice.status) }
+ TableCell(class: "text-right") { format_amount(invoice.amount) }
+ end
+ end
+ end
+ end
+ RUBY
+ end
+
+ render Docs::VisualCodeExample.new(title: "With actions", description: "A row stays highlighted while a menu inside it is open (anything in it with aria-expanded=true).", context: self) do
+ <<~RUBY
+ Table do
+ TableHeader do
+ TableRow do
+ TableHead { "Product" }
+ TableHead { "Price" }
+ TableHead(class: "text-right") { "Actions" }
+ end
+ end
+ TableBody do
+ [["Wireless Mouse", "$29.99"], ["Mechanical Keyboard", "$129.99"], ["USB-C Hub", "$49.99"]].each do |product, price|
+ TableRow do
+ TableCell(class: "font-medium") { product }
+ TableCell { price }
+ TableCell(class: "text-right") do
+ DropdownMenu do
+ DropdownMenuTrigger do
+ Button(variant: :ghost, size: :icon, class: "size-8") do
+ more_icon
+ span(class: "sr-only") { "Open menu" }
+ end
+ end
+ DropdownMenuContent(align: :end) do
+ DropdownMenuItem { "Edit" }
+ DropdownMenuItem { "Duplicate" }
+ DropdownMenuSeparator()
+ DropdownMenuItem(variant: :destructive) { "Delete" }
+ end
+ end
+ end
+ end
end
end
end
@@ -54,49 +114,33 @@ def view_template
def invoices
[
- Invoice.new(identifier: "INV-0001", status: "Active", method: "Credit Card", amount: 100),
- Invoice.new(identifier: "INV-0002", status: "Active", method: "Bank Transfer", amount: 230),
- Invoice.new(identifier: "INV-0003", status: "Pending", method: "PayPal", amount: 350),
- Invoice.new(identifier: "INV-0004", status: "Inactive", method: "Credit Card", amount: 100)
- ]
- end
-
- def users
- [
- User.new(avatar_url: "https://avatars.githubusercontent.com/u/246692?v=4", name: "Joel Drapper", username: "joeldrapper", commits: 404),
- User.new(avatar_url: "https://avatars.githubusercontent.com/u/33979976?v=4", name: "Alexandre Ruban", username: "alexandreruban", commits: 16),
- User.new(avatar_url: "https://avatars.githubusercontent.com/u/77887?v=4", name: "Will Cosgrove", username: "willcosgrove", commits: 12),
- User.new(avatar_url: "https://avatars.githubusercontent.com/u/3025661?v=4", name: "Stephann V.", username: "stephannv", commits: 8),
- User.new(avatar_url: "https://avatars.githubusercontent.com/u/6411752?v=4", name: "Marco Roth", username: "marcoroth", commits: 8)
+ Invoice.new(identifier: "INV001", status: "Paid", method: "Credit Card", amount: 250),
+ Invoice.new(identifier: "INV002", status: "Pending", method: "PayPal", amount: 150),
+ Invoice.new(identifier: "INV003", status: "Unpaid", method: "Bank Transfer", amount: 350)
]
end
- def github_link(user)
- "https://github.com/#{user.username}"
- end
-
- def github_icon
- svg(viewbox: "0 0 438.549 438.549", class: "h-4 w-4") do |s|
- s.path(
- fill: "currentColor",
- d:
- "M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
- )
- end
- end
-
def format_amount(amount)
"$#{amount}.00"
end
def render_status_badge(status)
- case status.downcase
- when "active"
+ case status
+ when "Paid"
Badge(size: :sm, class: "bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300") { status }
- when "inactive"
- Badge(variant: :destructive, size: :sm) { status }
- when "pending"
+ when "Pending"
Badge(size: :sm, class: "bg-amber-50 text-amber-700 dark:bg-amber-950 dark:text-amber-300") { status }
+ else
+ Badge(variant: :destructive, size: :sm) { status }
+ end
+ end
+
+ def more_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|
+ s.circle(cx: "12", cy: "12", r: "1")
+ s.circle(cx: "19", cy: "12", r: "1")
+ s.circle(cx: "5", cy: "12", r: "1")
end
end
end
diff --git a/gem/lib/ruby_ui/table/table_footer.rb b/gem/lib/ruby_ui/table/table_footer.rb
index 24bf71ac1..020bdd1f7 100644
--- a/gem/lib/ruby_ui/table/table_footer.rb
+++ b/gem/lib/ruby_ui/table/table_footer.rb
@@ -2,6 +2,8 @@
module RubyUI
class TableFooter < Base
+ include TableSlot
+
def view_template(&)
tfoot(**attrs, &)
end
@@ -10,7 +12,8 @@ def view_template(&)
def default_attrs
{
- class: "border-t bg-muted/50 font-medium[& amp;>tr]:last:border-b-0"
+ data: {slot: default_slot("table-footer")}.compact,
+ class: "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0"
}
end
end
diff --git a/gem/lib/ruby_ui/table/table_head.rb b/gem/lib/ruby_ui/table/table_head.rb
index c0c8d0e48..b17f4e308 100644
--- a/gem/lib/ruby_ui/table/table_head.rb
+++ b/gem/lib/ruby_ui/table/table_head.rb
@@ -2,6 +2,8 @@
module RubyUI
class TableHead < Base
+ include TableSlot
+
def view_template(&)
th(**attrs, &)
end
@@ -10,7 +12,8 @@ def view_template(&)
def default_attrs
{
- class: "h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]"
+ data: {slot: default_slot("table-head")}.compact,
+ class: "h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0 [&:has([data-slot=checkbox])]:pr-0"
}
end
end
diff --git a/gem/lib/ruby_ui/table/table_header.rb b/gem/lib/ruby_ui/table/table_header.rb
index 5d632e8c8..0d3e27e65 100644
--- a/gem/lib/ruby_ui/table/table_header.rb
+++ b/gem/lib/ruby_ui/table/table_header.rb
@@ -2,6 +2,8 @@
module RubyUI
class TableHeader < Base
+ include TableSlot
+
def view_template(&)
thead(**attrs, &)
end
@@ -10,6 +12,7 @@ def view_template(&)
def default_attrs
{
+ data: {slot: default_slot("table-header")}.compact,
class: "[&_tr]:border-b"
}
end
diff --git a/gem/lib/ruby_ui/table/table_row.rb b/gem/lib/ruby_ui/table/table_row.rb
index d1eeb2b4f..98e88fb29 100644
--- a/gem/lib/ruby_ui/table/table_row.rb
+++ b/gem/lib/ruby_ui/table/table_row.rb
@@ -2,6 +2,8 @@
module RubyUI
class TableRow < Base
+ include TableSlot
+
def view_template(&)
tr(**attrs, &)
end
@@ -10,7 +12,8 @@ def view_template(&)
def default_attrs
{
- class: "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted"
+ data: {slot: default_slot("table-row")}.compact,
+ class: "border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted"
}
end
end
diff --git a/gem/lib/ruby_ui/table/table_slot.rb b/gem/lib/ruby_ui/table/table_slot.rb
new file mode 100644
index 000000000..1b0a5401e
--- /dev/null
+++ b/gem/lib/ruby_ui/table/table_slot.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module RubyUI
+ # shadcn spreads props after data-slot, so a caller's slot replaces a Table
+ # part's default instead of Base#mix joining both values (as in Input).
+ module TableSlot
+ SLOT_ATTRS = [:data_slot, :"data-slot", "data-slot", "data_slot"].freeze
+ SLOT_KEYS = [:slot, :slot!, "slot", "slot!"].freeze
+
+ def initialize(**attrs)
+ @caller_slot = caller_slot?(attrs)
+ super
+ end
+
+ private
+
+ def caller_slot?(attrs)
+ return true if attrs.key?(:data!) || SLOT_ATTRS.any? { |key| attrs.key?(key) }
+
+ data = attrs[:data] || attrs["data"]
+ data.is_a?(Hash) && SLOT_KEYS.any? { |key| data.key?(key) }
+ end
+
+ # The part's own slot, or nil when the caller passed one.
+ def default_slot(name)
+ @caller_slot ? nil : name
+ end
+ end
+end
diff --git a/gem/test/ruby_ui/table_test.rb b/gem/test/ruby_ui/table_test.rb
index 2ae1e4ba6..3837078f8 100644
--- a/gem/test/ruby_ui/table_test.rb
+++ b/gem/test/ruby_ui/table_test.rb
@@ -34,8 +34,8 @@ def test_render_with_all_items
end
RubyUI.TableFooter do
RubyUI.TableRow do
- RubyUI.TableHead(class: "font-medium", colspan: 3) { "Total" }
- RubyUI.TableHead(class: "font-medium text-right") { invoices.sum { |invoice| invoice[:amount] } }
+ RubyUI.TableCell(colspan: 3) { "Total" }
+ RubyUI.TableCell(class: "text-right") { invoices.sum { |invoice| invoice[:amount] } }
end
end
end
@@ -43,4 +43,75 @@ def test_render_with_all_items
assert_match(/Total/, output)
end
+
+ def test_every_part_has_its_data_slot
+ output = phlex do
+ RubyUI.Table do
+ RubyUI.TableCaption { "Caption" }
+ RubyUI.TableHeader { RubyUI.TableRow { RubyUI.TableHead { "Head" } } }
+ RubyUI.TableBody { RubyUI.TableRow { RubyUI.TableCell { "Cell" } } }
+ RubyUI.TableFooter { RubyUI.TableRow { RubyUI.TableCell { "Foot" } } }
+ end
+ end
+
+ {
+ "table-container" => "div", "table" => "table", "table-caption" => "caption",
+ "table-header" => "thead", "table-body" => "tbody", "table-footer" => "tfoot",
+ "table-row" => "tr", "table-head" => "th", "table-cell" => "td"
+ }.each do |slot, tag|
+ assert_match(/<#{tag}\b[^>]*\bdata-slot="#{slot}"/, output, "#{slot} on <#{tag}>")
+ end
+ end
+
+ def test_container_scrolls_horizontally_and_attrs_go_to_the_table
+ output = phlex { RubyUI.Table(id: "invoices", class: "min-w-96") { "" } }
+
+ assert_match(//, output)
+ assert_match(/
]*id="invoices"/, output)
+ assert_match(/]*class="[^"]*min-w-96/, output)
+ end
+
+ def test_footer_classes_are_valid_tokens
+ output = phlex { RubyUI.TableFooter { "" } }
+
+ assert_match(%r{class="border-t bg-muted/50 font-medium \[&>tr\]:last:border-b-0"}, output)
+ end
+
+ def test_head_and_cell_do_not_wrap
+ head = phlex { RubyUI.TableHead { "Head" } }
+ cell = phlex { RubyUI.TableCell { "Cell" } }
+
+ assert_match(/whitespace-nowrap/, head)
+ assert_match(/text-foreground/, head)
+ refute_match(/text-muted-foreground/, head)
+ assert_match(/whitespace-nowrap/, cell)
+ [head, cell].each { |output| refute_match(/translate-y/, output) }
+ end
+
+ def test_row_is_highlighted_while_a_control_inside_is_expanded
+ output = phlex { RubyUI.TableRow { "" } }
+
+ assert_match(%r{has-aria-expanded:bg-muted/50}, output)
+ assert_match(/data-\[state=selected\]:bg-muted/, output)
+ end
+
+ def test_a_caller_slot_replaces_the_default
+ {
+ RubyUI::Table => "table", RubyUI::TableHeader => "thead", RubyUI::TableBody => "tbody",
+ RubyUI::TableFooter => "tfoot", RubyUI::TableRow => "tr", RubyUI::TableHead => "th",
+ RubyUI::TableCell => "td", RubyUI::TableCaption => "caption"
+ }.each do |klass, tag|
+ output = phlex { RubyUI.public_send(klass.name.split("::").last, data: {slot: "custom", column: "email"}) { "x" } }
+
+ assert_match(/<#{tag}\b[^>]*data-slot="custom"/, output, klass.name)
+ assert_match(/data-column="email"/, output, klass.name)
+ end
+ assert_match(/data-slot="table-container"/, phlex { RubyUI.Table(data: {slot: "custom"}) { "" } })
+ end
+
+ def test_checkbox_columns_drop_the_right_padding
+ [phlex { RubyUI.TableHead { "" } }, phlex { RubyUI.TableCell { "" } }].each do |output|
+ assert_match(/\[&:has\(\[data-slot=checkbox\]\)\]:pr-0|\[&:has\(\[data-slot=checkbox\]\)\]:pr-0/, output)
+ end
+ end
end
diff --git a/mcp/data/registry/components/table.json b/mcp/data/registry/components/table.json
index a3f64c3da..14592e54c 100644
--- a/mcp/data/registry/components/table.json
+++ b/mcp/data/registry/components/table.json
@@ -4,35 +4,39 @@
"files": [
{
"path": "table.rb",
- "content": "# frozen_string_literal: true\n\nmodule RubyUI\n class Table < Base\n def view_template(&block)\n div(class: \"relative w-full overflow-auto\") do\n table(**attrs, &block)\n end\n end\n\n private\n\n def default_attrs\n {\n class: \"w-full caption-bottom text-sm\"\n }\n end\n end\nend\n"
+ "content": "# frozen_string_literal: true\n\nmodule RubyUI\n # A native inside a horizontally scrolling container, like shadcn's\n # Table: wide tables scroll instead of wrapping their cells. Attributes go\n # to the ; the wrapper is data-slot=\"table-container\".\n class Table < Base\n include TableSlot\n\n def view_template(&block)\n div(data: {slot: \"table-container\"}, class: \"relative w-full overflow-x-auto\") do\n table(**attrs, &block)\n end\n end\n\n private\n\n def default_attrs\n {\n data: {slot: default_slot(\"table\")}.compact,\n class: \"w-full caption-bottom text-sm\"\n }\n end\n end\nend\n"
},
{
"path": "table_body.rb",
- "content": "# frozen_string_literal: true\n\nmodule RubyUI\n class TableBody < Base\n def view_template(&)\n tbody(**attrs, &)\n end\n\n private\n\n def default_attrs\n {\n class: \"[&_tr:last-child]:border-0\"\n }\n end\n end\nend\n"
+ "content": "# frozen_string_literal: true\n\nmodule RubyUI\n class TableBody < Base\n include TableSlot\n\n def view_template(&)\n tbody(**attrs, &)\n end\n\n private\n\n def default_attrs\n {\n data: {slot: default_slot(\"table-body\")}.compact,\n class: \"[&_tr:last-child]:border-0\"\n }\n end\n end\nend\n"
},
{
"path": "table_caption.rb",
- "content": "# frozen_string_literal: true\n\nmodule RubyUI\n class TableCaption < Base\n def view_template(&)\n caption(**attrs, &)\n end\n\n private\n\n def default_attrs\n {\n class: \"mt-4 text-sm text-muted-foreground\"\n }\n end\n end\nend\n"
+ "content": "# frozen_string_literal: true\n\nmodule RubyUI\n class TableCaption < Base\n include TableSlot\n\n def view_template(&)\n caption(**attrs, &)\n end\n\n private\n\n def default_attrs\n {\n data: {slot: default_slot(\"table-caption\")}.compact,\n class: \"mt-4 text-sm text-muted-foreground\"\n }\n end\n end\nend\n"
},
{
"path": "table_cell.rb",
- "content": "# frozen_string_literal: true\n\nmodule RubyUI\n class TableCell < Base\n def view_template(&)\n td(**attrs, &)\n end\n\n private\n\n def default_attrs\n {\n class: \"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\"\n }\n end\n end\nend\n"
+ "content": "# frozen_string_literal: true\n\nmodule RubyUI\n class TableCell < Base\n include TableSlot\n\n def view_template(&)\n td(**attrs, &)\n end\n\n private\n\n def default_attrs\n {\n data: {slot: default_slot(\"table-cell\")}.compact,\n class: \"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&:has([data-slot=checkbox])]:pr-0\"\n }\n end\n end\nend\n"
},
{
"path": "table_footer.rb",
- "content": "# frozen_string_literal: true\n\nmodule RubyUI\n class TableFooter < Base\n def view_template(&)\n tfoot(**attrs, &)\n end\n\n private\n\n def default_attrs\n {\n class: \"border-t bg-muted/50 font-medium[& amp;>tr]:last:border-b-0\"\n }\n end\n end\nend\n"
+ "content": "# frozen_string_literal: true\n\nmodule RubyUI\n class TableFooter < Base\n include TableSlot\n\n def view_template(&)\n tfoot(**attrs, &)\n end\n\n private\n\n def default_attrs\n {\n data: {slot: default_slot(\"table-footer\")}.compact,\n class: \"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0\"\n }\n end\n end\nend\n"
},
{
"path": "table_head.rb",
- "content": "# frozen_string_literal: true\n\nmodule RubyUI\n class TableHead < Base\n def view_template(&)\n th(**attrs, &)\n end\n\n private\n\n def default_attrs\n {\n class: \"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\"\n }\n end\n end\nend\n"
+ "content": "# frozen_string_literal: true\n\nmodule RubyUI\n class TableHead < Base\n include TableSlot\n\n def view_template(&)\n th(**attrs, &)\n end\n\n private\n\n def default_attrs\n {\n data: {slot: default_slot(\"table-head\")}.compact,\n class: \"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0 [&:has([data-slot=checkbox])]:pr-0\"\n }\n end\n end\nend\n"
},
{
"path": "table_header.rb",
- "content": "# frozen_string_literal: true\n\nmodule RubyUI\n class TableHeader < Base\n def view_template(&)\n thead(**attrs, &)\n end\n\n private\n\n def default_attrs\n {\n class: \"[&_tr]:border-b\"\n }\n end\n end\nend\n"
+ "content": "# frozen_string_literal: true\n\nmodule RubyUI\n class TableHeader < Base\n include TableSlot\n\n def view_template(&)\n thead(**attrs, &)\n end\n\n private\n\n def default_attrs\n {\n data: {slot: default_slot(\"table-header\")}.compact,\n class: \"[&_tr]:border-b\"\n }\n end\n end\nend\n"
},
{
"path": "table_row.rb",
- "content": "# frozen_string_literal: true\n\nmodule RubyUI\n class TableRow < Base\n def view_template(&)\n tr(**attrs, &)\n end\n\n private\n\n def default_attrs\n {\n class: \"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted\"\n }\n end\n end\nend\n"
+ "content": "# frozen_string_literal: true\n\nmodule RubyUI\n class TableRow < Base\n include TableSlot\n\n def view_template(&)\n tr(**attrs, &)\n end\n\n private\n\n def default_attrs\n {\n data: {slot: default_slot(\"table-row\")}.compact,\n class: \"border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted\"\n }\n end\n end\nend\n"
+ },
+ {
+ "path": "table_slot.rb",
+ "content": "# frozen_string_literal: true\n\nmodule RubyUI\n # shadcn spreads props after data-slot, so a caller's slot replaces a Table\n # part's default instead of Base#mix joining both values (as in Input).\n module TableSlot\n SLOT_ATTRS = [:data_slot, :\"data-slot\", \"data-slot\", \"data_slot\"].freeze\n SLOT_KEYS = [:slot, :slot!, \"slot\", \"slot!\"].freeze\n\n def initialize(**attrs)\n @caller_slot = caller_slot?(attrs)\n super\n end\n\n private\n\n def caller_slot?(attrs)\n return true if attrs.key?(:data!) || SLOT_ATTRS.any? { |key| attrs.key?(key) }\n\n data = attrs[:data] || attrs[\"data\"]\n data.is_a?(Hash) && SLOT_KEYS.any? { |key| data.key?(key) }\n end\n\n # The part's own slot, or nil when the caller passed one.\n def default_slot(name)\n @caller_slot ? nil : name\n end\n end\nend\n"
}
],
"dependencies": {
@@ -41,11 +45,21 @@
"gems": []
},
"install_command": "rails g ruby_ui:component Table",
- "docs_markdown": "# Table\n\nA responsive table component.\n\n## Usage\n\n### Without builder\n\n```ruby\nTable do\n TableCaption { \"Employees at Acme inc.\" }\n TableHeader do\n TableRow do\n TableHead { \"Name\" }\n TableHead { \"Email\" }\n TableHead { \"Status\" }\n TableHead(class: \"text-right\") { \"Role\" }\n end\n end\n TableBody do\n invoices.each do |invoice|\n TableRow do\n TableCell(class: 'font-medium') { invoice.identifier }\n TableCell { render_status_badge(invoice.status) }\n TableCell { invoice.method }\n TableCell(class: \"text-right\") { format_amount(invoice.amount) }\n end\n end\n end\n TableFooter do\n TableRow do\n TableHead(class: \"font-medium\", colspan: 3) { \"Total\" }\n TableHead(class: \"font-medium text-right\") { format_amount(invoices.sum(&:amount)) }\n end\n end\nend\n```",
+ "docs_markdown": "# Table\n\nA responsive table component.\n\n## Usage\n\n### With footer\n\n```ruby\nTable do\n TableCaption { \"A list of your recent invoices.\" }\n TableHeader do\n TableRow do\n TableHead(class: \"w-[100px]\") { \"Invoice\" }\n TableHead { \"Status\" }\n TableHead { \"Method\" }\n TableHead(class: \"text-right\") { \"Amount\" }\n end\n end\n TableBody do\n invoices.each do |invoice|\n TableRow do\n TableCell(class: \"font-medium\") { invoice.identifier }\n TableCell { invoice.status }\n TableCell { invoice.method }\n TableCell(class: \"text-right\") { format_amount(invoice.amount) }\n end\n end\n end\n TableFooter do\n TableRow do\n TableCell(colspan: 3) { \"Total\" }\n TableCell(class: \"text-right\") { format_amount(invoices.sum(&:amount)) }\n end\n end\nend\n```\n\n### With badges\n\n```ruby\nTable do\n TableHeader do\n TableRow do\n TableHead { \"Invoice\" }\n TableHead { \"Status\" }\n TableHead(class: \"text-right\") { \"Amount\" }\n end\n end\n TableBody do\n invoices.each do |invoice|\n TableRow do\n TableCell(class: \"font-medium\") { invoice.identifier }\n TableCell { render_status_badge(invoice.status) }\n TableCell(class: \"text-right\") { format_amount(invoice.amount) }\n end\n end\n end\nend\n```\n\n### With actions\n\n```ruby\nTable do\n TableHeader do\n TableRow do\n TableHead { \"Product\" }\n TableHead { \"Price\" }\n TableHead(class: \"text-right\") { \"Actions\" }\n end\n end\n TableBody do\n [[\"Wireless Mouse\", \"$29.99\"], [\"Mechanical Keyboard\", \"$129.99\"], [\"USB-C Hub\", \"$49.99\"]].each do |product, price|\n TableRow do\n TableCell(class: \"font-medium\") { product }\n TableCell { price }\n TableCell(class: \"text-right\") do\n DropdownMenu do\n DropdownMenuTrigger do\n Button(variant: :ghost, size: :icon, class: \"size-8\") do\n more_icon\n span(class: \"sr-only\") { \"Open menu\" }\n end\n end\n DropdownMenuContent(align: :end) do\n DropdownMenuItem { \"Edit\" }\n DropdownMenuItem { \"Duplicate\" }\n DropdownMenuSeparator()\n DropdownMenuItem(variant: :destructive) { \"Delete\" }\n end\n end\n end\n end\n end\n end\nend\n```",
"examples": [
{
- "title": "Without builder",
- "code": "Table do\n TableCaption { \"Employees at Acme inc.\" }\n TableHeader do\n TableRow do\n TableHead { \"Name\" }\n TableHead { \"Email\" }\n TableHead { \"Status\" }\n TableHead(class: \"text-right\") { \"Role\" }\n end\n end\n TableBody do\n invoices.each do |invoice|\n TableRow do\n TableCell(class: 'font-medium') { invoice.identifier }\n TableCell { render_status_badge(invoice.status) }\n TableCell { invoice.method }\n TableCell(class: \"text-right\") { format_amount(invoice.amount) }\n end\n end\n end\n TableFooter do\n TableRow do\n TableHead(class: \"font-medium\", colspan: 3) { \"Total\" }\n TableHead(class: \"font-medium text-right\") { format_amount(invoices.sum(&:amount)) }\n end\n end\nend\n",
+ "title": "With footer",
+ "code": "Table do\n TableCaption { \"A list of your recent invoices.\" }\n TableHeader do\n TableRow do\n TableHead(class: \"w-[100px]\") { \"Invoice\" }\n TableHead { \"Status\" }\n TableHead { \"Method\" }\n TableHead(class: \"text-right\") { \"Amount\" }\n end\n end\n TableBody do\n invoices.each do |invoice|\n TableRow do\n TableCell(class: \"font-medium\") { invoice.identifier }\n TableCell { invoice.status }\n TableCell { invoice.method }\n TableCell(class: \"text-right\") { format_amount(invoice.amount) }\n end\n end\n end\n TableFooter do\n TableRow do\n TableCell(colspan: 3) { \"Total\" }\n TableCell(class: \"text-right\") { format_amount(invoices.sum(&:amount)) }\n end\n end\nend\n",
+ "language": "ruby"
+ },
+ {
+ "title": "With badges",
+ "code": "Table do\n TableHeader do\n TableRow do\n TableHead { \"Invoice\" }\n TableHead { \"Status\" }\n TableHead(class: \"text-right\") { \"Amount\" }\n end\n end\n TableBody do\n invoices.each do |invoice|\n TableRow do\n TableCell(class: \"font-medium\") { invoice.identifier }\n TableCell { render_status_badge(invoice.status) }\n TableCell(class: \"text-right\") { format_amount(invoice.amount) }\n end\n end\n end\nend\n",
+ "language": "ruby"
+ },
+ {
+ "title": "With actions",
+ "code": "Table do\n TableHeader do\n TableRow do\n TableHead { \"Product\" }\n TableHead { \"Price\" }\n TableHead(class: \"text-right\") { \"Actions\" }\n end\n end\n TableBody do\n [[\"Wireless Mouse\", \"$29.99\"], [\"Mechanical Keyboard\", \"$129.99\"], [\"USB-C Hub\", \"$49.99\"]].each do |product, price|\n TableRow do\n TableCell(class: \"font-medium\") { product }\n TableCell { price }\n TableCell(class: \"text-right\") do\n DropdownMenu do\n DropdownMenuTrigger do\n Button(variant: :ghost, size: :icon, class: \"size-8\") do\n more_icon\n span(class: \"sr-only\") { \"Open menu\" }\n end\n end\n DropdownMenuContent(align: :end) do\n DropdownMenuItem { \"Edit\" }\n DropdownMenuItem { \"Duplicate\" }\n DropdownMenuSeparator()\n DropdownMenuItem(variant: :destructive) { \"Delete\" }\n end\n end\n end\n end\n end\n end\nend\n",
"language": "ruby"
}
]