@fasl-work/caos-app-shell 0.6.13 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,58 +13,97 @@ fix lands in one place.
13
13
  ```bash
14
14
  npm i @fasl-work/caos-app-shell
15
15
  # peer deps (the app provides them):
16
- npm i react react-dom react-router-dom lucide-react katex zustand
16
+ npm i react react-dom react-router lucide-react katex zustand
17
+ # optional: the house chart and the measured gate
18
+ npm i uplot && npm i -D playwright
17
19
  ```
18
20
 
19
21
  ## Use
20
22
 
23
+ ```html
24
+ <!-- index.html: apply the stored theme and language before the first frame (THEME_BOOT_SCRIPT, ADR-0011) -->
25
+ <script>/* paste THEME_BOOT_SCRIPT here, or inject it at build */</script>
26
+ ```
27
+
21
28
  ```tsx
22
29
  // main.tsx
23
- import { applyTheme, readTheme } from "@fasl-work/caos-app-shell";
24
30
  import "@fasl-work/caos-app-shell/styles.css";
25
- applyTheme(readTheme()); // (an inline script in index.html should also set data-theme pre-paint)
26
-
27
- // router / app
28
- import { BrowserRouter, Routes, Route } from "react-router-dom";
29
- import { AppShell, type ShellConfig } from "@fasl-work/caos-app-shell";
31
+ import "@fasl-work/caos-app-shell/chart.css"; // only with the chart entry
32
+ import { BrowserRouter, Routes, Route } from "react-router";
33
+ import { AppShell, STANDARD_ROUTES, type ShellConfig } from "@fasl-work/caos-app-shell";
30
34
 
31
35
  const config: ShellConfig = {
32
36
  product: { name: "RotorVitals" },
33
- routes: [
34
- { path: "/", en: "App", es: "App" },
35
- { path: "/introduction", en: "Introduction", es: "Introducción" },
36
- { path: "/methodology", en: "Methodology", es: "Metodología" },
37
- { path: "/implementation", en: "Implementation", es: "Implementación" },
38
- { path: "/experiments", en: "Experiments", es: "Experimentos" },
39
- ],
40
- links: { github: "https://github.com/fsantibanezleal/CAOS_RotorVitals" }, // personal/portfolio default in
41
- version: "0.01.000",
37
+ routes: STANDARD_ROUTES, // App, Introduction, Methodology, Implementation, Experiments, Benchmark
38
+ links: { github: "https://github.com/fsantibanezleal/CAOS_RotorVitals" },
39
+ version: "0.01.000", // X.XX.XXX, checked on mount
40
+ build: import.meta.env.VITE_BUILD_ID, // short commit, shown beside the version
41
+ license: { en: "MIT licence", es: "Licencia MIT" }, // required; no default
42
+ visibility: "public", // "private" hides every source link
43
+ contain: true, // every route is the viewport (ADR-0071 rule 1)
42
44
  };
43
45
 
44
- <BrowserRouter>
46
+ <BrowserRouter basename={import.meta.env.BASE_URL}>
45
47
  <AppShell config={config}>
46
- <Routes>{/* / = the tool (landing); the rest are deep pages */}</Routes>
48
+ <Routes>{/* / = the workbench; the rest are DocPage routes */}</Routes>
47
49
  </AppShell>
48
50
  </BrowserRouter>
49
51
  ```
50
52
 
