@fasl-work/caos-app-shell 0.9.3 → 0.10.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 CHANGED
@@ -16,7 +16,9 @@
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. */
19
+ /* A parity plot is square and centred in its box, across and, when it fills a box taller than it is wide, down (it
20
+ sat at the top with the spare height under it until 0.10.0); a chart whose points can be picked shows a pointer. */
20
21
  .caos-chart[data-parity] .uplot { margin-inline: auto; }
22
+ .caos-chart.fill[data-parity] > .caos-chart-plot { display: flex; align-items: center; justify-content: center; }
21
23
  .caos-chart[data-pick] .u-over { cursor: pointer; }
22
24
  .caos-chart .u-cursor-x, .caos-chart .u-cursor-y { border-color: var(--color-fg-faint) !important; }
package/dist/chart.d.ts CHANGED
@@ -14,6 +14,8 @@ interface ChartSeries {
14
14
  dash?: number[];
15
15
  /** `points` draws markers only (a scatter of cases over a curve); `null` values are simply absent. */
16
16
  mode?: 'line' | 'points';
17
+ /** The marker diameter of a `points` series, in CSS pixels (9 by default): a larger one rings the selected case. */
18
+ size?: number;
17
19
  }
18
20
  interface ChartAxis {
19
21
  label: BiText;
@@ -38,17 +40,31 @@ interface UPlotChartProps {
38
40
  x: number;
39
41
  label: BiText;
40
42
  }[];
43
+ /** Horizontal reference lines at y values, labelled at their right end (a null model's constant prediction, a
44
+ * target, a limit). */
45
+ yMarks?: {
46
+ y: number;
47
+ label: BiText;
48
+ }[];
41
49
  /** Height in pixels, or `fill` to take the container's height (inside `PlotCard fill`); the width is always the
42
50
  * container's. */
43
51
  height?: number | 'fill';
44
52
  /** Called with the index under the cursor (or null), for linked views; the index is the caller's, before any
45
53
  * sorting by x. */
46
54
  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). */
55
+ /** Called with the caller's index of the point under the cursor when the plot is clicked (select a case, a blast).
56
+ * On a chart whose series are all `points` (a scatter, a parity plot) the point under the cursor is the one nearest
57
+ * the pointer in the plane, not the one nearest in x. */
48
58
  onPick?: (index: number) => void;
49
59
  /** 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). */
60
+ * identity line. Unsorted x is allowed (the chart sorts it and maps every index back). The range takes in the
61
+ * reference lines (`yMarks`), so a null model's level is always in view. */
51
62
  parity?: boolean;
63
+ /** The readout under the plot for the point under the cursor (the caller's index), in place of the list of values:
64
+ * a scatter names its case ("Mg1, Murgul: measured 23.0 cm, predicted 25.1 cm"). */
65
+ readout?: (index: number) => string;
66
+ /** The readout at rest; by default "Hover the chart to read the values". */
67
+ hint?: BiText;
52
68
  }
53
69
  /** The colour and dash of a series: its own, or the rotation's; when the rotation comes round (a seventh series
54
70
  * repeats the first colour) the repeat is dashed, so two series never look the same (CAOS_Fragmenta drew this for
@@ -96,6 +112,24 @@ declare function drawMarks(u: uPlot, marks: {
96
112
  halo: string;
97
113
  family: string;
98
114
  }): void;
115
+ /**
116
+ * The horizontal reference lines of a chart and their labels: a dashed line across the plot, its label at the right
117
+ * end above the line (below it when the line runs along the top), inside the plot, haloed; a label that would overlap
118
+ * the one placed before it moves left of it. A line outside the y range is not drawn.
119
+ */
120
+ declare function drawYMarks(u: uPlot, marks: {
121
+ y: number;
122
+ label: string;
123
+ }[], style: {
124
+ color: string;
125
+ halo: string;
126
+ family: string;
127
+ }): void;
128
+ /**
129
+ * The index of the point nearest (left, top) in the plane, over every series, or null when no point has a value.
130
+ * `xs` and every column of `ys` are positions in the same pixels as (left, top); a null position is no point.
131
+ */
132
+ declare function nearestPoint(xs: readonly number[], ys: readonly (readonly (number | null)[])[], left: number, top: number): number | null;
99
133
  /**
100
134
  * The house chart (S12 of the 2026-10-04 requirements; uPlot, ADR on interactive charts). It is built only once its
101
135
  * box has a size; it resolves the theme's colours to concrete values and rebuilds when the theme or language changes
@@ -107,6 +141,6 @@ declare function drawMarks(u: uPlot, marks: {
107
141
  * (known shell defect 17). An axis with fixed decimals ticks only where its labels differ (`tickSteps`). The host
108
142
  * declares `data-series`, `data-axis-titles`, `data-ticks-cut`, `data-ticks-repeat` and `data-drawn` for the gate.
109
143
  */
