@fasl-work/caos-app-shell 0.22.0 → 0.23.1

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/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## [0.23.001] - 2026-11-07
2
+
3
+ ### Fixed
4
+
5
+ - The gate's document walk (`G.readDocument`) verifies a long, fully readable documentation page instead of stopping at a fixed budget of 600 samples and 30 seconds (known defect 55, issue 196). The budget now scales with the document: every scrollport the walk visits grants the samples of its own grid (the positions along its scroll extent, y by x), and the time allowed is 30 seconds plus 500 ms for every sample granted, under two absolute ceilings (20,000 samples, 15 minutes), so a page that never settles still fails with the same message. The report lists the cost of every walked state (`docWalks`: samples and time against the budget earned).
6
+ - The same walk counted a dense paragraph as unread when its inline math ended in a subscript and a comma followed at once: KaTeX builds a subscript from tables and an invisible 2px strut (`.vlist-s`) that overhang the math into the text beside it, and the hit test of the comma returned one of them instead of the paragraph. The hit test now looks through an element of the math that paints nothing at that point (no glyph of its own, no background, no border, no drawing) to the element beneath, and judges that by the unchanged rule (viewport, clip, scrollports, hit); math glyphs drawn over text still fail.
7
+
8
+ ### Adoption
9
+
10
+ - No product change. A product pinned below 0.23.000 for this defect (CEFOP_DinHot, at 0.19.6) can move once a release carries this entry.
11
+
12
+ ## [0.23.000] - 2026-11-06
13
+
14
+ ### Added
15
+ - Add the `StoryFrame.mobileStageMinHeight` option for products whose mobile scene, controls and three required callouts need a taller stage. The default remains 300 px and the panel stays below the stage. the product template's story showed the need when the 390 × 844 gate could not keep its plot labels, scrub control and callouts visible together.
16
+
1
17
  ## [0.22.000] - 2026-11-06
2
18
 
3
19
  ### Added
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.22.000
1
+ 0.23.001
package/dist/index.d.ts CHANGED
@@ -1417,13 +1417,14 @@ interface StoryFrameProps {
1417
1417
  exit: BiText;
1418
1418
  }>;
1419
1419
  autoAdvanceMs?: number;
1420
+ mobileStageMinHeight?: number;
1420
1421
  className?: string;
1421
1422
  }
1422
1423
  /**
1423
1424
  * A guided 3 to 6 step story over the same state as a product App. The product owns each step's real view and panel;
1424
1425
  * the shell owns the stepper, deep links, keyboard behavior, pause-on-interaction, and semantic structure (ADR-0082).
1425
1426
  */
1426
- declare function StoryFrame({ steps, activeId, onStepChange, onExit, labels, autoAdvanceMs, className }: StoryFrameProps): react.JSX.Element | null;
1427
+ declare function StoryFrame({ steps, activeId, onStepChange, onExit, labels, autoAdvanceMs, mobileStageMinHeight, className }: StoryFrameProps): react.JSX.Element | null;
1427
1428
 
1428
1429
  type CalloutState = 'alarm' | 'warning' | 'normal' | 'selected' | 'inactive';
