@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 +16 -0
- package/VERSION +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -2
- package/gate/inpage.mjs +35 -7
- package/gate/run.mjs +3 -1
- package/package.json +1 -1
- package/styles.css +2 -2
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.
|
|
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 ({
|
|
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
|
-
|
|
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
|
-
|
|
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 >
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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) {
|