Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions audit/component_map/table.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
shadcn: table
shadcn_sha: 8a3536678a528cc012b9e62484290e79d0be410f
kind: static
docs_view: table
# RubyUI::Table renders the scroll container <div> (data-slot="table-container")
# around the <table>, 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: {}
23 changes: 23 additions & 0 deletions audit/fixtures/table/content.yml
Original file line number Diff line number Diff line change
@@ -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
47 changes: 47 additions & 0 deletions audit/fixtures/table/rubyui.rb
Original file line number Diff line number Diff line change
@@ -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
53 changes: 53 additions & 0 deletions audit/fixtures/table/shadcn.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="w-[640px]">
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((row, i) => (
<TableRow
key={row.invoice}
{...(i === 1 ? { "data-state": "selected", "data-audit-trigger": "" } : {})}
>
<TableCell className="font-medium">{row.invoice}</TableCell>
<TableCell>{row.status}</TableCell>
<TableCell>{row.method}</TableCell>
<TableCell className="text-right">{row.amount}</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={3}>Total</TableCell>
<TableCell className="text-right">$2,500.00</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
)
}
132 changes: 88 additions & 44 deletions docs/app/views/docs/table.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,102 @@

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
end
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
Expand All @@ -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
8 changes: 7 additions & 1 deletion gem/lib/ruby_ui/table/table.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# frozen_string_literal: true

module RubyUI
# A native <table> inside a horizontally scrolling container, like shadcn's
# Table: wide tables scroll instead of wrapping their cells. Attributes go
# to the <table>; 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
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions gem/lib/ruby_ui/table/table_body.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

module RubyUI
class TableBody < Base
include TableSlot

def view_template(&)
tbody(**attrs, &)
end
Expand All @@ -10,6 +12,7 @@ def view_template(&)

def default_attrs
{
data: {slot: default_slot("table-body")}.compact,
class: "[&_tr:last-child]:border-0"
}
end
Expand Down
3 changes: 3 additions & 0 deletions gem/lib/ruby_ui/table/table_caption.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

module RubyUI
class TableCaption < Base
include TableSlot

def view_template(&)
caption(**attrs, &)
end
Expand All @@ -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
Expand Down
Loading
Loading