@corbits/react-ui 0.1.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.
Files changed (255) hide show
  1. package/LICENSE +501 -0
  2. package/README.md +125 -0
  3. package/dist/blocks/login/auth-layout.d.ts +38 -0
  4. package/dist/blocks/login/auth-layout.js +52 -0
  5. package/dist/blocks/login/login-form.d.ts +60 -0
  6. package/dist/blocks/login/login-form.js +151 -0
  7. package/dist/hooks/use-command-palette-navigation.d.ts +25 -0
  8. package/dist/hooks/use-command-palette-navigation.js +52 -0
  9. package/dist/hooks/use-controllable-state.d.ts +28 -0
  10. package/dist/hooks/use-controllable-state.js +44 -0
  11. package/dist/hooks/use-delayed-autofocus.d.ts +9 -0
  12. package/dist/hooks/use-delayed-autofocus.js +20 -0
  13. package/dist/hooks/use-dismissable-popover.d.ts +37 -0
  14. package/dist/hooks/use-dismissable-popover.js +64 -0
  15. package/dist/hooks/use-list-selection.d.ts +66 -0
  16. package/dist/hooks/use-list-selection.js +147 -0
  17. package/dist/hooks/use-prefers-reduced-motion.d.ts +13 -0
  18. package/dist/hooks/use-prefers-reduced-motion.js +28 -0
  19. package/dist/hooks/use-scroll-current-into-view.d.ts +19 -0
  20. package/dist/hooks/use-scroll-current-into-view.js +33 -0
  21. package/dist/index.d.ts +122 -0
  22. package/dist/index.js +131 -0
  23. package/dist/lib/artifact.d.ts +72 -0
  24. package/dist/lib/artifact.js +67 -0
  25. package/dist/lib/chart-geometry.d.ts +62 -0
  26. package/dist/lib/chart-geometry.js +86 -0
  27. package/dist/lib/chart-palette.d.ts +76 -0
  28. package/dist/lib/chart-palette.js +92 -0
  29. package/dist/lib/chat-message.d.ts +115 -0
  30. package/dist/lib/chat-message.js +41 -0
  31. package/dist/lib/chat-parts.d.ts +64 -0
  32. package/dist/lib/chat-parts.js +16 -0
  33. package/dist/lib/csv.d.ts +60 -0
  34. package/dist/lib/csv.js +108 -0
  35. package/dist/lib/metrics.d.ts +70 -0
  36. package/dist/lib/metrics.js +101 -0
  37. package/dist/lib/now-item.d.ts +65 -0
  38. package/dist/lib/now-item.js +59 -0
  39. package/dist/lib/relative-time.d.ts +9 -0
  40. package/dist/lib/relative-time.js +47 -0
  41. package/dist/lib/theme.d.ts +30 -0
  42. package/dist/lib/theme.js +77 -0
  43. package/dist/lib/url.d.ts +26 -0
  44. package/dist/lib/url.js +37 -0
  45. package/dist/lib/utils.d.ts +2 -0
  46. package/dist/lib/utils.js +5 -0
  47. package/dist/lib/workflow-display-flow.d.ts +83 -0
  48. package/dist/lib/workflow-display-flow.js +104 -0
  49. package/dist/lib/workflow-registry.d.ts +90 -0
  50. package/dist/lib/workflow-registry.js +56 -0
  51. package/dist/lib/workflow-run-error.d.ts +31 -0
  52. package/dist/lib/workflow-run-error.js +105 -0
  53. package/dist/lib/workflow-run-progress.d.ts +97 -0
  54. package/dist/lib/workflow-run-progress.js +135 -0
  55. package/dist/lib/workflow-run.d.ts +43 -0
  56. package/dist/lib/workflow-run.js +56 -0
  57. package/dist/styles.css +3080 -0
  58. package/dist/test/render-hook.d.ts +14 -0
  59. package/dist/test/render-hook.js +38 -0
  60. package/dist/theme.css +628 -0
  61. package/dist/ui/agent-turn.d.ts +26 -0
  62. package/dist/ui/agent-turn.js +60 -0
  63. package/dist/ui/animated-number.d.ts +25 -0
  64. package/dist/ui/animated-number.js +63 -0
  65. package/dist/ui/approval-card.d.ts +46 -0
  66. package/dist/ui/approval-card.js +116 -0
  67. package/dist/ui/artifact-body.d.ts +26 -0
  68. package/dist/ui/artifact-body.js +170 -0
  69. package/dist/ui/artifact-notice.d.ts +21 -0
  70. package/dist/ui/artifact-notice.js +36 -0
  71. package/dist/ui/avatar.d.ts +37 -0
  72. package/dist/ui/avatar.js +73 -0
  73. package/dist/ui/badge.d.ts +10 -0
  74. package/dist/ui/badge.js +35 -0
  75. package/dist/ui/bar-chart.d.ts +38 -0
  76. package/dist/ui/bar-chart.js +118 -0
  77. package/dist/ui/block-card.d.ts +29 -0
  78. package/dist/ui/block-card.js +66 -0
  79. package/dist/ui/boot-screen.d.ts +23 -0
  80. package/dist/ui/boot-screen.js +49 -0
  81. package/dist/ui/bulk-action-bar.d.ts +37 -0
  82. package/dist/ui/bulk-action-bar.js +64 -0
  83. package/dist/ui/button.d.ts +12 -0
  84. package/dist/ui/button.js +73 -0
  85. package/dist/ui/card.d.ts +6 -0
  86. package/dist/ui/card.js +37 -0
  87. package/dist/ui/category-bars.d.ts +20 -0
  88. package/dist/ui/category-bars.js +69 -0
  89. package/dist/ui/chart-frame.d.ts +54 -0
  90. package/dist/ui/chart-frame.js +108 -0
  91. package/dist/ui/chat-composer.d.ts +24 -0
  92. package/dist/ui/chat-composer.js +32 -0
  93. package/dist/ui/chat-dock-timing.d.ts +15 -0
  94. package/dist/ui/chat-dock-timing.js +11 -0
  95. package/dist/ui/chat-input.d.ts +38 -0
  96. package/dist/ui/chat-input.js +138 -0
  97. package/dist/ui/chat-panel.d.ts +20 -0
  98. package/dist/ui/chat-panel.js +56 -0
  99. package/dist/ui/chat-thread.d.ts +28 -0
  100. package/dist/ui/chat-thread.js +61 -0
  101. package/dist/ui/checkbox.d.ts +25 -0
  102. package/dist/ui/checkbox.js +63 -0
  103. package/dist/ui/command-palette.d.ts +100 -0
  104. package/dist/ui/command-palette.js +341 -0
  105. package/dist/ui/compare-body.d.ts +52 -0
  106. package/dist/ui/compare-body.js +140 -0
  107. package/dist/ui/confirm-button.d.ts +29 -0
  108. package/dist/ui/confirm-button.js +41 -0
  109. package/dist/ui/corbits-mark.d.ts +29 -0
  110. package/dist/ui/corbits-mark.js +33 -0
  111. package/dist/ui/csv-table.d.ts +22 -0
  112. package/dist/ui/csv-table.js +85 -0
  113. package/dist/ui/dialog.d.ts +37 -0
  114. package/dist/ui/dialog.js +105 -0
  115. package/dist/ui/dictation-status-line.d.ts +20 -0
  116. package/dist/ui/dictation-status-line.js +38 -0
  117. package/dist/ui/dither-canvas.d.ts +31 -0
  118. package/dist/ui/dither-canvas.js +169 -0
  119. package/dist/ui/embed-body.d.ts +30 -0
  120. package/dist/ui/embed-body.js +92 -0
  121. package/dist/ui/empty-state.d.ts +26 -0
  122. package/dist/ui/empty-state.js +42 -0
  123. package/dist/ui/file-input.d.ts +25 -0
  124. package/dist/ui/file-input.js +66 -0
  125. package/dist/ui/filter-bar.d.ts +42 -0
  126. package/dist/ui/filter-bar.js +95 -0
  127. package/dist/ui/filter-chip.d.ts +14 -0
  128. package/dist/ui/filter-chip.js +23 -0
  129. package/dist/ui/gate-block.d.ts +16 -0
  130. package/dist/ui/gate-block.js +37 -0
  131. package/dist/ui/horizontal-stepper.d.ts +13 -0
  132. package/dist/ui/horizontal-stepper.js +65 -0
  133. package/dist/ui/input.d.ts +2 -0
  134. package/dist/ui/input.js +14 -0
  135. package/dist/ui/inspector-shell.d.ts +47 -0
  136. package/dist/ui/inspector-shell.js +98 -0
  137. package/dist/ui/intake-form.d.ts +39 -0
  138. package/dist/ui/intake-form.js +119 -0
  139. package/dist/ui/kind-card-grid.d.ts +23 -0
  140. package/dist/ui/kind-card-grid.js +43 -0
  141. package/dist/ui/library-page-header.d.ts +20 -0
  142. package/dist/ui/library-page-header.js +43 -0
  143. package/dist/ui/list-detail.d.ts +28 -0
  144. package/dist/ui/list-detail.js +55 -0
  145. package/dist/ui/live-run-banner.d.ts +13 -0
  146. package/dist/ui/live-run-banner.js +23 -0
  147. package/dist/ui/live-status-line.d.ts +22 -0
  148. package/dist/ui/live-status-line.js +37 -0
  149. package/dist/ui/menu.d.ts +18 -0
  150. package/dist/ui/menu.js +45 -0
  151. package/dist/ui/message-bubble.d.ts +24 -0
  152. package/dist/ui/message-bubble.js +64 -0
  153. package/dist/ui/mic-button.d.ts +35 -0
  154. package/dist/ui/mic-button.js +61 -0
  155. package/dist/ui/mic-permission-dialog.d.ts +20 -0
  156. package/dist/ui/mic-permission-dialog.js +50 -0
  157. package/dist/ui/notifications-bell.d.ts +28 -0
  158. package/dist/ui/notifications-bell.js +69 -0
  159. package/dist/ui/now-cards.d.ts +37 -0
  160. package/dist/ui/now-cards.js +116 -0
  161. package/dist/ui/page-panel.d.ts +37 -0
  162. package/dist/ui/page-panel.js +23 -0
  163. package/dist/ui/page-shell.d.ts +21 -0
  164. package/dist/ui/page-shell.js +26 -0
  165. package/dist/ui/parts-renderer.d.ts +22 -0
  166. package/dist/ui/parts-renderer.js +234 -0
  167. package/dist/ui/profile-card.d.ts +39 -0
  168. package/dist/ui/profile-card.js +124 -0
  169. package/dist/ui/progress-checklist.d.ts +25 -0
  170. package/dist/ui/progress-checklist.js +70 -0
  171. package/dist/ui/provider-mark.d.ts +32 -0
  172. package/dist/ui/provider-mark.js +31 -0
  173. package/dist/ui/quick-reply-chips.d.ts +14 -0
  174. package/dist/ui/quick-reply-chips.js +24 -0
  175. package/dist/ui/quote-card.d.ts +39 -0
  176. package/dist/ui/quote-card.js +66 -0
  177. package/dist/ui/reasoning-block.d.ts +35 -0
  178. package/dist/ui/reasoning-block.js +60 -0
  179. package/dist/ui/research-body.d.ts +70 -0
  180. package/dist/ui/research-body.js +270 -0
  181. package/dist/ui/rich-empty-state.d.ts +35 -0
  182. package/dist/ui/rich-empty-state.js +65 -0
  183. package/dist/ui/run-now-button.d.ts +23 -0
  184. package/dist/ui/run-now-button.js +42 -0
  185. package/dist/ui/section.d.ts +20 -0
  186. package/dist/ui/section.js +48 -0
  187. package/dist/ui/select.d.ts +2 -0
  188. package/dist/ui/select.js +22 -0
  189. package/dist/ui/selection-checkbox.d.ts +42 -0
  190. package/dist/ui/selection-checkbox.js +54 -0
  191. package/dist/ui/settings-panel.d.ts +35 -0
  192. package/dist/ui/settings-panel.js +68 -0
  193. package/dist/ui/shimmer-text.d.ts +19 -0
  194. package/dist/ui/shimmer-text.js +20 -0
  195. package/dist/ui/sidebar-item-row.d.ts +40 -0
  196. package/dist/ui/sidebar-item-row.js +64 -0
  197. package/dist/ui/sidebar-panel.d.ts +25 -0
  198. package/dist/ui/sidebar-panel.js +56 -0
  199. package/dist/ui/sidebar.d.ts +46 -0
  200. package/dist/ui/sidebar.js +111 -0
  201. package/dist/ui/skeleton.d.ts +13 -0
  202. package/dist/ui/skeleton.js +20 -0
  203. package/dist/ui/sortable-table.d.ts +43 -0
  204. package/dist/ui/sortable-table.js +103 -0
  205. package/dist/ui/sparkline.d.ts +27 -0
  206. package/dist/ui/sparkline.js +68 -0
  207. package/dist/ui/stat-grid.d.ts +55 -0
  208. package/dist/ui/stat-grid.js +87 -0
  209. package/dist/ui/status-dot.d.ts +13 -0
  210. package/dist/ui/status-dot.js +51 -0
  211. package/dist/ui/step-list.d.ts +21 -0
  212. package/dist/ui/step-list.js +77 -0
  213. package/dist/ui/switch.d.ts +23 -0
  214. package/dist/ui/switch.js +29 -0
  215. package/dist/ui/table.d.ts +8 -0
  216. package/dist/ui/table.js +48 -0
  217. package/dist/ui/tabs.d.ts +44 -0
  218. package/dist/ui/tabs.js +101 -0
  219. package/dist/ui/textarea.d.ts +8 -0
  220. package/dist/ui/textarea.js +32 -0
  221. package/dist/ui/theme-provider.d.ts +36 -0
  222. package/dist/ui/theme-provider.js +126 -0
  223. package/dist/ui/theme-toggle.d.ts +11 -0
  224. package/dist/ui/theme-toggle.js +32 -0
  225. package/dist/ui/thinking-indicator.d.ts +7 -0
  226. package/dist/ui/thinking-indicator.js +19 -0
  227. package/dist/ui/thinking-label.d.ts +19 -0
  228. package/dist/ui/thinking-label.js +54 -0
  229. package/dist/ui/thinking-mark.d.ts +10 -0
  230. package/dist/ui/thinking-mark.js +153 -0
  231. package/dist/ui/time-series-chart.d.ts +55 -0
  232. package/dist/ui/time-series-chart.js +249 -0
  233. package/dist/ui/toast.d.ts +16 -0
  234. package/dist/ui/toast.js +41 -0
  235. package/dist/ui/token-mosaic.d.ts +22 -0
  236. package/dist/ui/token-mosaic.js +65 -0
  237. package/dist/ui/tool-block.d.ts +54 -0
  238. package/dist/ui/tool-block.js +170 -0
  239. package/dist/ui/tool-narrative.d.ts +22 -0
  240. package/dist/ui/tool-narrative.js +77 -0
  241. package/dist/ui/tool-picker.d.ts +44 -0
  242. package/dist/ui/tool-picker.js +200 -0
  243. package/dist/ui/tooltip.d.ts +21 -0
  244. package/dist/ui/tooltip.js +48 -0
  245. package/dist/ui/top-bar.d.ts +53 -0
  246. package/dist/ui/top-bar.js +98 -0
  247. package/dist/ui/trace-waterfall.d.ts +38 -0
  248. package/dist/ui/trace-waterfall.js +121 -0
  249. package/dist/ui/typing-indicator.d.ts +9 -0
  250. package/dist/ui/typing-indicator.js +25 -0
  251. package/dist/ui/view-toggle.d.ts +17 -0
  252. package/dist/ui/view-toggle.js +46 -0
  253. package/dist/ui/voice-waveform.d.ts +16 -0
  254. package/dist/ui/voice-waveform.js +23 -0
  255. package/package.json +583 -0
