@corbits/react-ui 0.1.0 → 0.2.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 (170) hide show
  1. package/LICENSE +174 -499
  2. package/README.md +228 -85
  3. package/dist/blocks/login/auth-layout.d.ts +1 -1
  4. package/dist/blocks/login/auth-layout.js +3 -33
  5. package/dist/blocks/login/login-form.js +6 -119
  6. package/dist/hooks/use-command-palette-navigation.js +18 -17
  7. package/dist/hooks/use-controllable-state.d.ts +2 -2
  8. package/dist/hooks/use-controllable-state.js +23 -15
  9. package/dist/hooks/use-delayed-autofocus.js +10 -11
  10. package/dist/hooks/use-dismissable-popover.d.ts +2 -2
  11. package/dist/hooks/use-dismissable-popover.js +21 -29
  12. package/dist/hooks/use-list-selection.js +54 -85
  13. package/dist/hooks/use-prefers-reduced-motion.js +8 -4
  14. package/dist/hooks/use-scroll-current-into-view.js +5 -7
  15. package/dist/index.d.ts +0 -11
  16. package/dist/index.js +10 -19
  17. package/dist/lib/artifact.js +16 -35
  18. package/dist/lib/chart-geometry.js +33 -20
  19. package/dist/lib/chart-palette.js +17 -31
  20. package/dist/lib/chat-message.js +32 -13
  21. package/dist/lib/chat-parts.js +2 -1
  22. package/dist/lib/csv.js +42 -26
  23. package/dist/lib/metrics.js +30 -48
  24. package/dist/lib/now-item.js +26 -25
  25. package/dist/lib/relative-time.js +14 -29
  26. package/dist/lib/theme.js +32 -30
  27. package/dist/lib/url.js +9 -8
  28. package/dist/lib/workflow-display-flow.js +37 -33
  29. package/dist/lib/workflow-registry.d.ts +0 -41
  30. package/dist/lib/workflow-registry.js +13 -23
  31. package/dist/lib/workflow-run-error.js +16 -20
  32. package/dist/lib/workflow-run-progress.js +94 -52
  33. package/dist/lib/workflow-run.js +17 -13
  34. package/dist/styles.css +11 -11
  35. package/dist/test/render-hook.js +11 -12
  36. package/dist/theme.css +51 -14
  37. package/dist/ui/agent-turn.d.ts +1 -1
  38. package/dist/ui/agent-turn.js +4 -37
  39. package/dist/ui/animated-number.js +15 -28
  40. package/dist/ui/approval-card.js +5 -90
  41. package/dist/ui/artifact-body.js +47 -125
  42. package/dist/ui/artifact-notice.d.ts +1 -1
  43. package/dist/ui/artifact-notice.js +9 -16
  44. package/dist/ui/avatar.d.ts +1 -1
  45. package/dist/ui/avatar.js +11 -48
  46. package/dist/ui/badge.js +4 -12
  47. package/dist/ui/bar-chart.d.ts +1 -1
  48. package/dist/ui/bar-chart.js +22 -74
  49. package/dist/ui/block-card.js +7 -45
  50. package/dist/ui/boot-screen.d.ts +1 -1
  51. package/dist/ui/boot-screen.js +4 -33
  52. package/dist/ui/bulk-action-bar.d.ts +1 -1
  53. package/dist/ui/bulk-action-bar.js +14 -33
  54. package/dist/ui/button.js +10 -24
  55. package/dist/ui/card.js +5 -25
  56. package/dist/ui/category-bars.js +14 -59
  57. package/dist/ui/chart-frame.d.ts +2 -2
  58. package/dist/ui/chart-frame.js +6 -85
  59. package/dist/ui/chat-composer.js +4 -16
  60. package/dist/ui/chat-dock-timing.js +7 -3
  61. package/dist/ui/chat-input.js +20 -100
  62. package/dist/ui/chat-panel.d.ts +1 -1
  63. package/dist/ui/chat-panel.js +7 -41
  64. package/dist/ui/chat-thread.js +13 -31
  65. package/dist/ui/checkbox.js +10 -52
  66. package/dist/ui/command-palette.js +71 -246
  67. package/dist/ui/compare-body.d.ts +1 -1
  68. package/dist/ui/compare-body.js +28 -98
  69. package/dist/ui/confirm-button.js +8 -15
  70. package/dist/ui/corbits-mark.js +7 -16
  71. package/dist/ui/csv-table.d.ts +1 -1
  72. package/dist/ui/csv-table.js +8 -50
  73. package/dist/ui/dialog.d.ts +1 -1
  74. package/dist/ui/dialog.js +32 -62
  75. package/dist/ui/dictation-status-line.js +7 -25
  76. package/dist/ui/dither-canvas.d.ts +1 -1
  77. package/dist/ui/dither-canvas.js +31 -56
  78. package/dist/ui/embed-body.d.ts +1 -1
  79. package/dist/ui/embed-body.js +7 -56
  80. package/dist/ui/empty-state.d.ts +1 -1
  81. package/dist/ui/empty-state.js +3 -26
  82. package/dist/ui/file-input.js +17 -37
  83. package/dist/ui/filter-bar.d.ts +1 -1
  84. package/dist/ui/filter-bar.js +11 -71
  85. package/dist/ui/filter-chip.d.ts +1 -1
  86. package/dist/ui/filter-chip.js +7 -15
  87. package/dist/ui/gate-block.d.ts +2 -1
  88. package/dist/ui/gate-block.js +4 -29
  89. package/dist/ui/horizontal-stepper.d.ts +1 -1
  90. package/dist/ui/horizontal-stepper.js +18 -45
  91. package/dist/ui/input.js +1 -6
  92. package/dist/ui/inspector-shell.d.ts +5 -5
  93. package/dist/ui/inspector-shell.js +14 -81
  94. package/dist/ui/intake-form.d.ts +1 -1
  95. package/dist/ui/intake-form.js +21 -97
  96. package/dist/ui/kind-card-grid.js +9 -31
  97. package/dist/ui/library-page-header.js +4 -32
  98. package/dist/ui/list-detail.js +5 -35
  99. package/dist/ui/live-run-banner.d.ts +1 -1
  100. package/dist/ui/live-run-banner.js +5 -15
  101. package/dist/ui/live-status-line.js +6 -24
  102. package/dist/ui/menu.js +7 -24
  103. package/dist/ui/message-bubble.d.ts +3 -3
  104. package/dist/ui/message-bubble.js +5 -44
  105. package/dist/ui/mic-button.d.ts +1 -1
  106. package/dist/ui/mic-button.js +15 -21
  107. package/dist/ui/mic-permission-dialog.js +5 -41
  108. package/dist/ui/notifications-bell.js +9 -43
  109. package/dist/ui/now-cards.d.ts +2 -2
  110. package/dist/ui/now-cards.js +16 -94
  111. package/dist/ui/page-panel.js +11 -11
  112. package/dist/ui/page-shell.js +4 -11
  113. package/dist/ui/parts-renderer.js +51 -149
  114. package/dist/ui/profile-card.js +7 -111
  115. package/dist/ui/progress-checklist.js +11 -49
  116. package/dist/ui/provider-mark.d.ts +2 -2
  117. package/dist/ui/provider-mark.js +9 -16
  118. package/dist/ui/quick-reply-chips.js +6 -14
  119. package/dist/ui/quote-card.js +12 -32
  120. package/dist/ui/reasoning-block.d.ts +1 -1
  121. package/dist/ui/reasoning-block.js +14 -33
  122. package/dist/ui/research-body.js +32 -213
  123. package/dist/ui/rich-empty-state.js +6 -48
  124. package/dist/ui/run-now-button.js +14 -15
  125. package/dist/ui/section.d.ts +1 -1
  126. package/dist/ui/section.js +4 -40
  127. package/dist/ui/select.js +1 -15
  128. package/dist/ui/selection-checkbox.d.ts +1 -1
  129. package/dist/ui/selection-checkbox.js +11 -29
  130. package/dist/ui/settings-panel.js +7 -50
  131. package/dist/ui/shimmer-text.js +3 -6
  132. package/dist/ui/sidebar-item-row.d.ts +1 -1
  133. package/dist/ui/sidebar-item-row.js +9 -39
  134. package/dist/ui/sidebar-panel.js +15 -40
  135. package/dist/ui/sidebar.js +15 -76
  136. package/dist/ui/skeleton.js +3 -7
  137. package/dist/ui/sortable-table.js +29 -72
  138. package/dist/ui/sparkline.js +14 -38
  139. package/dist/ui/stat-grid.js +17 -52
  140. package/dist/ui/status-dot.d.ts +1 -1
  141. package/dist/ui/status-dot.js +8 -20
  142. package/dist/ui/step-list.d.ts +1 -0
  143. package/dist/ui/step-list.js +11 -50
  144. package/dist/ui/switch.js +4 -16
  145. package/dist/ui/table.js +7 -32
  146. package/dist/ui/tabs.js +39 -54
  147. package/dist/ui/textarea.js +10 -19
  148. package/dist/ui/theme-provider.js +54 -78
  149. package/dist/ui/theme-toggle.js +13 -16
  150. package/dist/ui/thinking-indicator.js +3 -15
  151. package/dist/ui/thinking-label.js +16 -35
  152. package/dist/ui/thinking-mark.d.ts +1 -1
  153. package/dist/ui/thinking-mark.js +4 -145
  154. package/dist/ui/time-series-chart.js +43 -174
  155. package/dist/ui/toast.js +12 -20
  156. package/dist/ui/token-mosaic.js +5 -43
  157. package/dist/ui/tool-block.js +18 -70
  158. package/dist/ui/tool-narrative.d.ts +1 -1
  159. package/dist/ui/tool-narrative.js +10 -57
  160. package/dist/ui/tool-picker.js +57 -145
  161. package/dist/ui/tooltip.d.ts +1 -1
  162. package/dist/ui/tooltip.js +4 -33
  163. package/dist/ui/top-bar.js +14 -72
  164. package/dist/ui/trace-waterfall.d.ts +1 -1
  165. package/dist/ui/trace-waterfall.js +21 -93
  166. package/dist/ui/typing-indicator.d.ts +1 -1
  167. package/dist/ui/typing-indicator.js +4 -18
  168. package/dist/ui/view-toggle.js +10 -29
  169. package/dist/ui/voice-waveform.js +3 -11
  170. package/package.json +94 -535
