@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 +71 -32
- package/bin/caos-shell-gate.mjs +67 -0
- package/chart.css +19 -0
- package/dist/chart.d.ts +58 -0
- package/dist/chart.js +153 -0
- package/dist/chunk-LOSSTLPP.js +174 -0
- package/dist/chunk-X6Y5CDUD.js +16 -0
- package/dist/index.d.ts +398 -119
- package/dist/index.js +708 -184
- package/dist/keys-CnvqkckF.d.ts +135 -0
- package/dist/keys.d.ts +4 -0
- package/dist/keys.js +1 -0
- package/dist/text-BlY47nIt.d.ts +21 -0
- package/dist/tokens-CSV0MOgR.d.ts +41 -0
- package/gate/inpage.mjs +494 -0
- package/gate/png.mjs +149 -0
- package/gate/run.mjs +595 -0
- package/gate/serve.mjs +97 -0
- package/package.json +40 -7
- package/reserved-classes.json +224 -0
- package/styles.css +225 -9
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
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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>{/* /
|
|
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:**
|
|
52
|
-
- **
|
|
53
|
-
|
|
54
|
-
- **
|
|
55
|
-
|
|
56
|
-
- **
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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`, `
|
|
65
|
-
`
|
|
66
|
-
`
|
|
67
|
-
|
|
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; }
|
package/dist/chart.d.ts
ADDED
|
@@ -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 };
|