@protolabsai/ui 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/AppShell.stories.tsx +28 -0
- package/src/navigation.tsx +37 -3
- package/src/styles/navigation.css +26 -0
package/package.json
CHANGED
package/src/AppShell.stories.tsx
CHANGED
|
@@ -60,3 +60,31 @@ export const TabsWithSlots: Story = {
|
|
|
60
60
|
return <Demo />;
|
|
61
61
|
},
|
|
62
62
|
};
|
|
63
|
+
|
|
64
|
+
export const TabsResponsive: Story = {
|
|
65
|
+
name: "Tabs (responsive → dropdown)",
|
|
66
|
+
render: () => {
|
|
67
|
+
function Demo() {
|
|
68
|
+
const [active, setActive] = useState("activity");
|
|
69
|
+
const items = [
|
|
70
|
+
{ id: "overview", label: "Overview", icon: <Glyph /> },
|
|
71
|
+
{ id: "activity", label: "Activity", icon: <Glyph />, badge: 12 },
|
|
72
|
+
{ id: "knowledge", label: "Knowledge", icon: <Glyph /> },
|
|
73
|
+
{ id: "settings", label: "Settings", icon: <Glyph /> },
|
|
74
|
+
];
|
|
75
|
+
// A container query responds to the Tabs' own width — resize these boxes (not
|
|
76
|
+
// the window) to see the wide one stay a strip and the narrow one collapse.
|
|
77
|
+
return (
|
|
78
|
+
<div style={{ display: "grid", gap: 24 }}>
|
|
79
|
+
<div style={{ width: 520, border: "1px solid var(--pl-color-border)", borderRadius: 4, padding: 8 }}>
|
|
80
|
+
<Tabs responsive ariaLabel="Section" items={items} active={active} onSelect={setActive} />
|
|
81
|
+
</div>
|
|
82
|
+
<div style={{ width: 260, border: "1px solid var(--pl-color-border)", borderRadius: 4, padding: 8 }}>
|
|
83
|
+
<Tabs responsive ariaLabel="Section" items={items} active={active} onSelect={setActive} />
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return <Demo />;
|
|
89
|
+
},
|
|
90
|
+
};
|
package/src/navigation.tsx
CHANGED
|
@@ -15,9 +15,25 @@ export type TabItem = {
|
|
|
15
15
|
|
|
16
16
|
/** A horizontal tab strip with optional icon/badge slots + disabled/locked
|
|
17
17
|
* support (gated workflows). */
|
|
18
|
-
export function Tabs({
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
export function Tabs({
|
|
19
|
+
items,
|
|
20
|
+
active,
|
|
21
|
+
onSelect,
|
|
22
|
+
responsive = false,
|
|
23
|
+
ariaLabel,
|
|
24
|
+
}: {
|
|
25
|
+
items: TabItem[];
|
|
26
|
+
active: string;
|
|
27
|
+
onSelect: (id: string) => void;
|
|
28
|
+
/** Collapse the strip to a `<select>` dropdown when the Tabs' *container* is narrow
|
|
29
|
+
* (a CSS container query — responds to its own width, not the viewport, so it works
|
|
30
|
+
* inside a narrow panel/split). Opt-in; non-responsive output is unchanged. */
|
|
31
|
+
responsive?: boolean;
|
|
32
|
+
/** Accessible name for the tablist + the collapsed select. */
|
|
33
|
+
ariaLabel?: string;
|
|
34
|
+
}) {
|
|
35
|
+
const strip = (
|
|
36
|
+
<div className="pl-tabs" role="tablist" aria-label={ariaLabel}>
|
|
21
37
|
{items.map((t) => (
|
|
22
38
|
<button
|
|
23
39
|
key={t.id}
|
|
@@ -44,6 +60,24 @@ export function Tabs({ items, active, onSelect }: { items: TabItem[]; active: st
|
|
|
44
60
|
))}
|
|
45
61
|
</div>
|
|
46
62
|
);
|
|
63
|
+
if (!responsive) return strip;
|
|
64
|
+
return (
|
|
65
|
+
<div className="pl-tabs-wrap pl-tabs-wrap--responsive">
|
|
66
|
+
{strip}
|
|
67
|
+
<select
|
|
68
|
+
className="pl-tabs__select"
|
|
69
|
+
value={active}
|
|
70
|
+
aria-label={ariaLabel ?? "Select tab"}
|
|
71
|
+
onChange={(e) => onSelect(e.target.value)}
|
|
72
|
+
>
|
|
73
|
+
{items.map((t) => (
|
|
74
|
+
<option key={t.id} value={t.id} disabled={t.disabled || t.locked}>
|
|
75
|
+
{typeof t.label === "string" ? t.label : t.id}
|
|
76
|
+
</option>
|
|
77
|
+
))}
|
|
78
|
+
</select>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
47
81
|
}
|
|
48
82
|
|
|
49
83
|
/** A horizontal kanban board. Wrap BoardColumn children. */
|
|
@@ -71,6 +71,32 @@
|
|
|
71
71
|
opacity: 0.6;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/* Responsive Tabs (opt-in via `responsive`): below ~30rem of CONTAINER width the
|
|
75
|
+
strip collapses to a <select>. A container query responds to the Tabs' own
|
|
76
|
+
container — not the viewport — so it collapses inside a narrow panel/split too. */
|
|
77
|
+
.pl-tabs-wrap--responsive {
|
|
78
|
+
container-type: inline-size;
|
|
79
|
+
width: 100%;
|
|
80
|
+
}
|
|
81
|
+
.pl-tabs__select {
|
|
82
|
+
display: none;
|
|
83
|
+
width: 100%;
|
|
84
|
+
padding: 6px 10px;
|
|
85
|
+
font: inherit;
|
|
86
|
+
color: var(--pl-color-fg);
|
|
87
|
+
background: var(--pl-color-bg-inset);
|
|
88
|
+
border: var(--pl-border-width) solid var(--pl-color-border);
|
|
89
|
+
border-radius: var(--pl-radius);
|
|
90
|
+
}
|
|
91
|
+
@container (max-width: 30rem) {
|
|
92
|
+
.pl-tabs-wrap--responsive > .pl-tabs {
|
|
93
|
+
display: none;
|
|
94
|
+
}
|
|
95
|
+
.pl-tabs-wrap--responsive > .pl-tabs__select {
|
|
96
|
+
display: block;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
74
100
|
/* ── Board (kanban) ────────────────────────────────────────────────────────── */
|
|
75
101
|
.pl-board {
|
|
76
102
|
display: flex;
|