@@ -9,7 +9,8 @@
9
9
  * bar chart and a line chart actually need is this file, and a library would
10
10
  * bring a renderer, a theme layer and an interaction model that then have to be
11
11
  * fought to match everything else.
12
- */ /**
12
+ */
13
+ /**
13
14
  * Axis ticks on round numbers, covering `[0, max]`.
14
15
  *
15
16
  * Rounds the *step* to 1, 2, 5 or 10 × a power of ten, then extends the top of
@@ -21,20 +22,20 @@
21
22
  * Always starts at zero. A bar chart with a truncated baseline overstates
22
23
  * differences by whatever fraction you cut off, which is the most effective way
23
24
  * to mislead with a chart and the easiest to do by accident.
24
- */ export function niceTicks(max, count = 4) {
25
- if (!Number.isFinite(max) || max <= 0) return [
26
- 0,
27
- 1
28
- ];
25
+ */
26
+ export function niceTicks(max, count = 4) {
27
+ if (!Number.isFinite(max) || max <= 0)
28
+ return [0, 1];
29
29
  const rough = max / count;
30
30
  const magnitude = 10 ** Math.floor(Math.log10(rough));
31
31
  const normalized = rough / magnitude;
32
- const step = (normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10) * magnitude;
32
+ const step = (normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10) *
33
+ magnitude;
33
34
  const top = Math.ceil(max / step) * step;
34
35
  const ticks = [];
35
36
  // Accumulating `value += step` drifts on fractional steps (0.1 + 0.2 …);
36
37
  // multiplying an integer index does not.
37
- for(let index = 0; index * step <= top + step / 2; index += 1){
38
+ for (let index = 0; index * step <= top + step / 2; index += 1) {
38
39
  ticks.push(index * step);
39
40
  }
40
41
  return ticks;
@@ -46,9 +47,11 @@
46
47
  * is a real thing to plot — "nothing happened" — and one `NaN` in a path
47
48
  * attribute makes the entire path vanish, so a flat line at the baseline is both
48
49
  * correct and the only readable failure mode.
49
- */ export function scaleLinear(value, domainMax, rangeMax) {
50
- if (domainMax <= 0) return 0;
51
- return value / domainMax * rangeMax;
50
+ */
51
+ export function scaleLinear(value, domainMax, rangeMax) {
52
+ if (domainMax <= 0)
53
+ return 0;
54
+ return (value / domainMax) * rangeMax;
52
55
  }
53
56
  /**
54
57
  * An SVG path through points, as straight segments.
@@ -56,20 +59,27 @@
56
59
  * Straight, not smoothed. A spline through data invents values between the
57
60
  * points — it will overshoot a local maximum and draw a peak that was never
58
61
  * measured. Curves are for decoration; these are measurements.
59
- */ export function linePath(points) {
60
- if (points.length === 0) return "";
61
- return points.map((point, index)=>`${index === 0 ? "M" : "L"}${point.x} ${point.y}`).join(" ");
62
+ */
63
+ export function linePath(points) {
64
+ if (points.length === 0)
65
+ return "";
66
+ return points
67
+ .map((point, index) => `${index === 0 ? "M" : "L"}${point.x} ${point.y}`)
68
+ .join(" ");
62
69
  }
63
70
  /**
64
71
  * The filled region under a line: the line itself, then down to `baselineY`
65
72
  * and back to the first x, closed. The baseline is passed in rather than
66
73
  * assumed to be the box edge so the fill lands on the plot's zero line, inside
67
74
  * whatever padding the chart drew around it.
68
- */ export function areaPath(points, baselineY) {
69
- if (points.length === 0) return "";
75
+ */
76
+ export function areaPath(points, baselineY) {
77
+ if (points.length === 0)
78
+ return "";
70
79
  const first = points[0];
71
80
  const last = points[points.length - 1];
72
- if (first === undefined || last === undefined) return "";
81
+ if (first === undefined || last === undefined)
82
+ return "";
73
83
  return `${linePath(points)} L${last.x} ${baselineY} L${first.x} ${baselineY} Z`;
74
84
  }
75
85
  /**
@@ -78,9 +88,12 @@
78
88
  * Compacts only past a thousand, and keeps one decimal so 12.9K and 13.4K stay
79
89
  * distinguishable. Below a thousand the exact number is short enough to show,
80
90
  * and rounding it would be a loss with no gain.
81
- */ export function formatCompact(value) {
91
+ */
92
+ export function formatCompact(value) {
82
93
  const magnitude = Math.abs(value);
83
- if (magnitude >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
84
- if (magnitude >= 10_000) return `${(value / 1000).toFixed(1)}K`;
94
+ if (magnitude >= 1_000_000)
95
+ return `${(value / 1_000_000).toFixed(1)}M`;
96
+ if (magnitude >= 10_000)
97
+ return `${(value / 1000).toFixed(1)}K`;
85
98
  return value.toLocaleString();
86
99
  }
@@ -18,37 +18,20 @@
18
18
  * legal **only** alongside a second, non-colour channel — which is why every
19
19
  * chart in this registry ships a legend, direct labels, a per-slot dash and a
20
20
  * data table rather than treating them as polish.
21
- */ /** How many distinct series the palette can carry. Past this, fold. */ export const CHART_SERIES_SLOTS = 5;
21
+ */
22
+ /** How many distinct series the palette can carry. Past this, fold. */
23
+ export const CHART_SERIES_SLOTS = 5;
22
24
  /**
23
25
  * The ramp itself, in slot order, for callers that lay out a legend or paint a
24
26
  * stacked mosaic and need the whole sequence rather than one slot. Same rules
25
27
  * as `seriesColor`: fixed order, never cycled, orange never appears.
26
- */ export const CHART_SERIES = [
27
- {
28
- key: "blue",
29
- color: "var(--chart-1)",
30
- dash: undefined
31
- },
32
- {
33
- key: "green",
34
- color: "var(--chart-2)",
35
- dash: "6 3"
36
- },
37
- {
38
- key: "green-deep",
39
- color: "var(--chart-3)",
40
- dash: "2 3"
41
- },
42
- {
43
- key: "blue-deep",
44
- color: "var(--chart-4)",
45
- dash: "10 3 2 3"
46
- },
47
- {
48
- key: "red",
49
- color: "var(--chart-5)",
50
- dash: "1 3"
51
- }
28
+ */
29
+ export const CHART_SERIES = [
30
+ { key: "blue", color: "var(--chart-1)", dash: undefined },
31
+ { key: "green", color: "var(--chart-2)", dash: "6 3" },
32
+ { key: "green-deep", color: "var(--chart-3)", dash: "2 3" },
33
+ { key: "blue-deep", color: "var(--chart-4)", dash: "10 3 2 3" },
34
+ { key: "red", color: "var(--chart-5)", dash: "1 3" },
52
35
  ];
53
36
  /**
54
37
  * The CSS colour for series `index`.
@@ -58,7 +41,8 @@
58
41
  * misread — and generating a hue would put an unvalidated colour on screen.
59
42
  * Grey says "these are lumped together", which is the truth once you are past
60
43
  * five; use `foldSeries` so the label says so too.
61
- */ export function seriesColor(index) {
44
+ */
45
+ export function seriesColor(index) {
62
46
  return CHART_SERIES[index]?.color ?? "var(--muted-foreground)";
63
47
  }
64
48
  /**
@@ -74,7 +58,8 @@
74
58
  * Returned as an SVG `stroke-dasharray`; `undefined` means solid. Out-of-range
75
59
  * indices are solid too, matching `seriesColor`'s grey — past five, the tail is
76
60
  * one lumped-together thing and does not need a pattern of its own.
77
- */ export function seriesDash(index) {
61
+ */
62
+ export function seriesDash(index) {
78
63
  return CHART_SERIES[index]?.dash;
79
64
  }
80
65
  /**
@@ -84,9 +69,10 @@
84
69
  * against a legend, and the sixth colour would have to be either a repeat or an
85
70
  * unvalidated one. Sum `rest` into an "Other" series, or facet into small
86
71
  * multiples — both are better charts than a nine-colour one.
87
- */ export function foldSeries(series) {
72
+ */
73
+ export function foldSeries(series) {
88
74
  return {
89
75
  named: series.slice(0, CHART_SERIES_SLOTS),
90
- rest: series.slice(CHART_SERIES_SLOTS)
76
+ rest: series.slice(CHART_SERIES_SLOTS),
91
77
  };
92
78
  }
@@ -6,27 +6,39 @@
6
6
  * agent reply genuinely interleaves kinds — think, call a tool, think again,
7
7
  * answer. Flattening that to `text` plus a `toolCalls` array loses the order,
8
8
  * and the order is the only thing that makes a transcript readable.
9
- */ /** The answer text: every text part joined, thinking and tools excluded. */ export function messageText(message) {
10
- return message.parts.filter((part)=>part.type === "text").map((part)=>part.text).join("");
9
+ */
10
+ /** The answer text: every text part joined, thinking and tools excluded. */
11
+ export function messageText(message) {
12
+ return message.parts
13
+ .filter((part) => part.type === "text")
14
+ .map((part) => part.text)
15
+ .join("");
11
16
  }
12
- /** The turn's thinking, if the agent exposed any. */ export function reasoningText(message) {
13
- return message.parts.filter((part)=>part.type === "reasoning").map((part)=>part.text).join("");
17
+ /** The turn's thinking, if the agent exposed any. */
18
+ export function reasoningText(message) {
19
+ return message.parts
20
+ .filter((part) => part.type === "reasoning")
21
+ .map((part) => part.text)
22
+ .join("");
14
23
  }
15
24
  export function toolParts(message) {
16
- return message.parts.filter((part)=>part.type === "tool");
25
+ return message.parts.filter((part) => part.type === "tool");
17
26
  }
18
- /** True while any tool in the turn is still running — drives the live marker. */ export function isTurnWorking(message) {
19
- return toolParts(message).some((part)=>part.state === "running");
27
+ /** True while any tool in the turn is still running — drives the live marker. */
28
+ export function isTurnWorking(message) {
29
+ return toolParts(message).some((part) => part.state === "running");
20
30
  }
21
31
  /**
22
32
  * A readable name for a tool id. `slack__post_message` → "Post message
23
33
  * (Slack)"; `mail_send` → "Mail send". Never shows a raw snake_case id, and
24
34
  * never invents a label for a tool it does not recognise — it only reformats
25
35
  * what it was given, so a new tool reads correctly with no table to update.
26
- */ export function toolLabel(part) {
27
- if (part.label !== undefined && part.label.length > 0) return part.label;
36
+ */
37
+ export function toolLabel(part) {
38
+ if (part.label !== undefined && part.label.length > 0)
39
+ return part.label;
28
40
  const [provider, ...rest] = part.toolName.split("__");
29
- const humanise = (value)=>{
41
+ const humanise = (value) => {
30
42
  const words = value.replace(/[_-]+/g, " ").trim();
31
43
  return words.charAt(0).toUpperCase() + words.slice(1);
32
44
  };
@@ -35,7 +47,14 @@ export function toolParts(message) {
35
47
  }
36
48
  return humanise(part.toolName);
37
49
  }
38
- /** Avatar initials from a display name, capped at two characters. */ export function agentInitials(identity) {
39
- if (identity.initials !== undefined) return identity.initials;
40
- return identity.name.split(/\s+/).map((word)=>word.charAt(0)).join("").slice(0, 2).toUpperCase() || "··";
50
+ /** Avatar initials from a display name, capped at two characters. */
51
+ export function agentInitials(identity) {
52
+ if (identity.initials !== undefined)
53
+ return identity.initials;
54
+ return (identity.name
55
+ .split(/\s+/)
56
+ .map((word) => word.charAt(0))
57
+ .join("")
58
+ .slice(0, 2)
59
+ .toUpperCase() || "··");
41
60
  }
@@ -13,4 +13,5 @@
13
13
  * host might reach for on its own, because `lib/chat-message.ts` already
14
14
  * publishes those names for its own, simpler message model — the two are
15
15
  * deliberately separate types.
16
- */ export { };
16
+ */
17
+ export {};
package/dist/lib/csv.js CHANGED
@@ -9,19 +9,24 @@
9
9
  * streaming it, caps what it hands back instead of virtualising, and reports
10
10
  * "not tabular" instead of guessing — a ragged file rendered as a grid puts
11
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;
12
+ */
13
+ /** Rows past this are dropped; the caller must say so in the UI. */
14
+ export const CSV_ROW_CAP = 500;
15
+ /** Columns past this are dropped; the caller must say so in the UI. */
16
+ export const CSV_COLUMN_CAP = 40;
14
17
  /**
15
18
  * Refuse to parse beyond this. 2 MiB of CSV is roughly 20k rows — already far
16
19
  * past what anyone reads on screen, and enough DOM to lock the tab.
17
- */ export const CSV_MAX_PREVIEW_BYTES = 2 * 1024 * 1024;
20
+ */
21
+ export const CSV_MAX_PREVIEW_BYTES = 2 * 1024 * 1024;
18
22
  /**
19
23
  * True UTF-8 length, not `string.length`.
20
24
  *
21
25
  * `"é".length` is 1 but it costs 2 bytes, and a file of CJK text is three times
22
26
  * its character count — sizing the guard by character count would let a file
23
27
  * three times over budget through.
24
- */ export function utf8ByteLength(text) {
28
+ */
29
+ export function utf8ByteLength(text) {
25
30
  return new TextEncoder().encode(text).length;
26
31
  }
27
32
  /**
@@ -33,29 +38,35 @@
33
38
  * needs a character-level state machine over the whole document, and embedded
34
39
  * newlines show up as an extra ragged row here — which `isTabular` catches and
35
40
  * degrades to raw text, the honest outcome.
36
- */ function splitLine(line) {
41
+ */
42
+ function splitLine(line) {
37
43
  const fields = [];
38
44
  let field = "";
39
45
  let quoted = false;
40
- for(let i = 0; i < line.length; i += 1){
46
+ for (let i = 0; i < line.length; i += 1) {
41
47
  const char = line[i];
42
48
  if (quoted) {
43
49
  if (char === '"') {
44
50
  if (line[i + 1] === '"') {
45
51
  field += '"';
46
52
  i += 1;
47
- } else {
53
+ }
54
+ else {
48
55
  quoted = false;
49
56
  }
50
- } else {
57
+ }
58
+ else {
51
59
  field += char;
52
60
  }
53
- } else if (char === '"') {
61
+ }
62
+ else if (char === '"') {
54
63
  quoted = true;
55
- } else if (char === ",") {
64
+ }
65
+ else if (char === ",") {
56
66
  fields.push(field);
57
67
  field = "";
58
- } else {
68
+ }
69
+ else {
59
70
  field += char;
60
71
  }
61
72
  }
@@ -68,17 +79,17 @@
68
79
  * `null` rather than a throw: "too big to preview" is an expected outcome of
69
80
  * showing a user their own file, not an exceptional one, and the caller's
70
81
  * 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
- };
82
+ */
83
+ export function parseCsv(text) {
84
+ if (utf8ByteLength(text) > CSV_MAX_PREVIEW_BYTES)
85
+ return null;
86
+ const lines = text.split(/\r\n|\n|\r/).filter((line) => line.trim() !== "");
87
+ if (lines.length === 0)
88
+ return { headers: [], rows: [] };
78
89
  const [headerLine, ...bodyLines] = lines;
79
90
  return {
80
91
  headers: splitLine(headerLine ?? ""),
81
- rows: bodyLines.map(splitLine)
92
+ rows: bodyLines.map(splitLine),
82
93
  };
83
94
  }
84
95
  /**
@@ -88,21 +99,26 @@
88
99
  * single-column "CSV" is prose that happens to have no commas, and a ragged one
89
100
  * has no reliable column alignment — in both cases raw text tells the truth and
90
101
  * 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);
102
+ */
103
+ export function isTabular(parsed) {
104
+ if (parsed.headers.length < 2)
105
+ return false;
106
+ return parsed.rows.every((row) => row.length === parsed.headers.length);
94
107
  }
95
- /** Trims a parse to the caps, keeping the original totals so the UI can say so. */ export function capCsv(parsed) {
108
+ /** Trims a parse to the caps, keeping the original totals so the UI can say so. */
109
+ export function capCsv(parsed) {
96
110
  const totalRows = parsed.rows.length;
97
111
  const totalColumns = parsed.headers.length;
98
112
  const columnsTruncated = totalColumns > CSV_COLUMN_CAP;
99
113
  const rowsTruncated = totalRows > CSV_ROW_CAP;
100
114
  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),
115
+ headers: columnsTruncated
116
+ ? parsed.headers.slice(0, CSV_COLUMN_CAP)
117
+ : parsed.headers,
118
+ rows: (rowsTruncated ? parsed.rows.slice(0, CSV_ROW_CAP) : parsed.rows).map((row) => (columnsTruncated ? row.slice(0, CSV_COLUMN_CAP) : row)),
103
119
  totalRows,
104
120
  totalColumns,
105
121
  rowsTruncated,
106
- columnsTruncated
122
+ columnsTruncated,
107
123
  };
108
124
  }
@@ -3,37 +3,24 @@
3
3
  * sparkline and mosaic geometry, and zero-filling a sparse daily series. Plain
4
4
  * numbers in, plain numbers out — no React, no SVG — so the part of a dashboard
5
5
  * that can silently lie is the part you can check by hand.
6
- */ export { formatCompact } from "./chart-geometry.js";
6
+ */
7
+ export { formatCompact } from "./chart-geometry.js";
7
8
  /**
8
9
  * Period-over-period change. `comparable` is false when there is no previous
9
10
  * window. `pct` is null when the previous value is absent or zero — there is no
10
11
  * meaningful baseline to divide by, so callers render the absolute delta rather
11
12
  * than a misleading "∞%".
12
- */ export function computeDelta(current, previous) {
13
+ */
14
+ export function computeDelta(current, previous) {
13
15
  if (previous === null || previous === undefined) {
14
- return {
15
- direction: "flat",
16
- pct: null,
17
- delta: 0,
18
- comparable: false
19
- };
16
+ return { direction: "flat", pct: null, delta: 0, comparable: false };
20
17
  }
21
18
  const delta = current - previous;
22
19
  const direction = delta > 0 ? "up" : delta < 0 ? "down" : "flat";
23
20
  if (previous === 0) {
24
- return {
25
- direction,
26
- pct: null,
27
- delta,
28
- comparable: true
29
- };
21
+ return { direction, pct: null, delta, comparable: true };
30
22
  }
31
- return {
32
- direction,
33
- pct: delta / previous * 100,
34
- delta,
35
- comparable: true
36
- };
23
+ return { direction, pct: (delta / previous) * 100, delta, comparable: true };
37
24
  }
38
25
  /**
39
26
  * Maps a value series to polyline coordinates in a `width` × `height` viewBox.
@@ -41,47 +28,42 @@
41
28
  * shape is the message, and a zero baseline would flatten every real series in
42
29
  * a box this small. A flat series renders along the vertical centre and a
43
30
  * single point is centred horizontally.
44
- */ export function buildSparkline(values, width, height) {
31
+ */
32
+ export function buildSparkline(values, width, height) {
45
33
  if (values.length === 0) {
46
- return {
47
- points: "",
48
- coords: []
49
- };
34
+ return { points: "", coords: [] };
50
35
  }
51
36
  const max = Math.max(...values);
52
37
  const min = Math.min(...values);
53
38
  const span = max - min;
54
39
  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
- };
40
+ const coords = values.map((value, index) => ({
41
+ x: values.length > 1 ? index * stepX : width / 2,
42
+ y: span === 0 ? height / 2 : height - ((value - min) / span) * height,
43
+ }));
44
+ const points = coords.map((c) => `${round(c.x)},${round(c.y)}`).join(" ");
45
+ return { points, coords };
64
46
  }
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
- }));
47
+ /** Shares of a whole, as percentage widths for a stacked strip. */
48
+ export function buildMosaic(parts) {
49
+ const total = parts.reduce((sum, part) => sum + part.value, 0);
50
+ return parts.map((part) => ({
51
+ label: part.label,
52
+ value: part.value,
53
+ pct: total <= 0 ? 0 : (part.value / total) * 100,
54
+ }));
72
55
  }
73
- /** Runaway guard: the longest spine `fillDailySeries` will build. */ export const FILL_DAILY_SERIES_MAX_DAYS = 400;
56
+ /** Runaway guard: the longest spine `fillDailySeries` will build. */
57
+ export const FILL_DAILY_SERIES_MAX_DAYS = 400;
74
58
  /**
75
59
  * Expands a sparse daily series (only days with activity) into a continuous,
76
60
  * zero-filled spine from `startDate` to `endDate` inclusive (UTC). Without this
77
61
  * a sparkline plots gapped days at equal spacing, drawing a false slope across
78
62
  * dates where nothing happened. Returns the input unchanged for an unparseable
79
63
  * 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
- ]));
64
+ */
65
+ export function fillDailySeries(series, startDate, endDate, options) {
66
+ const byDate = new Map(series.map((row) => [options.dateOf(row), row]));
85
67
  const msPerDay = 86_400_000;
86
68
  let cursor = new Date(`${startDate}T00:00:00.000Z`).getTime();
87
69
  const end = new Date(`${endDate}T00:00:00.000Z`).getTime();
@@ -89,7 +71,7 @@
89
71
  return series;
90
72
  }
91
73
  const out = [];
92
- while(cursor <= end && out.length < FILL_DAILY_SERIES_MAX_DAYS){
74
+ while (cursor <= end && out.length < FILL_DAILY_SERIES_MAX_DAYS) {
93
75
  const date = new Date(cursor).toISOString().slice(0, 10);
94
76
  out.push(byDate.get(date) ?? options.zero(date));
95
77
  cursor += msPerDay;
@@ -11,49 +11,50 @@
11
11
  * `classification` is a free string, not an enum: what counts as a category
12
12
  * differs per app, and the label is shown verbatim. Ranking is driven by
13
13
  * `priority`, which is fixed, so a new classification never changes ordering.
14
- */ const PRIORITY_RANK = {
14
+ */
15
+ const PRIORITY_RANK = {
15
16
  now: 0,
16
17
  next: 1,
17
- later: 2
18
+ later: 2,
18
19
  };
19
- /** Short band label for the queue's priority column. */ export const PRIORITY_LABEL = {
20
+ /** Short band label for the queue's priority column. */
21
+ export const PRIORITY_LABEL = {
20
22
  now: "HIGH",
21
23
  next: "MED",
22
- later: "LOW"
24
+ later: "LOW",
23
25
  };
24
- /** Human label for the status marker. */ export const STATUS_LABEL = {
26
+ /** Human label for the status marker. */
27
+ export const STATUS_LABEL = {
25
28
  "needs-action": "Needs action",
26
- done: "Done"
29
+ done: "Done",
27
30
  };
28
31
  /**
29
32
  * Priority first, then newest. Sorts a copy — a component handed a frozen or
30
33
  * host-owned array must not reorder it in place.
31
- */ export function sortNowItems(items) {
32
- return [
33
- ...items
34
- ].sort((a, b)=>{
34
+ */
35
+ export function sortNowItems(items) {
36
+ return [...items].sort((a, b) => {
35
37
  const byPriority = PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority];
36
- if (byPriority !== 0) return byPriority;
38
+ if (byPriority !== 0)
39
+ return byPriority;
37
40
  return Date.parse(b.when) - Date.parse(a.when);
38
41
  });
39
42
  }
40
43
  /**
41
44
  * Sorted items split into priority bands. Empty bands are dropped rather than
42
45
  * returned empty, so a caller can map straight to headings without checking.
43
- */ export function groupNowItemsByPriority(items) {
46
+ */
47
+ export function groupNowItemsByPriority(items) {
44
48
  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);
49
+ const order = ["now", "next", "later"];
50
+ return order
51
+ .map((priority) => ({
52
+ priority,
53
+ items: sorted.filter((item) => item.priority === priority),
54
+ }))
55
+ .filter((group) => group.items.length > 0);
54
56
  }
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
- ];
57
+ /** The classifications present, in first-appearance order — for a filter row. */
58
+ export function nowItemClassifications(items) {
59
+ return [...new Set(sortNowItems(items).map((item) => item.classification))];
59
60
  }
@@ -1,28 +1,10 @@
1
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
- ]
2
+ ["year", 365 * 24 * 60 * 60 * 1000],
3
+ ["month", 30 * 24 * 60 * 60 * 1000],
4
+ ["week", 7 * 24 * 60 * 60 * 1000],
5
+ ["day", 24 * 60 * 60 * 1000],
6
+ ["hour", 60 * 60 * 1000],
7
+ ["minute", 60 * 1000],
26
8
  ];
27
9
  /**
28
10
  * "3 min ago" / "in 2 days", localised, from an ISO timestamp.
@@ -31,17 +13,20 @@ const UNITS = [
31
13
  * a pure function of its inputs — otherwise every list that renders a timestamp
32
14
  * is untestable and re-renders to a different string than the server produced.
33
15
  * Pass the server's render time when hydrating.
34
- */ export function formatRelativeTime(iso, now = Date.now()) {
16
+ */
17
+ export function formatRelativeTime(iso, now = Date.now()) {
35
18
  const then = Date.parse(iso);
36
- if (Number.isNaN(then)) return "";
19
+ if (Number.isNaN(then))
20
+ return "";
37
21
  const delta = then - now;
38
22
  const magnitude = Math.abs(delta);
39
23
  const format = new Intl.RelativeTimeFormat(undefined, {
40
24
  numeric: "auto",
41
- style: "narrow"
25
+ style: "narrow",
42
26
  });
43
- for (const [unit, ms] of UNITS){
44
- if (magnitude >= ms) return format.format(Math.round(delta / ms), unit);
27
+ for (const [unit, ms] of UNITS) {
28
+ if (magnitude >= ms)
29
+ return format.format(Math.round(delta / ms), unit);
45
30
  }
46
31
  return format.format(Math.round(delta / 1000), "second");
47
32
  }