@fasl-work/caos-app-shell 0.19.12 → 0.20.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, useStageSize, barExtent, clipExtent, allIntegers, bandsOf, windowRange, windowZoomed, unusedLegendTones, NOT_AVAILABLE, formatNumber, widestLabel, fitLabel, GROUP_MARGIN, axisTitleFits, wrapLines, formatTicks, niceTicks, textWidth, repeatedLabels, chartSummary, NBSP, CHART_ROLE, ChartSummary, ChartDataTable, ResetZoomButton, memberSlots, wheelFactor, pinchState, DRAG_THRESHOLD, panRange, brushWindow, keyAction, KEY_ZOOM, stepIndex, KEY_PAN, stepWindow, roundWindow, zoomRange, categoryAt, memberAt } from './chunk-FXPSZD6X.js';
2
- export { NBSP, NOT_AVAILABLE, SCIENTIFIC_BELOW, SHELL_TOKENS, Stage, applyTheme, fitLabel, fontFamily, formatNumber, formatTicks, niceStep, niceTicks, pick, readTheme, resolveToken, textWidth, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens, widestLabel } from './chunk-FXPSZD6X.js';
1
+ import { useThemeStore, useShellLang, useLangStore, SHELL_TOKENS, pick, useStageSize, barExtent, clipExtent, allIntegers, bandsOf, windowRange, windowZoomed, unusedLegendTones, NOT_AVAILABLE, formatNumber, widestLabel, fitLabel, GROUP_MARGIN, axisTitleFits, wrapLines, formatTicks, niceTicks, textWidth, repeatedLabels, chartSummary, NBSP, CHART_ROLE, ChartSummary, ChartDataTable, ResetZoomButton, memberSlots, wheelFactor, pinchState, DRAG_THRESHOLD, panRange, brushWindow, keyAction, KEY_ZOOM, stepIndex, KEY_PAN, stepWindow, roundWindow, zoomRange, categoryAt, memberAt } from './chunk-FPPJQC2J.js';
2
+ export { NBSP, NOT_AVAILABLE, SCIENTIFIC_BELOW, SHELL_TOKENS, Stage, applyTheme, fitLabel, fontFamily, formatNumber, formatTicks, niceStep, niceTicks, pick, readTheme, resolveToken, textWidth, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens, widestLabel } from './chunk-FPPJQC2J.js';
3
3
  import { APP_ROUTE, LANDING_ROUTE } from './chunk-33WTUQ7Q.js';
4
4
  export { APP_ROUTE, LANDING_ROUTE, LANG_STORAGE_KEY, STANDARD_ROUTES, THEME_BOOT_SCRIPT, THEME_STORAGE_KEY } from './chunk-33WTUQ7Q.js';
5
5
  import { createContext, Component, useState, useRef, useId, useEffect, useLayoutEffect, useCallback, useContext, useMemo, Children, isValidElement } from 'react';
@@ -7,6 +7,8 @@ import { useInRouterContext, NavLink, useLocation, Navigate, Link } from 'react-
7
7
  import { Sun, Moon, Languages, Boxes, CodeXml, Globe, Briefcase, Info, ChevronLeft, ChevronRight, Maximize2, RotateCw } from 'lucide-react';
8
8
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
9
9
  import katex from 'katex';
10
+ import { createStore } from 'zustand/vanilla';
11
+ import { useStore } from 'zustand';
10
12
 
11
13
  // src/lib/chrome.ts
12
14
  var CHROME = {
@@ -4145,5 +4147,302 @@ function ViewsRow({ children, shares }) {
4145
4147
  return /* @__PURE__ */ jsx("div", { className: "caos-views-col", "data-share": share ?? 1, style: share && share > 0 ? { flexGrow: share } : void 0, children: child }, i);
4146
4148
  }) });
4147
4149
  }
