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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,7 +57,7 @@ const config: ShellConfig = {
57
57
  (labelled groups, `Synthetic | Real | Uploaded` source control, locked-knobs explanation, divergence
58
58
  badge, `?case=` deep-linking).
59
59
  - **Animated views:** drive every canvas/3D loop through `usePausedViz` (default paused, run-once,
60
- halt on a hidden tab) — never call `requestAnimationFrame` directly.
60
+ halt on a hidden tab), never call `requestAnimationFrame` directly.
61
61
 
62
62
  ## Exports
63
63
 
package/dist/index.d.ts CHANGED
@@ -131,6 +131,22 @@ interface WorkbenchShellProps {
131
131
  /** Shared CAOS frame for authenticated consoles with desktop sidebar and mobile bottom navigation. */
132
132
  declare function WorkbenchShell({ brand, routes, navigationLabel, children, sidebarFooter, headerLead, headerActions, overlays, mainId, className, }: WorkbenchShellProps): react.JSX.Element;
133
133
 
134
+ interface FocusShellProps {
135
+ stage: ReactNode;
136
+ rail: ReactNode;
137
+ title: string;
138
+ description: string;
139
+ hud: Array<{
140
+ label: string;
141
+ value: string;
142
+ }>;
143
+ onExit: () => void;
144
+ exitLabel: string;
145
+ stageLabel: string;
146
+ }
147
+ /** Full-viewport, scenario-scoped analysis frame. The product supplies only its instrument and controls. */
148
+ declare function FocusShell({ stage, rail, title, description, hud, onExit, exitLabel, stageLabel }: FocusShellProps): react.JSX.Element;
149
+
134
150
  /** Light/dark icon toggle. Shows the icon of the theme you would switch TO. */
135
151
  declare function ThemeToggle(): react.JSX.Element;
136
152
 
@@ -167,13 +183,13 @@ interface VizLoopOptions {
167
183
  /** Auto-complete the pass once `elapsed` reaches this many ms (a hard cap so a frame that never
168
184
  * returns false still stops). The final frame is drawn clamped to exactly this value. */
169
185
  durationMs?: number;
170
- /** Fired once each time the loop stops on its own (a completed pass with loop=false) — not on a
186
+ /** Fired once each time the loop stops on its own (a completed pass with loop=false), not on a
171
187
  * manual pause and not on a visibility halt. */
172
188
  onComplete?: () => void;
173
189
  /** Notified whenever the running state changes, so a React wrapper can mirror it into state. */
174
190
  onPlayingChange?: (playing: boolean) => void;
175
191
  }
176
- /** Injected side effects — real ones in the hook, fakes in tests. The frame timestamp comes from
192
+ /** Injected side effects, real ones in the hook, fakes in tests. The frame timestamp comes from
177
193
  * the rAF callback argument, so no separate clock is needed. */
178
194
  interface VizLoopDeps {
179
195
  raf: (cb: (now: number) => void) => number;
@@ -206,7 +222,7 @@ interface PausedVizController {
206
222
  restart: () => void;
207
223
  }
208
224
  interface UsePausedVizOptions extends VizLoopOptions {
209
- /** Begin playing on mount. Default false — the mandatory no-autoplay default: a page must never
225
+ /** Begin playing on mount. Default false, the mandatory no-autoplay default: a page must never
210
226
  * burn CPU unattended. Opt in only for a view the user explicitly expects to move on arrival. */
211
227
  autoStart?: boolean;
212
228
  }
@@ -224,7 +240,7 @@ declare function usePausedViz(frame: VizFrame, options?: UsePausedVizOptions): P
224
240
 
225
241
  type CaseKind = 'synthetic' | 'real' | 'uploaded';
226
242
  interface CaseDef {
227
- /** stable slug — the value used in the ?case= deep link and reported to onSelect. */
243
+ /** stable slug, the value used in the ?case= deep link and reported to onSelect. */
228
244
  id: string;
229
245
  /** short human name shown ON the chip next to the id (never demoted to a tooltip-only string). */
230
246
  name: string;
@@ -232,9 +248,9 @@ interface CaseDef {
232
248
  category?: string;
233
249
  /** which source lane this case belongs to. Defaults to 'synthetic'. */
234
250
  kind?: CaseKind;
235
- /** validation anchor (e.g. "published optimum 26,086,899") — surfaced in the chip tooltip. */
251
+ /** validation anchor (e.g. "published optimum 26,086,899"), surfaced in the chip tooltip. */
236
252
  anchor?: string;
237
- /** expected-range hint — surfaced in the chip tooltip. */
253
+ /** expected-range hint, surfaced in the chip tooltip. */
238
254
  expectedBand?: string;
239
255
  disabled?: boolean;
240
256
  }
@@ -272,7 +288,7 @@ interface CaseSelectorProps {
272
288
  selectedId: string;
273
289
  onSelect: (id: string) => void;
274
290
  /** When provided, renders the first-level source control and filters cases to the active lane.
275
- * Controlled — pair with onSourceChange. Omit for a single-lane deck (no source control). */
291
+ * Controlled, pair with onSourceChange. Omit for a single-lane deck (no source control). */
276
292
  source?: CaseKind;
277
293
  onSourceChange?: (k: CaseKind) => void;
278
294
  /** Overrides the default locked-knobs explanation shown for a non-synthetic source. */
@@ -301,7 +317,7 @@ interface CaseSelectorProps {
301
317
  layout?: 'chips' | 'select';
302
318
  }
303
319
  /**
304
- * CaseSelector v2 — the shared source + case picker. Renders (1) an optional first-level source
320
+ * CaseSelector v2, the shared source + case picker. Renders (1) an optional first-level source
305
321
  * segmented control (Synthetic | Real | Uploaded, only the lanes present), (2) a labelled group per
306
322
  * case category, each case a chip showing "ID · name" with its validation anchor as the tooltip and
307
323
  * a source tag, (3) a locked-knobs explanation when a non-synthetic source is active, and (4) a
@@ -391,4 +407,4 @@ declare function ReferenceList({ ids, heading }: {
391
407
  heading?: string;
392
408
  }): react.JSX.Element;
393
409
 
394
- export { AppShell, type ArchTab, type ArchitectureConfig, ArchitectureModal, Callout, type CaseDef, type CaseGroup, type CaseKind, CaseSelector, type CaseSelectorProps, type CaseSelectorText, type Citation, CitationsProvider, Cite, Equation, Figure, InlineMath, type Lang, LanguageToggle, type PausedVizController, ReferenceList, Refs, type ShellConfig, type ShellRoute, type SubTabDef, SubTabs, type TabDef, Tabs, type Theme, ThemeToggle, type UsePausedVizOptions, type VizFrame, type VizLoop, type VizLoopDeps, type VizLoopOptions, type WorkbenchRoute, WorkbenchShell, type WorkbenchShellProps, applyTheme, caseKind, caseTooltip, casesInSource, createVizLoop, groupByCategory, readCaseParam, readTheme, sourcesPresent, useLangStore, usePausedViz, useShellLang, useThemeStore, withCaseParam };
410
+ export { AppShell, type ArchTab, type ArchitectureConfig, ArchitectureModal, Callout, type CaseDef, type CaseGroup, type CaseKind, CaseSelector, type CaseSelectorProps, type CaseSelectorText, type Citation, CitationsProvider, Cite, Equation, Figure, FocusShell, type FocusShellProps, InlineMath, type Lang, LanguageToggle, type PausedVizController, ReferenceList, Refs, type ShellConfig, type ShellRoute, type SubTabDef, SubTabs, type TabDef, Tabs, type Theme, ThemeToggle, type UsePausedVizOptions, type VizFrame, type VizLoop, type VizLoopDeps, type VizLoopOptions, type WorkbenchRoute, WorkbenchShell, type WorkbenchShellProps, applyTheme, caseKind, caseTooltip, casesInSource, createVizLoop, groupByCategory, readCaseParam, readTheme, sourcesPresent, useLangStore, usePausedViz, useShellLang, useThemeStore, withCaseParam };
package/dist/index.js CHANGED
@@ -324,6 +324,7 @@ function AppShell({ config, children }) {
324
324
  {
325
325
  to: r.path,
326
326
  end: r.path === "/",
327
+ onFocus: (event) => event.currentTarget.scrollIntoView({ block: "nearest", inline: "nearest" }),
327
328
  className: ({ isActive }) => isActive ? "nav-link active" : "nav-link",
328
329
  children: lang === "es" ? r.es : r.en
329
330
  },
@@ -359,7 +360,7 @@ function AppShell({ config, children }) {
359
360
  /* @__PURE__ */ jsx("span", { children: config.footer.provenance[lang] })
360
361
  ] }),
361
362
  /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
362
- /* @__PURE__ */ jsx("a", { href: config.links.github, target: "_blank", rel: "noreferrer noopener", children: c.github }),
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 }),
363
364
  /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
