@fasl-work/caos-app-shell 0.19.9 → 0.19.11
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 +18 -0
- package/VERSION +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -3
- package/gate/inpage.mjs +361 -15
- package/gate/run.mjs +6 -2
- package/package.json +100 -100
- package/reserved-classes.json +2 -0
- package/styles.css +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## [0.19.011] - 2026-11-01
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- Measure complete scrollable documents through actual readable viewport evidence rather than treating content below the current position as missing. Long executive briefs, source-link titles and technical areas retain the same content floors and hidden/clipped/font checks (issue #170).
|
|
6
|
+
- Retain reached native controls by identity, reject permanent clipping through the full document ancestor chain and reject opaque ancestor covers. Document inspection restores and verifies the original keyboard focus and scrolling state.
|
|
7
|
+
|
|
8
|
+
### Adoption
|
|
9
|
+
|
|
10
|
+
- Products pin npm `0.19.11` and run the installed gate against the built site. No component API, style override or scientific-data change is required.
|
|
11
|
+
|
|
12
|
+
## [0.19.010] - 2026-10-31
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- `SurfacePage document` gives a documentation surface a full-column heading and lede, with one root-owned inset and the existing tabs and Focus controls. OreFlow's Introduction needed document framing without a nested page wrapper or product overrides (issue #167, SHD-088). Products adopt the optional `document` prop and remove their document-width workaround; ordinary surfaces retain their current layout.
|
|
17
|
+
- Executive illustration checks measure transformed text bounds in the outer SVG viewBox and rendered font size through the complete text-to-screen matrix. Translated, scaled, rotated and nested labels are checked in their actual coordinate space; unavailable geometry fails explicitly. The existing bounds tolerance and 9 px font floor are unchanged (issue #167, SHG-076 to SHG-078).
|
|
18
|
+
|
|
1
19
|
## [0.19.009] - 2026-10-30
|
|
2
20
|
|
|
3
21
|
### Fixed
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.19.
|
|
1
|
+
0.19.011
|
package/dist/index.d.ts
CHANGED
|
@@ -888,6 +888,8 @@ interface CardDeckProps {
|
|
|
888
888
|
declare function CardDeck({ cards, flip, carouselAbove, ariaLabel, className, lang: forced }: CardDeckProps): react.JSX.Element;
|
|
889
889
|
|
|
890
890
|
interface SurfacePageProps {
|
|
891
|
+
/** Full-column document framing with the same tabs and Focus behavior. The root owns page padding. */
|
|
892
|
+
document?: boolean;
|
|
891
893
|
/** The route's heading; omit it when the header's active route already names the surface. */
|
|
892
894
|
title?: BiText;
|
|
893
895
|
/** One line under the title: what the surface is for. */
|
|
@@ -925,7 +927,7 @@ interface SurfacePageProps {
|
|
|
925
927
|
* `CaseWorkbench`'s, one piece of code (`useFocusMode`). The control sits at the end of the tab row (a slot beside the tab list) with
|
|
926
928
|
* its space reserved, so it adds no row and moves nothing; it is hidden while the open view has no stage.
|
|
927
929
|
*/
|
|
928
|
-
declare function SurfacePage({ title, lede, actions, tabs, value, onChange, deepLinkView, ariaLabel, instrument, focus, children }: SurfacePageProps): react.JSX.Element;
|
|
930
|
+
declare function SurfacePage({ document: documentMode, title, lede, actions, tabs, value, onChange, deepLinkView, ariaLabel, instrument, focus, children }: SurfacePageProps): react.JSX.Element;
|
|
929
931
|
|
|
930
932
|
/** Writes one query parameter with replaceState, keeping the router's history state (React Router keeps its entry
|
|
931
933
|
* key and index there; replacing it with null broke the index the back button relies on). */
|
package/dist/index.js
CHANGED
|
@@ -3052,7 +3052,7 @@ function textOf(node) {
|
|
|
3052
3052
|
return "";
|
|
3053
3053
|
}
|
|
3054
3054
|
var viewFocus = (tab) => !tab || !tab.focus ? null : tab.focus === true ? {} : tab.focus;
|
|
3055
|
-
function SurfacePage({ title, lede, actions, tabs, value, onChange, deepLinkView = true, ariaLabel, instrument, focus, children }) {
|
|
3055
|
+
function SurfacePage({ document: documentMode = false, title, lede, actions, tabs, value, onChange, deepLinkView = true, ariaLabel, instrument, focus, children }) {
|
|
3056
3056
|
const lang = useShellLang();
|
|
3057
3057
|
const placeAbove = useContext(PanelPathContext);
|
|
3058
3058
|
const ids = (tabs ?? []).map((t2) => t2.id);
|
|
@@ -3110,11 +3110,11 @@ function SurfacePage({ title, lede, actions, tabs, value, onChange, deepLinkView
|
|
|
3110
3110
|
) });
|
|
3111
3111
|
}
|
|
3112
3112
|
const head = title !== void 0 || lede !== void 0 || actions !== void 0 && actions !== null;
|
|
3113
|
-
const page = /* @__PURE__ */ jsxs("div", { className:
|
|
3113
|
+
const page = /* @__PURE__ */ jsxs("div", { className: `page-body wide caos-surface${documentMode ? " caos-surface-doc" : ""}`, "data-doc-page": documentMode ? "" : void 0, "data-surface": "", "data-view-param": reserve ? viewParam ?? "" : void 0, children: [
|
|
3114
3114
|
head && /* @__PURE__ */ jsxs("div", { className: "caos-surface-head", children: [
|
|
3115
3115
|
/* @__PURE__ */ jsxs("div", { className: "caos-surface-heading", children: [
|
|
3116
3116
|
title !== void 0 && /* @__PURE__ */ jsx("h1", { className: "caos-surface-title", children: pick(title, lang) }),
|
|
3117
|
-
lede !== void 0 && /* @__PURE__ */ jsx("p", { className:
|
|
3117
|
+
lede !== void 0 && /* @__PURE__ */ jsx("p", { className: `caos-surface-lede${documentMode ? " lede" : ""}`, children: lede })
|
|
3118
3118
|
] }),
|
|
3119
3119
|
actions !== void 0 && actions !== null && /* @__PURE__ */ jsx("div", { className: "caos-surface-actions", children: actions })
|
|
3120
3120
|
] }),
|
package/gate/inpage.mjs
CHANGED
|
@@ -9,6 +9,55 @@ export function installLib() {
|
|
|
9
9
|
G.raf = 0;
|
|
10
10
|
// The smallest rendered size of an illustration label (EXEC_LIMITS.minLabelPx of gate/areas.mjs; test/executive.test.ts keeps them equal).
|
|
11
11
|
const EXEC_MIN_LABEL_PX = 9;
|
|
12
|
+
/** Local text bounds mapped into the outer illustration's SVG user space, not screen pixels.
|
|
13
|
+
* All four corners matter for rotation/skew. Null is an explicit unmeasurable bound, never a pass.
|
|
14
|
+
* Screen matrices include nested viewports and root CSS transforms; the root inverse cancels them.
|
|
15
|
+
*/
|
|
16
|
+
G.svgTextBounds = (root, text) => {
|
|
17
|
+
try {
|
|
18
|
+
const rootMatrix = root.getScreenCTM();
|
|
19
|
+
const textMatrix = text.getScreenCTM();
|
|
20
|
+
const valid = (m) => m && ['a', 'b', 'c', 'd', 'e', 'f'].every((k) => Number.isFinite(m[k]))
|
|
21
|
+
&& m.a * m.d - m.b * m.c !== 0;
|
|
22
|
+
if (!valid(rootMatrix) || !valid(textMatrix)) return null;
|
|
23
|
+
const matrix = rootMatrix.inverse().multiply(textMatrix);
|
|
24
|
+
if (!valid(matrix)) return null;
|
|
25
|
+
const box = text.getBBox();
|
|
26
|
+
if (!['x', 'y', 'width', 'height'].every((k) => Number.isFinite(box[k]))) return null;
|
|
27
|
+
const corners = [[box.x, box.y], [box.x + box.width, box.y],
|
|
28
|
+
[box.x, box.y + box.height], [box.x + box.width, box.y + box.height]]
|
|
29
|
+
.map(([x, y]) => ({ x: matrix.a * x + matrix.c * y + matrix.e,
|
|
30
|
+
y: matrix.b * x + matrix.d * y + matrix.f }));
|
|
31
|
+
if (corners.some((p) => !Number.isFinite(p.x) || !Number.isFinite(p.y))) return null;
|
|
32
|
+
const x = Math.min(...corners.map((p) => p.x));
|
|
33
|
+
const y = Math.min(...corners.map((p) => p.y));
|
|
34
|
+
return { x, y, width: Math.max(...corners.map((p) => p.x)) - x,
|
|
35
|
+
height: Math.max(...corners.map((p) => p.y)) - y };
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
/** Conservative rendered font size: the least stretch of the full text-to-screen mapping.
|
|
41
|
+
* Unlike a root bounding-rect ratio, singular values retain group/text/viewport scaling and
|
|
42
|
+
* ignore pure rotation. Normalize first, then use |det| / sigmaMax to avoid subtracting eigenvalues.
|
|
43
|
+
*/
|
|
44
|
+
const svgTextFontPx = (text) => {
|
|
45
|
+
try {
|
|
46
|
+
const m = text.getScreenCTM();
|
|
47
|
+
const font = parseFloat(getComputedStyle(text).fontSize);
|
|
48
|
+
if (!m || m.is2D === false || !['a', 'b', 'c', 'd'].every((k) => Number.isFinite(m[k]))
|
|
49
|
+
|| !Number.isFinite(font) || font <= 0) return null;
|
|
50
|
+
const norm = Math.max(Math.abs(m.a), Math.abs(m.b), Math.abs(m.c), Math.abs(m.d));
|
|
51
|
+
if (norm === 0) return null;
|
|
52
|
+
const a = m.a / norm, b = m.b / norm, c = m.c / norm, d = m.d / norm;
|
|
53
|
+
const largest = (Math.hypot(a + d, b - c) + Math.hypot(a - d, b + c)) / 2;
|
|
54
|
+
const scale = Math.abs(a * d - b * c) / largest * norm;
|
|
55
|
+
const px = font * scale;
|
|
56
|
+
return Number.isFinite(px) && px > 0 ? px : null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
12
61
|
G.counting = false;
|
|
13
62
|
const nativeRaf = window.requestAnimationFrame ? window.requestAnimationFrame.bind(window) : null;
|
|
14
63
|
if (nativeRaf) {
|
|
@@ -51,7 +100,7 @@ export function installLib() {
|
|
|
51
100
|
return { left: ar.left + o.left, top: ar.top + o.top, right: ar.right - o.right, bottom: ar.bottom - o.bottom };
|
|
52
101
|
};
|
|
53
102
|
/**
|
|
54
|
-
* The part of `rect` a reader can see: inside `within`, inside every ancestor from `el`
|
|
103
|
+
* The part of `rect` a reader can see: inside `within`, inside every ancestor from `el` to the document root that
|
|
55
104
|
* hides its overflow (`el` itself only when `self`: its overflow cuts its own text, not its own box) and inside every
|
|
56
105
|
* `clip-path` of `el` and of those ancestors. Null when nothing is left.
|
|
57
106
|
*/
|
|
@@ -61,7 +110,7 @@ export function installLib() {
|
|
|
61
110
|
let top = Math.max(rect.top, b.top);
|
|
62
111
|
let right = Math.min(rect.right, b.right);
|
|
63
112
|
let bottom = Math.min(rect.bottom, b.bottom);
|
|
64
|
-
for (let a = el; a
|
|
113
|
+
for (let a = el; a; a = a.parentElement) {
|
|
65
114
|
const s = getComputedStyle(a);
|
|
66
115
|
const ar = a.getBoundingClientRect();
|
|
67
116
|
if (a !== el || self) {
|
|
@@ -514,6 +563,8 @@ export function installLib() {
|
|
|
514
563
|
// ---- 0.11.0: the required areas of the documentation pages (ADR-0017 section 2.1) ------------------------------
|
|
515
564
|
|
|
516
565
|
let docBound = null; // the page being measured: what lies outside it, or is clipped out of it, is not read
|
|
566
|
+
let docEvidence = null; // scoped to one completed real traversal, never shared across active tabs
|
|
567
|
+
const docSeen = (el) => docEvidence ? docEvidence.complete(el) : readable(el, docBound, 4, 4);
|
|
517
568
|
const wordsOf = (el) => {
|
|
518
569
|
// the words a reader reads: visible text outside equations; `linkWords` of them inside links
|
|
519
570
|
let words = 0;
|
|
@@ -521,7 +572,7 @@ export function installLib() {
|
|
|
521
572
|
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
522
573
|
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
|
|
523
574
|
const parent = n.parentElement;
|
|
524
|
-
if (!parent || parent.closest('.katex, script, style') || !readable(parent, docBound, 4, 4, n)) continue;
|
|
575
|
+
if (!parent || parent.closest('.katex, script, style') || !(docEvidence ? docEvidence.nodeComplete(n) : readable(parent, docBound, 4, 4, n))) continue;
|
|
525
576
|
const k = (n.nodeValue || '').split(/\s+/).filter(Boolean).length;
|
|
526
577
|
words += k;
|
|
527
578
|
if (parent.closest('a')) linkWords += k;
|
|
@@ -545,19 +596,19 @@ export function installLib() {
|
|
|
545
596
|
}
|
|
546
597
|
return { label: labels.join(' > '), key: ids.join('/'), head: ids[0] ?? null };
|
|
547
598
|
};
|
|
548
|
-
const textOf = (el) => (el.innerText || '').trim().replace(/\s+/g, ' '); //
|
|
599
|
+
const textOf = (el) => (docEvidence ? docEvidence.text(el) : el.innerText || '').trim().replace(/\s+/g, ' '); // only witnessed text during traversal
|
|
549
600
|
const norm = (s) => String(s || '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
550
601
|
/** A part of a statement a reader sees: the declared text is in the rendered text, in the readable part (`sel`) when the element has one. */
|
|
551
602
|
const statPart = (el, want, sel) => {
|
|
552
603
|
if (!want) return false;
|
|
553
604
|
const kids = [...el.querySelectorAll(sel)];
|
|
554
|
-
|
|
605
|
+
return kids.length ? kids.some((k) => docSeen(k) && norm(textOf(k)).includes(want)) : norm(textOf(el)).includes(want);
|
|
555
606
|
};
|
|
556
607
|
/** A quantified statement: visible, a digit in its value, and the unit and the source it declares readable beside it (the `Stat` component). */
|
|
557
608
|
const validStat = (el) => {
|
|
558
|
-
if (!
|
|
609
|
+
if (!docSeen(el)) return false;
|
|
559
610
|
const value = el.querySelector('.caos-stat-value');
|
|
560
|
-
return /\d/.test((value
|
|
611
|
+
return /\d/.test(textOf(value || el)) && statPart(el, norm(el.getAttribute('data-stat-unit')), '.caos-stat-unit') && statPart(el, norm(el.getAttribute('data-stat-source')), '.caos-stat-source');
|
|
561
612
|
};
|
|
562
613
|
|
|
563
614
|
/**
|
|
@@ -570,7 +621,7 @@ export function installLib() {
|
|
|
570
621
|
const page = document.querySelector('[data-doc-page]');
|
|
571
622
|
if (!page || !visible(page)) return null;
|
|
572
623
|
docBound = page;
|
|
573
|
-
const seen = (el) =>
|
|
624
|
+
const seen = (el) => docSeen(el);
|
|
574
625
|
const lede = page.querySelector('.lede');
|
|
575
626
|
// the document column: the page body without its padding; the lede and the executive view take all of it (Felipe, 2026-10-13)
|
|
576
627
|
const pageStyle = getComputedStyle(page);
|
|
@@ -694,13 +745,14 @@ export function installLib() {
|
|
|
694
745
|
// A card the carousel's window hides (`hidden` on its item) is out of sight by design: G16 walks the carousel and fails one it
|
|
695
746
|
// cannot reach. Any other card that is not seen is not shown, as before 0.14.0.
|
|
696
747
|
const hiddenByDeck = (el) => {
|
|
748
|
+
if (docEvidence) return false; // a real control must first expose every card
|
|
697
749
|
const li = el.closest('[data-deck-card]');
|
|
698
750
|
return Boolean(li) && li.hidden && Boolean(li.closest('[data-deck-mode="carousel"]'));
|
|
699
751
|
};
|
|
700
752
|
// The back of a flip card is content the reader gets by turning the card: read every field with the cards turned too (the
|
|
701
753
|
// same `seen` and `innerText` as for any text), and call a field readable when it is seen in either face.
|
|
702
754
|
const turned = new Map();
|
|
703
|
-
withFlipsTurned(() => {
|
|
755
|
+
if (!docEvidence) withFlipsTurned(() => {
|
|
704
756
|
for (const e of root.querySelectorAll('[data-exec-field], [data-exec-cdef]')) turned.set(e, { text: textOf(e), seen: seen(e) });
|
|
705
757
|
});
|
|
706
758
|
const fullText = (e) => textOf(e) || (turned.get(e) ? turned.get(e).text : '');
|
|
@@ -730,18 +782,18 @@ export function installLib() {
|
|
|
730
782
|
if (!svg) return info;
|
|
731
783
|
info.literal = literalsOf(svg);
|
|
732
784
|
const vb = svg.viewBox && svg.viewBox.baseVal;
|
|
733
|
-
const w = svg.getBoundingClientRect().width;
|
|
734
|
-
const scale = vb && vb.width ? w / vb.width : 1;
|
|
735
785
|
for (const t of svg.querySelectorAll('text')) {
|
|
736
786
|
const label = (t.textContent || '').trim();
|
|
737
787
|
if (!label || !visible(t)) continue;
|
|
738
788
|
info.labels += 1;
|
|
739
789
|
if (vb && vb.width) {
|
|
740
|
-
const b =
|
|
741
|
-
if (b
|
|
790
|
+
const b = G.svgTextBounds(svg, t);
|
|
791
|
+
if (!b) info.outside.push(`${label.slice(0, 40)} (bounds unavailable)`);
|
|
792
|
+
else if (b.x < vb.x - 1 || b.y < vb.y - 1 || b.x + b.width > vb.x + vb.width + 1 || b.y + b.height > vb.y + vb.height + 1) info.outside.push(label.slice(0, 40));
|
|
742
793
|
}
|
|
743
|
-
const px =
|
|
744
|
-
if (px
|
|
794
|
+
const px = svgTextFontPx(t);
|
|
795
|
+
if (px === null) info.small.push(`"${label.slice(0, 30)}" (font scale unavailable)`);
|
|
796
|
+
else if (px < EXEC_MIN_LABEL_PX) info.small.push(`"${label.slice(0, 30)}" at ${px.toFixed(1)}px`);
|
|
745
797
|
}
|
|
746
798
|
return info;
|
|
747
799
|
});
|
|
@@ -840,6 +892,300 @@ export function installLib() {
|
|
|
840
892
|
};
|
|
841
893
|
};
|
|
842
894
|
|
|
895
|
+
/** SHG-079..082: collect glyph evidence by real scrolling, then compute the existing metric once.
|
|
896
|
+
* No DOM-presence fallback: every word fragment must fit the viewport, original clip bounds,
|
|
897
|
+
* scrollports and hit tests. Text-node identities deduplicate overlapping observations.
|
|
898
|
+
*/
|
|
899
|
+
G.readDocument = async ({ maxSamples = 600, timeoutMs = 30000 } = {}) => {
|
|
900
|
+
const page = document.querySelector('[data-doc-page]');
|
|
901
|
+
if (!page || !visible(page)) return null;
|
|
902
|
+
const started = performance.now();
|
|
903
|
+
const saved = new Map([...document.querySelectorAll('*')].map((e) => [e, [e.scrollLeft, e.scrollTop]]));
|
|
904
|
+
const windowPosition = [scrollX, scrollY], focused = document.activeElement;
|
|
905
|
+
const state = [...page.querySelectorAll('[role="tab"][aria-selected="true"]')];
|
|
906
|
+
const nodes = new Map(), witnessed = new Set(), flips = new Map(), decks = new Map();
|
|
907
|
+
const inkContext = document.createElement('canvas').getContext('2d');
|
|
908
|
+
if (!inkContext) throw new Error('document glyph measurement canvas is unavailable');
|
|
909
|
+
let samples = 0;
|
|
910
|
+
const check = () => {
|
|
911
|
+
if (++samples > maxSamples || performance.now() - started > timeoutMs) throw new Error(`document traversal exceeded ${maxSamples} samples or ${timeoutMs}ms; active document remains unverified`);
|
|
912
|
+
if (!page.isConnected || state.some((e) => !e.isConnected || e.getAttribute('aria-selected') !== 'true')) throw new Error('document traversal changed the active page/tab');
|
|
913
|
+
};
|
|
914
|
+
const frame = () => new Promise((resolve) => nativeRaf ? nativeRaf(() => nativeRaf(resolve)) : setTimeout(resolve, 32));
|
|
915
|
+
const textNodes = (el) => textNodesOf(el).filter((n) => !n.parentElement.closest('.katex, svg, .sr-only, [data-deck-position]'));
|
|
916
|
+
const available = (el) => visible(el) && !el.closest('[hidden], [inert], [aria-hidden="true"]');
|
|
917
|
+
const nodeComplete = (n) => {
|
|
918
|
+
const row = nodes.get(n);
|
|
919
|
+
return Boolean(row && row.value === n.nodeValue && row.parts.length && row.parts.every((p) => p.count > 0 && p.seen.size === p.count));
|
|
920
|
+
};
|
|
921
|
+
const evidence = {
|
|
922
|
+
nodeComplete,
|
|
923
|
+
text: (el) => el ? textNodes(el).filter(nodeComplete).map((n) => n.nodeValue).join(' ') : '',
|
|
924
|
+
complete: (el) => {
|
|
925
|
+
if (!el) return false;
|
|
926
|
+
if (el.matches('[data-control]') && !witnessed.has(el)) return false;
|
|
927
|
+
const required = textNodes(el).filter((n) => nodes.has(n) || available(n.parentElement));
|
|
928
|
+
return required.length ? required.every(nodeComplete) : witnessed.has(el);
|
|
929
|
+
},
|
|
930
|
+
};
|
|
931
|
+
const pseudoPaint = new Map();
|
|
932
|
+
// This box model supports only ordinary, unmapped coordinates. Apply the same identity
|
|
933
|
+
// contract to the pseudo and its ancestors: separate CSS transform channels do not appear
|
|
934
|
+
// in computed transform, and motion/filter/zoom can also move or expand actual paint.
|
|
935
|
+
const knownPaintCoordinates = (s) =>
|
|
936
|
+
['transform', 'translate', 'rotate', 'scale', 'offsetPath', 'filter', 'backdropFilter', 'perspective']
|
|
937
|
+
.every((key) => !s[key] || s[key] === 'none')
|
|
938
|
+
&& (!s.zoom || Number(s.zoom) === 1)
|
|
939
|
+
&& !/layout|paint|strict|content/.test(s.contain)
|
|
940
|
+
&& !/transform|translate|rotate|scale|offset|filter|perspective|contain|content-visibility|zoom/.test(s.willChange)
|
|
941
|
+
&& s.contentVisibility !== 'auto'
|
|
942
|
+
&& (!s.transformStyle || s.transformStyle === 'flat')
|
|
943
|
+
&& (!s.positionAnchor || ['auto', 'normal'].includes(s.positionAnchor))
|
|
944
|
+
&& (!s.positionArea || s.positionArea === 'none')
|
|
945
|
+
&& (!s.positionTryFallbacks || s.positionTryFallbacks === 'none');
|
|
946
|
+
const pseudoCover = (parent, x, y) => {
|
|
947
|
+
for (let a = parent; a; a = a.parentElement) {
|
|
948
|
+
let boxes = pseudoPaint.get(a);
|
|
949
|
+
if (!boxes) {
|
|
950
|
+
boxes = [];
|
|
951
|
+
for (const pseudo of ['::before', '::after']) {
|
|
952
|
+
const s = getComputedStyle(a, pseudo);
|
|
953
|
+
if (s.content === 'none' || s.content === 'normal' || s.display === 'none' || s.visibility !== 'visible' || Number(s.opacity) === 0 || Number(s.zIndex) < 0) continue;
|
|
954
|
+
const background = parseColor(s.backgroundColor);
|
|
955
|
+
const outline = !['none', 'hidden'].includes(s.outlineStyle) && parseFloat(s.outlineWidth) > 0;
|
|
956
|
+
const painted = (background && background.a > 0) || s.backgroundImage !== 'none' || s.boxShadow !== 'none'
|
|
957
|
+
|| outline || s.borderImageSource !== 'none'
|
|
958
|
+
|| ['Top', 'Right', 'Bottom', 'Left'].some((side) => parseFloat(s[`border${side}Width`]) > 0 && (parseColor(s[`border${side}Color`])?.a ?? 0) > 0);
|
|
959
|
+
if (!painted) continue;
|
|
960
|
+
// Unmapped or expanding paint is unknown, even when its nominal layout box is remote.
|
|
961
|
+
if (!['absolute', 'fixed'].includes(s.position) || !knownPaintCoordinates(s)
|
|
962
|
+
|| s.boxShadow !== 'none' || outline || s.borderImageSource !== 'none') { boxes.push(null); continue; }
|
|
963
|
+
// Local CSS dimensions cannot be combined with a viewport-space origin when any
|
|
964
|
+
// ancestor changes that mapping or establishes a non-viewport fixed containing block.
|
|
965
|
+
// We do not have a complete paint transform here: unknown positioned paint fails closed.
|
|
966
|
+
let ambiguous = false;
|
|
967
|
+
for (let ancestor = a; ancestor; ancestor = ancestor.parentElement) {
|
|
968
|
+
const cs = getComputedStyle(ancestor);
|
|
969
|
+
if (!knownPaintCoordinates(cs)) { ambiguous = true; break; }
|
|
970
|
+
}
|
|
971
|
+
if (ambiguous) { boxes.push(null); continue; }
|
|
972
|
+
let cb = s.position === 'fixed' ? null : a;
|
|
973
|
+
while (cb && getComputedStyle(cb).position === 'static' && getComputedStyle(cb).transform === 'none') cb = cb.parentElement;
|
|
974
|
+
if (cb && (cb.scrollLeft || cb.scrollTop)) { boxes.push(null); continue; }
|
|
975
|
+
const r = cb?.getBoundingClientRect();
|
|
976
|
+
const base = r ? { left:r.left + cb.clientLeft, top:r.top + cb.clientTop, width:cb.clientWidth, height:cb.clientHeight }
|
|
977
|
+
: { left:s.position === 'fixed' ? 0 : -scrollX, top:s.position === 'fixed' ? 0 : -scrollY, width:innerWidth, height:innerHeight };
|
|
978
|
+
let width = parseFloat(s.width), height = parseFloat(s.height);
|
|
979
|
+
if (s.boxSizing !== 'border-box') {
|
|
980
|
+
width += parseFloat(s.paddingLeft) + parseFloat(s.paddingRight) + parseFloat(s.borderLeftWidth) + parseFloat(s.borderRightWidth);
|
|
981
|
+
height += parseFloat(s.paddingTop) + parseFloat(s.paddingBottom) + parseFloat(s.borderTopWidth) + parseFloat(s.borderBottomWidth);
|
|
982
|
+
}
|
|
983
|
+
const left = parseFloat(s.left), right = parseFloat(s.right), top = parseFloat(s.top), bottom = parseFloat(s.bottom);
|
|
984
|
+
const px = Number.isFinite(left) ? base.left + left + parseFloat(s.marginLeft) : base.left + base.width - right - width - parseFloat(s.marginRight);
|
|
985
|
+
const py = Number.isFinite(top) ? base.top + top + parseFloat(s.marginTop) : base.top + base.height - bottom - height - parseFloat(s.marginBottom);
|
|
986
|
+
if (![px,py,width,height].every(Number.isFinite)) { boxes.push(null); continue; }
|
|
987
|
+
boxes.push({ left:px, right:px+width, top:py, bottom:py+height });
|
|
988
|
+
}
|
|
989
|
+
pseudoPaint.set(a, boxes);
|
|
990
|
+
}
|
|
991
|
+
if (boxes.some((b) => !b || x >= b.left && x <= b.right && y >= b.top && y <= b.bottom)) return true;
|
|
992
|
+
}
|
|
993
|
+
return false;
|
|
994
|
+
};
|
|
995
|
+
const clearFragment = (rect, parent, control = false) => {
|
|
996
|
+
const b = clipRect(rect, parent, page, true);
|
|
997
|
+
if (!b || rect.width < 0.1 || rect.height < 0.1) return false;
|
|
998
|
+
let left = Math.max(b.left, 0), top = Math.max(b.top, 0);
|
|
999
|
+
let right = Math.min(b.right, innerWidth), bottom = Math.min(b.bottom, innerHeight);
|
|
1000
|
+
for (let a = parent; a; a = a.parentElement) {
|
|
1001
|
+
const s = getComputedStyle(a), r = a.getBoundingClientRect();
|
|
1002
|
+
if (/auto|scroll/.test(s.overflowX)) { left = Math.max(left, r.left + a.clientLeft); right = Math.min(right, r.left + a.clientLeft + a.clientWidth); }
|
|
1003
|
+
if (/auto|scroll/.test(s.overflowY)) { top = Math.max(top, r.top + a.clientTop); bottom = Math.min(bottom, r.top + a.clientTop + a.clientHeight); }
|
|
1004
|
+
}
|
|
1005
|
+
if (left > rect.left + 0.5 || top > rect.top + 0.5 || right < rect.right - 0.5 || bottom < rect.bottom - 0.5) return false;
|
|
1006
|
+
// Hit testing quantizes fractional inline boxes to CSS pixels. Keep samples within fully
|
|
1007
|
+
// contained pixel cells: a narrow visible separator must not probe the adjacent inline box.
|
|
1008
|
+
// Full glyph extent was checked above; this does not admit clipped or covered fragments.
|
|
1009
|
+
const sampleAt = (start, end, fraction) => {
|
|
1010
|
+
const first = Math.ceil(start), last = Math.floor(end) - 1;
|
|
1011
|
+
return first <= last ? Math.max(first, Math.min(last, start + (end - start) * fraction)) : (start + end) / 2;
|
|
1012
|
+
};
|
|
1013
|
+
for (const q of [0.15, 0.5, 0.85]) for (const v of [0.15, 0.5, 0.85]) {
|
|
1014
|
+
const x = sampleAt(rect.left, rect.right, q), y = sampleAt(rect.top, rect.bottom, v);
|
|
1015
|
+
const hit = document.elementFromPoint(x, y);
|
|
1016
|
+
const transparentFallthrough = getComputedStyle(parent).pointerEvents === 'none' && hit?.contains(parent);
|
|
1017
|
+
if (!hit || !(hit === parent || (control ? parent.contains(hit) : transparentFallthrough)) || pseudoCover(parent,x,y)) return false;
|
|
1018
|
+
const link = parent.closest('a[href]');
|
|
1019
|
+
if (link && !(hit === link || link.contains(hit))) return false;
|
|
1020
|
+
}
|
|
1021
|
+
return true;
|
|
1022
|
+
};
|
|
1023
|
+
const sample = () => {
|
|
1024
|
+
check();
|
|
1025
|
+
pseudoPaint.clear();
|
|
1026
|
+
for (const n of textNodes(page)) {
|
|
1027
|
+
const parent = n.parentElement;
|
|
1028
|
+
if (!available(parent)) continue;
|
|
1029
|
+
let row = nodes.get(n);
|
|
1030
|
+
if (row && row.value !== n.nodeValue) throw new Error(`document text changed during traversal (${parent.tagName}: ${row.value.slice(0, 40)}); retry a settled state`);
|
|
1031
|
+
if (!row) {
|
|
1032
|
+
row = { value: n.nodeValue, parts: [...n.nodeValue.matchAll(/\S+/g)].map((m) => ({ start: m.index, end: m.index + m[0].length, count: 0, seen: new Set() })) };
|
|
1033
|
+
nodes.set(n, row);
|
|
1034
|
+
}
|
|
1035
|
+
if (fontPx(parent) < FONT_FLOOR) continue;
|
|
1036
|
+
const style = getComputedStyle(parent);
|
|
1037
|
+
inkContext.font = `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
|
|
1038
|
+
for (const part of row.parts) {
|
|
1039
|
+
if (part.count > 0 && part.seen.size === part.count) continue;
|
|
1040
|
+
const r = document.createRange(); r.setStart(n, part.start); r.setEnd(n, part.end);
|
|
1041
|
+
const boxes = [...r.getClientRects()].filter((b) => b.width > 0 && b.height > 0);
|
|
1042
|
+
if (part.count && part.count !== boxes.length) throw new Error('document layout changed during traversal; retry a settled state');
|
|
1043
|
+
part.count = boxes.length;
|
|
1044
|
+
const ink = inkContext.measureText(n.nodeValue.slice(part.start, part.end));
|
|
1045
|
+
const fontHeight = ink.fontBoundingBoxAscent + ink.fontBoundingBoxDescent;
|
|
1046
|
+
if (!Number.isFinite(fontHeight) || fontHeight <= 0) throw new Error('document glyph font metrics are unavailable');
|
|
1047
|
+
boxes.forEach((b, i) => {
|
|
1048
|
+
// Range boxes include font leading, which may extend above a tight heading's line box.
|
|
1049
|
+
// Judge the actual ink ascent/descent, not invisible leading; retain all clip/hit checks.
|
|
1050
|
+
const scale = b.height / fontHeight;
|
|
1051
|
+
if (fontPx(parent) * scale < FONT_FLOOR) return;
|
|
1052
|
+
const baseline = b.bottom - ink.fontBoundingBoxDescent * scale;
|
|
1053
|
+
const top = baseline - ink.actualBoundingBoxAscent * scale;
|
|
1054
|
+
const bottom = baseline + ink.actualBoundingBoxDescent * scale;
|
|
1055
|
+
if (clearFragment({left:b.left,right:b.right,top,bottom,width:b.width,height:bottom-top}, parent)) part.seen.add(i);
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
for (const e of page.querySelectorAll('figure, svg, canvas, img, .equation')) {
|
|
1060
|
+
const b = e.getBoundingClientRect();
|
|
1061
|
+
if (available(e) && b.bottom > 0 && b.top < innerHeight && readable(e, page, 4, 4)) witnessed.add(e);
|
|
1062
|
+
}
|
|
1063
|
+
// Native inputs have no text nodes. Witness the actual control, never its metadata alone.
|
|
1064
|
+
for (const e of page.querySelectorAll('[data-control]')) {
|
|
1065
|
+
const target = e.matches('input, select, button, textarea, [role="slider"]') ? e
|
|
1066
|
+
: e.querySelector('input, select, button, textarea, [role="slider"]') ?? e;
|
|
1067
|
+
if (available(e) && available(target) && readable(target, page, 4, 4)
|
|
1068
|
+
&& clearFragment(target.getBoundingClientRect(), target, true)) witnessed.add(e);
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
const scrollable = (e) => {
|
|
1072
|
+
const s = getComputedStyle(e);
|
|
1073
|
+
return available(e) && ((/^(auto|scroll)$/.test(s.overflowY) && e.scrollHeight > e.clientHeight + 1) || (/^(auto|scroll)$/.test(s.overflowX) && e.scrollWidth > e.clientWidth + 1));
|
|
1074
|
+
};
|
|
1075
|
+
const bringSafely = (e) => {
|
|
1076
|
+
const x = scrollX, y = scrollY;
|
|
1077
|
+
bring(e); // only user-scrollable element ancestors; protect an unscrollable document too
|
|
1078
|
+
if (/hidden|clip/.test(getComputedStyle(document.documentElement).overflowY) || /hidden|clip/.test(getComputedStyle(document.body).overflowY)) winScroll(x, y);
|
|
1079
|
+
};
|
|
1080
|
+
const positions = (max, size) => {
|
|
1081
|
+
const out = [0], step = Math.max(16, Math.floor(size * 0.65));
|
|
1082
|
+
for (let p = step; p < max; p += step) {
|
|
1083
|
+
if (out.length > maxSamples) throw new Error('document scroll extent exceeds traversal budget');
|
|
1084
|
+
out.push(p);
|
|
1085
|
+
}
|
|
1086
|
+
if (max > 0) out.push(max);
|
|
1087
|
+
return out;
|
|
1088
|
+
};
|
|
1089
|
+
const sweep = async () => {
|
|
1090
|
+
sample();
|
|
1091
|
+
const containers = [...ancestorsUntil(page, document.body), page, ...page.querySelectorAll('*')].filter(scrollable);
|
|
1092
|
+
const de = document.scrollingElement;
|
|
1093
|
+
if (de && de.scrollHeight > innerHeight + 1 && !/hidden|clip/.test(getComputedStyle(de).overflowY) && !/hidden|clip/.test(getComputedStyle(document.body).overflowY)) containers.unshift(de);
|
|
1094
|
+
const unique = [...new Set(containers)];
|
|
1095
|
+
const owner = (e) => unique.find((p) => p !== e && p.contains(e) && !unique.some((q) => q !== p && q !== e && p.contains(q) && q.contains(e)));
|
|
1096
|
+
const walk = async (e) => {
|
|
1097
|
+
const s = getComputedStyle(e);
|
|
1098
|
+
const maxX = /auto|scroll/.test(s.overflowX) ? e.scrollWidth - e.clientWidth : 0;
|
|
1099
|
+
const maxY = e === de || /auto|scroll/.test(s.overflowY) ? e.scrollHeight - e.clientHeight : 0;
|
|
1100
|
+
for (const y of positions(maxY, Math.min(e.clientHeight, innerHeight))) for (const x of positions(maxX, Math.min(e.clientWidth, innerWidth))) {
|
|
1101
|
+
// A settled card/animated illustration may reduce incidental overflow between frames.
|
|
1102
|
+
// Clamp to the real extent; coverage still requires every fragment, never a partial word.
|
|
1103
|
+
const wantedX = Math.min(x, e.scrollWidth - e.clientWidth), wantedY = Math.min(y, e.scrollHeight - e.clientHeight);
|
|
1104
|
+
setScroll(e, wantedX, wantedY); await frame();
|
|
1105
|
+
if (Math.abs(e.scrollLeft - Math.min(wantedX, e.scrollWidth-e.clientWidth)) > 1 || Math.abs(e.scrollTop - Math.min(wantedY, e.scrollHeight-e.clientHeight)) > 1) throw new Error(`document scroller ${e.className} could not reach its requested position`);
|
|
1106
|
+
sample();
|
|
1107
|
+
for (const child of unique.filter((c) => owner(c) === e)) {
|
|
1108
|
+
const r = child.getBoundingClientRect();
|
|
1109
|
+
if (r.bottom > 0 && r.top < innerHeight && r.right > 0 && r.left < innerWidth) await walk(child);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
for (const e of unique.filter((e) => !owner(e))) { bringSafely(e); await walk(e); }
|
|
1114
|
+
};
|
|
1115
|
+
const click = async (button) => {
|
|
1116
|
+
if (!button || !available(button) || button.disabled) throw new Error('document card control is unavailable');
|
|
1117
|
+
bringSafely(button); await frame();
|
|
1118
|
+
const r = button.getBoundingClientRect(), hit = document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2);
|
|
1119
|
+
if (!hit || !(hit === button || button.contains(hit))) throw new Error('document card control is covered or clipped');
|
|
1120
|
+
button.click(); await frame();
|
|
1121
|
+
// Await the real transition, without changing attributes/styles or exposing a hidden face.
|
|
1122
|
+
const running = document.getAnimations().filter((a) => a.playState === 'running' && a.effect?.target?.closest?.('[data-flip-card], [data-card-deck]'));
|
|
1123
|
+
let timer;
|
|
1124
|
+
try {
|
|
1125
|
+
await Promise.race([Promise.all(running.map((a) => a.finished)), new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('document card transition did not settle')), 1500); })]);
|
|
1126
|
+
} finally { clearTimeout(timer); }
|
|
1127
|
+
await frame();
|
|
1128
|
+
};
|
|
1129
|
+
const signature = (deck) => [...deck.querySelectorAll('[data-deck-card]')].map((c, i) => c.hidden ? '' : i).join(',');
|
|
1130
|
+
const visitFlips = async () => {
|
|
1131
|
+
for (const card of page.querySelectorAll('[data-flip-card]')) {
|
|
1132
|
+
if (!available(card)) continue;
|
|
1133
|
+
if (!flips.has(card)) flips.set(card, card.getAttribute('data-flip-open'));
|
|
1134
|
+
const open = card.getAttribute('data-flip-open') === 'true';
|
|
1135
|
+
await click(card.querySelector(`[data-flip-toggle="${open ? 'back' : 'front'}"]`));
|
|
1136
|
+
if ((card.getAttribute('data-flip-open') === 'true') === open) throw new Error('document flip control did not turn the card');
|
|
1137
|
+
await sweep();
|
|
1138
|
+
await click(card.querySelector(`[data-flip-toggle="${open ? 'front' : 'back'}"]`));
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
try {
|
|
1142
|
+
await sweep(); await visitFlips();
|
|
1143
|
+
for (const deck of page.querySelectorAll('[data-deck-mode="carousel"]')) {
|
|
1144
|
+
decks.set(deck, signature(deck));
|
|
1145
|
+
const count = deck.querySelectorAll('[data-deck-card]').length;
|
|
1146
|
+
for (const direction of ['prev', 'next']) {
|
|
1147
|
+
for (let i = 0; ; i++) {
|
|
1148
|
+
const button = deck.querySelector(`[data-deck-${direction}]`);
|
|
1149
|
+
if (!button) throw new Error('document carousel has no traversal control');
|
|
1150
|
+
if (button.getAttribute('aria-disabled') === 'true') break;
|
|
1151
|
+
if (i > count) throw new Error('document carousel traversal did not terminate');
|
|
1152
|
+
const before = signature(deck); await click(button);
|
|
1153
|
+
if (signature(deck) === before) throw new Error('document carousel control did not expose another card');
|
|
1154
|
+
await sweep(); await visitFlips();
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
docEvidence = evidence;
|
|
1159
|
+
return G.docPage();
|
|
1160
|
+
} finally {
|
|
1161
|
+
docEvidence = null;
|
|
1162
|
+
try {
|
|
1163
|
+
for (const [card, open] of flips) if (card.isConnected && card.getAttribute('data-flip-open') !== open) await click(card.querySelector(`[data-flip-toggle="${open === 'true' ? 'front' : 'back'}"]`));
|
|
1164
|
+
for (const [deck, initial] of decks) {
|
|
1165
|
+
const count = deck.querySelectorAll('[data-deck-card]').length;
|
|
1166
|
+
for (let i = 0; signature(deck) !== initial; i++) {
|
|
1167
|
+
if (i > Math.min(count * 2, 32)) throw new Error('document carousel position could not be restored');
|
|
1168
|
+
const first = [...deck.querySelectorAll('[data-deck-card]')].findIndex((c) => !c.hidden);
|
|
1169
|
+
const target = initial.split(',').findIndex((v) => v !== '');
|
|
1170
|
+
await click(deck.querySelector(`[data-deck-${first > target ? 'prev' : 'next'}]`));
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
} finally {
|
|
1174
|
+
for (const [e, [x, y]] of saved) if (e.isConnected) setScroll(e, x, y);
|
|
1175
|
+
winScroll(...windowPosition);
|
|
1176
|
+
if (focused === document.body) {
|
|
1177
|
+
if (document.activeElement !== focused) document.activeElement?.blur?.();
|
|
1178
|
+
} else if (focused?.isConnected) focused.focus({ preventScroll: true });
|
|
1179
|
+
await frame();
|
|
1180
|
+
if (focused && document.activeElement !== focused) throw new Error('document focus could not be restored');
|
|
1181
|
+
if (state.some((e) => !e.isConnected || e.getAttribute('aria-selected') !== 'true')) throw new Error('document tab state could not be restored');
|
|
1182
|
+
for (const [card, open] of flips) if (!card.isConnected || card.getAttribute('data-flip-open') !== open) throw new Error('document flip state could not be restored');
|
|
1183
|
+
for (const [deck, initial] of decks) if (!deck.isConnected || signature(deck) !== initial) throw new Error('document carousel state could not be restored');
|
|
1184
|
+
for (const [e, [x, y]] of saved) if (e.isConnected && (Math.abs(e.scrollLeft - x) > 1 || Math.abs(e.scrollTop - y) > 1)) throw new Error('document scroll position could not be restored');
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
1188
|
+
|
|
843
1189
|
/** A tall view must move when scrolled (S1, G5). Restores the positions it changed. */
|
|
844
1190
|
G.scrolls = () => {
|
|
845
1191
|
const out = [];
|
package/gate/run.mjs
CHANGED
|
@@ -480,8 +480,12 @@ export async function runGate(o) {
|
|
|
480
480
|
if (f.refsDeclaredNoBlock) fail('ADR-0017.4', where, `${f.refsDeclaredNoBlock} section(s) cite references the page never lists (no references block in their page or tab)`);
|
|
481
481
|
if (f.uncaptioned) fail('ADR-0017.2', where, `${f.uncaptioned} equation(s) without a caption`);
|
|
482
482
|
if (s.walkState?.doc) {
|
|
483
|
-
|
|
484
|
-
|
|
483
|
+
try {
|
|
484
|
+
const d = await page.evaluate(() => window.__caosGate.readDocument());
|
|
485
|
+
if (d) s.walkState.doc.push(d);
|
|
486
|
+
} catch (e) {
|
|
487
|
+
fail('ADR-0017.2', where, `document readability traversal could not complete: ${e.message.split('\n')[0]}`);
|
|
488
|
+
}
|
|
485
489
|
}
|
|
486
490
|
for (const r of f.repeatedTicks ?? []) {
|
|
487
491
|
// named x ticks (UPlotChart declares the repeated names): a name used twice, wherever the two ticks sit on the axis
|
package/package.json
CHANGED
|
@@ -1,100 +1,100 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@fasl-work/caos-app-shell",
|
|
3
|
-
"version": "0.19.
|
|
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
|
-
"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
|
-
"./chart": {
|
|
24
|
-
"types": "./dist/chart.d.ts",
|
|
25
|
-
"import": "./dist/chart.js"
|
|
26
|
-
},
|
|
27
|
-
"./keys": {
|
|
28
|
-
"types": "./dist/keys.d.ts",
|
|
29
|
-
"import": "./dist/keys.js"
|
|
30
|
-
},
|
|
31
|
-
"./styles.css": "./styles.css",
|
|
32
|
-
"./chart.css": "./chart.css",
|
|
33
|
-
"./reserved-classes.json": "./reserved-classes.json",
|
|
34
|
-
"./executive.schema.json": "./src/content/executive.schema.json",
|
|
35
|
-
"./serve": "./gate/serve.mjs",
|
|
36
|
-
"./gate": "./gate/run.mjs",
|
|
37
|
-
"./package.json": "./package.json"
|
|
38
|
-
},
|
|
39
|
-
"files": [
|
|
40
|
-
"dist",
|
|
41
|
-
"styles.css",
|
|
42
|
-
"chart.css",
|
|
43
|
-
"bin",
|
|
44
|
-
"gate",
|
|
45
|
-
"reserved-classes.json",
|
|
46
|
-
"src/content/executive.schema.json",
|
|
47
|
-
"VERSION",
|
|
48
|
-
"CHANGELOG.md"
|
|
49
|
-
],
|
|
50
|
-
"scripts": {
|
|
51
|
-
"build": "tsup",
|
|
52
|
-
"test": "node --import tsx --test \"test/**/*.test.ts\" \"test/**/*.test.tsx\"",
|
|
53
|
-
"test:browser": "node --import tsx --test \"e2e/**/*.test.ts\"",
|
|
54
|
-
"prepare": "npm run build",
|
|
55
|
-
"prepublishOnly": "npm run build",
|
|
56
|
-
"prebuild": "node scripts/reserved-classes.mjs",
|
|
57
|
-
"test:gate": "node --test test/gate-selftest.mjs",
|
|
58
|
-
"check:sdd": "python scripts/check_sdd.py ."
|
|
59
|
-
},
|
|
60
|
-
"peerDependencies": {
|
|
61
|
-
"katex": "^0.16.0 || ^0.17.0 || ^0.18.0 || ^0.19.0",
|
|
62
|
-
"lucide-react": ">=0.400.0",
|
|
63
|
-
"react": "^18 || ^19",
|
|
64
|
-
"react-dom": "^18 || ^19",
|
|
65
|
-
"react-router": "^6 || ^7 || ^8",
|
|
66
|
-
"zustand": "^4 || ^5",
|
|
67
|
-
"playwright": ">=1.40",
|
|
68
|
-
"uplot": "^1.6.30"
|
|
69
|
-
},
|
|
70
|
-
"devDependencies": {
|
|
71
|
-
"@playwright/test": "1.63.0",
|
|
72
|
-
"@types/react": "^19.0.0",
|
|
73
|
-
"@types/react-dom": "^19.0.0",
|
|
74
|
-
"esbuild": "0.28.1",
|
|
75
|
-
"katex": "0.19.0",
|
|
76
|
-
"lucide-react": "1.43.0",
|
|
77
|
-
"react": "^19.0.0",
|
|
78
|
-
"react-dom": "^19.0.0",
|
|
79
|
-
"react-router": "^8.3.0",
|
|
80
|
-
"tsup": "^8.3.5",
|
|
81
|
-
"tsx": "^4.23.0",
|
|
82
|
-
"typescript": "^5.7.3",
|
|
83
|
-
"zustand": "^5.0.2",
|
|
84
|
-
"uplot": "1.6.32"
|
|
85
|
-
},
|
|
86
|
-
"overrides": {
|
|
87
|
-
"esbuild": "0.28.1"
|
|
88
|
-
},
|
|
89
|
-
"bin": {
|
|
90
|
-
"caos-shell-gate": "bin/caos-shell-gate.mjs"
|
|
91
|
-
},
|
|
92
|
-
"peerDependenciesMeta": {
|
|
93
|
-
"playwright": {
|
|
94
|
-
"optional": true
|
|
95
|
-
},
|
|
96
|
-
"uplot": {
|
|
97
|
-
"optional": true
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@fasl-work/caos-app-shell",
|
|
3
|
+
"version": "0.19.11",
|
|
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
|
+
"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
|
+
"./chart": {
|
|
24
|
+
"types": "./dist/chart.d.ts",
|
|
25
|
+
"import": "./dist/chart.js"
|
|
26
|
+
},
|
|
27
|
+
"./keys": {
|
|
28
|
+
"types": "./dist/keys.d.ts",
|
|
29
|
+
"import": "./dist/keys.js"
|
|
30
|
+
},
|
|
31
|
+
"./styles.css": "./styles.css",
|
|
32
|
+
"./chart.css": "./chart.css",
|
|
33
|
+
"./reserved-classes.json": "./reserved-classes.json",
|
|
34
|
+
"./executive.schema.json": "./src/content/executive.schema.json",
|
|
35
|
+
"./serve": "./gate/serve.mjs",
|
|
36
|
+
"./gate": "./gate/run.mjs",
|
|
37
|
+
"./package.json": "./package.json"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"styles.css",
|
|
42
|
+
"chart.css",
|
|
43
|
+
"bin",
|
|
44
|
+
"gate",
|
|
45
|
+
"reserved-classes.json",
|
|
46
|
+
"src/content/executive.schema.json",
|
|
47
|
+
"VERSION",
|
|
48
|
+
"CHANGELOG.md"
|
|
49
|
+
],
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsup",
|
|
52
|
+
"test": "node --import tsx --test \"test/**/*.test.ts\" \"test/**/*.test.tsx\"",
|
|
53
|
+
"test:browser": "node --import tsx --test \"e2e/**/*.test.ts\"",
|
|
54
|
+
"prepare": "npm run build",
|
|
55
|
+
"prepublishOnly": "npm run build",
|
|
56
|
+
"prebuild": "node scripts/reserved-classes.mjs",
|
|
57
|
+
"test:gate": "node --test test/gate-selftest.mjs",
|
|
58
|
+
"check:sdd": "python scripts/check_sdd.py ."
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"katex": "^0.16.0 || ^0.17.0 || ^0.18.0 || ^0.19.0",
|
|
62
|
+
"lucide-react": ">=0.400.0",
|
|
63
|
+
"react": "^18 || ^19",
|
|
64
|
+
"react-dom": "^18 || ^19",
|
|
65
|
+
"react-router": "^6 || ^7 || ^8",
|
|
66
|
+
"zustand": "^4 || ^5",
|
|
67
|
+
"playwright": ">=1.40",
|
|
68
|
+
"uplot": "^1.6.30"
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@playwright/test": "1.63.0",
|
|
72
|
+
"@types/react": "^19.0.0",
|
|
73
|
+
"@types/react-dom": "^19.0.0",
|
|
74
|
+
"esbuild": "0.28.1",
|
|
75
|
+
"katex": "0.19.0",
|
|
76
|
+
"lucide-react": "1.43.0",
|
|
77
|
+
"react": "^19.0.0",
|
|
78
|
+
"react-dom": "^19.0.0",
|
|
79
|
+
"react-router": "^8.3.0",
|
|
80
|
+
"tsup": "^8.3.5",
|
|
81
|
+
"tsx": "^4.23.0",
|
|
82
|
+
"typescript": "^5.7.3",
|
|
83
|
+
"zustand": "^5.0.2",
|
|
84
|
+
"uplot": "1.6.32"
|
|
85
|
+
},
|
|
86
|
+
"overrides": {
|
|
87
|
+
"esbuild": "0.28.1"
|
|
88
|
+
},
|
|
89
|
+
"bin": {
|
|
90
|
+
"caos-shell-gate": "bin/caos-shell-gate.mjs"
|
|
91
|
+
},
|
|
92
|
+
"peerDependenciesMeta": {
|
|
93
|
+
"playwright": {
|
|
94
|
+
"optional": true
|
|
95
|
+
},
|
|
96
|
+
"uplot": {
|
|
97
|
+
"optional": true
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
package/reserved-classes.json
CHANGED
|
@@ -167,6 +167,7 @@
|
|
|
167
167
|
"caos-surface",
|
|
168
168
|
"caos-surface-actions",
|
|
169
169
|
"caos-surface-body",
|
|
170
|
+
"caos-surface-doc",
|
|
170
171
|
"caos-surface-focus",
|
|
171
172
|
"caos-surface-head",
|
|
172
173
|
"caos-surface-heading",
|
|
@@ -494,6 +495,7 @@
|
|
|
494
495
|
"caos-surface",
|
|
495
496
|
"caos-surface-actions",
|
|
496
497
|
"caos-surface-body",
|
|
498
|
+
"caos-surface-doc",
|
|
497
499
|
"caos-surface-focus",
|
|
498
500
|
"caos-surface-head",
|
|
499
501
|
"caos-surface-heading",
|
package/styles.css
CHANGED
|
@@ -252,6 +252,9 @@ code, kbd { font-family: var(--font-mono); }
|
|
|
252
252
|
.caos-surface-title { margin: 0; font-size: var(--text-xl); }
|
|
253
253
|
.caos-surface-actions { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
|
|
254
254
|
.caos-surface-lede { margin: 0; color: var(--color-fg-subtle); font-size: var(--text-md); max-width: var(--measure-text); }
|
|
255
|
+
/* Document surfaces own their one page inset; actions cannot shrink the title/lede column. */
|
|
256
|
+
.caos-surface-doc > .caos-surface-head > .caos-surface-heading { flex: 1 1 100%; width: 100%; }
|
|
257
|
+
.caos-surface-doc .caos-surface-lede { max-width: none; font-size: var(--text-md); }
|
|
255
258
|
.caos-surface-body { flex: 1 1 auto; min-height: 0; min-width: 0; display: flex; flex-direction: column; }
|
|
256
259
|
.caos-surface-body > .tabs { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
|
|
257
260
|
.caos-surface-body > .tabs > .tabpanel:not([hidden]) { flex: 1 1 auto; min-height: 0; overflow: auto; display: flex; flex-direction: column; gap: 0.6rem; }
|