1429
1430
  interface SceneCalloutDef {
package/dist/index.js CHANGED
@@ -4160,7 +4160,10 @@ var DEFAULT_LABELS = {
4160
4160
  pause: { en: "Pause story", es: "Pausar historia" },
4161
4161
  exit: { en: "Return to exploration", es: "Volver a explorar" }
4162
4162
  };
4163
- function StoryFrame({ steps, activeId, onStepChange, onExit, labels = {}, autoAdvanceMs = 5e3, className }) {
4163
+ function StoryFrame({ steps, activeId, onStepChange, onExit, labels = {}, autoAdvanceMs = 5e3, mobileStageMinHeight = 300, className }) {
4164
+ if (!Number.isInteger(mobileStageMinHeight) || mobileStageMinHeight < 300 || mobileStageMinHeight > 900) {
4165
+ throw new RangeError("StoryFrame mobileStageMinHeight must be an integer from 300 through 900 CSS pixels");
4166
+ }
4164
4167
  const lang = useShellLang();
4165
4168
  const [playing, setPlaying] = useState(false);
4166
4169
  const activeIndex = Math.max(0, steps.findIndex((step) => step.id === activeId));
@@ -4204,7 +4207,7 @@ function StoryFrame({ steps, activeId, onStepChange, onExit, labels = {}, autoAd
4204
4207
  }
4205
4208
  }
4206
4209
  if (!active) return null;
4207
- return /* @__PURE__ */ jsxs("section", { className: ["caos-story", "page-body", "wide", className].filter(Boolean).join(" "), "data-story": "", "data-story-step": active.id, "data-step-accent": active.accent, "aria-label": pick(copy.steps, lang), tabIndex: 0, onKeyDown, children: [
4210
+ return /* @__PURE__ */ jsxs("section", { className: ["caos-story", "page-body", "wide", className].filter(Boolean).join(" "), style: { "--caos-story-stage-minimum": `${mobileStageMinHeight}px` }, "data-story": "", "data-story-step": active.id, "data-step-accent": active.accent, "aria-label": pick(copy.steps, lang), tabIndex: 0, onKeyDown, children: [
4208
4211
  /* @__PURE__ */ jsxs("header", { className: "caos-story-header", children: [
4209
4212
  /* @__PURE__ */ jsxs("div", { className: "caos-story-heading", children: [
4210
4213
  /* @__PURE__ */ jsxs("span", { className: "caos-story-verb", children: [
package/gate/inpage.mjs CHANGED
@@ -923,8 +923,14 @@ export function installLib() {
923
923
  /** SHG-079..082: collect glyph evidence by real scrolling, then compute the existing metric once.
924
924
  * No DOM-presence fallback: every word fragment must fit the viewport, original clip bounds,
925
925
  * scrollports and hit tests. Text-node identities deduplicate overlapping observations.
926
+ *
927
+ * The budget scales with the document (CAOS_APP_SHELL#196, known defect 55). A fixed count stopped a long, fully
928
+ * readable page. Now every scrollport the walk visits grants the samples of its own grid (the positions along its
929
+ * scroll extent, y by x, at the walk's viewport step), and the time allowed is `baseMs` plus `msPerSample` for every
930
+ * sample granted. `baseSamples` covers the first sample and the clicks of the flip cards and carousels. Two ceilings
931
+ * stay absolute, so a page that never settles still fails with the same message: `ceilingSamples`, `ceilingMs`.
926
932
  */
927
- G.readDocument = async ({ maxSamples = 600, timeoutMs = 30000 } = {}) => {
933
+ G.readDocument = async ({ baseSamples = 100, baseMs = 30000, msPerSample = 500, ceilingSamples = 20000, ceilingMs = 900000 } = {}) => {
928
934
  const page = document.querySelector('[data-doc-page]');
929
935
  if (!page || !visible(page)) return null;
930
936
  const started = performance.now();
@@ -934,9 +940,11 @@ export function installLib() {
934
940
  const nodes = new Map(), witnessed = new Set(), flips = new Map(), decks = new Map();
935
941
  const inkContext = document.createElement('canvas').getContext('2d');
936
942
  if (!inkContext) throw new Error('document glyph measurement canvas is unavailable');
937
- let samples = 0;
943
+ let samples = 0, granted = baseSamples;
944
+ const budget = () => ({ samples: Math.min(granted, ceilingSamples), ms: Math.min(baseMs + granted * msPerSample, ceilingMs) });
938
945
  const check = () => {
939
- if (++samples > maxSamples || performance.now() - started > timeoutMs) throw new Error(`document traversal exceeded ${maxSamples} samples or ${timeoutMs}ms; active document remains unverified`);
946
+ const limit = budget();
947
+ if (++samples > limit.samples || performance.now() - started > limit.ms) throw new Error(`document traversal exceeded ${limit.samples} samples or ${Math.round(limit.ms)}ms; active document remains unverified`);
940
948
  if (!page.isConnected || state.some((e) => !e.isConnected || e.getAttribute('aria-selected') !== 'true')) throw new Error('document traversal changed the active page/tab');
941
949
  };
942
950
  const frame = () => new Promise((resolve) => nativeRaf ? nativeRaf(() => nativeRaf(resolve)) : setTimeout(resolve, 32));
@@ -1038,9 +1046,23 @@ export function installLib() {
1038
1046
  const first = Math.ceil(start), last = Math.floor(end) - 1;
1039
1047
  return first <= last ? Math.max(first, Math.min(last, start + (end - start) * fraction)) : (start + end) / 2;
1040
1048
  };
1049
+ // KaTeX builds a subscript from tables and a 2px strut (`.vlist-s`, a zero-width space in a 1px font) that overhang the
1050
+ // box of the math into the text beside it, so the hit test of the first glyph after the math (a comma) returns a
1051
+ // structural math element and not the paragraph. An element of the math that paints nothing at the point (no glyph of
1052
+ // its own, no background, no border, no drawing) cannot hide the text: look through it at what lies beneath, and judge
1053
+ // that by the same rule. A glyph of the math over the text is in the stack as a painting element, so it still fails.
1054
+ const paints = (e) => {
1055
+ if (/^(svg|img|canvas|video)$/i.test(e.tagName) || e.closest('svg')) return true;
1056
+ if ([...e.childNodes].some((c) => c.nodeType === 3 && /[^\s\u200b]/.test(c.nodeValue))) return true;
1057
+ const cs = getComputedStyle(e);
1058
+ return (parseColor(cs.backgroundColor)?.a ?? 0) > 0 || cs.backgroundImage !== 'none'
1059
+ || ['Top', 'Right', 'Bottom', 'Left'].some((side) => parseFloat(cs[`border${side}Width`]) > 0 && (parseColor(cs[`border${side}Color`])?.a ?? 0) > 0);
1060
+ };
1061
+ const inert = (e) => !parent.closest('.katex') && Boolean(e.closest('.katex')) && !paints(e);
1041
1062
  for (const q of [0.15, 0.5, 0.85]) for (const v of [0.15, 0.5, 0.85]) {
1042
1063
  const x = sampleAt(rect.left, rect.right, q), y = sampleAt(rect.top, rect.bottom, v);
1043
- const hit = document.elementFromPoint(x, y);
1064
+ let hit = document.elementFromPoint(x, y);
1065
+ if (hit && inert(hit)) hit = document.elementsFromPoint(x, y).find((e) => !inert(e)) ?? null;
1044
1066
  const transparentFallthrough = getComputedStyle(parent).pointerEvents === 'none' && hit?.contains(parent);
1045
1067
  if (!hit || !(hit === parent || (control ? parent.contains(hit) : transparentFallthrough)) || pseudoCover(parent,x,y)) return false;
1046
1068
  const link = parent.closest('a[href]');
@@ -1108,13 +1130,14 @@ export function installLib() {
1108
1130
  const positions = (max, size) => {
1109
1131
  const out = [0], step = Math.max(16, Math.floor(size * 0.65));
1110
1132
  for (let p = step; p < max; p += step) {
1111
- if (out.length > maxSamples) throw new Error('document scroll extent exceeds traversal budget');
1133
+ if (out.length > ceilingSamples) throw new Error('document scroll extent exceeds traversal budget');
1112
1134
  out.push(p);
1113
1135
  }
1114
1136
  if (max > 0) out.push(max);
1115
1137
  return out;
1116
1138
  };
1117
1139
  const sweep = async () => {
1140
+ granted += 1;
1118
1141
  sample();
1119
1142
  const containers = [...ancestorsUntil(page, document.body), page, ...page.querySelectorAll('*')].filter(scrollable);
1120
1143
  const de = document.scrollingElement;
@@ -1125,7 +1148,9 @@ export function installLib() {
1125
1148
  const s = getComputedStyle(e);
1126
1149
  const maxX = /auto|scroll/.test(s.overflowX) ? e.scrollWidth - e.clientWidth : 0;
1127
1150
  const maxY = e === de || /auto|scroll/.test(s.overflowY) ? e.scrollHeight - e.clientHeight : 0;
1128
- for (const y of positions(maxY, Math.min(e.clientHeight, innerHeight))) for (const x of positions(maxX, Math.min(e.clientWidth, innerWidth))) {
1151
+ const ys = positions(maxY, Math.min(e.clientHeight, innerHeight)), xs = positions(maxX, Math.min(e.clientWidth, innerWidth));
1152
+ granted += ys.length * xs.length; // this scrollport's grid is the work the budget must cover
1153
+ for (const y of ys) for (const x of xs) {
1129
1154
  // A settled card/animated illustration may reduce incidental overflow between frames.
1130
1155
  // Clamp to the real extent; coverage still requires every fragment, never a partial word.
1131
1156
  const wantedX = Math.min(x, e.scrollWidth - e.clientWidth), wantedY = Math.min(y, e.scrollHeight - e.clientHeight);
@@ -1184,7 +1209,10 @@ export function installLib() {
1184
1209
  }
1185
1210
  }
1186
1211
  docEvidence = evidence;
1187
- return G.docPage();
1212
+ const doc = G.docPage();
1213
+ // what the walk cost, so a gate run can report samples and time per state
1214
+ if (doc) doc.walk = { samples, budgetSamples: budget().samples, ms: Math.round(performance.now() - started), budgetMs: Math.round(budget().ms) };
1215
+ return doc;
1188
1216
  } finally {
1189
1217
  docEvidence = null;
1190
1218
  try {
package/gate/run.mjs CHANGED
@@ -127,6 +127,7 @@ export async function runGate(o) {
127
127
  const failures = [];
128
128
  const focusReported = new Set(); // G18: an App without a Focus control is reported once per route and mode
129
129
  const waivers = []; // ADR-0017 2.1: every waiver a documentation page declares, with its verdict
130
+ const docWalks = []; // the document walk's cost per state (samples and time against the budget it earned), for the report
130
131
  // The smallest measured values against each floor, so a pass shows its margin (and a CI font change that eats it
131
132
  // is visible before it fails).
132
133
  const minima = { drawnShare: null, stageFill: null };
@@ -642,7 +643,7 @@ export async function runGate(o) {
642
643
  if (s.walkState?.doc) {
643
644
  try {
644
645
  const d = await page.evaluate(() => window.__caosGate.readDocument());
645
- if (d) s.walkState.doc.push(d);
646
+ if (d) { s.walkState.doc.push(d); if (d.walk) docWalks.push({ route: where.route, lang: s.lang, trail: where.trail, ...d.walk }); }
646
647
  } catch (e) {
647
648
  fail('ADR-0017.2', where, `document readability traversal could not complete: ${e.message.split('\n')[0]}`);
648
649
  }
@@ -1288,6 +1289,7 @@ export async function runGate(o) {
1288
1289
  states,
1289
1290
  minima,
1290
1291
  waivers,
1292
+ docWalks,
1291
1293
  checks,
1292
1294
  captures: captures.length,
1293
1295
  failures,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fasl-work/caos-app-shell",
3
- "version": "0.22.0",
3
+ "version": "0.23.1",
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
@@ -1044,9 +1044,9 @@ svg.lucide { flex: none; }
1044
1044
  .caos-story button { min-height: 34px; padding: .3rem .5rem; font-size: .78rem; white-space: nowrap; }
1045
1045
  .caos-story-stepper { grid-auto-columns: minmax(90px, 1fr); grid-auto-flow: column; grid-template-columns: unset; overflow-x: auto; }
1046
1046
  .caos-story-step-headline { display: none; }
1047
- .caos-story-layout { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(46dvh, 1fr) auto; }
1047
+ .caos-story-layout { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(max(46dvh, var(--caos-story-stage-minimum, 300px)), auto) auto; }
1048
1048
  .caos-story-panel { max-height: 30dvh; }
1049
- .caos-story-stage { min-height: 300px; }
1049
+ .caos-story-stage { min-height: max(300px, var(--caos-story-stage-minimum, 300px)); }
1050
1050
  .caos-scene-callout { padding: .25rem .35rem; }
1051
1051
  }
1052
1052
  @media (prefers-reduced-motion: reduce) {