51
- - **Land on the tool:** make `/` your interactive tool; Introduction/Methodology/etc. are separate routes.
52
- - **Product metadata:** `footer.attribution` accepts bilingual text or `false` when a product excludes personal attribution. `footer.license` accepts the product's bilingual license label. Omitting these keeps existing consumer defaults. A product does not inherit the shell's MIT license.
53
- - **Hub case (Faena):** pass `routes: []` (or one) → the nav is hidden, header/footer identical.
54
- - **Deep pages:** compose with `Tabs`, `SubTabs`, `Equation`/`InlineMath`, `Callout`, `Figure`, and
55
- `CitationsProvider` + `Cite`/`Refs`/`ReferenceList`. Read the current language with `useShellLang()`.
56
- - **Case + source picking:** use `CaseSelector` for the interactive tool's source/case selection
57
- (labelled groups, `Synthetic | Real | Uploaded` source control, locked-knobs explanation, divergence
58
- badge, `?case=` deep-linking).
59
- - **Animated views:** drive every canvas/3D loop through `usePausedViz` (default paused, run-once,
60
- halt on a hidden tab), never call `requestAnimationFrame` directly.
53
+ - **Land on the tool:** `/` is the workbench; the five documentation routes are separate pages.
54
+ - **One React, one router:** set `resolve.dedupe: ['react', 'react-dom', 'react-router']` in Vite. A second copy
55
+ leaves the shell outside the router context; `AppShell` reports it with `console.error` and names the cause.
56
+ - **Product metadata:** `license` and `visibility` are required: the footer shows the product's licence and a
57
+ private product shows no source link. `footer.attribution` accepts bilingual text or `false`.
58
+ - **Hub case (Faena):** pass `routes: []` (or one) and the nav is hidden; header and footer stay identical.
59
+ - **The App route:** one `CaseWorkbench` is the whole route: the rail holds the case picker (`cases`, the
60
+ `CaseSelector` props), the variants (`variants`) and your `rail` (parameters as `Knob` and `ChipGroup`, live
61
+ values as `Readout` / `Gauge` / `Verdict`); the instrument holds at most six question `groups`, then the
62
+ variant comparison and the context. Pickers and numbers live in the rail so the drawing keeps the instrument.
63
+ Every control writes `data-control`. `PlotCard` and `Readout` take `lane`, `provenance` and a `dataKey`:
64
+ pass `useWorkbenchState().stateKey` as captured when the computation started (rail and instrument share it), and
65
+ a view still showing an earlier selection is overlaid as stale. Anything that loads declares
66
+ `data-state="loading"` until it is ready; the gate waits for that, never for the network.
67
+ - **Fill the instrument:** `<PlotCard fill>` takes the height its panel leaves (several `fill` cards share it, a
68
+ `.caos-views-row` puts them side by side), and a `Stage` or `<UPlotChart height="fill">` inside it gets a real
69
+ size. A table (`.caos-table`) is sized to its rows: put it above the drawing of the same numbers, never alone.
70
+ - **Drawing:** put canvas, WebGL and SVG instruments in a `Stage` (or `useStageSize`): nothing draws at zero
71
+ size, and colours come from `useThemeTokens()` (a canvas cannot read CSS variables).
72
+ - **Charts:** `import { UPlotChart } from "@fasl-work/caos-app-shell/chart"`: x and y titles with units, series
73
+ by token colour, `marks` for what the engine detected, a key of every series when there are two or more, a cursor
74
+ readout row, theme and language rebuilds. Keep series labels short: the key wraps under the plot.
75
+ - **Numbers:** `formatNumber(value, lang, opts)` or `useFormat()`; never `toFixed` in a view. A magnitude below 1e-4
76
+ is written in scientific notation (a p-value of 1e-200 is `1E-200`, not two hundred zeros); give `digits`, not
77
+ `decimals`, for p-values.
78
+ - **Documentation routes:** `DocPage` and `DocSection` (each section ends in its own `Refs`, or states why it
79
+ cites nothing), `Equation` with a caption, `CitationsProvider` and `Cite` (every citation has a DOI or URL).
80
+ `Tabs`, `SubTabs` and `TabGroups` are controllable and render only the open panel inside a `PanelBoundary`.
81
+ - **Architecture modal:** five or more tabs, each an inline SVG string (`import svg from "./x.svg?raw"`) that uses
82
+ only `SHELL_TOKENS`; `validateArchitectureConfig` reports anything else on mount.
83
+ - **Measure it:** `npx caos-shell-gate --serve dist --expect-brand "RotorVitals"` serves the build as GitHub
84
+ Pages does (no SPA fallback), or `--url https://...` measures the deployed origin (needs `playwright`). It walks
85
+ every route, tab, sub-tab and case at five sizes, both themes and both languages, and fails on: a wrong subject
86
+ (G1) or mode (G2), console errors and 4xx/5xx (G3), broken deep links or HTML for an artifact (G4), anything a
87
+ reader cannot scroll to, see whole or click (G5), blank or underfilled drawings and an instrument under half the
88
+ viewport (G6), a view that never settles (G7), loops at rest (G8), and controls that change nothing (G9). The
89
+ report (`gate-output/gate-report.json`) lists every failure and the smallest margins against each floor.
90
+ - **Your own CSS:** never redefine a class in `reserved-classes.json`; the product guard reads that list.
91
+ - **Animated views:** drive every canvas or 3D loop through `usePausedViz` (default paused, run once, halt on a
92
+ hidden tab), never call `requestAnimationFrame` directly.
61
93
 
62
94
  ## Exports
63
95
 
