@fasl-work/caos-app-shell 0.16.0 → 0.17.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/dist/chart.d.ts +41 -2
- package/dist/chart.js +206 -20
- package/dist/index.d.ts +4 -3
- package/dist/index.js +55 -14
- package/gate/inpage.mjs +66 -0
- package/gate/run.mjs +12 -1
- package/package.json +3 -4
- package/reserved-classes.json +8 -2
- package/styles.css +25 -4
package/dist/chart.d.ts
CHANGED
|
@@ -16,6 +16,17 @@ interface ChartSeries {
|
|
|
16
16
|
mode?: 'line' | 'points';
|
|
17
17
|
/** The marker diameter of a `points` series, in CSS pixels (9 by default): a larger one rings the selected case. */
|
|
18
18
|
size?: number;
|
|
19
|
+
/** A `points` series on x values of its own (measured cases over a modelled curve that has other sizes): one x per
|
|
20
|
+
* value. It is drawn over the plot on its own x, outside the curve's data, so it takes no part in the cursor, the
|
|
21
|
+
* pick or the readout, and the axes take its extent in. */
|
|
22
|
+
x?: number[];
|
|
23
|
+
}
|
|
24
|
+
/** What a reference line may be dragged over: the range it stays inside, and the step it snaps to (a hundredth of the
|
|
25
|
+
* range when not given; on a log axis the step is a factor). */
|
|
26
|
+
interface YMarkDrag {
|
|
27
|
+
min: number;
|
|
28
|
+
max: number;
|
|
29
|
+
step?: number;
|
|
19
30
|
}
|
|
20
31
|
interface ChartAxis {
|
|
21
32
|
label: BiText;
|
|
@@ -52,7 +63,12 @@ interface UPlotChartProps {
|
|
|
52
63
|
y: number;
|
|
53
64
|
label: BiText;
|
|
54
65
|
tone?: YMarkTone;
|
|
66
|
+
drag?: YMarkDrag;
|
|
55
67
|
}[];
|
|
68
|
+
/** Called with the index in `yMarks` and the new y of a draggable reference line (`drag`) every time the reader moves
|
|
69
|
+
* it, by the pointer or the keyboard (a slider on the line: arrows, Page keys, Home and End); the caller sets the new
|
|
70
|
+
* y in `yMarks`. */
|
|
71
|
+
onYMarkChange?: (index: number, y: number) => void;
|
|
56
72
|
/** Height in pixels, or `fill` to take the container's height (inside `PlotCard fill`); the width is always the
|
|
57
73
|
* container's. */
|
|
58
74
|
height?: number | 'fill';
|
|
@@ -110,6 +126,29 @@ declare const LOG_TABLE_FLOOR = 1e-22;
|
|
|
110
126
|
declare function logDecades(min: number, max: number, most?: number): number[];
|
|
111
127
|
/** The smallest positive value among columns of values and the lower end of a fixed range; Infinity if none. */
|
|
112
128
|
declare function smallestPositive(columns: readonly (readonly (number | null)[])[], range?: readonly [number, number] | null): number;
|
|
129
|
+
/**
|
|
130
|
+
* The ticks of a log x axis that get a label: the powers of ten only (null for every other tick, which formatTicks leaves
|
|
131
|
+
* blank). A range with no power of ten in it keeps its ticks, so the axis is never left without a label.
|
|
132
|
+
*/
|
|
133
|
+
declare function decadeOnly(ticks: readonly (number | null)[]): (number | null)[];
|
|
134
|
+
/** The side of the square box of a parity plot: the smaller of the box's width and the height asked for (the box's
|
|
135
|
+
* height when the chart fills it). */
|
|
136
|
+
declare function parityBox(boxWidth: number, boxHeight: number, height: number, fill: boolean): number;
|
|
137
|
+
/** A dragged value kept inside the range of its line and snapped to its step; a value that is not a number falls to the
|
|
138
|
+
* lower end. */
|
|
139
|
+
declare function clampMark(y: number, drag: YMarkDrag): number;
|
|
140
|
+
/**
|
|
141
|
+
* The value a key sets on a draggable line, or null for a key it does not handle: the arrows one step, Page Up and Down
|
|
142
|
+
* ten, Home and End the ends of the range. A step is a hundredth of the range, or `drag.step`; on a log axis it is a factor
|
|
143
|
+
* (a hundredth of the range in decades) and `drag.step` is not used.
|
|
144
|
+
*/
|
|
145
|
+
declare function nudgeMark(y: number, key: string, drag: YMarkDrag, log?: boolean): number | null;
|
|
146
|
+
/** The x and y extent of the series that bring an x of their own, finite points only (on a log x axis, `positiveX`,
|
|
147
|
+
* only the points with x above 0); null when there are none. */
|
|
148
|
+
declare function overlayExtent(series: readonly ChartSeries[], positiveX?: boolean): {
|
|
149
|
+
x: [number, number];
|
|
150
|
+
y: [number, number];
|
|
151
|
+
} | null;
|
|
113
152
|
/** How many tick labels repeat the label before them (empty labels apart); the gate fails a chart with any (G6). */
|
|
114
153
|
declare function repeatedLabels(labels: readonly (string | null | undefined)[]): number;
|
|
115
154
|
/**
|
|
@@ -158,6 +197,6 @@ declare function nearestPoint(xs: readonly number[], ys: readonly (readonly (num
|
|
|
158
197
|
* (known shell defect 17). An axis with fixed decimals ticks only where its labels differ (`tickSteps`). The host
|
|
159
198
|
* declares `data-series`, `data-axis-titles`, `data-ticks-cut`, `data-ticks-repeat` and `data-drawn` for the gate.
|
|
160
199
|
*/
|
|
161
|
-
declare function UPlotChart({ x, y, series, marks, yMarks, height, onCursor, onPick, parity, readout, hint }: UPlotChartProps): react.JSX.Element;
|
|
200
|
+
declare function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height, onCursor, onPick, parity, readout, hint }: UPlotChartProps): react.JSX.Element;
|
|
162
201
|
|
|
163
|
-
export { type ChartAxis, type ChartSeries, LOG_TABLE_FLOOR, UPlotChart, type UPlotChartProps, YMARK_TONE_TOKENS, type YMarkTone, drawMarks, drawYMarks, logDecades, nearestPoint, parityRange, rangeWithLevels, repeatedLabels, seriesStyle, smallestPositive, sortOrder, tickSteps };
|
|
202
|
+
export { type ChartAxis, type ChartSeries, LOG_TABLE_FLOOR, UPlotChart, type UPlotChartProps, YMARK_TONE_TOKENS, type YMarkDrag, type YMarkTone, clampMark, decadeOnly, drawMarks, drawYMarks, logDecades, nearestPoint, nudgeMark, overlayExtent, parityBox, parityRange, rangeWithLevels, repeatedLabels, seriesStyle, smallestPositive, sortOrder, tickSteps };
|
package/dist/chart.js
CHANGED
|
@@ -83,11 +83,73 @@ function smallestPositive(columns, range) {
|
|
|
83
83
|
for (const c of columns) for (const v of c) if (v !== null && Number.isFinite(v) && v > 0 && v < lo) lo = v;
|
|
84
84
|
return lo;
|
|
85
85
|
}
|
|
86
|
+
function isDecade(t) {
|
|
87
|
+
if (t === null || t === void 0 || !Number.isFinite(t) || t <= 0) return false;
|
|
88
|
+
const d = Math.log10(t);
|
|
89
|
+
return Math.abs(d - Math.round(d)) < 1e-6;
|
|
90
|
+
}
|
|
91
|
+
function decadeOnly(ticks) {
|
|
92
|
+
if (!ticks.some(isDecade)) return [...ticks];
|
|
93
|
+
return ticks.map((t) => isDecade(t) ? t : null);
|
|
94
|
+
}
|
|
95
|
+
function parityBox(boxWidth, boxHeight, height, fill) {
|
|
96
|
+
return Math.min(boxWidth, fill ? boxHeight : height);
|
|
97
|
+
}
|
|
98
|
+
function clampMark(y, drag) {
|
|
99
|
+
if (!Number.isFinite(y)) return drag.min;
|
|
100
|
+
const v = drag.step && drag.step > 0 ? drag.min + Math.round((y - drag.min) / drag.step) * drag.step : y;
|
|
101
|
+
const clamped = Math.min(drag.max, Math.max(drag.min, v));
|
|
102
|
+
return Number(clamped.toPrecision(12));
|
|
103
|
+
}
|
|
104
|
+
function nudgeMark(y, key, drag, log = false) {
|
|
105
|
+
const dir = key === "ArrowUp" || key === "ArrowRight" || key === "PageUp" ? 1 : key === "ArrowDown" || key === "ArrowLeft" || key === "PageDown" ? -1 : 0;
|
|
106
|
+
if (key === "Home") return drag.min;
|
|
107
|
+
if (key === "End") return drag.max;
|
|
108
|
+
if (dir === 0) return null;
|
|
109
|
+
const n = key === "PageUp" || key === "PageDown" ? 10 : 1;
|
|
110
|
+
if (log && drag.min > 0 && drag.max > drag.min) {
|
|
111
|
+
const decades = (Math.log10(drag.max) - Math.log10(drag.min)) / 100;
|
|
112
|
+
return Math.min(drag.max, Math.max(drag.min, y * 10 ** (dir * n * decades)));
|
|
113
|
+
}
|
|
114
|
+
const step = drag.step && drag.step > 0 ? drag.step : (drag.max - drag.min) / 100;
|
|
115
|
+
return clampMark(y + dir * n * step, { ...drag, step: void 0 });
|
|
116
|
+
}
|
|
117
|
+
function isOverlay(s) {
|
|
118
|
+
return Boolean(s.x) && s.mode === "points";
|
|
119
|
+
}
|
|
120
|
+
function dragWithin(drag, range) {
|
|
121
|
+
if (!range) return drag;
|
|
122
|
+
const lo = Math.min(range[0], range[1]);
|
|
123
|
+
const hi = Math.max(range[0], range[1]);
|
|
124
|
+
const min = Math.min(hi, Math.max(lo, drag.min));
|
|
125
|
+
const max = Math.max(lo, Math.min(hi, drag.max));
|
|
126
|
+
return { ...drag, min, max: Math.max(min, max) };
|
|
127
|
+
}
|
|
128
|
+
function overlayExtent(series, positiveX = false) {
|
|
129
|
+
let x0 = Infinity;
|
|
130
|
+
let x1 = -Infinity;
|
|
131
|
+
let y0 = Infinity;
|
|
132
|
+
let y1 = -Infinity;
|
|
133
|
+
for (const s of series) {
|
|
134
|
+
if (!isOverlay(s) || !s.x) continue;
|
|
135
|
+
for (let i = 0; i < Math.min(s.x.length, s.values.length); i++) {
|
|
136
|
+
const px = s.x[i];
|
|
137
|
+
const py = s.values[i];
|
|
138
|
+
if (py === null || py === void 0 || !Number.isFinite(px) || !Number.isFinite(py) || positiveX && !(px > 0)) continue;
|
|
139
|
+
x0 = Math.min(x0, px);
|
|
140
|
+
x1 = Math.max(x1, px);
|
|
141
|
+
y0 = Math.min(y0, py);
|
|
142
|
+
y1 = Math.max(y1, py);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return Number.isFinite(x0) ? { x: [x0, x1], y: [y0, y1] } : null;
|
|
146
|
+
}
|
|
86
147
|
var DECADE_AXIS = {
|
|
87
148
|
splits: (_u, _i, min, max) => logDecades(min, max),
|
|
88
149
|
filter: (_u, splits) => splits
|
|
89
150
|
};
|
|
90
151
|
var PARITY_TICK_SPACE = 40;
|
|
152
|
+
var HANDLE_HEIGHT = 14;
|
|
91
153
|
function repeatedLabels(labels) {
|
|
92
154
|
let n = 0;
|
|
93
155
|
for (let i = 1; i < labels.length; i++) if (labels[i] && labels[i] === labels[i - 1]) n++;
|
|
@@ -193,7 +255,7 @@ function nearestPoint(xs, ys, left, top) {
|
|
|
193
255
|
}
|
|
194
256
|
return best;
|
|
195
257
|
}
|
|
196
|
-
function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPick, parity, readout, hint }) {
|
|
258
|
+
function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280, onCursor, onPick, parity, readout, hint }) {
|
|
197
259
|
const lang = useShellLang();
|
|
198
260
|
const theme = useThemeStore((s) => s.theme);
|
|
199
261
|
const fill = height === "fill";
|
|
@@ -206,7 +268,7 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
|
|
|
206
268
|
},
|
|
207
269
|
[measureRef]
|
|
208
270
|
);
|
|
209
|
-
const width = parity ?
|
|
271
|
+
const width = parity ? parityBox(box.width, box.height, typeof height === "number" ? height : 0, fill) : box.width;
|
|
210
272
|
const h = parity ? width : fill ? box.height : height;
|
|
211
273
|
const uRef = useRef(null);
|
|
212
274
|
const hostRef = useRef(null);
|
|
@@ -220,36 +282,57 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
|
|
|
220
282
|
const orderRef = useRef(order);
|
|
221
283
|
orderRef.current = order;
|
|
222
284
|
const xs = useMemo(() => order ? order.map((i) => x.values[i]) : x.values, [order, x.values]);
|
|
223
|
-
const
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
const
|
|
285
|
+
const entries = useMemo(() => series.map((s, i) => ({ s, i })), [series]);
|
|
286
|
+
const baseEntries = useMemo(() => entries.filter(({ s }) => !isOverlay(s)), [entries]);
|
|
287
|
+
const overlays = useMemo(() => entries.filter(({ s }) => isOverlay(s)), [entries]);
|
|
288
|
+
const ext = useMemo(() => overlayExtent(series, Boolean(x.log)), [series, x.log]);
|
|
289
|
+
const ys = useMemo(() => baseEntries.map(({ s }) => order ? order.map((i) => s.values[i] ?? null) : s.values), [order, baseEntries]);
|
|
290
|
+
const levels = useMemo(() => (yMarks ?? []).flatMap((m) => m.drag ? [m.drag.min, m.drag.max] : [m.y]), [yMarks]);
|
|
291
|
+
const range = useMemo(() => parity ? parityRange(xs, ...ys, levels, ext ? [...ext.x, ...ext.y] : []) : null, [parity, xs, ys, levels, ext]);
|
|
292
|
+
const yLevels = !parity && !y.range && (levels.length > 0 || ext) ? [...levels, ...ext ? ext.y : []] : null;
|
|
293
|
+
const xExt = useMemo(() => {
|
|
294
|
+
if (!ext || range || xs.length === 0) return null;
|
|
295
|
+
const lo = Math.min(xs[0], ext.x[0]);
|
|
296
|
+
const hi = Math.max(xs[xs.length - 1], ext.x[1]);
|
|
297
|
+
if (x.log && !(lo > 0)) return null;
|
|
298
|
+
return hi > lo ? [lo, hi] : x.log ? [lo / 10, hi * 10] : [lo - 1, hi + 1];
|
|
299
|
+
}, [ext, range, xs, x.log]);
|
|
227
300
|
const tiny = useMemo(
|
|
228
301
|
() => ({
|
|
229
|
-
x: Boolean(x.log) && smallestPositive([xs], range) < LOG_TABLE_FLOOR,
|
|
302
|
+
x: Boolean(x.log) && smallestPositive(ext ? [xs, ext.x] : [xs], range) < LOG_TABLE_FLOOR,
|
|
230
303
|
y: Boolean(y.log) && smallestPositive(yLevels ? [...ys, yLevels] : ys, range ?? y.range) < LOG_TABLE_FLOOR
|
|
231
304
|
}),
|
|
232
|
-
[x.log, y.log, xs, ys, range, y.range, yLevels]
|
|
305
|
+
[x.log, y.log, xs, ys, range, y.range, yLevels, ext]
|
|
233
306
|
);
|
|
234
307
|
const xLabel = pick(x.label, lang);
|
|
235
308
|
const yLabel = pick(y.label, lang);
|
|
309
|
+
const yMarksRef = useRef(yMarks);
|
|
310
|
+
yMarksRef.current = yMarks;
|
|
311
|
+
const liveRef = useRef([]);
|
|
312
|
+
liveRef.current = (yMarks ?? []).map((m) => m.y);
|
|
313
|
+
const overlaysRef = useRef(overlays);
|
|
314
|
+
overlaysRef.current = overlays;
|
|
315
|
+
const onYMarkChangeRef = useRef(onYMarkChange);
|
|
316
|
+
onYMarkChangeRef.current = onYMarkChange;
|
|
317
|
+
const dragCount = (yMarks ?? []).filter((m) => m.drag).length;
|
|
236
318
|
const xUnit = x.unit ? pick(x.unit, lang) : "";
|
|
237
319
|
const yUnit = y.unit ? pick(y.unit, lang) : "";
|
|
238
320
|
const structure = JSON.stringify({
|
|
239
|
-
s: series.map((s, i) => [pick(s.label, lang), seriesStyle(s, i), s.width ?? 2, s.mode ?? "line", s.size ?? 9]),
|
|
240
|
-
x: [xLabel, xUnit, Boolean(x.time), Boolean(x.log), x.format ?? null, tiny.x],
|
|
321
|
+
s: series.map((s, i) => [pick(s.label, lang), seriesStyle(s, i), s.width ?? 2, s.mode ?? "line", s.size ?? 9, isOverlay(s)]),
|
|
322
|
+
x: [xLabel, xUnit, Boolean(x.time), Boolean(x.log), x.format ?? null, tiny.x, xExt],
|
|
323
|
+
oe: ext,
|
|
241
324
|
p: range,
|
|
242
325
|
pick: Boolean(onPick),
|
|
243
326
|
y: [yLabel, yUnit, Boolean(y.log), y.range ?? null, y.format ?? null, tiny.y],
|
|
244
327
|
m: (marks ?? []).map((m) => [m.x, pick(m.label, lang)]),
|
|
245
|
-
ym: (yMarks ?? []).map((m) => [m.y, pick(m.label, lang), m.tone ?? "warn"]),
|
|
328
|
+
ym: (yMarks ?? []).map((m) => m.drag ? ["drag", m.drag] : [m.y, pick(m.label, lang), m.tone ?? "warn"]),
|
|
246
329
|
theme,
|
|
247
330
|
lang,
|
|
248
331
|
h,
|
|
249
332
|
w: width
|
|
250
333
|
});
|
|
251
334
|
const data = useMemo(() => [xs, ...ys], [xs, ys]);
|
|
252
|
-
const scatter =
|
|
335
|
+
const scatter = baseEntries.length > 0 && baseEntries.every(({ s }) => s.mode === "points");
|
|
253
336
|
useEffect(() => {
|
|
254
337
|
const el = plotRef.current;
|
|
255
338
|
if (!el || width <= 0 || h <= 0) return;
|
|
@@ -259,8 +342,9 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
|
|
|
259
342
|
const font = `11px ${family}`;
|
|
260
343
|
const surface = resolveToken("--color-surface", "#fff");
|
|
261
344
|
const repeats = { x: 0, y: 0 };
|
|
262
|
-
const tick = (fmt, axis) => (_u, vals) => {
|
|
263
|
-
const labels = formatTicks(vals, lang, fmt ?? { digits: 4 });
|
|
345
|
+
const tick = (fmt, axis, pre) => (_u, vals) => {
|
|
346
|
+
const labels = formatTicks(pre ? pre(vals) : vals, lang, fmt ?? { digits: 4 });
|
|
347
|
+
if (axis === "x" && pre) hostRef.current?.setAttribute("data-log-labels", labels.filter((l) => l).join("|"));
|
|
264
348
|
repeats[axis] = repeatedLabels(labels);
|
|
265
349
|
hostRef.current?.setAttribute("data-ticks-repeat", String(repeats.x + repeats.y));
|
|
266
350
|
hostRef.current?.setAttribute(`data-ticks-${axis}`, String(labels.filter((l) => l).length));
|
|
@@ -294,7 +378,7 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
|
|
|
294
378
|
x: {
|
|
295
379
|
time: Boolean(x.time),
|
|
296
380
|
distr: x.log ? 3 : 1,
|
|
297
|
-
...range ? { range } : x.log ? { range: (_u, min, max) => [min, max] } : {}
|
|
381
|
+
...range ? { range } : xExt ? { range: xExt } : x.log ? { range: (_u, min, max) => [min, max] } : {}
|
|
298
382
|
},
|
|
299
383
|
y: {
|
|
300
384
|
distr: y.log ? 3 : 1,
|
|
@@ -316,7 +400,8 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
|
|
|
316
400
|
font,
|
|
317
401
|
label: xUnit ? `${xLabel} (${xUnit})` : xLabel,
|
|
318
402
|
labelFont: font,
|
|
319
|
-
|
|
403
|
+
// a log x axis labels the decades only; the ticks between stay drawn, unlabelled
|
|
404
|
+
values: x.time ? void 0 : tick(x.format, "x", x.log && !tiny.x ? decadeOnly : void 0),
|
|
320
405
|
...xSteps ? { incrs: xSteps } : {},
|
|
321
406
|
...range ? { space: PARITY_TICK_SPACE } : {},
|
|
322
407
|
...tiny.x ? DECADE_AXIS : {}
|
|
@@ -346,7 +431,7 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
|
|
|
346
431
|
],
|
|
347
432
|
series: [
|
|
348
433
|
{},
|
|
349
|
-
...
|
|
434
|
+
...baseEntries.map(({ s, i }) => {
|
|
350
435
|
const style = seriesStyle(s, i);
|
|
351
436
|
const stroke = resolveToken(style.color, "#4a8");
|
|
352
437
|
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 } };
|
|
@@ -368,6 +453,55 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
|
|
|
368
453
|
if (idx === null || idx === void 0 || !onPickRef.current) return;
|
|
369
454
|
onPickRef.current(orderRef.current ? orderRef.current[idx] : idx);
|
|
370
455
|
});
|
|
456
|
+
const accent = resolveToken("--color-accent", "#06c");
|
|
457
|
+
(yMarks ?? []).forEach((m, i) => {
|
|
458
|
+
if (!m.drag) return;
|
|
459
|
+
const drag = dragWithin(m.drag, range ? void 0 : y.range);
|
|
460
|
+
const el2 = document.createElement("div");
|
|
461
|
+
el2.setAttribute("role", "slider");
|
|
462
|
+
el2.setAttribute("tabindex", "0");
|
|
463
|
+
el2.setAttribute("aria-orientation", "vertical");
|
|
464
|
+
el2.setAttribute("aria-valuemin", String(drag.min));
|
|
465
|
+
el2.setAttribute("aria-valuemax", String(drag.max));
|
|
466
|
+
el2.setAttribute("data-y-handle", String(i));
|
|
467
|
+
Object.assign(el2.style, { position: "absolute", left: "0", width: "100%", height: `${HANDLE_HEIGHT}px`, cursor: "ns-resize", touchAction: "none", outlineOffset: "-2px", zIndex: "3" });
|
|
468
|
+
el2.addEventListener("focus", () => el2.style.outline = `2px solid ${accent}`);
|
|
469
|
+
el2.addEventListener("blur", () => el2.style.outline = "none");
|
|
470
|
+
el2.addEventListener("click", (e) => e.stopPropagation());
|
|
471
|
+
const set = (raw) => {
|
|
472
|
+
const v = clampMark(raw, y.log ? { ...drag, step: void 0 } : drag);
|
|
473
|
+
if (v === liveRef.current[i]) return;
|
|
474
|
+
liveRef.current[i] = v;
|
|
475
|
+
onYMarkChangeRef.current?.(i, v);
|
|
476
|
+
u.redraw(false);
|
|
477
|
+
};
|
|
478
|
+
let dragging = false;
|
|
479
|
+
el2.addEventListener("pointerdown", (e) => {
|
|
480
|
+
if (e.button !== 0) return;
|
|
481
|
+
dragging = true;
|
|
482
|
+
el2.setPointerCapture(e.pointerId);
|
|
483
|
+
el2.focus();
|
|
484
|
+
e.preventDefault();
|
|
485
|
+
});
|
|
486
|
+
el2.addEventListener("pointermove", (e) => {
|
|
487
|
+
if (!dragging) return;
|
|
488
|
+
set(u.posToVal(e.clientY - u.over.getBoundingClientRect().top, "y"));
|
|
489
|
+
});
|
|
490
|
+
const end = (e) => {
|
|
491
|
+
dragging = false;
|
|
492
|
+
if (el2.hasPointerCapture(e.pointerId)) el2.releasePointerCapture(e.pointerId);
|
|
493
|
+
};
|
|
494
|
+
el2.addEventListener("pointerup", end);
|
|
495
|
+
el2.addEventListener("pointercancel", end);
|
|
496
|
+
el2.addEventListener("keydown", (e) => {
|
|
497
|
+
const v = nudgeMark(liveRef.current[i], e.key, drag, Boolean(y.log));
|
|
498
|
+
if (v === null) return;
|
|
499
|
+
e.preventDefault();
|
|
500
|
+
set(v);
|
|
501
|
+
});
|
|
502
|
+
u.over.appendChild(el2);
|
|
503
|
+
});
|
|
504
|
+
if (dragCount) queueMicrotask(() => uRef.current === u && u.redraw(false));
|
|
371
505
|
}
|
|
372
506
|
],
|
|
373
507
|
draw: [
|
|
@@ -385,8 +519,53 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
|
|
|
385
519
|
ctx.stroke();
|
|
386
520
|
ctx.restore();
|
|
387
521
|
}
|
|
522
|
+
const dots = overlaysRef.current;
|
|
523
|
+
if (dots.length) {
|
|
524
|
+
const ctx = u.ctx;
|
|
525
|
+
const pr = uPlot.pxRatio || 1;
|
|
526
|
+
let drawn = 0;
|
|
527
|
+
ctx.save();
|
|
528
|
+
ctx.beginPath();
|
|
529
|
+
ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
|
|
530
|
+
ctx.clip();
|
|
531
|
+
for (const { s, i } of dots) {
|
|
532
|
+
ctx.fillStyle = resolveToken(seriesStyle(s, i).color, "#4a8");
|
|
533
|
+
ctx.strokeStyle = surface;
|
|
534
|
+
ctx.lineWidth = Math.max(1, pr);
|
|
535
|
+
const r = (s.size ?? 9) / 2 * pr;
|
|
536
|
+
const ox = s.x ?? [];
|
|
537
|
+
for (let k = 0; k < Math.min(ox.length, s.values.length); k++) {
|
|
538
|
+
const v = s.values[k];
|
|
539
|
+
if (v === null || v === void 0 || !Number.isFinite(v) || !Number.isFinite(ox[k])) continue;
|
|
540
|
+
const px = u.valToPos(ox[k], "x", true);
|
|
541
|
+
const py = u.valToPos(v, "y", true);
|
|
542
|
+
if (!Number.isFinite(px) || !Number.isFinite(py) || px < u.bbox.left - 0.5 || px > u.bbox.left + u.bbox.width + 0.5 || py < u.bbox.top - 0.5 || py > u.bbox.top + u.bbox.height + 0.5) continue;
|
|
543
|
+
ctx.beginPath();
|
|
544
|
+
ctx.arc(px, py, r, 0, Math.PI * 2);
|
|
545
|
+
ctx.fill();
|
|
546
|
+
ctx.stroke();
|
|
547
|
+
drawn++;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
ctx.restore();
|
|
551
|
+
hostRef.current?.setAttribute("data-overlay-drawn", String(drawn));
|
|
552
|
+
}
|
|
388
553
|
const markStyle = { color: resolveToken("--color-warn", "#c80"), halo: surface, family };
|
|
389
|
-
|
|
554
|
+
const lines = yMarksRef.current ?? [];
|
|
555
|
+
if (lines.length) drawYMarks(u, lines.map((m, i) => ({ y: liveRef.current[i] ?? m.y, label: pick(m.label, lang), color: resolveToken(YMARK_TONE_TOKENS[m.tone ?? "warn"], "#c80") })), markStyle);
|
|
556
|
+
u.over.querySelectorAll("[data-y-handle]").forEach((el2) => {
|
|
557
|
+
const i = Number(el2.getAttribute("data-y-handle"));
|
|
558
|
+
const m = lines[i];
|
|
559
|
+
const v = liveRef.current[i];
|
|
560
|
+
if (!m || v === void 0) return;
|
|
561
|
+
const top = u.valToPos(v, "y");
|
|
562
|
+
el2.style.display = Number.isFinite(top) && top >= 0 && top <= u.bbox.height / (uPlot.pxRatio || 1) ? "" : "none";
|
|
563
|
+
el2.style.top = `${top - HANDLE_HEIGHT / 2}px`;
|
|
564
|
+
const label = pick(m.label, lang);
|
|
565
|
+
el2.setAttribute("aria-label", label);
|
|
566
|
+
el2.setAttribute("aria-valuenow", String(Number(v.toPrecision(12))));
|
|
567
|
+
el2.setAttribute("aria-valuetext", `${label}: ${formatNumber(v, lang, y.format ?? { digits: 4 })}${yUnit ? `${NBSP}${yUnit}` : ""}`);
|
|
568
|
+
});
|
|
390
569
|
if (marks?.length) drawMarks(u, marks.map((m) => ({ x: m.x, label: pick(m.label, lang) })), markStyle);
|
|
391
570
|
}
|
|
392
571
|
]
|
|
@@ -402,9 +581,14 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
|
|
|
402
581
|
useEffect(() => {
|
|
403
582
|
uRef.current?.setData(data);
|
|
404
583
|
}, [data]);
|
|
584
|
+
const overlaysKey = JSON.stringify(overlays.map(({ s, i }) => [i, s.x, s.values, s.size ?? 9, seriesStyle(s, i).color]));
|
|
585
|
+
const linesKey = JSON.stringify((yMarks ?? []).map((m) => [m.y, pick(m.label, lang), m.tone ?? "warn"]));
|
|
586
|
+
useEffect(() => {
|
|
587
|
+
uRef.current?.redraw(false);
|
|
588
|
+
}, [linesKey, overlaysKey]);
|
|
405
589
|
const read = cursor !== null && readout ? readout(order ? order[cursor] : cursor) : cursor !== null ? [
|
|
406
590
|
`${xLabel} ${formatNumber(xs[cursor], lang, x.format ?? { digits: 4 })}${xUnit ? `${NBSP}${xUnit}` : ""}`,
|
|
407
|
-
...
|
|
591
|
+
...baseEntries.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}` : ""}`)
|
|
408
592
|
].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";
|
|
409
593
|
return /* @__PURE__ */ jsxs(
|
|
410
594
|
"div",
|
|
@@ -420,6 +604,8 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
|
|
|
420
604
|
"data-log-x": x.log ? "1" : void 0,
|
|
421
605
|
"data-pick": onPick ? "1" : void 0,
|
|
422
606
|
"data-y-marks": yMarks?.length ? String(yMarks.length) : void 0,
|
|
607
|
+
"data-y-drag": dragCount ? String(dragCount) : void 0,
|
|
608
|
+
"data-overlay": overlays.length ? String(overlays.length) : void 0,
|
|
423
609
|
children: [
|
|
424
610
|
/* @__PURE__ */ jsx("div", { ref: setPlot, className: "caos-chart-plot" }),
|
|
425
611
|
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: [
|
|
@@ -432,4 +618,4 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
|
|
|
432
618
|
);
|
|
433
619
|
}
|
|
434
620
|
|
|
435
|
-
export { LOG_TABLE_FLOOR, UPlotChart, YMARK_TONE_TOKENS, drawMarks, drawYMarks, logDecades, nearestPoint, parityRange, rangeWithLevels, repeatedLabels, seriesStyle, smallestPositive, sortOrder, tickSteps };
|
|
621
|
+
export { LOG_TABLE_FLOOR, UPlotChart, YMARK_TONE_TOKENS, clampMark, decadeOnly, drawMarks, drawYMarks, logDecades, nearestPoint, nudgeMark, overlayExtent, parityBox, parityRange, rangeWithLevels, repeatedLabels, seriesStyle, smallestPositive, sortOrder, tickSteps };
|
package/dist/index.d.ts
CHANGED
|
@@ -260,8 +260,9 @@ interface CaseSelectorProps {
|
|
|
260
260
|
* category structure is preserved either way; only the height changes.
|
|
261
261
|
*/
|
|
262
262
|
layout?: 'chips' | 'select';
|
|
263
|
-
/** A control that sits on the select's row, after it
|
|
264
|
-
*
|
|
263
|
+
/** A control of the product that sits on the select's row, after it: the select shrinks to make room, so the selector is no taller with
|
|
264
|
+
* it (a long case name may then be cut: keep the control narrow). The workbench's own Focus control is not an accessory since 0.17.0:
|
|
265
|
+
* it sits in the group tab row. With `layout: 'chips'` it gets a row of its own, after the chips. */
|
|
265
266
|
accessory?: ReactNode;
|
|
266
267
|
}
|
|
267
268
|
/**
|
|
@@ -1251,7 +1252,7 @@ interface CaseWorkbenchProps {
|
|
|
1251
1252
|
deepLinkView?: boolean | string;
|
|
1252
1253
|
ariaLabel?: BiText;
|
|
1253
1254
|
/**
|
|
1254
|
-
* Focus mode for the App (ADR-0070, shell 0.14.0): a Focus
|
|
1255
|
+
* Focus mode for the App (ADR-0070, shell 0.14.0): a Focus control at the end of the group tab row opens the selected case's open group in
|
|
1255
1256
|
* the full viewport through `FocusShell` (the stage is the view, the rail stays), holds `?focus=1` beside `?case=` and
|
|
1256
1257
|
* `?view=` in the URL, and returns with Escape or the exit button to the same case, group and sub-view. `true` takes all
|
|
1257
1258
|
* the defaults. Omit it and the App is exactly what it was.
|
package/dist/index.js
CHANGED
|
@@ -795,17 +795,64 @@ function CaseSelector(props) {
|
|
|
795
795
|
const visible = casesInSource(cases, source);
|
|
796
796
|
const groups = groupByCategory(visible);
|
|
797
797
|
const modified = cases.find((c) => c.id === modifiedFromId) ?? null;
|
|
798
|
+
const wrapRef = useRef(null);
|
|
799
|
+
const [minHeight, setMinHeight] = useState(null);
|
|
800
|
+
const nameKey = visible.map((c) => c.name).join("");
|
|
801
|
+
useLayoutEffect(() => {
|
|
802
|
+
const wrap = wrapRef.current;
|
|
803
|
+
const display = wrap?.querySelector(".cs-select-display");
|
|
804
|
+
if (!wrap || !display) return void 0;
|
|
805
|
+
const names = nameKey ? nameKey.split("") : [];
|
|
806
|
+
const measure = () => {
|
|
807
|
+
const st = getComputedStyle(display);
|
|
808
|
+
const room = display.clientWidth - parseFloat(st.paddingLeft) - parseFloat(st.paddingRight);
|
|
809
|
+
if (!(room > 0)) return;
|
|
810
|
+
const copy = document.createElement("span");
|
|
811
|
+
copy.className = "cs-select-text";
|
|
812
|
+
copy.setAttribute("aria-hidden", "true");
|
|
813
|
+
copy.setAttribute("style", `position:absolute;visibility:hidden;left:0;top:0;width:${room}px;display:block;overflow:visible;max-height:none;-webkit-line-clamp:unset;line-clamp:unset`);
|
|
814
|
+
display.append(copy);
|
|
815
|
+
let tallest = 0;
|
|
816
|
+
for (const n of names) {
|
|
817
|
+
copy.textContent = n;
|
|
818
|
+
tallest = Math.max(tallest, copy.getBoundingClientRect().height);
|
|
819
|
+
}
|
|
820
|
+
const line = parseFloat(getComputedStyle(copy).lineHeight) || parseFloat(st.fontSize) * 1.25;
|
|
821
|
+
copy.remove();
|
|
822
|
+
const frame = parseFloat(st.paddingTop) + parseFloat(st.paddingBottom) + parseFloat(st.borderTopWidth) + parseFloat(st.borderBottomWidth);
|
|
823
|
+
const next = tallest > line * 1.5 ? Math.ceil((tallest + frame) * 100) / 100 : null;
|
|
824
|
+
setMinHeight((prev) => prev === next ? prev : next);
|
|
825
|
+
};
|
|
826
|
+
measure();
|
|
827
|
+
let width = wrap.getBoundingClientRect().width;
|
|
828
|
+
const ro = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => {
|
|
829
|
+
const w = wrap.getBoundingClientRect().width;
|
|
830
|
+
if (w !== width) {
|
|
831
|
+
width = w;
|
|
832
|
+
measure();
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
ro?.observe(wrap);
|
|
836
|
+
void document.fonts?.ready.then(measure);
|
|
837
|
+
return () => ro?.disconnect();
|
|
838
|
+
}, [layout, nameKey]);
|
|
839
|
+
const options = groups.flatMap((g) => g.cases);
|
|
840
|
+
const selectedName = (options.find((c) => c.id === selectedId) ?? options.find((c) => !c.disabled))?.name ?? "";
|
|
798
841
|
const select = /* @__PURE__ */ jsx(
|
|
799
842
|
"select",
|
|
800
843
|
{
|
|
801
844
|
"data-control": "case",
|
|
802
|
-
className: "select cs-select",
|
|
845
|
+
className: "select cs-select cs-select-native",
|
|
803
846
|
"aria-label": ariaLabel ?? "Case selector",
|
|
804
847
|
value: selectedId,
|
|
805
848
|
onChange: (e) => onSelect(e.target.value),
|
|
806
849
|
children: groups.map((g) => /* @__PURE__ */ jsx("optgroup", { label: g.category || " ", children: g.cases.map((c) => /* @__PURE__ */ jsx("option", { value: c.id, disabled: c.disabled, title: caseTooltip(c) || void 0, children: c.name }, c.id)) }, g.category || "_"))
|
|
807
850
|
}
|
|
808
851
|
);
|
|
852
|
+
const selectBox = /* @__PURE__ */ jsxs("div", { className: "cs-select-wrap", ref: wrapRef, children: [
|
|
853
|
+
/* @__PURE__ */ jsx("div", { className: "cs-select-display", style: minHeight === null ? void 0 : { minHeight }, "aria-hidden": "true", children: /* @__PURE__ */ jsx("span", { className: "cs-select-text", children: selectedName }) }),
|
|
854
|
+
select
|
|
855
|
+
] });
|
|
809
856
|
return /* @__PURE__ */ jsxs("div", { className: `cs${className ? ` ${className}` : ""}`, role: "group", "aria-label": ariaLabel ?? "Case selector", children: [
|
|
810
857
|
source && lanes.length > 1 && /* @__PURE__ */ jsxs("div", { className: "cs-source", children: [
|
|
811
858
|
/* @__PURE__ */ jsx("span", { className: "cs-source-label", children: t2.source }),
|
|
@@ -824,9 +871,9 @@ function CaseSelector(props) {
|
|
|
824
871
|
] }),
|
|
825
872
|
source && source !== "synthetic" && /* @__PURE__ */ jsx("p", { className: "cs-locked banner", children: lockedNote ?? t2.lockedNote }),
|
|
826
873
|
layout === "select" && (props.accessory ? /* @__PURE__ */ jsxs("div", { className: "cs-select-row", children: [
|
|
827
|
-
|
|
874
|
+
selectBox,
|
|
828
875
|
props.accessory
|
|
829
|
-
] }) :
|
|
876
|
+
] }) : selectBox),
|
|
830
877
|
layout === "chips" && groups.map((g) => /* @__PURE__ */ jsxs("div", { className: "cs-group", role: "group", "aria-label": g.category || void 0, "data-control": "case", children: [
|
|
831
878
|
g.category && /* @__PURE__ */ jsx("span", { className: "cs-group-label", children: g.category }),
|
|
832
879
|
/* @__PURE__ */ jsx("div", { className: "cs-chips", children: g.cases.map((c) => {
|
|
@@ -3487,19 +3534,12 @@ function CaseWorkbench(props) {
|
|
|
3487
3534
|
]
|
|
3488
3535
|
}
|
|
3489
3536
|
);
|
|
3490
|
-
const besideSelect = Boolean(cases && (cases.layout ?? "chips") === "select");
|
|
3491
3537
|
const pickers = cases || variants ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3492
|
-
cases && /* @__PURE__ */ jsx(CaseSelector, { lang, ...cases
|
|
3538
|
+
cases && /* @__PURE__ */ jsx(CaseSelector, { lang, ...cases }),
|
|
3493
3539
|
variants && /* @__PURE__ */ jsx(VariantBar, { ...variants })
|
|
3494
3540
|
] }) : null;
|
|
3495
|
-
const
|
|
3496
|
-
|
|
3497
|
-
variants && /* @__PURE__ */ jsx(VariantBar, { ...variants })
|
|
3498
|
-
] }) : null;
|
|
3499
|
-
const railHead = pickers || enter ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3500
|
-
pickers,
|
|
3501
|
-
besideSelect ? null : enter
|
|
3502
|
-
] }) : null;
|
|
3541
|
+
const railHead = pickers;
|
|
3542
|
+
const focusPickers = pickers;
|
|
3503
3543
|
const instrumentData = {
|
|
3504
3544
|
"data-case-workbench": "",
|
|
3505
3545
|
"data-group-count": tabs.length,
|
|
@@ -3533,7 +3573,8 @@ function CaseWorkbench(props) {
|
|
|
3533
3573
|
tabs,
|
|
3534
3574
|
value: openGroup,
|
|
3535
3575
|
onChange: selectGroup,
|
|
3536
|
-
ariaLabel: pick(ariaLabel ?? { en: "Case views", es: "Vistas del caso" }, lang)
|
|
3576
|
+
ariaLabel: pick(ariaLabel ?? { en: "Case views", es: "Vistas del caso" }, lang),
|
|
3577
|
+
trailing: enter || void 0
|
|
3537
3578
|
}
|
|
3538
3579
|
) }) }) }) });
|
|
3539
3580
|
}
|
package/gate/inpage.mjs
CHANGED
|
@@ -181,6 +181,67 @@ export function installLib() {
|
|
|
181
181
|
};
|
|
182
182
|
};
|
|
183
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Selects whose selected option does not fit the room it leaves, or a case select that shows another name than the one it holds (BL-074, shell
|
|
186
|
+
* 0.17.0; the reviews of 0.17.0). What is measured is what the reader sees:
|
|
187
|
+
* - a select with the shell's display (`.cs-select-wrap`, the case select since the trial of 0.17.0) shows the name whole at any length in the
|
|
188
|
+
* display's real `.cs-select-text` element: the name is cut when that element overflows its box (`scrollHeight` or `scrollWidth` past its
|
|
189
|
+
* `clientHeight` or `clientWidth` by more than 1px, e.g. a product stylesheet that clamps it), measured on the element itself, never on a probe
|
|
190
|
+
* copy, and when the display's own box is smaller than its content (a product stylesheet that fixes its height); the display is stale when the name it shows, whitespace normalized, differs from the text of the option the select holds;
|
|
191
|
+
* - any other select shows its text on one line: the room is its width less its own CSS padding and less the native arrow, which is
|
|
192
|
+
* measured, not assumed (a probe select of the same class holding one character, sized to its content; 0 with `appearance: none`).
|
|
193
|
+
* Plain text is measured with the select's own font on one canvas. Runs in the App and inside Focus (its rail holds a case select too).
|
|
194
|
+
*/
|
|
195
|
+
const selectCuts = () => {
|
|
196
|
+
const cuts = [];
|
|
197
|
+
const mctx = document.createElement('canvas').getContext('2d');
|
|
198
|
+
const norm = (t) => (t ?? '').trim().replace(/\s+/g, ' ');
|
|
199
|
+
for (const sel of document.querySelectorAll('select')) {
|
|
200
|
+
if (!mctx || !visible(sel)) continue;
|
|
201
|
+
const ss = getComputedStyle(sel);
|
|
202
|
+
const held = norm(sel.options[sel.selectedIndex]?.textContent);
|
|
203
|
+
const display = sel.closest('.cs-select-wrap')?.querySelector('.cs-select-display');
|
|
204
|
+
if (display) {
|
|
205
|
+
const t = display.querySelector('.cs-select-text') ?? display;
|
|
206
|
+
const shown = norm(t.textContent);
|
|
207
|
+
if (shown !== held) {
|
|
208
|
+
cuts.push(`${describe(sel)} shows "${shown}" while the select holds "${held}" (a stale display)`);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const overV = t.scrollHeight > t.clientHeight + 1;
|
|
212
|
+
const overH = t.scrollWidth > t.clientWidth + 1;
|
|
213
|
+
if (overV || overH) {
|
|
214
|
+
const line = Number.parseFloat(getComputedStyle(t).lineHeight) || 1;
|
|
215
|
+
cuts.push(`${describe(sel)} cuts the case name "${shown}" (the name needs ${Math.round(t.scrollHeight / line)} lines, ${Math.round(t.scrollHeight)}px high and ${Math.round(t.scrollWidth)}px wide, and its box holds ${Math.round(t.clientHeight)}px by ${Math.round(t.clientWidth)}px; held option "${held}")`);
|
|
216
|
+
} else if (display.scrollHeight > display.clientHeight + 1 || display.scrollWidth > display.clientWidth + 1) {
|
|
217
|
+
// the text fits its own box but the display's box is smaller (a product stylesheet that fixes the display's height)
|
|
218
|
+
cuts.push(`${describe(sel)} cuts the case name "${shown}" in its display (the name needs ${Math.round(display.scrollHeight)}px by ${Math.round(display.scrollWidth)}px, the display holds ${Math.round(display.clientHeight)}px by ${Math.round(display.clientWidth)}px; held option "${held}")`);
|
|
219
|
+
}
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const text = held;
|
|
223
|
+
if (!text) continue;
|
|
224
|
+
mctx.font = `${ss.fontStyle} ${ss.fontWeight} ${ss.fontSize} ${ss.fontFamily}`;
|
|
225
|
+
const pad = Number.parseFloat(ss.paddingLeft) + Number.parseFloat(ss.paddingRight);
|
|
226
|
+
let arrow = 0;
|
|
227
|
+
const host = sel.parentElement;
|
|
228
|
+
if (host) {
|
|
229
|
+
const probe = sel.cloneNode(false);
|
|
230
|
+
for (const a of [...probe.attributes]) if (a.name !== 'class') probe.removeAttribute(a.name);
|
|
231
|
+
probe.append(new Option('x'));
|
|
232
|
+
probe.setAttribute('style', 'position:absolute;visibility:hidden;width:auto;min-width:0;max-width:none;flex:none;left:0;top:0');
|
|
233
|
+
host.append(probe);
|
|
234
|
+
const pv = getComputedStyle(probe);
|
|
235
|
+
arrow = Math.max(0, probe.clientWidth - Number.parseFloat(pv.paddingLeft) - Number.parseFloat(pv.paddingRight) - mctx.measureText('x').width);
|
|
236
|
+
probe.remove();
|
|
237
|
+
}
|
|
238
|
+
const need = mctx.measureText(text).width;
|
|
239
|
+
const has = sel.clientWidth - pad - arrow;
|
|
240
|
+
if (need > has + 1) cuts.push(`${describe(sel)} cuts "${text}" (${Math.round(need)}px of text in ${Math.round(has)}px of room, the ${Math.round(arrow)}px arrow taken out)`);
|
|
241
|
+
}
|
|
242
|
+
return cuts;
|
|
243
|
+
};
|
|
244
|
+
|
|
184
245
|
/** The static layout of the current view (G5, G6 inputs, ADR-0071 and ADR-0017 measures). */
|
|
185
246
|
G.facts = () => {
|
|
186
247
|
const vw = window.innerWidth;
|
|
@@ -272,6 +333,8 @@ export function installLib() {
|
|
|
272
333
|
if (!named) truncated.push(describe(el));
|
|
273
334
|
}
|
|
274
335
|
|
|
336
|
+
const cutSelects = selectCuts();
|
|
337
|
+
|
|
275
338
|
// Containers that cut their content: overflow hidden or clip with more content than box (S1, G5).
|
|
276
339
|
const clipped = [];
|
|
277
340
|
const containers = new Set([de, body, document.getElementById('root'), ...document.querySelectorAll('main, .page, .page-body, [data-instrument], [data-rail], .tabpanel, .caos-cw, [data-plot]')]);
|
|
@@ -415,6 +478,8 @@ export function installLib() {
|
|
|
415
478
|
rail,
|
|
416
479
|
truncated: truncated.slice(0, 5),
|
|
417
480
|
truncatedCount: truncated.length,
|
|
481
|
+
cutSelects: cutSelects.slice(0, 5),
|
|
482
|
+
cutSelectCount: cutSelects.length,
|
|
418
483
|
clipped,
|
|
419
484
|
instrument,
|
|
420
485
|
surfaces,
|
|
@@ -1063,6 +1128,7 @@ export function installLib() {
|
|
|
1063
1128
|
return {
|
|
1064
1129
|
focus: Boolean(shell),
|
|
1065
1130
|
covered,
|
|
1131
|
+
cutSelects: (() => { const c = selectCuts(); return { count: c.length, list: c.slice(0, 5) }; })(),
|
|
1066
1132
|
enterInside: [...document.querySelectorAll('[data-focus-shell] [data-focus-enter]')].filter(visible).length,
|
|
1067
1133
|
app: Boolean(document.querySelector('[data-workbench], [data-surface]')),
|
|
1068
1134
|
caseId: stage ? stage.getAttribute('data-case') : null,
|
package/gate/run.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// G4 deep links every route loaded directly (and with a trailing slash), 200 and rendered; artifacts
|
|
9
9
|
// are JSON, not the HTML fallback; a missing asset answers 404; doc text clears a floor
|
|
10
10
|
// G5 scroll and reach boxes inside the viewport, the rail and their scrollers; tall views scroll; nothing is
|
|
11
|
-
// cut; truncated text carries its full text; every control reachable by the pointer
|
|
11
|
+
// cut; truncated text carries its full text; a select shows its whole selected text (0.17.0); every control reachable by the pointer
|
|
12
12
|
// G6 painted area drawing surfaces have painted pixels, fill their stage, and cover at least half the
|
|
13
13
|
// viewport on the App route, measured on the screenshot, never on a host box
|
|
14
14
|
// G7 walk everything every route, tab, sub-tab and case, at five sizes, both themes, both languages,
|
|
@@ -269,6 +269,15 @@ export async function runGate(o) {
|
|
|
269
269
|
fail('G18', { route: appRoute, mode: 'routes' }, `the App route ${appRoute} is not among the measured routes (${routes.join(', ') || 'none'}): with --workbench '' and --require-focus the gate measures the App as a surface and must walk it; add it to --routes or name it with --app-route`);
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
// ---- G18 on a workbench App (shell 0.17.0): under --require-focus every workbench route must be among the measured routes, or no Focus would be measured
|
|
273
|
+
if (opt.requireFocus) {
|
|
274
|
+
for (const wr of workbenchRoutes) {
|
|
275
|
+
if (!routes.includes(wr)) {
|
|
276
|
+
fail('G18', { route: wr, mode: 'routes' }, `the workbench route ${wr} is not among the measured routes (${routes.join(', ') || 'none'}): with --require-focus the gate must walk the App to measure its Focus mode; add it to --routes or drop it from --workbench`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
272
281
|
// ---- The landing (shell 0.13.0): the root opens the Introduction on its executive view; an old App link with a query string reaches the App.
|
|
273
282
|
{
|
|
274
283
|
const ctx = await openContext(primarySize, themes[0], langs[0]);
|
|
@@ -460,6 +469,7 @@ export async function runGate(o) {
|
|
|
460
469
|
if (f.rail.outsideCount) fail('G5', where, `${f.rail.outsideCount} rail element(s) outside the rail: ${f.rail.outside.join('; ')}`);
|
|
461
470
|
}
|
|
462
471
|
if (f.truncatedCount) fail('G5', where, `${f.truncatedCount} truncated text(s) without the full text in a title: ${f.truncated.join('; ')}`);
|
|
472
|
+
if (f.cutSelectCount) fail('G5', where, `${f.cutSelectCount} select(s) cut the selected text (a case name cut in its box, or a display that names another case than the one the select holds, or a plain select with a text wider than its room): ${f.cutSelects.join('; ')}`);
|
|
463
473
|
for (const c of f.clipped) fail('G5', where, c);
|
|
464
474
|
if (f.offCentre !== null && f.offCentre > 2) fail('ADR-0017.1', where, `the prose page is off-centre by ${f.offCentre.toFixed(1)}px`);
|
|
465
475
|
if (f.referenceLists) fail('ADR-0017.4', where, `${f.referenceLists} bibliography outside the end-of-page references block (a .reference-list); give each DocSection its refs and let the page list them once`);
|
|
@@ -913,6 +923,7 @@ export async function runGate(o) {
|
|
|
913
923
|
if (big && f.share < opt.instrumentMin) fail('G18', at, `${label}: the stage covers ${(100 * f.share).toFixed(1)}% of the viewport (floor ${(100 * opt.instrumentMin).toFixed(0)}%)`);
|
|
914
924
|
if (f.exitInside === false) fail('G18', at, `${label}: the exit control is outside the viewport`);
|
|
915
925
|
for (const c of f.covered) fail('G18', at, `${label}: ${c.chrome} covers the control "${c.control}" (the stage must clear the Focus chrome)`);
|
|
926
|
+
if (f.cutSelects.count) fail('G5', at, `${label}: ${f.cutSelects.count} select(s) cut the selected text (a case name cut in its box, or a display that names another case than the one the select holds, or a plain select with a text wider than its room): ${f.cutSelects.list.join('; ')}`);
|
|
916
927
|
if (f.enterInside > 0) fail('G18', at, `${label}: the Focus control shows inside Focus mode (it opens the mode the reader is already in)`);
|
|
917
928
|
for (const m of await page.evaluate(() => window.__caosGate.drawingText())) fail('G10', at, m);
|
|
918
929
|
for (const m of await page.evaluate(() => window.__caosGate.brokenValues())) fail('G19', at, m);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fasl-work/caos-app-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"check:sdd": "python scripts/check_sdd.py ."
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"katex": "^0.16.0",
|
|
59
|
+
"katex": "^0.16.0 || ^0.17.0 || ^0.18.0 || ^0.19.0",
|
|
60
60
|
"lucide-react": ">=0.400.0",
|
|
61
61
|
"react": "^18 || ^19",
|
|
62
62
|
"react-dom": "^18 || ^19",
|
|
@@ -67,11 +67,10 @@
|
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@playwright/test": "1.63.0",
|
|
70
|
-
"@types/katex": "^0.16.7",
|
|
71
70
|
"@types/react": "^19.0.0",
|
|
72
71
|
"@types/react-dom": "^19.0.0",
|
|
73
72
|
"esbuild": "0.28.1",
|
|
74
|
-
"katex": "
|
|
73
|
+
"katex": "0.19.0",
|
|
75
74
|
"lucide-react": "1.43.0",
|
|
76
75
|
"react": "^19.0.0",
|
|
77
76
|
"react-dom": "^19.0.0",
|
package/reserved-classes.json
CHANGED
|
@@ -198,8 +198,11 @@
|
|
|
198
198
|
"cs-locked",
|
|
199
199
|
"cs-modified",
|
|
200
200
|
"cs-reset",
|
|
201
|
-
"cs-select",
|
|
201
|
+
"cs-select-display",
|
|
202
|
+
"cs-select-native",
|
|
202
203
|
"cs-select-row",
|
|
204
|
+
"cs-select-text",
|
|
205
|
+
"cs-select-wrap",
|
|
203
206
|
"cs-source",
|
|
204
207
|
"cs-source-label",
|
|
205
208
|
"cs-source-tabs",
|
|
@@ -512,8 +515,11 @@
|
|
|
512
515
|
"cs-locked",
|
|
513
516
|
"cs-modified",
|
|
514
517
|
"cs-reset",
|
|
515
|
-
"cs-select",
|
|
518
|
+
"cs-select-display",
|
|
519
|
+
"cs-select-native",
|
|
516
520
|
"cs-select-row",
|
|
521
|
+
"cs-select-text",
|
|
522
|
+
"cs-select-wrap",
|
|
517
523
|
"cs-source",
|
|
518
524
|
"cs-source-label",
|
|
519
525
|
"cs-source-tabs",
|
package/styles.css
CHANGED
|
@@ -507,11 +507,28 @@ svg.lucide { flex: none; }
|
|
|
507
507
|
.cs-reset { border: none; background: transparent; color: var(--color-accent); font-size: 0.8rem; font-weight: 600; padding: 0.1rem 0.2rem; text-decoration: underline; text-underline-offset: 2px; }
|
|
508
508
|
/* The compact one-of-N case picker (ADR-0071 rule 7): ten cases in four categories cost a 308px chip block, 21% of a
|
|
509
509
|
2560x1440 viewport, and one row as a dropdown. */
|
|
510
|
-
|
|
511
|
-
|
|
510
|
+
/* The case select shows the selected name whole, at any length, in a display (as many lines as the longest name of the list needs, measured in the DOM by CaseSelector; no clamp, no ellipsis), the native select on top of it, transparent by colour (shell 0.17.0; BL-074). */
|
|
511
|
+
.cs-select-wrap { position: relative; width: 100%; max-width: 460px; min-width: 0; }
|
|
512
|
+
.cs-select-display { box-sizing: border-box; display: flex; align-items: center; position: relative; min-width: 0; background: var(--color-surface-2); color: var(--color-fg); border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: 0.35rem 1.7rem 0.35rem 0.5rem; font-size: 0.85rem; line-height: 1.25; }
|
|
513
|
+
.cs-select-text { display: block; max-width: 100%; min-width: 0; overflow-wrap: anywhere; }
|
|
514
|
+
.cs-select-display::after { content: ''; position: absolute; right: 0.65rem; top: 50%; width: 0.4rem; height: 0.4rem; margin-top: -0.3rem; border-right: 2px solid var(--color-fg-subtle); border-bottom: 2px solid var(--color-fg-subtle); transform: rotate(45deg); }
|
|
515
|
+
.cs-select-wrap > .cs-select-native { position: absolute; inset: 0; width: 100%; height: 100%; max-width: none; margin: 0; padding: 0 0.5rem; box-sizing: border-box; background: transparent; color: transparent; border-color: transparent; cursor: pointer; appearance: none; -webkit-appearance: none; }
|
|
516
|
+
.cs-select-native option, .cs-select-native optgroup { color: var(--color-fg); background: var(--color-surface-2); }
|
|
517
|
+
.cs-select-wrap > .cs-select-native:focus { outline: none; }
|
|
518
|
+
.cs-select-wrap:focus-within > .cs-select-display { outline: 2px solid var(--color-accent); outline-offset: 2px; }
|
|
519
|
+
@supports selector(:has(*)) {
|
|
520
|
+
.cs-select-wrap:focus-within > .cs-select-display { outline: none; }
|
|
521
|
+
.cs-select-wrap:has(> .cs-select-native:focus-visible) > .cs-select-display { outline: 2px solid var(--color-accent); outline-offset: 2px; }
|
|
522
|
+
}
|
|
523
|
+
/* Windows High Contrast forces a select's text back to CanvasText, which would paint its one cut line over the display's lines (the re-review of 0.17.0, B1) */
|
|
524
|
+
@media (forced-colors: active) {
|
|
525
|
+
.cs-select-wrap > .cs-select-native { forced-color-adjust: none; }
|
|
526
|
+
.cs-select-native option, .cs-select-native optgroup { forced-color-adjust: auto; }
|
|
527
|
+
}
|
|
528
|
+
/* the select and a control beside it (a product's accessory): one row, the select shrinks, the control keeps its size */
|
|
512
529
|
.cs-select-row { display: flex; align-items: center; gap: 0.5rem; min-width: 0; }
|
|
513
|
-
.cs-select-row > .cs-select { flex: 1 1 auto; min-width: 0; }
|
|
514
|
-
.cs-select-row > :not(.cs-select) { flex: none; }
|
|
530
|
+
.cs-select-row > .cs-select-wrap { flex: 1 1 auto; min-width: 0; }
|
|
531
|
+
.cs-select-row > :not(.cs-select-wrap) { flex: none; }
|
|
515
532
|
|
|
516
533
|
/* == 9 workbench ================================================================================================ */
|
|
517
534
|
|
|
@@ -858,3 +875,7 @@ svg.lucide { flex: none; }
|
|
|
858
875
|
.tabs-has-slot > .tabs-slot { grid-column: 2; grid-row: 1; align-self: stretch; display: flex; align-items: center; padding-bottom: 0.4rem; margin-bottom: 0.9rem; border-bottom: 1px solid var(--color-border); }
|
|
859
876
|
.tabs-has-slot > .tabs-slot > .caos-focus-enter { align-self: center; padding-block: 0.2rem; }
|
|
860
877
|
.tabs-has-slot > .tabpanel { grid-column: 1 / -1; grid-row: 2; }
|
|
878
|
+
/* The same slot in the workbench's group tab row (shell 0.17.0, BL-074): the Focus control of the App ends the row, so the case select keeps
|
|
879
|
+
the rail head's whole width. The tab row's own spacing (a tighter rule than the surface's) is repeated under the slot. */
|
|
880
|
+
.caos-cw > .tabs.tabs-has-slot { display: grid; grid-template-columns: minmax(0, 1fr) auto; grid-template-rows: auto minmax(0, 1fr); column-gap: 0.5rem; }
|
|
881
|
+
.caos-cw > .tabs-has-slot > .tabs-slot { margin-bottom: 0.5rem; padding-bottom: var(--space-1); }
|