@fasl-work/caos-app-shell 0.6.8 → 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
@@ -360,7 +360,7 @@ function AppShell({ config, children }) {
360
360
  /* @__PURE__ */ jsx("span", { children: config.footer.provenance[lang] })
361
361
  ] }),
362
362
  /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
363
- /* @__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 }),
364
364
  /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
365
365
  /* @__PURE__ */ jsx("span", { className: "faint", children: config.footer?.license?.[lang] ?? c.license }),
366
366
  config.footer?.disclaimer && /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -419,6 +419,27 @@ function WorkbenchShell({
419
419
  overlays
420
420
  ] });
421
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
+ }
422
443
 
423
444
  // src/lib/vizLoop.ts
424
445
  function createVizLoop(frame, options, deps) {
@@ -601,7 +622,7 @@ var TEXT = {
601
622
  synthetic: "Synthetic",
602
623
  real: "Real",
603
624
  uploaded: "Uploaded",
604
- 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.",
605
626
  modifiedPrefix: "modified from",
606
627
  reset: "reset"
607
628
  },
@@ -910,4 +931,4 @@ function ReferenceList({ ids, heading }) {
910
931
  ] });
911
932
  }
912
933
 
913
- 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.8",
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"; */
@@ -111,11 +111,20 @@ code, kbd { font-family: var(--font-mono); }
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
113
  @media (max-width: 760px) {
114
- .header-inner { height: auto; min-height: 56px; flex-wrap: wrap; gap: 0 0.5rem; padding: 4px 1rem; }
115
- .header-actions { gap: 0; }
116
- .header-actions .icon-btn { padding: 0.35rem; }
117
- .main-nav { order: 3; flex: 0 0 100%; min-width: 0; overflow-x: auto; white-space: nowrap; padding: 3px 0; scrollbar-width: thin; }
118
- .main-nav .nav-link { flex: none; padding: 0.3rem 0.6rem; font-size: 0.8rem; }
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; }
119
128
  }
120
129
  @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
121
130
 
@@ -325,6 +334,14 @@ html, body { overflow-x: hidden; }
325
334
  text stays (ADR-0016 s2: provenance and the honest disclaimer are the footer's job); it is set
326
335
  compact, so it takes two to three lines, not five. */
327
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
+ }
328
345
 
329
346
  /* ADR-0058 diagram i18n. A hand-authored architecture SVG carries BOTH languages inside ONE file,
330
347
  and the modal wrapper picks one. Two files would be two things to keep in step, and the one not
@@ -338,3 +355,29 @@ html, body { overflow-x: hidden; }
338
355
  /* The compact one-of-N case picker (ADR-0071 rule 7). Ten cases in four categories
339
356
  cost a 308px chip block, 21% of a 2560x1440 viewport, and one row as a dropdown. */
340
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
+ }