@fasl-work/caos-app-shell 0.7.0 → 0.7.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/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). The host declares `data-series`, `data-axis-titles`, `data-ticks-cut` and `data-drawn` for
54
+ * 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,4 +1,4 @@
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';
@@ -140,6 +140,10 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
140
140
  "data-drawn": width > 0 && h > 0 ? "1" : "0",
141
141
  children: [
142
142
  /* @__PURE__ */ jsx("div", { ref: setPlot, className: "caos-chart-plot" }),
143
+ 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: [
144
+ /* @__PURE__ */ jsx("span", { className: "caos-chart-swatch", style: { "--swatch": `var(${s.color ?? ROTATION[i % ROTATION.length]})` }, "aria-hidden": "true" }),
145
+ pick(s.label, lang)
146
+ ] }, i)) }),
143
147
  /* @__PURE__ */ jsx("p", { className: "caos-chart-readout", "aria-live": "polite", title: read, children: read })
144
148
  ]
145
149
  }
@@ -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).
@@ -312,13 +318,15 @@ export function installLib() {
312
318
  const s = getComputedStyle(a);
313
319
  const ar = a.getBoundingClientRect();
314
320
  let r = el.getBoundingClientRect();
321
+ // An element larger than the container is aligned by its start: aligning its end pushed its start out of
322
+ // view, and the probe then landed on whatever covers the container (known shell defect 18).
315
323
  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);
324
+ if (r.left < ar.left || r.width > ar.width) setScroll(a, a.scrollLeft - (ar.left - r.left), a.scrollTop);
317
325
  else if (r.right > ar.right) setScroll(a, a.scrollLeft + (r.right - ar.right + 8), a.scrollTop);
318
326
  }
319
327
  r = el.getBoundingClientRect();
320
328
  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));
329
+ if (r.top < ar.top || r.height > ar.height) setScroll(a, a.scrollLeft, a.scrollTop - (ar.top - r.top));
322
330
  else if (r.bottom > ar.bottom) setScroll(a, a.scrollLeft, a.scrollTop + (r.bottom - ar.bottom + 8));
323
331
  }
324
332
  }
@@ -334,7 +342,27 @@ export function installLib() {
334
342
  return Boolean(el);
335
343
  };
336
344
 
337
- /** Every visible control can be brought into the viewport and receives the pointer at its centre (G5). */
345
+ /** The viewport cut by every ancestor that clips its content (overflow other than visible). */
346
+ const visibleArea = (el) => {
347
+ const area = { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight };
348
+ for (let a = el.parentElement; a && a !== document.body && a !== document.documentElement; a = a.parentElement) {
349
+ const s = getComputedStyle(a);
350
+ if (s.overflowX === 'visible' && s.overflowY === 'visible') continue;
351
+ const ar = a.getBoundingClientRect();
352
+ if (s.overflowX !== 'visible') {
353
+ area.left = Math.max(area.left, ar.left);
354
+ area.right = Math.min(area.right, ar.right);
355
+ }
356
+ if (s.overflowY !== 'visible') {
357
+ area.top = Math.max(area.top, ar.top);
358
+ area.bottom = Math.min(area.bottom, ar.bottom);
359
+ }
360
+ }
361
+ return area;
362
+ };
363
+
364
+ /** Every visible control can be brought into the viewport and receives the pointer at the centre of its visible
365
+ * part (G5). */
338
366
  G.reach = () => {
339
367
  const sel = 'a[href], button, select, input:not([type="hidden"]), textarea, [role="tab"], [role="button"], [tabindex]:not([tabindex="-1"])';
340
368
  const saved = new Map();
@@ -352,13 +380,25 @@ export function installLib() {
352
380
  const r = frags.length ? frags.reduce((a, b) => (b.width * b.height > a.width * a.height ? b : a)) : el.getBoundingClientRect();
353
381
  if (r.width < 1 || r.height < 1) continue;
354
382
  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)) {
383
+ // The area the element can show in: the viewport cut by every ancestor that clips (known shell defect 18; an
384
+ // element that fits the window but not its scroll container was reported as never brought into view).
385
+ const area = visibleArea(el);
386
+ const fits = r.width <= area.right - area.left + 1 && r.height <= area.bottom - area.top + 1;
387
+ if (fits && (r.left < area.left - 1 || r.right > area.right + 1 || r.top < area.top - 1 || r.bottom > area.bottom + 1)) {
388
+ out.push(`${describe(el)} cannot be brought into the viewport (at ${Math.round(r.left)},${Math.round(r.top)})`);
389
+ continue;
390
+ }
391
+ // the pointer goes to the centre of the part that shows
392
+ const vx0 = Math.max(r.left, area.left);
393
+ const vx1 = Math.min(r.right, area.right);
394
+ const vy0 = Math.max(r.top, area.top);
395
+ const vy1 = Math.min(r.bottom, area.bottom);
396
+ if (vx1 - vx0 < 1 || vy1 - vy0 < 1) {
357
397
  out.push(`${describe(el)} cannot be brought into the viewport (at ${Math.round(r.left)},${Math.round(r.top)})`);
358
398
  continue;
359
399
  }
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);
400
+ const px = Math.min(Math.max((vx0 + vx1) / 2, 0), window.innerWidth - 1);
401
+ const py = Math.min(Math.max((vy0 + vy1) / 2, 0), window.innerHeight - 1);
362
402
  const hit = document.elementFromPoint(px, py);
363
403
  const ok =
364
404
  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('; ')}`);
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.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",
@@ -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; }