@sero-ai/ui 0.3.2 → 0.4.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/.turbo/turbo-build.log +615 -0
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +58 -0
- package/dist/components/context-editor/CapabilitySection.cjs +57 -0
- package/dist/components/context-editor/CapabilitySection.d.cts +20 -0
- package/dist/components/context-editor/CapabilitySection.d.ts +20 -0
- package/dist/components/context-editor/CapabilitySection.js +57 -0
- package/dist/components/context-editor/ContextEditor.cjs +108 -0
- package/dist/components/context-editor/ContextEditor.d.cts +26 -0
- package/dist/components/context-editor/ContextEditor.d.ts +26 -0
- package/dist/components/context-editor/ContextEditor.js +108 -0
- package/dist/components/context-editor/PresetBar.cjs +71 -0
- package/dist/components/context-editor/PresetBar.d.cts +19 -0
- package/dist/components/context-editor/PresetBar.d.ts +19 -0
- package/dist/components/context-editor/PresetBar.js +71 -0
- package/dist/components/context-editor/SkillsSection.cjs +31 -0
- package/dist/components/context-editor/SkillsSection.d.cts +14 -0
- package/dist/components/context-editor/SkillsSection.d.ts +14 -0
- package/dist/components/context-editor/SkillsSection.js +31 -0
- package/dist/components/context-editor/SystemPromptSection.cjs +58 -0
- package/dist/components/context-editor/SystemPromptSection.d.cts +25 -0
- package/dist/components/context-editor/SystemPromptSection.d.ts +25 -0
- package/dist/components/context-editor/SystemPromptSection.js +58 -0
- package/dist/components/context-editor/ToolsSection.cjs +31 -0
- package/dist/components/context-editor/ToolsSection.d.cts +14 -0
- package/dist/components/context-editor/ToolsSection.d.ts +14 -0
- package/dist/components/context-editor/ToolsSection.js +31 -0
- package/dist/components/context-editor/index.cjs +8 -0
- package/dist/components/context-editor/index.d.cts +5 -0
- package/dist/components/context-editor/index.d.ts +5 -0
- package/dist/components/context-editor/index.js +8 -0
- package/dist/components/context-editor/parts.cjs +184 -0
- package/dist/components/context-editor/parts.d.cts +32 -0
- package/dist/components/context-editor/parts.d.ts +32 -0
- package/dist/components/context-editor/parts.js +184 -0
- package/dist/components/context-editor/use-context-editor-model.cjs +194 -0
- package/dist/components/context-editor/use-context-editor-model.d.cts +57 -0
- package/dist/components/context-editor/use-context-editor-model.d.ts +57 -0
- package/dist/components/context-editor/use-context-editor-model.js +194 -0
- package/dist/components/dashboard/activity-list.cjs +59 -0
- package/dist/components/dashboard/activity-list.d.cts +27 -0
- package/dist/components/dashboard/activity-list.d.ts +27 -0
- package/dist/components/dashboard/activity-list.js +59 -0
- package/dist/components/dashboard/catalog.cjs +1 -0
- package/dist/components/dashboard/catalog.d.cts +13 -0
- package/dist/components/dashboard/catalog.d.ts +13 -0
- package/dist/components/dashboard/catalog.js +0 -0
- package/dist/components/dashboard/catalog.json +312 -0
- package/dist/components/dashboard/data-boundary.cjs +29 -0
- package/dist/components/dashboard/data-boundary.d.cts +21 -0
- package/dist/components/dashboard/data-boundary.d.ts +21 -0
- package/dist/components/dashboard/data-boundary.js +29 -0
- package/dist/components/dashboard/empty-state.cjs +49 -0
- package/dist/components/dashboard/empty-state.d.cts +20 -0
- package/dist/components/dashboard/empty-state.d.ts +20 -0
- package/dist/components/dashboard/empty-state.js +49 -0
- package/dist/components/dashboard/icon-button.cjs +40 -0
- package/dist/components/dashboard/icon-button.d.cts +24 -0
- package/dist/components/dashboard/icon-button.d.ts +24 -0
- package/dist/components/dashboard/icon-button.js +40 -0
- package/dist/components/dashboard/index.cjs +15 -0
- package/dist/components/dashboard/index.d.cts +24 -0
- package/dist/components/dashboard/index.d.ts +24 -0
- package/dist/components/dashboard/index.js +15 -0
- package/dist/components/dashboard/item-list.cjs +71 -0
- package/dist/components/dashboard/item-list.d.cts +28 -0
- package/dist/components/dashboard/item-list.d.ts +28 -0
- package/dist/components/dashboard/item-list.js +71 -0
- package/dist/components/dashboard/key-value.cjs +39 -0
- package/dist/components/dashboard/key-value.d.cts +17 -0
- package/dist/components/dashboard/key-value.d.ts +17 -0
- package/dist/components/dashboard/key-value.js +39 -0
- package/dist/components/dashboard/layout.cjs +193 -0
- package/dist/components/dashboard/layout.d.cts +91 -0
- package/dist/components/dashboard/layout.d.ts +91 -0
- package/dist/components/dashboard/layout.js +193 -0
- package/dist/components/dashboard/metric.cjs +98 -0
- package/dist/components/dashboard/metric.d.cts +44 -0
- package/dist/components/dashboard/metric.d.ts +44 -0
- package/dist/components/dashboard/metric.js +98 -0
- package/dist/components/dashboard/progress-ring.cjs +77 -0
- package/dist/components/dashboard/progress-ring.d.cts +25 -0
- package/dist/components/dashboard/progress-ring.d.ts +25 -0
- package/dist/components/dashboard/progress-ring.js +77 -0
- package/dist/components/dashboard/skeletons.cjs +54 -0
- package/dist/components/dashboard/skeletons.d.cts +15 -0
- package/dist/components/dashboard/skeletons.d.ts +15 -0
- package/dist/components/dashboard/skeletons.js +54 -0
- package/dist/components/dashboard/spacing.cjs +10 -0
- package/dist/components/dashboard/spacing.d.cts +4 -0
- package/dist/components/dashboard/spacing.d.ts +4 -0
- package/dist/components/dashboard/spacing.js +10 -0
- package/dist/components/dashboard/stale-indicator.cjs +42 -0
- package/dist/components/dashboard/stale-indicator.d.cts +15 -0
- package/dist/components/dashboard/stale-indicator.d.ts +15 -0
- package/dist/components/dashboard/stale-indicator.js +42 -0
- package/dist/components/dashboard/status.cjs +66 -0
- package/dist/components/dashboard/status.d.cts +15 -0
- package/dist/components/dashboard/status.d.ts +15 -0
- package/dist/components/dashboard/status.js +66 -0
- package/dist/components/dashboard/tone.cjs +34 -0
- package/dist/components/dashboard/tone.d.cts +15 -0
- package/dist/components/dashboard/tone.d.ts +15 -0
- package/dist/components/dashboard/tone.js +34 -0
- package/dist/components/dashboard/typography.cjs +99 -0
- package/dist/components/dashboard/typography.d.cts +57 -0
- package/dist/components/dashboard/typography.d.ts +57 -0
- package/dist/components/dashboard/typography.js +99 -0
- package/dist/components/ui/badge.cjs +5 -1
- package/dist/components/ui/badge.d.cts +1 -1
- package/dist/components/ui/badge.d.ts +1 -1
- package/dist/components/ui/badge.js +5 -1
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +1 -0
- package/dist/reference.cjs +1260 -0
- package/dist/reference.d.cts +11 -0
- package/dist/reference.d.ts +11 -0
- package/dist/reference.js +1230 -0
- package/dist/styles/glass-board.css +43 -0
- package/dist/styles/globals.css +59 -0
- package/package.json +16 -4
- package/scripts/ensure-pnpm-publish.mjs +23 -0
- package/scripts/smoke-dist.mjs +68 -0
- package/src/components/context-editor/CapabilitySection.tsx +85 -0
- package/src/components/context-editor/ContextEditor.tsx +138 -0
- package/src/components/context-editor/PresetBar.tsx +94 -0
- package/src/components/context-editor/SkillsSection.tsx +37 -0
- package/src/components/context-editor/SystemPromptSection.tsx +88 -0
- package/src/components/context-editor/ToolsSection.tsx +37 -0
- package/src/components/context-editor/index.ts +7 -0
- package/src/components/context-editor/parts.tsx +217 -0
- package/src/components/context-editor/use-context-editor-model.ts +296 -0
- package/src/components/dashboard/__tests__/catalog.test.ts +62 -0
- package/src/components/dashboard/__tests__/glass.test.tsx +42 -0
- package/src/components/dashboard/__tests__/state.test.tsx +127 -0
- package/src/components/dashboard/__tests__/variants.test.tsx +95 -0
- package/src/components/dashboard/activity-list.tsx +99 -0
- package/src/components/dashboard/catalog.json +312 -0
- package/src/components/dashboard/catalog.ts +29 -0
- package/src/components/dashboard/data-boundary.tsx +54 -0
- package/src/components/dashboard/empty-state.tsx +70 -0
- package/src/components/dashboard/icon-button.tsx +58 -0
- package/src/components/dashboard/index.ts +21 -0
- package/src/components/dashboard/item-list.tsx +106 -0
- package/src/components/dashboard/key-value.tsx +54 -0
- package/src/components/dashboard/layout.tsx +286 -0
- package/src/components/dashboard/metric.tsx +139 -0
- package/src/components/dashboard/progress-ring.tsx +100 -0
- package/src/components/dashboard/skeletons.tsx +73 -0
- package/src/components/dashboard/spacing.ts +14 -0
- package/src/components/dashboard/stale-indicator.tsx +55 -0
- package/src/components/dashboard/status.tsx +88 -0
- package/src/components/dashboard/tone.ts +48 -0
- package/src/components/dashboard/typography.tsx +151 -0
- package/src/components/reference/ActivityExample.tsx +68 -0
- package/src/components/reference/ResourceExample.tsx +78 -0
- package/src/components/reference/SchedulerExample.tsx +88 -0
- package/src/components/reference/StarterExample.tsx +48 -0
- package/src/components/reference/index.ts +10 -0
- package/src/components/ui/badge.tsx +6 -0
- package/src/index.ts +7 -0
- package/src/reference.ts +15 -0
- package/src/styles/glass-board.css +43 -0
- package/src/styles/globals.css +59 -0
- package/tsup.config.ts +18 -4
- package/tsup.reference.config.ts +15 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Catalogue integrity — keeps the JSON catalogue honest against the exports.
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import type { DashboardComponentCatalogEntry } from "../catalog";
|
|
6
|
+
import catalogJson from "../catalog.json";
|
|
7
|
+
import * as dashboard from "../index";
|
|
8
|
+
|
|
9
|
+
// The JSON is the source of truth. JSON imports widen literal unions to
|
|
10
|
+
// `string`, so assert the authored entry shape for the checks below.
|
|
11
|
+
const dashboardComponentCatalog = catalogJson as DashboardComponentCatalogEntry[];
|
|
12
|
+
|
|
13
|
+
// Reused primitives live in other component folders, not the dashboard barrel.
|
|
14
|
+
const REUSED_PRIMITIVES = new Set(["Badge", "Alert", "Button", "Skeleton"]);
|
|
15
|
+
|
|
16
|
+
const names = dashboardComponentCatalog.map((e) => e.name);
|
|
17
|
+
|
|
18
|
+
describe("dashboard catalogue", () => {
|
|
19
|
+
it("has no duplicate component names", () => {
|
|
20
|
+
expect(new Set(names).size).toBe(names.length);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("every composite entry is exported from the dashboard barrel", () => {
|
|
24
|
+
const missing = dashboardComponentCatalog
|
|
25
|
+
.filter((e) => e.kind === "composite")
|
|
26
|
+
.filter((e) => !(e.name in dashboard))
|
|
27
|
+
.map((e) => e.name);
|
|
28
|
+
expect(missing).toEqual([]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("every primitive entry is a known reused primitive", () => {
|
|
32
|
+
const bad = dashboardComponentCatalog
|
|
33
|
+
.filter((e) => e.kind === "primitive")
|
|
34
|
+
.filter((e) => !REUSED_PRIMITIVES.has(e.name))
|
|
35
|
+
.map((e) => e.name);
|
|
36
|
+
expect(bad).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("every related name resolves to a catalogue entry", () => {
|
|
40
|
+
const known = new Set(names);
|
|
41
|
+
const broken: string[] = [];
|
|
42
|
+
for (const entry of dashboardComponentCatalog) {
|
|
43
|
+
for (const rel of entry.related ?? []) {
|
|
44
|
+
if (!known.has(rel)) broken.push(`${entry.name} -> ${rel}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
expect(broken).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("every composite exported from the barrel appears in the catalogue", () => {
|
|
51
|
+
// The barrel also exports helpers (tone maps, gapClass) and types; only
|
|
52
|
+
// check the React components (PascalCase functions) are catalogued.
|
|
53
|
+
const catalogued = new Set(names);
|
|
54
|
+
const uncatalogued = Object.entries(dashboard)
|
|
55
|
+
.filter(
|
|
56
|
+
([name, value]) =>
|
|
57
|
+
/^[A-Z]/.test(name) && typeof value === "function" && !catalogued.has(name),
|
|
58
|
+
)
|
|
59
|
+
.map(([name]) => name);
|
|
60
|
+
expect(uncatalogued).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Glass-scope tests: WidgetContent applies the token scope, and surfaces
|
|
2
|
+
// route through the scope-aware `--surface-*` tokens rather than solid ones.
|
|
3
|
+
|
|
4
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { WidgetContent } from "../layout";
|
|
8
|
+
import { MetricCard } from "../metric";
|
|
9
|
+
import { ItemList, ItemListItem } from "../item-list";
|
|
10
|
+
|
|
11
|
+
describe("WidgetContent glass scope", () => {
|
|
12
|
+
it("applies the glass class by default", () => {
|
|
13
|
+
const html = renderToStaticMarkup(<WidgetContent />);
|
|
14
|
+
expect(html).toContain("glass");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("omits the glass class when glass={false}", () => {
|
|
18
|
+
const html = renderToStaticMarkup(<WidgetContent glass={false} />);
|
|
19
|
+
expect(html).not.toContain(' class="glass');
|
|
20
|
+
expect(html).not.toMatch(/\bglass\b/);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("surface token routing", () => {
|
|
25
|
+
it("MetricCard reads raised surface + rim tokens, not solid bg-surface", () => {
|
|
26
|
+
const html = renderToStaticMarkup(<MetricCard />);
|
|
27
|
+
expect(html).toContain("--surface-raised");
|
|
28
|
+
expect(html).toContain("--surface-line");
|
|
29
|
+
expect(html).toContain("--surface-rim");
|
|
30
|
+
expect(html).not.toContain("--bg-surface");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("ItemListItem reads the flat surface token, not the muted variant", () => {
|
|
34
|
+
const html = renderToStaticMarkup(
|
|
35
|
+
<ItemList>
|
|
36
|
+
<ItemListItem primary="Row" />
|
|
37
|
+
</ItemList>,
|
|
38
|
+
);
|
|
39
|
+
expect(html).toContain("--surface-flat");
|
|
40
|
+
expect(html).not.toContain("bg-muted/50");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// State-selection + accessibility-contract tests for dashboard components.
|
|
2
|
+
|
|
3
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
4
|
+
import { RotateCw, Clock } from "lucide-react";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { DataBoundary } from "../data-boundary";
|
|
8
|
+
import { EmptyState } from "../empty-state";
|
|
9
|
+
import { IconButton } from "../icon-button";
|
|
10
|
+
import { Icon } from "../typography";
|
|
11
|
+
import { ProgressRing } from "../progress-ring";
|
|
12
|
+
import { Status } from "../status";
|
|
13
|
+
|
|
14
|
+
describe("DataBoundary", () => {
|
|
15
|
+
const slots = {
|
|
16
|
+
loading: <div>LOADING</div>,
|
|
17
|
+
empty: <div>EMPTY</div>,
|
|
18
|
+
error: <div>ERROR</div>,
|
|
19
|
+
stale: <div>STALE</div>,
|
|
20
|
+
children: <div>READY</div>,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
it("renders only the loading slot when loading", () => {
|
|
24
|
+
const html = renderToStaticMarkup(
|
|
25
|
+
<DataBoundary state="loading" {...slots} />,
|
|
26
|
+
);
|
|
27
|
+
expect(html).toContain("LOADING");
|
|
28
|
+
expect(html).not.toContain("READY");
|
|
29
|
+
expect(html).not.toContain("EMPTY");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("renders only the empty slot when empty", () => {
|
|
33
|
+
const html = renderToStaticMarkup(<DataBoundary state="empty" {...slots} />);
|
|
34
|
+
expect(html).toContain("EMPTY");
|
|
35
|
+
expect(html).not.toContain("READY");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("renders only the error slot when error", () => {
|
|
39
|
+
const html = renderToStaticMarkup(<DataBoundary state="error" {...slots} />);
|
|
40
|
+
expect(html).toContain("ERROR");
|
|
41
|
+
expect(html).not.toContain("READY");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("renders the stale banner above ready content when stale", () => {
|
|
45
|
+
const html = renderToStaticMarkup(<DataBoundary state="stale" {...slots} />);
|
|
46
|
+
expect(html).toContain("STALE");
|
|
47
|
+
expect(html).toContain("READY");
|
|
48
|
+
expect(html.indexOf("STALE")).toBeLessThan(html.indexOf("READY"));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("renders ready children when ready", () => {
|
|
52
|
+
const html = renderToStaticMarkup(<DataBoundary state="ready" {...slots} />);
|
|
53
|
+
expect(html).toContain("READY");
|
|
54
|
+
expect(html).not.toContain("STALE");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("accessibility contracts", () => {
|
|
59
|
+
it("ProgressRing exposes progressbar value semantics", () => {
|
|
60
|
+
const html = renderToStaticMarkup(
|
|
61
|
+
<ProgressRing value={30} max={60} label="Usage" />,
|
|
62
|
+
);
|
|
63
|
+
expect(html).toContain('role="progressbar"');
|
|
64
|
+
expect(html).toContain('aria-label="Usage"');
|
|
65
|
+
expect(html).toContain('aria-valuenow="30"');
|
|
66
|
+
expect(html).toContain('aria-valuemax="60"');
|
|
67
|
+
// 30 / 60 = 50%
|
|
68
|
+
expect(html).toContain("50%");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("ProgressRing clamps out-of-range values", () => {
|
|
72
|
+
const html = renderToStaticMarkup(<ProgressRing value={999} max={100} />);
|
|
73
|
+
expect(html).toContain('aria-valuenow="100"');
|
|
74
|
+
expect(html).toContain("100%");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("Icon is hidden from assistive tech without a label", () => {
|
|
78
|
+
const html = renderToStaticMarkup(<Icon icon={Clock} />);
|
|
79
|
+
expect(html).toContain('aria-hidden="true"');
|
|
80
|
+
expect(html).not.toContain('role="img"');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("Icon is exposed as an image with a label", () => {
|
|
84
|
+
const html = renderToStaticMarkup(<Icon icon={Clock} label="Time" />);
|
|
85
|
+
expect(html).toContain('role="img"');
|
|
86
|
+
expect(html).toContain('aria-label="Time"');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("IconButton carries its required accessible label", () => {
|
|
90
|
+
const html = renderToStaticMarkup(
|
|
91
|
+
<IconButton icon={RotateCw} label="Refresh" />,
|
|
92
|
+
);
|
|
93
|
+
expect(html).toContain('aria-label="Refresh"');
|
|
94
|
+
expect(html).toContain('type="button"');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("a bare Status dot exposes its tone as announceable text", () => {
|
|
98
|
+
const html = renderToStaticMarkup(<Status tone="success" />);
|
|
99
|
+
expect(html).toContain('role="status"');
|
|
100
|
+
// Not an empty live region: assistive tech gets a text alternative.
|
|
101
|
+
expect(html).toContain('class="sr-only">Success<');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("a bare Status dot honours a caller aria-label instead of the tone", () => {
|
|
105
|
+
const html = renderToStaticMarkup(
|
|
106
|
+
<Status tone="success" aria-label="Backup succeeded" />,
|
|
107
|
+
);
|
|
108
|
+
expect(html).toContain('aria-label="Backup succeeded"');
|
|
109
|
+
// The caller supplied the name, so no generic fallback is added.
|
|
110
|
+
expect(html).not.toContain("sr-only");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("a labelled Status uses its visible text and adds no fallback", () => {
|
|
114
|
+
const html = renderToStaticMarkup(<Status tone="neutral">Idle</Status>);
|
|
115
|
+
expect(html).toContain("Idle");
|
|
116
|
+
expect(html).not.toContain('role="status"');
|
|
117
|
+
expect(html).not.toContain("sr-only");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("EmptyState renders its title and message", () => {
|
|
121
|
+
const html = renderToStaticMarkup(
|
|
122
|
+
<EmptyState title="Nothing here" message="Add an item to start" />,
|
|
123
|
+
);
|
|
124
|
+
expect(html).toContain("Nothing here");
|
|
125
|
+
expect(html).toContain("Add an item to start");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Variant / tone tests for dashboard components.
|
|
2
|
+
|
|
3
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { Badge } from "../../ui/badge";
|
|
7
|
+
import { Grid, Inline, Stack } from "../layout";
|
|
8
|
+
import { Metric } from "../metric";
|
|
9
|
+
import { Status } from "../status";
|
|
10
|
+
import { Text } from "../typography";
|
|
11
|
+
|
|
12
|
+
describe("Status tones", () => {
|
|
13
|
+
it("renders a tinted pill for a tone", () => {
|
|
14
|
+
const html = renderToStaticMarkup(
|
|
15
|
+
<Status tone="warning" variant="pill">
|
|
16
|
+
Degraded
|
|
17
|
+
</Status>,
|
|
18
|
+
);
|
|
19
|
+
expect(html).toContain("--status-warning-muted");
|
|
20
|
+
expect(html).toContain("Degraded");
|
|
21
|
+
expect(html).toContain('data-tone="warning"');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("maps neutral tone to the muted token", () => {
|
|
25
|
+
const html = renderToStaticMarkup(<Status tone="neutral">Idle</Status>);
|
|
26
|
+
expect(html).toContain("--text-muted");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("Badge semantic tones", () => {
|
|
31
|
+
it.each([
|
|
32
|
+
["success", "--status-success"],
|
|
33
|
+
["warning", "--status-warning"],
|
|
34
|
+
["info", "--status-info"],
|
|
35
|
+
] as const)("badge %s uses its status token", (variant, token) => {
|
|
36
|
+
const html = renderToStaticMarkup(<Badge variant={variant}>x</Badge>);
|
|
37
|
+
expect(html).toContain(token);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("Text variants", () => {
|
|
42
|
+
it("numeric variant uses tabular numerals", () => {
|
|
43
|
+
const html = renderToStaticMarkup(<Text variant="numeric">42</Text>);
|
|
44
|
+
expect(html).toContain("tabular-nums");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("clamp overrides truncate", () => {
|
|
48
|
+
const html = renderToStaticMarkup(
|
|
49
|
+
<Text truncate clamp={2}>
|
|
50
|
+
long
|
|
51
|
+
</Text>,
|
|
52
|
+
);
|
|
53
|
+
expect(html).toContain("line-clamp-2");
|
|
54
|
+
expect(html).not.toContain("truncate");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("Metric", () => {
|
|
59
|
+
it("renders label, value and a trend", () => {
|
|
60
|
+
const html = renderToStaticMarkup(
|
|
61
|
+
<Metric
|
|
62
|
+
label="Jobs"
|
|
63
|
+
value={6}
|
|
64
|
+
trend={{ direction: "up", value: "+2", tone: "success" }}
|
|
65
|
+
/>,
|
|
66
|
+
);
|
|
67
|
+
expect(html).toContain("Jobs");
|
|
68
|
+
expect(html).toContain("6");
|
|
69
|
+
expect(html).toContain("+2");
|
|
70
|
+
expect(html).toContain("--status-success");
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("layout spacing scale", () => {
|
|
75
|
+
it("Stack maps semantic gap to a utility", () => {
|
|
76
|
+
expect(renderToStaticMarkup(<Stack gap="lg" />)).toContain("gap-4");
|
|
77
|
+
});
|
|
78
|
+
it("Stack fill grows without scrolling; scroll adds overflow", () => {
|
|
79
|
+
const fill = renderToStaticMarkup(<Stack fill />);
|
|
80
|
+
expect(fill).toContain("flex-1");
|
|
81
|
+
expect(fill).not.toContain("overflow-auto");
|
|
82
|
+
expect(renderToStaticMarkup(<Stack scroll />)).toContain("overflow-auto");
|
|
83
|
+
});
|
|
84
|
+
it("Inline wraps when asked", () => {
|
|
85
|
+
expect(renderToStaticMarkup(<Inline wrap />)).toContain("flex-wrap");
|
|
86
|
+
});
|
|
87
|
+
it("Grid with fixed columns uses a column utility", () => {
|
|
88
|
+
expect(renderToStaticMarkup(<Grid columns={3} />)).toContain("grid-cols-3");
|
|
89
|
+
});
|
|
90
|
+
it("Grid auto uses an inline template", () => {
|
|
91
|
+
expect(renderToStaticMarkup(<Grid columns="auto" minColumnWidth={80} />)).toContain(
|
|
92
|
+
"minmax(80px",
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// ActivityList and ActivityListItem — a compact chronological event list.
|
|
2
|
+
//
|
|
3
|
+
// Each row carries a status marker or icon, a label, a timestamp and optional
|
|
4
|
+
// supporting detail. Timestamps are passed as pre-formatted strings — the
|
|
5
|
+
// component never computes relative time, so it stays stateless and the plugin
|
|
6
|
+
// owns any refresh.
|
|
7
|
+
|
|
8
|
+
import * as React from "react";
|
|
9
|
+
|
|
10
|
+
import { cn } from "../../lib/utils";
|
|
11
|
+
import { Icon } from "./typography";
|
|
12
|
+
import { toneDot, type Tone } from "./tone";
|
|
13
|
+
|
|
14
|
+
export interface ActivityListItemProps
|
|
15
|
+
extends Omit<React.ComponentProps<"li">, "title"> {
|
|
16
|
+
label: React.ReactNode;
|
|
17
|
+
/** Pre-formatted timestamp, e.g. "08:00" or "in 2h". */
|
|
18
|
+
timestamp?: React.ReactNode;
|
|
19
|
+
/** Supporting detail under the label. */
|
|
20
|
+
detail?: React.ReactNode;
|
|
21
|
+
/** Status marker tone. Ignored when `icon` is provided. */
|
|
22
|
+
tone?: Tone;
|
|
23
|
+
/** Leading icon component, used instead of the status dot. */
|
|
24
|
+
icon?: React.ComponentType<{ className?: string }>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A single chronological event row. */
|
|
28
|
+
function ActivityListItem({
|
|
29
|
+
className,
|
|
30
|
+
label,
|
|
31
|
+
timestamp,
|
|
32
|
+
detail,
|
|
33
|
+
tone = "neutral",
|
|
34
|
+
icon,
|
|
35
|
+
...props
|
|
36
|
+
}: ActivityListItemProps) {
|
|
37
|
+
return (
|
|
38
|
+
<li
|
|
39
|
+
data-slot="activity-list-item"
|
|
40
|
+
className={cn("flex items-start gap-2 py-1", className)}
|
|
41
|
+
{...props}
|
|
42
|
+
>
|
|
43
|
+
<span className="mt-1 flex size-3.5 shrink-0 items-center justify-center">
|
|
44
|
+
{icon ? (
|
|
45
|
+
<Icon icon={icon} size="xs" tone={tone === "neutral" ? undefined : tone} />
|
|
46
|
+
) : (
|
|
47
|
+
<span className={cn("size-2 rounded-full", toneDot[tone])} />
|
|
48
|
+
)}
|
|
49
|
+
</span>
|
|
50
|
+
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
51
|
+
<div className="flex items-baseline justify-between gap-2">
|
|
52
|
+
<span className="truncate text-xs text-[var(--text-primary)]">
|
|
53
|
+
{label}
|
|
54
|
+
</span>
|
|
55
|
+
{timestamp && (
|
|
56
|
+
<span className="shrink-0 text-[11px] tabular-nums text-[var(--text-muted)]">
|
|
57
|
+
{timestamp}
|
|
58
|
+
</span>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
{detail && (
|
|
62
|
+
<span className="line-clamp-1 text-xs text-[var(--text-muted)]">
|
|
63
|
+
{detail}
|
|
64
|
+
</span>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
</li>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ActivityListProps extends React.ComponentProps<"ul"> {
|
|
72
|
+
/** Count of events not shown; renders a "+N more" footer when > 0. */
|
|
73
|
+
overflowCount?: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A chronological list of ActivityListItem rows. */
|
|
77
|
+
function ActivityList({
|
|
78
|
+
className,
|
|
79
|
+
overflowCount = 0,
|
|
80
|
+
children,
|
|
81
|
+
...props
|
|
82
|
+
}: ActivityListProps) {
|
|
83
|
+
return (
|
|
84
|
+
<ul
|
|
85
|
+
data-slot="activity-list"
|
|
86
|
+
className={cn("flex min-w-0 flex-col", className)}
|
|
87
|
+
{...props}
|
|
88
|
+
>
|
|
89
|
+
{children}
|
|
90
|
+
{overflowCount > 0 && (
|
|
91
|
+
<li className="pl-5 pt-0.5 text-[11px] text-[var(--text-muted)]">
|
|
92
|
+
+{overflowCount} more
|
|
93
|
+
</li>
|
|
94
|
+
)}
|
|
95
|
+
</ul>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { ActivityList, ActivityListItem };
|