@checkstack/ui 1.7.1 → 1.8.1
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/.storybook/main.ts +22 -0
- package/.storybook/manager-head.html +2 -0
- package/.storybook/mock-access-api.ts +5 -0
- package/.storybook/preview.css +5 -0
- package/.storybook/preview.tsx +73 -0
- package/CHANGELOG.md +234 -0
- package/package.json +24 -10
- package/postcss.config.js +6 -0
- package/src/components/Card.tsx +4 -3
- package/src/components/Dialog.tsx +2 -2
- package/src/components/EmptyState.tsx +48 -3
- package/src/components/InstanceScopeBanner.tsx +46 -0
- package/src/components/LinksEditor.tsx +156 -0
- package/src/components/UserMenu.tsx +1 -1
- package/src/index.ts +2 -0
- package/stories/AccessDenied.stories.tsx +17 -0
- package/stories/AccessGate.stories.tsx +49 -0
- package/stories/Accordion.stories.tsx +56 -0
- package/stories/Alert.stories.tsx +88 -0
- package/stories/AmbientBackground.stories.tsx +28 -0
- package/stories/AnimatedCounter.stories.tsx +41 -0
- package/stories/AnimatedNumber.stories.tsx +39 -0
- package/stories/BackLink.stories.tsx +32 -0
- package/stories/Badge.stories.tsx +37 -0
- package/stories/Button.stories.tsx +57 -0
- package/stories/Card.stories.tsx +39 -0
- package/stories/Checkbox.stories.tsx +45 -0
- package/stories/CodeEditor.stories.tsx +67 -0
- package/stories/ColorPicker.stories.tsx +24 -0
- package/stories/CommandPalette.stories.tsx +36 -0
- package/stories/ConfirmationModal.stories.tsx +40 -0
- package/stories/DateRangeFilter.stories.tsx +30 -0
- package/stories/DateTimePicker.stories.tsx +26 -0
- package/stories/Dialog.stories.tsx +62 -0
- package/stories/DynamicForm.stories.tsx +83 -0
- package/stories/DynamicIcon.stories.tsx +52 -0
- package/stories/EditableText.stories.tsx +29 -0
- package/stories/Feedback.stories.tsx +45 -0
- package/stories/IDELayout.stories.tsx +70 -0
- package/stories/InfoBanner.stories.tsx +41 -0
- package/stories/Input.stories.tsx +29 -0
- package/stories/InstanceScopeBanner.stories.tsx +35 -0
- package/stories/Introduction.mdx +50 -0
- package/stories/Label.stories.tsx +21 -0
- package/stories/LinksEditor.stories.tsx +37 -0
- package/stories/Markdown.stories.tsx +35 -0
- package/stories/Menu.stories.tsx +35 -0
- package/stories/MetricTile.stories.tsx +31 -0
- package/stories/NavItem.stories.tsx +29 -0
- package/stories/NotFound.stories.tsx +17 -0
- package/stories/Page.stories.tsx +29 -0
- package/stories/PageLayout.stories.tsx +59 -0
- package/stories/PaginatedList.stories.tsx +44 -0
- package/stories/Pagination.stories.tsx +31 -0
- package/stories/PerformanceProvider.stories.tsx +33 -0
- package/stories/PluginConfigForm.stories.tsx +64 -0
- package/stories/Popover.stories.tsx +30 -0
- package/stories/SectionHeader.stories.tsx +20 -0
- package/stories/Select.stories.tsx +36 -0
- package/stories/Sheet.stories.tsx +59 -0
- package/stories/Slider.stories.tsx +24 -0
- package/stories/StatusCard.stories.tsx +30 -0
- package/stories/StatusUpdateTimeline.stories.tsx +77 -0
- package/stories/StrategyConfigCard.stories.tsx +57 -0
- package/stories/SubscribeButton.stories.tsx +27 -0
- package/stories/Table.stories.tsx +56 -0
- package/stories/Tabs.stories.tsx +40 -0
- package/stories/TerminalFeed.stories.tsx +47 -0
- package/stories/Textarea.stories.tsx +25 -0
- package/stories/ThemeProvider.stories.tsx +38 -0
- package/stories/Toast.stories.tsx +32 -0
- package/stories/Toggle.stories.tsx +43 -0
- package/stories/Tooltip.stories.tsx +20 -0
- package/stories/UserMenu.stories.tsx +35 -0
- package/tailwind.config.js +74 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { InstanceScopeBanner } from "../src/components/InstanceScopeBanner";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof InstanceScopeBanner> = {
|
|
5
|
+
title: "Components/Feedback/InstanceScopeBanner",
|
|
6
|
+
component: InstanceScopeBanner,
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof InstanceScopeBanner>;
|
|
12
|
+
|
|
13
|
+
export const InstanceScope: Story = {
|
|
14
|
+
args: {
|
|
15
|
+
scope: "instance",
|
|
16
|
+
subject: "Queue",
|
|
17
|
+
recommendation: "Switch to a Redis-backed queue to see cluster-wide totals.",
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const ClusterScope: Story = {
|
|
22
|
+
args: {
|
|
23
|
+
scope: "cluster",
|
|
24
|
+
subject: "Queue",
|
|
25
|
+
recommendation: "Banner is hidden when scope is 'cluster'.",
|
|
26
|
+
},
|
|
27
|
+
render: (args) => (
|
|
28
|
+
<div>
|
|
29
|
+
<InstanceScopeBanner {...args} />
|
|
30
|
+
<p className="text-sm text-muted-foreground">
|
|
31
|
+
Renders nothing — confirm by inspecting the DOM.
|
|
32
|
+
</p>
|
|
33
|
+
</div>
|
|
34
|
+
),
|
|
35
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="Introduction" />
|
|
4
|
+
|
|
5
|
+
# Checkstack UI
|
|
6
|
+
|
|
7
|
+
This is the component library for Checkstack plugin developers. Every primitive
|
|
8
|
+
exported from `@checkstack/ui` is intended to be safe for plugin use — both in
|
|
9
|
+
core plugins shipped in this repo and in third-party plugins published to npm.
|
|
10
|
+
|
|
11
|
+
## Why this exists
|
|
12
|
+
|
|
13
|
+
Plugins ship their own React components but render *inside* the host frontend's
|
|
14
|
+
React tree, so they must use the host's design tokens, theme, and spacing scale.
|
|
15
|
+
The `@checkstack/ui` package is the public surface for that. If a primitive
|
|
16
|
+
isn't documented here, treat it as an internal implementation detail.
|
|
17
|
+
|
|
18
|
+
## How to use
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import { Button, Card, CardHeader, CardTitle, CardContent } from "@checkstack/ui";
|
|
22
|
+
|
|
23
|
+
export function MyPanel() {
|
|
24
|
+
return (
|
|
25
|
+
<Card>
|
|
26
|
+
<CardHeader>
|
|
27
|
+
<CardTitle>My Plugin</CardTitle>
|
|
28
|
+
</CardHeader>
|
|
29
|
+
<CardContent>
|
|
30
|
+
<Button variant="primary">Do the thing</Button>
|
|
31
|
+
</CardContent>
|
|
32
|
+
</Card>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Theming
|
|
38
|
+
|
|
39
|
+
All components consume CSS custom properties defined in `themes.css`. The host
|
|
40
|
+
frontend wraps the entire app in `ThemeProvider`, so plugins automatically
|
|
41
|
+
inherit light/dark mode. Use semantic Tailwind classes (`bg-primary`,
|
|
42
|
+
`text-muted-foreground`, …) — never hardcoded colors.
|
|
43
|
+
|
|
44
|
+
## Accessibility & performance
|
|
45
|
+
|
|
46
|
+
- Components are built on Radix primitives where keyboard / focus / ARIA
|
|
47
|
+
behavior is non-trivial.
|
|
48
|
+
- Animation-heavy components respect the `usePerformance` hook's `isLowPower`
|
|
49
|
+
flag — see [the performance rule](https://github.com/enyineer/checkstack/blob/main/.agent/rules/performance.md)
|
|
50
|
+
for the full contract plugins are expected to follow.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Input } from "../src/components/Input";
|
|
3
|
+
import { Label } from "../src/components/Label";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Label> = {
|
|
6
|
+
title: "Components/Inputs/Label",
|
|
7
|
+
component: Label,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof Label>;
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
render: () => (
|
|
16
|
+
<div className="space-y-2 max-w-sm">
|
|
17
|
+
<Label htmlFor="email">Email</Label>
|
|
18
|
+
<Input id="email" type="email" placeholder="you@example.com" />
|
|
19
|
+
</div>
|
|
20
|
+
),
|
|
21
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { LinksEditor, type HotLink } from "../src/components/LinksEditor";
|
|
4
|
+
|
|
5
|
+
const meta: Meta = {
|
|
6
|
+
title: "Components/Inputs/LinksEditor",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default meta;
|
|
10
|
+
type Story = StoryObj;
|
|
11
|
+
|
|
12
|
+
const Demo = ({ canManage }: { canManage: boolean }) => {
|
|
13
|
+
const [links, setLinks] = useState<HotLink[]>([
|
|
14
|
+
{ id: "1", label: "Runbook", url: "https://example.com/runbook" },
|
|
15
|
+
{ id: "2", label: null, url: "https://example.com/dashboards/api" },
|
|
16
|
+
]);
|
|
17
|
+
return (
|
|
18
|
+
<div className="max-w-md">
|
|
19
|
+
<LinksEditor
|
|
20
|
+
links={links}
|
|
21
|
+
canManage={canManage}
|
|
22
|
+
onAdd={({ label, url }) => {
|
|
23
|
+
setLinks((prev) => [
|
|
24
|
+
...prev,
|
|
25
|
+
{ id: String(prev.length + 1), label: label ?? null, url },
|
|
26
|
+
]);
|
|
27
|
+
}}
|
|
28
|
+
onRemove={(link) => {
|
|
29
|
+
setLinks((prev) => prev.filter((l) => l.id !== link.id));
|
|
30
|
+
}}
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const Editable: Story = { render: () => <Demo canManage /> };
|
|
37
|
+
export const ReadOnly: Story = { render: () => <Demo canManage={false} /> };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Markdown, MarkdownBlock } from "../src/components/Markdown";
|
|
3
|
+
|
|
4
|
+
const meta: Meta = {
|
|
5
|
+
title: "Components/Display/Markdown",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default meta;
|
|
9
|
+
type Story = StoryObj;
|
|
10
|
+
|
|
11
|
+
export const Inline: Story = {
|
|
12
|
+
render: () => (
|
|
13
|
+
<p>
|
|
14
|
+
<Markdown>
|
|
15
|
+
{"Build **fast**, ship **safe**. See the [docs](https://example.com)."}
|
|
16
|
+
</Markdown>
|
|
17
|
+
</p>
|
|
18
|
+
),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const Block: Story = {
|
|
22
|
+
render: () => (
|
|
23
|
+
<MarkdownBlock>
|
|
24
|
+
{`# Health checks
|
|
25
|
+
|
|
26
|
+
A *health check* probes a system and asserts an outcome.
|
|
27
|
+
|
|
28
|
+
- HTTP probes
|
|
29
|
+
- TCP probes
|
|
30
|
+
- Custom strategies
|
|
31
|
+
|
|
32
|
+
Read more in the [strategies guide](https://example.com).`}
|
|
33
|
+
</MarkdownBlock>
|
|
34
|
+
),
|
|
35
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { LogOut, Settings, User } from "lucide-react";
|
|
3
|
+
import {
|
|
4
|
+
DropdownMenuItem,
|
|
5
|
+
DropdownMenuLabel,
|
|
6
|
+
DropdownMenuSeparator,
|
|
7
|
+
} from "../src/components/Menu";
|
|
8
|
+
|
|
9
|
+
const meta: Meta = {
|
|
10
|
+
title: "Components/Overlays/Menu (Dropdown items)",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
type Story = StoryObj;
|
|
15
|
+
|
|
16
|
+
export const Items: Story = {
|
|
17
|
+
render: () => (
|
|
18
|
+
<div className="w-64 rounded-md border border-border bg-popover p-1 shadow-md">
|
|
19
|
+
<DropdownMenuLabel>Account</DropdownMenuLabel>
|
|
20
|
+
<DropdownMenuItem icon={<User className="h-4 w-4" />}>
|
|
21
|
+
Profile
|
|
22
|
+
</DropdownMenuItem>
|
|
23
|
+
<DropdownMenuItem
|
|
24
|
+
icon={<Settings className="h-4 w-4" />}
|
|
25
|
+
description="Theme, notifications, integrations"
|
|
26
|
+
>
|
|
27
|
+
Settings
|
|
28
|
+
</DropdownMenuItem>
|
|
29
|
+
<DropdownMenuSeparator />
|
|
30
|
+
<DropdownMenuItem icon={<LogOut className="h-4 w-4" />}>
|
|
31
|
+
Sign out
|
|
32
|
+
</DropdownMenuItem>
|
|
33
|
+
</div>
|
|
34
|
+
),
|
|
35
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Activity, Cpu, MemoryStick, Network } from "lucide-react";
|
|
3
|
+
import { MetricTile } from "../src/components/MetricTile";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof MetricTile> = {
|
|
6
|
+
title: "Components/Display/MetricTile",
|
|
7
|
+
component: MetricTile,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof MetricTile>;
|
|
13
|
+
|
|
14
|
+
export const Single: Story = {
|
|
15
|
+
args: {
|
|
16
|
+
icon: Activity,
|
|
17
|
+
label: "Throughput",
|
|
18
|
+
value: "1.2k req/s",
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const Grid: Story = {
|
|
23
|
+
render: () => (
|
|
24
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 max-w-3xl">
|
|
25
|
+
<MetricTile icon={Cpu} label="CPU" value="42%" subtitle="across 4 cores" />
|
|
26
|
+
<MetricTile icon={MemoryStick} label="Memory" value="3.1 GB" variant="warning" />
|
|
27
|
+
<MetricTile icon={Network} label="Network" value="220 Mbps" variant="success" />
|
|
28
|
+
<MetricTile icon={Activity} label="Errors" value="14" variant="destructive" />
|
|
29
|
+
</div>
|
|
30
|
+
),
|
|
31
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Activity, Bell, Cog } from "lucide-react";
|
|
3
|
+
import { NavItem } from "../src/components/NavItem";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof NavItem> = {
|
|
6
|
+
title: "Components/Navigation/NavItem",
|
|
7
|
+
component: NavItem,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof NavItem>;
|
|
13
|
+
|
|
14
|
+
export const TopLevel: Story = {
|
|
15
|
+
args: {
|
|
16
|
+
to: "/health",
|
|
17
|
+
label: "Health",
|
|
18
|
+
icon: <Activity className="h-4 w-4" />,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const Nested: Story = {
|
|
23
|
+
render: () => (
|
|
24
|
+
<NavItem label="Settings" icon={<Cog className="h-4 w-4" />}>
|
|
25
|
+
<NavItem to="/settings/notifications" label="Notifications" icon={<Bell className="h-4 w-4" />} />
|
|
26
|
+
<NavItem to="/settings/general" label="General" icon={<Cog className="h-4 w-4" />} />
|
|
27
|
+
</NavItem>
|
|
28
|
+
),
|
|
29
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { NotFound } from "../src/components/NotFound";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof NotFound> = {
|
|
5
|
+
title: "Components/Feedback/NotFound",
|
|
6
|
+
component: NotFound,
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof NotFound>;
|
|
12
|
+
|
|
13
|
+
export const Default: Story = {};
|
|
14
|
+
|
|
15
|
+
export const CustomMessage: Story = {
|
|
16
|
+
args: { message: "We couldn't find the catalog system you requested." },
|
|
17
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Activity } from "lucide-react";
|
|
3
|
+
import { Button } from "../src/components/Button";
|
|
4
|
+
import { Page, PageContent, PageHeader } from "../src/components/Page";
|
|
5
|
+
|
|
6
|
+
const meta: Meta = {
|
|
7
|
+
title: "Components/Layout/Page",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj;
|
|
12
|
+
|
|
13
|
+
export const Default: Story = {
|
|
14
|
+
render: () => (
|
|
15
|
+
<Page className="h-[60vh]">
|
|
16
|
+
<PageHeader
|
|
17
|
+
title="Health"
|
|
18
|
+
subtitle="Live system signals across all environments."
|
|
19
|
+
icon={Activity}
|
|
20
|
+
actions={<Button>New check</Button>}
|
|
21
|
+
/>
|
|
22
|
+
<PageContent>
|
|
23
|
+
<p className="text-sm text-muted-foreground">
|
|
24
|
+
Page content scrolls independently of the header.
|
|
25
|
+
</p>
|
|
26
|
+
</PageContent>
|
|
27
|
+
</Page>
|
|
28
|
+
),
|
|
29
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { ShieldCheck } from "lucide-react";
|
|
3
|
+
import { Button } from "../src/components/Button";
|
|
4
|
+
import { PageLayout } from "../src/components/PageLayout";
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof PageLayout> = {
|
|
7
|
+
title: "Components/Layout/PageLayout",
|
|
8
|
+
component: PageLayout,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
type Story = StoryObj<typeof PageLayout>;
|
|
14
|
+
|
|
15
|
+
export const Allowed: Story = {
|
|
16
|
+
render: () => (
|
|
17
|
+
<div className="h-[60vh]">
|
|
18
|
+
<PageLayout
|
|
19
|
+
title="Access rules"
|
|
20
|
+
subtitle="Define who can do what."
|
|
21
|
+
icon={ShieldCheck}
|
|
22
|
+
allowed
|
|
23
|
+
actions={<Button>New rule</Button>}
|
|
24
|
+
>
|
|
25
|
+
<p className="text-sm text-muted-foreground">Rules table…</p>
|
|
26
|
+
</PageLayout>
|
|
27
|
+
</div>
|
|
28
|
+
),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const Denied: Story = {
|
|
32
|
+
render: () => (
|
|
33
|
+
<div className="h-[60vh]">
|
|
34
|
+
<PageLayout
|
|
35
|
+
title="Access rules"
|
|
36
|
+
subtitle="Define who can do what."
|
|
37
|
+
icon={ShieldCheck}
|
|
38
|
+
allowed={false}
|
|
39
|
+
>
|
|
40
|
+
<p>(Hidden)</p>
|
|
41
|
+
</PageLayout>
|
|
42
|
+
</div>
|
|
43
|
+
),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const Loading: Story = {
|
|
47
|
+
render: () => (
|
|
48
|
+
<div className="h-[60vh]">
|
|
49
|
+
<PageLayout
|
|
50
|
+
title="Access rules"
|
|
51
|
+
subtitle="Define who can do what."
|
|
52
|
+
icon={ShieldCheck}
|
|
53
|
+
loading
|
|
54
|
+
>
|
|
55
|
+
<p>(Hidden while loading)</p>
|
|
56
|
+
</PageLayout>
|
|
57
|
+
</div>
|
|
58
|
+
),
|
|
59
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { useEffect, useMemo } from "react";
|
|
3
|
+
import { PaginatedList } from "../src/components/PaginatedList";
|
|
4
|
+
import { usePagination } from "../src/hooks/usePagination";
|
|
5
|
+
|
|
6
|
+
const meta: Meta = {
|
|
7
|
+
title: "Components/Data/PaginatedList",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj;
|
|
12
|
+
|
|
13
|
+
const ITEMS = Array.from({ length: 73 }, (_, i) => ({
|
|
14
|
+
id: String(i + 1),
|
|
15
|
+
name: `System ${i + 1}`,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
const Demo = () => {
|
|
19
|
+
const pagination = usePagination({ defaultLimit: 10 });
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
pagination.setTotal(ITEMS.length);
|
|
22
|
+
}, [pagination]);
|
|
23
|
+
const slice = useMemo(
|
|
24
|
+
() => ITEMS.slice(pagination.offset, pagination.offset + pagination.limit),
|
|
25
|
+
[pagination.offset, pagination.limit],
|
|
26
|
+
);
|
|
27
|
+
return (
|
|
28
|
+
<div className="max-w-md">
|
|
29
|
+
<PaginatedList items={slice} loading={false} pagination={pagination}>
|
|
30
|
+
{(items) => (
|
|
31
|
+
<ul className="divide-y rounded-md border border-border">
|
|
32
|
+
{items.map((item) => (
|
|
33
|
+
<li key={item.id} className="px-3 py-2 text-sm">
|
|
34
|
+
{item.name}
|
|
35
|
+
</li>
|
|
36
|
+
))}
|
|
37
|
+
</ul>
|
|
38
|
+
)}
|
|
39
|
+
</PaginatedList>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const Default: Story = { render: () => <Demo /> };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Pagination } from "../src/components/Pagination";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Pagination> = {
|
|
6
|
+
title: "Components/Navigation/Pagination",
|
|
7
|
+
component: Pagination,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof Pagination>;
|
|
13
|
+
|
|
14
|
+
const Demo = () => {
|
|
15
|
+
const [page, setPage] = useState(1);
|
|
16
|
+
const [limit, setLimit] = useState(20);
|
|
17
|
+
return (
|
|
18
|
+
<Pagination
|
|
19
|
+
page={page}
|
|
20
|
+
totalPages={12}
|
|
21
|
+
onPageChange={setPage}
|
|
22
|
+
total={234}
|
|
23
|
+
limit={limit}
|
|
24
|
+
onPageSizeChange={setLimit}
|
|
25
|
+
showPageSize
|
|
26
|
+
showTotal
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const Default: Story = { render: () => <Demo /> };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Button } from "../src/components/Button";
|
|
3
|
+
import { usePerformance } from "../src/components/PerformanceProvider";
|
|
4
|
+
|
|
5
|
+
const meta: Meta = {
|
|
6
|
+
title: "Foundations/PerformanceProvider",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default meta;
|
|
10
|
+
type Story = StoryObj;
|
|
11
|
+
|
|
12
|
+
const Demo = () => {
|
|
13
|
+
const { isLowPower, manualLowPower, toggleManualLowPower } = usePerformance();
|
|
14
|
+
return (
|
|
15
|
+
<div className="space-y-3 max-w-md">
|
|
16
|
+
<p className="text-sm">
|
|
17
|
+
<code>isLowPower</code>: <strong>{String(isLowPower)}</strong>
|
|
18
|
+
</p>
|
|
19
|
+
<p className="text-sm">
|
|
20
|
+
<code>manualLowPower</code>: <strong>{String(manualLowPower)}</strong>
|
|
21
|
+
</p>
|
|
22
|
+
<Button onClick={toggleManualLowPower}>
|
|
23
|
+
Toggle low-power mode
|
|
24
|
+
</Button>
|
|
25
|
+
<p className="text-xs text-muted-foreground">
|
|
26
|
+
Plugins should consume <code>usePerformance().isLowPower</code> to
|
|
27
|
+
disable expensive animations / blurs.
|
|
28
|
+
</p>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const Toggle: Story = { render: () => <Demo /> };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
PluginConfigForm,
|
|
5
|
+
type PluginOption,
|
|
6
|
+
} from "../src/components/PluginConfigForm";
|
|
7
|
+
|
|
8
|
+
const meta: Meta = {
|
|
9
|
+
title: "Components/Forms/PluginConfigForm",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
type Story = StoryObj;
|
|
14
|
+
|
|
15
|
+
const plugins: PluginOption[] = [
|
|
16
|
+
{
|
|
17
|
+
id: "http-probe",
|
|
18
|
+
displayName: "HTTP Probe",
|
|
19
|
+
description: "Probes a public HTTP endpoint and asserts a 2xx response.",
|
|
20
|
+
configSchema: {
|
|
21
|
+
type: "object",
|
|
22
|
+
required: ["url"],
|
|
23
|
+
properties: {
|
|
24
|
+
url: { type: "string", title: "URL", format: "uri" },
|
|
25
|
+
timeoutMs: { type: "number", title: "Timeout (ms)", default: 5000 },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "tcp-probe",
|
|
31
|
+
displayName: "TCP Probe",
|
|
32
|
+
description: "Opens a TCP connection to verify the port is reachable.",
|
|
33
|
+
configSchema: {
|
|
34
|
+
type: "object",
|
|
35
|
+
required: ["host", "port"],
|
|
36
|
+
properties: {
|
|
37
|
+
host: { type: "string", title: "Host" },
|
|
38
|
+
port: { type: "number", title: "Port" },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const Demo = () => {
|
|
45
|
+
const [pluginId, setPluginId] = useState("http-probe");
|
|
46
|
+
const [config, setConfig] = useState<Record<string, unknown>>({});
|
|
47
|
+
return (
|
|
48
|
+
<div className="max-w-xl">
|
|
49
|
+
<PluginConfigForm
|
|
50
|
+
label="Strategy"
|
|
51
|
+
plugins={plugins}
|
|
52
|
+
selectedPluginId={pluginId}
|
|
53
|
+
onPluginChange={(id) => {
|
|
54
|
+
setPluginId(id);
|
|
55
|
+
setConfig({});
|
|
56
|
+
}}
|
|
57
|
+
config={config}
|
|
58
|
+
onConfigChange={setConfig}
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const Default: Story = { render: () => <Demo /> };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Button } from "../src/components/Button";
|
|
3
|
+
import {
|
|
4
|
+
Popover,
|
|
5
|
+
PopoverContent,
|
|
6
|
+
PopoverTrigger,
|
|
7
|
+
} from "../src/components/Popover";
|
|
8
|
+
|
|
9
|
+
const meta: Meta = {
|
|
10
|
+
title: "Components/Overlays/Popover",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
type Story = StoryObj;
|
|
15
|
+
|
|
16
|
+
export const Default: Story = {
|
|
17
|
+
render: () => (
|
|
18
|
+
<Popover>
|
|
19
|
+
<PopoverTrigger asChild>
|
|
20
|
+
<Button variant="outline">Open popover</Button>
|
|
21
|
+
</PopoverTrigger>
|
|
22
|
+
<PopoverContent className="w-80 p-4">
|
|
23
|
+
<h4 className="font-medium leading-none mb-2">Filter</h4>
|
|
24
|
+
<p className="text-sm text-muted-foreground">
|
|
25
|
+
Use this for filter chips, info hovers, or quick edits.
|
|
26
|
+
</p>
|
|
27
|
+
</PopoverContent>
|
|
28
|
+
</Popover>
|
|
29
|
+
),
|
|
30
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Activity } from "lucide-react";
|
|
3
|
+
import { SectionHeader } from "../src/components/SectionHeader";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof SectionHeader> = {
|
|
6
|
+
title: "Components/Layout/SectionHeader",
|
|
7
|
+
component: SectionHeader,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof SectionHeader>;
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
args: {
|
|
16
|
+
title: "Recent activity",
|
|
17
|
+
description: "Last 24 hours of health-check transitions.",
|
|
18
|
+
icon: <Activity className="h-5 w-5" />,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
Select,
|
|
5
|
+
SelectContent,
|
|
6
|
+
SelectItem,
|
|
7
|
+
SelectTrigger,
|
|
8
|
+
SelectValue,
|
|
9
|
+
} from "../src/components/Select";
|
|
10
|
+
|
|
11
|
+
const meta: Meta = {
|
|
12
|
+
title: "Components/Inputs/Select",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default meta;
|
|
16
|
+
type Story = StoryObj;
|
|
17
|
+
|
|
18
|
+
const Demo = () => {
|
|
19
|
+
const [value, setValue] = useState("");
|
|
20
|
+
return (
|
|
21
|
+
<div className="max-w-xs">
|
|
22
|
+
<Select value={value} onValueChange={setValue}>
|
|
23
|
+
<SelectTrigger>
|
|
24
|
+
<SelectValue placeholder="Pick an environment" />
|
|
25
|
+
</SelectTrigger>
|
|
26
|
+
<SelectContent>
|
|
27
|
+
<SelectItem value="prod">Production</SelectItem>
|
|
28
|
+
<SelectItem value="staging">Staging</SelectItem>
|
|
29
|
+
<SelectItem value="dev">Development</SelectItem>
|
|
30
|
+
</SelectContent>
|
|
31
|
+
</Select>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const Default: Story = { render: () => <Demo /> };
|