@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 +1 -1
- package/dist/index.d.ts +25 -9
- package/dist/index.js +25 -3
- package/package.json +2 -1
- package/styles.css +51 -2
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)
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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")
|
|
251
|
+
/** validation anchor (e.g. "published optimum 26,086,899"), surfaced in the chip tooltip. */
|
|
236
252
|
anchor?: string;
|
|
237
|
-
/** expected-range hint
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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) {
|
|
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
|
+
}
|