364
365
  /* @__PURE__ */ jsx("span", { className: "faint", children: config.footer?.license?.[lang] ?? c.license }),
365
366
  config.footer?.disclaimer && /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -418,6 +419,27 @@ function WorkbenchShell({
418
419
  overlays
419
420
  ] });
420
421
  }
422
+ function FocusShell({ stage, rail, title, description, hud, onExit, exitLabel, stageLabel }) {
423
+ return /* @__PURE__ */ jsxs("main", { className: "caos-focus-shell", children: [
424
+ /* @__PURE__ */ jsxs("section", { className: "caos-focus-stage", "aria-label": stageLabel, children: [
425
+ /* @__PURE__ */ jsx("div", { className: "caos-focus-instrument", children: stage }),
426
+ /* @__PURE__ */ jsxs("div", { className: "caos-focus-label", children: [
427
+ /* @__PURE__ */ jsx("strong", { children: title }),
428
+ /* @__PURE__ */ jsx("span", { children: description })
429
+ ] }),
430
+ /* @__PURE__ */ jsx("div", { className: "caos-focus-hud", "aria-label": "HUD", children: hud.map((item) => /* @__PURE__ */ jsxs("div", { children: [
431
+ /* @__PURE__ */ jsx("strong", { children: item.value }),
432
+ /* @__PURE__ */ jsx("span", { children: item.label })
433
+ ] }, item.label)) }),
434
+ /* @__PURE__ */ jsxs("div", { className: "caos-focus-actions", children: [
435
+ /* @__PURE__ */ jsx(LanguageToggle, {}),
436
+ /* @__PURE__ */ jsx(ThemeToggle, {}),
437
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onExit, children: exitLabel })
438
+ ] })
439
+ ] }),
440
+ /* @__PURE__ */ jsx("aside", { className: "caos-focus-rail", children: rail })
441
+ ] });
442
+ }
421
443
 
