Skip to content

Commit 21bf2a9

Browse files
author
Brian Casel
committed
sub navigation dropdown and data table element.
1 parent cc3edb5 commit 21bf2a9

5 files changed

Lines changed: 199 additions & 5 deletions

File tree

app/frontend/components/design-system/DesignSystem.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { FormsSection } from "@/components/design-system/sections/elements/Forms
1919
import { BadgesSection } from "@/components/design-system/sections/elements/BadgesSection";
2020
import { ToggleButtonsSection } from "@/components/design-system/sections/elements/ToggleButtonsSection";
2121
import { ListingsSection } from "@/components/design-system/sections/elements/ListingsSection";
22+
import { DataTableSection } from "@/components/design-system/sections/elements/DataTableSection";
2223
import { ModalSection } from "@/components/design-system/sections/elements/ModalSection";
2324
import { CalloutSection } from "@/components/design-system/sections/elements/CalloutSection";
2425
import { DropdownMenuSection } from "@/components/design-system/sections/elements/DropdownMenuSection";
@@ -116,6 +117,7 @@ export function DesignSystem() {
116117
<BadgesSection />
117118
<ToggleButtonsSection />
118119
<ListingsSection />
120+
<DataTableSection />
119121
<ModalSection />
120122
<DropdownMenuSection />
121123
<CalloutSection />

app/frontend/components/design-system/SidebarNav.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const NAV: NavGroup[] = [
3333
{ id: "badges", label: "Badges" },
3434
{ id: "toggle-buttons", label: "Toggle buttons" },
3535
{ id: "listings", label: "Listings" },
36+
{ id: "data-table", label: "Data table" },
3637
{ id: "modal", label: "Modal" },
3738
{ id: "dropdown-menu", label: "Dropdown menu" },
3839
{ id: "callout", label: "Callout" },
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { SectionShell } from "@/components/design-system/SectionShell";
2+
import { Badge } from "@/components/ui/badge";
3+
import { DataTable, DataRow } from "@/components/ui/data-table";
4+
5+
const code = `<DataTable>
6+
<DataRow title="Status">
7+
<Badge tone="accent">Active</Badge>
8+
</DataRow>
9+
<DataRow title="Owner">Marie Chen</DataRow>
10+
<DataRow title="Description">
11+
A longer value can wrap and flow across multiple lines without breaking the
12+
visual rhythm of the table.
13+
</DataRow>
14+
<DataRow title="Updated">May 16, 2026 at 1:27pm</DataRow>
15+
</DataTable>`;
16+
17+
export function DataTableSection() {
18+
return (
19+
<SectionShell
20+
id="data-table"
21+
title="Data table"
22+
description={
23+
<>
24+
A vertical key/value table for displaying record details. Each row
25+
is a horizontal stripe with a title on the left and a value on the
26+
right. Rows stack on mobile; from <code>md</code>, the title takes
27+
1 column and the value spans 3 (out of 4); from <code>xl</code>,
28+
the value spans 4 (out of 5) so wider screens give the value more
29+
room while the title stays the same size.
30+
</>
31+
}
32+
whenToUse={
33+
<ul>
34+
<li>Summarizing the fields of a single record (a video, a profile, a settings panel).</li>
35+
<li>Read-only metadata views where each line is a labeled value.</li>
36+
<li>Anywhere a long stack of <code>&lt;h3&gt;</code> + paragraph blocks is harder to scan than a labeled grid.</li>
37+
</ul>
38+
}
39+
whenNotToUse={
40+
<ul>
41+
<li>Collections of similar items — use Listings instead.</li>
42+
<li>Dense, multi-column tabular data — use a real <code>&lt;table&gt;</code>.</li>
43+
<li>Editable forms — use the Forms primitives.</li>
44+
</ul>
45+
}
46+
preview={
47+
<DataTable>
48+
<DataRow title="Status">
49+
<Badge tone="accent">Active</Badge>
50+
</DataRow>
51+
<DataRow title="Owner">Marie Chen</DataRow>
52+
<DataRow title="Description">
53+
A longer value can wrap and flow across multiple lines without
54+
breaking the visual rhythm of the table. On wider screens the
55+
value takes more room so long copy doesn't have to wrap as
56+
aggressively.
57+
</DataRow>
58+
<DataRow title="Notes">
59+
Drop any node in here — text, a Badge, a list, a small grid of
60+
nested values.
61+
</DataRow>
62+
<DataRow title="Updated">May 16, 2026 at 1:27pm</DataRow>
63+
</DataTable>
64+
}
65+
code={code}
66+
options={
67+
<ul className="list-disc pl-5">
68+
<li>
69+
<code>&lt;DataRow title&gt;</code> accepts any node — usually a
70+
short label, but can include an icon or a small badge.
71+
</li>
72+
<li>
73+
Values are <code>text-ink-body</code> by default. Drop in any node:
74+
text, a Badge, a list, a small grid of nested values.
75+
</li>
76+
<li>
77+
<strong>Responsive grid</strong>: stacked (single column) under{" "}
78+
<code>md</code> — title sits above value, divider rhythm
79+
preserved. From <code>md</code> (768px), 4 columns with title{" "}
80+
<code>md:col-span-1</code> and value <code>md:col-span-3</code>.
81+
From <code>xl</code> (1280px), 5 columns with value{" "}
82+
<code>xl:col-span-4</code>.
83+
</li>
84+
<li>
85+
Wrap the table in a card (<code>rounded-lg border border-hairline</code>) if
86+
it needs to feel like its own panel.
87+
</li>
88+
</ul>
89+
}
90+
/>
91+
);
92+
}

app/frontend/components/design-system/sections/structure/SubNavSection.tsx

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
11
import { SectionShell } from "@/components/design-system/SectionShell";
2+
import { Select } from "@/components/ui/select";
23

3-
const code = `<nav className="flex items-end justify-between gap-4 border-b border-hairline">
4+
const code = `{/* Mobile (< md): full-width dropdown showing the active tab */}
5+
<div className="md:hidden border-b border-hairline">
6+
<div className="py-2">
7+
<Select
8+
aria-label="Section"
9+
value={active}
10+
onChange={(e) => onChange(e.target.value)}
11+
className="w-full"
12+
>
13+
<option value="overview">Overview</option>
14+
<option value="activity">Activity</option>
15+
<option value="members">Members</option>
16+
<option value="billing">Billing</option>
17+
</Select>
18+
</div>
19+
</div>
20+
21+
{/* Desktop (md+): horizontal tab strip */}
22+
<nav className="hidden md:flex items-end justify-between gap-4 border-b border-hairline">
423
<div className="flex items-center gap-6">
524
<a href="#" className="-mb-px cursor-pointer border-b-2 border-accent px-1 py-3 text-sm font-medium text-accent-display no-underline">Overview</a>
625
<a href="#" className="-mb-px cursor-pointer border-b-2 border-transparent px-1 py-3 text-sm text-ink-body no-underline hover:text-ink-display">Activity</a>
@@ -25,7 +44,10 @@ export function SubNavSection() {
2544
app (settings sub-pages, project tabs, profile sections). The
2645
active tab's underline merges with the row's bottom hairline. An
2746
optional right-side slot accepts secondary links, filters, search
28-
fields, or small actions.
47+
fields, or small actions. Below the <code>md</code> breakpoint,
48+
the tab strip collapses into a full-width dropdown that surfaces
49+
the active tab — tap to switch sections without horizontal
50+
scrolling or wrapped rows.
2951
</>
3052
}
3153
whenToUse={
@@ -46,7 +68,23 @@ export function SubNavSection() {
4668
<div className="space-y-8">
4769
<div>
4870
<p className="mb-2 text-xs uppercase tracking-wider text-ink-muted">
49-
Tabs only
71+
Mobile (&lt; md) — full-width dropdown
72+
</p>
73+
<div className="max-w-sm border-b border-hairline">
74+
<div className="py-2">
75+
<Select aria-label="Section" defaultValue="overview" className="w-full">
76+
<option value="overview">Overview</option>
77+
<option value="activity">Activity</option>
78+
<option value="members">Members</option>
79+
<option value="billing">Billing</option>
80+
</Select>
81+
</div>
82+
</div>
83+
</div>
84+
85+
<div>
86+
<p className="mb-2 text-xs uppercase tracking-wider text-ink-muted">
87+
Desktop (md+) — tab strip
5088
</p>
5189
<nav className="flex items-end gap-6 border-b border-hairline">
5290
<span className="-mb-px cursor-pointer border-b-2 border-accent px-1 py-3 text-sm font-medium text-accent-display">
@@ -66,7 +104,7 @@ export function SubNavSection() {
66104

67105
<div>
68106
<p className="mb-2 text-xs uppercase tracking-wider text-ink-muted">
69-
With right-side content
107+
Desktop — with right-side content
70108
</p>
71109
<nav className="flex items-end justify-between gap-4 border-b border-hairline">
72110
<div className="flex items-center gap-6">
@@ -90,6 +128,15 @@ export function SubNavSection() {
90128
code={code}
91129
options={
92130
<ul className="list-disc pl-5">
131+
<li>
132+
<strong>Mobile breakpoint</strong>: tabs collapse into a
133+
full-width <code>&lt;Select&gt;</code> below the <code>md</code>{" "}
134+
breakpoint. Show the dropdown with{" "}
135+
<code>md:hidden</code> and the tab strip with{" "}
136+
<code>hidden md:flex</code>. The dropdown's current value is the
137+
active tab so users can see and change sections without
138+
horizontal scrolling.
139+
</li>
93140
<li>
94141
<strong>Active state</strong>:{" "}
95142
<code>border-b-2 border-accent text-accent-display font-medium</code>{" "}
@@ -100,13 +147,23 @@ export function SubNavSection() {
100147
<strong>Right-side slot</strong>: any flex children — links,
101148
filters, search fields, small icon buttons. Use{" "}
102149
<code>pb-2</code> to keep them visually centered against the tab
103-
text baseline.
150+
text baseline. On mobile, stack right-side content above the
151+
dropdown trigger (use <code>flex flex-col gap-2</code> on the
152+
wrapper) so the dropdown remains truly full-width.
104153
</li>
105154
<li>
106155
<strong>Pair with Page header</strong>: drop the parent{" "}
107156
<code>border-b</code> off the page header, then place this nav
108157
beneath it. Result: two horizontal lines.
109158
</li>
159+
<li>
160+
<strong>Rich dropdown variant</strong>: when tabs carry icons or
161+
status indicators that wouldn't survive a native{" "}
162+
<code>&lt;select&gt;</code>, swap the <code>Select</code> for a{" "}
163+
<code>DropdownMenu</code> with a full-width trigger and
164+
matching <code>w-[var(--radix-dropdown-menu-trigger-width)]</code>{" "}
165+
content width.
166+
</li>
110167
</ul>
111168
}
112169
/>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// bm-design-system: data table primitive
2+
import * as React from "react";
3+
import { cn } from "@/lib/utils";
4+
5+
const DataTable = React.forwardRef<
6+
HTMLDListElement,
7+
React.HTMLAttributes<HTMLDListElement>
8+
>(({ className, ...props }, ref) => (
9+
<dl
10+
ref={ref}
11+
className={cn("divide-y divide-hairline border-y border-hairline", className)}
12+
{...props}
13+
/>
14+
));
15+
DataTable.displayName = "DataTable";
16+
17+
export interface DataRowProps
18+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
19+
title: React.ReactNode;
20+
children: React.ReactNode;
21+
}
22+
23+
const DataRow = React.forwardRef<HTMLDivElement, DataRowProps>(
24+
({ className, title, children, ...props }, ref) => (
25+
<div
26+
ref={ref}
27+
className={cn(
28+
"grid gap-1 py-4 md:grid-cols-4 md:gap-6 xl:grid-cols-5",
29+
className,
30+
)}
31+
{...props}
32+
>
33+
<dt className="text-sm font-medium text-ink-muted md:col-span-1">
34+
{title}
35+
</dt>
36+
<dd className="text-ink-body md:col-span-3 xl:col-span-4">{children}</dd>
37+
</div>
38+
),
39+
);
40+
DataRow.displayName = "DataRow";
41+
42+
export { DataTable, DataRow };

0 commit comments

Comments
 (0)