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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,40 +1,12 @@
1
- import { createContext, useState, useRef, useId, useEffect, useCallback, useContext } from 'react';
2
- import { useLocation, NavLink } from 'react-router';
1
+ import { useThemeStore, useShellLang, useLangStore, SHELL_TOKENS, pick, formatNumber } from './chunk-OV4HK6EQ.js';
2
+ export { NOT_AVAILABLE, SHELL_TOKENS, Stage, applyTheme, formatNumber, pick, readTheme, resolveToken, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens } from './chunk-OV4HK6EQ.js';
3
+ export { LANG_STORAGE_KEY, STANDARD_ROUTES, THEME_BOOT_SCRIPT, THEME_STORAGE_KEY } from './chunk-X6Y5CDUD.js';
4
+ import { createContext, Component, useState, useRef, useId, useEffect, useCallback, useContext, useMemo } from 'react';
5
+ import { useInRouterContext, NavLink, useLocation } from 'react-router';
3
6
  import { Sun, Moon, Languages, Boxes, CodeXml, Globe, Briefcase, Info } from 'lucide-react';
4
- import { create } from 'zustand';
5
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
7
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
8
  import katex from 'katex';
7
9
 
8
- // src/shell/AppShell.tsx
9
- var KEY = "caos.lang";
10
- function readLang() {
11
- try {
12
- const s = localStorage.getItem(KEY);
13
- if (s === "en" || s === "es") return s;
14
- } catch {
15
- }
16
- return "en";
17
- }
18
- var useLangStore = create((set, get) => ({
19
- lang: readLang(),
20
- setLang: (l) => {
21
- try {
22
- localStorage.setItem(KEY, l);
23
- } catch {
24
- }
25
- set({ lang: l });
26
- },
27
- toggle: () => {
28
- const next = get().lang === "en" ? "es" : "en";
29
- try {
30
- localStorage.setItem(KEY, next);
31
- } catch {
32
- }
33
- set({ lang: next });
34
- }
35
- }));
36
- var useShellLang = () => useLangStore((s) => s.lang);
37
-
38
10
  // src/lib/chrome.ts