4150
+ var DEFAULT_LABELS = {
4151
+ steps: { en: "Story steps", es: "Pasos de la historia" },
4152
+ previous: { en: "Previous step", es: "Paso anterior" },
4153
+ next: { en: "Next step", es: "Paso siguiente" },
4154
+ play: { en: "Play story", es: "Reproducir historia" },
4155
+ pause: { en: "Pause story", es: "Pausar historia" },
4156
+ exit: { en: "Return to exploration", es: "Volver a explorar" }
4157
+ };
4158
+ function StoryFrame({ steps, activeId, onStepChange, onExit, labels = {}, autoAdvanceMs = 5e3, className }) {
4159
+ const lang = useShellLang();
4160
+ const [playing, setPlaying] = useState(false);
4161
+ const activeIndex = Math.max(0, steps.findIndex((step) => step.id === activeId));
4162
+ const active = steps[activeIndex];
4163
+ const copy = useMemo(() => ({ ...DEFAULT_LABELS, ...labels }), [labels]);
4164
+ useEffect(() => {
4165
+ if (steps.length < 3 || steps.length > 6) console.error("[caos-app-shell] A showcase story has 3 to 6 steps (ADR-0082 rule 3)");
4166
+ }, [steps.length]);
4167
+ useEffect(() => {
4168
+ if (!playing || steps.length < 2) return;
4169
+ const timer = window.setTimeout(() => {
4170
+ if (activeIndex >= steps.length - 1) setPlaying(false);
4171
+ else onStepChange(steps[activeIndex + 1].id);
4172
+ }, Math.max(1e3, autoAdvanceMs));
4173
+ return () => window.clearTimeout(timer);
4174
+ }, [activeIndex, autoAdvanceMs, onStepChange, playing, steps]);
4175
+ function move(delta) {
4176
+ setPlaying(false);
4177
+ const next = Math.max(0, Math.min(steps.length - 1, activeIndex + delta));
4178
+ if (steps[next]) onStepChange(steps[next].id);
4179
+ }
4180
+ function onKeyDown(event) {
4181
+ if (event.target !== event.currentTarget) return;
4182
+ if (event.key === "ArrowLeft") {
4183
+ event.preventDefault();
4184
+ move(-1);
4185
+ } else if (event.key === "ArrowRight") {
4186
+ event.preventDefault();
4187
+ move(1);
4188
+ } else if (event.key === "Home") {
4189
+ event.preventDefault();
4190
+ setPlaying(false);
4191
+ onStepChange(steps[0].id);
4192
+ } else if (event.key === "End") {
4193
+ event.preventDefault();
4194
+ setPlaying(false);
4195
+ onStepChange(steps[steps.length - 1].id);
4196
+ } else if (event.key === " ") {
4197
+ event.preventDefault();
4198
+ setPlaying((value) => !value);
4199
+ }
4200
+ }
4201
+ if (!active) return null;
4202
+ return /* @__PURE__ */ jsxs("section", { className: ["caos-story", "page-body", "wide", className].filter(Boolean).join(" "), "data-story": "", "data-story-step": active.id, "data-step-accent": active.accent, "aria-label": pick(copy.steps, lang), tabIndex: 0, onKeyDown, children: [
4203
+ /* @__PURE__ */ jsxs("header", { className: "caos-story-header", children: [
4204
+ /* @__PURE__ */ jsxs("div", { className: "caos-story-heading", children: [
4205
+ /* @__PURE__ */ jsxs("span", { className: "caos-story-verb", children: [
4206
+ active.number.toString().padStart(2, "0"),
4207
+ " \xB7 ",
4208
+ pick(active.verb, lang)
4209
+ ] }),
4210
+ /* @__PURE__ */ jsx("h1", { className: "caos-story-headline", "data-story-headline": "", children: pick(active.headline, lang) }),
4211
+ /* @__PURE__ */ jsx("p", { title: pick(active.summary, lang), children: pick(active.summary, lang) })
4212
+ ] }),
4213
+ /* @__PURE__ */ jsxs("div", { className: "caos-story-actions", children: [
4214
+ /* @__PURE__ */ jsxs("div", { className: "caos-story-controls", "aria-label": pick(copy.steps, lang), children: [
4215
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => move(-1), disabled: activeIndex === 0, children: pick(copy.previous, lang) }),
4216
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => {
4217
+ setPlaying((value) => !value);
4218
+ }, "aria-pressed": playing, children: pick(playing ? copy.pause : copy.play, lang) }),
4219
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => move(1), disabled: activeIndex === steps.length - 1, children: pick(copy.next, lang) })
4220
+ ] }),
4221
+ onExit && /* @__PURE__ */ jsx("button", { type: "button", className: "caos-story-exit", onClick: () => {
4222
+ setPlaying(false);
4223
+ onExit();
4224
+ }, children: pick(copy.exit, lang) })
4225
+ ] })
4226
+ ] }),
4227
+ /* @__PURE__ */ jsx("nav", { className: "caos-story-stepper", role: "tablist", "aria-label": pick(copy.steps, lang), children: steps.map((step) => /* @__PURE__ */ jsxs(Link, { role: "tab", "aria-selected": step.id === active.id, "aria-current": step.id === active.id ? "step" : void 0, to: step.href, "data-story-link": step.id, className: step.id === active.id ? "caos-story-step is-active" : "caos-story-step", onClick: () => {
4228
+ setPlaying(false);
4229
+ onStepChange(step.id);
4230
+ }, children: [
4231
+ /* @__PURE__ */ jsx("span", { className: "caos-story-step-number", children: step.number }),
4232
+ /* @__PURE__ */ jsx("span", { className: "caos-story-step-verb", title: pick(step.verb, lang), children: pick(step.verb, lang) }),
4233
+ /* @__PURE__ */ jsx("span", { className: "caos-story-step-headline", title: pick(step.headline, lang), children: pick(step.headline, lang) })
4234
+ ] }, step.id)) }),
4235
+ /* @__PURE__ */ jsxs("div", { className: "caos-story-layout", "data-stage-layout": "", "data-story-transition": active.id, children: [
4236
+ /* @__PURE__ */ jsx("section", { className: "caos-story-stage", "data-stage": "", "data-drawn": "1", "aria-label": pick(active.verb, lang), onPointerDown: () => setPlaying(false), onKeyDownCapture: (event) => {
4237
+ if (event.target !== event.currentTarget) setPlaying(false);
4238
+ }, children: active.stage }),
4239
+ /* @__PURE__ */ jsx("aside", { className: "caos-story-panel", "data-stage-panel": "", "aria-label": pick(active.verb, lang), onPointerDown: () => setPlaying(false), onKeyDownCapture: (event) => {
4240
+ if (event.target !== event.currentTarget) setPlaying(false);
4241
+ }, children: active.panel })
4242
+ ] }, active.id)
4243
+ ] });
4244
+ }
4245
+ var overlaps = (a, b) => a.left < b.left + b.width && a.left + a.width > b.left && a.top < b.top + b.height && a.top + a.height > b.top;
4246
+ var coversAnchor = (box, x, y, clearance = 8) => x >= box.left - clearance && x <= box.left + box.width + clearance && y >= box.top - clearance && y <= box.top + box.height + clearance;
4247
+ function layoutSceneCallouts(items, width, height, compact = false) {
4248
+ const w = compact ? 142 : 184;
4249
+ const h = compact ? 56 : 68;
4250
+ const margin = 8;
4251
+ const boxes = [];
4252
+ for (const item of items) {
4253
+ const x = Math.max(0, Math.min(width, item.anchor.x * width));
4254
+ const y = Math.max(0, Math.min(height, item.anchor.y * height));
4255
+ const d = compact ? 82 : 112;
4256
+ const candidates = [
4257
+ [x + d, y - h / 2],
4258
+ [x - d - w, y - h / 2],
4259
+ [x + d, y - h / 2 - h - 12],
4260
+ [x - d - w, y - h / 2 - h - 12],
4261
+ [x - w / 2, y - d - h],
4262
+ [x - w / 2, y + d]
4263
+ ].map(([left, top]) => ({ id: item.id, left: Math.max(margin, Math.min(width - w - margin, left)), top: Math.max(margin, Math.min(height - h - margin, top)), width: w, height: h, anchorX: x, anchorY: y }));
4264
+ const ranked = candidates.map((candidate) => ({ candidate, score: boxes.reduce((score, box) => score + (overlaps(candidate, box) ? 1e6 : 0), 0) + items.reduce((score, subject) => score + (coversAnchor(candidate, subject.anchor.x * width, subject.anchor.y * height) ? 1e7 : 0), 0) + Math.hypot(candidate.left + w / 2 - x, candidate.top + h / 2 - y) }));
4265
+ ranked.sort((a, b) => a.score - b.score);
4266
+ boxes.push(ranked[0].candidate);
4267
+ }
4268
+ return boxes;
4269
+ }
4270
+ function layoutCalloutHeader(items, width, height, compact = false) {
4271
+ const w = compact ? 164 : 184, h = compact ? 54 : 68, margin = 8, gap = compact ? 6 : 8;
4272
+ const columns = width < 520 ? 2 : Math.max(1, Math.min(items.length, Math.floor((width - margin * 2 + gap) / (w + gap))));
4273
+ return items.map((item, index) => {
4274
+ const column = index % columns, row = Math.floor(index / columns), count = Math.min(columns, items.length - row * columns);
4275
+ const cell = (width - margin * 2 - gap * (count - 1)) / count;
4276
+ return { id: item.id, left: margin + column * (cell + gap) + (cell - w) / 2, top: margin + row * (h + gap), width: w, height: h, anchorX: item.anchor.x * width, anchorY: item.anchor.y * height };
4277
+ });
4278
+ }
4279
+ function SceneCallouts({ items, className, placement = "anchored" }) {
4280
+ const lang = useShellLang();
4281
+ const host = useRef(null);
4282
+ const [size, setSize] = useState({ width: 0, height: 0 });
4283
+ useEffect(() => {
4284
+ const node = host.current;
4285
+ if (!node) return;
4286
+ const observer = new ResizeObserver((entries) => {
4287
+ const rect = entries[0]?.contentRect;
4288
+ if (rect) setSize({ width: rect.width, height: rect.height });
4289
+ });
4290
+ observer.observe(node);
4291
+ return () => observer.disconnect();
4292
+ }, []);
4293
+ const boxes = useMemo(() => {
4294
+ if (placement === "anchored") return layoutSceneCallouts(items, size.width, size.height, size.width < 640);
4295
+ return layoutCalloutHeader(items, size.width, size.height, size.width < 640);
4296
+ }, [items, placement, size]);
4297
+ return /* @__PURE__ */ jsxs("div", { ref: host, className: ["caos-callout-layer", className].filter(Boolean).join(" "), "aria-label": lang === "es" ? "Valores fijados a los componentes" : "Values anchored to components", "data-caos-callout-layer": "", "data-placement": placement, children: [
4298
+ /* @__PURE__ */ jsx("svg", { className: "caos-callout-leaders", "aria-hidden": "true", viewBox: `0 0 ${Math.max(1, size.width)} ${Math.max(1, size.height)}`, preserveAspectRatio: "none", children: boxes.map((box) => /* @__PURE__ */ jsx("line", { x1: box.anchorX, y1: box.anchorY, x2: box.left + box.width / 2, y2: box.top + box.height / 2, "data-caos-callout-leader": box.id }, box.id)) }),
4299
+ boxes.map((box) => {
4300
+ const item = items.find((candidate) => candidate.id === box.id);
4301
+ return /* @__PURE__ */ jsxs("article", { className: "caos-scene-callout", "data-caos-callout": item.id, "data-callout-value": item.value, "data-callout-unit": pick(item.unit, lang), "data-callout-state": item.state, "data-callout-source": pick(item.source, lang), "data-callout-anchor-x": item.anchor.x, "data-callout-anchor-y": item.anchor.y, style: { left: box.left, top: box.top, width: box.width, minHeight: box.height }, children: [
4302
+ /* @__PURE__ */ jsx("span", { className: "caos-callout-eyebrow", title: pick(item.eyebrow, lang), children: pick(item.eyebrow, lang) }),
4303
+ /* @__PURE__ */ jsxs("strong", { title: `${item.value} ${pick(item.unit, lang)}`, children: [
4304
+ item.value,
4305
+ " ",
4306
+ /* @__PURE__ */ jsx("small", { children: pick(item.unit, lang) })
4307
+ ] }),
4308
+ item.trend && /* @__PURE__ */ jsx("span", { className: `caos-callout-trend trend-${item.trend}`, "aria-label": item.trend, children: item.trend === "up" ? "\u2191" : item.trend === "down" ? "\u2193" : "\u2192" }),
4309
+ /* @__PURE__ */ jsx("small", { className: "caos-callout-source", title: pick(item.source, lang), children: pick(item.source, lang) })
4310
+ ] }, box.id);
4311
+ })
4312
+ ] });
4313
+ }
4314
+ function Scene3DFrame({ label, legendLabel = "Scene legend", children, controls, legend, status, className }) {
4315
+ return /* @__PURE__ */ jsxs("section", { className: ["caos-scene3d-frame", className].filter(Boolean).join(" "), "data-caos-scene3d-frame": "", children: [
4316
+ controls && /* @__PURE__ */ jsx("div", { className: "caos-scene3d-controls", "data-caos-scene3d-controls": "", children: controls }),
4317
+ /* @__PURE__ */ jsxs("div", { className: "caos-scene3d-viewport", role: "region", "aria-label": label, "data-caos-scene3d-viewport": "", children: [
4318
+ /* @__PURE__ */ jsx("div", { className: "caos-scene3d-renderer", "data-caos-scene3d-renderer": "", children }),
4319
+ status && /* @__PURE__ */ jsx("div", { className: "caos-scene3d-status", role: "status", children: status }),
4320
+ legend && /* @__PURE__ */ jsx("aside", { className: "caos-scene3d-legend", "aria-label": legendLabel, children: legend })
4321
+ ] })
4322
+ ] });
4323
+ }
4324
+ var LinkedViewContext = createContext(null);
4325
+ function createLinkedViewStore(initial = {}) {
4326
+ return createStore((set) => ({ selectedId: initial.selectedId ?? null, cursor: initial.cursor ?? null, seriesId: initial.seriesId ?? null, origin: "external", select: (selectedId, origin) => set({ selectedId, origin }), scrub: (cursor) => set({ cursor }), showSeries: (seriesId) => set({ seriesId }) }));
4327
+ }
4328
+ function LinkedViewProvider({ children, initial }) {
4329
+ const storeRef = useRef(null);
4330
+ if (!storeRef.current) storeRef.current = createLinkedViewStore(initial);
4331
+ return /* @__PURE__ */ jsx(LinkedViewContext.Provider, { value: storeRef.current, children });
4332
+ }
4333
+ function useLinkedView(selector) {
4334
+ const store = useContext(LinkedViewContext);
4335
+ if (!store) throw new Error("useLinkedView must be inside LinkedViewProvider");
4336
+ return useStore(store, selector);
4337
+ }
4338
+ function useLinkedItem(id, side) {
4339
+ const selected = useLinkedView((state) => state.selectedId === id);
4340
+ const select = useLinkedView((state) => state.select);
4341
+ return {
4342
+ "data-linked-id": id,
4343
+ "data-linked-view": side,
4344
+ "data-linked-selected": selected ? "true" : "false",
4345
+ "aria-pressed": selected,
4346
+ onClick: () => select(id, side)
4347
+ };
4348
+ }
4349
+ function useLinkedCursor() {
4350
+ const cursor = useLinkedView((state) => state.cursor);
4351
+ const scrub = useLinkedView((state) => state.scrub);
4352
+ const onChange = (event) => scrub(Number(event.currentTarget.value));
4353
+ return { cursor, scrub, inputProps: { "data-linked-cursor": "", value: cursor ?? 0, onChange }, markerProps: { "data-linked-cursor-marker": "", "data-linked-cursor-position": cursor ?? "" } };
4354
+ }
4355
+
4356
+ // src/workbench/motion.ts
4357
+ function prefersReducedMotion() {
4358
+ return typeof matchMedia !== "undefined" && matchMedia("(prefers-reduced-motion: reduce)").matches;
4359
+ }
4360
+ async function runStagedTransition(stages, options = {}) {
4361
+ const reduced = options.reducedMotion ?? prefersReducedMotion();
4362
+ for (const [index, stage] of stages.entries()) {
4363
+ if (options.signal?.aborted) return;
4364
+ options.onStage?.(index);
4365
+ stage.run();
4366
+ if (!reduced && (stage.durationMs ?? 240) > 0) await new Promise((resolve2) => {
4367
+ const timer = setTimeout(resolve2, Math.min(stage.durationMs ?? 240, 1500));
4368
+ options.signal?.addEventListener("abort", () => {
4369
+ clearTimeout(timer);
4370
+ resolve2();
4371
+ }, { once: true });
4372
+ });
4373
+ }
4374
+ }
4375
+ function flyCamera(frame, from, to, options = {}) {
4376
+ const duration = options.reducedMotion ?? prefersReducedMotion() ? 0 : Math.min(1500, Math.max(600, options.durationMs ?? 900));
4377
+ if (!duration) {
4378
+ frame.setPosition(...to.position);
4379
+ frame.lookAt(...to.target);
4380
+ options.onFrame?.(1);
4381
+ return Promise.resolve();
4382
+ }
4383
+ return new Promise((resolve2) => {
4384
+ const start = typeof performance !== "undefined" ? performance.now() : Date.now();
4385
+ const tick = (now) => {
4386
+ if (options.signal?.aborted) {
4387
+ resolve2();
4388
+ return;
4389
+ }
4390
+ const t2 = Math.max(0, Math.min(1, (now - start) / duration));
4391
+ const eased = t2 * t2 * (3 - 2 * t2);
4392
+ const mix = (a, b) => a + (b - a) * eased;
4393
+ frame.setPosition(...from.position.map((v, i) => mix(v, to.position[i])));
4394
+ frame.lookAt(...from.target.map((v, i) => mix(v, to.target[i])));
4395
+ options.onFrame?.(t2);
4396
+ if (t2 < 1) requestAnimationFrame(tick);
4397
+ else resolve2();
4398
+ };
4399
+ requestAnimationFrame(tick);
4400
+ });
4401
+ }
4402
+
4403
+ // src/workbench/scene3d.ts
4404
+ function cameraForBounds(bounds, aspect, fovDegrees = 42, margin = 1.22) {
4405
+ const center = {
4406
+ x: (bounds.min.x + bounds.max.x) / 2,
4407
+ y: (bounds.min.y + bounds.max.y) / 2,
4408
+ z: (bounds.min.z + bounds.max.z) / 2
4409
+ };
4410
+ const size = {
4411
+ x: Math.max(0.01, bounds.max.x - bounds.min.x),
4412
+ y: Math.max(0.01, bounds.max.y - bounds.min.y),
4413
+ z: Math.max(0.01, bounds.max.z - bounds.min.z)
4414
+ };
4415
+ const vertical = Math.max(size.y, size.z / Math.max(0.1, aspect));
4416
+ const distance = vertical / 2 / Math.tan(Math.max(10, Math.min(100, fovDegrees)) * Math.PI / 360) * margin;
4417
+ const horizontal = Math.max(size.x, size.z) * 0.76;
4418
+ return { target: [center.x, center.y, center.z], position: [center.x + distance * 0.65, center.y + distance * 0.48, center.z + Math.max(distance, horizontal)], distance };
4419
+ }
4420
+ function flowSpeed(rate, referenceRate) {
4421
+ if (!Number.isFinite(rate) || !Number.isFinite(referenceRate) || rate <= 0 || referenceRate <= 0) return 0;
4422
+ return Math.min(1.25, 0.16 + 0.32 * Math.log1p(rate / referenceRate));
4423
+ }
4424
+ function explodedOffsets(parts, scale = 1) {
4425
+ const finiteScale = Number.isFinite(scale) ? Math.max(0, scale) : 0;
4426
+ const center = parts.reduce((sum, part) => ({ x: sum.x + part.center.x, y: sum.y + part.center.y, z: sum.z + part.center.z }), { x: 0, y: 0, z: 0 });
4427
+ if (parts.length) {
4428
+ center.x /= parts.length;
4429
+ center.y /= parts.length;
4430
+ center.z /= parts.length;
4431
+ }
4432
+ return Object.fromEntries(parts.map(({ id, center: p }) => {
4433
+ const dx = p.x - center.x, dy = p.y - center.y, dz = p.z - center.z;
4434
+ const magnitude = Math.hypot(dx, dy, dz) || 1;
4435
+ return [id, { x: dx / magnitude * finiteScale, y: dy / magnitude * finiteScale, z: dz / magnitude * finiteScale }];
4436
+ }));
4437
+ }
4438
+ function detailTier(distance) {
4439
+ if (!Number.isFinite(distance) || distance < 0) return "high";
4440
+ if (distance < 20) return "high";
4441
+ if (distance < 65) return "medium";
4442
+ return "low";
4443
+ }
4444
+ function sceneEnvironment(theme) {
4445
+ return theme === "dark" ? { background: "#101a22", ground: "#202b33", key: { color: "#fff0d5", intensity: 2.1, castShadow: true }, fill: { color: "#98c5db", intensity: 0.8 }, ambient: { color: "#c9d7df", intensity: 1.1 } } : { background: "#e9eef0", ground: "#d6dee1", key: { color: "#fff8e9", intensity: 1.7, castShadow: true }, fill: { color: "#8eabb9", intensity: 0.65 }, ambient: { color: "#ffffff", intensity: 1.35 } };
4446
+ }
4148
4447
 
