@fasl-work/caos-app-shell 0.7.2 → 0.8.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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { useThemeStore, useShellLang, useLangStore, SHELL_TOKENS, pick, formatNumber } from './chunk-LOSSTLPP.js';
2
- export { NOT_AVAILABLE, SHELL_TOKENS, Stage, applyTheme, formatNumber, pick, readTheme, resolveToken, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens } from './chunk-LOSSTLPP.js';
1
+ import { useThemeStore, useShellLang, useLangStore, SHELL_TOKENS, pick, useStageSize, NOT_AVAILABLE, formatTicks, formatNumber, NBSP } from './chunk-ZLJCB7DD.js';
2
+ export { NBSP, NOT_AVAILABLE, SCIENTIFIC_BELOW, SHELL_TOKENS, Stage, applyTheme, formatNumber, formatTicks, pick, readTheme, resolveToken, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens } from './chunk-ZLJCB7DD.js';
3
3
  export { LANG_STORAGE_KEY, STANDARD_ROUTES, THEME_BOOT_SCRIPT, THEME_STORAGE_KEY } from './chunk-X6Y5CDUD.js';
4
4
  import { createContext, Component, useState, useRef, useId, useEffect, useCallback, useContext, useMemo } from 'react';
5
5
  import { useInRouterContext, NavLink, useLocation } from 'react-router';
@@ -20,7 +20,8 @@ var CHROME = {
20
20
  attribution: "Developed by Felipe Santib\xE1\xF1ez-Leal",
21
21
  complement: "A CAOS research project",
22
22
  license: "",
23
- version: "v"
23
+ version: "v",
24
+ skip: "Skip to the content"
24
25
  },
25
26
  es: {
26
27
  github: "C\xF3digo en GitHub",
@@ -33,7 +34,8 @@ var CHROME = {
33
34
  attribution: "Desarrollado por Felipe Santib\xE1\xF1ez-Leal",
34
35
  complement: "Un proyecto de investigaci\xF3n CAOS",
35
36
  license: "",
36
- version: "v"
37
+ version: "v",
38
+ skip: "Saltar al contenido"
37
39
  }
38
40
  };
39
41
  var chrome = (lang) => CHROME[lang];
@@ -378,6 +380,7 @@ function Frame({ config, pathname, routed, children }) {
378
380
  /* @__PURE__ */ jsx("span", { children: config.product.name })
379
381
  ] });