39
11
  var CHROME = {
40
12
  en: {
@@ -47,7 +19,7 @@ var CHROME = {
47
19
  dark: "Dark",
48
20
  attribution: "Developed by Felipe Santib\xE1\xF1ez-Leal",
49
21
  complement: "A CAOS research project",
50
- license: "MIT licensed \xB7 open source",
22
+ license: "",
51
23
  version: "v"
52
24
  },
53
25
  es: {
@@ -60,47 +32,73 @@ var CHROME = {
60
32
  dark: "Oscuro",
61
33
  attribution: "Desarrollado por Felipe Santib\xE1\xF1ez-Leal",
62
34
  complement: "Un proyecto de investigaci\xF3n CAOS",
63
- license: "Licencia MIT \xB7 c\xF3digo abierto",
35
+ license: "",
64
36
  version: "v"
65
37
  }
66
38
  };
67
39
  var chrome = (lang) => CHROME[lang];
68
- var KEY2 = "caos.theme";
69
- function readTheme() {
70
- try {
71
- const s = localStorage.getItem(KEY2);
72
- if (s === "light" || s === "dark") return s;
73
- return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
74
- } catch {
75
- return "dark";
76
- }
40
+ function FailedView({ panel: panel2, error }) {
41
+ const lang = useShellLang();
42
+ return /* @__PURE__ */ jsxs("div", { className: "caos-panel-error", role: "alert", "data-panel-error": panel2, children: [
43
+ /* @__PURE__ */ jsx("strong", { children: lang === "es" ? "Esta vista fall\xF3." : "This view failed." }),
44
+ " ",
45
+ /* @__PURE__ */ jsx("span", { children: lang === "es" ? "El resto de la p\xE1gina sigue funcionando." : "The rest of the page still works." }),
46
+ /* @__PURE__ */ jsx("code", { children: error.message })
47
+ ] });
77
48
  }
78
- function applyTheme(t) {
79
- try {
80
- document.documentElement.dataset.theme = t;
81
- } catch {
49
+ var PanelBoundary = class extends Component {
50
+ state = { error: null };
51
+ static getDerivedStateFromError(error) {
52
+ return { error };
82
53
  }
83
- }
84
- var useThemeStore = create((set, get) => ({
85
- theme: readTheme(),
86
- toggleTheme: () => {
87
- const next = get().theme === "dark" ? "light" : "dark";
88
- applyTheme(next);
89
- try {
90
- localStorage.setItem(KEY2, next);
91
- } catch {
92
- }
93
- set({ theme: next });
94
- },
95
- setTheme: (t) => {
96
- applyTheme(t);
97
- try {
98
- localStorage.setItem(KEY2, t);
99
- } catch {
100
- }
101
- set({ theme: t });
54
+ componentDidCatch(error, info) {
55
+ console.error(`[caos-app-shell] view "${this.props.panel}" failed: ${error.message}`, info.componentStack ?? "");
56
+ }
57
+ render() {
58
+ if (this.state.error) return /* @__PURE__ */ jsx(FailedView, { panel: this.props.panel, error: this.state.error });
59
+ return this.props.children;
60
+ }
61
+ };
62
+ var FADE = 28;
63
+ function reveal(row, item) {
64
+ const r = row.getBoundingClientRect();
65
+ const b = item.getBoundingClientRect();
66
+ const margin = Math.max(0, Math.min(FADE, (r.width - b.width) / 2));
67
+ if (b.left < r.left + margin) {
68
+ row.scrollTo({ left: Math.max(0, row.scrollLeft - (r.left + margin - b.left)), behavior: "instant" });
69
+ } else if (b.right > r.right - margin) {
70
+ row.scrollTo({ left: Math.min(row.scrollWidth - row.clientWidth, row.scrollLeft + (b.right - (r.right - margin))), behavior: "instant" });
102
71
  }
103
- }));
72
+ }
73
+ function useOverflowFade(ref, activeSelector, deps = []) {
74
+ useEffect(() => {
75
+ const el = ref.current;
76
+ if (!el) return;
77
+ const update = () => {
78
+ const start = el.scrollLeft > 1;
79
+ const end = el.scrollLeft + el.clientWidth < el.scrollWidth - 1;
80
+ el.dataset.fadeStart = start ? "1" : "0";
81
+ el.dataset.fadeEnd = end ? "1" : "0";
82
+ };
83
+ const focused = document.activeElement instanceof HTMLElement && el.contains(document.activeElement) && document.activeElement !== el ? document.activeElement : null;
84
+ const active = focused ?? (activeSelector ? el.querySelector(activeSelector) : null);
85
+ if (active && el.scrollWidth > el.clientWidth) reveal(el, active);
86
+ const onFocus = (e) => {
87
+ const target = e.target;
88
+ if (target && target !== el && el.contains(target) && el.scrollWidth > el.clientWidth) reveal(el, target);
89
+ };
90
+ update();
91
+ el.addEventListener("scroll", update, { passive: true });
92
+ el.addEventListener("focusin", onFocus);
93
+ const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(update) : null;
94
+ ro?.observe(el);
95
+ return () => {
96
+ el.removeEventListener("scroll", update);
97
+ el.removeEventListener("focusin", onFocus);
98
+ ro?.disconnect();
99
+ };
100
+ }, deps);
101
+ }
104
102
  function ThemeToggle() {
105
103
  const theme = useThemeStore((s) => s.theme);
106
104
  const toggleTheme = useThemeStore((s) => s.toggleTheme);
@@ -119,7 +117,29 @@ function LanguageToggle() {
119
117
  /* @__PURE__ */ jsx("span", { className: "lang-code", children: lang })
120
118
  ] });
121
119
  }
122
- var cache = {};
120
+ function validateArchitectureConfig(config) {
121
+ const out = [];
122
+ if (config.tabs.length < 5) out.push(`the architecture modal has ${config.tabs.length} tabs; ADR-0058 requires at least 5`);
123
+ const known = new Set(SHELL_TOKENS);
124
+ for (const t of config.tabs) {
125
+ if (!t.en?.trim() || !t.es?.trim() || !t.body_en?.trim() || !t.body_es?.trim()) {
126
+ out.push(`architecture tab "${t.id}" lacks a label or a body in one language`);
127
+ }
128
+ const svg = t.svg.trim();
129
+ if (!svg.startsWith("<svg")) {
130
+ out.push(`architecture tab "${t.id}": the diagram must be an inline SVG string (import it with ?raw), not a URL`);
131
+ continue;
132
+ }
133
+ for (const name of new Set([...svg.matchAll(/var\((--[a-z0-9-]+)/gi)].map((x) => x[1]))) {
134
+ if (!known.has(name)) out.push(`architecture tab "${t.id}" uses ${name}, which the shell does not define`);
135
+ }
136
+ const hex = svg.replace(/url\(\s*#[^)]*\)/gi, "").replace(/href\s*=\s*["']#[^"']*["']/gi, "").match(/#[0-9a-f]{3,8}\b/gi);
137
+ if (hex) {
138
+ out.push(`architecture tab "${t.id}" uses hex colours (${[...new Set(hex)].slice(0, 3).join(", ")}); use shell tokens so it follows the theme`);
139
+ }
140
+ }
141
+ return out;
142
+ }
123
143
  function ArchitectureModal({ config, onClose }) {
124
144
  const lang = useShellLang();
125
145
  const es = lang === "es";
@@ -179,27 +199,10 @@ function ArchitectureModal({ config, onClose }) {
179
199
  setSvg(raw);
180
200
  return;
181
201
  }
182
- if (cache[raw]) {
183
- setSvg(cache[raw]);
184
- return;
185
- }
202
+ console.error(`[caos-app-shell] architecture tab "${tab.id}": the diagram must be an inline SVG string, not "${raw.slice(0, 40)}"`);
186
203
  setSvg(null);
187
- let cancelled = false;
188
- const baseUrl = import.meta.env?.BASE_URL || "/";
189
- const url = baseUrl + raw.replace(/^\//, "");
190
- fetch(url).then(async (r) => {
191
- if (!r.ok) throw new Error(`${r.status}`);
192
- return r.text();
193
- }).then((text) => {
194
- cache[raw] = text;
195
- if (!cancelled) setSvg(text);
196
- }).catch((e) => {
197
- if (!cancelled) setErr(String(e?.message ?? e));
198
- });
199
- return () => {
200
- cancelled = true;
201
- };
202
- }, [tab]);
204
+ setErr(es ? "diagrama no disponible" : "diagram unavailable");
205
+ }, [tab, es]);
203
206
  useEffect(() => {
204
207
  const element = diagramRef.current?.querySelector("svg");
205
208
  if (!element) return;
@@ -275,7 +278,7 @@ function ArchitectureModal({ config, onClose }) {
275
278
  t.id
276
279
  );
277
280
  }) }),
278
- /* @__PURE__ */ jsxs("div", { id: `${id}-panel`, role: "tabpanel", "aria-labelledby": `${id}-${tab.id}`, "data-arch-lang": es ? "es" : "en", style: { overflowY: "auto", minHeight: 0, padding: "14px 16px 18px" }, children: [
281
+ /* @__PURE__ */ jsxs("div", { id: `${id}-panel`, role: "tabpanel", "aria-labelledby": `${id}-${tab.id}`, "data-arch-lang": es ? "es" : "en", "data-tab": tab.id, "data-state": err ? "missing" : svg === null ? "loading" : "ready", style: { overflowY: "auto", minHeight: 0, padding: "14px 16px 18px" }, children: [
279
282
  body.map((p, i) => /* @__PURE__ */ jsx("p", { style: { color: "var(--color-fg)", fontSize: 13, lineHeight: 1.65, margin: i === 0 ? "0 0 10px" : "10px 0" }, children: p }, i)),
280
283
  err && /* @__PURE__ */ jsxs("div", { style: { color: "var(--color-warn, #f87171)", fontSize: 12 }, children: [
281
284
  "SVG: ",
@@ -283,7 +286,7 @@ function ArchitectureModal({ config, onClose }) {
283
286
  ] }),
284
287
  !err && svg === null && /* @__PURE__ */ jsx("div", { style: { color: "var(--color-fg-faint)", fontSize: 12 }, children: "\u2026" }),
285
288
  svg !== null && /* @__PURE__ */ jsxs(Fragment, { children: [
286
- /* @__PURE__ */ jsx("button", { type: "button", "aria-pressed": fullSize, "aria-controls": `${id}-diagram`, onClick: () => setFullSize((value) => !value), style: { margin: "0 0 8px", fontSize: 12, padding: "5px 10px" }, children: fullSize ? es ? "Ajustar diagrama" : "Fit diagram" : es ? "Leer a tama\xF1o completo" : "Read at full size" }),
289
+ /* @__PURE__ */ jsx("button", { type: "button", "aria-pressed": fullSize, "aria-controls": `${id}-diagram`, onClick: () => setFullSize((value) => !value), className: "caos-arch-toggle", children: fullSize ? es ? "Ajustar diagrama" : "Fit diagram" : es ? "Leer a tama\xF1o completo" : "Read at full size" }),
287
290
  /* @__PURE__ */ jsx("div", { id: `${id}-diagram`, role: "region", "aria-label": es ? "Diagrama de arquitectura" : "Architecture diagram", tabIndex: 0, style: { border: "1px solid var(--color-border)", borderRadius: 8, padding: 8, background: "var(--color-bg, var(--color-surface))", overflow: "auto", maxWidth: "100%" }, children: /* @__PURE__ */ jsx(
288
291
  "div",
289
292
  {
@@ -303,53 +306,102 @@ function ArchitectureModal({ config, onClose }) {
303
306
  }
304
307
  var PERSONAL = "https://fsantibanezleal.github.io";
305
308
  var PORTFOLIO = "https://fasl-work.com";
309
+ var DISPLAY_VERSION = /^\d+\.\d{2}\.\d{3}$/;
310
+ function validate(config) {
311
+ if (!DISPLAY_VERSION.test(config.version)) {
312
+ console.error(`[caos-app-shell] version "${config.version}" is not the display form X.XX.XXX (ADR-0068)`);
313
+ }
314
+ if (!config.license && !config.footer?.license) {
315
+ console.error("[caos-app-shell] ShellConfig.license is required: the footer shows the product licence");
316
+ }
317
+ if (config.visibility !== "public" && config.visibility !== "private") {
318
+ console.error('[caos-app-shell] ShellConfig.visibility is required ("public" or "private")');
319
+ }
320
+ if (config.architecture) {
321
+ for (const problem of validateArchitectureConfig(config.architecture)) console.error(`[caos-app-shell] ${problem}`);
322
+ }
323
+ }
324
+ function Nav({ routes, name, routed, pathname }) {
325
+ const lang = useShellLang();
326
+ const ref = useRef(null);
327
+ useOverflowFade(ref, ".nav-link.active", [lang, pathname]);
328
+ return /* @__PURE__ */ jsx("nav", { className: "main-nav", "aria-label": name, ref, children: routes.map(
329
+ (r) => routed ? /* @__PURE__ */ jsx(
330
+ NavLink,
331
+ {
332
+ to: r.path,
333
+ end: r.path === "/",
334
+ className: ({ isActive }) => isActive ? "nav-link active" : "nav-link",
335
+ children: lang === "es" ? r.es : r.en
336
+ },
337
+ r.path
338
+ ) : /* @__PURE__ */ jsx("a", { href: r.path, className: pathname === r.path ? "nav-link active" : "nav-link", children: lang === "es" ? r.es : r.en }, r.path)
339
+ ) });
340
+ }
341
+ function RoutedPath({ children }) {
342
+ const { pathname } = useLocation();
343
+ return /* @__PURE__ */ jsx(Fragment, { children: children(pathname) });
344
+ }
306
345
  function AppShell({ config, children }) {
346
+ const routed = useInRouterContext();
347
+ useEffect(() => {
348
+ if (!routed) {
349
+ console.error(
350
+ "[caos-app-shell] AppShell is outside a Router context. If a Router is mounted, two copies of react-router are installed: depend on react-router (not react-router-dom) and add resolve.dedupe ['react', 'react-dom', 'react-router'] to vite.config.ts."
351
+ );
352
+ }
353
+ }, [routed]);
354
+ useEffect(() => {
355
+ validate(config);
356
+ }, []);
357
+ if (routed) return /* @__PURE__ */ jsx(RoutedPath, { children: (pathname) => /* @__PURE__ */ jsx(Frame, { config, pathname, routed: true, children }) });
358
+ return /* @__PURE__ */ jsx(Frame, { config, pathname: typeof window !== "undefined" ? window.location.pathname : "/", routed: false, children });
359
+ }
360
+ function Frame({ config, pathname, routed, children }) {
307
361
  const lang = useShellLang();
308
362
  const c = chrome(lang);
309
363
  const routes = config.routes ?? [];
310
364
  const personal = config.links.personal ?? PERSONAL;
311
365
  const portfolio = config.links.portfolio ?? PORTFOLIO;
366
+ const isPublic = config.visibility !== "private";
312
367
  const [archOpen, setArchOpen] = useState(false);
368
+ const footRef = useRef(null);
369
+ useOverflowFade(footRef, null, [lang, pathname]);
370
+ useEffect(() => {
371
+ document.documentElement.lang = lang;
372
+ }, [lang]);
313
373
  const archLabel = lang === "es" ? "Arquitectura / C\xF3mo funciona" : "Architecture / How it works";
314
- const { pathname } = useLocation();
315
- const fixed = config.fixed === true || (config.fixedRoutes ?? []).some((p) => p === pathname || p !== "/" && pathname.startsWith(p.replace(/\/$/, "") + "/"));
316
- return /* @__PURE__ */ jsxs("div", { className: fixed ? "app-shell fixed" : "app-shell", children: [
374
+ const fixed = config.contain === true || config.fixed === true || (config.fixedRoutes ?? []).some((p) => p === pathname || p !== "/" && pathname.startsWith(p.replace(/\/$/, "") + "/"));
375
+ const license = config.license ? pick(config.license, lang) : config.footer?.license?.[lang] ?? "";
376
+ const brand = /* @__PURE__ */ jsxs(Fragment, { children: [
377
+ /* @__PURE__ */ jsx("span", { className: "brand-mark", children: config.product.mark ?? /* @__PURE__ */ jsx(Boxes, { size: 18, "aria-hidden": "true" }) }),
378
+ /* @__PURE__ */ jsx("span", { children: config.product.name })
379
+ ] });
380
+ return /* @__PURE__ */ jsxs("div", { className: config.contain ? "app-shell fixed contain" : fixed ? "app-shell fixed" : "app-shell", children: [
317
381
  /* @__PURE__ */ jsx("header", { className: "site-header", children: /* @__PURE__ */ jsxs("div", { className: "header-inner", children: [
318
- /* @__PURE__ */ jsxs(NavLink, { to: "/", className: "brand", "aria-label": config.product.name, children: [
319
- /* @__PURE__ */ jsx("span", { className: "brand-mark", children: config.product.mark ?? /* @__PURE__ */ jsx(Boxes, { size: 18, "aria-hidden": "true" }) }),
320
- /* @__PURE__ */ jsx("span", { children: config.product.name })
321
- ] }),
322
- routes.length > 1 && /* @__PURE__ */ jsx("nav", { className: "main-nav", "aria-label": config.product.name, children: routes.map((r) => /* @__PURE__ */ jsx(
323
- NavLink,
324
- {
325
- to: r.path,
326
- end: r.path === "/",
327
- onFocus: (event) => event.currentTarget.scrollIntoView({ block: "nearest", inline: "nearest" }),
328
- className: ({ isActive }) => isActive ? "nav-link active" : "nav-link",
329
- children: lang === "es" ? r.es : r.en
330
- },
331
- r.path
332
- )) }),
382
+ 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
+ routes.length > 1 && /* @__PURE__ */ jsx(Nav, { routes, name: config.product.name, routed, pathname }),
333
384
  /* @__PURE__ */ jsxs("div", { className: "header-actions", children: [
334
- /* @__PURE__ */ jsx("a", { className: "icon-btn", href: config.links.github, target: "_blank", rel: "noreferrer noopener", "aria-label": c.github, title: c.github, children: /* @__PURE__ */ jsx(CodeXml, { size: 18, "aria-hidden": "true" }) }),
385
+ isPublic && /* @__PURE__ */ jsx("a", { className: "icon-btn", href: config.links.github, target: "_blank", rel: "noreferrer noopener", "aria-label": c.github, title: c.github, children: /* @__PURE__ */ jsx(CodeXml, { size: 18, "aria-hidden": "true" }) }),
335
386
  /* @__PURE__ */ jsx("a", { className: "icon-btn", href: personal, target: "_blank", rel: "noreferrer noopener", "aria-label": c.personal, title: c.personal, children: /* @__PURE__ */ jsx(Globe, { size: 18, "aria-hidden": "true" }) }),
336
387
  /* @__PURE__ */ jsx("a", { className: "icon-btn", href: portfolio, target: "_blank", rel: "noreferrer noopener", "aria-label": c.portfolio, title: c.portfolio, children: /* @__PURE__ */ jsx(Briefcase, { size: 18, "aria-hidden": "true" }) }),
337
- config.architecture && /* @__PURE__ */ jsx("button", { className: "icon-btn", type: "button", onClick: () => setArchOpen(true), "aria-label": archLabel, title: archLabel, children: /* @__PURE__ */ jsx(Info, { size: 18, "aria-hidden": "true" }) }),
388
+ config.architecture && /* @__PURE__ */ jsx("button", { className: "icon-btn", type: "button", onClick: () => setArchOpen(true), "aria-label": archLabel, title: archLabel, "data-architecture-button": "", children: /* @__PURE__ */ jsx(Info, { size: 18, "aria-hidden": "true" }) }),
338
389
  /* @__PURE__ */ jsx("span", { className: "header-sep", "aria-hidden": "true" }),
339
390
  /* @__PURE__ */ jsx(LanguageToggle, {}),
340
391
  /* @__PURE__ */ jsx(ThemeToggle, {})
341
392
  ] })
342
393
  ] }) }),
343
394
  config.architecture && archOpen && /* @__PURE__ */ jsx(ArchitectureModal, { config: config.architecture, onClose: () => setArchOpen(false) }),
344
- /* @__PURE__ */ jsx("main", { className: "page", children }),
345
- /* @__PURE__ */ jsx("footer", { className: "site-footer", children: /* @__PURE__ */ jsx("div", { className: "footer-inner", children: /* @__PURE__ */ jsxs("div", { className: "footer-meta", children: [
395
+ /* @__PURE__ */ jsx("main", { className: "page", children: /* @__PURE__ */ jsx(PanelBoundary, { panel: "page", children }) }),
396
+ /* @__PURE__ */ jsx("footer", { className: "site-footer", children: /* @__PURE__ */ jsx("div", { className: "footer-inner", children: /* @__PURE__ */ jsxs("div", { className: "footer-meta", ref: footRef, children: [
346
397
  /* @__PURE__ */ jsx("span", { children: config.product.name }),
347
398
  /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
348
399
  /* @__PURE__ */ jsx("span", { children: c.complement }),
349
400
  /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
350
- /* @__PURE__ */ jsx("span", { className: "footer-build", children: /* @__PURE__ */ jsxs("span", { children: [
401
+ /* @__PURE__ */ jsx("span", { className: "footer-build", "data-version": config.version, "data-build": config.build ?? "", children: /* @__PURE__ */ jsxs("span", { children: [
351
402
  c.version,
352
- config.version
403
+ config.version,
404
+ config.build ? ` \xB7 ${config.build}` : ""
353
405
  ] }) }),
354
406
  config.footer?.attribution !== false && /* @__PURE__ */ jsxs(Fragment, { children: [
355
407
  /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
@@ -359,10 +411,14 @@ function AppShell({ config, children }) {
359
411
  /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
360
412
  /* @__PURE__ */ jsx("span", { children: config.footer.provenance[lang] })
361
413
  ] }),
362
- /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
363
- /* @__PURE__ */ jsx("a", { href: config.links.github, target: "_blank", rel: "noreferrer noopener", onFocus: (event) => event.currentTarget.scrollIntoView({ block: "nearest", inline: "nearest" }), children: c.github }),
364
- /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
365
- /* @__PURE__ */ jsx("span", { className: "faint", children: config.footer?.license?.[lang] ?? c.license }),
414
+ isPublic && /* @__PURE__ */ jsxs(Fragment, { children: [
415
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
416
+ /* @__PURE__ */ jsx("a", { href: config.links.github, target: "_blank", rel: "noreferrer noopener", children: c.github })
417
+ ] }),
418
+ license && /* @__PURE__ */ jsxs(Fragment, { children: [
419
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
420
+ /* @__PURE__ */ jsx("span", { className: "faint", children: license })
421
+ ] }),
366
422
  config.footer?.disclaimer && /* @__PURE__ */ jsxs(Fragment, { children: [
367
423
  /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
368
424
  /* @__PURE__ */ jsx("span", { className: "faint", children: config.footer.disclaimer[lang] })
@@ -657,18 +713,20 @@ function CaseSelector(props) {
657
713
  const t = { ...TEXT[lang], ...text };
658
714
  const baseId = useId();
659
715
  const param = typeof deepLink === "string" ? deepLink : "case";
716
+ const linkPending = useRef(Boolean(deepLink));
660
717
  useEffect(() => {
661
- if (!deepLink || typeof window === "undefined") return;
718
+ if (!linkPending.current || typeof window === "undefined" || cases.length === 0) return;
719
+ linkPending.current = false;
662
720
  const fromUrl = readCaseParam(window.location.search, param);
663
721
  if (fromUrl && fromUrl !== selectedId && cases.some((c) => c.id === fromUrl)) onSelect(fromUrl);
664
- }, []);
722
+ }, [cases]);
665
723
  useEffect(() => {
666
- if (!deepLink || typeof window === "undefined") return;
724
+ if (!deepLink || linkPending.current || !selectedId || typeof window === "undefined") return;
667
725
  const next = withCaseParam(window.location.search, selectedId, param);
668
726
  if (next !== window.location.search) {
669
727
  window.history.replaceState(null, "", `${window.location.pathname}${next}${window.location.hash}`);
670
728
  }
671
- }, [deepLink, param, selectedId]);
729
+ }, [deepLink, param, selectedId, cases]);
672
730
  const lanes = source ? sourcesPresent(cases) : [];
673
731
  const visible = casesInSource(cases, source);
674
732
  const groups = groupByCategory(visible);
@@ -693,14 +751,15 @@ function CaseSelector(props) {
693
751
  layout === "select" && /* @__PURE__ */ jsx(
694
752
  "select",
695
753
  {
754
+ "data-control": "case",
696
755
  className: "select cs-select",
697
756
  "aria-label": ariaLabel ?? "Case selector",
698
757
  value: selectedId,
699
758
  onChange: (e) => onSelect(e.target.value),
700
- children: groups.map((g) => /* @__PURE__ */ jsx("optgroup", { label: g.category || " ", children: g.cases.map((c) => /* @__PURE__ */ jsx("option", { value: c.id, disabled: c.disabled, title: caseTooltip(c) || void 0, children: `${c.id} \xB7 ${c.name}` }, c.id)) }, g.category || "_"))
759
+ children: groups.map((g) => /* @__PURE__ */ jsx("optgroup", { label: g.category || " ", children: g.cases.map((c) => /* @__PURE__ */ jsx("option", { value: c.id, disabled: c.disabled, title: caseTooltip(c) || void 0, children: c.name }, c.id)) }, g.category || "_"))
701
760
  }
702
761
  ),
703
- layout === "chips" && groups.map((g) => /* @__PURE__ */ jsxs("div", { className: "cs-group", role: "group", "aria-label": g.category || void 0, children: [
762
+ layout === "chips" && groups.map((g) => /* @__PURE__ */ jsxs("div", { className: "cs-group", role: "group", "aria-label": g.category || void 0, "data-control": "case", children: [
704
763
  g.category && /* @__PURE__ */ jsx("span", { className: "cs-group-label", children: g.category }),
705
764
  /* @__PURE__ */ jsx("div", { className: "cs-chips", children: g.cases.map((c) => {
706
765
  const on = c.id === selectedId;
@@ -715,8 +774,8 @@ function CaseSelector(props) {
715
774
  disabled: c.disabled,
716
775
  title: tip || void 0,
717
776
  onClick: () => onSelect(c.id),
777
+ "data-case": c.id,
718
778
  children: [
719
- /* @__PURE__ */ jsx("span", { className: "cs-chip-id", children: c.id }),
720
779
  /* @__PURE__ */ jsx("span", { className: "cs-chip-name", children: c.name }),
721
780
  /* @__PURE__ */ jsx("span", { className: `cs-kind cs-kind-${caseKindOf(c)}`, "aria-hidden": "true", children: KIND_TAG[caseKindOf(c)] })
722
781
  ]
@@ -729,7 +788,7 @@ function CaseSelector(props) {
729
788
  /* @__PURE__ */ jsxs("span", { className: "badge accent", children: [
730
789
  t.modifiedPrefix,
731
790
  " ",
732
- /* @__PURE__ */ jsx("strong", { children: modified.id })
791
+ /* @__PURE__ */ jsx("strong", { children: modified.name })
733
792
  ] }),
734
793
  onResetToCanonical && /* @__PURE__ */ jsx("button", { type: "button", className: "cs-reset", onClick: onResetToCanonical, children: t.reset })
735
794
  ] })
@@ -738,10 +797,21 @@ function CaseSelector(props) {
738
797
  function caseKindOf(c) {
739
798
  return c.kind ?? "synthetic";
740
799
  }
741
- function Tabs({ tabs, initial, ariaLabel }) {
800
+ var MAX_PEER_TABS = 6;
801
+ function Tabs({ tabs, initial, value, onChange, ariaLabel }) {
742
802
  const baseId = useId();
743
803
  const first = tabs[0]?.id ?? "";
744
- const [active, setActive] = useState(initial ?? first);
804
+ const [own, setOwn] = useState(initial ?? first);
805
+ const active = value ?? own;
806
+ const rowRef = useRef(null);
807
+ useOverflowFade(rowRef, ".tab.active", [active, tabs.length]);
808
+ if (tabs.length > MAX_PEER_TABS) {
809
+ console.error(`[caos-app-shell] ${tabs.length} peer tabs (${ariaLabel ?? "unnamed"}); at most ${MAX_PEER_TABS}, then group (ADR-0071 rule 5)`);
810
+ }
811
+ function select(id) {
812
+ if (value === void 0) setOwn(id);
813
+ onChange?.(id);
814
+ }
745
815
  function onKeyDown(e, idx) {
746
816
  if (e.key !== "ArrowRight" && e.key !== "ArrowLeft" && e.key !== "Home" && e.key !== "End") return;
747
817
  e.preventDefault();
@@ -752,13 +822,14 @@ function Tabs({ tabs, initial, ariaLabel }) {
752
822
  else if (e.key === "End") next = tabs.length - 1;
753
823
  const target = tabs[next];
754
824
  if (target) {
755
- setActive(target.id);
825
+ select(target.id);
756
826
  document.getElementById(`${baseId}-tab-${target.id}`)?.focus();
757
827
  }
758
828
  }
829
+ const current = tabs.find((t) => t.id === active) ?? tabs[0];
759
830
  return /* @__PURE__ */ jsxs("div", { className: "tabs", children: [
760
- /* @__PURE__ */ jsx("div", { className: "tablist", role: "tablist", "aria-label": ariaLabel, children: tabs.map((tab, idx) => {
761
- const selected = tab.id === active;
831
+ /* @__PURE__ */ jsx("div", { className: "tablist", role: "tablist", "aria-label": ariaLabel, ref: rowRef, children: tabs.map((tab, idx) => {
832
+ const selected = tab.id === current?.id;
762
833
  return /* @__PURE__ */ jsx(
763
834
  "button",
764
835
  {
@@ -769,38 +840,44 @@ function Tabs({ tabs, initial, ariaLabel }) {
769
840
  "aria-controls": `${baseId}-panel-${tab.id}`,
770
841
  tabIndex: selected ? 0 : -1,
771
842
  className: selected ? "tab active" : "tab",
772
- onClick: () => setActive(tab.id),
843
+ "data-tab": tab.id,
844
+ onClick: () => select(tab.id),
773
845
  onKeyDown: (e) => onKeyDown(e, idx),
774
846
  children: tab.label
775
847
  },
776
848
  tab.id
777
849
  );
778
850
  }) }),
779
- tabs.map((tab) => /* @__PURE__ */ jsx(
851
+ current && /* @__PURE__ */ jsx(
780
852
  "div",
781
853
  {
782
- id: `${baseId}-panel-${tab.id}`,
854
+ id: `${baseId}-panel-${current.id}`,
783
855
  role: "tabpanel",
784
- "aria-labelledby": `${baseId}-tab-${tab.id}`,
785
- hidden: tab.id !== active,
856
+ "aria-labelledby": `${baseId}-tab-${current.id}`,
786
857
  tabIndex: 0,
787
858
  className: "tabpanel",
788
- children: tab.id === active ? tab.content : null
859
+ "data-panel": current.id,
860
+ children: /* @__PURE__ */ jsx(PanelBoundary, { panel: current.id, children: current.content })
789
861
  },
790
- tab.id
791
- ))
862
+ current.id
863
+ )
792
864
  ] });
793
865
  }
794
- function SubTabs({
795
- tabs,
796
- initial,
797
- ariaLabel,
798
- orientation = "horizontal"
799
- }) {
866
+ function SubTabs({ tabs, initial, value, onChange, ariaLabel, orientation = "horizontal" }) {
800
867
  const baseId = useId();
801
868
  const first = tabs[0]?.id ?? "";
802
- const [active, setActive] = useState(initial ?? first);
869
+ const [own, setOwn] = useState(initial ?? first);
870
+ const active = value ?? own;
803
871
  const vertical = orientation === "vertical";
872
+ const rowRef = useRef(null);
873
+ useOverflowFade(rowRef, ".subtab.active", [active, tabs.length, vertical]);
874
+ if (tabs.length > MAX_PEER_TABS) {
875
+ console.error(`[caos-app-shell] ${tabs.length} peer sub-tabs (${ariaLabel ?? "unnamed"}); at most ${MAX_PEER_TABS}, then group (ADR-0071 rule 5)`);
876
+ }
877
+ function select(id) {
878
+ if (value === void 0) setOwn(id);
879
+ onChange?.(id);
880
+ }
804
881
  function onKeyDown(e, idx) {
805
882
  const fwd = vertical ? "ArrowDown" : "ArrowRight";
806
883
  const back = vertical ? "ArrowUp" : "ArrowLeft";
@@ -813,45 +890,90 @@ function SubTabs({
813
890
  else if (e.key === "End") next = tabs.length - 1;
814
891
  const target = tabs[next];
815
892
  if (target) {
816
- setActive(target.id);
893
+ select(target.id);
817
894
  document.getElementById(`${baseId}-subtab-${target.id}`)?.focus();
818
895
  }
819
896
  }
897
+ const current = tabs.find((t) => t.id === active) ?? tabs[0];
820
898
  return /* @__PURE__ */ jsxs("div", { className: vertical ? "subtabs subtabs-vertical" : "subtabs", children: [
821
- /* @__PURE__ */ jsx("div", { className: "subtablist", role: "tablist", "aria-label": ariaLabel, "aria-orientation": vertical ? "vertical" : "horizontal", children: tabs.map((tab, idx) => {
822
- const selected = tab.id === active;
823
- return /* @__PURE__ */ jsx(
824
- "button",
825
- {
826
- id: `${baseId}-subtab-${tab.id}`,
827
- role: "tab",
828
- type: "button",
829
- "aria-selected": selected,
830
- "aria-controls": `${baseId}-subpanel-${tab.id}`,
831
- tabIndex: selected ? 0 : -1,
832
- className: selected ? "subtab active" : "subtab",
833
- onClick: () => setActive(tab.id),
834
- onKeyDown: (e) => onKeyDown(e, idx),
835
- children: tab.label
836
- },
837
- tab.id
838
- );
839
- }) }),
840
- /* @__PURE__ */ jsx("div", { className: "subtabpanels", children: tabs.map((tab) => /* @__PURE__ */ jsx(
899
+ /* @__PURE__ */ jsx(
841
900
  "div",
842
901
  {
843
- id: `${baseId}-subpanel-${tab.id}`,
902
+ className: "subtablist",
903
+ role: "tablist",
904
+ "aria-label": ariaLabel,
905
+ "aria-orientation": vertical ? "vertical" : "horizontal",
906
+ ref: rowRef,
907
+ children: tabs.map((tab, idx) => {
908
+ const selected = tab.id === current?.id;
909
+ return /* @__PURE__ */ jsx(
910
+ "button",
911
+ {
912
+ id: `${baseId}-subtab-${tab.id}`,
913
+ role: "tab",
914
+ type: "button",
915
+ "aria-selected": selected,
916
+ "aria-controls": `${baseId}-subpanel-${tab.id}`,
917
+ tabIndex: selected ? 0 : -1,
918
+ className: selected ? "subtab active" : "subtab",
919
+ "data-tab": tab.id,
920
+ onClick: () => select(tab.id),
921
+ onKeyDown: (e) => onKeyDown(e, idx),
922
+ children: tab.label
923
+ },
924
+ tab.id
925
+ );
926
+ })
927
+ }
928
+ ),
929
+ /* @__PURE__ */ jsx("div", { className: "subtabpanels", children: current && /* @__PURE__ */ jsx(
930
+ "div",
931
+ {
932
+ id: `${baseId}-subpanel-${current.id}`,
844
933
  role: "tabpanel",
845
- "aria-labelledby": `${baseId}-subtab-${tab.id}`,
846
- hidden: tab.id !== active,
934
+ "aria-labelledby": `${baseId}-subtab-${current.id}`,
847
935
  tabIndex: 0,
848
936
  className: "subtabpanel",
849
- children: tab.id === active ? tab.content : null
937
+ "data-panel": current.id,
938
+ children: /* @__PURE__ */ jsx(PanelBoundary, { panel: current.id, children: current.content })
850
939
  },
851
- tab.id
852
- )) })
940
+ current.id
941
+ ) })
853
942
  ] });
854
943
  }
944
+ function TabGroups({ groups, value, onChange, ariaLabel }) {
945
+ const [own, setOwn] = useState({ group: groups[0]?.id ?? "" });
946
+ const sel = value ?? own;
947
+ const group = groups.find((g) => g.id === sel.group) ?? groups[0];
948
+ const tabOf = (g, wanted) => g.tabs.find((t) => t.id === wanted)?.id ?? g.tabs[0]?.id ?? "";
949
+ function set(next) {
950
+ if (value === void 0) setOwn(next);
951
+ onChange?.(next);
952
+ }
953
+ return /* @__PURE__ */ jsx(
954
+ Tabs,
955
+ {
956
+ ariaLabel,
957
+ value: group?.id,
958
+ onChange: (gid) => {
959
+ const g = groups.find((x) => x.id === gid);
960
+ if (g) set({ group: g.id, tab: tabOf(g) });
961
+ },
962
+ tabs: groups.map((g) => ({
963
+ id: g.id,
964
+ label: g.label,
965
+ content: g.tabs.length === 1 ? g.tabs[0].content : /* @__PURE__ */ jsx(
966
+ SubTabs,
967
+ {
968
+ tabs: g.tabs,
969
+ value: tabOf(g, g.id === group?.id ? sel.tab : void 0),
970
+ onChange: (tid) => set({ group: g.id, tab: tid })
971
+ }
972
+ )
973
+ }))
974
+ }
975
+ );
976
+ }
855
977
  function Callout({
856
978
  variant = "note",
857
979
  title,
@@ -864,6 +986,9 @@ function Callout({
864
986
  ] });
865
987
  }
866
988
  function Equation({ tex, caption }) {
989
+ if (caption === void 0 || caption === null || caption === "") {
990
+ console.error(`[caos-app-shell] Equation without a caption (ADR-0017 s2): ${tex.slice(0, 60)}`);
991
+ }
867
992
  const html = katex.renderToString(tex, { displayMode: true, throwOnError: false });
868
993
  return /* @__PURE__ */ jsxs("div", { className: "equation", children: [
869
994
  /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: html } }),
@@ -883,7 +1008,11 @@ function Figure({ caption, children }) {
883
1008
  var CitationsContext = createContext({ list: [], byId: {} });
884
1009
  function CitationsProvider({ items, children }) {
885
1010
  const byId = {};
886
- for (const c of items) byId[c.id] = c;
1011
+ for (const c of items) {
1012
+ if (byId[c.id]) console.error(`[caos-app-shell] duplicate citation id ${c.id}`);
1013
+ if (!c.doi && !c.url) console.error(`[caos-app-shell] citation ${c.id} has no doi or url (ADR-0017 s4)`);
1014
+ byId[c.id] = c;
1015
+ }
887
1016
  return /* @__PURE__ */ jsx(CitationsContext.Provider, { value: { list: items, byId }, children });
888
1017
  }
889
1018
  function href(c) {
@@ -891,14 +1020,19 @@ function href(c) {
891
1020
  }
892
1021
  function Cite({ id, paren = true }) {
893
1022
  const { byId } = useContext(CitationsContext);
1023
+ const lang = useShellLang();
894
1024
  const c = byId[id];
895
- if (!c) return /* @__PURE__ */ jsxs("cite", { className: "cite-inline", children: [
896
- "[",
897
- id,
898
- "]"
899
- ] });
1025
+ if (!c) {
1026
+ console.error(`[caos-app-shell] Cite of an unknown citation id ${id}`);
1027
+ return /* @__PURE__ */ jsxs("cite", { className: "cite-inline", children: [
1028
+ "[",
1029
+ id,
1030
+ "]"
1031
+ ] });
1032
+ }
900
1033
  const h = href(c);
901
- const label = h ? /* @__PURE__ */ jsx("a", { href: h, target: "_blank", rel: "noreferrer noopener", children: c.label }) : /* @__PURE__ */ jsx("span", { children: c.label });
1034
+ const text = pick(c.label, lang);
1035
+ const label = h ? /* @__PURE__ */ jsx("a", { href: h, target: "_blank", rel: "noreferrer noopener", children: text }) : /* @__PURE__ */ jsx("span", { children: text });
902
1036
  return /* @__PURE__ */ jsxs("cite", { className: "cite-inline", children: [
903
1037
  paren ? "(" : null,
904
1038
  label,
@@ -916,6 +1050,7 @@ function Refs({ ids, label }) {
916
1050
  ] });
917
1051
  }
918
1052
  function ReferenceList({ ids, heading }) {
1053
+ console.error("[caos-app-shell] ReferenceList is banned (ADR-0017 s4): end each section with Refs");
919
1054
  const { list, byId } = useContext(CitationsContext);
920
1055
  const items = ids ? ids.map((k) => byId[k]).filter(Boolean) : list;
921
1056
  return /* @__PURE__ */ jsxs("section", { className: "references", "aria-label": heading ?? "References", children: [
@@ -930,5 +1065,395 @@ function ReferenceList({ ids, heading }) {
930
1065
  }) })
931
1066
  ] });
932
1067
  }
1068
+ function DocPage({ title, lede, wide, children }) {
1069
+ const lang = useShellLang();
1070
+ return /* @__PURE__ */ jsxs("div", { className: wide ? "page-body wide prose caos-doc" : "page-body prose caos-doc", "data-doc-page": "", children: [
1071
+ /* @__PURE__ */ jsxs("div", { className: "page-head", children: [
1072
+ /* @__PURE__ */ jsx("h1", { children: pick(title, lang) }),
1073
+ /* @__PURE__ */ jsx("p", { className: "lede", children: lede })
1074
+ ] }),
1075
+ children
1076
+ ] });
1077
+ }
1078
+ function DocSection({ title, id, refs, noRefsReason, children }) {
1079
+ const lang = useShellLang();
1080
+ const hasRefs = Boolean(refs && refs.length > 0);
1081
+ return /* @__PURE__ */ jsxs(
1082
+ "section",
1083
+ {
1084
+ id,
1085
+ className: "caos-doc-section",
1086
+ "data-doc-section": "",
1087
+ "data-no-refs-reason": hasRefs ? void 0 : pick(noRefsReason, lang) || "missing",
1088
+ children: [
1089
+ /* @__PURE__ */ jsx("h2", { children: pick(title, lang) }),
1090
+ children,
1091
+ hasRefs && /* @__PURE__ */ jsx(Refs, { ids: refs, label: lang === "es" ? "Referencias" : "References" })
1092
+ ]
1093
+ }
1094
+ );
1095
+ }
1096
+ function isSections(rail) {
1097
+ return Array.isArray(rail) && rail.every((s) => typeof s === "object" && s !== null && "id" in s && "content" in s);
1098
+ }
1099
+ function WorkbenchLayout({ rail, railHead, children, railLabel, className }) {
1100
+ const lang = useShellLang();
1101
+ const sections = isSections(rail) ? rail : null;
1102
+ const [active, setActive] = useState(sections?.[0]?.id ?? "");
1103
+ const baseId = useId();
1104
+ const label = pick(railLabel ?? { en: "Controls", es: "Controles" }, lang);
1105
+ const current = sections?.find((s) => s.id === active) ?? sections?.[0];
1106
+ return /* @__PURE__ */ jsxs("div", { className: ["page-body", "wide", "caos-wb", className].filter(Boolean).join(" "), "data-workbench": "", children: [
1107
+ /* @__PURE__ */ jsxs("aside", { className: "caos-wb-rail", "aria-label": label, "data-rail": "", children: [
1108
+ railHead && /* @__PURE__ */ jsx("div", { className: "caos-wb-rail-head", children: railHead }),
1109
+ sections ? /* @__PURE__ */ jsxs(Fragment, { children: [
1110
+ /* @__PURE__ */ jsx("div", { className: "caos-wb-rail-sections", role: "tablist", "aria-label": label, children: sections.map((s) => /* @__PURE__ */ jsx(
1111
+ "button",
1112
+ {
1113
+ type: "button",
1114
+ role: "tab",
1115
+ id: `${baseId}-rs-${s.id}`,
1116
+ "aria-selected": s.id === current?.id,
1117
+ "aria-controls": `${baseId}-rp-${s.id}`,
1118
+ className: s.id === current?.id ? "chip on" : "chip",
1119
+ onClick: () => setActive(s.id),
1120
+ children: pick(s.label, lang)
1121
+ },
1122
+ s.id
1123
+ )) }),
1124
+ current && /* @__PURE__ */ jsx(
1125
+ "div",
1126
+ {
1127
+ className: "caos-wb-rail-panel",
1128
+ role: "tabpanel",
1129
+ id: `${baseId}-rp-${current.id}`,
1130
+ "aria-labelledby": `${baseId}-rs-${current.id}`,
1131
+ children: current.content
1132
+ }
1133
+ )
1134
+ ] }) : /* @__PURE__ */ jsx("div", { className: "caos-wb-rail-panel", children: rail })
1135
+ ] }),
1136
+ /* @__PURE__ */ jsx("section", { className: "caos-wb-main", "data-instrument": "", children })
1137
+ ] });
1138
+ }
1139
+ var WorkbenchStateContext = createContext(null);
1140
+ function useWorkbenchState() {
1141
+ return useContext(WorkbenchStateContext);
1142
+ }
1143
+ function stable(value) {
1144
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "undefined";
1145
+ if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
1146
+ const obj = value;
1147
+ return `{${Object.keys(obj).sort().map((k) => `${JSON.stringify(k)}:${stable(obj[k])}`).join(",")}}`;
1148
+ }
1149
+ function makeStateKey(parts) {
1150
+ const text = stable(parts);
1151
+ let h = 2166136261;
1152
+ for (let i = 0; i < text.length; i += 1) {
1153
+ h ^= text.charCodeAt(i);
1154
+ h = Math.imul(h, 16777619) >>> 0;
1155
+ }
1156
+ return h.toString(16).padStart(8, "0");
1157
+ }
1158
+ function isStale(dataKey, current) {
1159
+ return Boolean(current && dataKey !== void 0 && dataKey !== current.stateKey);
1160
+ }
1161
+ var LANE_LABELS = {
1162
+ live: { en: "Live in your browser", es: "En vivo en tu navegador" },
1163
+ replay: { en: "Replay of the offline result", es: "Reproduce el resultado offline" },
1164
+ offline: { en: "Offline only", es: "Solo offline" }
1165
+ };
1166
+ var PROVENANCE_LABELS = {
1167
+ real: { en: "Real data", es: "Datos reales" },
1168
+ synthetic: { en: "Synthetic data", es: "Datos sint\xE9ticos" },
1169
+ published: { en: "Published reference", es: "Referencia publicada" }
1170
+ };
1171
+ function LaneBadge({ lane, provenance, label }) {
1172
+ const lang = useShellLang();
1173
+ return /* @__PURE__ */ jsxs("span", { className: "caos-badges", children: [
1174
+ /* @__PURE__ */ jsx("span", { className: `badge caos-lane caos-lane-${lane}`, "data-lane": lane, children: pick(label ?? LANE_LABELS[lane], lang) }),
1175
+ provenance && /* @__PURE__ */ jsx("span", { className: `badge caos-prov caos-prov-${provenance}`, "data-provenance": provenance, children: pick(PROVENANCE_LABELS[provenance], lang) })
1176
+ ] });
1177
+ }
1178
+ function VariantBar({ variants, activeId, onSelect, title, lane }) {
1179
+ const lang = useShellLang();
1180
+ const active = variants.find((v) => v.id === activeId) ?? variants[0];
1181
+ const shownLane = active?.lane ?? lane;
1182
+ return /* @__PURE__ */ jsxs("div", { className: "caos-variant-bar", "data-variant-bar": "", "data-variant-count": variants.length, children: [
1183
+ /* @__PURE__ */ jsxs("div", { className: "caos-variant-head", children: [
1184
+ /* @__PURE__ */ jsxs("span", { className: "caos-variant-title", children: [
1185
+ pick(title ?? { en: "Variants", es: "Variantes" }, lang),
1186
+ " (",
1187
+ variants.length,
1188
+ ")"
1189
+ ] }),
1190
+ shownLane && /* @__PURE__ */ jsx(LaneBadge, { lane: shownLane })
1191
+ ] }),
1192
+ /* @__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(
1193
+ "button",
1194
+ {
1195
+ type: "button",
1196
+ role: "radio",
1197
+ "aria-checked": v.id === active?.id,
1198
+ disabled: v.disabled,
1199
+ className: v.id === active?.id ? "chip on" : "chip",
1200
+ onClick: () => onSelect(v.id),
1201
+ children: pick(v.label, lang)
1202
+ },
1203
+ v.id
1204
+ )) }),
1205
+ active?.note && /* @__PURE__ */ jsx("p", { className: "caos-variant-note", children: pick(active.note, lang) })
1206
+ ] });
1207
+ }
1208
+ var MAX_WORKBENCH_GROUPS = MAX_PEER_TABS;
1209
+ function panel(id, lane, provenance, content) {
1210
+ return /* @__PURE__ */ jsx("div", { className: "caos-cw-panel", "data-group": id, "data-lane": lane, "data-provenance": provenance, children: content });
1211
+ }
1212
+ function CaseWorkbench(props) {
1213
+ const { caseId, cases, source, controls, replayOnly, variants, rail, railLabel, groups, compare, context, group, onGroupChange, ariaLabel } = props;
1214
+ const lang = useShellLang();
1215
+ const variantId = variants?.activeId;
1216
+ const stateKey = useMemo(
1217
+ () => makeStateKey({ caseId, variantId: variantId ?? null, source: source ?? null, controls: controls ?? {} }),
1218
+ [caseId, variantId, source, controls]
1219
+ );
1220
+ const tabs = groups.map((g) => ({ id: g.id, label: pick(g.label, lang), content: panel(g.id, g.lane, g.provenance, g.content) }));
1221
+ if (compare) {
1222
+ tabs.push({
1223
+ id: "compare",
1224
+ label: pick(compare.label ?? { en: "Compare variants", es: "Comparar variantes" }, lang),
1225
+ content: panel("compare", compare.lane, compare.provenance, compare.content)
1226
+ });
1227
+ }
1228
+ tabs.push({
1229
+ id: "context",
1230
+ label: pick(context.label ?? { en: "Context", es: "Contexto" }, lang),
1231
+ content: /* @__PURE__ */ jsx("div", { className: "prose caos-cw-context", children: context.content })
1232
+ });
1233
+ if (tabs.length > MAX_WORKBENCH_GROUPS) {
1234
+ throw new Error(
1235
+ `CaseWorkbench: ${tabs.length} groups; at most ${MAX_WORKBENCH_GROUPS} peers, then group by question (ADR-0071 rule 5)`
1236
+ );
1237
+ }
1238
+ const railHead = cases || variants ? /* @__PURE__ */ jsxs(Fragment, { children: [
1239
+ cases && /* @__PURE__ */ jsx(CaseSelector, { lang, ...cases }),
1240
+ variants && /* @__PURE__ */ jsx(VariantBar, { ...variants })
1241
+ ] }) : null;
1242
+ return /* @__PURE__ */ jsx(WorkbenchStateContext.Provider, { value: { stateKey, caseId, variantId, source }, children: /* @__PURE__ */ jsx(WorkbenchLayout, { rail: rail ?? null, railHead, railLabel, children: /* @__PURE__ */ jsx(
1243
+ "div",
1244
+ {
1245
+ className: "caos-cw",
1246
+ "data-case-workbench": "",
1247
+ "data-group-count": tabs.length,
1248
+ "data-case": caseId,
1249
+ "data-variant": variantId ?? "",
1250
+ "data-source": source ?? "",
1251
+ "data-state-key": stateKey,
1252
+ "data-replay-only": replayOnly ? "1" : void 0,
1253
+ children: /* @__PURE__ */ jsx(
1254
+ Tabs,
1255
+ {
1256
+ tabs,
1257
+ value: group,
1258
+ onChange: onGroupChange,
1259
+ ariaLabel: pick(ariaLabel ?? { en: "Case views", es: "Vistas del caso" }, lang)
1260
+ }
1261
+ )
1262
+ }
1263
+ ) }) });
1264
+ }
1265
+ function toneOf(it) {
1266
+ if (it.tone) return it.tone;
1267
+ if (!it.better || it.value === null || it.value === void 0 || !Number.isFinite(it.value)) return "neutral";
1268
+ const v = it.value;
1269
+ const higher = it.better === "higher";
1270
+ if (it.bad !== void 0 && (higher ? v <= it.bad : v >= it.bad)) return "bad";
1271
+ if (it.good !== void 0 && (higher ? v >= it.good : v <= it.good)) return "good";
1272
+ if (it.good !== void 0 || it.bad !== void 0) return "warn";
1273
+ return "neutral";
1274
+ }
1275
+ function checkItem(it, where) {
1276
+ if (!it.unit && !it.unitless && it.text === void 0) {
1277
+ console.error(`[caos-app-shell] readout "${where}" has no unit; give a unit or set unitless`);
1278
+ }
1279
+ if (typeof it.value === "number" && Number.isNaN(it.value)) {
1280
+ console.error(`[caos-app-shell] readout "${where}" received NaN; pass null when the value is not available`);
1281
+ }
1282
+ }
1283
+ function Readout({ items, title, lane, provenance, dataKey }) {
1284
+ const lang = useShellLang();
1285
+ const ws = useWorkbenchState();
1286
+ const stale = isStale(dataKey, ws);
1287
+ if (!lane || !provenance) console.error("[caos-app-shell] Readout needs lane and provenance");
1288
+ return /* @__PURE__ */ jsxs(
1289
+ "div",
1290
+ {
1291
+ className: stale ? "caos-readout caos-stale" : "caos-readout",
1292
+ "data-readout": "",
1293
+ "data-lane": lane,
1294
+ "data-provenance": provenance,
1295
+ "data-state-key": dataKey ?? "",
1296
+ "data-stale": stale ? "1" : "0",
1297
+ children: [
1298
+ /* @__PURE__ */ jsxs("div", { className: "caos-readout-head", children: [
1299
+ title && /* @__PURE__ */ jsx("p", { className: "caos-readout-title", children: pick(title, lang) }),
1300
+ /* @__PURE__ */ jsx(LaneBadge, { lane, provenance })
1301
+ ] }),
1302
+ /* @__PURE__ */ jsx("dl", { children: items.map((it, i) => {
1303
+ const label = pick(it.label, lang);
1304
+ checkItem(it, label);
1305
+ const shown = it.text !== void 0 ? pick(it.text, lang) : formatNumber(it.value, lang, it.format ?? {});
1306
+ return /* @__PURE__ */ jsxs("div", { className: `caos-readout-row tone-${toneOf(it)}`, title: pick(it.hint, lang) || void 0, children: [
1307
+ /* @__PURE__ */ jsx("dt", { children: label }),
1308
+ /* @__PURE__ */ jsxs("dd", { children: [
1309
+ /* @__PURE__ */ jsx("span", { className: "caos-readout-value", children: shown }),
1310
+ it.unit && it.text === void 0 && /* @__PURE__ */ jsxs("span", { className: "caos-readout-unit", children: [
1311
+ " ",
1312
+ pick(it.unit, lang)
1313
+ ] })
1314
+ ] })
1315
+ ] }, i);
1316
+ }) }),
1317
+ stale && /* @__PURE__ */ jsx("p", { className: "caos-stale-note", children: lang === "es" ? "Recalculando para la selecci\xF3n actual" : "Recomputing for the current selection" })
1318
+ ]
1319
+ }
1320
+ );
1321
+ }
1322
+ function Gauge({ title, value, min, max, zones, unit, unitless, format }) {
1323
+ const lang = useShellLang();
1324
+ checkItem({ value, unit, unitless }, pick(title, lang));
1325
+ const span = max - min || 1;
1326
+ const finite = value !== null && value !== void 0 && Number.isFinite(value);
1327
+ const pos = (v) => `${Math.min(100, Math.max(0, (v - min) / span * 100))}%`;
1328
+ const fmt = (v) => formatNumber(v, lang, format ?? {});
1329
+ return /* @__PURE__ */ jsxs("div", { className: "caos-gauge", "data-gauge": "", children: [
1330
+ /* @__PURE__ */ jsx("p", { className: "caos-gauge-title", children: pick(title, lang) }),
1331
+ /* @__PURE__ */ jsxs(
1332
+ "div",
1333
+ {
1334
+ className: "caos-gauge-track",
1335
+ role: "meter",
1336
+ "aria-valuemin": min,
1337
+ "aria-valuemax": max,
1338
+ "aria-valuenow": finite ? value : void 0,
1339
+ "aria-label": pick(title, lang),
1340
+ children: [
1341
+ zones.map((z, i) => /* @__PURE__ */ jsx("span", { className: `caos-gauge-zone tone-${z.tone}`, style: { left: pos(z.from), width: `calc(${pos(z.to)} - ${pos(z.from)})` } }, i)),
1342
+ finite && /* @__PURE__ */ jsx("span", { className: "caos-gauge-needle", style: { left: pos(value) } })
1343
+ ]
1344
+ }
1345
+ ),
1346
+ /* @__PURE__ */ jsxs("div", { className: "caos-gauge-scale", children: [
1347
+ /* @__PURE__ */ jsx("span", { children: fmt(min) }),
1348
+ /* @__PURE__ */ jsxs("span", { className: "caos-gauge-value", children: [
1349
+ fmt(value),
1350
+ finite && unit ? ` ${pick(unit, lang)}` : ""
1351
+ ] }),
1352
+ /* @__PURE__ */ jsx("span", { children: fmt(max) })
1353
+ ] }),
1354
+ /* @__PURE__ */ jsx("div", { className: "caos-gauge-legend", children: zones.map((z, i) => /* @__PURE__ */ jsxs("span", { children: [
1355
+ /* @__PURE__ */ jsx("i", { className: `dot tone-${z.tone}`, "aria-hidden": "true" }),
1356
+ pick(z.label, lang)
1357
+ ] }, i)) })
1358
+ ] });
1359
+ }
1360
+ function Verdict(props) {
1361
+ const lang = useShellLang();
1362
+ let tone;
1363
+ let text;
1364
+ if ("code" in props) {
1365
+ const m = props.messages[props.code];
1366
+ if (!m) console.error(`[caos-app-shell] Verdict code "${props.code}" has no message`);
1367
+ tone = m?.tone ?? "neutral";
1368
+ text = m ? pick(m.text, lang) : lang === "es" ? "estado desconocido" : "unknown status";
1369
+ } else {
1370
+ tone = props.tone;
1371
+ text = pick(props.verdict, lang);
1372
+ }
1373
+ return /* @__PURE__ */ jsxs("div", { className: `caos-verdict tone-${tone}${props.compact ? " compact" : ""}`, "data-verdict": tone, children: [
1374
+ /* @__PURE__ */ jsx("p", { className: "caos-verdict-title", children: pick(props.title, lang) }),
1375
+ /* @__PURE__ */ jsx("p", { className: "caos-verdict-text", children: text }),
1376
+ props.children
1377
+ ] });
1378
+ }
1379
+ function PlotCard({ title, lane, provenance, dataKey, actions, note, fill, children }) {
1380
+ const lang = useShellLang();
1381
+ const ws = useWorkbenchState();
1382
+ const stale = isStale(dataKey, ws);
1383
+ const name = pick(title, lang);
1384
+ if (!lane || !provenance) console.error(`[caos-app-shell] PlotCard "${name}" needs lane and provenance`);
1385
+ return /* @__PURE__ */ jsxs(
1386
+ "figure",
1387
+ {
1388
+ className: ["caos-plot", fill ? "fill" : "", stale ? "caos-stale" : ""].filter(Boolean).join(" "),
1389
+ "data-plot": name,
1390
+ "data-lane": lane,
1391
+ "data-provenance": provenance,
1392
+ "data-state-key": dataKey ?? "",
1393
+ "data-stale": stale ? "1" : "0",
1394
+ children: [
1395
+ /* @__PURE__ */ jsxs("figcaption", { className: "caos-plot-head", children: [
1396
+ /* @__PURE__ */ jsx("span", { className: "caos-plot-title", children: name }),
1397
+ /* @__PURE__ */ jsxs("span", { className: "caos-plot-actions", children: [
1398
+ actions,
1399
+ /* @__PURE__ */ jsx(LaneBadge, { lane, provenance })
1400
+ ] })
1401
+ ] }),
1402
+ /* @__PURE__ */ jsxs("div", { className: "caos-plot-body", children: [
1403
+ /* @__PURE__ */ jsx(PanelBoundary, { panel: name, children }),
1404
+ stale && /* @__PURE__ */ jsx("div", { className: "caos-stale-overlay", role: "status", children: lang === "es" ? "Recalculando para la selecci\xF3n actual" : "Recomputing for the current selection" })
1405
+ ] }),
1406
+ note && /* @__PURE__ */ jsx("p", { className: "caos-plot-note", children: pick(note, lang) })
1407
+ ]
1408
+ }
1409
+ );
1410
+ }
1411
+ function Knob({ id, label, value, min, max, step, unit, hint, format, disabled, onChange }) {
1412
+ const lang = useShellLang();
1413
+ const inputId = useId();
1414
+ return /* @__PURE__ */ jsxs("div", { className: "caos-knob", "data-control": id, title: pick(hint, lang) || void 0, children: [
1415
+ /* @__PURE__ */ jsxs("label", { htmlFor: inputId, className: "caos-knob-head", children: [
1416
+ /* @__PURE__ */ jsx("span", { children: pick(label, lang) }),
1417
+ /* @__PURE__ */ jsxs("span", { className: "caos-knob-value", children: [
1418
+ formatNumber(value, lang, format ?? {}),
1419
+ unit ? ` ${pick(unit, lang)}` : ""
1420
+ ] })
1421
+ ] }),
1422
+ /* @__PURE__ */ jsx(
1423
+ "input",
1424
+ {
1425
+ id: inputId,
1426
+ className: "range",
1427
+ type: "range",
1428
+ min,
1429
+ max,
1430
+ step,
1431
+ value,
1432
+ disabled,
1433
+ onChange: (e) => onChange(Number(e.target.value))
1434
+ }
1435
+ )
1436
+ ] });
1437
+ }
1438
+ function ChipGroup({ id, label, options, value, onChange }) {
1439
+ const lang = useShellLang();
1440
+ return /* @__PURE__ */ jsxs("div", { className: "caos-chipgroup", "data-control": id, children: [
1441
+ /* @__PURE__ */ jsx("span", { className: "caos-chipgroup-label", children: pick(label, lang) }),
1442
+ /* @__PURE__ */ jsx("div", { className: "caos-chip-row", role: "radiogroup", "aria-label": pick(label, lang), children: options.map((o) => /* @__PURE__ */ jsx(
1443
+ "button",
1444
+ {
1445
+ type: "button",
1446
+ role: "radio",
1447
+ "aria-checked": o.id === value,
1448
+ className: o.id === value ? "chip on" : "chip",
1449
+ disabled: o.disabled,
1450
+ title: pick(o.hint, lang) || void 0,
1451
+ onClick: () => onChange(o.id),
1452
+ children: pick(o.label, lang)
1453
+ },
1454
+ o.id
1455
+ )) })
1456
+ ] });
1457
+ }
933
1458
 
934
- export { AppShell, ArchitectureModal, Callout, CaseSelector, CitationsProvider, Cite, Equation, Figure, FocusShell, InlineMath, LanguageToggle, ReferenceList, Refs, SubTabs, Tabs, ThemeToggle, WorkbenchShell, applyTheme, caseKind, caseTooltip, casesInSource, createVizLoop, groupByCategory, readCaseParam, readTheme, sourcesPresent, useLangStore, usePausedViz, useShellLang, useThemeStore, withCaseParam };
1459
+ 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 };