422
444
  // src/lib/vizLoop.ts
423
445
  function createVizLoop(frame, options, deps) {
@@ -600,7 +622,7 @@ var TEXT = {
600
622
  synthetic: "Synthetic",
601
623
  real: "Real",
602
624
  uploaded: "Uploaded",
603
- lockedNote: "Scenario knobs are locked on a real sample \u2014 you pick which datum; every tool runs on it as-is.",
625
+ lockedNote: "Scenario knobs are locked on a real sample: you pick which datum; every tool runs on it as-is.",
604
626
  modifiedPrefix: "modified from",
605
627
  reset: "reset"
606
628
  },
@@ -909,4 +931,4 @@ function ReferenceList({ ids, heading }) {
909
931
  ] });
910
932
  }
911
933
 
912
- export { AppShell, ArchitectureModal, Callout, CaseSelector, CitationsProvider, Cite, Equation, Figure, InlineMath, LanguageToggle, ReferenceList, Refs, SubTabs, Tabs, ThemeToggle, WorkbenchShell, applyTheme, caseKind, caseTooltip, casesInSource, createVizLoop, groupByCategory, readCaseParam, readTheme, sourcesPresent, useLangStore, usePausedViz, useShellLang, useThemeStore, withCaseParam };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fasl-work/caos-app-shell",
3
- "version": "0.6.7",
3
+ "version": "0.6.13",
4
4
  "description": "Shared web-app shell + content primitives + design system for the CAOS / Faena public apps (ADR-0016).",