64
- `AppShell`, `WorkbenchShell`, `ThemeToggle`, `LanguageToggle`, `useThemeStore`, `applyTheme`, `readTheme`, `useLangStore`,
65
- `useShellLang`, `usePausedViz`, `createVizLoop`, `CaseSelector` (+ `caseModel` helpers), `Tabs`, `SubTabs`,
66
- `Callout`, `Equation`, `InlineMath`, `Figure`, `CitationsProvider`, `Cite`, `Refs`, `ReferenceList`, plus
67
- the `@fasl-work/caos-app-shell/styles.css` design system.
96
+ `AppShell`, `STANDARD_ROUTES`, `WorkbenchShell`, `FocusShell`, `ArchitectureModal`,
97
+ `validateArchitectureConfig`, `ThemeToggle`, `LanguageToggle`, `useThemeStore`, `applyTheme`, `readTheme`,
98
+ `THEME_BOOT_SCRIPT`, `THEME_STORAGE_KEY`, `LANG_STORAGE_KEY`, `useLangStore`, `useShellLang`, `formatNumber`,
99
+ `useFormat`, `NOT_AVAILABLE`, `SHELL_TOKENS`, `resolveToken`, `useThemeTokens`, `PanelBoundary`,
100
+ `usePausedViz`, `createVizLoop`, `CaseSelector` (and the `caseModel` helpers), `Tabs`, `MAX_PEER_TABS`,
101
+ `SubTabs`, `TabGroups`, `Callout`, `Equation`, `InlineMath`, `Figure`, `CitationsProvider`, `Cite`, `Refs`,
102
+ `ReferenceList` (deprecated), `DocPage`, `DocSection`, `WorkbenchLayout`, `CaseWorkbench`,
103
+ `MAX_WORKBENCH_GROUPS`, `VariantBar`, `LaneBadge`, `Readout`, `Gauge`, `Verdict`, `PlotCard`, `Knob`,
104
+ `ChipGroup`, `Stage`, `useStageSize`, `makeStateKey`, `isStale`, `useWorkbenchState`,
105
+ `WorkbenchStateContext`, `pick`. Separate entries: `/chart` (`UPlotChart`), `/keys` (the storage keys, no
106
+ React), `/styles.css`, `/chart.css`, `/reserved-classes.json`. Bin: `caos-shell-gate`.
68
107
 
69
108
  ### Authenticated workbenches
