@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 +5 -2
- package/chart.css +8 -0
- package/dist/chart.d.ts +5 -3
- package/dist/chart.js +33 -4
- package/dist/{chunk-OV4HK6EQ.js → chunk-LOSSTLPP.js} +3 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -3
- package/dist/{tokens-BCqyHYx2.d.ts → tokens-CSV0MOgR.d.ts} +4 -3
- package/gate/inpage.mjs +57 -13
- package/gate/run.mjs +4 -0
- package/package.json +1 -1
- package/reserved-classes.json +3 -0
- package/styles.css +5 -0
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
|
|
74
|
-
|
|
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-
|
|
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).
|
|
52
|
-
*
|
|
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-
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
2
|
-
export { NOT_AVAILABLE, SHELL_TOKENS, Stage, applyTheme, formatNumber, pick, readTheme, resolveToken, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens } from './chunk-
|
|
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).
|
|
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
|
|
109
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
356
|
-
|
|
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(
|
|
361
|
-
const py = Math.min(Math.max(
|
|
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.
|
|
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",
|
package/reserved-classes.json
CHANGED
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; }
|