@fasl-work/caos-app-shell 0.14.1 → 0.14.3

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.d.ts CHANGED
@@ -69,7 +69,16 @@ interface FocusShellProps {
69
69
  */
70
70
  landmark?: boolean;
71
71
  }
72
- /** Full-viewport, scenario-scoped analysis frame. The product supplies only its instrument and controls. */
72
+ /**
73
+ * Full-viewport, scenario-scoped analysis frame. The product supplies only its instrument and controls.
74
+ *
75
+ * The frame measures its overlay chrome and sets three lengths on `.caos-focus-stage` (custom properties, so the instrument
76
+ * inherits them): `--caos-focus-clear-top` (the stage's top edge to the lowest chrome box in its upper half, plus 8px),
77
+ * `--caos-focus-clear-bottom` (likewise from the bottom edge, 0 or unset when nothing sits in the lower half) and
78
+ * `--caos-focus-label-bottom` (the label's bottom edge from the stage's top). A product that renders its own instrument keeps
79
+ * its controls clear of the chrome with `padding-top: var(--caos-focus-clear-top, 4.6rem)`. Before the first measurement and in
80
+ * a non-browser render the properties are unset and the CSS fallbacks apply.
81
+ */
73
82
  declare function FocusShell({ stage, rail, title, description, hud, onExit, exitLabel, stageLabel, landmark }: FocusShellProps): react.JSX.Element;
74
83
 
75
84
  /** Light/dark icon toggle. Shows the icon of the theme you would switch TO. */
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { useThemeStore, useShellLang, useLangStore, SHELL_TOKENS, pick, useStage
2
2
  export { NBSP, NOT_AVAILABLE, SCIENTIFIC_BELOW, SHELL_TOKENS, Stage, applyTheme, formatNumber, formatTicks, pick, readTheme, resolveToken, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens } from './chunk-YECZXGVG.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
- import { createContext, Component, useState, useRef, useId, useEffect, useCallback, useContext, useMemo, useLayoutEffect, Children } from 'react';
5
+ import { createContext, Component, useState, useRef, useId, useEffect, useLayoutEffect, useCallback, useContext, useMemo, Children } from 'react';
6
6
  import { useInRouterContext, NavLink, useLocation, Navigate, Link } from 'react-router';
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';
@@ -483,10 +483,65 @@ function WorkbenchShell({
483
483
  overlays
484
484
  ] });
485
485
  }