110
- declare function UPlotChart({ x, y, series, marks, height, onCursor, onPick, parity }: UPlotChartProps): react.JSX.Element;
144
+ declare function UPlotChart({ x, y, series, marks, yMarks, height, onCursor, onPick, parity, readout, hint }: UPlotChartProps): react.JSX.Element;
111
145
 
112
- export { type ChartAxis, type ChartSeries, LOG_TABLE_FLOOR, UPlotChart, type UPlotChartProps, drawMarks, logDecades, parityRange, repeatedLabels, seriesStyle, smallestPositive, sortOrder, tickSteps };
146
+ export { type ChartAxis, type ChartSeries, LOG_TABLE_FLOOR, UPlotChart, type UPlotChartProps, drawMarks, drawYMarks, logDecades, nearestPoint, parityRange, repeatedLabels, seriesStyle, smallestPositive, sortOrder, tickSteps };
package/dist/chart.js CHANGED
@@ -1,4 +1,4 @@
1
- import { useShellLang, useThemeStore, useStageSize, pick, resolveToken, NBSP, formatNumber, formatTicks } from './chunk-IDUNUMCR.js';
1
+ import { useShellLang, useThemeStore, useStageSize, pick, resolveToken, NBSP, formatNumber, formatTicks } from './chunk-DX36XRD5.js';
2
2
  import './chunk-X6Y5CDUD.js';
3
3
  import { useRef, useCallback, useState, useMemo, useEffect } from 'react';
4
4
  import uPlot from 'uplot';
@@ -57,6 +57,7 @@ var DECADE_AXIS = {
57
57
  splits: (_u, _i, min, max) => logDecades(min, max),
58
58
  filter: (_u, splits) => splits
59
59
  };
60
+ var PARITY_TICK_SPACE = 40;
60
61
  function repeatedLabels(labels) {
61
62
  let n = 0;
62
63
  for (let i = 1; i < labels.length; i++) if (labels[i] && labels[i] === labels[i - 1]) n++;
@@ -102,7 +103,66 @@ function drawMarks(u, marks, style) {
102
103
  }
103
104
  ctx.restore();
104
105
  }