4149
- export { AppShell, ArchitectureModal, BREAKPOINTS, BarChart, Callout, CardDeck, CaseSelector, CaseWorkbench, ChipGroup, CitationsProvider, Cite, DECK_TEXT, DOC_AREAS, DocPage, DocReferences, DocRefsScope, DocSection, EXECUTIVE_SCHEMA_VERSION, Equation, ExecutiveView, FLIP_LIMITS, FLIP_TEXT, FOCUS_PARAM, FOCUS_TEXT, FOUR_CS, Figure, FlipCard, FocusShell, Gauge, Glossary, InlineMath, Knob, Landing, LaneBadge, LanguageToggle, MAX_PEER_TABS, MAX_WORKBENCH_GROUPS, PanelBoundary, PlotCard, Question, Readout, ReferenceList, Refs, Stat, SubTabs, SurfacePage, TabGroups, Tabs, ThemeToggle, VariantBar, Verdict, ViewsRow, WorkbenchLayout, WorkbenchShell, WorkbenchStateContext, caseKind, caseTooltip, casesInSource, createVizLoop, deckPosition, deckWindow, firstAppearance, groupByCategory, isStale, makeStateKey, perPageFor, readCaseParam, removeQueryParam, replaceQueryParam, sourcesPresent, useFocusMode, usePausedViz, useUrlView, useWorkbenchState, validateArchitectureConfig, validateExecutive, viewParamOf, withCaseParam };
4448
+ export { AppShell, ArchitectureModal, BREAKPOINTS, BarChart, Callout, CardDeck, CaseSelector, CaseWorkbench, ChipGroup, CitationsProvider, Cite, DECK_TEXT, DOC_AREAS, DocPage, DocReferences, DocRefsScope, DocSection, EXECUTIVE_SCHEMA_VERSION, Equation, ExecutiveView, FLIP_LIMITS, FLIP_TEXT, FOCUS_PARAM, FOCUS_TEXT, FOUR_CS, Figure, FlipCard, FocusShell, Gauge, Glossary, InlineMath, Knob, Landing, LaneBadge, LanguageToggle, LinkedViewProvider, MAX_PEER_TABS, MAX_WORKBENCH_GROUPS, PanelBoundary, PlotCard, Question, Readout, ReferenceList, Refs, Scene3DFrame, SceneCallouts, Stat, StoryFrame, SubTabs, SurfacePage, TabGroups, Tabs, ThemeToggle, VariantBar, Verdict, ViewsRow, WorkbenchLayout, WorkbenchShell, WorkbenchStateContext, cameraForBounds, caseKind, caseTooltip, casesInSource, createLinkedViewStore, createVizLoop, deckPosition, deckWindow, detailTier, explodedOffsets, firstAppearance, flowSpeed, flyCamera, groupByCategory, isStale, layoutSceneCallouts, makeStateKey, perPageFor, prefersReducedMotion, readCaseParam, removeQueryParam, replaceQueryParam, runStagedTransition, sceneEnvironment, sourcesPresent, useFocusMode, useLinkedCursor, useLinkedItem, useLinkedView, usePausedViz, useUrlView, useWorkbenchState, validateArchitectureConfig, validateExecutive, viewParamOf, withCaseParam };
@@ -86,7 +86,7 @@ declare function repeatedLabels(labels: readonly (string | null | undefined)[]):
86
86
  * defined renders as nothing, silently (failure class 25 of the 2026-10-04 history). A unit test keeps this list
