@fasl-work/caos-app-shell 0.17.0 → 0.19.0
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/chart.css +5 -0
- package/dist/chart.d.ts +106 -14
- package/dist/chart.js +715 -111
- package/dist/chunk-3LONJM5B.js +680 -0
- package/dist/index.d.ts +36 -39
- package/dist/index.js +509 -144
- package/dist/{tokens-BTOzar-U.d.ts → tokens-BvU9bFPw.d.ts} +42 -1
- package/gate/inpage.mjs +170 -1
- package/gate/run.mjs +15 -1
- package/package.json +1 -1
- package/reserved-classes.json +24 -0
- package/styles.css +32 -0
- package/dist/chunk-YECZXGVG.js +0 -217
|
@@ -39,6 +39,47 @@ declare function formatTicks(values: readonly (number | null | undefined)[], lan
|
|
|
39
39
|
/** `formatNumber` bound to the current shell language. */
|
|
40
40
|
declare function useFormat(): (value: number | null | undefined, opts?: FormatOptions) => string;
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Text that fits the drawing it labels, measured in the font the page renders.
|
|
44
|
+
*
|
|
45
|
+
* Products drew their axes and labels with fixed margins and a fixed width per character. The Linux fonts (DejaVu
|
|
46
|
+
* Sans) are wider than the Windows ones and Spanish labels are longer than English ones, so a column sized for one
|
|
47
|
+
* ran its labels out of the drawing in the other (CAOS_Fragmenta 0.05.000: "Tamaño medio clásico, limitado al bloque
|
|
48
|
+
* in situ" read "dio clásico, limitado al bloque in situ"; its deploy gate failed on Linux). Here a label is measured
|
|
49
|
+
* with the canvas text metrics of the page's own font (`--font-sans`, which the gate's wide-font pass replaces),
|
|
50
|
+
* broken over two lines at a word when it does not fit, and only then shortened with an ellipsis, so the drawing can
|
|
51
|
+
* give the whole label as its title. Ported from CAOS_Fragmenta `viz/text.ts` (0.07.000).
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/** The name of a reference line that has no label, for the handle a reader drags (a screen reader says it): numbered when
|
|
55
|
+
* several unlabelled lines of one chart can be dragged, so each handle has a name of its own. */
|
|
56
|
+
declare function referenceLineName(lang: Lang, n: number, of: number): string;
|
|
57
|
+
/** The page's sans-serif stack as the shell token resolves it. */
|
|
58
|
+
declare function fontFamily(): string;
|
|
59
|
+
/** The width of one line of text at a font size, in CSS pixels; outside a browser, an estimate on the wide side. */
|
|
60
|
+
declare function textWidth(text: string, px: number, weight?: number): number;
|
|
61
|
+
/** The widest a set of labels needs, each on one line. */
|
|
62
|
+
declare function widestLabel(labels: readonly string[], px: number, weight?: number): number;
|
|
63
|
+
interface FittedLabel {
|
|
64
|
+
/** One or two lines; the last is shortened with an ellipsis only when the lines allowed are not enough. */
|
|
65
|
+
lines: string[];
|
|
66
|
+
/** Whether the label was shortened, so the drawing gives the whole label as its title. */
|
|
67
|
+
shortened: boolean;
|
|
68
|
+
}
|
|
69
|
+
/** A label in at most `maxLines` lines of `width` pixels: whole when it fits, broken at a word, shortened last. */
|
|
70
|
+
declare function fitLabel(label: string, width: number, px: number, maxLines?: 1 | 2): FittedLabel;
|
|
71
|
+
/** The 1, 2, 2.5 or 5 times a power of ten at or above `x`. */
|
|
72
|
+
declare function niceStep(x: number): number;
|
|
73
|
+
/**
|
|
74
|
+
* Round ticks covering [lo, hi], as many as leave room for labels about `labelSize` pixels along an axis of `room`
|
|
75
|
+
* pixels (at least two, at most `most`). The first tick is at or below `lo` and the last at or above `hi`, so a bar
|
|
76
|
+
* or a point never runs past the last labelled value. With a `resolution` (1 for an axis of counts, 10^-d for d fixed
|
|
77
|
+
* decimals) every step is a multiple of it, so no two labels read the same and none is a rounded fraction (0.19.0).
|
|
78
|
+
*/
|
|
79
|
+
declare function niceTicks(lo: number, hi: number, room: number, labelSize: number, most?: number, resolution?: number): number[];
|
|
80
|
+
/** How many tick labels repeat the label before them (empty labels apart); the gate fails a chart with any (G6). */
|
|
81
|
+
declare function repeatedLabels(labels: readonly (string | null | undefined)[]): number;
|
|
82
|
+
|
|
42
83
|
/**
|
|
43
84
|
* The colour and font tokens `styles.css` defines for both themes. The architecture-modal validation and the
|
|
44
85
|
* template's web-baseline guard check every `var(--x)` a product names against this list; a token that is not
|
|
@@ -58,4 +99,4 @@ declare function resolveToken(token: string, fallback?: string): string;
|
|
|
58
99
|
*/
|
|
59
100
|
declare function useThemeTokens(): Record<ShellToken, string>;
|
|
60
101
|
|
|
61
|
-
export { type FormatOptions as F, NBSP as N, type ShellColorToken as S,
|
|
102
|
+
export { type FormatOptions as F, NBSP as N, type ShellColorToken as S, repeatedLabels as a, type FittedLabel as b, NOT_AVAILABLE as c, SCIENTIFIC_BELOW as d, SHELL_TOKENS as e, type ShellToken as f, fitLabel as g, fontFamily as h, formatNumber as i, formatTicks as j, niceTicks as k, resolveToken as l, useThemeTokens as m, niceStep as n, referenceLineName as r, textWidth as t, useFormat as u, widestLabel as w };
|
package/gate/inpage.mjs
CHANGED
|
@@ -498,7 +498,7 @@ export function installLib() {
|
|
|
498
498
|
// charts whose axis gives a tick the label of the tick before it (UPlotChart declares data-ticks-repeat)
|
|
499
499
|
repeatedTicks: [...document.querySelectorAll('[data-ticks-repeat]')]
|
|
500
500
|
.filter((el) => visible(el) && Number(el.getAttribute('data-ticks-repeat')) > 0)
|
|
501
|
-
.map((el) => ({ label: describe(el.closest('[data-plot]') || el), n: Number(el.getAttribute('data-ticks-repeat')) })),
|
|
501
|
+
.map((el) => ({ label: describe(el.closest('[data-plot]') || el), n: Number(el.getAttribute('data-ticks-repeat')), names: el.getAttribute('data-ticks-repeated') || '' })),
|
|
502
502
|
// the drawing a filling card keeps under its note (G6 stage floor): the body of every visible `PlotCard fill`
|
|
503
503
|
fillStages: [...document.querySelectorAll('.caos-plot.fill')]
|
|
504
504
|
.filter(visible)
|
|
@@ -1580,4 +1580,173 @@ export function installLib() {
|
|
|
1580
1580
|
}
|
|
1581
1581
|
return out.slice(0, 10);
|
|
1582
1582
|
};
|
|
1583
|
+
|
|
1584
|
+
// ---- G20 (0.19.0): every chart host carries Tier A: one tab stop with a name, the table of the values, a summary, a reset --------------
|
|
1585
|
+
const reasonWords = (t) => (String(t).match(/[\p{L}\p{Nd}]/gu) || []).length;
|
|
1586
|
+
G.chartsTierA = async (drive) => {
|
|
1587
|
+
const out = [];
|
|
1588
|
+
const pause = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1589
|
+
for (const host of document.querySelectorAll('[data-chart-host]')) {
|
|
1590
|
+
if (!visible(host)) continue;
|
|
1591
|
+
const kind = host.getAttribute('data-chart-host');
|
|
1592
|
+
const label = describe(host.closest('[data-plot]') || host);
|
|
1593
|
+
const problems = [];
|
|
1594
|
+
const stop = host.querySelector('[data-chart-focus]');
|
|
1595
|
+
// a chart that turns zoom off declares data-zoom-off, an empty reason included (the attribute is there, its text is the reason)
|
|
1596
|
+
const entry = { label, kind, problems, zoomOffSet: host.hasAttribute('data-zoom-off'), zoomOff: host.getAttribute('data-zoom-off') || '', zoomOffValid: true };
|
|
1597
|
+
if (entry.zoomOffSet) entry.zoomOffValid = reasonWords(entry.zoomOff) >= 12;
|
|
1598
|
+
out.push(entry);
|
|
1599
|
+
// the labels of a chart's marks (x and y) share one set of boxes, and none lies over another (0.19.0): the chart says how many do
|
|
1600
|
+
const overlaps = host.getAttribute('data-mark-overlaps');
|
|
1601
|
+
if (overlaps !== null && Number(overlaps) > 0) {
|
|
1602
|
+
// the pairs that overlap, each label named with its kind (an x mark or a y mark), from the boxes the chart declares (re-review of 0.19.0, m1)
|
|
1603
|
+
let pairs = [];
|
|
1604
|
+
try {
|
|
1605
|
+
const boxes = JSON.parse(host.getAttribute('data-mark-boxes') || '[]');
|
|
1606
|
+
for (let i = 0; i < boxes.length; i++)
|
|
1607
|
+
for (let j = i + 1; j < boxes.length; j++) {
|
|
1608
|
+
const a = boxes[i];
|
|
1609
|
+
const b = boxes[j];
|
|
1610
|
+
if (a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1) pairs.push(`"${a.t}" (${a.k}) lies over "${b.t}" (${b.k})`);
|
|
1611
|
+
}
|
|
1612
|
+
} catch {
|
|
1613
|
+
pairs = [];
|
|
1614
|
+
}
|
|
1615
|
+
problems.push(`${overlaps} pair(s) of mark label(s) overlap (data-mark-overlaps="${overlaps}")${pairs.length ? `: ${pairs.slice(0, 4).join('; ')}` : ': the chart declares no boxes to name them'}`);
|
|
1616
|
+
}
|
|
1617
|
+
if (!stop) {
|
|
1618
|
+
problems.push('the chart is not a tab stop (no element with data-chart-focus): the keyboard cannot reach its crosshair, zoom or reset');
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
if (stop.tabIndex < 0) problems.push('the plot is not a tab stop (tabindex is below 0)');
|
|
1622
|
+
const labelledBy = (stop.getAttribute('aria-labelledby') || '').split(/\s+/).filter(Boolean).map((id) => document.getElementById(id)?.textContent || '').join(' ');
|
|
1623
|
+
const name = (stop.getAttribute('aria-label') || labelledBy || '').trim();
|
|
1624
|
+
if (name.length < 2) problems.push('the plot has no accessible name (aria-label or aria-labelledby)');
|
|
1625
|
+
// the summary describes the plot (aria-describedby); the table of the values is linked by aria-details (or, as in 0.19.0, described by)
|
|
1626
|
+
const ids = `${stop.getAttribute('aria-describedby') || ''} ${stop.getAttribute('aria-details') || ''}`.split(/\s+/).filter(Boolean);
|
|
1627
|
+
const described = ids.map((id) => document.getElementById(id)).filter(Boolean);
|
|
1628
|
+
const table = described.find((el) => el.tagName === 'TABLE');
|
|
1629
|
+
const points = Number(host.getAttribute('data-points') || '0');
|
|
1630
|
+
if (!table) problems.push('the plot is not linked to a data table (aria-details must name a table of the plotted values)');
|
|
1631
|
+
else {
|
|
1632
|
+
const rows = table.querySelectorAll('tbody tr').length;
|
|
1633
|
+
if (points > 0 && rows === 0) problems.push('the data table has no rows');
|
|
1634
|
+
if (table.querySelectorAll('thead th').length < 2) problems.push('the data table has no header for the values');
|
|
1635
|
+
}
|
|
1636
|
+
const summary = described.find((el) => el.tagName !== 'TABLE');
|
|
1637
|
+
if (!summary || !(summary.textContent || '').trim()) problems.push('the plot has no one-line text summary (aria-describedby must name it)');
|
|
1638
|
+
// the bar forms of 0.19.0: a floating bar adds the two ends to the table (From, To), grouped bars add the group; a legend is drawn when one is declared
|
|
1639
|
+
const spans = Number(host.getAttribute('data-spans') || '0');
|
|
1640
|
+
const groups = Number(host.getAttribute('data-groups') || '0');
|
|
1641
|
+
const legend = Number(host.getAttribute('data-legend') || '0');
|
|
1642
|
+
if (kind === 'bars' && table) {
|
|
1643
|
+
const want = 3 + (spans > 0 ? 2 : 0) + (groups > 0 ? 1 : 0);
|
|
1644
|
+
const have = table.querySelectorAll('thead th').length;
|
|
1645
|
+
if (have !== want) problems.push(`the data table of ${spans > 0 ? 'floating' : 'grouped'} bars has ${have} columns, it must have ${want} (${[groups > 0 ? 'group' : '', 'category', 'value', spans > 0 ? 'from, to' : '', 'note'].filter(Boolean).join(', ')})`);
|
|
1646
|
+
}
|
|
1647
|
+
if (kind === 'bars' && legend > 0) {
|
|
1648
|
+
const keys = [...host.querySelectorAll('.caos-bars-legend li')].filter(visible);
|
|
1649
|
+
if (keys.length !== legend) problems.push(`the legend is not shown: the chart declares ${legend} entries and ${keys.length} are visible`);
|
|
1650
|
+
}
|
|
1651
|
+
// driven (the `interact` state): the arrow keys move the crosshair and change the readout, a scripted brush zooms, `+` zooms, the one
|
|
1652
|
+
// visible control and `0` reset. A chart that is zoomed already, or cannot be zoomed, is not driven for the zoom
|
|
1653
|
+
if (drive) {
|
|
1654
|
+
const key = (k) => stop.dispatchEvent(new KeyboardEvent('keydown', { key: k, bubbles: true, cancelable: true }));
|
|
1655
|
+
let zeroDead = false;
|
|
1656
|
+
const readout = host.querySelector('.caos-chart-readout, .caos-bars-readout');
|
|
1657
|
+
if (readout && points > 0) {
|
|
1658
|
+
const rest = (readout.textContent || '').trim();
|
|
1659
|
+
key('ArrowRight');
|
|
1660
|
+
await pause(80);
|
|
1661
|
+
const moved = (readout.textContent || '').trim();
|
|
1662
|
+
if (!moved || moved === rest) problems.push('the arrow keys do not move the crosshair (the readout does not change on ArrowRight)');
|
|
1663
|
+
key('End');
|
|
1664
|
+
key('Home');
|
|
1665
|
+
await pause(40);
|
|
1666
|
+
// floating and grouped bars: the keyboard walks every bar, each readout reads the bar's two ends (a floating bar) and its group (a grouped one)
|
|
1667
|
+
if (kind === 'bars' && (spans > 0 || groups > 0) && points > 1 && points <= 60) {
|
|
1668
|
+
const cells = table ? [...table.querySelectorAll('tbody tr')].map((tr) => [...tr.querySelectorAll('th, td')].map((c) => (c.textContent || '').trim())) : [];
|
|
1669
|
+
const seen = new Set();
|
|
1670
|
+
let unread = 0;
|
|
1671
|
+
let ungrouped = 0;
|
|
1672
|
+
for (let k = 0; k < points; k++) {
|
|
1673
|
+
if (k > 0) key('ArrowRight');
|
|
1674
|
+
await pause(30);
|
|
1675
|
+
const text = (readout.textContent || '').trim();
|
|
1676
|
+
seen.add(text);
|
|
1677
|
+
const row = cells[k] || [];
|
|
1678
|
+
const offset = groups > 0 ? 1 : 0;
|
|
1679
|
+
if (spans > 0 && row[offset + 2] && row[offset + 3] && !(text.includes(row[offset + 2]) && text.includes(row[offset + 3]))) unread += 1;
|
|
1680
|
+
if (groups > 0 && row[0] && !text.includes(row[0])) ungrouped += 1;
|
|
1681
|
+
}
|
|
1682
|
+
if (seen.size !== points) problems.push(`the keyboard walk reached ${seen.size} of ${points} bars (the arrow keys skip bars, or two bars read the same)`);
|
|
1683
|
+
if (unread > 0) problems.push(`the readout of ${unread} floating bar(s) does not read its two ends (the table has them: From and To)`);
|
|
1684
|
+
if (ungrouped > 0) problems.push(`the readout of ${ungrouped} grouped bar(s) does not read its group`);
|
|
1685
|
+
key('Home');
|
|
1686
|
+
await pause(40);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
if (host.getAttribute('data-zoomable') === '1' && !host.hasAttribute('data-zoomed')) {
|
|
1690
|
+
key('+');
|
|
1691
|
+
await pause(80);
|
|
1692
|
+
if (!host.hasAttribute('data-zoomed')) problems.push('the + key does not zoom the chart');
|
|
1693
|
+
else {
|
|
1694
|
+
const control = [...host.querySelectorAll('[data-chart-reset]')].filter(visible);
|
|
1695
|
+
if (control.length !== 1) problems.push(`a zoomed chart shows ${control.length} reset controls, it must show exactly one`);
|
|
1696
|
+
key('0');
|
|
1697
|
+
await pause(80);
|
|
1698
|
+
if (host.hasAttribute('data-zoomed')) {
|
|
1699
|
+
zeroDead = true;
|
|
1700
|
+
key('Escape');
|
|
1701
|
+
await pause(80);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
// a scripted brush across the plot: the mouse events uPlot listens for, the pointer events the bar chart listens for (it brushes along its
|
|
1705
|
+
// category axis: down the rows of a horizontal chart, across the columns of a vertical one)
|
|
1706
|
+
const target = kind === 'bars' ? host.querySelector('[data-chart-focus]') : host.querySelector('.u-over');
|
|
1707
|
+
if (target && visible(target) && !host.hasAttribute('data-zoomed')) {
|
|
1708
|
+
const r = target.getBoundingClientRect();
|
|
1709
|
+
const bars = kind === 'bars';
|
|
1710
|
+
const down = bars && host.getAttribute('data-orientation') !== 'vertical';
|
|
1711
|
+
// a bar chart is brushed from the middle of its first bar to the middle of a bar a third of the way along (the Contraste adoption, F8-3):
|
|
1712
|
+
// a fixed share of the drawing reached across every row of a short horizontal chart, whose axis and title take most of its height, and
|
|
1713
|
+
// a brush over every category is no zoom. Without bars to aim at, a fixed share of the drawing is dragged
|
|
1714
|
+
const drawnBars = bars ? [...host.querySelectorAll('svg [data-bar]')].map((g) => g.getBoundingClientRect()).filter((q) => q.width > 0 && q.height > 0) : [];
|
|
1715
|
+
const aim = drawnBars.length >= 2 ? [drawnBars[0], drawnBars[Math.max(1, Math.floor(drawnBars.length / 3))]] : null;
|
|
1716
|
+
const mid = (q) => ({ x: q.left + q.width / 2, y: q.top + q.height / 2 });
|
|
1717
|
+
const at = (f) => {
|
|
1718
|
+
if (aim) {
|
|
1719
|
+
const [a, b] = [mid(aim[0]), mid(aim[1])];
|
|
1720
|
+
return down ? { x: r.left + r.width * 0.5, y: a.y + (b.y - a.y) * f } : { x: a.x + (b.x - a.x) * f, y: r.top + r.height * 0.5 };
|
|
1721
|
+
}
|
|
1722
|
+
return down ? { x: r.left + r.width * 0.5, y: r.top + r.height * (0.15 + 0.4 * f) } : { x: r.left + r.width * (0.3 + 0.3 * f), y: r.top + r.height * 0.5 };
|
|
1723
|
+
};
|
|
1724
|
+
const fire = (type, f) => {
|
|
1725
|
+
const p = at(f);
|
|
1726
|
+
// uPlot ignores a mouse move with no movement while it drags (a Chrome-on-Windows stray event), so the scripted ones carry some
|
|
1727
|
+
const init = { bubbles: true, cancelable: true, clientX: p.x, clientY: p.y, button: 0, buttons: type.endsWith('up') ? 0 : 1, view: window, movementX: down ? 0 : 3, movementY: down ? 3 : 0 };
|
|
1728
|
+
target.dispatchEvent(bars ? new PointerEvent(type.replace('mouse', 'pointer'), { ...init, pointerId: 41, pointerType: 'mouse', isPrimary: true }) : new MouseEvent(type, init));
|
|
1729
|
+
};
|
|
1730
|
+
if (!bars) target.dispatchEvent(new MouseEvent('mouseenter', { bubbles: false, clientX: at(0).x, clientY: at(0).y }));
|
|
1731
|
+
fire('mousedown', 0);
|
|
1732
|
+
for (let k = 1; k <= 6; k++) fire('mousemove', k / 6);
|
|
1733
|
+
fire('mouseup', 1);
|
|
1734
|
+
await pause(120);
|
|
1735
|
+
if (!host.hasAttribute('data-zoomed')) problems.push('a drag across the plot does not zoom the chart (the brush is dead)');
|
|
1736
|
+
else {
|
|
1737
|
+
key('0');
|
|
1738
|
+
await pause(80);
|
|
1739
|
+
if (host.hasAttribute('data-zoomed')) {
|
|
1740
|
+
zeroDead = true;
|
|
1741
|
+
key('Escape');
|
|
1742
|
+
await pause(80);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
if (zeroDead) problems.push('the 0 key does not reset the zoom');
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
return out;
|
|
1751
|
+
};
|
|
1583
1752
|
}
|
package/gate/run.mjs
CHANGED
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
// on a surface App (--workbench '', --app-route; 0.16.0) the same for the views of its tab row: another view or
|
|
33
33
|
// sub-view in Focus or after the return, no ?focus=1 and view in the URL, no view that offers the control
|
|
34
34
|
// G19 values whole a short value (number, percentage, amount, unit) sits on one line in every state
|
|
35
|
+
// G20 chart interactions every chart is one tab stop with an accessible name, described by a table of its values and a one-line summary,
|
|
36
|
+
// and a zoomed chart resets (the + key zooms, the visible control shows, the 0 key resets); a chart that turns zoom
|
|
37
|
+
// off says why (12 letters and digits at least) and is listed among the waivers (0.19.0)
|
|
35
38
|
// plus the ADR-0071 and ADR-0017 measures (viewport, one-row tabs, rail without scroll, centred prose, captions,
|
|
36
39
|
// references).
|
|
37
40
|
|
|
@@ -481,7 +484,18 @@ export async function runGate(o) {
|
|
|
481
484
|
if (d) s.walkState.doc.push(d);
|
|
482
485
|
}
|
|
483
486
|
for (const r of f.repeatedTicks ?? []) {
|
|
484
|
-
|
|
487
|
+
// named x ticks (UPlotChart declares the repeated names): a name used twice, wherever the two ticks sit on the axis
|
|
488
|
+
if (r.names) fail('G6', where, `${r.label}: ${r.n} named x tick label(s) repeat another tick's label (${r.names.split('|').map((n) => `"${n}"`).join(', ')}); give every named tick a label of its own`);
|
|
489
|
+
else fail('G6', where, `${r.label}: ${r.n} axis tick label(s) repeat the label before them (give the axis fixed decimals)`);
|
|
490
|
+
}
|
|
491
|
+
// G20 (0.19.0): every chart is a tab stop with a name, a table of its values, a summary and a reset; one that turns zoom off is listed with its reason
|
|
492
|
+
for (const c of await page.evaluate((drive) => window.__caosGate.chartsTierA(drive), Boolean(s.interact))) {
|
|
493
|
+
for (const m of c.problems) fail('G20', where, `${c.label} (${c.kind} chart): ${m}`);
|
|
494
|
+
if (c.zoomOffSet) {
|
|
495
|
+
// listed once per route and language (the first state), failed in every state while the reason is too short
|
|
496
|
+
if (s.interact) waivers.push({ route: where.route, lang: s.lang, floor: 'chart zoom off', reason: c.zoomOff, valid: c.zoomOffValid });
|
|
497
|
+
if (!c.zoomOffValid) fail('G20', where, `${c.label}: zoom is turned off with a reason of fewer than 12 letters and digits ("${c.zoomOff}"); say why the chart cannot be zoomed`);
|
|
498
|
+
}
|
|
485
499
|
}
|
|
486
500
|
// a filling card whose note leaves its drawing under the floor (CAOS_APP_SHELL#95); below 1280 px the document scrolls
|
|
487
501
|
if (big) for (const c of f.fillStages ?? []) if (c.h < opt.stageFloor) fail('G6', where, `the drawing of ${c.label} keeps ${Math.round(c.h)} px under its note (floor ${opt.stageFloor} px)`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fasl-work/caos-app-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
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
|
@@ -22,12 +22,24 @@
|
|
|
22
22
|
"caos-architecture-diagram",
|
|
23
23
|
"caos-badges",
|
|
24
24
|
"caos-bars",
|
|
25
|
+
"caos-bars-brush",
|
|
26
|
+
"caos-bars-cursor",
|
|
27
|
+
"caos-bars-foot",
|
|
28
|
+
"caos-bars-host",
|
|
29
|
+
"caos-bars-key",
|
|
30
|
+
"caos-bars-legend",
|
|
31
|
+
"caos-bars-readout",
|
|
32
|
+
"caos-bars-swatch",
|
|
25
33
|
"caos-card",
|
|
26
34
|
"caos-chart",
|
|
35
|
+
"caos-chart-axis-title",
|
|
36
|
+
"caos-chart-foot",
|
|
27
37
|
"caos-chart-key",
|
|
38
|
+
"caos-chart-key-button",
|
|
28
39
|
"caos-chart-legend",
|
|
29
40
|
"caos-chart-plot",
|
|
30
41
|
"caos-chart-readout",
|
|
42
|
+
"caos-chart-reset",
|
|
31
43
|
"caos-chart-swatch",
|
|
32
44
|
"caos-chip-row",
|
|
33
45
|
"caos-chipgroup",
|
|
@@ -335,12 +347,24 @@
|
|
|
335
347
|
"caos-architecture-diagram",
|
|
336
348
|
"caos-badges",
|
|
337
349
|
"caos-bars",
|
|
350
|
+
"caos-bars-brush",
|
|
351
|
+
"caos-bars-cursor",
|
|
352
|
+
"caos-bars-foot",
|
|
353
|
+
"caos-bars-host",
|
|
354
|
+
"caos-bars-key",
|
|
355
|
+
"caos-bars-legend",
|
|
356
|
+
"caos-bars-readout",
|
|
357
|
+
"caos-bars-swatch",
|
|
338
358
|
"caos-card",
|
|
339
359
|
"caos-chart",
|
|
360
|
+
"caos-chart-axis-title",
|
|
361
|
+
"caos-chart-foot",
|
|
340
362
|
"caos-chart-key",
|
|
363
|
+
"caos-chart-key-button",
|
|
341
364
|
"caos-chart-legend",
|
|
342
365
|
"caos-chart-plot",
|
|
343
366
|
"caos-chart-readout",
|
|
367
|
+
"caos-chart-reset",
|
|
344
368
|
"caos-chart-swatch",
|
|
345
369
|
"caos-chip-row",
|
|
346
370
|
"caos-chipgroup",
|
package/styles.css
CHANGED
|
@@ -356,6 +356,9 @@ code, kbd { font-family: var(--font-mono); }
|
|
|
356
356
|
.prose table { display: block; max-width: 100%; overflow-x: auto; }
|
|
357
357
|
.prose pre { max-width: 100%; overflow-x: auto; }
|
|
358
358
|
.prose img, .prose svg, .prose video { max-width: 100%; height: auto; }
|
|
359
|
+
/* KaTeX draws a square root, a wide accent and a stretchy arrow as an SVG sized by its own rule (height: inherit, width 100% of
|
|
360
|
+
a span the glyph sizes); the media rule above made it height: auto, which collapsed it to under 2 px (shell 0.19.0, Contraste b11 and b13). */
|
|
361
|
+
.prose .katex svg { height: inherit; max-width: none; }
|
|
359
362
|
|
|
360
363
|
.fig-row { display: grid; grid-template-columns: 1.1fr 1fr; gap: 1.5rem; align-items: center; margin-block: 1.5rem; }
|
|
361
364
|
.fig-row.rev { grid-template-columns: 1fr 1.1fr; }
|
|
@@ -652,6 +655,35 @@ svg.lucide { flex: none; }
|
|
|
652
655
|
.caos-bars { position: relative; width: 100%; min-width: 0; }
|
|
653
656
|
.caos-bars.fill { height: 100%; min-height: 0; overflow-y: auto; }
|
|
654
657
|
.caos-bars > svg { display: block; }
|
|
658
|
+
/* The bar chart's host holds the drawing, the hidden table, the readout and the reset control (shell 0.19.0, Tier A); a filling
|
|
659
|
+
chart gives the drawing the height the foot leaves. */
|
|
660
|
+
.caos-bars-host { position: relative; width: 100%; min-width: 0; display: flex; flex-direction: column; }
|
|
661
|
+
.caos-bars-host.fill { height: 100%; min-height: 0; }
|
|
662
|
+
.caos-bars-host.fill > .caos-bars { flex: 1 1 0; height: auto; min-height: 0; }
|
|
663
|
+
.caos-bars-host > .caos-bars { flex: none; }
|
|
664
|
+
.caos-bars-host.fill > .caos-bars-foot, .caos-bars-host.fill > .caos-chart-axis-title { flex: none; }
|
|
665
|
+
.caos-bars-foot { display: flex; align-items: center; gap: 0.5rem; min-width: 0; }
|
|
666
|
+
.caos-bars-readout { flex: 1 1 0; min-width: 0; 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; }
|
|
667
|
+
.caos-bars[data-chart-focus] { outline-offset: -2px; border-radius: var(--radius-sm); touch-action: pan-y; }
|
|
668
|
+
.caos-bars[data-chart-focus][data-zoomed] { touch-action: none; }
|
|
669
|
+
.caos-bars-brush { fill: var(--color-accent); fill-opacity: 0.16; stroke: var(--color-accent); stroke-width: 1; pointer-events: none; }
|
|
670
|
+
.caos-bars-cursor { fill: var(--color-accent); fill-opacity: 0.1; pointer-events: none; }
|
|
671
|
+
/* grouped bars (0.19.0): the legend of what each tone means, under the bars; a square swatch in the tone's colour */
|
|
672
|
+
.caos-bars-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); }
|
|
673
|
+
.caos-bars-key { display: inline-flex; align-items: center; gap: 0.35rem; min-width: 0; }
|
|
674
|
+
.caos-bars-swatch { flex: none; width: 10px; height: 10px; border-radius: 2px; background: var(--swatch); }
|
|
675
|
+
/* Tier A of the shell charts (0.19.0): the tab stop's ring, an axis title that moved off the axis, the foot with the reset control. */
|
|
676
|
+
.caos-chart-plot[data-chart-focus]:focus-visible, .caos-bars[data-chart-focus]:focus-visible { outline: 2px solid var(--color-accent); outline-offset: -2px; }
|
|
677
|
+
/* A chart that holds the focus takes the wheel (a click gives it the focus, however it was given): the ring says so, in both themes. */
|
|
678
|
+
.caos-chart[data-zoomable="1"] > .caos-chart-plot[data-chart-focus]:focus, .caos-bars-host[data-zoomable="1"] > .caos-bars[data-chart-focus]:focus { outline: 2px solid var(--color-accent); outline-offset: -2px; }
|
|
679
|
+
.caos-chart-axis-title { margin: 0 0 0.15rem; font-size: 0.73rem; line-height: 1.3; color: var(--color-fg-subtle); overflow-wrap: anywhere; }
|
|
680
|
+
.caos-chart-axis-title[data-axis-title="x"] { margin: 0.1rem 0 0; text-align: center; }
|
|
681
|
+
.caos-chart.fill > .caos-chart-axis-title, .caos-chart.fill > .caos-chart-foot { flex: none; }
|
|
682
|
+
.caos-chart-foot { display: flex; align-items: center; gap: 0.5rem; min-width: 0; }
|
|
683
|
+
.caos-chart-foot > .caos-chart-readout { flex: 1 1 0; min-width: 0; }
|
|
684
|
+
.caos-chart-reset { flex: none; box-sizing: border-box; min-width: 24px; min-height: 24px; margin: 0.15rem 0 0; padding: 0.15rem 0.5rem; border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-surface-2); color: var(--color-fg); font: inherit; font-size: 0.72rem; line-height: 1.15; cursor: pointer; }
|
|
685
|
+
.caos-chart-reset:hover { border-color: var(--color-fg-subtle); }
|
|
686
|
+
.caos-chart-reset:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 1px; }
|
|
655
687
|
/* A view whose data is not there yet says so (and declares data-state="loading" for the gate). */
|
|
656
688
|
.caos-pending { margin: auto; padding: var(--space-5); color: var(--color-fg-faint); font-size: 0.86rem; text-align: center; }
|
|
657
689
|
/* A failed view (PanelBoundary): an inline message in its own frame, never a blank page. */
|
package/dist/chunk-YECZXGVG.js
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import { LANG_STORAGE_KEY, THEME_STORAGE_KEY } from './chunk-33WTUQ7Q.js';
|
|
2
|
-
import { create } from 'zustand';
|
|
3
|
-
import { useMemo, useState, useRef, useCallback, useEffect } from 'react';
|
|
4
|
-
import { jsx } from 'react/jsx-runtime';
|
|
5
|
-
|
|
6
|
-
var KEY = LANG_STORAGE_KEY;
|
|
7
|
-
function readLang() {
|
|
8
|
-
try {
|
|
9
|
-
const s = localStorage.getItem(KEY);
|
|
10
|
-
if (s === "en" || s === "es") return s;
|
|
11
|
-
} catch {
|
|
12
|
-
}
|
|
13
|
-
return "en";
|
|
14
|
-
}
|
|
15
|
-
var useLangStore = create((set, get) => ({
|
|
16
|
-
lang: readLang(),
|
|
17
|
-
setLang: (l) => {
|
|
18
|
-
try {
|
|
19
|
-
localStorage.setItem(KEY, l);
|
|
20
|
-
} catch {
|
|
21
|
-
}
|
|
22
|
-
set({ lang: l });
|
|
23
|
-
},
|
|
24
|
-
toggle: () => {
|
|
25
|
-
const next = get().lang === "en" ? "es" : "en";
|
|
26
|
-
try {
|
|
27
|
-
localStorage.setItem(KEY, next);
|
|
28
|
-
} catch {
|
|
29
|
-
}
|
|
30
|
-
set({ lang: next });
|
|
31
|
-
}
|
|
32
|
-
}));
|
|
33
|
-
var useShellLang = () => useLangStore((s) => s.lang);
|
|
34
|
-
|
|
35
|
-
// src/lib/text.ts
|
|
36
|
-
function pick(text, lang) {
|
|
37
|
-
if (text === void 0) return "";
|
|
38
|
-
if (typeof text === "string") return text;
|
|
39
|
-
return text[lang] ?? text.en;
|
|
40
|
-
}
|
|
41
|
-
var KEY2 = THEME_STORAGE_KEY;
|
|
42
|
-
function readTheme() {
|
|
43
|
-
try {
|
|
44
|
-
const s = localStorage.getItem(KEY2);
|
|
45
|
-
if (s === "light" || s === "dark") return s;
|
|
46
|
-
return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
|
|
47
|
-
} catch {
|
|
48
|
-
return "dark";
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function applyTheme(t) {
|
|
52
|
-
try {
|
|
53
|
-
document.documentElement.dataset.theme = t;
|
|
54
|
-
} catch {
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
var useThemeStore = create((set, get) => ({
|
|
58
|
-
theme: readTheme(),
|
|
59
|
-
toggleTheme: () => {
|
|
60
|
-
const next = get().theme === "dark" ? "light" : "dark";
|
|
61
|
-
applyTheme(next);
|
|
62
|
-
try {
|
|
63
|
-
localStorage.setItem(KEY2, next);
|
|
64
|
-
} catch {
|
|
65
|
-
}
|
|
66
|
-
set({ theme: next });
|
|
67
|
-
},
|
|
68
|
-
setTheme: (t) => {
|
|
69
|
-
applyTheme(t);
|
|
70
|
-
try {
|
|
71
|
-
localStorage.setItem(KEY2, t);
|
|
72
|
-
} catch {
|
|
73
|
-
}
|
|
74
|
-
set({ theme: t });
|
|
75
|
-
}
|
|
76
|
-
}));
|
|
77
|
-
var SHELL_TOKENS = [
|
|
78
|
-
"--color-bg",
|
|
79
|
-
"--color-surface",
|
|
80
|
-
"--color-surface-2",
|
|
81
|
-
"--color-border",
|
|
82
|
-
"--color-fg",
|
|
83
|
-
"--color-fg-subtle",
|
|
84
|
-
"--color-fg-faint",
|
|
85
|
-
"--color-accent",
|
|
86
|
-
"--color-accent-fg",
|
|
87
|
-
"--color-accent-soft",
|
|
88
|
-
"--color-accent-2",
|
|
89
|
-
"--color-magenta",
|
|
90
|
-
"--color-good",
|
|
91
|
-
"--color-warn",
|
|
92
|
-
"--color-bad",
|
|
93
|
-
"--color-shadow",
|
|
94
|
-
"--font-sans",
|
|
95
|
-
"--font-mono",
|
|
96
|
-
"--text-xs",
|
|
97
|
-
"--text-sm",
|
|
98
|
-
"--text-md",
|
|
99
|
-
"--text-base",
|
|
100
|
-
"--text-lg",
|
|
101
|
-
"--text-xl",
|
|
102
|
-
"--text-2xl",
|
|
103
|
-
"--space-1",
|
|
104
|
-
"--space-2",
|
|
105
|
-
"--space-3",
|
|
106
|
-
"--space-4",
|
|
107
|
-
"--space-5",
|
|
108
|
-
"--space-6",
|
|
109
|
-
"--radius-sm",
|
|
110
|
-
"--radius-md",
|
|
111
|
-
"--radius-lg",
|
|
112
|
-
"--radius-pill",
|
|
113
|
-
"--maxw",
|
|
114
|
-
"--maxw-wide",
|
|
115
|
-
"--measure",
|
|
116
|
-
"--measure-text",
|
|
117
|
-
"--header-h",
|
|
118
|
-
"--rail-w",
|
|
119
|
-
"--fade",
|
|
120
|
-
"--icon-sm",
|
|
121
|
-
"--icon-md",
|
|
122
|
-
"--icon-lg",
|
|
123
|
-
"--z-header",
|
|
124
|
-
"--z-focus",
|
|
125
|
-
"--z-modal"
|
|
126
|
-
];
|
|
127
|
-
function resolveToken(token, fallback = "") {
|
|
128
|
-
if (typeof document === "undefined") return fallback;
|
|
129
|
-
const v = getComputedStyle(document.documentElement).getPropertyValue(token).trim();
|
|
130
|
-
return v || fallback;
|
|
131
|
-
}
|
|
132
|
-
function useThemeTokens() {
|
|
133
|
-
const theme = useThemeStore((s) => s.theme);
|
|
134
|
-
return useMemo(() => {
|
|
135
|
-
const out = {};
|
|
136
|
-
for (const t of SHELL_TOKENS) out[t] = resolveToken(t);
|
|
137
|
-
return out;
|
|
138
|
-
}, [theme]);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// src/lib/format.ts
|
|
142
|
-
var LOCALE = { en: "en-US", es: "es-CL" };
|
|
143
|
-
var NBSP = "\xA0";
|
|
144
|
-
var SCIENTIFIC_BELOW = 1e-4;
|
|
145
|
-
function belowScientific(magnitude) {
|
|
146
|
-
return magnitude !== 0 && Math.abs(magnitude) < SCIENTIFIC_BELOW * (1 - 1e-12);
|
|
147
|
-
}
|
|
148
|
-
var NOT_AVAILABLE = { en: "not available", es: "no disponible" };
|
|
149
|
-
function formatNumber(value, lang, opts = {}) {
|
|
150
|
-
if (value === null || value === void 0 || !Number.isFinite(value)) return NOT_AVAILABLE[lang];
|
|
151
|
-
const v = opts.percent ? value * 100 : value;
|
|
152
|
-
const digits = opts.digits ?? 4;
|
|
153
|
-
const scientific = v !== 0 && (opts.notation === "scientific" || belowScientific(v));
|
|
154
|
-
const grouping = opts.grouping === false ? { useGrouping: false } : {};
|
|
155
|
-
const nf = opts.decimals !== void 0 ? new Intl.NumberFormat(LOCALE[lang], { minimumFractionDigits: opts.decimals, maximumFractionDigits: opts.decimals, ...grouping }) : scientific ? new Intl.NumberFormat(LOCALE[lang], { notation: "scientific", maximumSignificantDigits: digits }) : Math.abs(v) >= 10 ** (digits - 1) ? new Intl.NumberFormat(LOCALE[lang], { maximumFractionDigits: 0, ...grouping }) : new Intl.NumberFormat(LOCALE[lang], { maximumSignificantDigits: digits, ...grouping });
|
|
156
|
-
return opts.percent ? `${nf.format(v)}${NBSP}%` : nf.format(v);
|
|
157
|
-
}
|
|
158
|
-
function formatTicks(values, lang, opts = {}) {
|
|
159
|
-
let o = opts;
|
|
160
|
-
if (opts.decimals === void 0 && opts.notation === void 0) {
|
|
161
|
-
const scale = opts.percent ? 100 : 1;
|
|
162
|
-
const small = values.some((v) => v !== null && v !== void 0 && Number.isFinite(v) && belowScientific(v * scale));
|
|
163
|
-
if (small) o = { ...opts, notation: "scientific" };
|
|
164
|
-
}
|
|
165
|
-
return values.map((v) => v === null || v === void 0 ? "" : formatNumber(v, lang, o));
|
|
166
|
-
}
|
|
167
|
-
function useFormat() {
|
|
168
|
-
const lang = useShellLang();
|
|
169
|
-
return (value, opts) => formatNumber(value, lang, opts);
|
|
170
|
-
}
|
|
171
|
-
function useStageSize() {
|
|
172
|
-
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
173
|
-
const roRef = useRef(null);
|
|
174
|
-
const ref = useCallback((el) => {
|
|
175
|
-
roRef.current?.disconnect();
|
|
176
|
-
roRef.current = null;
|
|
177
|
-
if (!el) return;
|
|
178
|
-
const measure = () => {
|
|
179
|
-
const r = el.getBoundingClientRect();
|
|
180
|
-
setSize((prev) => prev.width === Math.round(r.width) && prev.height === Math.round(r.height) ? prev : { width: Math.round(r.width), height: Math.round(r.height) });
|
|
181
|
-
};
|
|
182
|
-
measure();
|
|
183
|
-
if (typeof ResizeObserver !== "undefined") {
|
|
184
|
-
roRef.current = new ResizeObserver(measure);
|
|
185
|
-
roRef.current.observe(el);
|
|
186
|
-
}
|
|
187
|
-
}, []);
|
|
188
|
-
useEffect(() => () => roRef.current?.disconnect(), []);
|
|
189
|
-
return [ref, size];
|
|
190
|
-
}
|
|
191
|
-
function Stage({ label, children, className }) {
|
|
192
|
-
const lang = useShellLang();
|
|
193
|
-
const [ref, size] = useStageSize();
|
|
194
|
-
const ready = size.width > 0 && size.height > 0;
|
|
195
|
-
const name = pick(label, lang);
|
|
196
|
-
useEffect(() => {
|
|
197
|
-
if (ready) return;
|
|
198
|
-
const t = setTimeout(() => {
|
|
199
|
-
console.error(`[caos-app-shell] Stage "${name}" has no size; give its container a height (ADR-0071 rule 8)`);
|
|
200
|
-
}, 1500);
|
|
201
|
-
return () => clearTimeout(t);
|
|
202
|
-
}, [ready, name]);
|
|
203
|
-
return /* @__PURE__ */ jsx(
|
|
204
|
-
"div",
|
|
205
|
-
{
|
|
206
|
-
ref,
|
|
207
|
-
className: ["caos-stage", className].filter(Boolean).join(" "),
|
|
208
|
-
"data-stage": name,
|
|
209
|
-
"data-drawn": ready ? "1" : "0",
|
|
210
|
-
"data-width": size.width,
|
|
211
|
-
"data-height": size.height,
|
|
212
|
-
children: ready ? children(size) : null
|
|
213
|
-
}
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export { NBSP, NOT_AVAILABLE, SCIENTIFIC_BELOW, SHELL_TOKENS, Stage, applyTheme, formatNumber, formatTicks, pick, readTheme, resolveToken, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens };
|