@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.
- package/LICENSE +174 -499
- package/README.md +228 -85
- package/dist/blocks/login/auth-layout.d.ts +1 -1
- package/dist/blocks/login/auth-layout.js +3 -33
- package/dist/blocks/login/login-form.js +6 -119
- package/dist/hooks/use-command-palette-navigation.js +18 -17
- package/dist/hooks/use-controllable-state.d.ts +2 -2
- package/dist/hooks/use-controllable-state.js +23 -15
- package/dist/hooks/use-delayed-autofocus.js +10 -11
- package/dist/hooks/use-dismissable-popover.d.ts +2 -2
- package/dist/hooks/use-dismissable-popover.js +21 -29
- package/dist/hooks/use-list-selection.js +54 -85
- package/dist/hooks/use-prefers-reduced-motion.js +8 -4
- package/dist/hooks/use-scroll-current-into-view.js +5 -7
- package/dist/index.d.ts +0 -11
- package/dist/index.js +10 -19
- package/dist/lib/artifact.js +16 -35
- package/dist/lib/chart-geometry.js +33 -20
- package/dist/lib/chart-palette.js +17 -31
- package/dist/lib/chat-message.js +32 -13
- package/dist/lib/chat-parts.js +2 -1
- package/dist/lib/csv.js +42 -26
- package/dist/lib/metrics.js +30 -48
- package/dist/lib/now-item.js +26 -25
- package/dist/lib/relative-time.js +14 -29
- package/dist/lib/theme.js +32 -30
- package/dist/lib/url.js +9 -8
- package/dist/lib/workflow-display-flow.js +37 -33
- package/dist/lib/workflow-registry.d.ts +0 -41
- package/dist/lib/workflow-registry.js +13 -23
- package/dist/lib/workflow-run-error.js +16 -20
- package/dist/lib/workflow-run-progress.js +94 -52
- package/dist/lib/workflow-run.js +17 -13
- package/dist/styles.css +11 -11
- package/dist/test/render-hook.js +11 -12
- package/dist/theme.css +51 -14
- package/dist/ui/agent-turn.d.ts +1 -1
- package/dist/ui/agent-turn.js +4 -37
- package/dist/ui/animated-number.js +15 -28
- package/dist/ui/approval-card.js +5 -90
- package/dist/ui/artifact-body.js +47 -125
- package/dist/ui/artifact-notice.d.ts +1 -1
- package/dist/ui/artifact-notice.js +9 -16
- package/dist/ui/avatar.d.ts +1 -1
- package/dist/ui/avatar.js +11 -48
- package/dist/ui/badge.js +4 -12
- package/dist/ui/bar-chart.d.ts +1 -1
- package/dist/ui/bar-chart.js +22 -74
- package/dist/ui/block-card.js +7 -45
- package/dist/ui/boot-screen.d.ts +1 -1
- package/dist/ui/boot-screen.js +4 -33
- package/dist/ui/bulk-action-bar.d.ts +1 -1
- package/dist/ui/bulk-action-bar.js +14 -33
- package/dist/ui/button.js +10 -24
- package/dist/ui/card.js +5 -25
- package/dist/ui/category-bars.js +14 -59
- package/dist/ui/chart-frame.d.ts +2 -2
- package/dist/ui/chart-frame.js +6 -85
- package/dist/ui/chat-composer.js +4 -16
- package/dist/ui/chat-dock-timing.js +7 -3
- package/dist/ui/chat-input.js +20 -100
- package/dist/ui/chat-panel.d.ts +1 -1
- package/dist/ui/chat-panel.js +7 -41
- package/dist/ui/chat-thread.js +13 -31
- package/dist/ui/checkbox.js +10 -52
- package/dist/ui/command-palette.js +71 -246
- package/dist/ui/compare-body.d.ts +1 -1
- package/dist/ui/compare-body.js +28 -98
- package/dist/ui/confirm-button.js +8 -15
- package/dist/ui/corbits-mark.js +7 -16
- package/dist/ui/csv-table.d.ts +1 -1
- package/dist/ui/csv-table.js +8 -50
- package/dist/ui/dialog.d.ts +1 -1
- package/dist/ui/dialog.js +32 -62
- package/dist/ui/dictation-status-line.js +7 -25
- package/dist/ui/dither-canvas.d.ts +1 -1
- package/dist/ui/dither-canvas.js +31 -56
- package/dist/ui/embed-body.d.ts +1 -1
- package/dist/ui/embed-body.js +7 -56
- package/dist/ui/empty-state.d.ts +1 -1
- package/dist/ui/empty-state.js +3 -26
- package/dist/ui/file-input.js +17 -37
- package/dist/ui/filter-bar.d.ts +1 -1
- package/dist/ui/filter-bar.js +11 -71
- package/dist/ui/filter-chip.d.ts +1 -1
- package/dist/ui/filter-chip.js +7 -15
- package/dist/ui/gate-block.d.ts +2 -1
- package/dist/ui/gate-block.js +4 -29
- package/dist/ui/horizontal-stepper.d.ts +1 -1
- package/dist/ui/horizontal-stepper.js +18 -45
- package/dist/ui/input.js +1 -6
- package/dist/ui/inspector-shell.d.ts +5 -5
- package/dist/ui/inspector-shell.js +14 -81
- package/dist/ui/intake-form.d.ts +1 -1
- package/dist/ui/intake-form.js +21 -97
- package/dist/ui/kind-card-grid.js +9 -31
- package/dist/ui/library-page-header.js +4 -32
- package/dist/ui/list-detail.js +5 -35
- package/dist/ui/live-run-banner.d.ts +1 -1
- package/dist/ui/live-run-banner.js +5 -15
- package/dist/ui/live-status-line.js +6 -24
- package/dist/ui/menu.js +7 -24
- package/dist/ui/message-bubble.d.ts +3 -3
- package/dist/ui/message-bubble.js +5 -44
- package/dist/ui/mic-button.d.ts +1 -1
- package/dist/ui/mic-button.js +15 -21
- package/dist/ui/mic-permission-dialog.js +5 -41
- package/dist/ui/notifications-bell.js +9 -43
- package/dist/ui/now-cards.d.ts +2 -2
- package/dist/ui/now-cards.js +16 -94
- package/dist/ui/page-panel.js +11 -11
- package/dist/ui/page-shell.js +4 -11
- package/dist/ui/parts-renderer.js +51 -149
- package/dist/ui/profile-card.js +7 -111
- package/dist/ui/progress-checklist.js +11 -49
- package/dist/ui/provider-mark.d.ts +2 -2
- package/dist/ui/provider-mark.js +9 -16
- package/dist/ui/quick-reply-chips.js +6 -14
- package/dist/ui/quote-card.js +12 -32
- package/dist/ui/reasoning-block.d.ts +1 -1
- package/dist/ui/reasoning-block.js +14 -33
- package/dist/ui/research-body.js +32 -213
- package/dist/ui/rich-empty-state.js +6 -48
- package/dist/ui/run-now-button.js +14 -15
- package/dist/ui/section.d.ts +1 -1
- package/dist/ui/section.js +4 -40
- package/dist/ui/select.js +1 -15
- package/dist/ui/selection-checkbox.d.ts +1 -1
- package/dist/ui/selection-checkbox.js +11 -29
- package/dist/ui/settings-panel.js +7 -50
- package/dist/ui/shimmer-text.js +3 -6
- package/dist/ui/sidebar-item-row.d.ts +1 -1
- package/dist/ui/sidebar-item-row.js +9 -39
- package/dist/ui/sidebar-panel.js +15 -40
- package/dist/ui/sidebar.js +15 -76
- package/dist/ui/skeleton.js +3 -7
- package/dist/ui/sortable-table.js +29 -72
- package/dist/ui/sparkline.js +14 -38
- package/dist/ui/stat-grid.js +17 -52
- package/dist/ui/status-dot.d.ts +1 -1
- package/dist/ui/status-dot.js +8 -20
- package/dist/ui/step-list.d.ts +1 -0
- package/dist/ui/step-list.js +11 -50
- package/dist/ui/switch.js +4 -16
- package/dist/ui/table.js +7 -32
- package/dist/ui/tabs.js +39 -54
- package/dist/ui/textarea.js +10 -19
- package/dist/ui/theme-provider.js +54 -78
- package/dist/ui/theme-toggle.js +13 -16
- package/dist/ui/thinking-indicator.js +3 -15
- package/dist/ui/thinking-label.js +16 -35
- package/dist/ui/thinking-mark.d.ts +1 -1
- package/dist/ui/thinking-mark.js +4 -145
- package/dist/ui/time-series-chart.js +43 -174
- package/dist/ui/toast.js +12 -20
- package/dist/ui/token-mosaic.js +5 -43
- package/dist/ui/tool-block.js +18 -70
- package/dist/ui/tool-narrative.d.ts +1 -1
- package/dist/ui/tool-narrative.js +10 -57
- package/dist/ui/tool-picker.js +57 -145
- package/dist/ui/tooltip.d.ts +1 -1
- package/dist/ui/tooltip.js +4 -33
- package/dist/ui/top-bar.js +14 -72
- package/dist/ui/trace-waterfall.d.ts +1 -1
- package/dist/ui/trace-waterfall.js +21 -93
- package/dist/ui/typing-indicator.d.ts +1 -1
- package/dist/ui/typing-indicator.js +4 -18
- package/dist/ui/view-toggle.js +10 -29
- package/dist/ui/voice-waveform.js +3 -11
- 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
|
-
*/
|
|
25
|
-
|
|
26
|
-
|
|
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) *
|
|
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
|
-
*/
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
*/
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
*/
|
|
69
|
-
|
|
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)
|
|
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
|
-
*/
|
|
91
|
+
*/
|
|
92
|
+
export function formatCompact(value) {
|
|
82
93
|
const magnitude = Math.abs(value);
|
|
83
|
-
if (magnitude >= 1_000_000)
|
|
84
|
-
|
|
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
|
-
*/
|
|
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
|
-
*/
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
*/
|
|
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
|
-
*/
|
|
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
|
-
*/
|
|
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
|
}
|
package/dist/lib/chat-message.js
CHANGED
|
@@ -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
|
-
*/
|
|
10
|
-
|
|
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. */
|
|
13
|
-
|
|
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. */
|
|
19
|
-
|
|
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
|
-
*/
|
|
27
|
-
|
|
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. */
|
|
39
|
-
|
|
40
|
-
|
|
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
|
}
|
package/dist/lib/chat-parts.js
CHANGED
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
|
-
*/
|
|
13
|
-
/**
|
|
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
|
-
*/
|
|
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
|
-
*/
|
|
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
|
-
*/
|
|
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
|
-
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
48
55
|
quoted = false;
|
|
49
56
|
}
|
|
50
|
-
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
51
59
|
field += char;
|
|
52
60
|
}
|
|
53
|
-
}
|
|
61
|
+
}
|
|
62
|
+
else if (char === '"') {
|
|
54
63
|
quoted = true;
|
|
55
|
-
}
|
|
64
|
+
}
|
|
65
|
+
else if (char === ",") {
|
|
56
66
|
fields.push(field);
|
|
57
67
|
field = "";
|
|
58
|
-
}
|
|
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
|
-
*/
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
*/
|
|
92
|
-
|
|
93
|
-
|
|
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. */
|
|
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
|
|
102
|
-
|
|
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
|
}
|
package/dist/lib/metrics.js
CHANGED
|
@@ -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
|
-
*/
|
|
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
|
-
*/
|
|
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
|
-
*/
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const points = coords.map((c)
|
|
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. */
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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. */
|
|
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
|
-
*/
|
|
81
|
-
|
|
82
|
-
|
|
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;
|
package/dist/lib/now-item.js
CHANGED
|
@@ -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
|
-
*/
|
|
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. */
|
|
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. */
|
|
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
|
-
*/
|
|
32
|
-
|
|
33
|
-
|
|
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)
|
|
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
|
-
*/
|
|
46
|
+
*/
|
|
47
|
+
export function groupNowItemsByPriority(items) {
|
|
44
48
|
const sorted = sortNowItems(items);
|
|
45
|
-
const order = [
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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. */
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
],
|
|
6
|
-
[
|
|
7
|
-
|
|
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
|
-
*/
|
|
16
|
+
*/
|
|
17
|
+
export function formatRelativeTime(iso, now = Date.now()) {
|
|
35
18
|
const then = Date.parse(iso);
|
|
36
|
-
if (Number.isNaN(then))
|
|
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)
|
|
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
|
}
|