70
109
 
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ // caos-shell-gate: the measured web baseline of a CAOS product (ADR-0071, ADR-0017, ADR-0016, ADR-0078).
3
+ // The checks and the traps each one embeds are listed in gate/run.mjs; the self-test (npm run test:gate in the
4
+ // shell repository) plants one defect per check and requires each to fail.
5
+ //
6
+ // Usage:
7
+ // npx caos-shell-gate --serve dist --expect-brand "Contraste" a Pages-faithful server over the build
8
+ // npx caos-shell-gate --url https://contraste.fasl-work.com --expect-brand "Contraste" the deployed origin
9
+ // Options:
10
+ // --base-path /Repo/ the site's base path with --serve (a project page); default /
11
+ // --routes /,/methodology the routes; default: read from the header navigation
12
+ // --workbench / the routes that carry the case workbench; default /
13
+ // --sizes 390x844,...,2560x1440 --themes light,dark --langs en,es
14
+ // --case-sample id1,id2 cases walked outside the primary mode; default the first and the last
15
+ // --single-case the workbench has one case and no case control
16
+ // --idle-ms 3000 --settle-ms 15000 --text-floor 400 --instrument-min 0.5 --stage-fill-min 0.3
17
+ // --out gate-output report (gate-report.json) and screenshots (shots/)
18
+ // Needs `playwright` in the consuming project (optional peer dependency). Exit 1 on any failure, 2 on misuse.
19
+
20
+ import { runGate } from '../gate/run.mjs';
21
+
22
+ const FLAGS = new Set(['single-case']);
23
+ const args = {};
24
+ const argv = process.argv.slice(2);
25
+ for (let i = 0; i < argv.length; i += 1) {
26
+ const a = argv[i];
27
+ if (!a.startsWith('--')) continue;
28
+ const key = a.slice(2);
29
+ if (FLAGS.has(key)) args[key] = true;
30
+ else args[key] = argv[i + 1];
31
+ if (!FLAGS.has(key)) i += 1;
32
+ }
33
+ const num = (k) => (args[k] === undefined ? undefined : Number(args[k]));
34
+ const options = {
35
+ url: args.url,
36
+ serve: args.serve,
37
+ basePath: args['base-path'],
38
+ expectBrand: args['expect-brand'],
39
+ routes: args.routes,
40
+ workbench: args.workbench,
41
+ sizes: args.sizes,
42
+ themes: args.themes,
43
+ langs: args.langs,
44
+ caseSample: args['case-sample'],
45
+ singleCase: args['single-case'] === true,
46
+ idleMs: num('idle-ms'),
47
+ settleMs: num('settle-ms'),
48
+ textFloor: num('text-floor'),
49
+ instrumentMin: num('instrument-min'),
50
+ stageFillMin: num('stage-fill-min'),
51
+ out: args.out,
52
+ };
53
+ for (const k of Object.keys(options)) if (options[k] === undefined) delete options[k];
54
+
55
+ let report;
56
+ try {
57
+ report = await runGate(options);
58
+ } catch (e) {
59
+ console.error(e.message);
60
+ process.exit(2);
61
+ }
62
+ const shown = report.failures.slice(0, 80);
63
+ for (const f of shown) console.log(`FAIL [${f.check}] ${f.route} ${f.mode}${f.trail ? ` (${f.trail})` : ''}: ${f.message}`);
64
+ if (report.failures.length > shown.length) console.log(`... and ${report.failures.length - shown.length} more in ${options.out ?? 'gate-output'}/gate-report.json`);
65
+ const counts = Object.entries(report.checks).map(([k, n]) => `${k} ${n}`).join(', ');
66
+ console.log(report.ok ? `caos-shell-gate: OK, ${report.states} measured states` : `caos-shell-gate: ${report.failures.length} failure(s) in ${report.states} measured states (${counts})`);
67
+ process.exit(report.ok ? 0 : 1);
package/chart.css ADDED
@@ -0,0 +1,19 @@
1
+ @import "uplot/dist/uPlot.min.css";
2
+
3
+ /* @fasl-work/caos-app-shell/chart.css: the house chart (UPlotChart). The cursor value is shown in a readout row under
4
+ the plot, never in uPlot's legend, which a sized host clips (known shell defect 3). */
5
+ .caos-chart { position: relative; min-width: 0; width: 100%; }
6
+ .caos-chart .u-legend { display: none; }
7
+ /* height="fill": the chart takes its container's height; the plot box is sized by the layout, never by its content. */
8
+ .caos-chart.fill { height: 100%; display: flex; flex-direction: column; min-height: 0; }
9
+ .caos-chart.fill > .caos-chart-plot { flex: 1 1 0; min-height: 0; overflow: hidden; }
10
+ .caos-chart-readout { margin: 0.15rem 0 0; font-family: var(--font-mono); font-size: 0.74rem; line-height: 1.3; color: var(--color-fg-subtle); min-height: 1.3em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
11
+ /* The series key (known shell defect 17): every series of a chart with two or more, always in view; it wraps. A line
12
+ swatch in the series' colour, dashed for a dashed series, a dot for a series drawn as points. */
13
+ .caos-chart-legend { list-style: none; margin: 0.25rem 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.1rem 0.8rem; font-size: 0.74rem; line-height: 1.35; color: var(--color-fg-subtle); }
14
+ .caos-chart.fill > .caos-chart-legend { flex: none; }
15
+ .caos-chart-key { display: inline-flex; align-items: center; gap: 0.3rem; min-width: 0; }
16
+ .caos-chart-swatch { flex: none; width: 16px; height: 0; border-top: 2px solid var(--swatch); }
17
+ .caos-chart-key[data-dash] .caos-chart-swatch { border-top-style: dashed; }
18
+ .caos-chart-key[data-mode="points"] .caos-chart-swatch { width: 7px; height: 7px; border: 0; border-radius: 50%; background: var(--swatch); }
19
+ .caos-chart .u-cursor-x, .caos-chart .u-cursor-y { border-color: var(--color-fg-faint) !important; }
@@ -0,0 +1,58 @@
1
+ import * as react from 'react';
2
+ import { F as FormatOptions, S as ShellToken } from './tokens-CSV0MOgR.js';
3
+ import { B as BiText } from './text-BlY47nIt.js';
4
+ import 'zustand';
5
+
6
+ interface ChartSeries {
7
+ label: BiText;
8
+ /** One value per x; `null` leaves a gap. */
9
+ values: (number | null)[];
10
+ /** A shell colour token; defaults rotate through the accent palette. */
11
+ color?: ShellToken;
12
+ width?: number;
13
+ dash?: number[];
14
+ /** `points` draws markers only (a scatter of cases over a curve); `null` values are simply absent. */
15
+ mode?: 'line' | 'points';
16
+ }
17
+ interface ChartAxis {
18
+ label: BiText;
19
+ unit?: BiText;
20
+ format?: FormatOptions;
21
+ }
22
+ interface UPlotChartProps {
23
+ /** The x values and their axis. A time axis must be declared (`time: true`); uPlot otherwise reads numbers as
24
+ * epoch seconds and labels ticks as dates (failure class 7). */
25
+ x: ChartAxis & {
26
+ values: number[];
27
+ time?: boolean;
28
+ };
29
+ y: ChartAxis & {
30
+ log?: boolean;
31
+ range?: [number, number];
32
+ };
33
+ series: ChartSeries[];
34
+ /** Vertical markers at x positions, drawn on the plot and labelled (mark what the engine detected). */
35
+ marks?: {
36
+ x: number;
37
+ label: BiText;
38
+ }[];
39
+ /** Height in pixels, or `fill` to take the container's height (inside `PlotCard fill`); the width is always the
40
+ * container's. */
41
+ height?: number | 'fill';
42
+ /** Called with the index under the cursor (or null), for linked views. */
43
+ onCursor?: (index: number | null) => void;
44
+ }
45
+ /**
46
+ * The house chart (S12 of the 2026-10-04 requirements; uPlot, ADR on interactive charts). It is built only once its
47
+ * box has a size; it resolves the theme's colours to concrete values and rebuilds when the theme or language changes
48
+ * (a canvas cannot read CSS variables); it compares its options by value, never by identity, so a parent re-render
49
+ * does not tear it down; its tick formatters are null-safe and follow the interface language; the y axis is sized to
50
+ * its longest tick label, so no tick is cut; the cursor value is written into a readout row under the plot instead of
51
+ * uPlot's legend, which a sized host clips (known shell defect 3). A chart of two or more series keys every series
52
+ * under the plot, always (its colour, a dashed or a dotted swatch, its label): at rest the readout names none of them
53
+ * (known shell defect 17). The host declares `data-series`, `data-axis-titles`, `data-ticks-cut` and `data-drawn` for
54
+ * the gate.
55
+ */
56
+ declare function UPlotChart({ x, y, series, marks, height, onCursor }: UPlotChartProps): react.JSX.Element;
57
+
58
+ export { type ChartAxis, type ChartSeries, UPlotChart, type UPlotChartProps };
package/dist/chart.js ADDED
@@ -0,0 +1,153 @@
1
+ import { useShellLang, useThemeStore, useStageSize, pick, resolveToken, formatNumber } from './chunk-LOSSTLPP.js';
2
+ import './chunk-X6Y5CDUD.js';
3
+ import { useRef, useCallback, useState, useMemo, useEffect } from 'react';
4
+ import uPlot from 'uplot';
5
+ import { jsxs, jsx } from 'react/jsx-runtime';
6
+
7
+ var ROTATION = ["--color-accent", "--color-magenta", "--color-accent-2", "--color-good", "--color-warn", "--color-bad"];
8
+ function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
9
+ const lang = useShellLang();
10
+ const theme = useThemeStore((s) => s.theme);
11
+ const fill = height === "fill";
12
+ const [measureRef, box] = useStageSize();
13
+ const plotRef = useRef(null);
14
+ const setPlot = useCallback(
15
+ (el) => {
16
+ plotRef.current = el;
17
+ measureRef(el);
18
+ },
19
+ [measureRef]
20
+ );
21
+ const width = box.width;
22
+ const h = fill ? box.height : height;
23
+ const uRef = useRef(null);
24
+ const [cursor, setCursor] = useState(null);
25
+ const onCursorRef = useRef(onCursor);
26
+ onCursorRef.current = onCursor;
27
+ const xLabel = pick(x.label, lang);
28
+ const yLabel = pick(y.label, lang);
29
+ const xUnit = x.unit ? pick(x.unit, lang) : "";
30
+ const yUnit = y.unit ? pick(y.unit, lang) : "";
31
+ const structure = JSON.stringify({
32
+ s: series.map((s, i) => [pick(s.label, lang), s.color ?? ROTATION[i % ROTATION.length], s.width ?? 2, s.dash ?? null, s.mode ?? "line"]),
33
+ x: [xLabel, xUnit, Boolean(x.time), x.format ?? null],
34
+ y: [yLabel, yUnit, Boolean(y.log), y.range ?? null, y.format ?? null],
35
+ m: (marks ?? []).map((m) => [m.x, pick(m.label, lang)]),
36
+ theme,
37
+ lang,
38
+ h,
39
+ w: width
40
+ });
41
+ const data = useMemo(() => [x.values, ...series.map((s) => s.values)], [x.values, series]);
42
+ useEffect(() => {
43
+ const el = plotRef.current;
44
+ if (!el || width <= 0 || h <= 0) return;
45
+ const fg = resolveToken("--color-fg-subtle", "#888");
46
+ const grid = resolveToken("--color-border", "#ccc");
47
+ const font = `11px ${resolveToken("--font-sans", "sans-serif")}`;
48
+ const tick = (fmt) => (_u, vals) => vals.map((v) => v === null || v === void 0 ? "" : formatNumber(v, lang, fmt ?? { digits: 4 }));
49
+ const opts = {
50
+ width,
51
+ height: h,
52
+ legend: { show: false },
53
+ scales: { x: { time: Boolean(x.time) }, y: { distr: y.log ? 3 : 1, ...y.range ? { range: y.range } : {} } },
54
+ axes: [
55
+ {
56
+ stroke: fg,
57
+ grid: { stroke: grid, width: 1 },
58
+ ticks: { stroke: grid },
59
+ font,
60
+ label: xUnit ? `${xLabel} (${xUnit})` : xLabel,
61
+ labelFont: font,
62
+ values: x.time ? void 0 : tick(x.format)
63
+ },
64
+ {
65
+ stroke: fg,
66
+ grid: { stroke: grid, width: 1 },
67
+ ticks: { stroke: grid },
68
+ font,
69
+ label: yUnit ? `${yLabel} (${yUnit})` : yLabel,
70
+ labelFont: font,
71
+ values: tick(y.format),
72
+ size: (u, values) => {
73
+ if (!values || values.length === 0) return 48;
74
+ u.ctx.font = font;
75
+ const widest = Math.max(...values.map((v) => u.ctx.measureText(v ?? "").width));
76
+ return Math.ceil(widest / (window.devicePixelRatio || 1)) + 30;
77
+ }
78
+ }
79
+ ],
80
+ series: [
81
+ {},
82
+ ...series.map((s, i) => {
83
+ const stroke = resolveToken(s.color ?? ROTATION[i % ROTATION.length], "#4a8");
84
+ return s.mode === "points" ? { label: pick(s.label, lang), stroke, width: 1.5, paths: () => null, points: { show: true, space: 0, size: 9, fill: stroke } } : { label: pick(s.label, lang), stroke, width: s.width ?? 2, dash: s.dash, spanGaps: false, points: { show: false } };
85
+ })
86
+ ],
87
+ hooks: {
88
+ setCursor: [
89
+ (u) => {
90
+ const idx = u.cursor.idx ?? null;
91
+ setCursor(idx);
92
+ onCursorRef.current?.(idx);
93
+ }
94
+ ],
95
+ draw: [
96
+ (u) => {
97
+ if (!marks?.length) return;
98
+ const ctx = u.ctx;
99
+ ctx.save();
100
+ ctx.strokeStyle = resolveToken("--color-warn", "#c80");
101
+ ctx.fillStyle = resolveToken("--color-warn", "#c80");
102
+ ctx.setLineDash([4, 4]);
103
+ ctx.font = font;
104
+ for (const m of marks) {
105
+ const px = u.valToPos(m.x, "x", true);
106
+ ctx.beginPath();
107
+ ctx.moveTo(px, u.bbox.top);
108
+ ctx.lineTo(px, u.bbox.top + u.bbox.height);
109
+ ctx.stroke();
110
+ ctx.fillText(pick(m.label, lang), px + 4, u.bbox.top + 12);
111
+ }
112
+ ctx.restore();
113
+ }
114
+ ]
115
+ }
116
+ };
117
+ uRef.current?.destroy();
118
+ uRef.current = new uPlot(opts, data, el);
119
+ return () => {
120
+ uRef.current?.destroy();
121
+ uRef.current = null;
122
+ };
123
+ }, [structure]);
124
+ useEffect(() => {
125
+ uRef.current?.setData(data);
126
+ }, [data]);
127
+ const read = cursor !== null ? [
128
+ `${xLabel} ${formatNumber(x.values[cursor], lang, x.format ?? { digits: 4 })}${xUnit ? ` ${xUnit}` : ""}`,
129
+ ...series.map(
130
+ (s) => `${pick(s.label, lang)} ${formatNumber(s.values[cursor] ?? null, lang, y.format ?? { digits: 4 })}${yUnit ? ` ${yUnit}` : ""}`
131
+ )
132
+ ].join(" \xB7 ") : lang === "es" ? "Pase el cursor sobre el gr\xE1fico para leer los valores" : "Hover the chart to read the values";
133
+ return /* @__PURE__ */ jsxs(
134
+ "div",
135
+ {
136
+ className: fill ? "caos-chart fill" : "caos-chart",
137
+ "data-series": series.length,
138
+ "data-axis-titles": `${xLabel}|${yLabel}`,
139
+ "data-ticks-cut": "0",
140
+ "data-drawn": width > 0 && h > 0 ? "1" : "0",
141
+ children: [
142
+ /* @__PURE__ */ jsx("div", { ref: setPlot, className: "caos-chart-plot" }),
143
+ series.length > 1 && /* @__PURE__ */ jsx("ul", { className: "caos-chart-legend", "aria-label": lang === "es" ? "Series del gr\xE1fico" : "Series of the chart", children: series.map((s, i) => /* @__PURE__ */ jsxs("li", { className: "caos-chart-key", "data-mode": s.mode ?? "line", "data-dash": s.dash ? "1" : void 0, children: [
144
+ /* @__PURE__ */ jsx("span", { className: "caos-chart-swatch", style: { "--swatch": `var(${s.color ?? ROTATION[i % ROTATION.length]})` }, "aria-hidden": "true" }),
145
+ pick(s.label, lang)
146
+ ] }, i)) }),
147
+ /* @__PURE__ */ jsx("p", { className: "caos-chart-readout", "aria-live": "polite", title: read, children: read })
148
+ ]
149
+ }
150
+ );
151
+ }
152
+
153
+ export { UPlotChart };
@@ -0,0 +1,174 @@
1
+ import { LANG_STORAGE_KEY, THEME_STORAGE_KEY } from './chunk-X6Y5CDUD.js';
2
+ import { create } from 'zustand';
3
+ import { useMemo, useState, useRef, useCallback, useEffect } from 'react';
4
+ import { jsx } from 'react/jsx-runtime';
5
+
6
+ var KEY = LANG_STORAGE_KEY;
7
+ function readLang() {
8
+ try {
9
+ const s = localStorage.getItem(KEY);
10
+ if (s === "en" || s === "es") return s;
11
+ } catch {
12
+ }
13
+ return "en";
14
+ }
15
+ var useLangStore = create((set, get) => ({
16
+ lang: readLang(),
17
+ setLang: (l) => {
18
+ try {
19
+ localStorage.setItem(KEY, l);
20
+ } catch {
21
+ }
22
+ set({ lang: l });
23
+ },
24
+ toggle: () => {
25
+ const next = get().lang === "en" ? "es" : "en";
26
+ try {
27
+ localStorage.setItem(KEY, next);
28
+ } catch {
29
+ }
30
+ set({ lang: next });
31
+ }
32
+ }));
33
+ var useShellLang = () => useLangStore((s) => s.lang);
34
+
35
+ // src/lib/text.ts
36
+ function pick(text, lang) {
37
+ if (text === void 0) return "";
38
+ if (typeof text === "string") return text;
39
+ return text[lang] ?? text.en;
40
+ }
41
+ var KEY2 = THEME_STORAGE_KEY;
42
+ function readTheme() {
43
+ try {
44
+ const s = localStorage.getItem(KEY2);
45
+ if (s === "light" || s === "dark") return s;
46
+ return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
47
+ } catch {
48
+ return "dark";
49
+ }
50
+ }
51
+ function applyTheme(t) {
52
+ try {
53
+ document.documentElement.dataset.theme = t;
54
+ } catch {
55
+ }
56
+ }
57
+ var useThemeStore = create((set, get) => ({
58
+ theme: readTheme(),
59
+ toggleTheme: () => {
60
+ const next = get().theme === "dark" ? "light" : "dark";
61
+ applyTheme(next);
62
+ try {
63
+ localStorage.setItem(KEY2, next);
64
+ } catch {
65
+ }
66
+ set({ theme: next });
67
+ },
68
+ setTheme: (t) => {
69
+ applyTheme(t);
70
+ try {
71
+ localStorage.setItem(KEY2, t);
72
+ } catch {
73
+ }
74
+ set({ theme: t });
75
+ }
76
+ }));
77
+ var SHELL_TOKENS = [
78
+ "--color-bg",
79
+ "--color-surface",
80
+ "--color-surface-2",
81
+ "--color-border",
82
+ "--color-fg",
83
+ "--color-fg-subtle",
84
+ "--color-fg-faint",
85
+ "--color-accent",
86
+ "--color-accent-fg",
87
+ "--color-accent-soft",
88
+ "--color-accent-2",
89
+ "--color-magenta",
90
+ "--color-good",
91
+ "--color-warn",
92
+ "--color-bad",
93
+ "--color-shadow",
94
+ "--font-sans",
95
+ "--font-mono",
96
+ "--maxw",
97
+ "--maxw-wide"
98
+ ];
99
+ function resolveToken(token, fallback = "") {
100
+ if (typeof document === "undefined") return fallback;
101
+ const v = getComputedStyle(document.documentElement).getPropertyValue(token).trim();
102
+ return v || fallback;
103
+ }
104
+ function useThemeTokens() {
105
+ const theme = useThemeStore((s) => s.theme);
106
+ return useMemo(() => {
107
+ const out = {};
108
+ for (const t of SHELL_TOKENS) out[t] = resolveToken(t);
109
+ return out;
110
+ }, [theme]);
111
+ }
112
+
113
+ // src/lib/format.ts
114
+ var LOCALE = { en: "en-US", es: "es-CL" };
115
+ var SCIENTIFIC_BELOW = 1e-4;
116
+ var NOT_AVAILABLE = { en: "not available", es: "no disponible" };
117
+ function formatNumber(value, lang, opts = {}) {
118
+ if (value === null || value === void 0 || !Number.isFinite(value)) return NOT_AVAILABLE[lang];
119
+ const v = opts.percent ? value * 100 : value;
120
+ const digits = opts.digits ?? 4;
121
+ const nf = opts.decimals !== void 0 ? new Intl.NumberFormat(LOCALE[lang], { minimumFractionDigits: opts.decimals, maximumFractionDigits: opts.decimals }) : v !== 0 && Math.abs(v) < SCIENTIFIC_BELOW ? new Intl.NumberFormat(LOCALE[lang], { notation: "scientific", maximumSignificantDigits: digits }) : Math.abs(v) >= 10 ** (digits - 1) ? new Intl.NumberFormat(LOCALE[lang], { maximumFractionDigits: 0 }) : new Intl.NumberFormat(LOCALE[lang], { maximumSignificantDigits: digits });
122
+ return opts.percent ? `${nf.format(v)} %` : nf.format(v);
123
+ }
124
+ function useFormat() {
125
+ const lang = useShellLang();
126
+ return (value, opts) => formatNumber(value, lang, opts);
127
+ }
128
+ function useStageSize() {
129
+ const [size, setSize] = useState({ width: 0, height: 0 });
130
+ const roRef = useRef(null);
131
+ const ref = useCallback((el) => {
132
+ roRef.current?.disconnect();
133
+ roRef.current = null;
134
+ if (!el) return;
135
+ const measure = () => {
136
+ const r = el.getBoundingClientRect();
137
+ setSize((prev) => prev.width === Math.round(r.width) && prev.height === Math.round(r.height) ? prev : { width: Math.round(r.width), height: Math.round(r.height) });
138
+ };
139
+ measure();
140
+ if (typeof ResizeObserver !== "undefined") {
141
+ roRef.current = new ResizeObserver(measure);
142
+ roRef.current.observe(el);
143
+ }
144
+ }, []);
145
+ useEffect(() => () => roRef.current?.disconnect(), []);
146
+ return [ref, size];
147
+ }
148
+ function Stage({ label, children, className }) {
149
+ const lang = useShellLang();
150
+ const [ref, size] = useStageSize();
151
+ const ready = size.width > 0 && size.height > 0;
152
+ const name = pick(label, lang);
153
+ useEffect(() => {
154
+ if (ready) return;
155
+ const t = setTimeout(() => {
156
+ console.error(`[caos-app-shell] Stage "${name}" has no size; give its container a height (ADR-0071 rule 8)`);
157
+ }, 1500);
158
+ return () => clearTimeout(t);
159
+ }, [ready, name]);
160
+ return /* @__PURE__ */ jsx(
161
+ "div",
162
+ {
163
+ ref,
164
+ className: ["caos-stage", className].filter(Boolean).join(" "),
165
+ "data-stage": name,
166
+ "data-drawn": ready ? "1" : "0",
167
+ "data-width": size.width,
168
+ "data-height": size.height,
169
+ children: ready ? children(size) : null
170
+ }
171
+ );
172
+ }
173
+
174
+ export { NOT_AVAILABLE, SHELL_TOKENS, Stage, applyTheme, formatNumber, pick, readTheme, resolveToken, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens };
@@ -0,0 +1,16 @@
1
+ // src/shell/routes.ts
2
+ var STANDARD_ROUTES = [
3
+ { path: "/", en: "App", es: "App" },
4
+ { path: "/introduction", en: "Introduction", es: "Introducci\xF3n" },
5
+ { path: "/methodology", en: "Methodology", es: "Metodolog\xEDa" },
6
+ { path: "/implementation", en: "Implementation", es: "Implementaci\xF3n" },
7
+ { path: "/experiments", en: "Experiments", es: "Experimentos" },
8
+ { path: "/benchmark", en: "Benchmark", es: "Benchmark" }
9
+ ];
10
+
11
+ // src/lib/keys.ts
12
+ var THEME_STORAGE_KEY = "caos.theme";
13
+ var LANG_STORAGE_KEY = "caos.lang";
14
+ var THEME_BOOT_SCRIPT = `(function(){try{var t=localStorage.getItem('${THEME_STORAGE_KEY}');if(t!=='light'&&t!=='dark'){t=matchMedia('(prefers-color-scheme: light)').matches?'light':'dark'}document.documentElement.dataset.theme=t;var l=localStorage.getItem('${LANG_STORAGE_KEY}');document.documentElement.lang=l==='es'?'es':'en'}catch(e){document.documentElement.dataset.theme='dark'}})();`;
15
+
16
+ export { LANG_STORAGE_KEY, STANDARD_ROUTES, THEME_BOOT_SCRIPT, THEME_STORAGE_KEY };