380
382
  return /* @__PURE__ */ jsxs("div", { className: config.contain ? "app-shell fixed contain" : fixed ? "app-shell fixed" : "app-shell", children: [
383
+ /* @__PURE__ */ jsx("a", { className: "skip-link", href: "#main", children: c.skip }),
381
384
  /* @__PURE__ */ jsx("header", { className: "site-header", children: /* @__PURE__ */ jsxs("div", { className: "header-inner", children: [
382
385
  routed ? /* @__PURE__ */ jsx(NavLink, { to: "/", className: "brand", "aria-label": config.product.name, "data-brand": config.product.name, children: brand }) : /* @__PURE__ */ jsx("a", { href: "/", className: "brand", "aria-label": config.product.name, "data-brand": config.product.name, children: brand }),
383
386
  routes.length > 1 && /* @__PURE__ */ jsx(Nav, { routes, name: config.product.name, routed, pathname }),
@@ -392,7 +395,7 @@ function Frame({ config, pathname, routed, children }) {
392
395
  ] })
393
396
  ] }) }),
394
397
  config.architecture && archOpen && /* @__PURE__ */ jsx(ArchitectureModal, { config: config.architecture, onClose: () => setArchOpen(false) }),
395
- /* @__PURE__ */ jsx("main", { className: "page", children: /* @__PURE__ */ jsx(PanelBoundary, { panel: "page", children }) }),
398
+ /* @__PURE__ */ jsx("main", { className: "page", id: "main", tabIndex: -1, children: /* @__PURE__ */ jsx(PanelBoundary, { panel: "page", children }) }),
396
399
  /* @__PURE__ */ jsx("footer", { className: "site-footer", children: /* @__PURE__ */ jsx("div", { className: "footer-inner", children: /* @__PURE__ */ jsxs("div", { className: "footer-meta", ref: footRef, children: [
397
400
  /* @__PURE__ */ jsx("span", { children: config.product.name }),
398
401
  /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
@@ -723,7 +726,7 @@ function CaseSelector(props) {
723
726
  if (!deepLink || linkPending.current || !selectedId || typeof window === "undefined") return;
724
727
  const next = withCaseParam(window.location.search, selectedId, param);
725
728
  if (next !== window.location.search) {
726
- window.history.replaceState(null, "", `${window.location.pathname}${next}${window.location.hash}`);
729
+ window.history.replaceState(window.history.state, "", `${window.location.pathname}${next}${window.location.hash}`);
727
730
  }
728
731
  }, [deepLink, param, selectedId, cases]);
729
732
  const lanes = source ? sourcesPresent(cases) : [];
@@ -1092,6 +1095,272 @@ function DocSection({ title, id, refs, noRefsReason, children }) {
1092
1095
  }
1093
1096
  );
1094
1097
  }
1098
+ function replaceQueryParam(param, value) {
1099
+ if (typeof window === "undefined") return;
1100
+ const next = withCaseParam(window.location.search, value, param);
1101
+ window.history.replaceState(window.history.state, "", `${window.location.pathname}${next}${window.location.hash}`);
1102
+ }
1103
+ function useUrlView(ids, param, value, onChange) {
1104
+ const [own, setOwn] = useState(
1105
+ () => param && typeof window !== "undefined" ? readCaseParam(window.location.search, param) : null
1106
+ );
1107
+ const open = value ?? (own && ids.includes(own) ? own : ids[0]);
1108
+ const select = (id) => {
1109
+ if (value === void 0) {
1110
+ setOwn(id);
1111
+ if (param) replaceQueryParam(param, id);
1112
+ }
1113
+ onChange?.(id);
1114
+ };
1115
+ return [open, select];
1116
+ }
1117
+ function viewParamOf(option) {
1118
+ return option === false ? null : typeof option === "string" ? option : "view";
1119
+ }
1120
+ function SurfacePage({ title, lede, actions, tabs, value, onChange, deepLinkView = true, ariaLabel, instrument, children }) {
1121
+ const lang = useShellLang();
1122
+ const [open, select] = useUrlView((tabs ?? []).map((t) => t.id), tabs ? viewParamOf(deepLinkView) : null, value, onChange);
1123
+ const head = title !== void 0 || lede !== void 0 || actions !== void 0;
1124
+ return /* @__PURE__ */ jsxs("div", { className: "page-body wide caos-surface", "data-surface": "", children: [
1125
+ head && /* @__PURE__ */ jsxs("div", { className: "caos-surface-head", children: [
1126
+ /* @__PURE__ */ jsxs("div", { className: "caos-surface-heading", children: [
1127
+ title !== void 0 && /* @__PURE__ */ jsx("h1", { className: "caos-surface-title", children: pick(title, lang) }),
1128
+ lede !== void 0 && /* @__PURE__ */ jsx("p", { className: "caos-surface-lede", children: lede })
1129
+ ] }),
1130
+ actions !== void 0 && /* @__PURE__ */ jsx("div", { className: "caos-surface-actions", children: actions })
1131
+ ] }),
1132
+ /* @__PURE__ */ jsx("div", { className: "caos-surface-body", "data-instrument": instrument ? "" : void 0, children: tabs ? /* @__PURE__ */ jsx(Tabs, { tabs, value: open, onChange: select, ariaLabel: pick(ariaLabel ?? title ?? { en: "Views", es: "Vistas" }, lang) }) : children })
1133
+ ] });
1134
+ }
1135
+
1136
+ // src/lib/breakpoints.ts
1137
+ var BREAKPOINTS = {
1138
+ /** A phone: the brand keeps only its mark; the header drops its separators. */
1139
+ phone: 480,
1140
+ /** The compact header, single-column grids, the vertical sub-tab list as a row. */
1141
+ compact: 760,
1142
+ /** The workbench stacks its rail over the instrument and the contained document scrolls. */
1143
+ stack: 900,
1144
+ /** The gate's large screen: the viewport rules of ADR-0071 are measured from here up. */
1145
+ large: 1280
1146
+ };
1147
+
1148
+ // src/chart/text.ts
1149
+ var context;
1150
+ function fontFamily() {
1151
+ if (typeof document === "undefined") return "sans-serif";
1152
+ return getComputedStyle(document.documentElement).getPropertyValue("--font-sans").trim() || "sans-serif";
1153
+ }
1154
+ function textWidth(text, px, weight = 400) {
1155
+ if (context === void 0) {
1156
+ try {
1157
+ context = typeof document === "undefined" ? null : document.createElement("canvas").getContext("2d");
1158
+ } catch {
1159
+ context = null;
1160
+ }
1161
+ }
1162
+ if (!context) return text.length * px * 0.62;
1163
+ context.font = `${weight} ${px}px ${fontFamily()}`;
1164
+ return context.measureText(text).width;
1165
+ }
1166
+ function widestLabel(labels, px, weight = 400) {
1167
+ return Math.max(0, ...labels.map((label) => textWidth(label, px, weight)));
1168
+ }
1169
+ function shorten(line, width, px) {
1170
+ if (textWidth(line, px) <= width) return line;
1171
+ let cut = line;
1172
+ while (cut.length > 1 && textWidth(`${cut}\u2026`, px) > width) cut = cut.slice(0, -1);
1173
+ return `${cut.trimEnd()}\u2026`;
1174
+ }
1175
+ function fitLabel(label, width, px, maxLines = 2) {
1176
+ if (textWidth(label, px) <= width) return { lines: [label], shortened: false };
1177
+ const words = label.split(" ");
1178
+ if (maxLines === 2 && words.length > 1) {
1179
+ let best = null;
1180
+ let bestWidth = Infinity;
1181
+ for (let k = 1; k < words.length; k += 1) {
1182
+ const first = words.slice(0, k).join(" ");
1183
+ const second = words.slice(k).join(" ");
1184
+ const widest = Math.max(textWidth(first, px), textWidth(second, px));
1185
+ if (widest < bestWidth) {
1186
+ bestWidth = widest;
1187
+ best = [first, second];
1188
+ }
1189
+ }
1190
+ if (best && bestWidth <= width) return { lines: best, shortened: false };
1191
+ if (best) return { lines: [shorten(best[0], width, px), shorten(best[1], width, px)], shortened: true };
1192
+ }
1193
+ return { lines: [shorten(label, width, px)], shortened: true };
1194
+ }
1195
+ function niceStep(x) {
1196
+ if (!(x > 0) || !Number.isFinite(x)) return 1;
1197
+ const magnitude = 10 ** Math.floor(Math.log10(x));
1198
+ return ([1, 2, 2.5, 5, 10].find((m) => m * magnitude >= x * (1 - 1e-12)) ?? 10) * magnitude;
1199
+ }
1200
+ function niceTicks(lo, hi, room, labelSize, most = 6) {
1201
+ if (!Number.isFinite(lo) || !Number.isFinite(hi)) return [0];
1202
+ if (hi < lo) [lo, hi] = [hi, lo];
1203
+ if (hi === lo) hi = lo === 0 ? 1 : lo + Math.abs(lo);
1204
+ const count = Math.max(2, Math.min(most, Math.floor(room / (labelSize + 16)) + 1));
1205
+ const step = niceStep((hi - lo) / (count - 1));
1206
+ const first = Math.floor(lo / step + 1e-9) * step;
1207
+ const ticks = [];
1208
+ for (let k = 0; first + k * step <= hi + step * 1e-9 || ticks.length < 2; k += 1) {
1209
+ ticks.push(Number((first + k * step).toPrecision(12)));
1210
+ if (ticks.length > 50) break;
1211
+ }
1212
+ if (ticks[ticks.length - 1] < hi - step * 1e-9) ticks.push(Number((ticks[ticks.length - 1] + step).toPrecision(12)));
1213
+ return ticks;
1214
+ }
1215
+ var PX = 11;
1216
+ var LINE = 13;
1217
+ var TONE = {
1218
+ good: "var(--color-good)",
1219
+ warn: "var(--color-warn)",
1220
+ bad: "var(--color-bad)",
1221
+ accent: "var(--color-accent)",
1222
+ neutral: "var(--color-fg-faint)"
1223
+ };
1224
+ function Lines({ x, y, lines, anchor, fill }) {
1225
+ return /* @__PURE__ */ jsx("text", { x, y, textAnchor: anchor, fontSize: PX, fill, style: { fontFamily: "var(--font-sans)" }, children: lines.map((line, i) => /* @__PURE__ */ jsx("tspan", { x, dy: i === 0 ? 0 : LINE, children: line }, i)) });
1226
+ }
1227
+ function BarChart({ title, data, axis, orientation = "horizontal", values = true, height, onSelect }) {
1228
+ const lang = useShellLang();
1229
+ const [ref, box] = useStageSize();
1230
+ const fill = height === "fill";
1231
+ const name = pick(title, lang);
1232
+ const axisTitle = axis.unit ? `${pick(axis.label, lang)} (${pick(axis.unit, lang)})` : pick(axis.label, lang);
1233
+ const fmt = axis.format ?? { digits: 3 };
1234
+ const finite = data.map((d) => d.value).filter((v) => v !== null && v !== void 0 && Number.isFinite(v));
1235
+ const lo = Math.min(0, axis.range ? axis.range[0] : Math.min(0, ...finite));
1236
+ const hi = Math.max(0, axis.range ? axis.range[1] : Math.max(0, ...finite));
1237
+ const anyHighlight = data.some((d) => d.highlight);
1238
+ const colour = (d) => d.tone ? TONE[d.tone] : !anyHighlight || d.highlight ? "var(--color-accent)" : "var(--color-fg-faint)";
1239
+ const shown = (d) => d.value === null || d.value === void 0 || !Number.isFinite(d.value) ? NOT_AVAILABLE[lang] : formatNumber(d.value, lang, fmt);
1240
+ const width = box.width;
1241
+ const select = (id) => onSelect?.(id);
1242
+ const keyed = (id) => (e) => {
1243
+ if (e.key === "Enter" || e.key === " ") {
1244
+ e.preventDefault();
1245
+ select(id);
1246
+ }
1247
+ };
1248
+ const interactive = (d) => onSelect ? { role: "button", tabIndex: 0, onClick: () => select(d.id), onKeyDown: keyed(d.id), style: { cursor: "pointer" } } : {};
1249
+ let drawing = null;
1250
+ let drawnHeight = 0;
1251
+ if (width > 0 && data.length > 0) {
1252
+ const valueLabels = data.map(shown);
1253
+ const valueCol = values ? widestLabel(valueLabels, PX) + 8 : 0;
1254
+ if (orientation === "horizontal") {
1255
+ const labelCol = Math.min(widestLabel(data.map((d) => pick(d.label, lang)), PX), Math.max(80, 0.38 * width));
1256
+ const fitted = data.map((d) => fitLabel(pick(d.label, lang), labelCol, PX, 2));
1257
+ const need = Math.max(22, Math.max(...fitted.map((f) => f.lines.length)) * LINE + 8);
1258
+ const axisH = 6 + LINE + 4 + LINE + 4;
1259
+ const x0 = labelCol + 10;
1260
+ const x1 = Math.max(x0 + 40, width - valueCol - 6);
1261
+ const tickProbe = widestLabel(formatTicks([lo, hi], lang, fmt), PX);
1262
+ const ticks = niceTicks(lo, hi, x1 - x0, tickProbe);
1263
+ const t0 = ticks[0];
1264
+ const t1 = ticks[ticks.length - 1];
1265
+ const tickLabels = formatTicks(ticks, lang, fmt);
1266
+ const sx = (v) => x0 + (v - t0) / (t1 - t0 || 1) * (x1 - x0);
1267
+ const rowH = fill && box.height > 0 ? Math.max(need, (box.height - axisH - 4) / data.length) : need;
1268
+ const top = 4;
1269
+ const plotBottom = top + data.length * rowH;
1270
+ drawnHeight = fill && box.height > 0 ? Math.max(box.height, plotBottom + axisH) : plotBottom + axisH;
1271
+ const barH = Math.min(rowH * 0.5, 40);
1272
+ drawing = /* @__PURE__ */ jsxs("svg", { width, height: drawnHeight, role: "img", "aria-label": name, "data-chart": "", "data-chart-kind": "bars", children: [
1273
+ /* @__PURE__ */ jsx("title", { children: name }),
1274
+ ticks.map((t, i) => /* @__PURE__ */ jsxs("g", { children: [
1275
+ /* @__PURE__ */ jsx("line", { x1: sx(t), x2: sx(t), y1: top, y2: plotBottom, stroke: "var(--color-border)", strokeWidth: t === 0 ? 1.4 : 1 }),
1276
+ /* @__PURE__ */ jsx("text", { x: sx(t), y: plotBottom + 6 + LINE - 2, textAnchor: i === 0 && sx(t) - textWidth(tickLabels[i], PX) / 2 < 0 ? "start" : i === ticks.length - 1 && sx(t) + textWidth(tickLabels[i], PX) / 2 > width ? "end" : "middle", fontSize: PX, fill: "var(--color-fg-subtle)", style: { fontFamily: "var(--font-sans)" }, children: tickLabels[i] })
1277
+ ] }, `t${i}`)),
1278
+ /* @__PURE__ */ jsx("text", { x: x0 + (x1 - x0) / 2, y: plotBottom + 6 + LINE + 4 + LINE - 2, textAnchor: "middle", fontSize: PX, fill: "var(--color-fg-subtle)", style: { fontFamily: "var(--font-sans)" }, children: axisTitle }),
1279
+ data.map((d, i) => {
1280
+ const cy = top + i * rowH + rowH / 2;
1281
+ const f = fitted[i];
1282
+ const v = d.value;
1283
+ const ok = v !== null && v !== void 0 && Number.isFinite(v);
1284
+ const xa = sx(0);
1285
+ const xb = ok ? sx(v) : xa;
1286
+ const labelText = valueLabels[i];
1287
+ const negative = ok && v < 0;
1288
+ return /* @__PURE__ */ jsxs("g", { "data-bar": d.id, ...interactive(d), children: [
1289
+ /* @__PURE__ */ jsx("title", { children: [pick(d.label, lang), labelText, pick(d.hint, lang)].filter(Boolean).join(": ") }),
1290
+ /* @__PURE__ */ jsx(Lines, { x: x0 - 8, y: cy - (f.lines.length - 1) * LINE / 2 + 4, lines: f.lines, anchor: "end", fill: d.highlight ? "var(--color-fg)" : "var(--color-fg-subtle)" }),
1291
+ ok && /* @__PURE__ */ jsx("rect", { x: Math.min(xa, xb), y: cy - barH / 2, width: Math.max(1, Math.abs(xb - xa)), height: barH, rx: 2, fill: colour(d) }),
1292
+ values && /* @__PURE__ */ jsx(
1293
+ "text",
1294
+ {
1295
+ x: ok ? negative ? Math.max(x0 + textWidth(labelText, PX) + 2, xb - 4) : xb + 4 : xa + 4,
1296
+ y: cy + 4,
1297
+ textAnchor: negative ? "end" : "start",
1298
+ fontSize: PX,
1299
+ fill: ok ? "var(--color-fg)" : "var(--color-fg-faint)",
1300
+ fontStyle: ok ? void 0 : "italic",
1301
+ style: { fontFamily: "var(--font-sans)", fontVariantNumeric: "tabular-nums" },
1302
+ children: labelText
1303
+ }
1304
+ )
1305
+ ] }, d.id);
1306
+ })
1307
+ ] });
1308
+ } else {
1309
+ const h = fill && box.height > 0 ? box.height : typeof height === "number" ? height : 240;
1310
+ const titleRow = LINE + 6;
1311
+ const valueRow = values ? LINE + 4 : 4;
1312
+ const probe = widestLabel(formatTicks([lo, hi], lang, fmt), PX);
1313
+ const left = probe + 12;
1314
+ const right = 8;
1315
+ const band = (width - left - right) / data.length;
1316
+ const fitted = data.map((d) => fitLabel(pick(d.label, lang), Math.max(24, band - 6), PX, 2));
1317
+ const bottom = Math.max(...fitted.map((f) => f.lines.length)) * LINE + 10;
1318
+ const y0 = titleRow + valueRow;
1319
+ const y1 = Math.max(y0 + 40, h - bottom);
1320
+ const ticks = niceTicks(lo, hi, y1 - y0, LINE);
1321
+ const t0 = ticks[0];
1322
+ const t1 = ticks[ticks.length - 1];
1323
+ const tickLabels = formatTicks(ticks, lang, fmt);
1324
+ const sy = (v) => y1 - (v - t0) / (t1 - t0 || 1) * (y1 - y0);
1325
+ const barW = Math.min(band * 0.62, 56);
1326
+ drawnHeight = h;
1327
+ drawing = /* @__PURE__ */ jsxs("svg", { width, height: h, role: "img", "aria-label": name, "data-chart": "", "data-chart-kind": "bars", children: [
1328
+ /* @__PURE__ */ jsx("title", { children: name }),
1329
+ /* @__PURE__ */ jsx("text", { x: 0, y: LINE - 2, textAnchor: "start", fontSize: PX, fill: "var(--color-fg-subtle)", style: { fontFamily: "var(--font-sans)" }, children: axisTitle }),
1330
+ ticks.map((t, i) => /* @__PURE__ */ jsxs("g", { children: [
1331
+ /* @__PURE__ */ jsx("line", { x1: left, x2: width - right, y1: sy(t), y2: sy(t), stroke: "var(--color-border)", strokeWidth: t === 0 ? 1.4 : 1 }),
1332
+ /* @__PURE__ */ jsx("text", { x: left - 6, y: sy(t) + 4, textAnchor: "end", fontSize: PX, fill: "var(--color-fg-subtle)", style: { fontFamily: "var(--font-sans)" }, children: tickLabels[i] })
1333
+ ] }, `t${i}`)),
1334
+ data.map((d, i) => {
1335
+ const cx = left + band * i + band / 2;
1336
+ const v = d.value;
1337
+ const ok = v !== null && v !== void 0 && Number.isFinite(v);
1338
+ const ya = sy(0);
1339
+ const yb = ok ? sy(v) : ya;
1340
+ const f = fitted[i];
1341
+ return /* @__PURE__ */ jsxs("g", { "data-bar": d.id, ...interactive(d), children: [
1342
+ /* @__PURE__ */ jsx("title", { children: [pick(d.label, lang), valueLabels[i], pick(d.hint, lang)].filter(Boolean).join(": ") }),
1343
+ ok && /* @__PURE__ */ jsx("rect", { x: cx - barW / 2, y: Math.min(ya, yb), width: barW, height: Math.max(1, Math.abs(yb - ya)), rx: 2, fill: colour(d) }),
1344
+ values && /* @__PURE__ */ jsx("text", { x: cx, y: Math.min(ya, yb) - 5, textAnchor: "middle", fontSize: PX, fill: ok ? "var(--color-fg)" : "var(--color-fg-faint)", fontStyle: ok ? void 0 : "italic", style: { fontFamily: "var(--font-sans)", fontVariantNumeric: "tabular-nums" }, children: valueLabels[i] }),
1345
+ /* @__PURE__ */ jsx(Lines, { x: cx, y: y1 + LINE + 2, lines: f.lines, anchor: "middle", fill: d.highlight ? "var(--color-fg)" : "var(--color-fg-subtle)" })
1346
+ ] }, d.id);
1347
+ })
1348
+ ] });
1349
+ }
1350
+ }
1351
+ return /* @__PURE__ */ jsx(
1352
+ "div",
1353
+ {
1354
+ ref,
1355
+ className: fill ? "caos-bars fill" : "caos-bars",
1356
+ style: !fill && orientation === "vertical" ? { height: typeof height === "number" ? height : 240 } : void 0,
1357
+ "data-drawn": width > 0 ? "1" : "0",
1358
+ "data-width": width,
1359
+ "data-height": drawnHeight,
1360
+ children: drawing
1361
+ }
1362
+ );
1363
+ }
1095
1364
  function isSections(rail) {
1096
1365
  return Array.isArray(rail) && rail.every((s) => typeof s === "object" && s !== null && "id" in s && "content" in s);
1097
1366
  }
@@ -1102,11 +1371,24 @@ function WorkbenchLayout({ rail, railHead, children, railLabel, className }) {
1102
1371
  const baseId = useId();
1103
1372
  const label = pick(railLabel ?? { en: "Controls", es: "Controles" }, lang);
1104
1373
  const current = sections?.find((s) => s.id === active) ?? sections?.[0];
1374
+ const rowRef = useRef(null);
1375
+ useOverflowFade(rowRef, ".chip.on", [current?.id, sections?.length, lang]);
1376
+ function onKeyDown(e, idx) {
1377
+ if (!sections || e.key !== "ArrowRight" && e.key !== "ArrowLeft" && e.key !== "Home" && e.key !== "End") return;
1378
+ e.preventDefault();
1379
+ const n = sections.length;
1380
+ const next = e.key === "Home" ? 0 : e.key === "End" ? n - 1 : (idx + (e.key === "ArrowRight" ? 1 : n - 1)) % n;
1381
+ const target = sections[next];
1382
+ if (target) {
1383
+ setActive(target.id);
1384
+ document.getElementById(`${baseId}-rs-${target.id}`)?.focus();
1385
+ }
1386
+ }
1105
1387
  return /* @__PURE__ */ jsxs("div", { className: ["page-body", "wide", "caos-wb", className].filter(Boolean).join(" "), "data-workbench": "", children: [
1106
1388
  /* @__PURE__ */ jsxs("aside", { className: "caos-wb-rail", "aria-label": label, "data-rail": "", children: [
1107
1389
  railHead && /* @__PURE__ */ jsx("div", { className: "caos-wb-rail-head", children: railHead }),
1108
1390
  sections ? /* @__PURE__ */ jsxs(Fragment, { children: [
1109
- /* @__PURE__ */ jsx("div", { className: "caos-wb-rail-sections", role: "tablist", "aria-label": label, children: sections.map((s) => /* @__PURE__ */ jsx(
1391
+ /* @__PURE__ */ jsx("div", { className: "caos-wb-rail-sections", role: "tablist", "aria-label": label, ref: rowRef, children: sections.map((s, idx) => /* @__PURE__ */ jsx(
1110
1392
  "button",
1111
1393
  {
1112
1394
  type: "button",
@@ -1114,8 +1396,10 @@ function WorkbenchLayout({ rail, railHead, children, railLabel, className }) {
1114
1396
  id: `${baseId}-rs-${s.id}`,
1115
1397
  "aria-selected": s.id === current?.id,
1116
1398
  "aria-controls": `${baseId}-rp-${s.id}`,
1399
+ tabIndex: s.id === current?.id ? 0 : -1,
1117
1400
  className: s.id === current?.id ? "chip on" : "chip",
1118
1401
  onClick: () => setActive(s.id),
1402
+ onKeyDown: (e) => onKeyDown(e, idx),
1119
1403
  children: pick(s.label, lang)
1120
1404
  },
1121
1405
  s.id
@@ -1178,6 +1462,8 @@ function VariantBar({ variants, activeId, onSelect, title, lane }) {
1178
1462
  const lang = useShellLang();
1179
1463
  const active = variants.find((v) => v.id === activeId) ?? variants[0];
1180
1464
  const shownLane = active?.lane ?? lane;
1465
+ const rowRef = useRef(null);
1466
+ useOverflowFade(rowRef, ".chip.on", [active?.id, variants.length, lang]);
1181
1467
  return /* @__PURE__ */ jsxs("div", { className: "caos-variant-bar", "data-variant-bar": "", "data-variant-count": variants.length, children: [
1182
1468
  /* @__PURE__ */ jsxs("div", { className: "caos-variant-head", children: [
1183
1469
  /* @__PURE__ */ jsxs("span", { className: "caos-variant-title", children: [
@@ -1188,7 +1474,7 @@ function VariantBar({ variants, activeId, onSelect, title, lane }) {
1188
1474
  ] }),
1189
1475
  shownLane && /* @__PURE__ */ jsx(LaneBadge, { lane: shownLane })
1190
1476
  ] }),
1191
- /* @__PURE__ */ jsx("div", { className: "caos-chip-row", role: "radiogroup", "aria-label": pick(title ?? { en: "Variants", es: "Variantes" }, lang), "data-control": "variant", children: variants.map((v) => /* @__PURE__ */ jsx(
1477
+ /* @__PURE__ */ jsx("div", { className: "caos-chip-row", role: "radiogroup", "aria-label": pick(title ?? { en: "Variants", es: "Variantes" }, lang), "data-control": "variant", ref: rowRef, children: variants.map((v) => /* @__PURE__ */ jsx(
1192
1478
  "button",
1193
1479
  {
1194
1480
  type: "button",
@@ -1209,7 +1495,7 @@ function panel(id, lane, provenance, content) {
1209
1495
  return /* @__PURE__ */ jsx("div", { className: "caos-cw-panel", "data-group": id, "data-lane": lane, "data-provenance": provenance, children: content });
1210
1496
  }
1211
1497
  function CaseWorkbench(props) {
1212
- const { caseId, cases, source, controls, replayOnly, variants, rail, railLabel, groups, compare, context, group, onGroupChange, ariaLabel } = props;
1498
+ const { caseId, cases, source, controls, replayOnly, variants, rail, railLabel, groups, compare, context: context2, group, onGroupChange, deepLinkView = true, ariaLabel } = props;
1213
1499
  const lang = useShellLang();
1214
1500
  const variantId = variants?.activeId;
1215
1501
  const stateKey = useMemo(
@@ -1226,14 +1512,15 @@ function CaseWorkbench(props) {
1226
1512
  }
1227
1513
  tabs.push({
1228
1514
  id: "context",
1229
- label: pick(context.label ?? { en: "Context", es: "Contexto" }, lang),
1230
- content: /* @__PURE__ */ jsx("div", { className: "prose caos-cw-context", children: context.content })
1515
+ label: pick(context2.label ?? { en: "Context", es: "Contexto" }, lang),
1516
+ content: /* @__PURE__ */ jsx("div", { className: "prose caos-cw-context", children: context2.content })
1231
1517
  });
1232
1518
  if (tabs.length > MAX_WORKBENCH_GROUPS) {
1233
1519
  throw new Error(
1234
1520
  `CaseWorkbench: ${tabs.length} groups; at most ${MAX_WORKBENCH_GROUPS} peers, then group by question (ADR-0071 rule 5)`
1235
1521
  );
1236
1522
  }
1523
+ const [openGroup, selectGroup] = useUrlView(tabs.map((t) => t.id), viewParamOf(deepLinkView), group, onGroupChange);
1237
1524
  const railHead = cases || variants ? /* @__PURE__ */ jsxs(Fragment, { children: [
1238
1525
  cases && /* @__PURE__ */ jsx(CaseSelector, { lang, ...cases }),
1239
1526
  variants && /* @__PURE__ */ jsx(VariantBar, { ...variants })
@@ -1253,8 +1540,8 @@ function CaseWorkbench(props) {
1253
1540
  Tabs,
1254
1541
  {
1255
1542
  tabs,
1256
- value: group,
1257
- onChange: onGroupChange,
1543
+ value: openGroup,
1544
+ onChange: selectGroup,
1258
1545
  ariaLabel: pick(ariaLabel ?? { en: "Case views", es: "Vistas del caso" }, lang)
1259
1546
  }
1260
1547
  )
@@ -1306,10 +1593,7 @@ function Readout({ items, title, lane, provenance, dataKey }) {
1306
1593
  /* @__PURE__ */ jsx("dt", { children: label }),
1307
1594
  /* @__PURE__ */ jsxs("dd", { children: [
1308
1595
  /* @__PURE__ */ jsx("span", { className: "caos-readout-value", children: shown }),
1309
- it.unit && it.text === void 0 && /* @__PURE__ */ jsxs("span", { className: "caos-readout-unit", children: [
1310
- " ",
1311
- pick(it.unit, lang)
1312
- ] })
1596
+ it.unit && it.text === void 0 && /* @__PURE__ */ jsx("span", { className: "caos-readout-unit", children: `${NBSP}${pick(it.unit, lang)}` })
1313
1597
  ] })
1314
1598
  ] }, i);
1315
1599
  }) }),
@@ -1346,12 +1630,12 @@ function Gauge({ title, value, min, max, zones, unit, unitless, format }) {
1346
1630
  /* @__PURE__ */ jsx("span", { children: fmt(min) }),
1347
1631
  /* @__PURE__ */ jsxs("span", { className: "caos-gauge-value", children: [
1348
1632
  fmt(value),
1349
- finite && unit ? ` ${pick(unit, lang)}` : ""
1633
+ finite && unit ? `${NBSP}${pick(unit, lang)}` : ""
1350
1634
  ] }),
1351
1635
  /* @__PURE__ */ jsx("span", { children: fmt(max) })
1352
1636
  ] }),
1353
1637
  /* @__PURE__ */ jsx("div", { className: "caos-gauge-legend", children: zones.map((z, i) => /* @__PURE__ */ jsxs("span", { children: [
1354
- /* @__PURE__ */ jsx("i", { className: `dot tone-${z.tone}`, "aria-hidden": "true" }),
1638
+ /* @__PURE__ */ jsx("i", { className: `caos-gauge-dot tone-${z.tone}`, "aria-hidden": "true" }),
1355
1639
  pick(z.label, lang)
1356
1640
  ] }, i)) })
1357
1641
  ] });
@@ -1415,7 +1699,7 @@ function Knob({ id, label, value, min, max, step, unit, hint, format, disabled,
1415
1699
  /* @__PURE__ */ jsx("span", { children: pick(label, lang) }),
1416
1700
  /* @__PURE__ */ jsxs("span", { className: "caos-knob-value", children: [
1417
1701
  formatNumber(value, lang, format ?? {}),
1418
- unit ? ` ${pick(unit, lang)}` : ""
1702
+ unit ? `${NBSP}${pick(unit, lang)}` : ""
1419
1703
  ] })
1420
1704
  ] }),
1421
1705
  /* @__PURE__ */ jsx(
@@ -1436,9 +1720,11 @@ function Knob({ id, label, value, min, max, step, unit, hint, format, disabled,
1436
1720
  }
1437
1721
  function ChipGroup({ id, label, options, value, onChange }) {
1438
1722
  const lang = useShellLang();
1723
+ const rowRef = useRef(null);
1724
+ useOverflowFade(rowRef, ".chip.on", [value, options.length, lang]);
1439
1725
  return /* @__PURE__ */ jsxs("div", { className: "caos-chipgroup", "data-control": id, children: [
1440
1726
  /* @__PURE__ */ jsx("span", { className: "caos-chipgroup-label", children: pick(label, lang) }),
1441
- /* @__PURE__ */ jsx("div", { className: "caos-chip-row", role: "radiogroup", "aria-label": pick(label, lang), children: options.map((o) => /* @__PURE__ */ jsx(
1727
+ /* @__PURE__ */ jsx("div", { className: "caos-chip-row", role: "radiogroup", "aria-label": pick(label, lang), ref: rowRef, children: options.map((o) => /* @__PURE__ */ jsx(
1442
1728
  "button",
1443
1729
  {
1444
1730
  type: "button",
@@ -1455,4 +1741,4 @@ function ChipGroup({ id, label, options, value, onChange }) {
1455
1741
  ] });
1456
1742
  }
1457
1743
 
1458
- export { AppShell, ArchitectureModal, Callout, CaseSelector, CaseWorkbench, ChipGroup, CitationsProvider, Cite, DocPage, DocSection, Equation, Figure, FocusShell, Gauge, InlineMath, Knob, LaneBadge, LanguageToggle, MAX_PEER_TABS, MAX_WORKBENCH_GROUPS, PanelBoundary, PlotCard, Readout, ReferenceList, Refs, SubTabs, TabGroups, Tabs, ThemeToggle, VariantBar, Verdict, WorkbenchLayout, WorkbenchShell, WorkbenchStateContext, caseKind, caseTooltip, casesInSource, createVizLoop, groupByCategory, isStale, makeStateKey, readCaseParam, sourcesPresent, usePausedViz, useWorkbenchState, validateArchitectureConfig, withCaseParam };
1744
+ export { AppShell, ArchitectureModal, BREAKPOINTS, BarChart, Callout, CaseSelector, CaseWorkbench, ChipGroup, CitationsProvider, Cite, DocPage, DocSection, Equation, Figure, FocusShell, Gauge, InlineMath, Knob, LaneBadge, LanguageToggle, MAX_PEER_TABS, MAX_WORKBENCH_GROUPS, PanelBoundary, PlotCard, Readout, ReferenceList, Refs, SubTabs, SurfacePage, TabGroups, Tabs, ThemeToggle, VariantBar, Verdict, WorkbenchLayout, WorkbenchShell, WorkbenchStateContext, caseKind, caseTooltip, casesInSource, createVizLoop, fitLabel, fontFamily, groupByCategory, isStale, makeStateKey, niceStep, niceTicks, readCaseParam, replaceQueryParam, sourcesPresent, textWidth, usePausedViz, useUrlView, useWorkbenchState, validateArchitectureConfig, viewParamOf, widestLabel, withCaseParam };
@@ -9,15 +9,30 @@ interface FormatOptions {
9
9
  decimals?: number;
10
10
  /** Render a fraction as a percentage (0.123 -> 12.3 %). */
11
11
  percent?: boolean;
12
+ /** `scientific` writes every non-zero value in scientific notation with `digits` significant digits; the default
13
+ * chooses by magnitude. An axis uses it so all its ticks share one notation (`formatTicks`). */
14
+ notation?: 'auto' | 'scientific';
12
15
  }
16
+ /** The no-break space (U+00A0) that joins a value to its unit or sign. */
17
+ declare const NBSP = "\u00A0";
18
+ /** Below this magnitude a value is written in scientific notation. In fixed notation a p-value of 1e-200 is two
19
+ * hundred zeros: one table cell 12,790 px wide (known shell defect 15, CAOS_Contraste, 2026-10-05). */
20
+ declare const SCIENTIFIC_BELOW = 0.0001;
13
21
  /** "Not available" in the interface language: what an absent or non-finite value renders as. */
14
22
  declare const NOT_AVAILABLE: Record<Lang, string>;
15
23
  /**
16
24
  * A number in the interface language: a decimal point in English and a decimal comma in Spanish
17
- * (`conventions/languages.md`), significant digits by default, scientific notation below 1e-4. `null`, `undefined` and non-finite values render as
18
- * "not available", never as `NaN` (failure classes 5 and 23 of the 2026-10-04 history).
25
+ * (`conventions/languages.md`), significant digits by default, scientific notation below 1e-4. `null`, `undefined`
26
+ * and non-finite values render as "not available", never as `NaN` (failure classes 5 and 23 of the 2026-10-04
27
+ * history). A percent is joined to its sign by a no-break space.
19
28
  */
20
29
  declare function formatNumber(value: number | null | undefined, lang: Lang, opts?: FormatOptions): string;
30
+ /**
31
+ * The labels of one axis's ticks, in ONE notation: when any non-zero tick is below 1e-4 (and the axis has no fixed
32
+ * decimals), every non-zero tick is written in scientific notation; deciding tick by tick wrote 0.0001 beside 7.5E-5
33
+ * on one axis (known shell defect 19, CAOS_Contraste, 2026-10-05). `null` ticks are blank.
34
+ */
35
+ declare function formatTicks(values: readonly (number | null | undefined)[], lang: Lang, opts?: FormatOptions): string[];
21
36
  /** `formatNumber` bound to the current shell language. */
22
37
  declare function useFormat(): (value: number | null | undefined, opts?: FormatOptions) => string;
23
38
 
@@ -27,8 +42,10 @@ declare function useFormat(): (value: number | null | undefined, opts?: FormatOp
27
42
  * defined renders as nothing, silently (failure class 25 of the 2026-10-04 history). A unit test keeps this list
28
43
  * equal to the stylesheet.
29
44
  */
30
- declare const SHELL_TOKENS: readonly ["--color-bg", "--color-surface", "--color-surface-2", "--color-border", "--color-fg", "--color-fg-subtle", "--color-fg-faint", "--color-accent", "--color-accent-fg", "--color-accent-soft", "--color-accent-2", "--color-magenta", "--color-good", "--color-warn", "--color-bad", "--color-shadow", "--font-sans", "--font-mono", "--maxw", "--maxw-wide"];
45
+ declare const SHELL_TOKENS: readonly ["--color-bg", "--color-surface", "--color-surface-2", "--color-border", "--color-fg", "--color-fg-subtle", "--color-fg-faint", "--color-accent", "--color-accent-fg", "--color-accent-soft", "--color-accent-2", "--color-magenta", "--color-good", "--color-warn", "--color-bad", "--color-shadow", "--font-sans", "--font-mono", "--text-xs", "--text-sm", "--text-md", "--text-base", "--text-lg", "--text-xl", "--text-2xl", "--space-1", "--space-2", "--space-3", "--space-4", "--space-5", "--space-6", "--radius-sm", "--radius-md", "--radius-lg", "--radius-pill", "--maxw", "--maxw-wide", "--measure", "--measure-text", "--header-h", "--rail-w", "--fade", "--icon-sm", "--icon-md", "--icon-lg", "--z-header", "--z-focus", "--z-modal"];
31
46
  type ShellToken = (typeof SHELL_TOKENS)[number];
47
+ /** A colour token: what a chart series, a tone or a canvas colour may name. */
48
+ type ShellColorToken = Extract<ShellToken, `--color-${string}`>;
32
49
  /** The computed value of a token on the document, or `fallback` outside a browser. */
33
50
  declare function resolveToken(token: string, fallback?: string): string;
34
51
  /**
@@ -38,4 +55,4 @@ declare function resolveToken(token: string, fallback?: string): string;
38
55
  */
39
56
  declare function useThemeTokens(): Record<ShellToken, string>;
40
57
 
41
- export { type FormatOptions as F, NOT_AVAILABLE as N, type ShellToken as S, SHELL_TOKENS as a, useThemeTokens as b, formatNumber as f, resolveToken as r, useFormat as u };
58
+ export { type FormatOptions as F, NBSP as N, type ShellColorToken as S, NOT_AVAILABLE as a, SCIENTIFIC_BELOW as b, SHELL_TOKENS as c, type ShellToken as d, formatTicks as e, formatNumber as f, useThemeTokens as g, resolveToken as r, useFormat as u };