@@ -0,0 +1,108 @@
1
+ /**
2
+ * A CSV parser sized for previewing a file in a browser tab, not for ingesting
3
+ * one.
4
+ *
5
+ * The distinction matters and drives every decision here. A preview has a
6
+ * hostile input (a user's arbitrary file), a hard latency budget (the tab must
7
+ * stay responsive), and a low cost of being approximate (the user can always
8
+ * download the original). So this refuses oversized input up front instead of
9
+ * streaming it, caps what it hands back instead of virtualising, and reports
10
+ * "not tabular" instead of guessing — a ragged file rendered as a grid puts
11
+ * cells under the wrong headers, which is a worse failure than showing raw text.
12
+ */ /** Rows past this are dropped; the caller must say so in the UI. */ export const CSV_ROW_CAP = 500;
13
+ /** Columns past this are dropped; the caller must say so in the UI. */ export const CSV_COLUMN_CAP = 40;
14
+ /**
15
+ * Refuse to parse beyond this. 2 MiB of CSV is roughly 20k rows — already far
16
+ * past what anyone reads on screen, and enough DOM to lock the tab.
17
+ */ export const CSV_MAX_PREVIEW_BYTES = 2 * 1024 * 1024;
18
+ /**
19
+ * True UTF-8 length, not `string.length`.
20
+ *
21
+ * `"é".length` is 1 but it costs 2 bytes, and a file of CJK text is three times
22
+ * its character count — sizing the guard by character count would let a file
23
+ * three times over budget through.
24
+ */ export function utf8ByteLength(text) {
25
+ return new TextEncoder().encode(text).length;
26
+ }
27
+ /**
28
+ * Splits one CSV line, honouring RFC 4180 quoting: `"a,b"` is one field, and a
29
+ * doubled `""` inside quotes is a literal quote.
30
+ *
31
+ * Line-at-a-time, so a quoted field containing a newline will split across two
32
+ * records. That is a deliberate limit rather than an oversight: handling it
33
+ * needs a character-level state machine over the whole document, and embedded
34
+ * newlines show up as an extra ragged row here — which `isTabular` catches and
35
+ * degrades to raw text, the honest outcome.
36
+ */ function splitLine(line) {
37
+ const fields = [];
38
+ let field = "";
39
+ let quoted = false;
40
+ for(let i = 0; i < line.length; i += 1){
41
+ const char = line[i];
42
+ if (quoted) {
43
+ if (char === '"') {
44
+ if (line[i + 1] === '"') {
45
+ field += '"';
46
+ i += 1;
47
+ } else {
48
+ quoted = false;
49
+ }
50
+ } else {
51
+ field += char;
52
+ }
53
+ } else if (char === '"') {
54
+ quoted = true;
55
+ } else if (char === ",") {
56
+ fields.push(field);
57
+ field = "";
58
+ } else {
59
+ field += char;
60
+ }
61
+ }
62
+ fields.push(field);
63
+ return fields;
64
+ }
65
+ /**
66
+ * Parses CSV text, or returns `null` when it is too large to parse safely.
67
+ *
68
+ * `null` rather than a throw: "too big to preview" is an expected outcome of
69
+ * showing a user their own file, not an exceptional one, and the caller's
70
+ * response is to render a different surface rather than to catch.
71
+ */ export function parseCsv(text) {
72
+ if (utf8ByteLength(text) > CSV_MAX_PREVIEW_BYTES) return null;
73
+ const lines = text.split(/\r\n|\n|\r/).filter((line)=>line.trim() !== "");
74
+ if (lines.length === 0) return {
75
+ headers: [],
76
+ rows: []
77
+ };
78
+ const [headerLine, ...bodyLines] = lines;
79
+ return {
80
+ headers: splitLine(headerLine ?? ""),
81
+ rows: bodyLines.map(splitLine)
82
+ };
83
+ }
84
+ /**
85
+ * Whether the parse is rectangular enough to render as a grid.
86
+ *
87
+ * Requires at least two columns and every row to match the header width. A
88
+ * single-column "CSV" is prose that happens to have no commas, and a ragged one
89
+ * has no reliable column alignment — in both cases raw text tells the truth and
90
+ * a table does not.
91
+ */ export function isTabular(parsed) {
92
+ if (parsed.headers.length < 2) return false;
93
+ return parsed.rows.every((row)=>row.length === parsed.headers.length);
94
+ }
95
+ /** Trims a parse to the caps, keeping the original totals so the UI can say so. */ export function capCsv(parsed) {
96
+ const totalRows = parsed.rows.length;
97
+ const totalColumns = parsed.headers.length;
98
+ const columnsTruncated = totalColumns > CSV_COLUMN_CAP;
99
+ const rowsTruncated = totalRows > CSV_ROW_CAP;
100
+ return {
101
+ headers: columnsTruncated ? parsed.headers.slice(0, CSV_COLUMN_CAP) : parsed.headers,
102
+ rows: (rowsTruncated ? parsed.rows.slice(0, CSV_ROW_CAP) : parsed.rows).map((row)=>columnsTruncated ? row.slice(0, CSV_COLUMN_CAP) : row),
103
+ totalRows,
104
+ totalColumns,
105
+ rowsTruncated,
106
+ columnsTruncated
107
+ };
108
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Pure arithmetic behind the dashboard viz pieces: period-over-period deltas,
3
+ * sparkline and mosaic geometry, and zero-filling a sparse daily series. Plain
4
+ * numbers in, plain numbers out — no React, no SVG — so the part of a dashboard
5
+ * that can silently lie is the part you can check by hand.
6
+ */
7
+ export { formatCompact } from "./chart-geometry.js";
8
+ export type DeltaDirection = "up" | "down" | "flat";
9
+ export type DeltaResult = {
10
+ readonly direction: DeltaDirection;
11
+ /** Percent change vs the previous window, or null when it cannot be computed. */
12
+ readonly pct: number | null;
13
+ readonly delta: number;
14
+ /**
15
+ * False when there is no previous window to compare against. Distinguishes
16
+ * "no change" (comparable, delta 0) from "no comparison available", so a UI
17
+ * never renders a flat indicator that reads as stability.
18
+ */
19
+ readonly comparable: boolean;
20
+ };
21
+ /**
22
+ * Period-over-period change. `comparable` is false when there is no previous
23
+ * window. `pct` is null when the previous value is absent or zero — there is no
24
+ * meaningful baseline to divide by, so callers render the absolute delta rather
25
+ * than a misleading "∞%".
26
+ */
27
+ export declare function computeDelta(current: number, previous: number | null | undefined): DeltaResult;
28
+ export type SparklineGeometry = {
29
+ /** SVG `<polyline>` `points` string. Empty for an empty series. */
30
+ readonly points: string;
31
+ readonly coords: readonly {
32
+ readonly x: number;
33
+ readonly y: number;
34
+ }[];
35
+ };
36
+ /**
37
+ * Maps a value series to polyline coordinates in a `width` × `height` viewBox.
38
+ * The minimum value sits on the bottom edge and the maximum on the top — the
39
+ * shape is the message, and a zero baseline would flatten every real series in
40
+ * a box this small. A flat series renders along the vertical centre and a
41
+ * single point is centred horizontally.
42
+ */
43
+ export declare function buildSparkline(values: readonly number[], width: number, height: number): SparklineGeometry;
44
+ export type MosaicSegment = {
45
+ readonly label: string;
46
+ readonly value: number;
47
+ /** Width as a percentage of the total, 0 when the total is 0. */
48
+ readonly pct: number;
49
+ };
50
+ /** Shares of a whole, as percentage widths for a stacked strip. */
51
+ export declare function buildMosaic(parts: readonly {
52
+ label: string;
53
+ value: number;
54
+ }[]): MosaicSegment[];
55
+ export type FillDailySeriesOptions<T> = {
56
+ /** Reads the row's UTC calendar date, `YYYY-MM-DD`. */
57
+ readonly dateOf: (row: T) => string;
58
+ /** Builds the zero row for a date with no activity. */
59
+ readonly zero: (date: string) => T;
60
+ };
61
+ /** Runaway guard: the longest spine `fillDailySeries` will build. */
62
+ export declare const FILL_DAILY_SERIES_MAX_DAYS = 400;
63
+ /**
64
+ * Expands a sparse daily series (only days with activity) into a continuous,
65
+ * zero-filled spine from `startDate` to `endDate` inclusive (UTC). Without this
66
+ * a sparkline plots gapped days at equal spacing, drawing a false slope across
67
+ * dates where nothing happened. Returns the input unchanged for an unparseable
68
+ * or inverted range, and caps at `FILL_DAILY_SERIES_MAX_DAYS`.
69
+ */
70
+ export declare function fillDailySeries<T>(series: readonly T[], startDate: string, endDate: string, options: FillDailySeriesOptions<T>): readonly T[];
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Pure arithmetic behind the dashboard viz pieces: period-over-period deltas,
3
+ * sparkline and mosaic geometry, and zero-filling a sparse daily series. Plain
4
+ * numbers in, plain numbers out — no React, no SVG — so the part of a dashboard
5
+ * that can silently lie is the part you can check by hand.
6
+ */ export { formatCompact } from "./chart-geometry.js";
7
+ /**
8
+ * Period-over-period change. `comparable` is false when there is no previous
9
+ * window. `pct` is null when the previous value is absent or zero — there is no
10
+ * meaningful baseline to divide by, so callers render the absolute delta rather
11
+ * than a misleading "∞%".
12
+ */ export function computeDelta(current, previous) {
13
+ if (previous === null || previous === undefined) {
14
+ return {
15
+ direction: "flat",
16
+ pct: null,
17
+ delta: 0,
18
+ comparable: false
19
+ };
20
+ }
21
+ const delta = current - previous;
22
+ const direction = delta > 0 ? "up" : delta < 0 ? "down" : "flat";
23
+ if (previous === 0) {
24
+ return {
25
+ direction,
26
+ pct: null,
27
+ delta,
28
+ comparable: true
29
+ };
30
+ }
31
+ return {
32
+ direction,
33
+ pct: delta / previous * 100,
34
+ delta,
35
+ comparable: true
36
+ };
37
+ }
38
+ /**
39
+ * Maps a value series to polyline coordinates in a `width` × `height` viewBox.
40
+ * The minimum value sits on the bottom edge and the maximum on the top — the
41
+ * shape is the message, and a zero baseline would flatten every real series in
42
+ * a box this small. A flat series renders along the vertical centre and a
43
+ * single point is centred horizontally.
44
+ */ export function buildSparkline(values, width, height) {
45
+ if (values.length === 0) {
46
+ return {
47
+ points: "",
48
+ coords: []
49
+ };
50
+ }
51
+ const max = Math.max(...values);
52
+ const min = Math.min(...values);
53
+ const span = max - min;
54
+ const stepX = values.length > 1 ? width / (values.length - 1) : 0;
55
+ const coords = values.map((value, index)=>({
56
+ x: values.length > 1 ? index * stepX : width / 2,
57
+ y: span === 0 ? height / 2 : height - (value - min) / span * height
58
+ }));
59
+ const points = coords.map((c)=>`${round(c.x)},${round(c.y)}`).join(" ");
60
+ return {
61
+ points,
62
+ coords
63
+ };
64
+ }
65
+ /** Shares of a whole, as percentage widths for a stacked strip. */ export function buildMosaic(parts) {
66
+ const total = parts.reduce((sum, part)=>sum + part.value, 0);
67
+ return parts.map((part)=>({
68
+ label: part.label,
69
+ value: part.value,
70
+ pct: total <= 0 ? 0 : part.value / total * 100
71
+ }));
72
+ }
73
+ /** Runaway guard: the longest spine `fillDailySeries` will build. */ export const FILL_DAILY_SERIES_MAX_DAYS = 400;
74
+ /**
75
+ * Expands a sparse daily series (only days with activity) into a continuous,
76
+ * zero-filled spine from `startDate` to `endDate` inclusive (UTC). Without this
77
+ * a sparkline plots gapped days at equal spacing, drawing a false slope across
78
+ * dates where nothing happened. Returns the input unchanged for an unparseable
79
+ * or inverted range, and caps at `FILL_DAILY_SERIES_MAX_DAYS`.
80
+ */ export function fillDailySeries(series, startDate, endDate, options) {
81
+ const byDate = new Map(series.map((row)=>[
82
+ options.dateOf(row),
83
+ row
84
+ ]));
85
+ const msPerDay = 86_400_000;
86
+ let cursor = new Date(`${startDate}T00:00:00.000Z`).getTime();
87
+ const end = new Date(`${endDate}T00:00:00.000Z`).getTime();
88
+ if (Number.isNaN(cursor) || Number.isNaN(end) || end < cursor) {
89
+ return series;
90
+ }
91
+ const out = [];
92
+ while(cursor <= end && out.length < FILL_DAILY_SERIES_MAX_DAYS){
93
+ const date = new Date(cursor).toISOString().slice(0, 10);
94
+ out.push(byDate.get(date) ?? options.zero(date));
95
+ cursor += msPerDay;
96
+ }
97
+ return out;
98
+ }
99
+ function round(n) {
100
+ return Math.round(n * 100) / 100;
101
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The model behind the "Now" surface: the things asking for the user's
3
+ * attention, whatever produced them.
4
+ *
5
+ * Two variants, because they behave differently and not because they look
6
+ * different: a `gate` is blocked work that cannot proceed without a decision,
7
+ * and a `mail` is something delivered to be read. A gate is never "read"; a
8
+ * mail has no decision to make. A single optional-field shape would let a
9
+ * caller build a read gate, which is not a thing.
10
+ *
11
+ * `classification` is a free string, not an enum: what counts as a category
12
+ * differs per app, and the label is shown verbatim. Ranking is driven by
13
+ * `priority`, which is fixed, so a new classification never changes ordering.
14
+ */
15
+ export type NowPriority = "now" | "next" | "later";
16
+ export type NowStatus = "needs-action" | "done";
17
+ type NowItemBase = {
18
+ readonly id: string;
19
+ readonly title: string;
20
+ readonly summary?: string;
21
+ /** Category shown as a badge, e.g. "Approval", "Brief", "Failure". */
22
+ readonly classification: string;
23
+ readonly priority: NowPriority;
24
+ /**
25
+ * Whether the item is still asking for something. On the base and not only on
26
+ * mail: a gate is resolved once decided, exactly as a message is once handled,
27
+ * and a row that had to check `type` before it could show "done" would be
28
+ * wrong the first time a third variant appeared.
29
+ */
30
+ readonly status: NowStatus;
31
+ /** ISO timestamp. Ties in priority are broken by this, newest first. */
32
+ readonly when: string;
33
+ /** Where the item opens. Without it the card renders as static text. */
34
+ readonly href?: string;
35
+ };
36
+ export type NowItem = (NowItemBase & {
37
+ readonly type: "gate";
38
+ /** The decision being asked for, e.g. "Approve send". */
39
+ readonly action: string;
40
+ }) | (NowItemBase & {
41
+ readonly type: "mail";
42
+ readonly from: string;
43
+ readonly read: boolean;
44
+ });
45
+ /** Short band label for the queue's priority column. */
46
+ export declare const PRIORITY_LABEL: Record<NowPriority, string>;
47
+ /** Human label for the status marker. */
48
+ export declare const STATUS_LABEL: Record<NowStatus, string>;
49
+ /**
50
+ * Priority first, then newest. Sorts a copy — a component handed a frozen or
51
+ * host-owned array must not reorder it in place.
52
+ */
53
+ export declare function sortNowItems(items: readonly NowItem[]): readonly NowItem[];
54
+ export type NowGroup = {
55
+ readonly priority: NowPriority;
56
+ readonly items: readonly NowItem[];
57
+ };
58
+ /**
59
+ * Sorted items split into priority bands. Empty bands are dropped rather than
60
+ * returned empty, so a caller can map straight to headings without checking.
61
+ */
62
+ export declare function groupNowItemsByPriority(items: readonly NowItem[]): readonly NowGroup[];
63
+ /** The classifications present, in first-appearance order — for a filter row. */
64
+ export declare function nowItemClassifications(items: readonly NowItem[]): readonly string[];
65
+ export {};
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The model behind the "Now" surface: the things asking for the user's
3
+ * attention, whatever produced them.
4
+ *
5
+ * Two variants, because they behave differently and not because they look
6
+ * different: a `gate` is blocked work that cannot proceed without a decision,
7
+ * and a `mail` is something delivered to be read. A gate is never "read"; a
8
+ * mail has no decision to make. A single optional-field shape would let a
9
+ * caller build a read gate, which is not a thing.
10
+ *
11
+ * `classification` is a free string, not an enum: what counts as a category
12
+ * differs per app, and the label is shown verbatim. Ranking is driven by
13
+ * `priority`, which is fixed, so a new classification never changes ordering.
14
+ */ const PRIORITY_RANK = {
15
+ now: 0,
16
+ next: 1,
17
+ later: 2
18
+ };
19
+ /** Short band label for the queue's priority column. */ export const PRIORITY_LABEL = {
20
+ now: "HIGH",
21
+ next: "MED",
22
+ later: "LOW"
23
+ };
24
+ /** Human label for the status marker. */ export const STATUS_LABEL = {
25
+ "needs-action": "Needs action",
26
+ done: "Done"
27
+ };
28
+ /**
29
+ * Priority first, then newest. Sorts a copy — a component handed a frozen or
30
+ * host-owned array must not reorder it in place.
31
+ */ export function sortNowItems(items) {
32
+ return [
33
+ ...items
34
+ ].sort((a, b)=>{
35
+ const byPriority = PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority];
36
+ if (byPriority !== 0) return byPriority;
37
+ return Date.parse(b.when) - Date.parse(a.when);
38
+ });
39
+ }
40
+ /**
41
+ * Sorted items split into priority bands. Empty bands are dropped rather than
42
+ * returned empty, so a caller can map straight to headings without checking.
43
+ */ export function groupNowItemsByPriority(items) {
44
+ const sorted = sortNowItems(items);
45
+ const order = [
46
+ "now",
47
+ "next",
48
+ "later"
49
+ ];
50
+ return order.map((priority)=>({
51
+ priority,
52
+ items: sorted.filter((item)=>item.priority === priority)
53
+ })).filter((group)=>group.items.length > 0);
54
+ }
55
+ /** The classifications present, in first-appearance order — for a filter row. */ export function nowItemClassifications(items) {
56
+ return [
57
+ ...new Set(sortNowItems(items).map((item)=>item.classification))
58
+ ];
59
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * "3 min ago" / "in 2 days", localised, from an ISO timestamp.
3
+ *
4
+ * `now` is a parameter rather than a `Date.now()` call inside so the output is
5
+ * a pure function of its inputs — otherwise every list that renders a timestamp
6
+ * is untestable and re-renders to a different string than the server produced.
7
+ * Pass the server's render time when hydrating.
8
+ */
9
+ export declare function formatRelativeTime(iso: string, now?: number): string;
@@ -0,0 +1,47 @@
1
+ const UNITS = [
2
+ [
3
+ "year",
4
+ 365 * 24 * 60 * 60 * 1000
5
+ ],
6
+ [
7
+ "month",
8
+ 30 * 24 * 60 * 60 * 1000
9
+ ],
10
+ [
11
+ "week",
12
+ 7 * 24 * 60 * 60 * 1000
13
+ ],
14
+ [
15
+ "day",
16
+ 24 * 60 * 60 * 1000
17
+ ],
18
+ [
19
+ "hour",
20
+ 60 * 60 * 1000
21
+ ],
22
+ [
23
+ "minute",
24
+ 60 * 1000
25
+ ]
26
+ ];
27
+ /**
28
+ * "3 min ago" / "in 2 days", localised, from an ISO timestamp.
29
+ *
30
+ * `now` is a parameter rather than a `Date.now()` call inside so the output is
31
+ * a pure function of its inputs — otherwise every list that renders a timestamp
32
+ * is untestable and re-renders to a different string than the server produced.
33
+ * Pass the server's render time when hydrating.
34
+ */ export function formatRelativeTime(iso, now = Date.now()) {
35
+ const then = Date.parse(iso);
36
+ if (Number.isNaN(then)) return "";
37
+ const delta = then - now;
38
+ const magnitude = Math.abs(delta);
39
+ const format = new Intl.RelativeTimeFormat(undefined, {
40
+ numeric: "auto",
41
+ style: "narrow"
42
+ });
43
+ for (const [unit, ms] of UNITS){
44
+ if (magnitude >= ms) return format.format(Math.round(delta / ms), unit);
45
+ }
46
+ return format.format(Math.round(delta / 1000), "second");
47
+ }
@@ -0,0 +1,30 @@
1
+ /** Theme mode and preset resolution for ThemeProvider.
2
+
3
+ * Dark mode is a `.dark` class on an ancestor, with an OS-following fallback
4
+ * when neither `.dark` nor `.light` is set (see theme.css — `@custom-variant
5
+ * dark` plus the `prefers-color-scheme` block). `.light` is the explicit
6
+ * opt-out and only works on `documentElement`. This module owns the pure
7
+ * resolution rules; ThemeProvider applies them to the document and persists
8
+ * the choice.
9
+ */
10
+ export type ThemeMode = "system" | "light" | "dark";
11
+ export type ResolvedThemeMode = "light" | "dark";
12
+ /** Named token overlays shipped as `data-theme` values on the root. */
13
+ export type ThemePreset = "default" | "warm" | "cool";
14
+ export declare const THEME_MODES: readonly ThemeMode[];
15
+ export declare const THEME_PRESETS: readonly ThemePreset[];
16
+ export declare const DEFAULT_THEME_STORAGE_KEY = "corbits-theme";
17
+ export type ThemePreference = {
18
+ readonly mode: ThemeMode;
19
+ readonly preset: ThemePreset;
20
+ };
21
+ export declare function isThemeMode(value: unknown): value is ThemeMode;
22
+ export declare function isThemePreset(value: unknown): value is ThemePreset;
23
+ /** Resolve light/dark after consulting the system preference when mode is system. */
24
+ export declare function resolveThemeMode(mode: ThemeMode, systemPrefersDark: boolean): ResolvedThemeMode;
25
+ export declare function parseThemePreference(raw: string | null, fallback: ThemePreference): ThemePreference;
26
+ export declare function serializeThemePreference(preference: ThemePreference): string;
27
+ /** Cycle system → light → dark → system for a one-control toggle. */
28
+ export declare function nextThemeMode(mode: ThemeMode): ThemeMode;
29
+ /** Apply resolved mode + preset to a root element (usually documentElement). */
30
+ export declare function applyThemeToRoot(root: Element, resolved: ResolvedThemeMode, preset: ThemePreset): void;
@@ -0,0 +1,77 @@
1
+ /** Theme mode and preset resolution for ThemeProvider.
2
+
3
+ * Dark mode is a `.dark` class on an ancestor, with an OS-following fallback
4
+ * when neither `.dark` nor `.light` is set (see theme.css — `@custom-variant
5
+ * dark` plus the `prefers-color-scheme` block). `.light` is the explicit
6
+ * opt-out and only works on `documentElement`. This module owns the pure
7
+ * resolution rules; ThemeProvider applies them to the document and persists
8
+ * the choice.
9
+ */ export const THEME_MODES = [
10
+ "system",
11
+ "light",
12
+ "dark"
13
+ ];
14
+ export const THEME_PRESETS = [
15
+ "default",
16
+ "warm",
17
+ "cool"
18
+ ];
19
+ export const DEFAULT_THEME_STORAGE_KEY = "corbits-theme";
20
+ export function isThemeMode(value) {
21
+ return value === "system" || value === "light" || value === "dark";
22
+ }
23
+ export function isThemePreset(value) {
24
+ return value === "default" || value === "warm" || value === "cool";
25
+ }
26
+ /** Resolve light/dark after consulting the system preference when mode is system. */ export function resolveThemeMode(mode, systemPrefersDark) {
27
+ if (mode === "light") return "light";
28
+ if (mode === "dark") return "dark";
29
+ return systemPrefersDark ? "dark" : "light";
30
+ }
31
+ export function parseThemePreference(raw, fallback) {
32
+ if (raw === null || raw.length === 0) return fallback;
33
+ try {
34
+ const parsed = JSON.parse(raw);
35
+ if (typeof parsed !== "object" || parsed === null) return fallback;
36
+ const record = parsed;
37
+ const mode = isThemeMode(record.mode) ? record.mode : fallback.mode;
38
+ const preset = isThemePreset(record.preset) ? record.preset : fallback.preset;
39
+ return {
40
+ mode,
41
+ preset
42
+ };
43
+ } catch {
44
+ // Legacy: a bare mode string from an earlier client.
45
+ if (isThemeMode(raw)) return {
46
+ mode: raw,
47
+ preset: fallback.preset
48
+ };
49
+ return fallback;
50
+ }
51
+ }
52
+ export function serializeThemePreference(preference) {
53
+ return JSON.stringify({
54
+ mode: preference.mode,
55
+ preset: preference.preset
56
+ });
57
+ }
58
+ /** Cycle system → light → dark → system for a one-control toggle. */ export function nextThemeMode(mode) {
59
+ if (mode === "system") return "light";
60
+ if (mode === "light") return "dark";
61
+ return "system";
62
+ }
63
+ /** Apply resolved mode + preset to a root element (usually documentElement). */ export function applyThemeToRoot(root, resolved, preset) {
64
+ root.classList.toggle("dark", resolved === "dark");
65
+ // `.light` is the explicit-light escape hatch from the `prefers-color-scheme`
66
+ // media query in theme.css — without it, "light" mode chosen on a dark-OS
67
+ // host would be overridden back to dark tokens by the media query.
68
+ root.classList.toggle("light", resolved === "light");
69
+ if (preset === "default") {
70
+ root.removeAttribute("data-theme");
71
+ } else {
72
+ root.setAttribute("data-theme", preset);
73
+ }
74
+ if (root instanceof HTMLElement) {
75
+ root.style.colorScheme = resolved;
76
+ }
77
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Is `url` safe to hand to a live sink — `href`, `src`, `window.open` — as-is?
3
+ *
4
+ * Agent- and artifact-supplied strings are untrusted input: a `javascript:`
5
+ * or `data:` value handed straight to `href` becomes a live script the
6
+ * moment someone clicks it. Checking `.protocol` after `new URL()` parses the
7
+ * string, rather than matching a prefix, is what defeats tricks like embedded
8
+ * tabs/newlines (`java\tscript:`) or mixed case (`JavaScript:`) — the URL
9
+ * parser strips and normalizes both away before the comparison ever runs.
10
+ *
11
+ * Relative and protocol-relative URLs (`/path`, `//host/path`) fail to parse
12
+ * without a base and are rejected rather than resolved against one: every
13
+ * sink this guards links to a third-party resource — a citation, a download,
14
+ * an embed — so there is no in-app page a bare path could sensibly mean, and
15
+ * inventing a base to resolve against would only guess wrong.
16
+ */
17
+ export declare function isSafeUrl(url: string, allowedProtocols?: readonly string[]): boolean;
18
+ /**
19
+ * `url` back, unchanged, if `isSafeUrl` accepts it — otherwise `undefined`.
20
+ *
21
+ * The shape callers want at a render site: a value they can put straight
22
+ * into `href`/`src` when present, and a clean signal to fall back to plain
23
+ * text when not. Never returns a placeholder like `"#"` — a disabled-looking
24
+ * link that is secretly still a link is its own trap.
25
+ */
26
+ export declare function toSafeHref(url: string | undefined, allowedProtocols?: readonly string[]): string | undefined;
@@ -0,0 +1,37 @@
1
+ const DEFAULT_ALLOWED_PROTOCOLS = [
2
+ "http:",
3
+ "https:"
4
+ ];
5
+ /**
6
+ * Is `url` safe to hand to a live sink — `href`, `src`, `window.open` — as-is?
7
+ *
8
+ * Agent- and artifact-supplied strings are untrusted input: a `javascript:`
9
+ * or `data:` value handed straight to `href` becomes a live script the
10
+ * moment someone clicks it. Checking `.protocol` after `new URL()` parses the
11
+ * string, rather than matching a prefix, is what defeats tricks like embedded
12
+ * tabs/newlines (`java\tscript:`) or mixed case (`JavaScript:`) — the URL
13
+ * parser strips and normalizes both away before the comparison ever runs.
14
+ *
15
+ * Relative and protocol-relative URLs (`/path`, `//host/path`) fail to parse
16
+ * without a base and are rejected rather than resolved against one: every
17
+ * sink this guards links to a third-party resource — a citation, a download,
18
+ * an embed — so there is no in-app page a bare path could sensibly mean, and
19
+ * inventing a base to resolve against would only guess wrong.
20
+ */ export function isSafeUrl(url, allowedProtocols = DEFAULT_ALLOWED_PROTOCOLS) {
21
+ try {
22
+ return allowedProtocols.includes(new URL(url).protocol);
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+ /**
28
+ * `url` back, unchanged, if `isSafeUrl` accepts it — otherwise `undefined`.
29
+ *
30
+ * The shape callers want at a render site: a value they can put straight
31
+ * into `href`/`src` when present, and a clean signal to fall back to plain
32
+ * text when not. Never returns a placeholder like `"#"` — a disabled-looking
33
+ * link that is secretly still a link is its own trap.
34
+ */ export function toSafeHref(url, allowedProtocols = DEFAULT_ALLOWED_PROTOCOLS) {
35
+ if (url === undefined) return undefined;
36
+ return isSafeUrl(url, allowedProtocols) ? url : undefined;
37
+ }
@@ -0,0 +1,2 @@
1
+ import { type ClassValue } from "clsx";
2
+ export declare function cn(...inputs: ClassValue[]): string;
@@ -0,0 +1,5 @@
1
+ import { clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+ export function cn(...inputs) {
4
+ return twMerge(clsx(inputs));
5
+ }