@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.
Files changed (169) hide show
  1. package/.turbo/turbo-build.log +615 -0
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/CHANGELOG.md +58 -0
  4. package/dist/components/context-editor/CapabilitySection.cjs +57 -0
  5. package/dist/components/context-editor/CapabilitySection.d.cts +20 -0
  6. package/dist/components/context-editor/CapabilitySection.d.ts +20 -0
  7. package/dist/components/context-editor/CapabilitySection.js +57 -0
  8. package/dist/components/context-editor/ContextEditor.cjs +108 -0
  9. package/dist/components/context-editor/ContextEditor.d.cts +26 -0
  10. package/dist/components/context-editor/ContextEditor.d.ts +26 -0
  11. package/dist/components/context-editor/ContextEditor.js +108 -0
  12. package/dist/components/context-editor/PresetBar.cjs +71 -0
  13. package/dist/components/context-editor/PresetBar.d.cts +19 -0
  14. package/dist/components/context-editor/PresetBar.d.ts +19 -0
  15. package/dist/components/context-editor/PresetBar.js +71 -0
  16. package/dist/components/context-editor/SkillsSection.cjs +31 -0
  17. package/dist/components/context-editor/SkillsSection.d.cts +14 -0
  18. package/dist/components/context-editor/SkillsSection.d.ts +14 -0
  19. package/dist/components/context-editor/SkillsSection.js +31 -0
  20. package/dist/components/context-editor/SystemPromptSection.cjs +58 -0
  21. package/dist/components/context-editor/SystemPromptSection.d.cts +25 -0
  22. package/dist/components/context-editor/SystemPromptSection.d.ts +25 -0
  23. package/dist/components/context-editor/SystemPromptSection.js +58 -0
  24. package/dist/components/context-editor/ToolsSection.cjs +31 -0
  25. package/dist/components/context-editor/ToolsSection.d.cts +14 -0
  26. package/dist/components/context-editor/ToolsSection.d.ts +14 -0
  27. package/dist/components/context-editor/ToolsSection.js +31 -0
  28. package/dist/components/context-editor/index.cjs +8 -0
  29. package/dist/components/context-editor/index.d.cts +5 -0
  30. package/dist/components/context-editor/index.d.ts +5 -0
  31. package/dist/components/context-editor/index.js +8 -0
  32. package/dist/components/context-editor/parts.cjs +184 -0
  33. package/dist/components/context-editor/parts.d.cts +32 -0
  34. package/dist/components/context-editor/parts.d.ts +32 -0
  35. package/dist/components/context-editor/parts.js +184 -0
  36. package/dist/components/context-editor/use-context-editor-model.cjs +194 -0
  37. package/dist/components/context-editor/use-context-editor-model.d.cts +57 -0
  38. package/dist/components/context-editor/use-context-editor-model.d.ts +57 -0
  39. package/dist/components/context-editor/use-context-editor-model.js +194 -0
  40. package/dist/components/dashboard/activity-list.cjs +59 -0
  41. package/dist/components/dashboard/activity-list.d.cts +27 -0
  42. package/dist/components/dashboard/activity-list.d.ts +27 -0
  43. package/dist/components/dashboard/activity-list.js +59 -0
  44. package/dist/components/dashboard/catalog.cjs +1 -0
  45. package/dist/components/dashboard/catalog.d.cts +13 -0
  46. package/dist/components/dashboard/catalog.d.ts +13 -0
  47. package/dist/components/dashboard/catalog.js +0 -0
  48. package/dist/components/dashboard/catalog.json +312 -0
  49. package/dist/components/dashboard/data-boundary.cjs +29 -0
  50. package/dist/components/dashboard/data-boundary.d.cts +21 -0
  51. package/dist/components/dashboard/data-boundary.d.ts +21 -0
  52. package/dist/components/dashboard/data-boundary.js +29 -0
  53. package/dist/components/dashboard/empty-state.cjs +49 -0
  54. package/dist/components/dashboard/empty-state.d.cts +20 -0
  55. package/dist/components/dashboard/empty-state.d.ts +20 -0
  56. package/dist/components/dashboard/empty-state.js +49 -0
  57. package/dist/components/dashboard/icon-button.cjs +40 -0
  58. package/dist/components/dashboard/icon-button.d.cts +24 -0
  59. package/dist/components/dashboard/icon-button.d.ts +24 -0
  60. package/dist/components/dashboard/icon-button.js +40 -0
  61. package/dist/components/dashboard/index.cjs +15 -0
  62. package/dist/components/dashboard/index.d.cts +24 -0
  63. package/dist/components/dashboard/index.d.ts +24 -0
  64. package/dist/components/dashboard/index.js +15 -0
  65. package/dist/components/dashboard/item-list.cjs +71 -0
  66. package/dist/components/dashboard/item-list.d.cts +28 -0
  67. package/dist/components/dashboard/item-list.d.ts +28 -0
  68. package/dist/components/dashboard/item-list.js +71 -0
  69. package/dist/components/dashboard/key-value.cjs +39 -0
  70. package/dist/components/dashboard/key-value.d.cts +17 -0
  71. package/dist/components/dashboard/key-value.d.ts +17 -0
  72. package/dist/components/dashboard/key-value.js +39 -0
  73. package/dist/components/dashboard/layout.cjs +193 -0
  74. package/dist/components/dashboard/layout.d.cts +91 -0
  75. package/dist/components/dashboard/layout.d.ts +91 -0
  76. package/dist/components/dashboard/layout.js +193 -0
  77. package/dist/components/dashboard/metric.cjs +98 -0
  78. package/dist/components/dashboard/metric.d.cts +44 -0
  79. package/dist/components/dashboard/metric.d.ts +44 -0
  80. package/dist/components/dashboard/metric.js +98 -0
  81. package/dist/components/dashboard/progress-ring.cjs +77 -0
  82. package/dist/components/dashboard/progress-ring.d.cts +25 -0
  83. package/dist/components/dashboard/progress-ring.d.ts +25 -0
  84. package/dist/components/dashboard/progress-ring.js +77 -0
  85. package/dist/components/dashboard/skeletons.cjs +54 -0
  86. package/dist/components/dashboard/skeletons.d.cts +15 -0
  87. package/dist/components/dashboard/skeletons.d.ts +15 -0
  88. package/dist/components/dashboard/skeletons.js +54 -0
  89. package/dist/components/dashboard/spacing.cjs +10 -0
  90. package/dist/components/dashboard/spacing.d.cts +4 -0
  91. package/dist/components/dashboard/spacing.d.ts +4 -0
  92. package/dist/components/dashboard/spacing.js +10 -0
  93. package/dist/components/dashboard/stale-indicator.cjs +42 -0
  94. package/dist/components/dashboard/stale-indicator.d.cts +15 -0
  95. package/dist/components/dashboard/stale-indicator.d.ts +15 -0
  96. package/dist/components/dashboard/stale-indicator.js +42 -0
  97. package/dist/components/dashboard/status.cjs +66 -0
  98. package/dist/components/dashboard/status.d.cts +15 -0
  99. package/dist/components/dashboard/status.d.ts +15 -0
  100. package/dist/components/dashboard/status.js +66 -0
  101. package/dist/components/dashboard/tone.cjs +34 -0
  102. package/dist/components/dashboard/tone.d.cts +15 -0
  103. package/dist/components/dashboard/tone.d.ts +15 -0
  104. package/dist/components/dashboard/tone.js +34 -0
  105. package/dist/components/dashboard/typography.cjs +99 -0
  106. package/dist/components/dashboard/typography.d.cts +57 -0
  107. package/dist/components/dashboard/typography.d.ts +57 -0
  108. package/dist/components/dashboard/typography.js +99 -0
  109. package/dist/components/ui/badge.cjs +5 -1
  110. package/dist/components/ui/badge.d.cts +1 -1
  111. package/dist/components/ui/badge.d.ts +1 -1
  112. package/dist/components/ui/badge.js +5 -1
  113. package/dist/index.cjs +1 -0
  114. package/dist/index.d.cts +16 -0
  115. package/dist/index.d.ts +16 -0
  116. package/dist/index.js +1 -0
  117. package/dist/reference.cjs +1260 -0
  118. package/dist/reference.d.cts +11 -0
  119. package/dist/reference.d.ts +11 -0
  120. package/dist/reference.js +1230 -0
  121. package/dist/styles/glass-board.css +43 -0
  122. package/dist/styles/globals.css +59 -0
  123. package/package.json +16 -4
  124. package/scripts/ensure-pnpm-publish.mjs +23 -0
  125. package/scripts/smoke-dist.mjs +68 -0
  126. package/src/components/context-editor/CapabilitySection.tsx +85 -0
  127. package/src/components/context-editor/ContextEditor.tsx +138 -0
  128. package/src/components/context-editor/PresetBar.tsx +94 -0
  129. package/src/components/context-editor/SkillsSection.tsx +37 -0
  130. package/src/components/context-editor/SystemPromptSection.tsx +88 -0
  131. package/src/components/context-editor/ToolsSection.tsx +37 -0
  132. package/src/components/context-editor/index.ts +7 -0
  133. package/src/components/context-editor/parts.tsx +217 -0
  134. package/src/components/context-editor/use-context-editor-model.ts +296 -0
  135. package/src/components/dashboard/__tests__/catalog.test.ts +62 -0
  136. package/src/components/dashboard/__tests__/glass.test.tsx +42 -0
  137. package/src/components/dashboard/__tests__/state.test.tsx +127 -0
  138. package/src/components/dashboard/__tests__/variants.test.tsx +95 -0
  139. package/src/components/dashboard/activity-list.tsx +99 -0
  140. package/src/components/dashboard/catalog.json +312 -0
  141. package/src/components/dashboard/catalog.ts +29 -0
  142. package/src/components/dashboard/data-boundary.tsx +54 -0
  143. package/src/components/dashboard/empty-state.tsx +70 -0
  144. package/src/components/dashboard/icon-button.tsx +58 -0
  145. package/src/components/dashboard/index.ts +21 -0
  146. package/src/components/dashboard/item-list.tsx +106 -0
  147. package/src/components/dashboard/key-value.tsx +54 -0
  148. package/src/components/dashboard/layout.tsx +286 -0
  149. package/src/components/dashboard/metric.tsx +139 -0
  150. package/src/components/dashboard/progress-ring.tsx +100 -0
  151. package/src/components/dashboard/skeletons.tsx +73 -0
  152. package/src/components/dashboard/spacing.ts +14 -0
  153. package/src/components/dashboard/stale-indicator.tsx +55 -0
  154. package/src/components/dashboard/status.tsx +88 -0
  155. package/src/components/dashboard/tone.ts +48 -0
  156. package/src/components/dashboard/typography.tsx +151 -0
  157. package/src/components/reference/ActivityExample.tsx +68 -0
  158. package/src/components/reference/ResourceExample.tsx +78 -0
  159. package/src/components/reference/SchedulerExample.tsx +88 -0
  160. package/src/components/reference/StarterExample.tsx +48 -0
  161. package/src/components/reference/index.ts +10 -0
  162. package/src/components/ui/badge.tsx +6 -0
  163. package/src/index.ts +7 -0
  164. package/src/reference.ts +15 -0
  165. package/src/styles/glass-board.css +43 -0
  166. package/src/styles/globals.css +59 -0
  167. package/tsup.config.ts +18 -4
  168. package/tsup.reference.config.ts +15 -0
  169. package/vitest.config.ts +8 -0