5
5
  "license": "MIT",
6
6
  "author": "Felipe Santibáñez-Leal",
@@ -30,6 +30,7 @@
30
30
  "build": "tsup",
31
31
  "test": "node --import tsx --test \"test/**/*.test.ts\" \"test/**/*.test.tsx\"",
32
32
  "test:browser": "node --import tsx --test \"e2e/**/*.test.ts\"",
33
+ "prepare": "npm run build",
33
34
  "prepublishOnly": "npm run build"
34
35
  },
35
36
  "peerDependencies": {
package/styles.css CHANGED
@@ -1,6 +1,6 @@
1
1
  @import "katex/dist/katex.min.css";
2
2
 
3
- /* @fasl-work/caos-app-shell — shared design system (ADR-0016 + visual-standards palette).
3
+ /* @fasl-work/caos-app-shell, shared design system (ADR-0016 + visual-standards palette).
4
4
  * Theming via CSS custom properties keyed on data-theme on <html> (set pre-paint by an inline
5
5
  * script in index.html, kept in sync by the shell theme store). Import once per app:
6
6
  * import "@fasl-work/caos-app-shell/styles.css"; */
@@ -110,7 +110,22 @@ code, kbd { font-family: var(--font-mono); }
110
110
  /* ── Misc ── */
111
111
  .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
112
112
  :where(a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])):focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; border-radius: 4px; }
113
- @media (max-width: 760px) { .main-nav { display: none; } }
113
+ @media (max-width: 760px) {
114
+ /* ADR-0071: route navigation shares the header row; it owns the horizontal
115
+ scroll instead of adding a permanent second row above every instrument. */
116
+ .header-inner { height: 52px; min-height: 0; flex-wrap: nowrap; gap: 0.3rem; padding-inline: 0.55rem; }
117
+ .brand, .header-actions { flex: none; white-space: nowrap; }
118
+ .brand { gap: 0.25rem; font-size: 0.8rem; }
119
+ .header-actions { margin-left: 0; gap: 0; }
120
+ .header-actions .icon-btn { padding: 0.25rem; }
121
+ .header-sep { margin-inline: 0.15rem; }
122
+ .main-nav { flex: 1 1 0; min-width: 0; overflow-x: auto; overflow-y: hidden; white-space: nowrap; padding: 2px 0; scrollbar-width: none; }
123
+ .main-nav::-webkit-scrollbar { display: none; }
124
+ .main-nav .nav-link { flex: none; padding: 0.3rem 0.3rem; font-size: 0.7rem; }
125
+ }
126
+ @media (max-width: 380px) {
127
+ .brand span:last-child { display: none; }
128
+ }
114
129
  @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
115
130
 
116
131
  /* ── Prose / content pages ── (fill the page-body width with grids/figures/sub-tab rails, not a narrow column) */
