@protolabsai/ui 0.3.0 → 0.5.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 +3 -2
- package/src/AppShell.stories.tsx +61 -0
- package/src/Data.stories.tsx +84 -0
- package/src/Forms.stories.tsx +60 -0
- package/src/Menu.stories.tsx +76 -0
- package/src/Overlays.stories.tsx +141 -0
- package/src/index.tsx +740 -1
- package/src/styles.css +784 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protolabsai/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -19,9 +19,10 @@
|
|
|
19
19
|
"src"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
+
"@radix-ui/react-dropdown-menu": "^2.1.17",
|
|
22
23
|
"@types/react": "^19.0.0",
|
|
23
24
|
"@types/react-dom": "^19.0.0",
|
|
24
|
-
"@protolabsai/design": "0.
|
|
25
|
+
"@protolabsai/design": "0.4.0"
|
|
25
26
|
},
|
|
26
27
|
"peerDependencies": {
|
|
27
28
|
"react": "^19.0.0",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Badge, Button, PanelHeader, Tabs } from "./index";
|
|
4
|
+
|
|
5
|
+
const meta: Meta = { title: "Components/AppShell" };
|
|
6
|
+
export default meta;
|
|
7
|
+
type Story = StoryObj;
|
|
8
|
+
|
|
9
|
+
const Glyph = () => (
|
|
10
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
11
|
+
<rect x="2.5" y="2.5" width="11" height="11" rx="2" />
|
|
12
|
+
</svg>
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export const Panel: Story = {
|
|
16
|
+
render: () => (
|
|
17
|
+
<div style={{ display: "grid", gap: 20, maxWidth: 560 }}>
|
|
18
|
+
<div style={{ border: "1px solid var(--pl-color-border)", borderRadius: 4 }}>
|
|
19
|
+
<PanelHeader
|
|
20
|
+
kicker="agent · streaming-router"
|
|
21
|
+
title="Activity"
|
|
22
|
+
actions={
|
|
23
|
+
<>
|
|
24
|
+
<Button>Filter</Button>
|
|
25
|
+
<Button variant="primary">New run</Button>
|
|
26
|
+
</>
|
|
27
|
+
}
|
|
28
|
+
/>
|
|
29
|
+
<div style={{ padding: 16, color: "var(--pl-color-fg-muted)", fontSize: 13 }}>panel body…</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div style={{ border: "1px solid var(--pl-color-border)", borderRadius: 4 }}>
|
|
33
|
+
<PanelHeader compact title="Inbox" actions={<Badge status="warning">3 unread</Badge>} />
|
|
34
|
+
<div style={{ padding: 12, color: "var(--pl-color-fg-muted)", fontSize: 13 }}>compact, nested panel…</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const TabsWithSlots: Story = {
|
|
41
|
+
render: () => {
|
|
42
|
+
function Demo() {
|
|
43
|
+
const [active, setActive] = useState("activity");
|
|
44
|
+
return (
|
|
45
|
+
<Tabs
|
|
46
|
+
active={active}
|
|
47
|
+
onSelect={setActive}
|
|
48
|
+
items={[
|
|
49
|
+
{ id: "chat", label: "Chat", icon: <Glyph /> },
|
|
50
|
+
{ id: "activity", label: "Activity", icon: <Glyph />, badge: 12 },
|
|
51
|
+
{ id: "knowledge", label: "Knowledge", icon: <Glyph /> },
|
|
52
|
+
{ id: "plugins", label: "Plugins", icon: <Glyph />, badge: "new" },
|
|
53
|
+
{ id: "settings", label: "Settings", icon: <Glyph /> },
|
|
54
|
+
{ id: "billing", label: "Billing", icon: <Glyph />, locked: true, disabled: true },
|
|
55
|
+
]}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return <Demo />;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Badge, ScrollArea, Spinner, StatusDot, TBody, THead, Table, Td, Th, Tr } from "./index";
|
|
4
|
+
|
|
5
|
+
const meta: Meta = { title: "Components/Data" };
|
|
6
|
+
export default meta;
|
|
7
|
+
type Story = StoryObj;
|
|
8
|
+
|
|
9
|
+
const ROWS = [
|
|
10
|
+
{ agent: "streaming-router", status: "success", calls: 1284, p95: "180ms" },
|
|
11
|
+
{ agent: "voice-intake", status: "warning", calls: 642, p95: "910ms" },
|
|
12
|
+
{ agent: "polyglot-bridge", status: "error", calls: 17, p95: "—" },
|
|
13
|
+
{ agent: "corpus-indexer", status: "info", calls: 3021, p95: "44ms" },
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export const DataTable: Story = {
|
|
17
|
+
render: () => {
|
|
18
|
+
function Demo() {
|
|
19
|
+
const [selected, setSelected] = useState("streaming-router");
|
|
20
|
+
return (
|
|
21
|
+
<Table>
|
|
22
|
+
<THead>
|
|
23
|
+
<Tr>
|
|
24
|
+
<Th>agent</Th>
|
|
25
|
+
<Th>status</Th>
|
|
26
|
+
<Th>calls</Th>
|
|
27
|
+
<Th>p95</Th>
|
|
28
|
+
</Tr>
|
|
29
|
+
</THead>
|
|
30
|
+
<TBody>
|
|
31
|
+
{ROWS.map((r) => (
|
|
32
|
+
<Tr key={r.agent} selected={r.agent === selected} onClick={() => setSelected(r.agent)}>
|
|
33
|
+
<Td style={{ fontFamily: "var(--pl-font-mono)" }}>{r.agent}</Td>
|
|
34
|
+
<Td>
|
|
35
|
+
<Badge status={r.status}>{r.status}</Badge>
|
|
36
|
+
</Td>
|
|
37
|
+
<Td>{r.calls.toLocaleString()}</Td>
|
|
38
|
+
<Td>{r.p95}</Td>
|
|
39
|
+
</Tr>
|
|
40
|
+
))}
|
|
41
|
+
</TBody>
|
|
42
|
+
</Table>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return <Demo />;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const StatusDots: Story = {
|
|
50
|
+
render: () => (
|
|
51
|
+
<div style={{ display: "grid", gap: 12 }}>
|
|
52
|
+
<StatusDot status="success" pulse label="connected" />
|
|
53
|
+
<StatusDot status="warning" label="degraded" />
|
|
54
|
+
<StatusDot status="error" pulse label="offline" />
|
|
55
|
+
<StatusDot status="info" label="review" />
|
|
56
|
+
<StatusDot label="idle" />
|
|
57
|
+
</div>
|
|
58
|
+
),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const Spinners: Story = {
|
|
62
|
+
render: () => (
|
|
63
|
+
<div style={{ display: "flex", gap: 24, alignItems: "center" }}>
|
|
64
|
+
<Spinner size={12} />
|
|
65
|
+
<Spinner size={16} />
|
|
66
|
+
<Spinner size={24} />
|
|
67
|
+
<Spinner size={32} />
|
|
68
|
+
</div>
|
|
69
|
+
),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const Scroll: Story = {
|
|
73
|
+
render: () => (
|
|
74
|
+
<ScrollArea style={{ height: 160, maxWidth: 360, border: "1px solid var(--pl-color-border)", borderRadius: 4, padding: 12 }}>
|
|
75
|
+
<div style={{ display: "grid", gap: 8 }}>
|
|
76
|
+
{Array.from({ length: 24 }, (_, i) => (
|
|
77
|
+
<div key={i} style={{ fontFamily: "var(--pl-font-mono)", fontSize: 12, color: "var(--pl-color-fg-muted)" }}>
|
|
78
|
+
event #{i + 1} — dispatched
|
|
79
|
+
</div>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
</ScrollArea>
|
|
83
|
+
),
|
|
84
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Checkbox, Field, Input, Select, Switch, Textarea } from "./index";
|
|
4
|
+
|
|
5
|
+
const meta: Meta = { title: "Components/Forms" };
|
|
6
|
+
export default meta;
|
|
7
|
+
type Story = StoryObj;
|
|
8
|
+
|
|
9
|
+
export const Inputs: Story = {
|
|
10
|
+
render: () => (
|
|
11
|
+
<div style={{ display: "grid", gap: 16, maxWidth: 360 }}>
|
|
12
|
+
<Field label="Inline field (label + input)" value="streaming-router" />
|
|
13
|
+
<label style={{ display: "grid", gap: 6 }}>
|
|
14
|
+
<span style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: "0.05em", color: "var(--pl-color-fg-muted)" }}>
|
|
15
|
+
Standalone Input
|
|
16
|
+
</span>
|
|
17
|
+
<Input placeholder="e.g. claude-opus-4-8" />
|
|
18
|
+
</label>
|
|
19
|
+
<label style={{ display: "grid", gap: 6 }}>
|
|
20
|
+
<span style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: "0.05em", color: "var(--pl-color-fg-muted)" }}>
|
|
21
|
+
Select
|
|
22
|
+
</span>
|
|
23
|
+
<Select defaultValue="a2a">
|
|
24
|
+
<option value="in-process">in-process</option>
|
|
25
|
+
<option value="a2a">remote A2A</option>
|
|
26
|
+
<option value="fn">function handler</option>
|
|
27
|
+
</Select>
|
|
28
|
+
</label>
|
|
29
|
+
<label style={{ display: "grid", gap: 6 }}>
|
|
30
|
+
<span style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: "0.05em", color: "var(--pl-color-fg-muted)" }}>
|
|
31
|
+
Textarea
|
|
32
|
+
</span>
|
|
33
|
+
<Textarea placeholder="System prompt…" />
|
|
34
|
+
</label>
|
|
35
|
+
<Input placeholder="disabled" disabled />
|
|
36
|
+
</div>
|
|
37
|
+
),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const Toggles: Story = {
|
|
41
|
+
render: () => {
|
|
42
|
+
function Demo() {
|
|
43
|
+
const [stream, setStream] = useState(true);
|
|
44
|
+
const [trace, setTrace] = useState(false);
|
|
45
|
+
const [a, setA] = useState(true);
|
|
46
|
+
const [b, setB] = useState(false);
|
|
47
|
+
return (
|
|
48
|
+
<div style={{ display: "grid", gap: 16 }}>
|
|
49
|
+
<Switch checked={stream} onCheckedChange={setStream} label="Stream responses" />
|
|
50
|
+
<Switch checked={trace} onCheckedChange={setTrace} label="Verbose tracing" />
|
|
51
|
+
<Switch checked={false} disabled label="Locked (disabled)" />
|
|
52
|
+
<Checkbox checked={a} onCheckedChange={setA} label="Capture corpus pair" />
|
|
53
|
+
<Checkbox checked={b} onCheckedChange={setB} label="Pin to latest model" />
|
|
54
|
+
<Checkbox checked disabled label="Required (disabled)" />
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return <Demo />;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { useRef } from "react";
|
|
3
|
+
import { Button, Menu, MenuHandle, MenuItem, MenuLabel, MenuSeparator, MenuSub } from "./index";
|
|
4
|
+
|
|
5
|
+
const meta: Meta = { title: "Components/Menu" };
|
|
6
|
+
export default meta;
|
|
7
|
+
type Story = StoryObj;
|
|
8
|
+
|
|
9
|
+
// Tiny inline icon so stories stay dependency-free.
|
|
10
|
+
const Dot = () => (
|
|
11
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
12
|
+
<circle cx="8" cy="8" r="3" />
|
|
13
|
+
</svg>
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const Items = () => (
|
|
17
|
+
<>
|
|
18
|
+
<MenuLabel>Rail surface</MenuLabel>
|
|
19
|
+
<MenuItem icon={<Dot />}>Move up</MenuItem>
|
|
20
|
+
<MenuItem icon={<Dot />}>Move down</MenuItem>
|
|
21
|
+
<MenuSub label="Move to other rail" icon={<Dot />}>
|
|
22
|
+
<MenuItem>Left rail</MenuItem>
|
|
23
|
+
<MenuItem>Right rail</MenuItem>
|
|
24
|
+
</MenuSub>
|
|
25
|
+
<MenuSeparator />
|
|
26
|
+
<MenuItem disabled icon={<Dot />}>
|
|
27
|
+
Pin (coming soon)
|
|
28
|
+
</MenuItem>
|
|
29
|
+
<MenuItem destructive icon={<Dot />}>
|
|
30
|
+
Remove surface
|
|
31
|
+
</MenuItem>
|
|
32
|
+
</>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export const Trigger: Story = {
|
|
36
|
+
render: () => (
|
|
37
|
+
<Menu trigger={<Button>Surface actions ▾</Button>}>
|
|
38
|
+
<Items />
|
|
39
|
+
</Menu>
|
|
40
|
+
),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const RightClickAtCoords: Story = {
|
|
44
|
+
render: () => {
|
|
45
|
+
function Demo() {
|
|
46
|
+
const menu = useRef<MenuHandle>(null);
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
<div
|
|
50
|
+
onContextMenu={(e) => {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
menu.current?.open({ x: e.clientX, y: e.clientY });
|
|
53
|
+
}}
|
|
54
|
+
style={{
|
|
55
|
+
height: 180,
|
|
56
|
+
display: "grid",
|
|
57
|
+
placeItems: "center",
|
|
58
|
+
border: "1px dashed var(--pl-color-border-strong)",
|
|
59
|
+
borderRadius: 4,
|
|
60
|
+
color: "var(--pl-color-fg-muted)",
|
|
61
|
+
fontFamily: "var(--pl-font-mono)",
|
|
62
|
+
fontSize: 13,
|
|
63
|
+
userSelect: "none",
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
right-click anywhere in this box
|
|
67
|
+
</div>
|
|
68
|
+
<Menu ref={menu}>
|
|
69
|
+
<Items />
|
|
70
|
+
</Menu>
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return <Demo />;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
ConfirmDialog,
|
|
6
|
+
Dialog,
|
|
7
|
+
Drawer,
|
|
8
|
+
Field,
|
|
9
|
+
Tooltip,
|
|
10
|
+
ToastProvider,
|
|
11
|
+
useToast,
|
|
12
|
+
} from "./index";
|
|
13
|
+
|
|
14
|
+
const meta: Meta = { title: "Components/Overlays" };
|
|
15
|
+
export default meta;
|
|
16
|
+
type Story = StoryObj;
|
|
17
|
+
|
|
18
|
+
export const Modal: Story = {
|
|
19
|
+
render: () => {
|
|
20
|
+
function Demo() {
|
|
21
|
+
const [open, setOpen] = useState(false);
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
<Button variant="primary" onClick={() => setOpen(true)}>
|
|
25
|
+
Open dialog
|
|
26
|
+
</Button>
|
|
27
|
+
<Dialog
|
|
28
|
+
open={open}
|
|
29
|
+
onClose={() => setOpen(false)}
|
|
30
|
+
title="Rename workflow"
|
|
31
|
+
footer={
|
|
32
|
+
<>
|
|
33
|
+
<Button onClick={() => setOpen(false)}>Cancel</Button>
|
|
34
|
+
<Button variant="primary" onClick={() => setOpen(false)}>
|
|
35
|
+
Save
|
|
36
|
+
</Button>
|
|
37
|
+
</>
|
|
38
|
+
}
|
|
39
|
+
>
|
|
40
|
+
<Field label="Name" value="streaming-router" />
|
|
41
|
+
<p style={{ marginTop: 12 }}>Esc, the × button, or a backdrop click all close it. Focus is trapped inside.</p>
|
|
42
|
+
</Dialog>
|
|
43
|
+
</>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return <Demo />;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const Confirm: Story = {
|
|
51
|
+
render: () => {
|
|
52
|
+
function Demo() {
|
|
53
|
+
const [open, setOpen] = useState(false);
|
|
54
|
+
const [deleted, setDeleted] = useState(false);
|
|
55
|
+
return (
|
|
56
|
+
<>
|
|
57
|
+
<Button onClick={() => setOpen(true)}>Delete agent…</Button>
|
|
58
|
+
{deleted && <span style={{ marginLeft: 12, fontFamily: "var(--pl-font-mono)", fontSize: 12 }}>deleted.</span>}
|
|
59
|
+
<ConfirmDialog
|
|
60
|
+
open={open}
|
|
61
|
+
title="Delete this agent?"
|
|
62
|
+
destructive
|
|
63
|
+
confirmLabel="Delete"
|
|
64
|
+
onConfirm={() => {
|
|
65
|
+
setDeleted(true);
|
|
66
|
+
setOpen(false);
|
|
67
|
+
}}
|
|
68
|
+
onClose={() => setOpen(false)}
|
|
69
|
+
>
|
|
70
|
+
This removes the agent and its run history. This cannot be undone.
|
|
71
|
+
</ConfirmDialog>
|
|
72
|
+
</>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return <Demo />;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const SidePanel: Story = {
|
|
80
|
+
render: () => {
|
|
81
|
+
function Demo() {
|
|
82
|
+
const [side, setSide] = useState<"left" | "right" | null>(null);
|
|
83
|
+
return (
|
|
84
|
+
<div style={{ display: "flex", gap: 8 }}>
|
|
85
|
+
<Button onClick={() => setSide("left")}>Open left</Button>
|
|
86
|
+
<Button onClick={() => setSide("right")}>Open right</Button>
|
|
87
|
+
<Drawer open={side != null} side={side ?? "right"} title="Event detail" onClose={() => setSide(null)}>
|
|
88
|
+
<p>Slides in from the {side}. Same dismiss + focus-trap behavior as Dialog.</p>
|
|
89
|
+
</Drawer>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return <Demo />;
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const Toasts: Story = {
|
|
98
|
+
render: () => {
|
|
99
|
+
function Trigger() {
|
|
100
|
+
const toast = useToast();
|
|
101
|
+
return (
|
|
102
|
+
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
|
103
|
+
<Button onClick={() => toast({ message: "Saved." })}>neutral</Button>
|
|
104
|
+
<Button onClick={() => toast({ tone: "success", title: "deployed", message: "Image is live on the studio stack." })}>
|
|
105
|
+
success
|
|
106
|
+
</Button>
|
|
107
|
+
<Button onClick={() => toast({ tone: "warning", title: "heads up", message: "main is protected — open a PR." })}>
|
|
108
|
+
warning
|
|
109
|
+
</Button>
|
|
110
|
+
<Button onClick={() => toast({ tone: "error", title: "failed", message: "Build red on the sixth attempt.", duration: 8000 })}>
|
|
111
|
+
error
|
|
112
|
+
</Button>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return (
|
|
117
|
+
<ToastProvider>
|
|
118
|
+
<Trigger />
|
|
119
|
+
</ToastProvider>
|
|
120
|
+
);
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const Tooltips: Story = {
|
|
125
|
+
render: () => (
|
|
126
|
+
<div style={{ display: "flex", gap: 40, padding: 60, justifyContent: "center" }}>
|
|
127
|
+
<Tooltip label="on top">
|
|
128
|
+
<Button>top</Button>
|
|
129
|
+
</Tooltip>
|
|
130
|
+
<Tooltip label="on the bottom" side="bottom">
|
|
131
|
+
<Button>bottom</Button>
|
|
132
|
+
</Tooltip>
|
|
133
|
+
<Tooltip label="to the left" side="left">
|
|
134
|
+
<Button>left</Button>
|
|
135
|
+
</Tooltip>
|
|
136
|
+
<Tooltip label="to the right" side="right">
|
|
137
|
+
<Button>right</Button>
|
|
138
|
+
</Tooltip>
|
|
139
|
+
</div>
|
|
140
|
+
),
|
|
141
|
+
};
|