@fasl-work/caos-app-shell 0.1.2 → 0.3.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,8 +1,8 @@
1
- import { createContext, useState, useRef, useEffect, useId, useContext } from 'react';
1
+ import { createContext, useState, useRef, useEffect, useCallback, useId, useContext } from 'react';
2
2
  import { NavLink } from 'react-router-dom';
3
3
  import { Sun, Moon, Languages, Boxes, Github, Globe, Briefcase, Info } from 'lucide-react';
4
4
  import { create } from 'zustand';
5
- import { jsxs, jsx } from 'react/jsx-runtime';
5
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
6
  import katex from 'katex';
7
7
 
8
8
  // src/shell/AppShell.tsx
@@ -45,7 +45,7 @@ var CHROME = {
45
45
  toggleLanguage: "Switch language",
46
46
  light: "Light",
47
47
  dark: "Dark",
48
- attribution: "Built by Felipe Santib\xE1\xF1ez-Leal",
48
+ attribution: "Developed by Felipe Santib\xE1\xF1ez-Leal",
49
49
  complement: "A CAOS research project",
50
50
  license: "MIT licensed \xB7 open source",
51
51
  version: "v"
@@ -58,7 +58,7 @@ var CHROME = {
58
58
  toggleLanguage: "Cambiar idioma",
59
59
  light: "Claro",
60
60
  dark: "Oscuro",
61
- attribution: "Hecho por Felipe Santib\xE1\xF1ez-Leal",
61
+ attribution: "Desarrollado por Felipe Santib\xE1\xF1ez-Leal",
62
62
  complement: "Un proyecto de investigaci\xF3n CAOS",
63
63
  license: "Licencia MIT \xB7 c\xF3digo abierto",
64
64
  version: "v"
@@ -282,28 +282,319 @@ function AppShell({ config, children }) {
282
282
  ] }) }),
283
283
  config.architecture && archOpen && /* @__PURE__ */ jsx(ArchitectureModal, { config: config.architecture, onClose: () => setArchOpen(false) }),
284
284
  /* @__PURE__ */ jsx("main", { className: "page", children }),
