@fasl-work/caos-app-shell 0.8.0 → 0.9.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/README.md CHANGED
@@ -121,8 +121,9 @@ const config: ShellConfig = {
121
121
  `ChipGroup`, `Stage`, `useStageSize`, `makeStateKey`, `isStale`, `useWorkbenchState`,
122
122
  `WorkbenchStateContext`, `pick`; since 0.8.0 `SurfacePage`, `useUrlView`, `viewParamOf`, `replaceQueryParam`,
123
123
  `BREAKPOINTS`, `BarChart`, `textWidth`, `widestLabel`, `fitLabel`, `niceTicks`, `niceStep`, `fontFamily`,
124
- `formatTicks`, `NBSP`, `SCIENTIFIC_BELOW`, the type `ShellColorToken`. Separate entries: `/chart` (`UPlotChart`,
125
- `drawMarks`, `tickSteps`, `repeatedLabels`), `/keys` (the storage keys, no React), `/styles.css`, `/chart.css`,
124
+ `formatTicks`, `NBSP`, `SCIENTIFIC_BELOW`, the type `ShellColorToken`; since 0.9.0 `ViewsRow`. Separate entries:
125
+ `/chart` (`UPlotChart` with `x.log`, `parity`, `onPick`; `drawMarks`, `tickSteps`, `repeatedLabels`, `seriesStyle`,
126
+ `sortOrder`, `parityRange`), `/keys` (the storage keys, no React), `/styles.css`, `/chart.css`,
126
127
  `/reserved-classes.json`. Bin: `caos-shell-gate`.
127
128
 
128
129
  ### Authenticated workbenches
package/chart.css CHANGED
@@ -16,4 +16,7 @@
16
16
  .caos-chart-swatch { flex: none; width: 16px; height: 0; border-top: 2px solid var(--swatch); }
17
17
  .caos-chart-key[data-dash] .caos-chart-swatch { border-top-style: dashed; }
18
18
  .caos-chart-key[data-mode="points"] .caos-chart-swatch { width: 7px; height: 7px; border: 0; border-radius: 50%; background: var(--swatch); }
19
+ /* A parity plot is square and centred in its box; a chart whose points can be picked shows a pointer. */
20
+ .caos-chart[data-parity] .uplot { margin-inline: auto; }
21
+ .caos-chart[data-pick] .u-over { cursor: pointer; }
19
22
  .caos-chart .u-cursor-x, .caos-chart .u-cursor-y { border-color: var(--color-fg-faint) !important; }
package/dist/chart.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import uPlot from 'uplot';
3
- import { F as FormatOptions, S as ShellColorToken } from './tokens-DkyEXX5g.js';
3
+ import { F as FormatOptions, S as ShellColorToken } from './tokens-BTOzar-U.js';
4
4
  import { B as BiText } from './text-BlY47nIt.js';
5
5
  import 'zustand';
6
6
 
@@ -26,6 +26,7 @@ interface UPlotChartProps {
26
26
  x: ChartAxis & {
27
27
  values: number[];
28
28
  time?: boolean;
29
+ log?: boolean;
29
30
  };
30
31
  y: ChartAxis & {
31
32
  log?: boolean;
@@ -40,9 +41,26 @@ interface UPlotChartProps {
40
41
  /** Height in pixels, or `fill` to take the container's height (inside `PlotCard fill`); the width is always the
41
42
  * container's. */
42
43
  height?: number | 'fill';
43
- /** Called with the index under the cursor (or null), for linked views. */
44
+ /** Called with the index under the cursor (or null), for linked views; the index is the caller's, before any
45
+ * sorting by x. */
44
46
  onCursor?: (index: number | null) => void;
47
+ /** Called with the caller's index of the point under the cursor when the plot is clicked (select a case, a blast). */
48
+ onPick?: (index: number) => void;
49
+ /** A parity plot: predicted (the series) against observed (x) on one shared range, in a square box, with the
50
+ * identity line. Unsorted x is allowed (the chart sorts it and maps every index back). */
51
+ parity?: boolean;
45
52
  }
53
+ /** The colour and dash of a series: its own, or the rotation's; when the rotation comes round (a seventh series
54
+ * repeats the first colour) the repeat is dashed, so two series never look the same (CAOS_Fragmenta drew this for
55
+ * itself until 0.9.0). */
56
+ declare function seriesStyle(s: ChartSeries, i: number): {
57
+ color: ShellColorToken;
58
+ dash?: number[];
59
+ };
60
+ /** The order that sorts x ascending (uPlot draws aligned data by increasing x), or null when x is already sorted. */
61
+ declare function sortOrder(xs: readonly number[]): number[] | null;
62
+ /** One range for both axes of a parity plot, padded by 5% of the span on each side. */
63
+ declare function parityRange(...arrays: readonly (readonly (number | null)[])[]): [number, number];
46
64
  /**
47
65
  * The tick steps an axis may use. With fixed `decimals` (scaled by 100 for a percent) only multiples of the smallest
48
66
  * difference its labels can show, so no two ticks read the same: an integer axis ticks at integers, never at 1.5,
@@ -78,6 +96,6 @@ declare function drawMarks(u: uPlot, marks: {
78
96
  * (known shell defect 17). An axis with fixed decimals ticks only where its labels differ (`tickSteps`). The host
79
97
  * declares `data-series`, `data-axis-titles`, `data-ticks-cut`, `data-ticks-repeat` and `data-drawn` for the gate.
80
98
  */
81
- declare function UPlotChart({ x, y, series, marks, height, onCursor }: UPlotChartProps): react.JSX.Element;
99
+ declare function UPlotChart({ x, y, series, marks, height, onCursor, onPick, parity }: UPlotChartProps): react.JSX.Element;
82
100
 
83
- export { type ChartAxis, type ChartSeries, UPlotChart, type UPlotChartProps, drawMarks, repeatedLabels, tickSteps };
101
+ export { type ChartAxis, type ChartSeries, UPlotChart, type UPlotChartProps, drawMarks, parityRange, repeatedLabels, seriesStyle, sortOrder, tickSteps };
package/dist/chart.js CHANGED
@@ -1,10 +1,35 @@
1
- import { useShellLang, useThemeStore, useStageSize, pick, resolveToken, NBSP, formatNumber, formatTicks } from './chunk-ZLJCB7DD.js';
1
+ import { useShellLang, useThemeStore, useStageSize, pick, resolveToken, NBSP, formatNumber, formatTicks } from './chunk-IDUNUMCR.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
+ function seriesStyle(s, i) {
9
+ const color = s.color ?? ROTATION[i % ROTATION.length];
10
+ const dash = s.dash ?? (!s.color && i >= ROTATION.length ? [6, 4] : void 0);
11
+ return { color, dash };
12
+ }
13
+ function sortOrder(xs) {
14
+ for (let i = 1; i < xs.length; i++) {
15
+ if (xs[i] < xs[i - 1]) return xs.map((_, j) => j).sort((a, b) => xs[a] - xs[b] || a - b);
16
+ }
17
+ return null;
18
+ }
19
+ function parityRange(...arrays) {
20
+ let lo = Infinity;
21
+ let hi = -Infinity;
22
+ for (const a of arrays) {
23
+ for (const v of a) {
24
+ if (v === null || !Number.isFinite(v)) continue;
25
+ lo = Math.min(lo, v);
26
+ hi = Math.max(hi, v);
27
+ }
28
+ }
29
+ if (!Number.isFinite(lo)) return [0, 1];
30
+ const pad = (hi - lo || Math.abs(hi) || 1) * 0.05;
31
+ return [lo - pad, hi + pad];
32
+ }
8
33
  var STEPS = [];
9
34
  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
35
  function tickSteps(fmt) {
@@ -57,7 +82,7 @@ function drawMarks(u, marks, style) {
57
82
  }
58
83
  ctx.restore();
59
84
  }
60
- function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
85
+ function UPlotChart({ x, y, series, marks, height = 280, onCursor, onPick, parity }) {
61
86
  const lang = useShellLang();
62
87
  const theme = useThemeStore((s) => s.theme);
63
88
  const fill = height === "fill";
@@ -70,20 +95,30 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
70
95
  },
71
96
  [measureRef]
72
97
  );
73
- const width = box.width;
74
- const h = fill ? box.height : height;
98
+ const width = parity ? Math.min(box.width, fill ? box.height : height) : box.width;
99
+ const h = parity ? width : fill ? box.height : height;
75
100
  const uRef = useRef(null);
76
101
  const hostRef = useRef(null);
77
102
  const [cursor, setCursor] = useState(null);
78
103
  const onCursorRef = useRef(onCursor);
79
104
  onCursorRef.current = onCursor;
105
+ const onPickRef = useRef(onPick);
106
+ onPickRef.current = onPick;
107
+ const order = useMemo(() => sortOrder(x.values), [x.values]);
108
+ const orderRef = useRef(order);
109
+ orderRef.current = order;
110
+ const xs = useMemo(() => order ? order.map((i) => x.values[i]) : x.values, [order, x.values]);
111
+ const ys = useMemo(() => series.map((s) => order ? order.map((i) => s.values[i] ?? null) : s.values), [order, series]);
112
+ const range = useMemo(() => parity ? parityRange(xs, ...ys) : null, [parity, xs, ys]);
80
113
  const xLabel = pick(x.label, lang);
81
114
  const yLabel = pick(y.label, lang);
82
115
  const xUnit = x.unit ? pick(x.unit, lang) : "";
83
116
  const yUnit = y.unit ? pick(y.unit, lang) : "";
84
117
  const structure = JSON.stringify({
85
- s: series.map((s, i) => [pick(s.label, lang), s.color ?? ROTATION[i % ROTATION.length], s.width ?? 2, s.dash ?? null, s.mode ?? "line"]),
86
- x: [xLabel, xUnit, Boolean(x.time), x.format ?? null],
118
+ s: series.map((s, i) => [pick(s.label, lang), seriesStyle(s, i), s.width ?? 2, s.mode ?? "line"]),
119
+ x: [xLabel, xUnit, Boolean(x.time), Boolean(x.log), x.format ?? null],
120
+ p: range,
121
+ pick: Boolean(onPick),
87
122
  y: [yLabel, yUnit, Boolean(y.log), y.range ?? null, y.format ?? null],
88
123
  m: (marks ?? []).map((m) => [m.x, pick(m.label, lang)]),
89
124
  theme,
@@ -91,7 +126,7 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
91
126
  h,
92
127
  w: width
93
128
  });
94
- const data = useMemo(() => [x.values, ...series.map((s) => s.values)], [x.values, series]);
129
+ const data = useMemo(() => [xs, ...ys], [xs, ys]);
95
130
  useEffect(() => {
96
131
  const el = plotRef.current;
97
132
  if (!el || width <= 0 || h <= 0) return;
@@ -107,13 +142,23 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
107
142
  hostRef.current?.setAttribute("data-ticks-repeat", String(repeats.x + repeats.y));
108
143
  return labels;
109
144
  };
110
- const xSteps = x.time ? void 0 : tickSteps(x.format);
145
+ const xSteps = x.time || x.log ? void 0 : tickSteps(x.format);
111
146
  const ySteps = y.log ? void 0 : tickSteps(y.format);
112
147
  const opts = {
113
148
  width,
114
149
  height: h,
115
150
  legend: { show: false },
116
- scales: { x: { time: Boolean(x.time) }, y: { distr: y.log ? 3 : 1, ...y.range ? { range: y.range } : {} } },
151
+ // a log axis labels the ticks uPlot keeps and passes null for the rest, which formatTicks leaves blank
152
+ scales: {
153
+ // a log x axis spans the data, not the decades around it (uPlot rounds out to the next decade, which left an
154
+ // axis to 1,000 for data that ends at 120)
155
+ x: {
156
+ time: Boolean(x.time),
157
+ distr: x.log ? 3 : 1,
158
+ ...range ? { range } : x.log ? { range: (_u, min, max) => [min, max] } : {}
159
+ },
160
+ y: { distr: y.log ? 3 : 1, ...range ? { range } : y.range ? { range: y.range } : {} }
161
+ },
117
162
  axes: [
118
163
  {
119
164
  stroke: fg,
@@ -149,8 +194,9 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
149
194
  series: [
150
195
  {},
151
196
  ...series.map((s, i) => {
152
- const stroke = resolveToken(s.color ?? ROTATION[i % ROTATION.length], "#4a8");
153
- return s.mode === "points" ? { label: pick(s.label, lang), stroke, width: 1.5, paths: () => null, points: { show: true, space: 0, size: 9, fill: stroke } } : { label: pick(s.label, lang), stroke, width: s.width ?? 2, dash: s.dash, spanGaps: false, points: { show: false } };
197
+ const style = seriesStyle(s, i);
198
+ const stroke = resolveToken(style.color, "#4a8");
199
+ return s.mode === "points" ? { label: pick(s.label, lang), stroke, width: 1.5, paths: () => null, points: { show: true, space: 0, size: 9, fill: stroke } } : { label: pick(s.label, lang), stroke, width: s.width ?? 2, dash: style.dash, spanGaps: false, points: { show: false } };
154
200
  })
155
201
  ],
156
202
  hooks: {
@@ -158,11 +204,33 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
158
204
  (u) => {
159
205
  const idx = u.cursor.idx ?? null;
160
206
  setCursor(idx);
161
- onCursorRef.current?.(idx);
207
+ onCursorRef.current?.(idx === null ? null : orderRef.current ? orderRef.current[idx] : idx);
208
+ }
209
+ ],
210
+ ready: [
211
+ (u) => {
212
+ u.over.addEventListener("click", () => {
213
+ const idx = u.cursor.idx;
214
+ if (idx === null || idx === void 0 || !onPickRef.current) return;
215
+ onPickRef.current(orderRef.current ? orderRef.current[idx] : idx);
216
+ });
162
217
  }
163
218
  ],
164
219
  draw: [
165
220
  (u) => {
221
+ if (range) {
222
+ const ctx = u.ctx;
223
+ const pr = uPlot.pxRatio || 1;
224
+ ctx.save();
225
+ ctx.strokeStyle = resolveToken("--color-fg-faint", "#888");
226
+ ctx.lineWidth = Math.max(1, pr);
227
+ ctx.setLineDash([5 * pr, 4 * pr]);
228
+ ctx.beginPath();
229
+ ctx.moveTo(u.valToPos(range[0], "x", true), u.valToPos(range[0], "y", true));
230
+ ctx.lineTo(u.valToPos(range[1], "x", true), u.valToPos(range[1], "y", true));
231
+ ctx.stroke();
232
+ ctx.restore();
233
+ }
166
234
  if (!marks?.length) return;
167
235
  drawMarks(u, marks.map((m) => ({ x: m.x, label: pick(m.label, lang) })), {
168
236
  color: resolveToken("--color-warn", "#c80"),
@@ -184,9 +252,9 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
184
252
  uRef.current?.setData(data);
185
253
  }, [data]);
186
254
  const read = cursor !== null ? [
187
- `${xLabel} ${formatNumber(x.values[cursor], lang, x.format ?? { digits: 4 })}${xUnit ? `${NBSP}${xUnit}` : ""}`,
255
+ `${xLabel} ${formatNumber(xs[cursor], lang, x.format ?? { digits: 4 })}${xUnit ? `${NBSP}${xUnit}` : ""}`,
188
256
  ...series.map(
189
- (s) => `${pick(s.label, lang)} ${formatNumber(s.values[cursor] ?? null, lang, y.format ?? { digits: 4 })}${yUnit ? `${NBSP}${yUnit}` : ""}`
257
+ (s, k) => `${pick(s.label, lang)} ${formatNumber(ys[k][cursor] ?? null, lang, y.format ?? { digits: 4 })}${yUnit ? `${NBSP}${yUnit}` : ""}`
190
258
  )
191
259
  ].join(" \xB7 ") : lang === "es" ? "Pase el cursor sobre el gr\xE1fico para leer los valores" : "Hover the chart to read the values";
192
260
  return /* @__PURE__ */ jsxs(
@@ -199,10 +267,13 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
199
267
  "data-ticks-cut": "0",
200
268
  "data-ticks-repeat": "0",
201
269
  "data-drawn": width > 0 && h > 0 ? "1" : "0",
270
+ "data-parity": parity ? "1" : void 0,
271
+ "data-log-x": x.log ? "1" : void 0,
272
+ "data-pick": onPick ? "1" : void 0,
202
273
  children: [
203
274
  /* @__PURE__ */ jsx("div", { ref: setPlot, className: "caos-chart-plot" }),
204
- 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: [
205
- /* @__PURE__ */ jsx("span", { className: "caos-chart-swatch", style: { "--swatch": `var(${s.color ?? ROTATION[i % ROTATION.length]})` }, "aria-hidden": "true" }),
275
+ 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": seriesStyle(s, i).dash ? "1" : void 0, children: [
276
+ /* @__PURE__ */ jsx("span", { className: "caos-chart-swatch", style: { "--swatch": `var(${seriesStyle(s, i).color})` }, "aria-hidden": "true" }),
206
277
  pick(s.label, lang)
207
278
  ] }, i)) }),
208
279
  /* @__PURE__ */ jsx("p", { className: "caos-chart-readout", "aria-live": "polite", title: read, children: read })
@@ -211,4 +282,4 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
211
282
  );
212
283
  }
213
284
 
214
- export { UPlotChart, drawMarks, repeatedLabels, tickSteps };
285
+ export { UPlotChart, drawMarks, parityRange, repeatedLabels, seriesStyle, sortOrder, tickSteps };
@@ -148,7 +148,8 @@ function formatNumber(value, lang, opts = {}) {
148
148
  const v = opts.percent ? value * 100 : value;
149
149
  const digits = opts.digits ?? 4;
150
150
  const scientific = v !== 0 && (opts.notation === "scientific" || Math.abs(v) < SCIENTIFIC_BELOW);
151
- const nf = opts.decimals !== void 0 ? new Intl.NumberFormat(LOCALE[lang], { minimumFractionDigits: opts.decimals, maximumFractionDigits: opts.decimals }) : scientific ? 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 });
151
+ const grouping = opts.grouping === false ? { useGrouping: false } : {};
152
+ 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 });
152
153
  return opts.percent ? `${nf.format(v)}${NBSP}%` : nf.format(v);
