@cboxdk/cbox-ui 0.6.0 → 0.8.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/README.md CHANGED
@@ -27,7 +27,7 @@ The package is published on npm as `@cboxdk/cbox-ui` under the MIT License. The
27
27
  @import '@cboxdk/cbox-ui/tokens';
28
28
  ```
29
29
 
30
- That's it — every `text-foreground`, `bg-card`, `border-border`, `text-mono`, etc. utility now resolves against the Cbox palette. Light + dark variants come bundled.
30
+ That's it — every `text-foreground`, `bg-card`, `border-border`, `text-mono`, etc. utility now resolves against the Cbox palette. Light + dark variants come bundled: dark applies under `.dark` or `data-theme="dark"` on the root, whichever the host writes. Add `@source '../../node_modules/@cboxdk/cbox-ui/dist';` so Tailwind generates the utilities the components use, and when the host stores its theme choice as an attribute, point Tailwind's `dark:` variant at it (`@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));`).
31
31
 
32
32
  ### 2. Wrap the app root with `<CboxIdProvider>`
33
33
 
@@ -113,7 +113,8 @@ Match id's existing patterns — see id/resources/js/pages/* for examples.
113
113
  | `TopBar` | Standard 56px-tall app bar. Slot for brand + centre + right cluster. |
114
114
  | `OrgSwitcher` | Dropdown listing the user's orgs. Calls `onSwitchOrganization` (or POSTs to `urls.switchOrgUrl`) on pick. |
115
115
  | `AppLauncher` | 9-dot grid showing sister Cbox apps the user can jump into. |
116
- | `UserMenu` | Avatar dropdown with "Manage account ↗" + "Team & billing ↗" (deep-links into id) + Sign out. |
116
+ | `UserMenu` | Avatar dropdown with "Manage account ↗" + "Team & billing ↗" (deep-links into id) + Sign out. Pass `LinkComponent` when those rows are pages of the host app, so the visit stays client-side. |
117
+ | `CboxAppCatalog` | The Switch-app control fed live from the host's catalog endpoint (see "App catalog" below). |
117
118
  | `NotificationsBell` | Bell icon with unread badge, links to id's unified inbox. Renders nothing if context lacks `notifications`. |
118
119
 
119
120
  ### Page primitives
@@ -130,6 +131,17 @@ Match id's existing patterns — see id/resources/js/pages/* for examples.
130
131
  | `KeyValue` / `KeyValueList` | Definition-list style "label → value" for detail panels. |
131
132
  | `Heading` | Section heading without the panel chrome. |
132
133
 
134
+ ### Charts
135
+
136
+ Time series use ECharts, from the `@cboxdk/cbox-ui/charts` entry so apps without charts never load it. Install `echarts` (^6) next to cbox-ui to use it; ECharts itself is fetched on the first chart's mount.
137
+
138
+ | Export | What it does |
139
+ |---|---|
140
+ | `TimeChart` (`/charts`) | Lines over a time window drawn to scale from zero with round ticks, a faint area under the primary series, thin lines for the rest (one per pod, say), labelled vertical lines for `events` (deploys, scaling), gaps for missing samples, an axis tooltip, and zoom by slider, pinch or Ctrl/⌘ + scroll. Colours come from `--chart-1…5` and follow dark mode. Times are milliseconds. Keep a table of the same numbers beside it. |
141
+ | `Chart` (`/charts`) | The ECharts wrapper under it: SVG renderer, token colours, theme and reduced-motion aware. Build other chart types on it. |
142
+ | `Sparkline` | A small decorative SVG line for table rows. No ECharts. |
143
+ | `niceTicks` / `timeTicks` / `seriesData` | The axis and gap arithmetic. |
144
+
133
145
  ## Versioning
134
146
 
135
147
  This package follows semver. **Breaking changes get a major bump** — every Cbox app pinning a major version stays safe. New components, new optional props, new tokens → minor. Token tweaks that don't change the visual contract → patch.
@@ -154,3 +166,89 @@ Switch back to the published version with `bun unlink @cboxdk/cbox-ui && bun ins
154
166
  ## Brand
155
167
 
156
168
  Brand guidelines, logo files, and the canonical asset set live in [`BRAND.md`](./BRAND.md) and `assets/`.
