@fasl-work/caos-app-shell 0.19.9 → 0.19.10

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,10 @@
1
+ ## [0.19.010] - 2026-10-31
2
+
3
+ ### Fixed
4
+
5
+ - `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.
6
+ - 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).
7
+
1
8
  ## [0.19.009] - 2026-10-30
2
9
 
3
10
  ### Fixed
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.19.009
1
+ 0.19.010
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: "page-body wide caos-surface", "data-surface": "", "data-view-param": reserve ? viewParam ?? "" : void 0, children: [
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: "caos-surface-lede", children: lede })
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) {
@@ -730,18 +779,18 @@ export function installLib() {
730
779
  if (!svg) return info;
731
780
  info.literal = literalsOf(svg);
732
781
  const vb = svg.viewBox && svg.viewBox.baseVal;
733
- const w = svg.getBoundingClientRect().width;
734
- const scale = vb && vb.width ? w / vb.width : 1;
735
782
  for (const t of svg.querySelectorAll('text')) {
736
783
  const label = (t.textContent || '').trim();
737
784
  if (!label || !visible(t)) continue;
738
785
  info.labels += 1;
739
786
  if (vb && vb.width) {
740
- const b = t.getBBox();
741
- 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));
787
+ const b = G.svgTextBounds(svg, t);
788
+ if (!b) info.outside.push(`${label.slice(0, 40)} (bounds unavailable)`);
789
+ 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
790
  }
743
- const px = parseFloat(getComputedStyle(t).fontSize) * scale;
744
- if (px < EXEC_MIN_LABEL_PX) info.small.push(`"${label.slice(0, 30)}" at ${px.toFixed(1)}px`);
791
+ const px = svgTextFontPx(t);
792
+ if (px === null) info.small.push(`"${label.slice(0, 30)}" (font scale unavailable)`);
793
+ else if (px < EXEC_MIN_LABEL_PX) info.small.push(`"${label.slice(0, 30)}" at ${px.toFixed(1)}px`);
745
794
  }
746
795
  return info;
747
796
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fasl-work/caos-app-shell",
3
- "version": "0.19.9",
3
+ "version": "0.19.10",
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",
@@ -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; }