153
154
  }
154
155
  function formatTicks(values, lang, opts = {}) {
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-DkyEXX5g.js';
6
- export { N as NBSP, a as NOT_AVAILABLE, b as SCIENTIFIC_BELOW, c as SHELL_TOKENS, S as ShellColorToken, d as ShellToken, f as formatNumber, e as formatTicks, r as resolveToken, u as useFormat, g as useThemeTokens } from './tokens-DkyEXX5g.js';
5
+ import { F as FormatOptions } from './tokens-BTOzar-U.js';
6
+ export { N as NBSP, a as NOT_AVAILABLE, b as SCIENTIFIC_BELOW, c as SHELL_TOKENS, S as ShellColorToken, d as ShellToken, f as formatNumber, e as formatTicks, r as resolveToken, u as useFormat, g as useThemeTokens } from './tokens-BTOzar-U.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
 
@@ -823,4 +823,17 @@ declare function Stage({ label, children, className }: {
823
823
  className?: string;
824
824
  }): react.JSX.Element;
825
825
 
826
- export { BREAKPOINTS, BarChart, type BarChartProps, type BarDatum, BiText, Callout, type CaseDef, type CaseGroup, type CaseKind, CaseSelector, type CaseSelectorProps, type CaseSelectorText, CaseWorkbench, type CaseWorkbenchProps, ChipGroup, type ChipGroupProps, type ChipOption, type Citation, CitationsProvider, Cite, DocPage, DocSection, type DocSectionProps, Equation, Figure, type FittedLabel, FocusShell, type FocusShellProps, FormatOptions, Gauge, type GaugeProps, type GaugeZone, InlineMath, Knob, type KnobProps, type Lane, LaneBadge, Lang, LanguageToggle, MAX_PEER_TABS, MAX_WORKBENCH_GROUPS, PanelBoundary, type PausedVizController, PlotCard, type PlotCardProps, type Provenance, type RailSection, Readout, type ReadoutItem, type ReadoutProps, ReferenceList, Refs, Stage, type StageSize, type SubTabDef, SubTabs, type SubTabsProps, SurfacePage, type SurfacePageProps, type TabDef, type TabGroupDef, TabGroups, type TabGroupsProps, Tabs, type TabsProps, type Theme, ThemeToggle, type Tone, type UsePausedVizOptions, VariantBar, type VariantBarProps, type VariantDef, Verdict, type VerdictProps, type VizFrame, type VizLoop, type VizLoopDeps, type VizLoopOptions, type WorkbenchGroup, WorkbenchLayout, type WorkbenchLayoutProps, type WorkbenchRoute, WorkbenchShell, type WorkbenchShellProps, type WorkbenchState, WorkbenchStateContext, applyTheme, caseKind, caseTooltip, casesInSource, createVizLoop, fitLabel, fontFamily, groupByCategory, isStale, makeStateKey, niceStep, niceTicks, readCaseParam, readTheme, replaceQueryParam, sourcesPresent, textWidth, usePausedViz, useStageSize, useThemeStore, useUrlView, useWorkbenchState, viewParamOf, widestLabel, withCaseParam };
826
+ interface ViewsRowProps {
827
+ /** The views side by side: `PlotCard`s, or components that render one. */
828
+ children: ReactNode;
829
+ /** The share of the row each view takes, in order (`[3, 2]`: the first takes three fifths); equal when absent. */
830
+ shares?: number[];
831
+ }
832
+ /**
833
+ * Views side by side in an instrument panel, each in a column the shell owns, so a filling `PlotCard` keeps its height
834
+ * chain and the row splits by `shares` (CAOS_Fragmenta's design row, three fifths for the response surface). Before
835
+ * 0.9.0 a product wrote this column itself (`.fr-viewcol`). Below 900 px the views stack.
836
+ */
837
+ declare function ViewsRow({ children, shares }: ViewsRowProps): react.JSX.Element;
838
+
839
+ export { BREAKPOINTS, BarChart, type BarChartProps, type BarDatum, BiText, Callout, type CaseDef, type CaseGroup, type CaseKind, CaseSelector, type CaseSelectorProps, type CaseSelectorText, CaseWorkbench, type CaseWorkbenchProps, ChipGroup, type ChipGroupProps, type ChipOption, type Citation, CitationsProvider, Cite, DocPage, DocSection, type DocSectionProps, Equation, Figure, type FittedLabel, FocusShell, type FocusShellProps, FormatOptions, Gauge, type GaugeProps, type GaugeZone, InlineMath, Knob, type KnobProps, type Lane, LaneBadge, Lang, LanguageToggle, MAX_PEER_TABS, MAX_WORKBENCH_GROUPS, PanelBoundary, type PausedVizController, PlotCard, type PlotCardProps, type Provenance, type RailSection, Readout, type ReadoutItem, type ReadoutProps, ReferenceList, Refs, Stage, type StageSize, type SubTabDef, SubTabs, type SubTabsProps, SurfacePage, type SurfacePageProps, type TabDef, type TabGroupDef, TabGroups, type TabGroupsProps, Tabs, type TabsProps, type Theme, ThemeToggle, type Tone, type UsePausedVizOptions, VariantBar, type VariantBarProps, type VariantDef, Verdict, type VerdictProps, ViewsRow, type ViewsRowProps, type VizFrame, type VizLoop, type VizLoopDeps, type VizLoopOptions, type WorkbenchGroup, WorkbenchLayout, type WorkbenchLayoutProps, type WorkbenchRoute, WorkbenchShell, type WorkbenchShellProps, type WorkbenchState, WorkbenchStateContext, applyTheme, caseKind, caseTooltip, casesInSource, createVizLoop, fitLabel, fontFamily, groupByCategory, isStale, makeStateKey, niceStep, niceTicks, readCaseParam, readTheme, replaceQueryParam, sourcesPresent, textWidth, usePausedViz, useStageSize, useThemeStore, useUrlView, useWorkbenchState, viewParamOf, widestLabel, withCaseParam };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { useThemeStore, useShellLang, useLangStore, SHELL_TOKENS, pick, useStageSize, NOT_AVAILABLE, formatTicks, formatNumber, NBSP } from './chunk-ZLJCB7DD.js';
2
- export { NBSP, NOT_AVAILABLE, SCIENTIFIC_BELOW, SHELL_TOKENS, Stage, applyTheme, formatNumber, formatTicks, pick, readTheme, resolveToken, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens } from './chunk-ZLJCB7DD.js';
1
+ import { useThemeStore, useShellLang, useLangStore, SHELL_TOKENS, pick, useStageSize, NOT_AVAILABLE, formatTicks, formatNumber, NBSP } from './chunk-IDUNUMCR.js';
2
+ export { NBSP, NOT_AVAILABLE, SCIENTIFIC_BELOW, SHELL_TOKENS, Stage, applyTheme, formatNumber, formatTicks, pick, readTheme, resolveToken, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens } from './chunk-IDUNUMCR.js';
3
3
  export { LANG_STORAGE_KEY, STANDARD_ROUTES, THEME_BOOT_SCRIPT, THEME_STORAGE_KEY } from './chunk-X6Y5CDUD.js';
4
- import { createContext, Component, useState, useRef, useId, useEffect, useCallback, useContext, useMemo } from 'react';
4
+ import { createContext, Component, useState, useRef, useId, useEffect, useCallback, useContext, useMemo, Children } from 'react';
5
5
  import { useInRouterContext, NavLink, useLocation } from 'react-router';
6
6
  import { Sun, Moon, Languages, Boxes, CodeXml, Globe, Briefcase, Info } from 'lucide-react';
7
7
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -1264,11 +1264,11 @@ function BarChart({ title, data, axis, orientation = "horizontal", values = true
1264
1264
  const t1 = ticks[ticks.length - 1];
1265
1265
  const tickLabels = formatTicks(ticks, lang, fmt);
1266
1266
  const sx = (v) => x0 + (v - t0) / (t1 - t0 || 1) * (x1 - x0);
1267
- const rowH = fill && box.height > 0 ? Math.max(need, Math.min(need * 2.2, (box.height - axisH - 4) / data.length)) : need;
1267
+ const rowH = fill && box.height > 0 ? Math.max(need, (box.height - axisH - 4) / data.length) : need;
1268
1268
  const top = 4;
1269
1269
  const plotBottom = top + data.length * rowH;
1270
1270
  drawnHeight = fill && box.height > 0 ? Math.max(box.height, plotBottom + axisH) : plotBottom + axisH;
1271
- const barH = Math.min(rowH * 0.62, 28);
1271
+ const barH = Math.min(rowH * 0.5, 40);
1272
1272
  drawing = /* @__PURE__ */ jsxs("svg", { width, height: drawnHeight, role: "img", "aria-label": name, "data-chart": "", "data-chart-kind": "bars", children: [
1273
1273
  /* @__PURE__ */ jsx("title", { children: name }),
1274
1274
  ticks.map((t, i) => /* @__PURE__ */ jsxs("g", { children: [
@@ -1740,5 +1740,12 @@ function ChipGroup({ id, label, options, value, onChange }) {
1740
1740
  )) })
1741
1741
  ] });
1742
1742
  }
1743
+ function ViewsRow({ children, shares }) {
1744
+ const items = Children.toArray(children).filter(Boolean);
1745
+ return /* @__PURE__ */ jsx("div", { className: "caos-views-row", "data-views": items.length, children: items.map((child, i) => {
1746
+ const share = shares?.[i];
1747
+ return /* @__PURE__ */ jsx("div", { className: "caos-views-col", "data-share": share ?? 1, style: share && share > 0 ? { flexGrow: share } : void 0, children: child }, i);
1748
+ }) });
1749
+ }
1743
1750
 
1744
- export { AppShell, ArchitectureModal, BREAKPOINTS, BarChart, Callout, CaseSelector, CaseWorkbench, ChipGroup, CitationsProvider, Cite, DocPage, DocSection, Equation, Figure, FocusShell, Gauge, InlineMath, Knob, LaneBadge, LanguageToggle, MAX_PEER_TABS, MAX_WORKBENCH_GROUPS, PanelBoundary, PlotCard, Readout, ReferenceList, Refs, SubTabs, SurfacePage, TabGroups, Tabs, ThemeToggle, VariantBar, Verdict, WorkbenchLayout, WorkbenchShell, WorkbenchStateContext, caseKind, caseTooltip, casesInSource, createVizLoop, fitLabel, fontFamily, groupByCategory, isStale, makeStateKey, niceStep, niceTicks, readCaseParam, replaceQueryParam, sourcesPresent, textWidth, usePausedViz, useUrlView, useWorkbenchState, validateArchitectureConfig, viewParamOf, widestLabel, withCaseParam };
1751
+ export { AppShell, ArchitectureModal, BREAKPOINTS, BarChart, Callout, CaseSelector, CaseWorkbench, ChipGroup, CitationsProvider, Cite, DocPage, DocSection, Equation, Figure, FocusShell, Gauge, InlineMath, Knob, LaneBadge, LanguageToggle, MAX_PEER_TABS, MAX_WORKBENCH_GROUPS, PanelBoundary, PlotCard, Readout, ReferenceList, Refs, SubTabs, SurfacePage, TabGroups, Tabs, ThemeToggle, VariantBar, Verdict, ViewsRow, WorkbenchLayout, WorkbenchShell, WorkbenchStateContext, caseKind, caseTooltip, casesInSource, createVizLoop, fitLabel, fontFamily, groupByCategory, isStale, makeStateKey, niceStep, niceTicks, readCaseParam, replaceQueryParam, sourcesPresent, textWidth, usePausedViz, useUrlView, useWorkbenchState, validateArchitectureConfig, viewParamOf, widestLabel, withCaseParam };
@@ -12,6 +12,9 @@ interface FormatOptions {
12
12
  /** `scientific` writes every non-zero value in scientific notation with `digits` significant digits; the default
13
13
  * chooses by magnitude. An axis uses it so all its ticks share one notation (`formatTicks`). */
14
14
  notation?: 'auto' | 'scientific';
15
+ /** `false` writes the integer part without a group separator, in both languages: a calendar year on an axis, in a
16
+ * readout or in a table reads 2021, never 2,021 or 2.021 (known shell defect 30). Default true. */
17
+ grouping?: boolean;
15
18
  }
16
19
  /** The no-break space (U+00A0) that joins a value to its unit or sign. */
17
20
  declare const NBSP = "\u00A0";
package/gate/inpage.mjs CHANGED
@@ -17,6 +17,11 @@ export function installLib() {
17
17
  });
18
18
  }
19
19
 
20
+ /** The ancestors of an element up to (not including) `stop`. */
21
+ function* ancestorsUntil(el, stop) {
22
+ for (let a = el.parentElement; a && a !== stop && a !== document.body; a = a.parentElement) yield a;
23
+ }
24
+
20
25
  const visible = (el) => {
21
26
  if (!el || el.nodeType !== 1) return false;
22
27
  const r = el.getBoundingClientRect();
@@ -233,7 +238,9 @@ export function installLib() {
233
238
  y,
234
239
  w: x2 - x,
235
240
  h: y2 - y,
236
- right: r.right,
241
+ // a table or drawing inside a box that SCROLLS sideways (a wide table on a phone, ADR-0071 rule 3) ends where
242
+ // its box ends: a reader scrolls it; one cut by a box that hides its overflow still extends past the page
243
+ right: Math.min(r.right, ...[...ancestorsUntil(el, inst)].filter((a) => ['auto', 'scroll'].includes(getComputedStyle(a).overflowX)).map((a) => a.getBoundingClientRect().right)),
237
244
  stageKey: path(stageEl),
238
245
  stage: clipRect(stageEl.getBoundingClientRect()),
239
246
  viewKey: path(viewEl),
@@ -523,6 +530,25 @@ export function installLib() {
523
530
 
524
531
  // ---- 0.8.0 measures ----------------------------------------------------------------------------------------
525
532
 
533
+ /** The part of the page the ancestors of an element let it show when they CUT their content (overflow hidden or
534
+ * clip). A scroll container does not cut (a reader scrolls it), and neither does the viewport. */
535
+ const cutArea = (el) => {
536
+ const area = { left: -Infinity, top: -Infinity, right: Infinity, bottom: Infinity };
537
+ for (let a = el.parentElement; a && a !== document.body && a !== document.documentElement; a = a.parentElement) {
538
+ const s = getComputedStyle(a);
539
+ const ar = a.getBoundingClientRect();
540
+ if (s.overflowX === 'hidden' || s.overflowX === 'clip') {
541
+ area.left = Math.max(area.left, ar.left);
542
+ area.right = Math.min(area.right, ar.right);
543
+ }
544
+ if (s.overflowY === 'hidden' || s.overflowY === 'clip') {
545
+ area.top = Math.max(area.top, ar.top);
546
+ area.bottom = Math.min(area.bottom, ar.bottom);
547
+ }
548
+ }
549
+ return area;
550
+ };
551
+
526
552
  /** The boxes a text element occupies on screen: one per line (tspan) when it has several. */
527
553
  const textBoxes = (t) => {
528
554
  const spans = [...t.querySelectorAll('tspan')].filter((s) => (s.textContent || '').trim());
@@ -550,7 +576,7 @@ export function installLib() {
550
576
  if (svg.classList.contains('lucide') || !visible(svg)) continue;
551
577
  const sr = svg.getBoundingClientRect();
552
578
  if (sr.width < 48 || sr.height < 24) continue;
553
- const area = visibleArea(svg);
579
+ const area = cutArea(svg);
554
580
  const box = { left: Math.max(sr.left, area.left), top: Math.max(sr.top, area.top), right: Math.min(sr.right, area.right), bottom: Math.min(sr.bottom, area.bottom) };
555
581
  const name = describe(svg.closest('[data-plot]') || svg);
556
582
  const texts = [...svg.querySelectorAll('text')].filter((t) => (t.textContent || '').trim() && visible(t));
@@ -559,7 +585,7 @@ export function installLib() {
559
585
  for (const b of textBoxes(t)) {
560
586
  // a label outside the visible part of the drawing: cut, or drawn where no one sees it
561
587
  if (b.top >= box.bottom || b.bottom <= box.top) {
562
- if (sr.bottom <= area.bottom + 1 && sr.top >= area.top - 1) out.push(`${name}: the label "${(t.textContent || '').trim().slice(0, 40)}" lies outside the drawing`);
588
+ out.push(`${name}: the label "${(t.textContent || '').trim().slice(0, 40)}" lies outside the drawing`);
563
589
  continue;
564
590
  }
565
591
  const cutX = Math.max(0, box.left - b.left) + Math.max(0, b.right - box.right);
@@ -595,7 +621,8 @@ export function installLib() {
595
621
  const out = [];
596
622
  const skip = 'code, pre, kbd, samp, .katex, [data-version], [data-build], [translate="no"], script, style, [aria-hidden="true"]';
597
623
  const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
598
- const re = /(?<![\w.,/:@#])(\d+)\.(\d+)(?![\w/@]|\.\d)/g;
624
+ // an exponent belongs to the number: 2.7e-08 on a Spanish page writes its decimal with a point too
625
+ const re = /(?<![\w.,/:@#])(\d+)\.(\d+)(?:[eE][-+]?\d+)?(?![\w/@]|\.\d)/g;
599
626
  for (let n = walker.nextNode(); n; n = walker.nextNode()) {
600
627
  const text = n.nodeValue || '';
601
628
  if (!/\d\.\d/.test(text)) continue;
package/gate/run.mjs CHANGED
@@ -318,7 +318,7 @@ export async function runGate(o) {
318
318
  if (primary) {
319
319
  // ADR-0071 rule 9 / G5: every header route reached by a real pointer click from the App route.
320
320
  for (const route of routes.filter((r) => r !== '/')) {
321
- await page.goto(urlFor('/'), { waitUntil: 'load', timeout: 30000 });
321
+ await page.goto(urlFor('/'), { waitUntil: 'load', timeout: 30000 }).catch(() => undefined);
322
322
  await settle(page);
323
323
  const link = page.locator('header nav a[href]');
324
324
  const hrefs = await link.evaluateAll((as) => as.map((a) => a.href));
@@ -326,8 +326,10 @@ export async function runGate(o) {
326
326
  let ok = false;
327
327
  if (idx >= 0) {
328
328
  // A reader scrolls a narrow nav row to the link first; so does the gate, then clicks with the pointer.
329
- await link.nth(idx).evaluate((el) => window.__caosGate.bringEl(el));
330
- const box = await link.nth(idx).boundingBox();
329
+ // A link that does not answer in time is a failure of this route, never an exception that ends the
330
+ // whole run (a locator waited its default 30 s under load and aborted the gate, 0.9.0 self-test).
331
+ await link.nth(idx).evaluate((el) => window.__caosGate.bringEl(el), undefined, { timeout: 10000 }).catch(() => undefined);
332
+ const box = await link.nth(idx).boundingBox({ timeout: 10000 }).catch(() => null);
331
333
  if (box) {
332
334
  await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
333
335
  await page.waitForURL((u) => routeOf(u.href) === route, { timeout: 3000 }).catch(() => undefined);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fasl-work/caos-app-shell",
3
- "version": "0.8.0",
3
+ "version": "0.9.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",
@@ -94,6 +94,7 @@
94
94
  "caos-verdict",
95
95
  "caos-verdict-text",
96
96
  "caos-verdict-title",
97
+ "caos-views-col",
97
98
  "caos-views-row",
98
99
  "caos-wb",
99
100
  "caos-wb-main",
@@ -210,7 +211,8 @@
210
211
  "tone-good",
211
212
  "tone-neutral",
212
213
  "tone-warn",
213
- "two-col"
214
+ "two-col",
215
+ "uplot"
214
216
  ],
215
217
  "modifiers": [
216
218
  "accent",
@@ -325,6 +327,7 @@
325
327
  "caos-verdict",
326
328
  "caos-verdict-text",
327
329
  "caos-verdict-title",
330
+ "caos-views-col",
328
331
  "caos-views-row",
329
332
  "caos-wb",
330
333
  "caos-wb-main",
@@ -454,6 +457,7 @@
454
457
  "tone-neutral",
455
458
  "tone-warn",
456
459
  "two-col",
460
+ "uplot",
457
461
  "warn",
458
462
  "wide"
459
463
  ]
package/styles.css CHANGED
@@ -579,6 +579,9 @@ svg.lucide { flex: none; }
579
579
  defect 23, #54: `.caos-plot.fill` outranked the row's zero basis and took the row). */
580
580
  .caos-views-row { display: flex; gap: 0.9rem; flex: 1 1 auto; min-height: 0; min-width: 0; }
581
581
  .caos-views-row > *, .caos-views-row > .caos-plot.fill { flex: 1 1 0; min-width: 0; }
582
+ /* ViewsRow's column: a view inside it keeps its height chain (a filling card fills the column); its share of the row
583
+ is its flex-grow. */
584
+ .caos-views-col { display: flex; flex-direction: column; min-width: 0; min-height: 0; gap: 0.6rem; }
582
585
 
583
586
  /* Staleness: a view showing data for an earlier selection says so. */
584
587
  .caos-stale .caos-plot-body > :not(.caos-stale-overlay), .caos-readout.caos-stale dl { opacity: 0.45; }