169
+
170
+ ## Localization
171
+
172
+ Wrap the app shell in `CboxLocalizationProvider` to translate shared navigation,
173
+ menus, placeholders and accessible labels. Existing consumers keep English when
174
+ no provider is present. The host owns the selected language and its catalogs:
175
+
176
+ ```tsx
177
+ <CboxLocalizationProvider locale={language} translate={t}>
178
+ <CboxAppShell {...shellProps}>{children}</CboxAppShell>
179
+ </CboxLocalizationProvider>
180
+ ```
181
+
182
+ `translate(message, values?)` returns plain text. Messages use English source
183
+ keys and named placeholders, such as `Settings for {name}`. Preserve placeholders
184
+ when translating. React escapes the returned text; do not return HTML. Consumer
185
+ labels, notification content, organization names and other user data remain the
186
+ host's responsibility. The provider does not persist settings or infer regional
187
+ number/date formats from interface language. Cbox apps read those preferences
188
+ from Cbox ID's versioned `preferences` namespace.
189
+
190
+ Pass an optional `formatDate(isoTimestamp)` callback when the host manages date
191
+ format and timezone preferences separately. Notification timestamps use that
192
+ callback; without it, existing browser formatting is preserved.
193
+
194
+ ### App catalog (Switch app)
195
+
196
+ `AppSwitcher` and `AppLauncher` render the static `apps` list from `CboxIdProvider`. Apps
197
+ that read the person's catalog live from Cbox ID use `CboxAppCatalog` instead: the host
198
+ serves a same-origin endpoint (a proxy of Cbox ID's catalog under the signed-in person's
199
+ grant) and the component renders its answer, refreshes it when `refresh_after` expires
200
+ while the panel is open, and offers the way back to Cbox ID when the grant is missing.
201
+
202
+ ```tsx
203
+ <CboxAppCatalog endpoint={appCatalogUrl} currentAppKey="infra" />
204
+ ```
205
+
206
+ The endpoint contract (`parseAppCatalog` is exported, so a host can test its own answer):
207
+
208
+ - `200 { apps: [{ key, name, launch_url, icon_url?, icon_dark_url?, description? }], refresh_after?: seconds }`
209
+ — `refresh_after` is clamped to 1–60 s; apps without an `http(s)` launch URL are dropped.
210
+ - `401`/`403` with `{ authorize_url }` or `{ state: 'authorization_required' }` — the panel
211
+ says the apps could not be loaded and links "Reconnect to Cbox ID" to `authorize_url` with
212
+ `return_to` set to the current path (same-origin URLs only).
213
+ - Anything else — "Apps could not be loaded." with a retry.
214
+
215
+ `endpoint={null}` renders the control disabled, so the chrome keeps its shape on pages
216
+ without an organization.
217
+
218
+ App icons come from Cbox ID's catalog: `icon_url` for light surfaces and `icon_dark_url` for
219
+ dark ones. The tiles render both and the token stylesheet shows the one for the current theme
220
+ (`.dark` or `[data-theme="dark"]` on the root); an app without an icon shows its initials. Apps
221
+ keep no local copy of the marks. Each answer is kept in this browser (`cbox-app-catalog`), so
222
+ `<CboxServiceMark appKey="infra" />` and the collapsed `CboxBrand` in the rail paint the app's
223
+ own icon at once on the next page. A host that loads the catalog itself calls
224
+ `rememberAppCatalog(apps)`; `CboxAppIcon` renders any single app's icon.
225
+
226
+ ### Workspace navigation (current design contract)
227
+
228
+ `CboxWorkspaceShell` is the two-tier product shell. Pass `areas` (each has a
229
+ label, href, icon and optional child `items`), the router's `currentPath` and
230
+ `LinkComponent`, plus `context`, `tools` and `account` slots. Include the real
231
+ organization context, search, notifications and Switch app in these slots;
232
+ render `UserMenu placement="top-start"` in `account`. The account stays at the
233
+ bottom of the main rail. Existing `CboxAppShell` consumers retain their API.
234
+
235
+ The rail reserves 64px, expands on hover/focus without moving content, and
236
+ reserves 222px when pinned. Context navigation only appears for multi-page
237
+ areas; its panel button and Command/Control-period collapse it to a 26px strip.
238
+ Mobile uses a modal navigation sheet and retains the header app switcher.
239
+ Query strings do not change active navigation, and the **most specific href wins**: an
240
+ area at the organization root (`/acme`) does not claim `/acme/projects/...` when a
241
+ Projects area exists, and inside a subnav only the deepest matching item is active. Active
242
+ links carry `aria-current="page"`.
243
+
244
+ Hosts own route construction, organization switching, search and preference persistence.
245
+ For the two shell preferences the host persists, pass them in: `pinned` + `onPinnedChange`
246
+ and `collapsed` + `onCollapsedChange` (controlled), or `defaultPinned` / `defaultCollapsed`
247
+ to seed the shell's own state. `brandHref` points the brand row somewhere other than `/`
248
+ (the current organization's overview), and `mainId` names the `<main>` landmark for the
249
+ host's skip link (default `main-content`).
250
+
251
+ Core tokens now match the warm cream/ink design-system colors. Body text uses
252
+ the system stack; hosts self-host Plus Jakarta Sans for headings and JetBrains
253
+ Mono for technical values. Card/control/badge radii are 12/8/6px. The main canvas
254
+ uses the approved single radial wash at 30% 0 in both themes.
@@ -0,0 +1,82 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { EChartsCoreOption } from 'echarts/core';
3
+ import { T as TimePoint } from './scale-BaaY5qJs.js';
4
+ export { S as SeriesDatum, n as niceTicks, s as seriesData, t as timeTicks } from './scale-BaaY5qJs.js';
5
+
6
+ /** The chart colours of the mode on screen, read from the design tokens. */
7
+ type ChartColors = {
8
+ series: string[];
9
+ text: string;
10
+ muted: string;
11
+ grid: string;
12
+ surface: string;
13
+ border: string;
14
+ font: string;
15
+ mono: string;
16
+ };
17
+ declare function readChartColors(): ChartColors;
18
+ /** Follows the theme switch: the `dark` class and `data-theme` on <html>, and the OS setting. */
19
+ declare function useChartColors(): ChartColors;
20
+ /**
21
+ * One ECharts chart, the Cbox convention: SVG renderer, sized by its box, ECharts imported on
22
+ * first mount. The option is built from the colours on screen, so light and dark follow, and
23
+ * animation is off under reduced motion. `label` is what a screen reader hears; keep a table
24
+ * of the same numbers beside the chart. Keep `option` stable (useCallback) to avoid redraws.
25
+ */
26
+ declare function Chart({ option, label, height, className }: {
27
+ option: (colors: ChartColors) => EChartsCoreOption;
28
+ label: string;
29
+ height?: number;
30
+ className?: string;
31
+ }): react_jsx_runtime.JSX.Element;
32
+
33
+ type TimeSeries = {
34
+ key: string;
35
+ label: string;
36
+ /** Contiguous runs of samples; a new run starts after a gap, which is drawn as a gap. */
37
+ segments: TimePoint[][];
38
+ /** The headline series: drawn with a faint area. Defaults to the first series. */
39
+ primary?: boolean;
40
+ /** A reference to read the others against (last week, a limit): dashed, no area. */
41
+ dashed?: boolean;
42
+ };
43
+ /** A moment worth explaining (a deploy, a scale step): a labelled vertical line, never a cause claim. */
44
+ type ChartEvent = {
45
+ /** Milliseconds since the epoch. */
46
+ at: number;
47
+ label: string;
48
+ /** Index into the chart tokens; deploys use 1 and scaling 2 by convention. */
49
+ tone?: number;
50
+ /** Extra tooltip line, such as the end time and outcome. Plain text. */
51
+ detail?: string;
52
+ };
53
+ type TimeChartProps = {
54
+ series: TimeSeries[];
55
+ /** The window, in milliseconds. */
56
+ from: number;
57
+ to: number;
58
+ /** Accessible name; keep a table of the same numbers next to the chart. */
59
+ label: string;
60
+ formatValue: (value: number) => string;
61
+ /** Axis and tooltip time. Say the zone once beside the chart. */
62
+ formatTime?: (t: number) => string;
63
+ /** Appended to values in the tooltip. */
64
+ unit?: string;
65
+ /** Lowest top of the y axis, so a flat near-zero line does not fill the plot. */
66
+ minimumTop?: number;
67
+ events?: ChartEvent[];
68
+ /** A moment marked with a dashed line, such as a selected request. */
69
+ marker?: number | null;
70
+ /** Zoom by slider, pinch or Ctrl/⌘ + scroll. On by default. */
71
+ zoom?: boolean;
72
+ height?: number;
73
+ className?: string;
74
+ };
75
+ /**
76
+ * THE CBOX TIME CHART (ECharts): drawn to scale from zero with round ticks, a line with a
77
+ * faint area for the headline series, thin lines for the rest, labelled vertical lines for
78
+ * events, gaps for missing samples, an axis tooltip with every series, and zoom.
79
+ */
80
+ declare function TimeChart({ series, from, to, label, formatValue, formatTime, unit, minimumTop, events, marker, zoom, height, className }: TimeChartProps): react_jsx_runtime.JSX.Element;
81
+
82
+ export { Chart, type ChartColors, type ChartEvent, TimeChart, type TimeChartProps, TimePoint, type TimeSeries, readChartColors, useChartColors };
package/dist/charts.js ADDED
@@ -0,0 +1,189 @@
1
+ import { cn, niceTicks, seriesData } from './chunk-OFTNLF7H.js';
2
+ export { niceTicks, seriesData, timeTicks } from './chunk-OFTNLF7H.js';
3
+ import { useState, useEffect, useRef, useCallback } from 'react';
4
+ import { jsx } from 'react/jsx-runtime';
5
+
6
+ function readChartColors() {
7
+ const style = getComputedStyle(document.documentElement);
8
+ const token = (name, fallback) => style.getPropertyValue(name).trim() || fallback;
9
+ return {
10
+ series: [1, 2, 3, 4, 5].map((index) => token(`--chart-${index}`, "#2a78d6")),
11
+ text: token("--foreground", "#1f1a13"),
12
+ muted: token("--muted-foreground", "#6a6257"),
13
+ grid: token("--border", "#e2ded5"),
14
+ surface: token("--popover", "#fffefc"),
15
+ border: token("--border", "#e2ded5"),
16
+ font: getComputedStyle(document.body).fontFamily,
17
+ mono: token("--font-mono", "") || "ui-monospace, SFMono-Regular, Menlo, Monaco, monospace"
18
+ };
19
+ }
20
+ function useChartColors() {
21
+ const [colors, setColors] = useState(readChartColors);
22
+ useEffect(() => {
23
+ const update = () => setColors(readChartColors());
24
+ const observer = new MutationObserver(update);
25
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class", "data-theme"] });
26
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
27
+ media.addEventListener("change", update);
28
+ return () => {
29
+ observer.disconnect();
30
+ media.removeEventListener("change", update);
31
+ };
32
+ }, []);
33
+ return colors;
34
+ }
35
+ function Chart({ option, label, height = 220, className }) {
36
+ const box = useRef(null);
37
+ const [chart, setChart] = useState(null);
38
+ const colors = useChartColors();
39
+ useEffect(() => {
40
+ let disposed = false;
41
+ let instance = null;
42
+ let resize = null;
43
+ void import('./engine-4DRZ3POZ.js').then(({ echarts }) => {
44
+ const element = box.current;
45
+ if (disposed || !element) {
46
+ return;
47
+ }
48
+ instance = echarts.init(element, null, { renderer: "svg" });
49
+ resize = new ResizeObserver(() => instance?.resize());
50
+ resize.observe(element);
51
+ setChart(instance);
52
+ });
53
+ return () => {
54
+ disposed = true;
55
+ resize?.disconnect();
56
+ instance?.dispose();
57
+ };
58
+ }, []);
59
+ useEffect(() => {
60
+ if (!chart) {
61
+ return;
62
+ }
63
+ const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
64
+ chart.setOption({ animation: !reduce, aria: { enabled: true, label: { description: label } }, ...option(colors) }, true);
65
+ }, [chart, option, colors, label]);
66
+ return /* @__PURE__ */ jsx("div", { ref: box, style: { height }, role: "img", "aria-label": label, className: cn("w-full", className) });
67
+ }
68
+ var defaultTime = (t) => new Intl.DateTimeFormat(void 0, { hour: "2-digit", minute: "2-digit", hour12: false }).format(t);
69
+ var escape = (text) => text.replace(/[&<>"']/g, (character) => `&#${character.charCodeAt(0)};`);
70
+ function TimeChart({ series, from, to, label, formatValue, formatTime = defaultTime, unit = "", minimumTop = 0, events = [], marker = null, zoom = true, height = 260, className }) {
71
+ const option = useCallback(
72
+ (colors) => {
73
+ const ticks = niceTicks(Math.max(minimumTop, ...series.flatMap((item) => item.segments.flat().map((point) => point.value))));
74
+ const top = ticks[ticks.length - 1] ?? 1;
75
+ const axisLabel = { color: colors.muted, fontFamily: colors.mono, fontSize: 11 };
76
+ const primary = Math.max(0, series.findIndex((item) => item.primary));
77
+ const tone = (index) => colors.series[index % colors.series.length] ?? colors.text;
78
+ const value = (number) => `${formatValue(number)}${unit ? ` ${unit}` : ""}`;
79
+ const lines = [
80
+ ...events.filter((event) => event.at >= from && event.at <= to).map((event) => ({
81
+ xAxis: event.at,
82
+ name: event.label,
83
+ event,
84
+ lineStyle: { color: tone(event.tone ?? 1), type: "solid", width: 1, opacity: 0.8 },
85
+ label: { show: true, formatter: event.label, position: "insideEndTop", color: tone(event.tone ?? 1), fontFamily: colors.mono, fontSize: 10 }
86
+ })),
87
+ ...marker !== null && marker >= from && marker <= to ? [{ xAxis: marker, name: "", lineStyle: { color: colors.text, type: "dashed", width: 1 }, label: { show: false } }] : []
88
+ ];
89
+ return {
90
+ color: colors.series,
91
+ textStyle: { fontFamily: colors.font, color: colors.text },
92
+ grid: { top: 16, right: 16, bottom: zoom ? 56 : 24, left: 56 },
93
+ xAxis: {
94
+ type: "time",
95
+ min: from,
96
+ max: to,
97
+ axisLine: { lineStyle: { color: colors.grid } },
98
+ axisTick: { show: false },
99
+ splitLine: { show: false },
100
+ axisLabel: { ...axisLabel, hideOverlap: true, formatter: (t) => formatTime(t) }
101
+ },
102
+ yAxis: {
103
+ type: "value",
104
+ min: 0,
105
+ max: top,
106
+ interval: top / Math.max(1, ticks.length - 1),
107
+ axisLabel: { ...axisLabel, formatter: (number) => formatValue(number) },
108
+ splitLine: { lineStyle: { color: colors.grid, type: "dashed" } }
109
+ },
110
+ dataZoom: zoom ? [
111
+ { type: "inside", xAxisIndex: 0, filterMode: "none", zoomOnMouseWheel: "ctrl", moveOnMouseWheel: false, moveOnMouseMove: true },
112
+ {
113
+ type: "slider",
114
+ xAxisIndex: 0,
115
+ filterMode: "none",
116
+ height: 18,
117
+ bottom: 8,
118
+ borderColor: colors.grid,
119
+ fillerColor: "rgba(127,127,127,0.12)",
120
+ handleStyle: { color: colors.surface, borderColor: colors.muted },
121
+ moveHandleSize: 0,
122
+ showDetail: false,
123
+ dataBackground: { lineStyle: { color: colors.muted, opacity: 0.4 }, areaStyle: { opacity: 0 } },
124
+ selectedDataBackground: { lineStyle: { color: tone(0) }, areaStyle: { opacity: 0 } },
125
+ textStyle: axisLabel
126
+ }
127
+ ] : [],
128
+ tooltip: {
129
+ trigger: "axis",
130
+ confine: true,
131
+ backgroundColor: colors.surface,
132
+ borderColor: colors.border,
133
+ borderWidth: 1,
134
+ padding: [6, 10],
135
+ textStyle: { color: colors.text, fontFamily: colors.mono, fontSize: 12 },
136
+ axisPointer: { type: "line", lineStyle: { color: colors.muted, opacity: 0.5 } },
137
+ formatter: (items) => {
138
+ const present = items.filter((item) => item.value[1] !== null);
139
+ const first = present[0];
140
+ if (!first) {
141
+ return "";
142
+ }
143
+ const rows = present.map(
144
+ (item) => series.length > 1 ? `<span style="color:${item.color}">\u25CF</span> ${escape(item.seriesName)} <b>${escape(value(item.value[1] ?? 0))}</b>` : `<b>${escape(value(item.value[1] ?? 0))}</b>`
145
+ );
146
+ return `${escape(formatTime(first.value[0]))}<br/>${rows.join("<br/>")}`;
147
+ }
148
+ },
149
+ series: series.map((item, index) => {
150
+ const color = tone(index);
151
+ const isPrimary = index === primary;
152
+ return {
153
+ type: "line",
154
+ name: item.label,
155
+ data: seriesData(item.segments),
156
+ connectNulls: false,
157
+ symbol: "circle",
158
+ symbolSize: 0,
159
+ lineStyle: { width: isPrimary ? 1.5 : 1, color, type: item.dashed ? "dashed" : "solid" },
160
+ itemStyle: { color },
161
+ areaStyle: isPrimary && !item.dashed ? { opacity: 0.14, color } : void 0,
162
+ emphasis: { disabled: true },
163
+ z: isPrimary ? 3 : 2,
164
+ markLine: isPrimary && lines.length > 0 ? {
165
+ silent: false,
166
+ symbol: "none",
167
+ data: lines,
168
+ tooltip: {
169
+ formatter: (entry) => {
170
+ const event = entry.data.event;
171
+ if (!event) {
172
+ return escape(entry.data.name);
173
+ }
174
+ return `${escape(event.label)}<br/>${escape(formatTime(event.at))}${event.detail ? ` \xB7 ${escape(event.detail)}` : ""}`;
175
+ }
176
+ }
177
+ } : void 0
178
+ };
179
+ })
180
+ };
181
+ },
182
+ [series, from, to, formatValue, formatTime, unit, minimumTop, events, marker, zoom]
183
+ );
184
+ return /* @__PURE__ */ jsx(Chart, { option, label, height, className });
185
+ }
186
+
187
+ export { Chart, TimeChart, readChartColors, useChartColors };
188
+ //# sourceMappingURL=charts.js.map
189
+ //# sourceMappingURL=charts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/charts/chart.tsx","../src/charts/time-chart.tsx"],"names":["jsx"],"mappings":"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","file":"charts.js","sourcesContent":["import type { ECharts, EChartsCoreOption } from 'echarts/core';\nimport { useEffect, useRef, useState } from 'react';\nimport { cn } from '../utils/cn';\n\n/** The chart colours of the mode on screen, read from the design tokens. */\nexport type ChartColors = {\n series: string[];\n text: string;\n muted: string;\n grid: string;\n surface: string;\n border: string;\n font: string;\n mono: string;\n};\n\nexport function readChartColors(): ChartColors {\n const style = getComputedStyle(document.documentElement);\n const token = (name: string, fallback: string) => style.getPropertyValue(name).trim() || fallback;\n\n return {\n series: [1, 2, 3, 4, 5].map((index) => token(`--chart-${index}`, '#2a78d6')),\n text: token('--foreground', '#1f1a13'),\n muted: token('--muted-foreground', '#6a6257'),\n grid: token('--border', '#e2ded5'),\n surface: token('--popover', '#fffefc'),\n border: token('--border', '#e2ded5'),\n font: getComputedStyle(document.body).fontFamily,\n mono: token('--font-mono', '') || 'ui-monospace, SFMono-Regular, Menlo, Monaco, monospace',\n };\n}\n\n/** Follows the theme switch: the `dark` class and `data-theme` on <html>, and the OS setting. */\nexport function useChartColors(): ChartColors {\n const [colors, setColors] = useState(readChartColors);\n\n useEffect(() => {\n const update = () => setColors(readChartColors());\n const observer = new MutationObserver(update);\n observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme'] });\n const media = window.matchMedia('(prefers-color-scheme: dark)');\n media.addEventListener('change', update);\n\n return () => {\n observer.disconnect();\n media.removeEventListener('change', update);\n };\n }, []);\n\n return colors;\n}\n\n/**\n * One ECharts chart, the Cbox convention: SVG renderer, sized by its box, ECharts imported on\n * first mount. The option is built from the colours on screen, so light and dark follow, and\n * animation is off under reduced motion. `label` is what a screen reader hears; keep a table\n * of the same numbers beside the chart. Keep `option` stable (useCallback) to avoid redraws.\n */\nexport function Chart({ option, label, height = 220, className }: { option: (colors: ChartColors) => EChartsCoreOption; label: string; height?: number; className?: string }) {\n const box = useRef<HTMLDivElement>(null);\n const [chart, setChart] = useState<ECharts | null>(null);\n const colors = useChartColors();\n\n useEffect(() => {\n let disposed = false;\n let instance: ECharts | null = null;\n let resize: ResizeObserver | null = null;\n\n void import('./engine').then(({ echarts }) => {\n const element = box.current;\n\n if (disposed || !element) {\n return;\n }\n\n instance = echarts.init(element, null, { renderer: 'svg' });\n resize = new ResizeObserver(() => instance?.resize());\n resize.observe(element);\n setChart(instance);\n });\n\n return () => {\n disposed = true;\n resize?.disconnect();\n instance?.dispose();\n };\n }, []);\n\n useEffect(() => {\n if (!chart) {\n return;\n }\n\n const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n chart.setOption({ animation: !reduce, aria: { enabled: true, label: { description: label } }, ...option(colors) }, true);\n }, [chart, option, colors, label]);\n\n return <div ref={box} style={{ height }} role=\"img\" aria-label={label} className={cn('w-full', className)} />;\n}\n","import { useCallback } from 'react';\nimport { Chart, type ChartColors } from './chart';\nimport { niceTicks, seriesData, type TimePoint } from './scale';\n\nexport type TimeSeries = {\n key: string;\n label: string;\n /** Contiguous runs of samples; a new run starts after a gap, which is drawn as a gap. */\n segments: TimePoint[][];\n /** The headline series: drawn with a faint area. Defaults to the first series. */\n primary?: boolean;\n /** A reference to read the others against (last week, a limit): dashed, no area. */\n dashed?: boolean;\n};\n\n/** A moment worth explaining (a deploy, a scale step): a labelled vertical line, never a cause claim. */\nexport type ChartEvent = {\n /** Milliseconds since the epoch. */\n at: number;\n label: string;\n /** Index into the chart tokens; deploys use 1 and scaling 2 by convention. */\n tone?: number;\n /** Extra tooltip line, such as the end time and outcome. Plain text. */\n detail?: string;\n};\n\nexport type TimeChartProps = {\n series: TimeSeries[];\n /** The window, in milliseconds. */\n from: number;\n to: number;\n /** Accessible name; keep a table of the same numbers next to the chart. */\n label: string;\n formatValue: (value: number) => string;\n /** Axis and tooltip time. Say the zone once beside the chart. */\n formatTime?: (t: number) => string;\n /** Appended to values in the tooltip. */\n unit?: string;\n /** Lowest top of the y axis, so a flat near-zero line does not fill the plot. */\n minimumTop?: number;\n events?: ChartEvent[];\n /** A moment marked with a dashed line, such as a selected request. */\n marker?: number | null;\n /** Zoom by slider, pinch or Ctrl/⌘ + scroll. On by default. */\n zoom?: boolean;\n height?: number;\n className?: string;\n};\n\nconst defaultTime = (t: number) => new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit', hour12: false }).format(t);\n\nconst escape = (text: string) => text.replace(/[&<>\"']/g, (character) => `&#${character.charCodeAt(0)};`);\n\n/**\n * THE CBOX TIME CHART (ECharts): drawn to scale from zero with round ticks, a line with a\n * faint area for the headline series, thin lines for the rest, labelled vertical lines for\n * events, gaps for missing samples, an axis tooltip with every series, and zoom.\n */\nexport function TimeChart({ series, from, to, label, formatValue, formatTime = defaultTime, unit = '', minimumTop = 0, events = [], marker = null, zoom = true, height = 260, className }: TimeChartProps) {\n const option = useCallback(\n (colors: ChartColors) => {\n const ticks = niceTicks(Math.max(minimumTop, ...series.flatMap((item) => item.segments.flat().map((point) => point.value))));\n const top = ticks[ticks.length - 1] ?? 1;\n const axisLabel = { color: colors.muted, fontFamily: colors.mono, fontSize: 11 };\n const primary = Math.max(0, series.findIndex((item) => item.primary));\n const tone = (index: number) => colors.series[index % colors.series.length] ?? colors.text;\n const value = (number: number) => `${formatValue(number)}${unit ? ` ${unit}` : ''}`;\n const lines = [\n ...events\n .filter((event) => event.at >= from && event.at <= to)\n .map((event) => ({\n xAxis: event.at,\n name: event.label,\n event,\n lineStyle: { color: tone(event.tone ?? 1), type: 'solid', width: 1, opacity: 0.8 },\n label: { show: true, formatter: event.label, position: 'insideEndTop', color: tone(event.tone ?? 1), fontFamily: colors.mono, fontSize: 10 },\n })),\n ...(marker !== null && marker >= from && marker <= to ? [{ xAxis: marker, name: '', lineStyle: { color: colors.text, type: 'dashed', width: 1 }, label: { show: false } }] : []),\n ];\n\n return {\n color: colors.series,\n textStyle: { fontFamily: colors.font, color: colors.text },\n grid: { top: 16, right: 16, bottom: zoom ? 56 : 24, left: 56 },\n xAxis: {\n type: 'time',\n min: from,\n max: to,\n axisLine: { lineStyle: { color: colors.grid } },\n axisTick: { show: false },\n splitLine: { show: false },\n axisLabel: { ...axisLabel, hideOverlap: true, formatter: (t: number) => formatTime(t) },\n },\n yAxis: {\n type: 'value',\n min: 0,\n max: top,\n interval: top / Math.max(1, ticks.length - 1),\n axisLabel: { ...axisLabel, formatter: (number: number) => formatValue(number) },\n splitLine: { lineStyle: { color: colors.grid, type: 'dashed' } },\n },\n dataZoom: zoom\n ? [\n { type: 'inside', xAxisIndex: 0, filterMode: 'none', zoomOnMouseWheel: 'ctrl', moveOnMouseWheel: false, moveOnMouseMove: true },\n {\n type: 'slider',\n xAxisIndex: 0,\n filterMode: 'none',\n height: 18,\n bottom: 8,\n borderColor: colors.grid,\n fillerColor: 'rgba(127,127,127,0.12)',\n handleStyle: { color: colors.surface, borderColor: colors.muted },\n moveHandleSize: 0,\n showDetail: false,\n dataBackground: { lineStyle: { color: colors.muted, opacity: 0.4 }, areaStyle: { opacity: 0 } },\n selectedDataBackground: { lineStyle: { color: tone(0) }, areaStyle: { opacity: 0 } },\n textStyle: axisLabel,\n },\n ]\n : [],\n tooltip: {\n trigger: 'axis',\n confine: true,\n backgroundColor: colors.surface,\n borderColor: colors.border,\n borderWidth: 1,\n padding: [6, 10],\n textStyle: { color: colors.text, fontFamily: colors.mono, fontSize: 12 },\n axisPointer: { type: 'line', lineStyle: { color: colors.muted, opacity: 0.5 } },\n formatter: (items: { seriesName: string; color: string; value: [number, number | null] }[]) => {\n const present = items.filter((item) => item.value[1] !== null);\n const first = present[0];\n\n if (!first) {\n return '';\n }\n\n const rows = present.map((item) =>\n series.length > 1 ? `<span style=\"color:${item.color}\">●</span> ${escape(item.seriesName)} <b>${escape(value(item.value[1] ?? 0))}</b>` : `<b>${escape(value(item.value[1] ?? 0))}</b>`,\n );\n\n return `${escape(formatTime(first.value[0]))}<br/>${rows.join('<br/>')}`;\n },\n },\n series: series.map((item, index) => {\n const color = tone(index);\n const isPrimary = index === primary;\n\n return {\n type: 'line',\n name: item.label,\n data: seriesData(item.segments),\n connectNulls: false,\n symbol: 'circle',\n symbolSize: 0,\n lineStyle: { width: isPrimary ? 1.5 : 1, color, type: item.dashed ? 'dashed' : 'solid' },\n itemStyle: { color },\n areaStyle: isPrimary && !item.dashed ? { opacity: 0.14, color } : undefined,\n emphasis: { disabled: true },\n z: isPrimary ? 3 : 2,\n markLine:\n isPrimary && lines.length > 0\n ? {\n silent: false,\n symbol: 'none',\n data: lines,\n tooltip: {\n formatter: (entry: { data: { event?: ChartEvent; name: string } }) => {\n const event = entry.data.event;\n\n if (!event) {\n return escape(entry.data.name);\n }\n\n return `${escape(event.label)}<br/>${escape(formatTime(event.at))}${event.detail ? ` · ${escape(event.detail)}` : ''}`;\n },\n },\n }\n : undefined,\n };\n }),\n };\n },\n [series, from, to, formatValue, formatTime, unit, minimumTop, events, marker, zoom],\n );\n\n return <Chart option={option} label={label} height={height} className={className} />;\n}\n"]}
@@ -0,0 +1,55 @@
1
+ import clsx from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ // src/utils/cn.ts
5
+ function cn(...inputs) {
6
+ return twMerge(clsx(inputs));
7
+ }
8
+
9
+ // src/charts/scale.ts
10
+ function niceTicks(max, count = 4) {
11
+ const top = max > 0 && Number.isFinite(max) ? max : 1;
12
+ const raw = top / count;
13
+ const power = 10 ** Math.floor(Math.log10(raw));
14
+ const fraction = raw / power;
15
+ const step = (fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 2.5 ? 2.5 : fraction <= 5 ? 5 : 10) * power;
16
+ const ticks = [];
17
+ for (let value = 0; value < top + step * 0.999; value += step) {
18
+ ticks.push(Number(value.toPrecision(12)));
19
+ }
20
+ if (ticks.length < 2) {
21
+ ticks.push(step);
22
+ }
23
+ return ticks;
24
+ }
25
+ var MINUTE = 6e4;
26
+ var STEPS = [1, 2, 5, 10, 15, 30, 60, 120, 180, 360, 720, 1440, 2880, 10080].map((minutes) => minutes * MINUTE);
27
+ function timeTicks(from, to, width) {
28
+ const most = Math.max(2, Math.floor(width / 84));
29
+ const step = STEPS.find((candidate) => (to - from) / candidate <= most) ?? 7 * 1440 * MINUTE;
30
+ const ticks = [];
31
+ for (let tick = Math.ceil(from / step) * step; tick <= to; tick += step) {
32
+ ticks.push(tick);
33
+ }
34
+ return ticks;
35
+ }
36
+ function seriesData(segments) {
37
+ const data = [];
38
+ segments.forEach((segment, index) => {
39
+ const first = segment[0];
40
+ if (!first) {
41
+ return;
42
+ }
43
+ if (index > 0) {
44
+ data.push({ value: [first.t - 1, null] });
45
+ }
46
+ for (const point of segment) {
47
+ data.push(segment.length === 1 ? { value: [point.t, point.value], symbolSize: 5 } : { value: [point.t, point.value] });
48
+ }
49
+ });
50
+ return data;
51
+ }
52
+
53
+ export { cn, niceTicks, seriesData, timeTicks };
54
+ //# sourceMappingURL=chunk-OFTNLF7H.js.map
55
+ //# sourceMappingURL=chunk-OFTNLF7H.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/cn.ts","../src/charts/scale.ts"],"names":[],"mappings":"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","file":"chunk-OFTNLF7H.js","sourcesContent":["import clsx, { type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","/**\n * The arithmetic under the Cbox charts (first written for Cbox Telemetry): round value\n * ticks from zero, and time ticks on whole minutes, hours or days.\n */\n\n/** 0 and round steps up to at least `max`: 0, 250, 500, 750, 1000. */\nexport function niceTicks(max: number, count = 4): number[] {\n const top = max > 0 && Number.isFinite(max) ? max : 1;\n const raw = top / count;\n const power = 10 ** Math.floor(Math.log10(raw));\n const fraction = raw / power;\n const step = (fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 2.5 ? 2.5 : fraction <= 5 ? 5 : 10) * power;\n const ticks: number[] = [];\n\n for (let value = 0; value < top + step * 0.999; value += step) {\n ticks.push(Number(value.toPrecision(12)));\n }\n\n if (ticks.length < 2) {\n ticks.push(step);\n }\n\n return ticks;\n}\n\nconst MINUTE = 60_000;\nconst STEPS = [1, 2, 5, 10, 15, 30, 60, 120, 180, 360, 720, 1440, 2880, 10080].map((minutes) => minutes * MINUTE);\n\n/** Ticks (milliseconds) on whole minutes, hours or days, about one per 84 px of plot. */\nexport function timeTicks(from: number, to: number, width: number): number[] {\n const most = Math.max(2, Math.floor(width / 84));\n const step = STEPS.find((candidate) => (to - from) / candidate <= most) ?? 7 * 1440 * MINUTE;\n const ticks: number[] = [];\n\n for (let tick = Math.ceil(from / step) * step; tick <= to; tick += step) {\n ticks.push(tick);\n }\n\n return ticks;\n}\n\n/** A sample at `t` milliseconds since the epoch. */\nexport type TimePoint = { t: number; value: number };\n\nexport type SeriesDatum = { value: [number, number | null]; symbolSize?: number };\n\n/**\n * Runs of samples as ECharts line data: a null between runs keeps missing samples a gap\n * (never interpolated or drawn as zero), and an isolated sample becomes a visible dot.\n */\nexport function seriesData(segments: TimePoint[][]): SeriesDatum[] {\n const data: SeriesDatum[] = [];\n\n segments.forEach((segment, index) => {\n const first = segment[0];\n\n if (!first) {\n return;\n }\n\n if (index > 0) {\n data.push({ value: [first.t - 1, null] });\n }\n\n for (const point of segment) {\n data.push(segment.length === 1 ? { value: [point.t, point.value], symbolSize: 5 } : { value: [point.t, point.value] });\n }\n });\n\n return data;\n}\n"]}
@@ -0,0 +1,10 @@
1
+ import { LineChart } from 'echarts/charts';
2
+ import { DataZoomComponent, GridComponent, TooltipComponent, MarkLineComponent, AriaComponent } from 'echarts/components';
3
+ import * as echarts from 'echarts/core';
4
+ export { echarts };
5
+ import { SVGRenderer } from 'echarts/renderers';
6
+
7
+ // src/charts/engine.ts
8
+ echarts.use([LineChart, DataZoomComponent, GridComponent, TooltipComponent, MarkLineComponent, AriaComponent, SVGRenderer]);
9
+ //# sourceMappingURL=engine-4DRZ3POZ.js.map
10
+ //# sourceMappingURL=engine-4DRZ3POZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/charts/engine.ts"],"names":[],"mappings":";;;;;;;AAMQ,OAAA,CAAA,GAAA,CAAI,CAAC,WAAW,iBAAA,EAAmB,aAAA,EAAe,kBAAkB,iBAAA,EAAmB,aAAA,EAAe,WAAW,CAAC,CAAA","file":"engine-4DRZ3POZ.js","sourcesContent":["import { LineChart } from 'echarts/charts';\nimport { AriaComponent, DataZoomComponent, GridComponent, MarkLineComponent, TooltipComponent } from 'echarts/components';\nimport * as echarts from 'echarts/core';\nimport { SVGRenderer } from 'echarts/renderers';\n\n/** Only what the Cbox time chart uses; loaded on first mount so pages without charts never pay for it. */\necharts.use([LineChart, DataZoomComponent, GridComponent, TooltipComponent, MarkLineComponent, AriaComponent, SVGRenderer]);\n\nexport { echarts };\n"]}