@fasl-work/caos-app-shell 0.7.0 → 0.7.2

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/README.md CHANGED
@@ -70,8 +70,11 @@ const config: ShellConfig = {
70
70
  - **Drawing:** put canvas, WebGL and SVG instruments in a `Stage` (or `useStageSize`): nothing draws at zero
71
71
  size, and colours come from `useThemeTokens()` (a canvas cannot read CSS variables).
72
72
  - **Charts:** `import { UPlotChart } from "@fasl-work/caos-app-shell/chart"`: x and y titles with units, series
73
- by token colour, `marks` for what the engine detected, a cursor readout row, theme and language rebuilds.
74
- - **Numbers:** `formatNumber(value, lang, opts)` or `useFormat()`; never `toFixed` in a view.
73
+ by token colour, `marks` for what the engine detected, a key of every series when there are two or more, a cursor
74
+ readout row, theme and language rebuilds. Keep series labels short: the key wraps under the plot.
75
+ - **Numbers:** `formatNumber(value, lang, opts)` or `useFormat()`; never `toFixed` in a view. A magnitude below 1e-4
76
+ is written in scientific notation (a p-value of 1e-200 is `1E-200`, not two hundred zeros); give `digits`, not
77
+ `decimals`, for p-values.
75
78
  - **Documentation routes:** `DocPage` and `DocSection` (each section ends in its own `Refs`, or states why it
76
79
  cites nothing), `Equation` with a caption, `CitationsProvider` and `Cite` (every citation has a DOI or URL).
77
80
  `Tabs`, `SubTabs` and `TabGroups` are controllable and render only the open panel inside a `PanelBoundary`.
package/chart.css CHANGED
@@ -8,4 +8,12 @@
8
8
  .caos-chart.fill { height: 100%; display: flex; flex-direction: column; min-height: 0; }
9
9
  .caos-chart.fill > .caos-chart-plot { flex: 1 1 0; min-height: 0; overflow: hidden; }
10
10
  .caos-chart-readout { margin: 0.15rem 0 0; font-family: var(--font-mono); font-size: 0.74rem; line-height: 1.3; color: var(--color-fg-subtle); min-height: 1.3em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
11
+ /* The series key (known shell defect 17): every series of a chart with two or more, always in view; it wraps. A line
12
+ swatch in the series' colour, dashed for a dashed series, a dot for a series drawn as points. */
13
+ .caos-chart-legend { list-style: none; margin: 0.25rem 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.1rem 0.8rem; font-size: 0.74rem; line-height: 1.35; color: var(--color-fg-subtle); }
14
+ .caos-chart.fill > .caos-chart-legend { flex: none; }
15
+ .caos-chart-key { display: inline-flex; align-items: center; gap: 0.3rem; min-width: 0; }
16
+ .caos-chart-swatch { flex: none; width: 16px; height: 0; border-top: 2px solid var(--swatch); }
17
+ .caos-chart-key[data-dash] .caos-chart-swatch { border-top-style: dashed; }
18
+ .caos-chart-key[data-mode="points"] .caos-chart-swatch { width: 7px; height: 7px; border: 0; border-radius: 50%; background: var(--swatch); }
11
19
  .caos-chart .u-cursor-x, .caos-chart .u-cursor-y { border-color: var(--color-fg-faint) !important; }
package/dist/chart.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { F as FormatOptions, S as ShellToken } from './tokens-BCqyHYx2.js';
2
+ import { F as FormatOptions, S as ShellToken } from './tokens-CSV0MOgR.js';
3
3
  import { B as BiText } from './text-BlY47nIt.js';
4
4
  import 'zustand';
5
5
 
@@ -48,8 +48,10 @@ interface UPlotChartProps {
48
48
  * (a canvas cannot read CSS variables); it compares its options by value, never by identity, so a parent re-render
49
49
  * does not tear it down; its tick formatters are null-safe and follow the interface language; the y axis is sized to
50
50
  * its longest tick label, so no tick is cut; the cursor value is written into a readout row under the plot instead of
51
- * uPlot's legend, which a sized host clips (known shell defect 3). The host declares `data-series`, `data-axis-titles`,
52
- * `data-ticks-cut` and `data-drawn` for the gate.
51
+ * uPlot's legend, which a sized host clips (known shell defect 3). A chart of two or more series keys every series
52
+ * under the plot, always (its colour, a dashed or a dotted swatch, its label): at rest the readout names none of them
53
+ * (known shell defect 17). An axis with fixed decimals ticks only where its labels differ (`tickSteps`). The host
54
+ * declares `data-series`, `data-axis-titles`, `data-ticks-cut`, `data-ticks-repeat` and `data-drawn` for the gate.
53
55
  */
54
56
  declare function UPlotChart({ x, y, series, marks, height, onCursor }: UPlotChartProps): react.JSX.Element;
55
57
 
package/dist/chart.js CHANGED
@@ -1,10 +1,22 @@
1
- import { useShellLang, useThemeStore, useStageSize, pick, resolveToken, formatNumber } from './chunk-OV4HK6EQ.js';
1
+ import { useShellLang, useThemeStore, useStageSize, pick, resolveToken, formatNumber } from './chunk-LOSSTLPP.js';
2
2
  import './chunk-X6Y5CDUD.js';
3
3
  import { useRef, useCallback, useState, useMemo, useEffect } from 'react';
4
4
  import uPlot from 'uplot';
5
5
  import { jsxs, jsx } from 'react/jsx-runtime';
6
6
 
7
7
  var ROTATION = ["--color-accent", "--color-magenta", "--color-accent-2", "--color-good", "--color-warn", "--color-bad"];
8
+ var STEPS = [];
9
+ for (let e = -16; e <= 16; e++) for (const m of [1, 2, 2.5, 5]) STEPS.push(Number((m * 10 ** e).toPrecision(3)));
10
+ function tickSteps(fmt) {
11
+ if (!fmt || fmt.decimals === void 0) return void 0;
12
+ const res = 10 ** -fmt.decimals / (fmt.percent ? 100 : 1);
13
+ return STEPS.filter((s) => s >= res * (1 - 1e-9) && Math.abs(s / res - Math.round(s / res)) < 1e-6);
14
+ }
15
+ function repeatedLabels(labels) {
16
+ let n = 0;
17
+ for (let i = 1; i < labels.length; i++) if (labels[i] && labels[i] === labels[i - 1]) n++;
18
+ return n;
19
+ }
8
20
  function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
9
21
  const lang = useShellLang();
10
22
  const theme = useThemeStore((s) => s.theme);
@@ -21,6 +33,7 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
21
33
  const width = box.width;
22
34
  const h = fill ? box.height : height;
23
35
  const uRef = useRef(null);
36
+ const hostRef = useRef(null);
24
37
  const [cursor, setCursor] = useState(null);
25
38
  const onCursorRef = useRef(onCursor);
26
39
  onCursorRef.current = onCursor;
@@ -45,7 +58,15 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
45
58
  const fg = resolveToken("--color-fg-subtle", "#888");
46
59
  const grid = resolveToken("--color-border", "#ccc");
47
60
  const font = `11px ${resolveToken("--font-sans", "sans-serif")}`;
48
- const tick = (fmt) => (_u, vals) => vals.map((v) => v === null || v === void 0 ? "" : formatNumber(v, lang, fmt ?? { digits: 4 }));
61
+ const repeats = { x: 0, y: 0 };
62
+ const tick = (fmt, axis) => (_u, vals) => {
63
+ const labels = vals.map((v) => v === null || v === void 0 ? "" : formatNumber(v, lang, fmt ?? { digits: 4 }));
64
+ repeats[axis] = repeatedLabels(labels);
65
+ hostRef.current?.setAttribute("data-ticks-repeat", String(repeats.x + repeats.y));
66
+ return labels;
67
+ };
68
+ const xSteps = x.time ? void 0 : tickSteps(x.format);
69
+ const ySteps = y.log ? void 0 : tickSteps(y.format);
49
70
  const opts = {
50
71
  width,
51
72
  height: h,
@@ -59,7 +80,8 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
59
80
  font,
60
81
  label: xUnit ? `${xLabel} (${xUnit})` : xLabel,
61
82
  labelFont: font,
62
- values: x.time ? void 0 : tick(x.format)
83
+ values: x.time ? void 0 : tick(x.format, "x"),
84
+ ...xSteps ? { incrs: xSteps } : {}
63
85
  },
64
86
  {
65
87
  stroke: fg,
@@ -68,7 +90,8 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
68
90
  font,
69
91
  label: yUnit ? `${yLabel} (${yUnit})` : yLabel,
70
92
  labelFont: font,
71
- values: tick(y.format),
93
+ values: tick(y.format, "y"),
94
+ ...ySteps ? { incrs: ySteps } : {},
72
95
  size: (u, values) => {
73
96
  if (!values || values.length === 0) return 48;
74
97
  u.ctx.font = font;
@@ -133,13 +156,19 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
133
156
  return /* @__PURE__ */ jsxs(
134
157
  "div",
135
158
  {
159
+ ref: hostRef,
136
160
  className: fill ? "caos-chart fill" : "caos-chart",
137
161
  "data-series": series.length,
138
162
  "data-axis-titles": `${xLabel}|${yLabel}`,
139
163
  "data-ticks-cut": "0",
164
+ "data-ticks-repeat": "0",
140
165
  "data-drawn": width > 0 && h > 0 ? "1" : "0",
141
166
  children: [
142
167
  /* @__PURE__ */ jsx("div", { ref: setPlot, className: "caos-chart-plot" }),
168
+ series.length > 1 && /* @__PURE__ */ jsx("ul", { className: "caos-chart-legend", "aria-label": lang === "es" ? "Series del gr\xE1fico" : "Series of the chart", children: series.map((s, i) => /* @__PURE__ */ jsxs("li", { className: "caos-chart-key", "data-mode": s.mode ?? "line", "data-dash": s.dash ? "1" : void 0, children: [
169
+ /* @__PURE__ */ jsx("span", { className: "caos-chart-swatch", style: { "--swatch": `var(${s.color ?? ROTATION[i % ROTATION.length]})` }, "aria-hidden": "true" }),
170
+ pick(s.label, lang)
171
+ ] }, i)) }),
143
172
  /* @__PURE__ */ jsx("p", { className: "caos-chart-readout", "aria-live": "polite", title: read, children: read })
144
173
  ]
145
174
  }
@@ -112,11 +112,13 @@ function useThemeTokens() {
112
112
 
113
113
  // src/lib/format.ts
114
114
  var LOCALE = { en: "en-US", es: "es-CL" };
115
+ var SCIENTIFIC_BELOW = 1e-4;
115
116
  var NOT_AVAILABLE = { en: "not available", es: "no disponible" };
116
117
  function formatNumber(value, lang, opts = {}) {
117
118
  if (value === null || value === void 0 || !Number.isFinite(value)) return NOT_AVAILABLE[lang];
118
119
  const v = opts.percent ? value * 100 : value;
119
- const nf = opts.decimals !== void 0 ? new Intl.NumberFormat(LOCALE[lang], { minimumFractionDigits: opts.decimals, maximumFractionDigits: opts.decimals }) : Math.abs(v) >= 10 ** ((opts.digits ?? 4) - 1) ? new Intl.NumberFormat(LOCALE[lang], { maximumFractionDigits: 0 }) : new Intl.NumberFormat(LOCALE[lang], { maximumSignificantDigits: opts.digits ?? 4 });
120
+ const digits = opts.digits ?? 4;
121
+ const nf = opts.decimals !== void 0 ? new Intl.NumberFormat(LOCALE[lang], { minimumFractionDigits: opts.decimals, maximumFractionDigits: opts.decimals }) : v !== 0 && Math.abs(v) < SCIENTIFIC_BELOW ? new Intl.NumberFormat(LOCALE[lang], { notation: "scientific", maximumSignificantDigits: digits }) : Math.abs(v) >= 10 ** (digits - 1) ? new Intl.NumberFormat(LOCALE[lang], { maximumFractionDigits: 0 }) : new Intl.NumberFormat(LOCALE[lang], { maximumSignificantDigits: digits });
120
122
  return opts.percent ? `${nf.format(v)} %` : nf.format(v);
121
123
  }
122
124
  function useFormat() {
package/dist/index.d.ts CHANGED
@@ -2,8 +2,8 @@ export { A as AppShell, a as ArchTab, b as ArchitectureConfig, c as Architecture
2
2
  import * as react from 'react';
3
3
  import { ReactNode, Component, ErrorInfo } from 'react';
4
4
  import * as zustand from 'zustand';
5
- import { F as FormatOptions } from './tokens-BCqyHYx2.js';
6
- export { N as NOT_AVAILABLE, a as SHELL_TOKENS, S as ShellToken, f as formatNumber, r as resolveToken, u as useFormat, b as useThemeTokens } from './tokens-BCqyHYx2.js';
5
+ import { F as FormatOptions } from './tokens-CSV0MOgR.js';
6
+ export { N as NOT_AVAILABLE, a as SHELL_TOKENS, S as ShellToken, f as formatNumber, r as resolveToken, u as useFormat, b as useThemeTokens } from './tokens-CSV0MOgR.js';
7
7
  import { L as Lang, B as BiText } from './text-BlY47nIt.js';
8
8
  export { p as pick, u as useLangStore, a as useShellLang } from './text-BlY47nIt.js';
9
9
 
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { useThemeStore, useShellLang, useLangStore, SHELL_TOKENS, pick, formatNumber } from './chunk-OV4HK6EQ.js';
2
- export { NOT_AVAILABLE, SHELL_TOKENS, Stage, applyTheme, formatNumber, pick, readTheme, resolveToken, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens } from './chunk-OV4HK6EQ.js';
1
+ import { useThemeStore, useShellLang, useLangStore, SHELL_TOKENS, pick, formatNumber } from './chunk-LOSSTLPP.js';
2
+ export { NOT_AVAILABLE, SHELL_TOKENS, Stage, applyTheme, formatNumber, pick, readTheme, resolveToken, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens } from './chunk-LOSSTLPP.js';
3
3
  export { LANG_STORAGE_KEY, STANDARD_ROUTES, THEME_BOOT_SCRIPT, THEME_STORAGE_KEY } from './chunk-X6Y5CDUD.js';
4
4
  import { createContext, Component, useState, useRef, useId, useEffect, useCallback, useContext, useMemo } from 'react';
5
5
  import { useInRouterContext, NavLink, useLocation } from 'react-router';
@@ -397,7 +397,6 @@ function Frame({ config, pathname, routed, children }) {
397
397
  /* @__PURE__ */ jsx("span", { children: config.product.name }),
398
398
  /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
399
399
  /* @__PURE__ */ jsx("span", { children: c.complement }),
400
- /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
401
400
  /* @__PURE__ */ jsx("span", { className: "footer-build", "data-version": config.version, "data-build": config.build ?? "", children: /* @__PURE__ */ jsxs("span", { children: [
402
401
  c.version,
403
402
  config.version,
@@ -2,9 +2,10 @@ import { L as Lang } from './text-BlY47nIt.js';
2
2
 
3
3
  interface FormatOptions {
4
4
  /** Significant digits for a value below 10^(digits-1) (default 4); a larger value shows every integer digit, so a
5
- * count is never rounded (27345 people, not 27,350). Ignored when `decimals` is set. */
5
+ * count is never rounded (27345 people, not 27,350). A magnitude below 1e-4 is written in scientific notation with
6
+ * these significant digits (6.53E-13). Ignored when `decimals` is set. */
6
7
  digits?: number;
7
- /** Fixed number of decimals. */
8
+ /** Fixed number of decimals. The caller's explicit choice: a value below the last decimal reads as zero. */
8
9
  decimals?: number;
9
10
  /** Render a fraction as a percentage (0.123 -> 12.3 %). */
10
11
  percent?: boolean;
@@ -13,7 +14,7 @@ interface FormatOptions {
13
14
  declare const NOT_AVAILABLE: Record<Lang, string>;
14
15
  /**
15
16
  * A number in the interface language: a decimal point in English and a decimal comma in Spanish
16
- * (`conventions/languages.md`), significant digits by default. `null`, `undefined` and non-finite values render as
17
+ * (`conventions/languages.md`), significant digits by default, scientific notation below 1e-4. `null`, `undefined` and non-finite values render as
17
18
  * "not available", never as `NaN` (failure classes 5 and 23 of the 2026-10-04 history).
18
19
  */
19
20
  declare function formatNumber(value: number | null | undefined, lang: Lang, opts?: FormatOptions): string;
package/gate/inpage.mjs CHANGED
@@ -101,17 +101,23 @@ export function installLib() {
101
101
  };
102
102
  walk(body, false);
103
103
 
104
- // Tab rows: one row each (ADR-0071 rule 4); tops clustered so a 1px active-tab offset is not a second row.
104
+ // Tab rows: one row each (ADR-0071 rule 4); tops clustered so a 1px active-tab offset is not a second row. A row
105
+ // also shows its tabs whole: a row that shrank under a tall panel cut its tabs' height (known shell defect 16).
105
106
  const rows = [...document.querySelectorAll('[role="tablist"]')]
106
107
  .filter((tl) => visible(tl) && tl.getAttribute('aria-orientation') !== 'vertical')
107
108
  .map((tl) => {
108
- const tops = [...tl.querySelectorAll('[role="tab"]')]
109
- .filter((t) => t.closest('[role="tablist"]') === tl && visible(t))
110
- .map((t) => t.getBoundingClientRect().top)
111
- .sort((a, b) => a - b);
109
+ const tabs = [...tl.querySelectorAll('[role="tab"]')].filter((t) => t.closest('[role="tablist"]') === tl && visible(t));
110
+ const tops = tabs.map((t) => t.getBoundingClientRect().top).sort((a, b) => a - b);
112
111
  let n = tops.length ? 1 : 0;
113
112
  for (let i = 1; i < tops.length; i += 1) if (tops[i] - tops[i - 1] > 8) n += 1;
114
- return { name: tl.getAttribute('aria-label') || describe(tl), rows: n };
113
+ // a shrunk row either lets its tabs overflow it or stretches them shorter than their own content
114
+ const lr = tl.getBoundingClientRect();
115
+ let cut = Math.max(0, tl.scrollHeight - tl.clientHeight);
116
+ for (const t of tabs) {
117
+ const r = t.getBoundingClientRect();
118
+ cut = Math.max(cut, t.scrollHeight - t.clientHeight, Math.round(Math.max(0, lr.top - r.top) + Math.max(0, r.bottom - lr.bottom)));
119
+ }
120
+ return { name: tl.getAttribute('aria-label') || describe(tl), rows: n, cut };
115
121
  });
116
122
 
117
123
  // The rail: no scroll (rule 6) and every descendant inside its box (G5).
@@ -270,6 +276,10 @@ export function installLib() {
270
276
  pageText: pageBody ? (pageBody.innerText || '').trim().length : 0,
271
277
  referenceLists: [...document.querySelectorAll('.reference-list')].filter(visible).length,
272
278
  uncaptioned: [...document.querySelectorAll('.equation')].filter((e) => visible(e) && !e.querySelector('.equation-caption')).length,
279
+ // charts whose axis gives a tick the label of the tick before it (UPlotChart declares data-ticks-repeat)
280
+ repeatedTicks: [...document.querySelectorAll('[data-ticks-repeat]')]
281
+ .filter((el) => visible(el) && Number(el.getAttribute('data-ticks-repeat')) > 0)
282
+ .map((el) => ({ label: describe(el.closest('[data-plot]') || el), n: Number(el.getAttribute('data-ticks-repeat')) })),
273
283
  sectionsWithoutRefs: [...document.querySelectorAll('[data-no-refs-reason="missing"]')].filter(visible).length,
274
284
  };
275
285
  };
@@ -312,13 +322,15 @@ export function installLib() {
312
322
  const s = getComputedStyle(a);
313
323
  const ar = a.getBoundingClientRect();
314
324
  let r = el.getBoundingClientRect();
325
+ // An element larger than the container is aligned by its start: aligning its end pushed its start out of
326
+ // view, and the probe then landed on whatever covers the container (known shell defect 18).
315
327
  if ((s.overflowX === 'auto' || s.overflowX === 'scroll') && a.scrollWidth > a.clientWidth) {
316
- if (r.left < ar.left) setScroll(a, a.scrollLeft - (ar.left - r.left + 8), a.scrollTop);
328
+ if (r.left < ar.left || r.width > ar.width) setScroll(a, a.scrollLeft - (ar.left - r.left), a.scrollTop);
317
329
  else if (r.right > ar.right) setScroll(a, a.scrollLeft + (r.right - ar.right + 8), a.scrollTop);
318
330
  }
319
331
  r = el.getBoundingClientRect();
320
332
  if ((s.overflowY === 'auto' || s.overflowY === 'scroll') && a.scrollHeight > a.clientHeight) {
321
- if (r.top < ar.top) setScroll(a, a.scrollLeft, a.scrollTop - (ar.top - r.top + 8));
333
+ if (r.top < ar.top || r.height > ar.height) setScroll(a, a.scrollLeft, a.scrollTop - (ar.top - r.top));
322
334
  else if (r.bottom > ar.bottom) setScroll(a, a.scrollLeft, a.scrollTop + (r.bottom - ar.bottom + 8));
323
335
  }
324
336
  }
@@ -334,7 +346,27 @@ export function installLib() {
334
346
  return Boolean(el);
335
347
  };
336
348
 
337
- /** Every visible control can be brought into the viewport and receives the pointer at its centre (G5). */
349
+ /** The viewport cut by every ancestor that clips its content (overflow other than visible). */
350
+ const visibleArea = (el) => {
351
+ const area = { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight };
352
+ for (let a = el.parentElement; a && a !== document.body && a !== document.documentElement; a = a.parentElement) {
353
+ const s = getComputedStyle(a);
354
+ if (s.overflowX === 'visible' && s.overflowY === 'visible') continue;
355
+ const ar = a.getBoundingClientRect();
356
+ if (s.overflowX !== 'visible') {
357
+ area.left = Math.max(area.left, ar.left);
358
+ area.right = Math.min(area.right, ar.right);
359
+ }
360
+ if (s.overflowY !== 'visible') {
361
+ area.top = Math.max(area.top, ar.top);
362
+ area.bottom = Math.min(area.bottom, ar.bottom);
363
+ }
364
+ }
365
+ return area;
366
+ };
367
+
368
+ /** Every visible control can be brought into the viewport and receives the pointer at the centre of its visible
369
+ * part (G5). */
338
370
  G.reach = () => {
339
371
  const sel = 'a[href], button, select, input:not([type="hidden"]), textarea, [role="tab"], [role="button"], [tabindex]:not([tabindex="-1"])';
340
372
  const saved = new Map();
@@ -352,13 +384,25 @@ export function installLib() {
352
384
  const r = frags.length ? frags.reduce((a, b) => (b.width * b.height > a.width * a.height ? b : a)) : el.getBoundingClientRect();
353
385
  if (r.width < 1 || r.height < 1) continue;
354
386
  checked += 1;
355
- const fits = r.width <= window.innerWidth && r.height <= window.innerHeight;
356
- if (fits && (r.left < -1 || r.right > window.innerWidth + 1 || r.top < -1 || r.bottom > window.innerHeight + 1)) {
387
+ // The area the element can show in: the viewport cut by every ancestor that clips (known shell defect 18; an
388
+ // element that fits the window but not its scroll container was reported as never brought into view).
389
+ const area = visibleArea(el);
390
+ const fits = r.width <= area.right - area.left + 1 && r.height <= area.bottom - area.top + 1;
391
+ if (fits && (r.left < area.left - 1 || r.right > area.right + 1 || r.top < area.top - 1 || r.bottom > area.bottom + 1)) {
392
+ out.push(`${describe(el)} cannot be brought into the viewport (at ${Math.round(r.left)},${Math.round(r.top)})`);
393
+ continue;
394
+ }
395
+ // the pointer goes to the centre of the part that shows
396
+ const vx0 = Math.max(r.left, area.left);
397
+ const vx1 = Math.min(r.right, area.right);
398
+ const vy0 = Math.max(r.top, area.top);
399
+ const vy1 = Math.min(r.bottom, area.bottom);
400
+ if (vx1 - vx0 < 1 || vy1 - vy0 < 1) {
357
401
  out.push(`${describe(el)} cannot be brought into the viewport (at ${Math.round(r.left)},${Math.round(r.top)})`);
358
402
  continue;
359
403
  }
360
- const px = Math.min(Math.max(r.left + Math.min(r.width, window.innerWidth) / 2, 0), window.innerWidth - 1);
361
- const py = Math.min(Math.max(r.top + Math.min(r.height, window.innerHeight) / 2, 0), window.innerHeight - 1);
404
+ const px = Math.min(Math.max((vx0 + vx1) / 2, 0), window.innerWidth - 1);
405
+ const py = Math.min(Math.max((vy0 + vy1) / 2, 0), window.innerHeight - 1);
362
406
  const hit = document.elementFromPoint(px, py);
363
407
  const ok =
364
408
  hit && (hit === el || el.contains(hit) || (el.labels && [...el.labels].some((l) => l.contains(hit))) || (hit.tagName === 'LABEL' && hit.control === el));
package/gate/run.mjs CHANGED
@@ -335,6 +335,7 @@ export async function runGate(o) {
335
335
  if (f.beyondCount) fail('G5', where, `${f.beyondCount} element(s) outside the viewport: ${f.beyond.join('; ')}`);
336
336
  if (big && f.docH > f.vh + 1) fail('ADR-0071.1', where, `the document scrolls: ${f.docH}px in a ${f.vh}px viewport (the page is the viewport)`);
337
337
  for (const r of f.rows) if (r.rows > 1) fail('ADR-0071.4', where, `tab row "${r.name}" wraps onto ${r.rows} rows`);
338
+ for (const r of f.rows) if (r.cut > 1) fail('G5', where, `tab row "${r.name}" cuts its tabs by ${r.cut}px (a row shrunk under its panel)`);
338
339
  if (f.rail) {
339
340
  if (big && f.rail.scroll > f.rail.client + 1) fail('ADR-0071.6', where, `the rail scrolls: ${f.rail.scroll}px of content in ${f.rail.client}px`);
340
341
  if (f.rail.outsideCount) fail('G5', where, `${f.rail.outsideCount} rail element(s) outside the rail: ${f.rail.outside.join('; ')}`);
@@ -344,6 +345,9 @@ export async function runGate(o) {
344
345
  if (f.offCentre !== null && f.offCentre > 2) fail('ADR-0017.1', where, `the prose page is off-centre by ${f.offCentre.toFixed(1)}px`);
345
346
  if (f.referenceLists) fail('ADR-0017.4', where, `${f.referenceLists} bibliography dump(s); references belong at the end of each section`);
346
347
  if (f.uncaptioned) fail('ADR-0017.2', where, `${f.uncaptioned} equation(s) without a caption`);
348
+ for (const r of f.repeatedTicks ?? []) {
349
+ fail('G6', where, `${r.label}: ${r.n} axis tick label(s) repeat the label before them (give the axis fixed decimals)`);
350
+ }
347
351
  if (f.sectionsWithoutRefs) fail('ADR-0017.4', where, `${f.sectionsWithoutRefs} section(s) end without references or a stated reason`);
348
352
  if (s.first && !s.isWorkbench && f.pageText < opt.textFloor) fail('G4', where, `the page body holds ${f.pageText} characters of text (floor ${opt.textFloor}); the route rendered without its content`);
349
353
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fasl-work/caos-app-shell",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
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",
@@ -24,8 +24,11 @@
24
24
  "caos-architecture-diagram",
25
25
  "caos-badges",
26
26
  "caos-chart",
27
+ "caos-chart-key",
28
+ "caos-chart-legend",
27
29
  "caos-chart-plot",
28
30
  "caos-chart-readout",
31
+ "caos-chart-swatch",
29
32
  "caos-chip-row",
30
33
  "caos-chipgroup",
31
34
  "caos-chipgroup-label",
package/styles.css CHANGED
@@ -563,6 +563,11 @@ html, body { overflow-x: hidden; }
563
563
  .caos-wb-rail .caos-variant-note { font-size: 0.8rem; }
564
564
  .caos-cw-panel > .subtabs { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; margin-block: 0; }
565
565
  .caos-cw-panel > .subtabs > .subtablist { margin-bottom: 0.45rem; }
566
+ /* Known shell defect 16: the group row and the sub-tab row are scroll containers (overflow-x auto), so their
567
+ automatic minimum height is zero, and in this column flex chain they shrank with a panel taller than the
568
+ instrument: the rows were clipped and the panel's top covered the sub-tabs (CAOS_Contraste, 2026-10-05). A tab
569
+ row keeps its height; the panel below it scrolls. */
570
+ .caos-cw > .tabs > .tablist, .caos-cw-panel > .subtabs > .subtablist { flex: none; }
566
571
  .page-body.wide.caos-wb { padding-inline: 1rem; }
567
572
  .caos-cw-panel > .subtabs > .subtabpanels, .caos-cw-panel > .subtabs > .subtabpanels > .subtabpanel { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
568
573
  .caos-cw-panel > .subtabs > .subtabpanels > .subtabpanel { gap: 0.6rem; }