@@ -0,0 +1,54 @@
1
+ // KeyValue and KeyValueList — compact label/value metadata.
2
+ //
3
+ // For configuration summaries, totals and metadata where a full metric is too
4
+ // heavy. Uses a semantic description list with predictable alignment and
5
+ // value truncation.
6
+
7
+ import * as React from "react";
8
+
9
+ import { cn } from "../../lib/utils";
10
+
11
+ export interface KeyValueProps extends React.ComponentProps<"div"> {
12
+ label: React.ReactNode;
13
+ value: React.ReactNode;
14
+ /** Use mono + tabular numerals for the value (ids, counts, sizes). */
15
+ mono?: boolean;
16
+ }
17
+
18
+ /** A single label/value row rendered as a `dt`/`dd` pair. */
19
+ function KeyValue({ className, label, value, mono, ...props }: KeyValueProps) {
20
+ return (
21
+ <div
22
+ data-slot="key-value"
23
+ className={cn("flex items-baseline justify-between gap-3", className)}
24
+ {...props}
25
+ >
26
+ <dt className="shrink-0 truncate text-xs text-[var(--text-muted)]">
27
+ {label}
28
+ </dt>
29
+ <dd
30
+ className={cn(
31
+ "min-w-0 truncate text-right text-xs text-[var(--text-primary)]",
32
+ mono && "font-mono tabular-nums",
33
+ )}
34
+ >
35
+ {value}
36
+ </dd>
37
+ </div>
38
+ );
39
+ }
40
+
41
+ export interface KeyValueListProps extends React.ComponentProps<"dl"> {}
42
+
43
+ /** A vertical list of KeyValue rows. */
44
+ function KeyValueList({ className, ...props }: KeyValueListProps) {
45
+ return (
46
+ <dl
47
+ data-slot="key-value-list"
48
+ className={cn("flex flex-col gap-1.5", className)}
49
+ {...props}
50
+ />
51
+ );
52
+ }
53
+
54
+ export { KeyValue, KeyValueList };
@@ -0,0 +1,286 @@
1
+ // Layout and structure primitives for dashboard widgets.
2
+ //
3
+ // WidgetContent, Stack, Inline, Grid, Section and Divider give widgets a
4
+ // consistent frame, spacing and container-query boundary without re-declaring
5
+ // `flex h-full flex-col gap-2 p-3` in every widget. They compose normal React
6
+ // children and accept `className` for escape hatches.
7
+
8
+ import * as React from "react";
9
+ import { cva, type VariantProps } from "class-variance-authority";
10
+
11
+ import { cn } from "../../lib/utils";
12
+ import { Separator } from "../ui/separator";
13
+ import { gapClass, type Gap } from "./spacing";
14
+
15
+ // ── WidgetContent ────────────────────────────────────────────────
16
+
17
+ const widgetContentVariants = cva(
18
+ // Full-height frame with a container-query boundary so child components can
19
+ // respond to the widget's own size rather than the viewport.
20
+ "@container/widget flex h-full min-h-0 flex-col",
21
+ {
22
+ variants: {
23
+ padding: {
24
+ none: "p-0",
25
+ sm: "p-2",
26
+ md: "p-3",
27
+ lg: "p-4",
28
+ },
29
+ scroll: {
30
+ true: "overflow-auto",
31
+ false: "overflow-hidden",
32
+ },
33
+ },
34
+ defaultVariants: { padding: "md", scroll: false },
35
+ },
36
+ );
37
+
38
+ export interface WidgetContentProps
39
+ extends React.ComponentProps<"div">,
40
+ VariantProps<typeof widgetContentVariants> {
41
+ /**
42
+ * Apply the glass token scope so surfaces read as translucent on the
43
+ * frosted dashboard tile. On by default; set `false` for solid full
44
+ * plugin views (outside a dashboard tile).
45
+ */
46
+ glass?: boolean;
47
+ }
48
+
49
+ /** Full-height widget content frame with standard padding + a container query. */
50
+ function WidgetContent({
51
+ className,
52
+ padding,
53
+ scroll,
54
+ glass = true,
55
+ ...props
56
+ }: WidgetContentProps) {
57
+ return (
58
+ <div
59
+ data-slot="widget-content"
60
+ className={cn(widgetContentVariants({ padding, scroll }), glass && "glass", className)}
61
+ {...props}
62
+ />
63
+ );
64
+ }
65
+
66
+ // ── Stack ────────────────────────────────────────────────────────
67
+
68
+ const alignClass = {
69
+ start: "items-start",
70
+ center: "items-center",
71
+ end: "items-end",
72
+ stretch: "items-stretch",
73
+ } as const;
74
+
75
+ const justifyClass = {
76
+ start: "justify-start",
77
+ center: "justify-center",
78
+ end: "justify-end",
79
+ between: "justify-between",
80
+ around: "justify-around",
81
+ } as const;
82
+
83
+ export interface StackProps extends React.ComponentProps<"div"> {
84
+ gap?: Gap;
85
+ align?: keyof typeof alignClass;
86
+ justify?: keyof typeof justifyClass;
87
+ /**
88
+ * Grow to fill the remaining height of a flex parent (e.g. the top-level
89
+ * Stack under `WidgetContent`, so a nested `scroll` region can bound).
90
+ */
91
+ fill?: boolean;
92
+ /** Fill remaining height and scroll overflow. Use for the main content flow. */
93
+ scroll?: boolean;
94
+ }
95
+
96
+ /** Vertical layout with semantic spacing, alignment and optional scrolling. */
97
+ function Stack({
98
+ className,
99
+ gap = "sm",
100
+ align,
101
+ justify,
102
+ fill = false,
103
+ scroll = false,
104
+ ...props
105
+ }: StackProps) {
106
+ return (
107
+ <div
108
+ data-slot="stack"
109
+ className={cn(
110
+ "flex flex-col",
111
+ gapClass[gap],
112
+ align && alignClass[align],
113
+ justify && justifyClass[justify],
114
+ (fill || scroll) && "min-h-0 flex-1",
115
+ scroll && "overflow-auto",
116
+ className,
117
+ )}
118
+ {...props}
119
+ />
120
+ );
121
+ }
122
+
123
+ // ── Inline ───────────────────────────────────────────────────────
124
+
125
+ export interface InlineProps extends React.ComponentProps<"div"> {
126
+ gap?: Gap;
127
+ align?: keyof typeof alignClass;
128
+ justify?: keyof typeof justifyClass;
129
+ wrap?: boolean;
130
+ }
131
+
132
+ /** Horizontal layout with semantic spacing, alignment, wrap and justify. */
133
+ function Inline({
134
+ className,
135
+ gap = "sm",
136
+ align = "center",
137
+ justify,
138
+ wrap = false,
139
+ ...props
140
+ }: InlineProps) {
141
+ return (
142
+ <div
143
+ data-slot="inline"
144
+ className={cn(
145
+ "flex flex-row",
146
+ gapClass[gap],
147
+ alignClass[align],
148
+ justify && justifyClass[justify],
149
+ wrap ? "flex-wrap" : "min-w-0",
150
+ className,
151
+ )}
152
+ {...props}
153
+ />
154
+ );
155
+ }
156
+
157
+ // ── Grid ─────────────────────────────────────────────────────────
158
+
159
+ const columnsClass = {
160
+ 1: "grid-cols-1",
161
+ 2: "grid-cols-2",
162
+ 3: "grid-cols-3",
163
+ 4: "grid-cols-4",
164
+ } as const;
165
+
166
+ export interface GridProps extends React.ComponentProps<"div"> {
167
+ /** Fixed column count, or `auto` for a responsive min-width fill. */
168
+ columns?: keyof typeof columnsClass | "auto";
169
+ gap?: Gap;
170
+ /** Minimum column width for `columns="auto"`. */
171
+ minColumnWidth?: number;
172
+ }
173
+
174
+ /** Responsive grid with bounded columns for metric and summary layouts. */
175
+ function Grid({
176
+ className,
177
+ columns = "auto",
178
+ gap = "sm",
179
+ minColumnWidth = 120,
180
+ style,
181
+ ...props
182
+ }: GridProps) {
183
+ const auto = columns === "auto";
184
+ return (
185
+ <div
186
+ data-slot="grid"
187
+ className={cn(
188
+ "grid",
189
+ gapClass[gap],
190
+ !auto && columnsClass[columns],
191
+ className,
192
+ )}
193
+ style={
194
+ auto
195
+ ? {
196
+ gridTemplateColumns: `repeat(auto-fill, minmax(${minColumnWidth}px, 1fr))`,
197
+ ...style,
198
+ }
199
+ : style
200
+ }
201
+ {...props}
202
+ />
203
+ );
204
+ }
205
+
206
+ // ── Section ──────────────────────────────────────────────────────
207
+
208
+ export interface SectionProps extends React.ComponentProps<"div"> {
209
+ /** Section heading text or node. Omit for an unlabelled group. */
210
+ heading?: React.ReactNode;
211
+ /** Supporting text shown under the heading. */
212
+ description?: React.ReactNode;
213
+ /** Trailing action aligned to the heading row (e.g. an IconButton). */
214
+ action?: React.ReactNode;
215
+ gap?: Gap;
216
+ }
217
+
218
+ /** Compact section with optional heading, description and trailing action. */
219
+ function Section({
220
+ className,
221
+ heading,
222
+ description,
223
+ action,
224
+ gap = "sm",
225
+ children,
226
+ ...props
227
+ }: SectionProps) {
228
+ return (
229
+ <section
230
+ data-slot="section"
231
+ className={cn("flex flex-col", gapClass[gap], className)}
232
+ {...props}
233
+ >
234
+ {(heading || action) && (
235
+ <div className="flex items-center justify-between gap-2">
236
+ <div className="flex min-w-0 flex-col gap-0.5">
237
+ {heading && (
238
+ <h3 className="truncate text-[11px] font-semibold uppercase tracking-wider text-[var(--text-muted)]">
239
+ {heading}
240
+ </h3>
241
+ )}
242
+ {description && (
243
+ <p className="truncate text-xs text-[var(--text-muted)]">
244
+ {description}
245
+ </p>
246
+ )}
247
+ </div>
248
+ {action && <div className="shrink-0">{action}</div>}
249
+ </div>
250
+ )}
251
+ {children}
252
+ </section>
253
+ );
254
+ }
255
+
256
+ // ── Divider ──────────────────────────────────────────────────────
257
+
258
+ export interface DividerProps
259
+ extends React.ComponentProps<typeof Separator> {
260
+ /** Vertical spacing around a horizontal divider. */
261
+ spacing?: "none" | "sm" | "md";
262
+ }
263
+
264
+ const dividerSpacing = { none: "my-0", sm: "my-1", md: "my-2" } as const;
265
+
266
+ /** Compact divider built on the Separator primitive. */
267
+ function Divider({
268
+ className,
269
+ orientation = "horizontal",
270
+ spacing = "sm",
271
+ ...props
272
+ }: DividerProps) {
273
+ return (
274
+ <Separator
275
+ data-slot="divider"
276
+ orientation={orientation}
277
+ className={cn(
278
+ orientation === "horizontal" ? dividerSpacing[spacing] : "mx-1",
279
+ className,
280
+ )}
281
+ {...props}
282
+ />
283
+ );
284
+ }
285
+
286
+ export { WidgetContent, Stack, Inline, Grid, Section, Divider };
@@ -0,0 +1,139 @@
1
+ // Metric, MetricGroup and MetricCard — consistent numeric summaries.
2
+ //
3
+ // A metric is a label + value with optional icon, supporting text and a trend.
4
+ // Metric works inline; MetricCard wraps it in a contained surface; MetricGroup
5
+ // arranges several responsively. Values use tabular numerals so columns align.
6
+
7
+ import * as React from "react";
8
+ import { ArrowDown, ArrowRight, ArrowUp } from "lucide-react";
9
+
10
+ import { cn } from "../../lib/utils";
11
+ import { Icon } from "./typography";
12
+ import { toneColor, type Tone } from "./tone";
13
+ import { Grid, type GridProps } from "./layout";
14
+
15
+ export interface MetricTrend {
16
+ direction: "up" | "down" | "flat";
17
+ /** Change label, e.g. "+12%". */
18
+ value?: React.ReactNode;
19
+ /** Colour of the trend. Defaults to neutral — "up" is not always good. */
20
+ tone?: Tone;
21
+ }
22
+
23
+ const trendIcon = {
24
+ up: ArrowUp,
25
+ down: ArrowDown,
26
+ flat: ArrowRight,
27
+ } as const;
28
+
29
+ export interface MetricProps extends Omit<React.ComponentProps<"div">, "title"> {
30
+ label: React.ReactNode;
31
+ value: React.ReactNode;
32
+ /** Leading icon component (e.g. a lucide-react icon). */
33
+ icon?: React.ComponentType<{ className?: string }>;
34
+ /** Supporting text under the value. */
35
+ supporting?: React.ReactNode;
36
+ trend?: MetricTrend;
37
+ /** Semantic colour for the value. */
38
+ tone?: Tone;
39
+ /** Vertical (default) or horizontal label/value arrangement. */
40
+ orientation?: "vertical" | "horizontal";
41
+ }
42
+
43
+ /** A single label + value metric, usable inline or inside a MetricCard. */
44
+ function Metric({
45
+ className,
46
+ label,
47
+ value,
48
+ icon,
49
+ supporting,
50
+ trend,
51
+ tone,
52
+ orientation = "vertical",
53
+ ...props
54
+ }: MetricProps) {
55
+ const TrendIcon = trend ? trendIcon[trend.direction] : null;
56
+ return (
57
+ <div
58
+ data-slot="metric"
59
+ className={cn(
60
+ "flex min-w-0",
61
+ orientation === "vertical"
62
+ ? "flex-col gap-0.5"
63
+ : "items-center justify-between gap-2",
64
+ className,
65
+ )}
66
+ {...props}
67
+ >
68
+ <div className="flex items-center gap-1.5">
69
+ {icon && <Icon icon={icon} size="sm" />}
70
+ <span className="truncate text-[11px] font-medium uppercase tracking-wide text-[var(--text-muted)]">
71
+ {label}
72
+ </span>
73
+ </div>
74
+ <div className="flex items-baseline gap-1.5">
75
+ <span
76
+ className={cn(
77
+ "text-lg font-semibold leading-none tabular-nums",
78
+ tone ? toneColor[tone] : "text-[var(--text-primary)]",
79
+ )}
80
+ >
81
+ {value}
82
+ </span>
83
+ {trend && TrendIcon && (
84
+ <span
85
+ className={cn(
86
+ "inline-flex items-center gap-0.5 text-xs font-medium tabular-nums",
87
+ trend.tone ? toneColor[trend.tone] : "text-[var(--text-muted)]",
88
+ )}
89
+ >
90
+ <TrendIcon className="size-3" aria-hidden />
91
+ {trend.value}
92
+ </span>
93
+ )}
94
+ </div>
95
+ {supporting && (
96
+ <span className="truncate text-xs text-[var(--text-muted)]">
97
+ {supporting}
98
+ </span>
99
+ )}
100
+ </div>
101
+ );
102
+ }
103
+
104
+ // ── MetricCard ───────────────────────────────────────────────────
105
+
106
+ export interface MetricCardProps extends React.ComponentProps<"div"> {}
107
+
108
+ /** A contained surface for a Metric (or any compact summary content). */
109
+ function MetricCard({ className, ...props }: MetricCardProps) {
110
+ return (
111
+ <div
112
+ data-slot="metric-card"
113
+ className={cn(
114
+ "flex min-w-0 flex-col rounded-lg border border-[var(--surface-line)] bg-[var(--surface-raised)] p-3 shadow-[inset_0_1px_0_var(--surface-rim)]",
115
+ className,
116
+ )}
117
+ {...props}
118
+ />
119
+ );
120
+ }
121
+
122
+ // ── MetricGroup ──────────────────────────────────────────────────
123
+
124
+ export interface MetricGroupProps extends GridProps {}
125
+
126
+ /** Arranges multiple metrics responsively in a bounded grid. */
127
+ function MetricGroup({ columns = "auto", gap = "sm", minColumnWidth = 96, ...props }: MetricGroupProps) {
128
+ return (
129
+ <Grid
130
+ data-slot="metric-group"
131
+ columns={columns}
132
+ gap={gap}
133
+ minColumnWidth={minColumnWidth}
134
+ {...props}
135
+ />
136
+ );
137
+ }
138
+
139
+ export { Metric, MetricCard, MetricGroup };
@@ -0,0 +1,100 @@
1
+ // ProgressRing — an accessible circular progress / donut gauge.
2
+ //
3
+ // Pure SVG so consuming plugins need no charting dependency. Exposes an
4
+ // accessible progressbar with value semantics and supports centre content.
5
+
6
+ import * as React from "react";
7
+
8
+ import { cn } from "../../lib/utils";
9
+ import { toneColor, type Tone } from "./tone";
10
+
11
+ export interface ProgressRingProps
12
+ extends Omit<React.ComponentProps<"div">, "children"> {
13
+ /** Current value. */
14
+ value: number;
15
+ /** Maximum value the ring represents. */
16
+ max?: number;
17
+ /** Outer diameter in pixels. */
18
+ size?: number;
19
+ /** Stroke width in pixels. */
20
+ thickness?: number;
21
+ tone?: Tone;
22
+ /** Accessible name for the gauge. */
23
+ label?: string;
24
+ /** Show the percentage in the centre when no children are provided. */
25
+ showValue?: boolean;
26
+ /** Custom centre content (overrides `showValue`). */
27
+ children?: React.ReactNode;
28
+ }
29
+
30
+ /** Circular progress / donut gauge for a bounded value. */
31
+ function ProgressRing({
32
+ className,
33
+ value,
34
+ max = 100,
35
+ size = 44,
36
+ thickness = 4,
37
+ tone = "info",
38
+ label,
39
+ showValue = true,
40
+ children,
41
+ ...props
42
+ }: ProgressRingProps) {
43
+ const clamped = Math.min(Math.max(value, 0), max);
44
+ const pct = max > 0 ? clamped / max : 0;
45
+ const radius = (size - thickness) / 2;
46
+ const circumference = 2 * Math.PI * radius;
47
+ const dashOffset = circumference * (1 - pct);
48
+ const center = size / 2;
49
+
50
+ return (
51
+ <div
52
+ data-slot="progress-ring"
53
+ role="progressbar"
54
+ aria-label={label}
55
+ aria-valuenow={Math.round(clamped)}
56
+ aria-valuemin={0}
57
+ aria-valuemax={max}
58
+ className={cn(
59
+ "relative inline-flex shrink-0 items-center justify-center",
60
+ toneColor[tone],
61
+ className,
62
+ )}
63
+ style={{ width: size, height: size }}
64
+ {...props}
65
+ >
66
+ <svg width={size} height={size} className="-rotate-90">
67
+ <circle
68
+ cx={center}
69
+ cy={center}
70
+ r={radius}
71
+ fill="none"
72
+ stroke="var(--surface-line)"
73
+ strokeWidth={thickness}
74
+ />
75
+ <circle
76
+ cx={center}
77
+ cy={center}
78
+ r={radius}
79
+ fill="none"
80
+ stroke="currentColor"
81
+ strokeWidth={thickness}
82
+ strokeLinecap="round"
83
+ strokeDasharray={circumference}
84
+ strokeDashoffset={dashOffset}
85
+ className="transition-[stroke-dashoffset] duration-500"
86
+ />
87
+ </svg>
88
+ <div className="absolute inset-0 flex items-center justify-center text-[var(--text-primary)]">
89
+ {children ??
90
+ (showValue && (
91
+ <span className="text-xs font-semibold tabular-nums">
92
+ {Math.round(pct * 100)}%
93
+ </span>
94
+ ))}
95
+ </div>
96
+ </div>
97
+ );
98
+ }
99
+
100
+ export { ProgressRing };
@@ -0,0 +1,73 @@
1
+ // Widget skeleton patterns — small compositions of the Skeleton primitive for
2
+ // the common metric, list and activity loading layouts, so every widget shows a
3
+ // consistent loading shape instead of hand-rolling one.
4
+
5
+ import * as React from "react";
6
+
7
+ import { cn } from "../../lib/utils";
8
+ import { Skeleton } from "../ui/skeleton";
9
+
10
+ export interface SkeletonPatternProps extends React.ComponentProps<"div"> {
11
+ /** Number of rows / cells to render. */
12
+ count?: number;
13
+ }
14
+
15
+ /** A row of metric placeholders (label + value stacks). */
16
+ function MetricSkeleton({ className, count = 3, ...props }: SkeletonPatternProps) {
17
+ return (
18
+ <div
19
+ data-slot="metric-skeleton"
20
+ className={cn("flex gap-4", className)}
21
+ {...props}
22
+ >
23
+ {Array.from({ length: count }).map((_, i) => (
24
+ <div key={i} className="flex flex-1 flex-col gap-1.5">
25
+ <Skeleton className="h-2.5 w-12" />
26
+ <Skeleton className="h-5 w-8" />
27
+ </div>
28
+ ))}
29
+ </div>
30
+ );
31
+ }
32
+
33
+ /** A stack of list-row placeholders. */
34
+ function ListSkeleton({ className, count = 4, ...props }: SkeletonPatternProps) {
35
+ return (
36
+ <div
37
+ data-slot="list-skeleton"
38
+ className={cn("flex flex-col gap-1", className)}
39
+ {...props}
40
+ >
41
+ {Array.from({ length: count }).map((_, i) => (
42
+ <div key={i} className="flex items-center gap-2 rounded-md p-2">
43
+ <Skeleton className="size-2 rounded-full" />
44
+ <Skeleton className="h-3 flex-1" />
45
+ <Skeleton className="h-3 w-8" />
46
+ </div>
47
+ ))}
48
+ </div>
49
+ );
50
+ }
51
+
52
+ /** A stack of activity-row placeholders (marker + label + timestamp). */
53
+ function ActivitySkeleton({ className, count = 4, ...props }: SkeletonPatternProps) {
54
+ return (
55
+ <div
56
+ data-slot="activity-skeleton"
57
+ className={cn("flex flex-col gap-2", className)}
58
+ {...props}
59
+ >
60
+ {Array.from({ length: count }).map((_, i) => (
61
+ <div key={i} className="flex items-start gap-2">
62
+ <Skeleton className="mt-0.5 size-2 rounded-full" />
63
+ <div className="flex flex-1 flex-col gap-1">
64
+ <Skeleton className="h-3 w-3/4" />
65
+ <Skeleton className="h-2.5 w-1/2" />
66
+ </div>
67
+ </div>
68
+ ))}
69
+ </div>
70
+ );
71
+ }
72
+
73
+ export { MetricSkeleton, ListSkeleton, ActivitySkeleton };
@@ -0,0 +1,14 @@
1
+ // Shared semantic spacing scale for dashboard layout components.
2
+ //
3
+ // One scale keeps gaps consistent across Stack, Inline, Grid and Section so
4
+ // widgets do not drift into ad-hoc `gap-[7px]` values.
5
+
6
+ export type Gap = "none" | "xs" | "sm" | "md" | "lg";
7
+
8
+ export const gapClass: Record<Gap, string> = {
9
+ none: "gap-0",
10
+ xs: "gap-1",
11
+ sm: "gap-2",
12
+ md: "gap-3",
13
+ lg: "gap-4",
14
+ };