285
- /* @__PURE__ */ jsx("footer", { className: "site-footer", children: /* @__PURE__ */ jsxs("div", { className: "footer-inner", children: [
286
- /* @__PURE__ */ jsxs("div", { className: "footer-meta", children: [
287
- /* @__PURE__ */ jsx("span", { children: c.attribution }),
285
+ /* @__PURE__ */ jsx("footer", { className: "site-footer", children: /* @__PURE__ */ jsx("div", { className: "footer-inner", children: /* @__PURE__ */ jsxs("div", { className: "footer-meta", children: [
286
+ /* @__PURE__ */ jsx("span", { children: config.product.name }),
287
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
288
+ /* @__PURE__ */ jsx("span", { children: c.complement }),
289
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
290
+ /* @__PURE__ */ jsx("span", { className: "footer-build", children: /* @__PURE__ */ jsxs("span", { children: [
291
+ c.version,
292
+ config.version
293
+ ] }) }),
294
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
295
+ /* @__PURE__ */ jsx("span", { children: c.attribution }),
296
+ config.footer?.provenance && /* @__PURE__ */ jsxs(Fragment, { children: [
288
297
  /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
289
- /* @__PURE__ */ jsx("span", { children: c.complement })
298
+ /* @__PURE__ */ jsx("span", { children: config.footer.provenance[lang] })
290
299
  ] }),
291
- /* @__PURE__ */ jsxs("div", { className: "footer-meta", children: [
292
- /* @__PURE__ */ jsx("a", { href: config.links.github, target: "_blank", rel: "noreferrer noopener", children: c.github }),
300
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
301
+ /* @__PURE__ */ jsx("a", { href: config.links.github, target: "_blank", rel: "noreferrer noopener", children: c.github }),
302
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
303
+ /* @__PURE__ */ jsx("span", { className: "faint", children: c.license }),
304
+ config.footer?.disclaimer && /* @__PURE__ */ jsxs(Fragment, { children: [
293
305
  /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
294
- /* @__PURE__ */ jsx("a", { href: personal, target: "_blank", rel: "noreferrer noopener", children: c.personal }),
295
- /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
296
- /* @__PURE__ */ jsx("a", { href: portfolio, target: "_blank", rel: "noreferrer noopener", children: c.portfolio }),
297
- /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
298
- /* @__PURE__ */ jsx("span", { className: "faint", children: c.license }),
299
- /* @__PURE__ */ jsx("span", { className: "footer-build", children: /* @__PURE__ */ jsxs("span", { children: [
300
- c.version,
301
- config.version
302
- ] }) })
306
+ /* @__PURE__ */ jsx("span", { className: "faint", children: config.footer.disclaimer[lang] })
303
307
  ] })
304
- ] }) })
308
+ ] }) }) })
309
+ ] });
310
+ }
311
+
312
+ // src/lib/vizLoop.ts
313
+ function createVizLoop(frame, options, deps) {
314
+ const { loop = false, durationMs, onComplete, onPlayingChange } = options;
315
+ const { raf, caf } = deps;
316
+ let handle = null;
317
+ let want = false;
318
+ let hidden = false;
319
+ let last = 0;
320
+ let baseline = true;
321
+ let elapsed = 0;
322
+ const playing = () => handle !== null;
323
+ const emitPlaying = (was) => {
324
+ if (was !== playing()) onPlayingChange?.(playing());
325
+ };
326
+ const request = () => {
327
+ if (handle === null) handle = raf(tick);
328
+ };
329
+ const cancel = () => {
330
+ if (handle !== null) {
331
+ caf(handle);
332
+ handle = null;
333
+ }
334
+ };
335
+ function tick(t) {
336
+ handle = null;
337
+ if (baseline) {
338
+ last = t;
339
+ baseline = false;
340
+ }
341
+ const dt = t - last;
342
+ last = t;
343
+ elapsed += dt;
344
+ let e = elapsed;
345
+ const capped = durationMs != null && e >= durationMs;
346
+ if (capped) e = durationMs;
347
+ const cont = frame(dt, e);
348
+ if (capped || cont === false) {
349
+ if (loop) {
350
+ elapsed = 0;
351
+ baseline = true;
352
+ request();
353
+ } else {
354
+ const was = true;
355
+ want = false;
356
+ emitPlaying(was);
357
+ onComplete?.();
358
+ }
359
+ return;
360
+ }
361
+ request();
362
+ }
363
+ const play = () => {
364
+ if (want && playing()) return;
365
+ const was = playing();
366
+ want = true;
367
+ baseline = true;
368
+ if (!hidden) request();
369
+ emitPlaying(was);
370
+ };
371
+ const pause = () => {
372
+ const was = playing();
373
+ want = false;
374
+ cancel();
375
+ emitPlaying(was);
376
+ };
377
+ const restart = () => {
378
+ const was = playing();
379
+ want = true;
380
+ elapsed = 0;
381
+ baseline = true;
382
+ cancel();
383
+ if (!hidden) request();
384
+ emitPlaying(was);
385
+ };
386
+ const setHidden = (h) => {
387
+ if (h === hidden) return;
388
+ hidden = h;
389
+ const was = playing();
390
+ if (hidden) {
391
+ cancel();
392
+ } else if (want) {
393
+ baseline = true;
394
+ request();
395
+ }
396
+ emitPlaying(was);
397
+ };
398
+ return {
399
+ isPlaying: playing,
400
+ isRunning: () => want,
401
+ play,
402
+ pause,
403
+ toggle: () => want ? pause() : play(),
404
+ restart,
405
+ setHidden,
406
+ dispose: () => {
407
+ want = false;
408
+ cancel();
409
+ }
410
+ };
411
+ }
412
+
413
+ // src/lib/usePausedViz.ts
414
+ function usePausedViz(frame, options = {}) {
415
+ const { autoStart = false, loop, durationMs, onComplete } = options;
416
+ const [playing, setPlaying] = useState(false);
417
+ const frameRef = useRef(frame);
418
+ frameRef.current = frame;
419
+ const completeRef = useRef(onComplete);
420
+ completeRef.current = onComplete;
421
+ const loopRef = useRef(null);
422
+ useEffect(() => {
423
+ if (typeof window === "undefined" || typeof requestAnimationFrame === "undefined") return;
424
+ const vloop = createVizLoop(
425
+ (dt, elapsed) => frameRef.current(dt, elapsed),
426
+ {
427
+ loop,
428
+ durationMs,
429
+ onComplete: () => completeRef.current?.(),
430
+ onPlayingChange: setPlaying
431
+ },
432
+ { raf: requestAnimationFrame, caf: cancelAnimationFrame }
433
+ );
434
+ loopRef.current = vloop;
435
+ const onVisibility = () => vloop.setHidden(document.hidden);
436
+ document.addEventListener("visibilitychange", onVisibility);
437
+ if (autoStart && !document.hidden) vloop.play();
438
+ return () => {
439
+ document.removeEventListener("visibilitychange", onVisibility);
440
+ vloop.dispose();
441
+ loopRef.current = null;
442
+ };
443
+ }, []);
444
+ const play = useCallback(() => loopRef.current?.play(), []);
445
+ const pause = useCallback(() => loopRef.current?.pause(), []);
446
+ const toggle = useCallback(() => loopRef.current?.toggle(), []);
447
+ const restart = useCallback(() => loopRef.current?.restart(), []);
448
+ return { playing, play, pause, toggle, restart };
449
+ }
450
+
451
+ // src/case/caseModel.ts
452
+ var caseKind = (c) => c.kind ?? "synthetic";
453
+ var KIND_ORDER = ["synthetic", "real", "uploaded"];
454
+ function sourcesPresent(cases) {
455
+ const set = new Set(cases.map(caseKind));
456
+ return KIND_ORDER.filter((k) => set.has(k));
457
+ }
458
+ function groupByCategory(cases) {
459
+ const order = [];
460
+ const map = /* @__PURE__ */ new Map();
461
+ for (const c of cases) {
462
+ const cat = c.category ?? "";
463
+ if (!map.has(cat)) {
464
+ map.set(cat, []);
465
+ order.push(cat);
466
+ }
467
+ map.get(cat).push(c);
468
+ }
469
+ return order.map((category) => ({ category, cases: map.get(category) }));
470
+ }
471
+ function casesInSource(cases, source) {
472
+ if (!source) return cases;
473
+ return cases.filter((c) => caseKind(c) === source);
474
+ }
475
+ function caseTooltip(c) {
476
+ return [c.anchor, c.expectedBand].filter(Boolean).join(" \xB7 ");
477
+ }
478
+ function readCaseParam(search, param = "case") {
479
+ return new URLSearchParams(search).get(param);
480
+ }
481
+ function withCaseParam(search, id, param = "case") {
482
+ const p = new URLSearchParams(search);
483
+ p.set(param, id);
484
+ const s = p.toString();
485
+ return s ? `?${s}` : "";
486
+ }
487
+ var TEXT = {
488
+ en: {
489
+ source: "Source",
490
+ synthetic: "Synthetic",
491
+ real: "Real",
492
+ uploaded: "Uploaded",
493
+ lockedNote: "Scenario knobs are locked on a real sample \u2014 you pick which datum; every tool runs on it as-is.",
494
+ modifiedPrefix: "modified from",
495
+ reset: "reset"
496
+ },
497
+ es: {
498
+ source: "Fuente",
499
+ synthetic: "Sint\xE9tico",
500
+ real: "Real",
501
+ uploaded: "Subido",
502
+ lockedNote: "Los controles del escenario quedan bloqueados en una muestra real: eliges el dato; todas las herramientas corren sobre \xE9l tal cual.",
503
+ modifiedPrefix: "modificado desde",
504
+ reset: "restablecer"
505
+ }
506
+ };
507
+ var KIND_TAG = { synthetic: "S", real: "R", uploaded: "U" };
508
+ function CaseSelector(props) {
509
+ const {
510
+ cases,
511
+ selectedId,
512
+ onSelect,
513
+ source,
514
+ onSourceChange,
515
+ lockedNote,
516
+ modifiedFromId,
517
+ onResetToCanonical,
518
+ lang = "en",
519
+ text,
520
+ deepLink,
521
+ ariaLabel,
522
+ className
523
+ } = props;
524
+ const t = { ...TEXT[lang], ...text };
525
+ const baseId = useId();
526
+ const param = typeof deepLink === "string" ? deepLink : "case";
527
+ useEffect(() => {
528
+ if (!deepLink || typeof window === "undefined") return;
529
+ const fromUrl = readCaseParam(window.location.search, param);
530
+ if (fromUrl && fromUrl !== selectedId && cases.some((c) => c.id === fromUrl)) onSelect(fromUrl);
531
+ }, []);
532
+ useEffect(() => {
533
+ if (!deepLink || typeof window === "undefined") return;
534
+ const next = withCaseParam(window.location.search, selectedId, param);
535
+ if (next !== window.location.search) {
536
+ window.history.replaceState(null, "", `${window.location.pathname}${next}${window.location.hash}`);
537
+ }
538
+ }, [deepLink, param, selectedId]);
539
+ const lanes = source ? sourcesPresent(cases) : [];
540
+ const visible = casesInSource(cases, source);
541
+ const groups = groupByCategory(visible);
542
+ const modified = cases.find((c) => c.id === modifiedFromId) ?? null;
543
+ return /* @__PURE__ */ jsxs("div", { className: `cs${className ? ` ${className}` : ""}`, role: "group", "aria-label": ariaLabel ?? "Case selector", children: [
544
+ source && lanes.length > 1 && /* @__PURE__ */ jsxs("div", { className: "cs-source", children: [
545
+ /* @__PURE__ */ jsx("span", { className: "cs-source-label", children: t.source }),
546
+ /* @__PURE__ */ jsx("div", { className: "cs-source-tabs", role: "tablist", "aria-label": t.source, children: lanes.map((k) => /* @__PURE__ */ jsx(
547
+ "button",
548
+ {
549
+ type: "button",
550
+ role: "tab",
551
+ "aria-selected": source === k,
552
+ className: source === k ? "subtab active" : "subtab",
553
+ onClick: () => onSourceChange?.(k),
554
+ children: k === "synthetic" ? t.synthetic : k === "real" ? t.real : t.uploaded
555
+ },
556
+ k
557
+ )) })
558
+ ] }),
559
+ source && source !== "synthetic" && /* @__PURE__ */ jsx("p", { className: "cs-locked banner", children: lockedNote ?? t.lockedNote }),
560
+ groups.map((g) => /* @__PURE__ */ jsxs("div", { className: "cs-group", role: "group", "aria-label": g.category || void 0, children: [
561
+ g.category && /* @__PURE__ */ jsx("span", { className: "cs-group-label", children: g.category }),
562
+ /* @__PURE__ */ jsx("div", { className: "cs-chips", children: g.cases.map((c) => {
563
+ const on = c.id === selectedId;
564
+ const tip = caseTooltip(c);
565
+ return /* @__PURE__ */ jsxs(
566
+ "button",
567
+ {
568
+ type: "button",
569
+ id: `${baseId}-case-${c.id}`,
570
+ className: `cs-chip${on ? " on" : ""}`,
571
+ "aria-pressed": on,
572
+ disabled: c.disabled,
573
+ title: tip || void 0,
574
+ onClick: () => onSelect(c.id),
575
+ children: [
576
+ /* @__PURE__ */ jsx("span", { className: "cs-chip-id", children: c.id }),
577
+ /* @__PURE__ */ jsx("span", { className: "cs-chip-name", children: c.name }),
578
+ /* @__PURE__ */ jsx("span", { className: `cs-kind cs-kind-${caseKindOf(c)}`, "aria-hidden": "true", children: KIND_TAG[caseKindOf(c)] })
579
+ ]
580
+ },
581
+ c.id
582
+ );
583
+ }) })
584
+ ] }, g.category || "_")),
585
+ modified && /* @__PURE__ */ jsxs("div", { className: "cs-modified", children: [
586
+ /* @__PURE__ */ jsxs("span", { className: "badge accent", children: [
587
+ t.modifiedPrefix,
588
+ " ",
589
+ /* @__PURE__ */ jsx("strong", { children: modified.id })
590
+ ] }),
591
+ onResetToCanonical && /* @__PURE__ */ jsx("button", { type: "button", className: "cs-reset", onClick: onResetToCanonical, children: t.reset })
592
+ ] })
305
593
  ] });
306
594
  }
595
+ function caseKindOf(c) {
596
+ return c.kind ?? "synthetic";
597
+ }
307
598
  function Tabs({ tabs, initial, ariaLabel }) {
308
599
  const baseId = useId();
309
600
  const first = tabs[0]?.id ?? "";
@@ -497,4 +788,4 @@ function ReferenceList({ ids, heading }) {
497
788
  ] });
498
789
  }