486
+ var GAP = 8;
487
+ function measureChrome(stage) {
488
+ const box = stage.getBoundingClientRect();
489
+ if (box.height <= 0 || box.width <= 0) return;
490
+ const label = stage.querySelector(":scope > .caos-focus-label");
491
+ const chrome2 = [
492
+ label,
493
+ ...Array.from(stage.querySelectorAll(":scope > .caos-focus-hud > div")),
494
+ stage.querySelector(":scope > .caos-focus-actions")
495
+ ].filter((e) => e !== null);
496
+ const write = (name, px) => {
497
+ const next = px === null ? "" : `${px}px`;
498
+ if (stage.style.getPropertyValue(name) === next) return false;
499
+ if (px === null) stage.style.removeProperty(name);
500
+ else stage.style.setProperty(name, next);
501
+ return true;
502
+ };
503
+ const labelBottom = label ? Math.ceil(label.getBoundingClientRect().bottom - box.top) : null;
504
+ const moved = write("--caos-focus-label-bottom", labelBottom !== null && labelBottom > 0 ? labelBottom : null);
505
+ if (moved) {
506
+ measureChrome(stage);
507
+ return;
508
+ }
509
+ const mid = box.height / 2;
510
+ let top = 0;
511
+ let bottom = 0;
512
+ for (const el of chrome2) {
513
+ const r = el.getBoundingClientRect();
514
+ if (r.width <= 0 || r.height <= 0) continue;
515
+ if (r.top - box.top < mid) top = Math.max(top, r.bottom - box.top + GAP);
516
+ else bottom = Math.max(bottom, box.bottom - r.top + GAP);
517
+ }
518
+ write("--caos-focus-clear-top", top > 0 ? Math.ceil(top) : null);
519
+ write("--caos-focus-clear-bottom", bottom > 0 ? Math.ceil(bottom) : null);
520
+ }
486
521
  function FocusShell({ stage, rail, title, description, hud, onExit, exitLabel, stageLabel, landmark = true }) {
487
522
  const Root = landmark ? "main" : "div";
523
+ const stageRef = useRef(null);
524
+ useLayoutEffect(() => {
525
+ const el = stageRef.current;
526
+ if (el) measureChrome(el);
527
+ });
528
+ useLayoutEffect(() => {
529
+ const el = stageRef.current;
530
+ if (!el) return void 0;
531
+ const run = () => measureChrome(el);
532
+ window.addEventListener("resize", run);
533
+ let ro;
534
+ if (typeof ResizeObserver !== "undefined") {
535
+ ro = new ResizeObserver(run);
536
+ for (const c of Array.from(el.querySelectorAll(":scope > .caos-focus-label, :scope > .caos-focus-hud, :scope > .caos-focus-actions"))) ro.observe(c);
537
+ }
538
+ return () => {
539
+ window.removeEventListener("resize", run);
540
+ ro?.disconnect();
541
+ };
542
+ }, []);
488
543
  return /* @__PURE__ */ jsxs(Root, { className: "caos-focus-shell", "data-focus-shell": "", children: [
489
- /* @__PURE__ */ jsxs("section", { className: "caos-focus-stage", "aria-label": stageLabel, children: [
544
+ /* @__PURE__ */ jsxs("section", { ref: stageRef, className: "caos-focus-stage", "aria-label": stageLabel, children: [
490
545
  /* @__PURE__ */ jsx("div", { className: "caos-focus-instrument", children: stage }),
491
546
  /* @__PURE__ */ jsxs("div", { className: "caos-focus-label", children: [
492
547
  /* @__PURE__ */ jsx("strong", { children: title }),
@@ -3355,6 +3410,10 @@ function CaseWorkbench(props) {
3355
3410
  cases && /* @__PURE__ */ jsx(CaseSelector, { lang, ...cases, accessory: besideSelect ? enter : cases.accessory }),
3356
3411
  variants && /* @__PURE__ */ jsx(VariantBar, { ...variants })
3357
3412
  ] }) : null;
3413
+ const focusPickers = cases || variants ? /* @__PURE__ */ jsxs(Fragment, { children: [
3414
+ cases && /* @__PURE__ */ jsx(CaseSelector, { lang, ...cases, accessory: cases.accessory }),
3415
+ variants && /* @__PURE__ */ jsx(VariantBar, { ...variants })
3416
+ ] }) : null;
3358
3417
  const railHead = pickers || enter ? /* @__PURE__ */ jsxs(Fragment, { children: [
3359
3418
  pickers,
3360
3419
  besideSelect ? null : enter
@@ -3382,7 +3441,7 @@ function CaseWorkbench(props) {
3382
3441
  stageLabel: FOCUS_TEXT.stage[lang],
3383
3442
  landmark: false,
3384
3443
  stage: /* @__PURE__ */ jsx("div", { className: "caos-cw caos-cw-focus", ...instrumentData, "data-focus-group": open?.id, "data-instrument": "", children: /* @__PURE__ */ jsx("div", { className: "tabs", children: open && /* @__PURE__ */ jsx("div", { className: "tabpanel", role: "tabpanel", tabIndex: 0, "aria-label": open.label, "data-panel": open.id, children: /* @__PURE__ */ jsx(PanelBoundary, { panel: open.id, children: /* @__PURE__ */ jsx(DocRefsScope, { children: open.content }) }) }) }) }),
3385
- rail: /* @__PURE__ */ jsx("div", { className: "caos-focus-rail-body", children: /* @__PURE__ */ jsx(RailBody, { rail: rail ?? null, railHead: pickers, railLabel }) })
3444
+ rail: /* @__PURE__ */ jsx("div", { className: "caos-focus-rail-body", children: /* @__PURE__ */ jsx(RailBody, { rail: rail ?? null, railHead: focusPickers, railLabel }) })
3386
3445
  }
3387
3446
  ) }) });
3388
3447
  }
package/gate/inpage.mjs CHANGED
@@ -1036,8 +1036,32 @@ export function installLib() {
1036
1036
  const panel = stage ? stage.querySelector('[role="tabpanel"]') : null;
1037
1037
  const st = document.querySelector('.caos-focus-stage');
1038
1038
  const box = st ? st.getBoundingClientRect() : null;
1039
+ // a stage control the Focus chrome (the label, the HUD tiles, the actions) sits on: more than 1px in both directions
1040
+ const chrome = st
1041
+ ? [
1042
+ ...[...st.querySelectorAll(':scope > .caos-focus-label')].map((e) => [e, 'the stage label']),
1043
+ ...[...st.querySelectorAll(':scope > .caos-focus-hud > div')].map((e) => [e, 'the HUD']),
1044
+ ...[...st.querySelectorAll(':scope > .caos-focus-actions')].map((e) => [e, 'the Focus actions']),
1045
+ ].filter(([e]) => visible(e))
1046
+ : [];
1047
+ const covered = [];
1048
+ const controlSel = '[role="tab"], button, a[href], select, input, textarea, summary, [role="button"], [role="radio"], [role="checkbox"], [role="switch"], [role="slider"]';
1049
+ for (const c of document.querySelectorAll('.caos-focus-instrument')) {
1050
+ for (const el of c.querySelectorAll(controlSel)) {
1051
+ if (covered.length >= 4) break;
1052
+ if (!visible(el)) continue;
1053
+ const r = el.getBoundingClientRect();
1054
+ const hit = chrome.find(([e]) => {
1055
+ const b = e.getBoundingClientRect();
1056
+ return r.left < b.right - 1 && r.right > b.left + 1 && r.top < b.bottom - 1 && r.bottom > b.top + 1;
1057
+ });
1058
+ if (hit) covered.push({ control: (el.getAttribute('aria-label') || el.textContent || el.tagName).trim().replace(/\s+/g, ' ').slice(0, 40), chrome: hit[1] });
1059
+ }
1060
+ }
1039
1061
  return {
1040
1062
  focus: Boolean(shell),
1063
+ covered,
1064
+ enterInside: [...document.querySelectorAll('[data-focus-shell] [data-focus-enter]')].filter(visible).length,
1041
1065
  app: Boolean(document.querySelector('[data-workbench]')),
1042
1066
  caseId: stage ? stage.getAttribute('data-case') : null,
1043
1067
  group: shell ? (stage ? stage.getAttribute('data-focus-group') : null) : panel ? panel.getAttribute('data-panel') : null,
package/gate/run.mjs CHANGED
@@ -27,7 +27,8 @@
27
27
  // Space, its hidden face out of the accessibility tree; a carousel reaches every card, never scrolls sideways
28
28
  // G17 animated drawings an animated SVG has its reduced-motion block, plays for 10 s at most and is still under reduced motion
29
29
  // G18 Focus mode the App's Focus control opens the case's open group in the full viewport and returns to the same case and
30
- // group (by the exit control and by Escape); no page scroll, the rail without scroll, one tab row
30
+ // group (by the exit control and by Escape); no page scroll, the rail without scroll, one tab row; no stage
31
+ // control under the Focus chrome (label, HUD, actions); no Focus control inside the Focus frame
31
32
  // plus the ADR-0071 and ADR-0017 measures (viewport, one-row tabs, rail without scroll, centred prose, captions,
32
33
  // references).
33
34
 
@@ -889,6 +890,8 @@ export async function runGate(o) {
889
890
  if (f.tabRows.some((r) => r > 1)) fail('G18', at, `${label}: a tab row wraps onto ${Math.max(...f.tabRows)} rows`);
890
891
  if (big && f.share < opt.instrumentMin) fail('G18', at, `${label}: the stage covers ${(100 * f.share).toFixed(1)}% of the viewport (floor ${(100 * opt.instrumentMin).toFixed(0)}%)`);
891
892
  if (f.exitInside === false) fail('G18', at, `${label}: the exit control is outside the viewport`);
893
+ for (const c of f.covered) fail('G18', at, `${label}: ${c.chrome} covers the control "${c.control}" (the stage must clear the Focus chrome)`);
894
+ if (f.enterInside > 0) fail('G18', at, `${label}: the Focus control shows inside Focus mode (it opens the mode the reader is already in)`);
892
895
  for (const m of await page.evaluate(() => window.__caosGate.drawingText())) fail('G10', at, m);
893
896
  if (s.contrast) for (const m of await page.evaluate(() => window.__caosGate.contrast())) fail('G13', at, m);
894
897
  if (index === 0) await saveShot(page, `focus_${slug(s.route)}_${s.w}x${s.h}_${s.theme}_${s.lang}.png`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fasl-work/caos-app-shell",
3
- "version": "0.14.1",
3
+ "version": "0.14.3",
4
4
  "description": "Shared web-app shell, workbench primitives, content primitives, design system and measured gate for the CAOS / Faena public apps (ADR-0016, ADR-0017, ADR-0071).",
5
5
  "license": "MIT",
6
6
  "author": "Felipe Santibáñez-Leal",
package/styles.css CHANGED
@@ -672,7 +672,7 @@ svg.lucide { flex: none; }
672
672
  .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; }
673
673
  .caos-focus-label strong { font-size: 0.96rem; line-height: 1.2; }
674
674
  .caos-focus-label span { font-size: 0.78rem; line-height: 1.4; color: var(--color-fg-subtle); }
675
- .caos-focus-hud { position: absolute; left: 14px; top: 104px; display: grid; gap: 6px; pointer-events: none; }
675
+ .caos-focus-hud { position: absolute; left: 14px; top: calc(var(--caos-focus-label-bottom, 96px) + 8px); display: grid; gap: 6px; pointer-events: none; }
676
676
  .caos-focus-hud > div { display: grid; gap: 1px; padding: 7px 10px; min-width: 120px; }
677
677
  .caos-focus-hud strong { font: 700 1rem var(--font-mono,monospace); }
678
678
  .caos-focus-hud span { color: var(--color-fg-subtle); font-size: 0.61rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
@@ -754,6 +754,8 @@ svg.lucide { flex: none; }
754
754
  .caos-exec-badge { align-self: flex-start; padding: 0.1rem 0.5rem; font-size: var(--text-xs); border: 1px solid var(--color-border); border-radius: 999px; color: var(--color-fg-subtle); }
755
755
  .caos-exec-cause { min-width: 0; overflow-wrap: anywhere; }
756
756
  .caos-exec-cause-head { display: flex; justify-content: space-between; gap: 1rem; }
757
+ /* the cause's share stays whole: the row lets its label wrap anywhere, and the squeezed value broke into "3", "5", "%" at 390px */
758
+ .caos-exec-cause-head > .caos-exec-value { flex: none; white-space: nowrap; overflow-wrap: normal; }
757
759
  .caos-exec-bar { height: 0.45rem; margin-top: 0.3rem; background: var(--color-surface-2); border-radius: 999px; overflow: hidden; }
758
760
  .caos-exec-bar > span { display: block; height: 100%; background: var(--color-accent); }
759
761
  .caos-exec-cdef { font-size: var(--text-xs); line-height: 1.4; color: var(--color-fg-faint); }
@@ -823,11 +825,15 @@ svg.lucide { flex: none; }
823
825
 
824
826
  /* == Focus mode for the App (shell 0.14.0, ADR-0070) ================================================================== */
825
827
 
826
- /* The stage is the open group; the label overlay (heading and line) sits above it, so the group starts below the label. */
827
- .caos-focus-instrument > .caos-cw-focus { position: absolute; inset: 0; padding: 4.6rem 0.9rem 0.9rem; }
828
+ /* The stage is the open group; the label overlay (heading and line) sits above it, so the group starts below the label.
829
+ FocusShell measures the overlay chrome and sets, on .caos-focus-stage, three lengths the instrument inherits (shell 0.14.3):
830
+ --caos-focus-clear-top (stage top to the lowest chrome box in the upper half, plus 8px), --caos-focus-clear-bottom (stage
831
+ bottom to the highest chrome box in the lower half, plus 8px; unset when none) and --caos-focus-label-bottom (the label's
832
+ bottom edge). The fallbacks below apply before the first measurement and in a non-browser render. */
833
+ .caos-focus-instrument > .caos-cw-focus { position: absolute; inset: 0; padding: var(--caos-focus-clear-top, 4.6rem) 0.9rem max(0.9rem, var(--caos-focus-clear-bottom, 0px)); }
828
834
  .caos-focus-instrument > .caos-cw-focus > .tabs { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }
829
835
  .caos-focus-rail-body { display: flex; flex-direction: column; gap: var(--space-3); padding: 0.85rem; min-width: 0; }
830
836
  .caos-focus-enter { align-self: flex-start; }
831
837
  @media (max-width: 900px) {
832
- .caos-focus-instrument > .caos-cw-focus { padding: 6.6rem 0.5rem 0.5rem; }
838
+ .caos-focus-instrument > .caos-cw-focus { padding: var(--caos-focus-clear-top, 6.6rem) 0.5rem max(0.5rem, var(--caos-focus-clear-bottom, 0px)); }
833
839
  }