105
- function UPlotChart({ x, y, series, marks, height = 280, onCursor, onPick, parity }) {
106
+ function drawYMarks(u, marks, style) {
107
+ const ctx = u.ctx;
108
+ const pr = uPlot.pxRatio || 1;
109
+ const left = u.bbox.left;
110
+ const right = u.bbox.left + u.bbox.width;
111
+ const top = u.bbox.top;
112
+ const bottom = u.bbox.top + u.bbox.height;
113
+ const gap = 4 * pr;
114
+ const textH = 11 * pr;
115
+ const placed = [];
116
+ ctx.save();
117
+ ctx.font = `${Math.round(11 * pr)}px ${style.family}`;
118
+ ctx.textBaseline = "alphabetic";
119
+ ctx.lineJoin = "round";
120
+ for (const m of [...marks].sort((a, b) => b.y - a.y)) {
121
+ const py = u.valToPos(m.y, "y", true);
122
+ if (!Number.isFinite(py) || py < top - 0.5 || py > bottom + 0.5) continue;
123
+ ctx.strokeStyle = style.color;
124
+ ctx.lineWidth = Math.max(1, pr);
125
+ ctx.setLineDash([2 * pr, 4 * pr]);
126
+ ctx.beginPath();
127
+ ctx.moveTo(left, py);
128
+ ctx.lineTo(right, py);
129
+ ctx.stroke();
130
+ ctx.setLineDash([]);
131
+ const w = ctx.measureText(m.label).width;
132
+ const base = py - gap - textH < top ? py + gap + textH : py - gap;
133
+ let x1 = right - gap;
134
+ while (placed.some((p) => x1 - w < p.x1 + gap && x1 > p.x0 - gap && base - textH < p.y1 && base > p.y0)) {
135
+ x1 = Math.min(...placed.filter((p) => base - textH < p.y1 && base > p.y0).map((p) => p.x0)) - 2 * gap;
136
+ }
137
+ const x0 = Math.max(left + gap, x1 - w);
138
+ placed.push({ x0, x1: x0 + w, y0: base - textH, y1: base });
139
+ ctx.textAlign = "left";
140
+ ctx.lineWidth = 3 * pr;
141
+ ctx.strokeStyle = style.halo;
142
+ ctx.strokeText(m.label, x0, base);
143
+ ctx.fillStyle = style.color;
144
+ ctx.fillText(m.label, x0, base);
145
+ }
146
+ ctx.restore();
147
+ }
148
+ function nearestPoint(xs, ys, left, top) {
149
+ let best = null;
150
+ let bestD = Infinity;
151
+ for (let i = 0; i < xs.length; i++) {
152
+ if (!Number.isFinite(xs[i])) continue;
153
+ for (const col of ys) {
154
+ const y = col[i];
155
+ if (y === null || y === void 0 || !Number.isFinite(y)) continue;
156
+ const d = (xs[i] - left) ** 2 + (y - top) ** 2;
157
+ if (d < bestD) {
158
+ bestD = d;
159
+ best = i;
160
+ }
161
+ }
162
+ }
163
+ return best;
164
+ }
165
+ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPick, parity, readout, hint }) {
106
166
  const lang = useShellLang();
107
167
  const theme = useThemeStore((s) => s.theme);
108
168
  const fill = height === "fill";
@@ -124,12 +184,14 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor, onPick, parit
124
184
  onCursorRef.current = onCursor;
125
185
  const onPickRef = useRef(onPick);
126
186
  onPickRef.current = onPick;
187
+ const pickedRef = useRef(null);
127
188
  const order = useMemo(() => sortOrder(x.values), [x.values]);
128
189
  const orderRef = useRef(order);
129
190
  orderRef.current = order;
130
191
  const xs = useMemo(() => order ? order.map((i) => x.values[i]) : x.values, [order, x.values]);
131
192
  const ys = useMemo(() => series.map((s) => order ? order.map((i) => s.values[i] ?? null) : s.values), [order, series]);
132
- const range = useMemo(() => parity ? parityRange(xs, ...ys) : null, [parity, xs, ys]);
193
+ const levels = useMemo(() => (yMarks ?? []).map((m) => m.y), [yMarks]);
194
+ const range = useMemo(() => parity ? parityRange(xs, ...ys, levels) : null, [parity, xs, ys, levels]);
133
195
  const tiny = useMemo(
134
196
  () => ({
135
197
  x: Boolean(x.log) && smallestPositive([xs], range) < LOG_TABLE_FLOOR,
@@ -142,18 +204,20 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor, onPick, parit
142
204
  const xUnit = x.unit ? pick(x.unit, lang) : "";
143
205
  const yUnit = y.unit ? pick(y.unit, lang) : "";
144
206
  const structure = JSON.stringify({
145
- s: series.map((s, i) => [pick(s.label, lang), seriesStyle(s, i), s.width ?? 2, s.mode ?? "line"]),
207
+ s: series.map((s, i) => [pick(s.label, lang), seriesStyle(s, i), s.width ?? 2, s.mode ?? "line", s.size ?? 9]),
146
208
  x: [xLabel, xUnit, Boolean(x.time), Boolean(x.log), x.format ?? null, tiny.x],
147
209
  p: range,
148
210
  pick: Boolean(onPick),
149
211
  y: [yLabel, yUnit, Boolean(y.log), y.range ?? null, y.format ?? null, tiny.y],
150
212
  m: (marks ?? []).map((m) => [m.x, pick(m.label, lang)]),
213
+ ym: (yMarks ?? []).map((m) => [m.y, pick(m.label, lang)]),
151
214
  theme,
152
215
  lang,
153
216
  h,
154
217
  w: width
155
218
  });
156
219
  const data = useMemo(() => [xs, ...ys], [xs, ys]);
220
+ const scatter = series.length > 0 && series.every((s) => s.mode === "points");
157
221
  useEffect(() => {
158
222
  const el = plotRef.current;
159
223
  if (!el || width <= 0 || h <= 0) return;
@@ -167,14 +231,30 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor, onPick, parit
167
231
  const labels = formatTicks(vals, lang, fmt ?? { digits: 4 });
168
232
  repeats[axis] = repeatedLabels(labels);
169
233
  hostRef.current?.setAttribute("data-ticks-repeat", String(repeats.x + repeats.y));
234
+ hostRef.current?.setAttribute(`data-ticks-${axis}`, String(labels.filter((l) => l).length));
170
235
  return labels;
171
236
  };
237
+ const near = { left: NaN, top: NaN, idx: null };
238
+ const nearest = (u) => {
239
+ const { left, top } = u.cursor;
240
+ if (left === void 0 || top === void 0 || left < 0 || top < 0) return null;
241
+ if (left !== near.left || top !== near.top) {
242
+ const d = u.data;
243
+ const px = d[0].map((v) => v === null ? NaN : u.valToPos(v, "x"));
244
+ const py = d.slice(1).map((col) => col.map((v) => v === null ? null : u.valToPos(v, "y")));
245
+ near.left = left;
246
+ near.top = top;
247
+ near.idx = nearestPoint(px, py, left, top);
248
+ }
249
+ return near.idx;
250
+ };
172
251
  const xSteps = x.time || x.log ? void 0 : tickSteps(x.format);
173
252
  const ySteps = y.log ? void 0 : tickSteps(y.format);
174
253
  const opts = {
175
254
  width,
176
255
  height: h,
177
256
  legend: { show: false },
257
+ ...scatter ? { cursor: { dataIdx: (u, sIdx, closest) => sIdx === 0 ? closest : nearest(u) ?? closest } } : {},
178
258
  // a log axis labels the ticks uPlot keeps and passes null for the rest, which formatTicks leaves blank
179
259
  scales: {
180
260
  // a log x axis spans the data, not the decades around it (uPlot rounds out to the next decade, which left an
@@ -196,6 +276,7 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor, onPick, parit
196
276
  labelFont: font,
197
277
  values: x.time ? void 0 : tick(x.format, "x"),
198
278
  ...xSteps ? { incrs: xSteps } : {},
279
+ ...range ? { space: PARITY_TICK_SPACE } : {},
199
280
  ...tiny.x ? DECADE_AXIS : {}
200
281
  },
201
282
  {
@@ -207,6 +288,7 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor, onPick, parit
207
288
  labelFont: font,
208
289
  values: tick(y.format, "y"),
209
290
  ...ySteps ? { incrs: ySteps } : {},
291
+ ...range ? { space: PARITY_TICK_SPACE } : {},
210
292
  ...tiny.y ? DECADE_AXIS : {},
211
293
  // Sized to the longest tick label. uPlot draws in device pixels with a font scaled by its pixel ratio, so the
212
294
  // label is measured in that font and divided back (measuring the unscaled font and dividing by the ratio
@@ -225,13 +307,14 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor, onPick, parit
225
307
  ...series.map((s, i) => {
226
308
  const style = seriesStyle(s, i);
227
309
  const stroke = resolveToken(style.color, "#4a8");
228
- 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 } };
310
+ return s.mode === "points" ? { label: pick(s.label, lang), stroke, width: 1.5, paths: () => null, points: { show: true, space: 0, size: s.size ?? 9, fill: stroke } } : { label: pick(s.label, lang), stroke, width: s.width ?? 2, dash: style.dash, spanGaps: false, points: { show: false } };
229
311
  })
230
312
  ],
231
313
  hooks: {
232
314
  setCursor: [
233
315
  (u) => {
234
- const idx = u.cursor.idx ?? null;
316
+ const idx = scatter ? nearest(u) : u.cursor.idx ?? null;
317
+ pickedRef.current = idx;
235
318
  setCursor(idx);
236
319
  onCursorRef.current?.(idx === null ? null : orderRef.current ? orderRef.current[idx] : idx);
237
320
  }
@@ -239,7 +322,7 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor, onPick, parit
239
322
  ready: [
240
323
  (u) => {
241
324
  u.over.addEventListener("click", () => {
242
- const idx = u.cursor.idx;
325
+ const idx = pickedRef.current;
243
326
  if (idx === null || idx === void 0 || !onPickRef.current) return;
244
327
  onPickRef.current(orderRef.current ? orderRef.current[idx] : idx);
245
328
  });
@@ -260,12 +343,9 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor, onPick, parit
260
343
  ctx.stroke();
261
344
  ctx.restore();
262
345
  }
263
- if (!marks?.length) return;
264
- drawMarks(u, marks.map((m) => ({ x: m.x, label: pick(m.label, lang) })), {
265
- color: resolveToken("--color-warn", "#c80"),
266
- halo: surface,
267
- family
268
- });
346
+ const markStyle = { color: resolveToken("--color-warn", "#c80"), halo: surface, family };
347
+ if (yMarks?.length) drawYMarks(u, yMarks.map((m) => ({ y: m.y, label: pick(m.label, lang) })), markStyle);
348
+ if (marks?.length) drawMarks(u, marks.map((m) => ({ x: m.x, label: pick(m.label, lang) })), markStyle);
269
349
  }
270
350
  ]
271
351
  }
@@ -280,12 +360,10 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor, onPick, parit
280
360
  useEffect(() => {
281
361
  uRef.current?.setData(data);
282
362
  }, [data]);
283
- const read = cursor !== null ? [
363
+ const read = cursor !== null && readout ? readout(order ? order[cursor] : cursor) : cursor !== null ? [
284
364
  `${xLabel} ${formatNumber(xs[cursor], lang, x.format ?? { digits: 4 })}${xUnit ? `${NBSP}${xUnit}` : ""}`,
285
- ...series.map(
286
- (s, k) => `${pick(s.label, lang)} ${formatNumber(ys[k][cursor] ?? null, lang, y.format ?? { digits: 4 })}${yUnit ? `${NBSP}${yUnit}` : ""}`
287
- )
288
- ].join(" \xB7 ") : lang === "es" ? "Pase el cursor sobre el gr\xE1fico para leer los valores" : "Hover the chart to read the values";
365
+ ...series.map((s, k) => ({ s, v: ys[k][cursor] ?? null })).filter(({ v }) => !scatter || v !== null).map(({ s, v }) => `${pick(s.label, lang)} ${formatNumber(v, lang, y.format ?? { digits: 4 })}${yUnit ? `${NBSP}${yUnit}` : ""}`)
366
+ ].join(" \xB7 ") : hint ? pick(hint, lang) : lang === "es" ? "Pase el cursor sobre el gr\xE1fico para leer los valores" : "Hover the chart to read the values";
289
367
  return /* @__PURE__ */ jsxs(
290
368
  "div",
291
369
  {
@@ -299,6 +377,7 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor, onPick, parit
299
377
  "data-parity": parity ? "1" : void 0,
300
378
  "data-log-x": x.log ? "1" : void 0,
301
379
  "data-pick": onPick ? "1" : void 0,
380
+ "data-y-marks": yMarks?.length ? String(yMarks.length) : void 0,
302
381
  children: [
303
382
  /* @__PURE__ */ jsx("div", { ref: setPlot, className: "caos-chart-plot" }),
304
383
  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: [
@@ -311,4 +390,4 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor, onPick, parit
311
390
  );
312
391
  }
313
392
 
314
- export { LOG_TABLE_FLOOR, UPlotChart, drawMarks, logDecades, parityRange, repeatedLabels, seriesStyle, smallestPositive, sortOrder, tickSteps };
393
+ export { LOG_TABLE_FLOOR, UPlotChart, drawMarks, drawYMarks, logDecades, nearestPoint, parityRange, repeatedLabels, seriesStyle, smallestPositive, sortOrder, tickSteps };
@@ -142,12 +142,15 @@ function useThemeTokens() {
142
142
  var LOCALE = { en: "en-US", es: "es-CL" };
143
143
  var NBSP = "\xA0";
144
144
  var SCIENTIFIC_BELOW = 1e-4;
145
+ function belowScientific(magnitude) {
146
+ return magnitude !== 0 && Math.abs(magnitude) < SCIENTIFIC_BELOW * (1 - 1e-12);
147
+ }
145
148
  var NOT_AVAILABLE = { en: "not available", es: "no disponible" };
146
149
  function formatNumber(value, lang, opts = {}) {
147
150
  if (value === null || value === void 0 || !Number.isFinite(value)) return NOT_AVAILABLE[lang];
148
151
  const v = opts.percent ? value * 100 : value;
149
152
  const digits = opts.digits ?? 4;
150
- const scientific = v !== 0 && (opts.notation === "scientific" || Math.abs(v) < SCIENTIFIC_BELOW);
153
+ const scientific = v !== 0 && (opts.notation === "scientific" || belowScientific(v));
151
154
  const grouping = opts.grouping === false ? { useGrouping: false } : {};
152
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 });
153
156
  return opts.percent ? `${nf.format(v)}${NBSP}%` : nf.format(v);
@@ -156,7 +159,7 @@ function formatTicks(values, lang, opts = {}) {
156
159
  let o = opts;
157
160
  if (opts.decimals === void 0 && opts.notation === void 0) {
158
161
  const scale = opts.percent ? 100 : 1;
159
- const small = values.some((v) => v !== null && v !== void 0 && Number.isFinite(v) && v !== 0 && Math.abs(v * scale) < SCIENTIFIC_BELOW);
162
+ const small = values.some((v) => v !== null && v !== void 0 && Number.isFinite(v) && belowScientific(v * scale));
160
163
  if (small) o = { ...opts, notation: "scientific" };
161
164
  }
162
165
  return values.map((v) => v === null || v === void 0 ? "" : formatNumber(v, lang, o));
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
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';
1
+ import { useThemeStore, useShellLang, useLangStore, SHELL_TOKENS, pick, useStageSize, NOT_AVAILABLE, formatTicks, formatNumber, NBSP } from './chunk-DX36XRD5.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-DX36XRD5.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, Children } from 'react';
5
5
  import { useInRouterContext, NavLink, useLocation } from 'react-router';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fasl-work/caos-app-shell",
3
- "version": "0.9.3",
3
+ "version": "0.10.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/styles.css CHANGED
@@ -446,6 +446,9 @@ code, kbd { font-family: var(--font-mono); }
446
446
  .chip { display: inline-flex; align-items: center; gap: 0.35rem; background: var(--color-surface); border: 1px solid var(--color-border); color: var(--color-fg-subtle); border-radius: var(--radius-pill); padding: 0.3rem 0.72rem; font-size: 0.82rem; font-weight: 500; white-space: nowrap; transition: color 0.15s, background-color 0.15s, border-color 0.15s; }
447
447
  .chip:hover:not(:disabled) { color: var(--color-fg); border-color: var(--color-fg-faint); }
448
448
  .chip.on { background: var(--color-accent); color: var(--color-accent-fg); border-color: transparent; font-weight: 600; }
449
+ /* the active chip keeps its colours under the pointer: the hover rule above outranks `.chip.on` and, until 0.9.4, wrote
450
+ the text colour on the accent (3.04:1 in light, 1.64:1 in dark; known shell defect 33) */
451
+ .chip.on:hover:not(:disabled) { color: var(--color-accent-fg); border-color: transparent; }
449
452
  .chip:disabled { opacity: 0.45; cursor: default; }
450
453
 
451
454
  /* Registered controls (Knob, ChipGroup). */
@@ -475,6 +478,8 @@ svg.lucide { flex: none; }
475
478
  .cs-chip { display: inline-flex; align-items: baseline; gap: 0.4rem; padding: 0.32rem 0.6rem; border: 1px solid var(--color-border); border-radius: var(--radius-pill); background: var(--color-surface); color: var(--color-fg-subtle); font-size: var(--text-sm); font-weight: 500; transition: color 0.15s, background-color 0.15s, border-color 0.15s; }
476
479
  .cs-chip:hover:not(:disabled) { color: var(--color-fg); background: var(--color-surface-2); }
477
480
  .cs-chip.on { color: var(--color-accent); background: var(--color-accent-soft); border-color: color-mix(in oklab, var(--color-accent) 40%, transparent); font-weight: 600; }
481
+ /* as for `.chip`: the chosen case's chip keeps its accent under the pointer (known shell defect 33) */
482
+ .cs-chip.on:hover:not(:disabled) { color: var(--color-accent); background: var(--color-accent-soft); }
478
483
  .cs-chip:disabled { opacity: 0.45; cursor: default; }
479
484
  .cs-chip-id { font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.76rem; font-weight: 600; opacity: 0.85; }
480
485
  .cs-kind { font-size: 0.62rem; font-weight: 700; line-height: 1; padding: 0.12rem 0.28rem; border-radius: 4px; border: 1px solid var(--color-border); color: var(--color-fg-faint); background: var(--color-surface-2); align-self: center; }