499
790
 
500
- export { AppShell, ArchitectureModal, Callout, CitationsProvider, Cite, Equation, Figure, InlineMath, LanguageToggle, ReferenceList, Refs, SubTabs, Tabs, ThemeToggle, applyTheme, readTheme, useLangStore, useShellLang, useThemeStore };
791
+ export { AppShell, ArchitectureModal, Callout, CaseSelector, CitationsProvider, Cite, Equation, Figure, InlineMath, LanguageToggle, ReferenceList, Refs, SubTabs, Tabs, ThemeToggle, applyTheme, caseKind, caseTooltip, casesInSource, createVizLoop, groupByCategory, readCaseParam, readTheme, sourcesPresent, useLangStore, usePausedViz, useShellLang, useThemeStore, withCaseParam };
package/package.json CHANGED
@@ -1,54 +1,56 @@
1
- {
2
- "name": "@fasl-work/caos-app-shell",
3
- "version": "0.1.2",
4
- "description": "Shared web-app shell + content primitives + design system for the CAOS / Faena public apps (ADR-0016).",
5
- "license": "MIT",
6
- "author": "Felipe Santibáñez-Leal",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/fsantibanezleal/CAOS_APP_SHELL.git"
10
- },
11
- "type": "module",
12
- "publishConfig": {
13
- "access": "public"
14
- },
15
- "sideEffects": [
16
- "*.css"
17
- ],
18
- "exports": {
19
- ".": {
20
- "types": "./dist/index.d.ts",
21
- "import": "./dist/index.js"
22
- },
23
- "./styles.css": "./styles.css"
24
- },
25
- "files": [
26
- "dist",
27
- "styles.css"
28
- ],
29
- "scripts": {
30
- "build": "tsup",
31
- "prepublishOnly": "npm run build"
32
- },
33
- "peerDependencies": {
34
- "katex": "^0.16.0",
35
- "lucide-react": ">=0.400.0",
36
- "react": "^18 || ^19",
37
- "react-dom": "^18 || ^19",
38
- "react-router-dom": "^6 || ^7",
39
- "zustand": "^4 || ^5"
40
- },
41
- "devDependencies": {
42
- "@types/katex": "^0.16.7",
43
- "@types/react": "^19.0.0",
44
- "@types/react-dom": "^19.0.0",
45
- "katex": "^0.16.11",
46
- "lucide-react": "^0.469.0",
47
- "react": "^19.0.0",
48
- "react-dom": "^19.0.0",
49
- "react-router-dom": "^7.1.1",
50
- "tsup": "^8.3.5",
51
- "typescript": "^5.7.3",
52
- "zustand": "^5.0.2"
53
- }
54
- }
1
+ {
2
+ "name": "@fasl-work/caos-app-shell",
3
+ "version": "0.3.0",
4
+ "description": "Shared web-app shell + content primitives + design system for the CAOS / Faena public apps (ADR-0016).",
5
+ "license": "MIT",
6
+ "author": "Felipe Santibáñez-Leal",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/fsantibanezleal/CAOS_APP_SHELL.git"
10
+ },
11
+ "type": "module",
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "sideEffects": [
16
+ "*.css"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ },
23
+ "./styles.css": "./styles.css"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "styles.css"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsup",
31
+ "test": "node --import tsx --test test/vizLoop.test.ts test/caseModel.test.ts",
32
+ "prepublishOnly": "npm run build"
33
+ },
34
+ "peerDependencies": {
35
+ "katex": "^0.16.0",
36
+ "lucide-react": ">=0.400.0",
37
+ "react": "^18 || ^19",
38
+ "react-dom": "^18 || ^19",
39
+ "react-router-dom": "^6 || ^7",
40
+ "zustand": "^4 || ^5"
41
+ },
42
+ "devDependencies": {
43
+ "@types/katex": "^0.16.7",
44
+ "@types/react": "^19.0.0",
45
+ "@types/react-dom": "^19.0.0",
46
+ "katex": "^0.16.11",
47
+ "lucide-react": "^0.469.0",
48
+ "react": "^19.0.0",
49
+ "react-dom": "^19.0.0",
50
+ "react-router-dom": "^7.1.1",
51
+ "tsup": "^8.3.5",
52
+ "tsx": "^4.23.0",
53
+ "typescript": "^5.7.3",
54
+ "zustand": "^5.0.2"
55
+ }
56
+ }