@fasl-work/caos-app-shell 0.24.0 → 0.25.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/CHANGELOG.md +19 -0
- package/VERSION +1 -1
- package/dist/chart.js +62 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.js +52 -48
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## [0.25.000] - 2026-11-08
|
|
2
|
+
|
|
3
|
+
Two gaps that CEFOP_DinHot's full gate found on its release build (shell 0.23.2; CAOS_APP_SHELL#207, #208; known defects 61 and 62).
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `VariantBar` `layout?: 'chips' | 'select'` (default `'chips'`, markup unchanged; known defect 61, SHW-063, issue 207). `'select'` draws the variants as one native select with the shell's display, the active variant's full label shown whole and wrapping inside the rail, and keeps the title with its count, the lane badge, the active variant's note, `data-variant-bar`, `data-variant-count`, `data-control="variant"`, the keyboard of a native select and the disabled options. A list of 14 long-named variants fits a 232 px rail at 1280 x 800 in en and es and in the wide-font pass (as chips it scrolled the rail: G5 441, ADR-0071.6 266 and G18 14 findings on DinHot).
|
|
8
|
+
- `SelectDisplay` (`src/lib/SelectDisplay.tsx`, internal): the display of the case select, extracted so `CaseSelector` and `VariantBar` share one component; the case select's markup and behaviour are unchanged.
|
|
9
|
+
- Gate plants: `variants-long-chips` (the long list as chips fails ADR-0071.6), `legit-variants-select` (the same list in `layout: 'select'` passes), `legit-chart-in-document`, `legit-chart-pointer-redispatch` and `chart-readout-scroll-move` (the document walk on a long page with a chart).
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- A chart's readout no longer changes when the page scrolls under a resting pointer (known defect 62, SHC-103, issue 208, ADR-0017.2). The browser re-dispatches the pointer position (a mouseover, mouseenter and mousemove at the same client coordinates) to the chart that came under it, and sends a mouseleave at the same coordinates when the scroll takes the plot away; `UPlotChart` moved its cursor and readout on both, so the gate's document walk reported "document text changed during traversal" (reproduced on the shell's own fixture, three tabs of /experiments). `UPlotChart` now changes its cursor and readout only on a real pointer move, an enter or a leave by moving, or the keyboard; the held leave is applied at the next real move of the pointer, and a touch or a pen is never judged.
|
|
14
|
+
|
|
15
|
+
### Adoption
|
|
16
|
+
|
|
17
|
+
- A product with a long or long-named variant list sets `layout: 'select'` on its `variants` prop (`CaseWorkbench` forwards it to `VariantBar`); nothing else changes. A product with a short list keeps the chips.
|
|
18
|
+
- Known defect 62: no product change. A product that worked around it (a pointer moved off the document before the walk, a readout excluded from the walk) removes the workaround. A chart scrolled away from a resting pointer keeps its last reading until the pointer next moves, leaves by moving, or the keyboard acts.
|
|
19
|
+
|
|
1
20
|
## [0.24.000] - 2026-11-08
|
|
2
21
|
|
|
3
22
|
Four defects that an ADR-0082 review of CAOS_ChancaDEM on 0.23.2 found (CAOS_MANAGE `wip/chancadem/viz-review-2026-11-07.md`, findings M11, M1, M3, m4).
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.25.000
|
package/dist/chart.js
CHANGED
|
@@ -5,6 +5,46 @@ import { useRef, useCallback, useState, useId, useEffect, useMemo } from 'react'
|
|
|
5
5
|
import uPlot from 'uplot';
|
|
6
6
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
7
7
|
|
|
8
|
+
// src/chart/pointer.ts
|
|
9
|
+
var SCROLL_WINDOW_MS = 300;
|
|
10
|
+
var newTrack = () => ({ x: Number.NaN, y: Number.NaN, scrolledAt: -Infinity, type: "mouse" });
|
|
11
|
+
function scrollSynthesised(t, e, now) {
|
|
12
|
+
if (t.type !== "mouse") return false;
|
|
13
|
+
if (!Number.isNaN(t.x)) return e.clientX === t.x && e.clientY === t.y;
|
|
14
|
+
return now - t.scrolledAt < SCROLL_WINDOW_MS && e.movementX === 0 && e.movementY === 0;
|
|
15
|
+
}
|
|
16
|
+
var track = null;
|
|
17
|
+
var realMoveListeners = /* @__PURE__ */ new Set();
|
|
18
|
+
function onRealMove(fn) {
|
|
19
|
+
realMoveListeners.add(fn);
|
|
20
|
+
return () => void realMoveListeners.delete(fn);
|
|
21
|
+
}
|
|
22
|
+
function pointerTrack() {
|
|
23
|
+
if (track) return track;
|
|
24
|
+
const t = newTrack();
|
|
25
|
+
track = t;
|
|
26
|
+
if (typeof window === "undefined") return t;
|
|
27
|
+
window.addEventListener("scroll", () => {
|
|
28
|
+
t.scrolledAt = performance.now();
|
|
29
|
+
}, { capture: true, passive: true });
|
|
30
|
+
const type = (e) => {
|
|
31
|
+
t.type = e.pointerType || "mouse";
|
|
32
|
+
};
|
|
33
|
+
window.addEventListener("pointerdown", type, { capture: true, passive: true });
|
|
34
|
+
window.addEventListener("pointermove", type, { capture: true, passive: true });
|
|
35
|
+
window.addEventListener(
|
|
36
|
+
"mousemove",
|
|
37
|
+
(e) => {
|
|
38
|
+
if (e.clientX === t.x && e.clientY === t.y) return;
|
|
39
|
+
t.x = e.clientX;
|
|
40
|
+
t.y = e.clientY;
|
|
41
|
+
for (const fn of [...realMoveListeners]) fn();
|
|
42
|
+
},
|
|
43
|
+
{ passive: true }
|
|
44
|
+
);
|
|
45
|
+
return t;
|
|
46
|
+
}
|
|
47
|
+
|
|
8
48
|
// src/chart/uplotZoom.ts
|
|
9
49
|
var rangeOf = (u, key) => [u.scales[key].min ?? 0, u.scales[key].max ?? 1];
|
|
10
50
|
function createZoom(u, ctx) {
|
|
@@ -506,6 +546,7 @@ function nearestPoint(xs, ys, left, top) {
|
|
|
506
546
|
}
|
|
507
547
|
function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280, onCursor, onPick, parity, readout, hint, solo, title, zoomY, zoomOff }) {
|
|
508
548
|
const lang = useShellLang();
|
|
549
|
+
pointerTrack();
|
|
509
550
|
const theme = useThemeStore((s) => s.theme);
|
|
510
551
|
const fill = height === "fill";
|
|
511
552
|
const [measureRef, box] = useStageSize();
|
|
@@ -767,6 +808,27 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
767
808
|
],
|
|
768
809
|
ready: [
|
|
769
810
|
(u2) => {
|
|
811
|
+
const track2 = pointerTrack();
|
|
812
|
+
let heldLeave = false;
|
|
813
|
+
let replay = false;
|
|
814
|
+
const real = (e) => {
|
|
815
|
+
if (replay) return;
|
|
816
|
+
if (scrollSynthesised(track2, e, performance.now())) {
|
|
817
|
+
if (e.type === "mouseleave") heldLeave = true;
|
|
818
|
+
e.stopImmediatePropagation();
|
|
819
|
+
} else if (e.type !== "mouseleave") heldLeave = false;
|
|
820
|
+
};
|
|
821
|
+
u2.root.addEventListener("mousemove", real, true);
|
|
822
|
+
u2.root.addEventListener("mouseenter", real, true);
|
|
823
|
+
u2.root.addEventListener("mouseleave", real, true);
|
|
824
|
+
const stopHeld = onRealMove(() => {
|
|
825
|
+
if (!heldLeave) return;
|
|
826
|
+
heldLeave = false;
|
|
827
|
+
replay = true;
|
|
828
|
+
u2.over.dispatchEvent(new MouseEvent("mouseleave", { clientX: track2.x, clientY: track2.y }));
|
|
829
|
+
replay = false;
|
|
830
|
+
});
|
|
831
|
+
u2.hooks.destroy = [...u2.hooks.destroy ?? [], stopHeld];
|
|
770
832
|
u2.over.addEventListener("click", () => {
|
|
771
833
|
const idx = pickedRef.current;
|
|
772
834
|
if (idx === null || idx === void 0 || !onPickRef.current) return;
|
package/dist/index.d.ts
CHANGED
|
@@ -1200,10 +1200,15 @@ interface VariantBarProps {
|
|
|
1200
1200
|
onSelect: (id: string) => void;
|
|
1201
1201
|
title?: BiText;
|
|
1202
1202
|
lane?: Lane;
|
|
1203
|
+
/** How the one-of-N choice is drawn. 'chips' (the default) is one scrollable row of chips; 'select' is one native select with the shell's display
|
|
1204
|
+
* (the case select's: the active variant's full label shown whole, wrapping inside the rail), for a long or long-named list (ADR-0071 rule 7: 14
|
|
1205
|
+
* methods named by their names drew a 605 px column of chips in a 713 px rail). The title with its count, the lane badge, the active variant's
|
|
1206
|
+
* note, `data-variant-bar`, `data-variant-count` and `data-control="variant"` are the same in both. */
|
|
1207
|
+
layout?: 'chips' | 'select';
|
|
1203
1208
|
}
|
|
1204
|
-
/** The regimes of the selected case on ONE scrollable row (ADR-0071 rule 4),
|
|
1205
|
-
* one-line note of what it shows. Selecting a regime must change every view of the case. */
|
|
1206
|
-
declare function VariantBar({ variants, activeId, onSelect, title, lane }: VariantBarProps): react.JSX.Element;
|
|
1209
|
+
/** The regimes of the selected case on ONE scrollable row (ADR-0071 rule 4) or, with `layout: 'select'`, in one select (rule 7), the active
|
|
1210
|
+
* regime's lane, and the one-line note of what it shows. Selecting a regime must change every view of the case. */
|
|
1211
|
+
declare function VariantBar({ variants, activeId, onSelect, title, lane, layout }: VariantBarProps): react.JSX.Element;
|
|
1207
1212
|
|
|
1208
1213
|
/** One group of views of the selected case, named for the question the user is asking (ADR-0071 rule 5). */
|
|
1209
1214
|
interface WorkbenchGroup {
|
package/dist/index.js
CHANGED
|
@@ -702,6 +702,53 @@ function usePausedViz(frame, options = {}) {
|
|
|
702
702
|
const restart = useCallback(() => loopRef.current?.restart(), []);
|
|
703
703
|
return { playing, play, pause, toggle, restart };
|
|
704
704
|
}
|
|
705
|
+
function SelectDisplay({ names, selectedName, children }) {
|
|
706
|
+
const wrapRef = useRef(null);
|
|
707
|
+
const [minHeight, setMinHeight] = useState(null);
|
|
708
|
+
const nameKey = names.join("");
|
|
709
|
+
useLayoutEffect(() => {
|
|
710
|
+
const wrap = wrapRef.current;
|
|
711
|
+
const display = wrap?.querySelector(".cs-select-display");
|
|
712
|
+
if (!wrap || !display) return void 0;
|
|
713
|
+
const all = nameKey ? nameKey.split("") : [];
|
|
714
|
+
const measure = () => {
|
|
715
|
+
const st = getComputedStyle(display);
|
|
716
|
+
const room = display.clientWidth - parseFloat(st.paddingLeft) - parseFloat(st.paddingRight);
|
|
717
|
+
if (!(room > 0)) return;
|
|
718
|
+
const copy = document.createElement("span");
|
|
719
|
+
copy.className = "cs-select-text";
|
|
720
|
+
copy.setAttribute("aria-hidden", "true");
|
|
721
|
+
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`);
|
|
722
|
+
display.append(copy);
|
|
723
|
+
let tallest = 0;
|
|
724
|
+
for (const n of all) {
|
|
725
|
+
copy.textContent = n;
|
|
726
|
+
tallest = Math.max(tallest, copy.getBoundingClientRect().height);
|
|
727
|
+
}
|
|
728
|
+
const line = parseFloat(getComputedStyle(copy).lineHeight) || parseFloat(st.fontSize) * 1.25;
|
|
729
|
+
copy.remove();
|
|
730
|
+
const frame = parseFloat(st.paddingTop) + parseFloat(st.paddingBottom) + parseFloat(st.borderTopWidth) + parseFloat(st.borderBottomWidth);
|
|
731
|
+
const next = tallest > line * 1.5 ? Math.ceil((tallest + frame) * 100) / 100 : null;
|
|
732
|
+
setMinHeight((prev) => prev === next ? prev : next);
|
|
733
|
+
};
|
|
734
|
+
measure();
|
|
735
|
+
let width = wrap.getBoundingClientRect().width;
|
|
736
|
+
const ro = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => {
|
|
737
|
+
const w = wrap.getBoundingClientRect().width;
|
|
738
|
+
if (w !== width) {
|
|
739
|
+
width = w;
|
|
740
|
+
measure();
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
ro?.observe(wrap);
|
|
744
|
+
void document.fonts?.ready.then(measure);
|
|
745
|
+
return () => ro?.disconnect();
|
|
746
|
+
}, [nameKey]);
|
|
747
|
+
return /* @__PURE__ */ jsxs("div", { className: "cs-select-wrap", ref: wrapRef, children: [
|
|
748
|
+
/* @__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 }) }),
|
|
749
|
+
children
|
|
750
|
+
] });
|
|
751
|
+
}
|
|
705
752
|
|
|
706
753
|
// src/case/caseModel.ts
|
|
707
754
|
var caseKind = (c) => c.kind ?? "synthetic";
|
|
@@ -798,47 +845,6 @@ function CaseSelector(props) {
|
|
|
798
845
|
const visible = casesInSource(cases, source);
|
|
799
846
|
const groups = groupByCategory(visible);
|
|
800
847
|
const modified = cases.find((c) => c.id === modifiedFromId) ?? null;
|
|
801
|
-
const wrapRef = useRef(null);
|
|
802
|
-
const [minHeight, setMinHeight] = useState(null);
|
|
803
|
-
const nameKey = visible.map((c) => c.name).join("");
|
|
804
|
-
useLayoutEffect(() => {
|
|
805
|
-
const wrap = wrapRef.current;
|
|
806
|
-
const display = wrap?.querySelector(".cs-select-display");
|
|
807
|
-
if (!wrap || !display) return void 0;
|
|
808
|
-
const names = nameKey ? nameKey.split("") : [];
|
|
809
|
-
const measure = () => {
|
|
810
|
-
const st = getComputedStyle(display);
|
|
811
|
-
const room = display.clientWidth - parseFloat(st.paddingLeft) - parseFloat(st.paddingRight);
|
|
812
|
-
if (!(room > 0)) return;
|
|
813
|
-
const copy = document.createElement("span");
|
|
814
|
-
copy.className = "cs-select-text";
|
|
815
|
-
copy.setAttribute("aria-hidden", "true");
|
|
816
|
-
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`);
|
|
817
|
-
display.append(copy);
|
|
818
|
-
let tallest = 0;
|
|
819
|
-
for (const n of names) {
|
|
820
|
-
copy.textContent = n;
|
|
821
|
-
tallest = Math.max(tallest, copy.getBoundingClientRect().height);
|
|
822
|
-
}
|
|
823
|
-
const line = parseFloat(getComputedStyle(copy).lineHeight) || parseFloat(st.fontSize) * 1.25;
|
|
824
|
-
copy.remove();
|
|
825
|
-
const frame = parseFloat(st.paddingTop) + parseFloat(st.paddingBottom) + parseFloat(st.borderTopWidth) + parseFloat(st.borderBottomWidth);
|
|
826
|
-
const next = tallest > line * 1.5 ? Math.ceil((tallest + frame) * 100) / 100 : null;
|
|
827
|
-
setMinHeight((prev) => prev === next ? prev : next);
|
|
828
|
-
};
|
|
829
|
-
measure();
|
|
830
|
-
let width = wrap.getBoundingClientRect().width;
|
|
831
|
-
const ro = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => {
|
|
832
|
-
const w = wrap.getBoundingClientRect().width;
|
|
833
|
-
if (w !== width) {
|
|
834
|
-
width = w;
|
|
835
|
-
measure();
|
|
836
|
-
}
|
|
837
|
-
});
|
|
838
|
-
ro?.observe(wrap);
|
|
839
|
-
void document.fonts?.ready.then(measure);
|
|
840
|
-
return () => ro?.disconnect();
|
|
841
|
-
}, [layout, nameKey]);
|
|
842
848
|
const options = groups.flatMap((g) => g.cases);
|
|
843
849
|
const selectedName = (options.find((c) => c.id === selectedId) ?? options.find((c) => !c.disabled))?.name ?? "";
|
|
844
850
|
const select = /* @__PURE__ */ jsx(
|
|
@@ -852,10 +858,7 @@ function CaseSelector(props) {
|
|
|
852
858
|
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 || "_"))
|
|
853
859
|
}
|
|
854
860
|
);
|
|
855
|
-
const selectBox = /* @__PURE__ */
|
|
856
|
-
/* @__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 }) }),
|
|
857
|
-
select
|
|
858
|
-
] });
|
|
861
|
+
const selectBox = /* @__PURE__ */ jsx(SelectDisplay, { names: visible.map((c) => c.name), selectedName, children: select });
|
|
859
862
|
return /* @__PURE__ */ jsxs("div", { className: `cs${className ? ` ${className}` : ""}`, role: "group", "aria-label": ariaLabel ?? "Case selector", children: [
|
|
860
863
|
source && lanes.length > 1 && /* @__PURE__ */ jsxs("div", { className: "cs-source", children: [
|
|
861
864
|
/* @__PURE__ */ jsx("span", { className: "cs-source-label", children: t2.source }),
|
|
@@ -3824,23 +3827,24 @@ function LaneBadge({ lane, provenance, label }) {
|
|
|
3824
3827
|
provenance && /* @__PURE__ */ jsx("span", { className: `badge caos-prov caos-prov-${provenance}`, "data-provenance": provenance, children: pick(PROVENANCE_LABELS[provenance], lang) })
|
|
3825
3828
|
] });
|
|
3826
3829
|
}
|
|
3827
|
-
function VariantBar({ variants, activeId, onSelect, title, lane }) {
|
|
3830
|
+
function VariantBar({ variants, activeId, onSelect, title, lane, layout = "chips" }) {
|
|
3828
3831
|
const lang = useShellLang();
|
|
3829
3832
|
const active = variants.find((v) => v.id === activeId) ?? variants[0];
|
|
3830
3833
|
const shownLane = active?.lane ?? lane;
|
|
3831
3834
|
const rowRef = useRef(null);
|
|
3832
3835
|
useOverflowFade(rowRef, ".chip.on", [active?.id, variants.length, lang]);
|
|
3836
|
+
const name = pick(title ?? { en: "Variants", es: "Variantes" }, lang);
|
|
3833
3837
|
return /* @__PURE__ */ jsxs("div", { className: "caos-variant-bar", "data-variant-bar": "", "data-variant-count": variants.length, children: [
|
|
3834
3838
|
/* @__PURE__ */ jsxs("div", { className: "caos-variant-head", children: [
|
|
3835
3839
|
/* @__PURE__ */ jsxs("span", { className: "caos-variant-title", children: [
|
|
3836
|
-
|
|
3840
|
+
name,
|
|
3837
3841
|
" (",
|
|
3838
3842
|
variants.length,
|
|
3839
3843
|
")"
|
|
3840
3844
|
] }),
|
|
3841
3845
|
shownLane && /* @__PURE__ */ jsx(LaneBadge, { lane: shownLane })
|
|
3842
3846
|
] }),
|
|
3843
|
-
/* @__PURE__ */ jsx("
|
|
3847
|
+
layout === "select" ? /* @__PURE__ */ jsx(SelectDisplay, { names: variants.map((v) => pick(v.label, lang)), selectedName: active ? pick(active.label, lang) : "", children: /* @__PURE__ */ jsx("select", { "data-control": "variant", className: "select cs-select cs-select-native", "aria-label": name, value: active?.id ?? "", onChange: (e) => onSelect(e.target.value), children: variants.map((v) => /* @__PURE__ */ jsx("option", { value: v.id, disabled: v.disabled, children: pick(v.label, lang) }, v.id)) }) }) : /* @__PURE__ */ jsx("div", { className: "caos-chip-row", role: "radiogroup", "aria-label": name, "data-control": "variant", ref: rowRef, children: variants.map((v) => /* @__PURE__ */ jsx(
|
|
3844
3848
|
"button",
|
|
3845
3849
|
{
|
|
3846
3850
|
type: "button",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fasl-work/caos-app-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.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",
|