@@ -319,6 +334,14 @@ html, body { overflow-x: hidden; }
319
334
  text stays (ADR-0016 s2: provenance and the honest disclaimer are the footer's job); it is set
320
335
  compact, so it takes two to three lines, not five. */
321
336
  .app-shell.fixed .site-footer .footer-inner { padding: 0.45rem 1.5rem; gap: 0.4rem; font-size: 0.74rem; line-height: 1.3; }
337
+ @media (max-width: 760px) {
338
+ /* A long, correctly attributed provenance must not consume the instrument.
339
+ Footer metadata remains accessible by horizontal scroll and keyboard focus. */
340
+ .app-shell.fixed .site-footer .footer-inner { padding: 0.35rem 0.55rem; }
341
+ .app-shell.fixed .footer-meta { flex-wrap: nowrap; overflow-x: auto; white-space: nowrap; scrollbar-width: none; }
342
+ .app-shell.fixed .footer-meta::-webkit-scrollbar { display: none; }
343
+ .app-shell.fixed .footer-meta > * { flex: none; }
344
+ }
322
345
 
323
346
  /* ADR-0058 diagram i18n. A hand-authored architecture SVG carries BOTH languages inside ONE file,
324
347
  and the modal wrapper picks one. Two files would be two things to keep in step, and the one not
@@ -332,3 +355,29 @@ html, body { overflow-x: hidden; }
332
355
  /* The compact one-of-N case picker (ADR-0071 rule 7). Ten cases in four categories
333
356
  cost a 308px chip block, 21% of a 2560x1440 viewport, and one row as a dropdown. */
334
357
  .cs-select { width: 100%; max-width: 460px; }
358
+
359
+ /* ADR-0070: additive, shell-owned, full-viewport scenario focus frame. */
360
+ .caos-focus-shell { position: fixed; inset: 0; z-index: 100; display: grid; grid-template-columns: minmax(0,1fr) clamp(300px,20vw,340px); min-width: 0; height: 100dvh; background: var(--color-bg); color: var(--color-fg); }
361
+ .caos-focus-stage { position: relative; min-width: 0; min-height: 0; overflow: hidden; background: var(--color-bg); }
362
+ .caos-focus-instrument { position: absolute; inset: 0; min-width: 0; min-height: 0; }
363
+ .caos-focus-label, .caos-focus-hud > div { background: color-mix(in oklab,var(--color-surface) 82%,transparent); backdrop-filter: blur(6px); border: 1px solid var(--color-border); border-radius: 6px; }
364
+ .caos-focus-label { position: absolute; top: 14px; left: 14px; max-width: min(46ch,calc(100% - 230px)); display: grid; gap: 4px; padding: 10px 12px; pointer-events: none; }
365
+ .caos-focus-label strong { font-size: 0.96rem; line-height: 1.2; }
366
+ .caos-focus-label span { font-size: 0.78rem; line-height: 1.4; color: var(--color-fg-subtle); }
367
+ .caos-focus-hud { position: absolute; left: 14px; top: 104px; display: grid; gap: 6px; pointer-events: none; }
368
+ .caos-focus-hud > div { display: grid; gap: 1px; padding: 7px 10px; min-width: 120px; }
369
+ .caos-focus-hud strong { font: 700 1rem var(--font-mono,monospace); }
370
+ .caos-focus-hud span { color: var(--color-fg-subtle); font-size: 0.61rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
371
+ .caos-focus-actions { position: absolute; top: 12px; right: 12px; display: flex; gap: 6px; align-items: center; padding: 4px; border: 1px solid var(--color-border); border-radius: 6px; background: color-mix(in oklab,var(--color-surface) 90%,transparent); backdrop-filter: blur(6px); }
372
+ .caos-focus-actions button { border: 0; border-radius: 4px; padding: 6px 9px; background: var(--color-surface-2); color: var(--color-fg); font-size: 0.75rem; font-weight: 700; cursor: pointer; }
373
+ .caos-focus-rail { min-width: 0; min-height: 0; overflow-y: auto; overscroll-behavior: contain; border-left: 1px solid var(--color-border); background: var(--color-surface); }
374
+ @media (max-width: 860px) {
375
+ .caos-focus-shell { grid-template-columns: minmax(0,1fr); grid-template-rows: minmax(0,54dvh) minmax(0,46dvh); }
376
+ .caos-focus-rail { border-left: 0; border-top: 1px solid var(--color-border); }
377
+ .caos-focus-label { max-width: calc(100% - 16px); top: 49px; left: 8px; padding: 6px 8px; }
378
+ .caos-focus-hud { top: auto; bottom: 8px; left: 8px; right: 8px; display: grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap: 4px; }
379
+ .caos-focus-hud > div { min-width: 0; padding: 5px; }
380
+ .caos-focus-hud strong { font-size: 0.72rem; white-space: nowrap; }
381
+ .caos-focus-hud span { font-size: 0.48rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
382
+ .caos-focus-actions { top: 7px; right: 7px; }
383
+ }