87
87
  * equal to the stylesheet.
88
88
  */
89
- 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"];
89
+ 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-step-1", "--color-step-2", "--color-step-3", "--color-step-4", "--color-step-5", "--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"];
90
90
  type ShellToken = (typeof SHELL_TOKENS)[number];
91
91
  /** A colour token: what a chart series, a tone or a canvas colour may name. */
92
92
  type ShellColorToken = Extract<ShellToken, `--color-${string}`>;
package/gate/inpage.mjs CHANGED
@@ -7,6 +7,34 @@
7
7
  export function installLib() {
8
8
  const G = (window.__caosGate = window.__caosGate || {});
9
9
  G.raf = 0;
10
+ const storyPerformance = { lcp: null, events: [] };
11
+ try {
12
+ new PerformanceObserver((list) => {
13
+ for (const entry of list.getEntries()) storyPerformance.lcp = Math.max(storyPerformance.lcp ?? 0, entry.startTime + entry.duration);
14
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
15
+ } catch { /* Chromium reference runner supports LCP; G21 reports it unavailable if not. */ }
16
+ try {
17
+ new PerformanceObserver((list) => {
18
+ storyPerformance.events.push(...list.getEntries().filter((entry) => entry.interactionId > 0).map((entry) => ({ name: entry.name, duration: entry.duration })));
19
+ }).observe({ type: 'event', buffered: true, durationThreshold: 16 });
20
+ } catch { /* G21 reports no interaction samples as an unmeasured requirement. */ }
21
+ G.resetStoryInteractions = () => { storyPerformance.events = []; };
22
+ G.storyPerformanceFacts = () => ({
23
+ lcp: storyPerformance.lcp,
24
+ events: storyPerformance.events.slice(),
25
+ assets3d: performance.getEntriesByType('resource').filter((entry) => /\.(?:glb|gltf|bin|ktx2|basis|draco|obj|mtl|stl|fbx)(?:$|\?)/i.test(entry.name))
26
+ .map((entry) => ({ name: entry.name, bytes: entry.transferSize || entry.encodedBodySize || 0 })),
27
+ });
28
+ G.sampleStoryFrames = (duration = 1600) => new Promise((resolve) => {
29
+ const times = [];
30
+ const start = performance.now();
31
+ const tick = (now) => { times.push(now); if (now - start < duration) requestAnimationFrame(tick); else {
32
+ const deltas = times.slice(1).map((time, index) => time - times[index]).filter((delta) => delta > 0).sort((a, b) => a - b);
33
+ const percentile = (q) => deltas.length ? deltas[Math.min(deltas.length - 1, Math.floor(q * (deltas.length - 1)))] : null;
34
+ resolve({ frames: deltas.length, median: percentile(.5) ? 1000 / percentile(.5) : null, p05: percentile(.95) ? 1000 / percentile(.95) : null });
35
+ } };
36
+ requestAnimationFrame(tick);
37
+ });
10
38
  // The smallest rendered size of an illustration label (EXEC_LIMITS.minLabelPx of gate/areas.mjs; test/executive.test.ts keeps them equal).
11
39
  const EXEC_MIN_LABEL_PX = 9;
12
40
  /** Local text bounds mapped into the outer illustration's SVG user space, not screen pixels.
@@ -1514,6 +1542,46 @@ export function installLib() {
1514
1542
  };
1515
1543
  };
1516
1544
 
1545
+ /** ADR-0082 showcase story, its docked stage/panel and the rendered geometry of its anchored callouts (G21). */
1546
+ G.showcaseFacts = () => {
1547
+ const story = document.querySelector('[data-story]');
1548
+ const box = (element) => {
1549
+ if (!element) return null;
1550
+ const r = element.getBoundingClientRect();
1551
+ return { left: r.left, top: r.top, right: r.right, bottom: r.bottom, width: r.width, height: r.height };
1552
+ };
1553
+ const stage = story?.querySelector('[data-stage]');
1554
+ const layout = story?.querySelector('[data-stage-layout]');
1555
+ const callouts = [...(stage?.querySelectorAll('[data-caos-callout]') ?? [])].filter(visible).map((element) => ({
1556
+ id: element.getAttribute('data-caos-callout'), value: element.getAttribute('data-callout-value'), unit: element.getAttribute('data-callout-unit'),
1557
+ state: element.getAttribute('data-callout-state'), source: element.getAttribute('data-callout-source'),
1558
+ anchor: { x: Number(element.getAttribute('data-callout-anchor-x')), y: Number(element.getAttribute('data-callout-anchor-y')) }, box: box(element),
1559
+ }));
1560
+ const links = [...(story?.querySelectorAll('[data-story-link]') ?? [])].filter(visible).map((element) => ({ id: element.getAttribute('data-story-link'), href: element.getAttribute('href'), active: element.getAttribute('aria-selected') === 'true' }));
1561
+ const linked = [...(story?.querySelectorAll('[data-linked-id][data-linked-view]') ?? [])].filter(visible).map((element) => ({ id: element.getAttribute('data-linked-id'), view: element.getAttribute('data-linked-view'), selected: element.getAttribute('data-linked-selected') === 'true', box: box(element) }));
1562
+ const cursors = [...(story?.querySelectorAll('[data-linked-cursor-marker]') ?? [])].filter(visible).map((element) => element.getAttribute('data-linked-cursor-position'));
1563
+ const cursorInputs = [...(story?.querySelectorAll('[data-linked-cursor]') ?? [])].filter(visible).map((element) => ({ value: element.value ?? element.getAttribute('value') ?? '', type: element.type ?? '' }));
1564
+ const headline = story?.querySelector('[data-story-headline]');
1565
+ return {
1566
+ present: Boolean(story),
1567
+ step: story?.getAttribute('data-story-step') ?? null,
1568
+ count: links.length,
1569
+ links,
1570
+ linked,
1571
+ cursors,
1572
+ cursorInputs,
1573
+ headline: headline?.textContent?.trim() ?? '',
1574
+ stage: box(stage),
1575
+ panel: box(story?.querySelector('[data-stage-panel]')),
1576
+ layout: box(layout),
1577
+ calls: callouts,
1578
+ headlineWords: (headline?.textContent?.trim() ?? '').split(/\s+/).filter(Boolean).length,
1579
+ motion: (() => { const stageMotion = stage?.parentElement?.getAttribute('data-story-transition'); const target = story?.querySelector('[data-story-transition] > .caos-story-stage'); if (!target) return null; const style = getComputedStyle(target); return { key: stageMotion, animationName: style.animationName, animationDuration: style.animationDuration, animationDelay: style.animationDelay, opacity: style.opacity, transform: style.transform }; })(),
1580
+ viewport: { width: innerWidth, height: innerHeight },
1581
+ scroll: { width: document.documentElement.scrollWidth, height: document.documentElement.scrollHeight },
1582
+ };
1583
+ };
1584
+
1517
1585
  /** Animation frames and DOM mutations over a window at rest (G8). */
1518
1586
  G.idle = (ms) =>
1519
1587
  new Promise((done) => {