@fasl-work/caos-app-shell 0.23.2 → 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 +40 -0
- package/VERSION +1 -1
- package/dist/chart.js +62 -0
- package/dist/index.d.ts +31 -7
- package/dist/index.js +168 -66
- package/gate/inpage.mjs +2 -0
- package/gate/run.mjs +59 -19
- package/package.json +1 -1
- package/reserved-classes.json +4 -0
- package/styles.css +13 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,43 @@
|
|
|
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
|
+
|
|
20
|
+
## [0.24.000] - 2026-11-08
|
|
21
|
+
|
|
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).
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- A scene callout no longer shows its state by colour alone (known defect 58, SHW-059, ADR-0082 rule 8). Every `SceneCallouts` card carries a state label, a word in the page language with a glyph of its own shape (en normal, warning, alarm, selected, inactive; es normal, aviso, alarma, seleccionado, inactivo), marked `data-callout-state-label`, on a row of its own; the border colour stays. A callout without a recognised state gets no label (G21 already fails it). The eyebrow wraps instead of ending in an ellipsis: on the shell's own fixture the Spanish eyebrows were cut by 19 to 58 px at 390 and 1280 px.
|
|
27
|
+
- G21 measures the callouts at every size, theme and language of the gate matrix (known defect 59, SHG-090). Its callout checks (value and unit, state and state label, provenance, the 8 px clearance of the own anchor, no overlap, inside the stage, one leader per callout, no horizontal overflow) ran at the primary size, first theme and first language only, so ChancaDEM's cards overlapped by 2 to 6 px at 390x844 and in Spanish while the gate passed. The interaction, motion and performance checks stay at the primary size. The report lists the measured contexts (`storyContexts`). A new G21 check fails a callout whose state label is not visible (SHG-091).
|
|
28
|
+
- Header callouts are laid out on their measured heights, in the order of their anchors (known defect 60, SHW-060, SHW-061). `layoutCalloutHeader` placed the cards in declaration order on nominal heights (68 px, 54 px compact), so a card that grew when its source wrapped covered the next row and a card anchored at the right could sit at the left with its leader across the subject. The layer now measures its cards (ResizeObserver) and lays them out again when a height changes; `layoutCalloutHeader(items, width, height, compact, heights?)` reads the cards in anchor x order (stable for equal x), sizes each row by its tallest card, takes the slot order with the fewest crossing leaders (the anchor order winning every tie) and narrows a compact card to the cell, never over its neighbour. `layoutSceneCallouts` takes the same optional `heights`.
|
|
29
|
+
- At a phone width every verb of the story stepper is whole (SHW-062, review m4). A step is at least as wide as its number and verb and never wider than the row (a longer verb wraps inside its step, so a platform with a wider system font shows it whole too: the first candidate's main CI on Linux overflowed a 320 px row), the row scrolls on x, and `StoryFrame` brings the active step inside the row whenever it changes (keyboard, autoplay, deep link).
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- `SceneCallouts` `onLayout?: (info: { bandHeight: number }) => void` and the CSS custom property `--caos-callout-band` (px) on the header layer: the distance from the top of the layer to the bottom of the lowest card after layout.
|
|
34
|
+
|
|
35
|
+
### Adoption
|
|
36
|
+
|
|
37
|
+
- Known defects 58 and 59: no product change beyond the product's own gate run on this release; a product that drew its own state word or glyph in the eyebrow to work around defect 58 removes it. A product whose cards overlapped only on a phone or in Spanish now fails G21 there and fixes its layout.
|
|
38
|
+
- Known defect 60: a product that reserves a header band reads `--caos-callout-band` (CSS) or `onLayout` (JavaScript) instead of a band computed from the nominal 184x68 or 164x54 cards, and drops any local reordering of its callouts by anchor x.
|
|
39
|
+
- The phone stepper: no product change; a product that shortened its step verbs to fit the 90 px columns may restore them.
|
|
40
|
+
|
|
1
41
|
## [0.23.002] - 2026-11-07
|
|
2
42
|
|
|
3
43
|
### Fixed
|
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 {
|
|
@@ -1451,13 +1456,32 @@ interface CalloutBox {
|
|
|
1451
1456
|
anchorX: number;
|
|
1452
1457
|
anchorY: number;
|
|
1453
1458
|
}
|
|
1459
|
+
/** Measured card heights in CSS pixels by callout id; a callout without one is laid out at the nominal height. */
|
|
1460
|
+
type CalloutHeights = Readonly<Record<string, number>>;
|
|
1454
1461
|
/** Deterministic callout placement, exported so a product can test its scene geometry before rendering. */
|
|
1455
|
-
declare function layoutSceneCallouts(items: SceneCalloutDef[], width: number, height: number, compact?: boolean): CalloutBox[];
|
|
1456
|
-
/**
|
|
1457
|
-
|
|
1462
|
+
declare function layoutSceneCallouts(items: SceneCalloutDef[], width: number, height: number, compact?: boolean, heights?: CalloutHeights): CalloutBox[];
|
|
1463
|
+
/**
|
|
1464
|
+
* Callout cards in a reserved top band; leaders retain their individual scene anchors. The cards are read in the order of
|
|
1465
|
+
* their anchors' x (stable for equal x) and placed in rows from left to right, each row as tall as its tallest card, so a card
|
|
1466
|
+
* that grew when its text wrapped never covers the next row (known defect 60). `heights` are the measured card heights
|
|
1467
|
+
* (SceneCallouts measures its cards and lays them out again when one changes); without them the nominal heights are used.
|
|
1468
|
+
* When that order would make two leaders cross (anchors at different depths), the slot order with the fewest crossings is
|
|
1469
|
+
* taken instead, the anchor order winning every tie. The boxes are returned in anchor x order.
|
|
1470
|
+
*/
|
|
1471
|
+
declare function layoutCalloutHeader(items: SceneCalloutDef[], width: number, height: number, compact?: boolean, heights?: CalloutHeights): CalloutBox[];
|
|
1472
|
+
/**
|
|
1473
|
+
* Overlay anchored value labels on a stage; normalized anchors are reprojected by the product as the camera moves.
|
|
1474
|
+
* The layer measures its cards and lays them out on their real heights. In the header placement it sets
|
|
1475
|
+
* `--caos-callout-band` (px) on the layer and calls `onLayout` with the band height: the distance from the top of the
|
|
1476
|
+
* layer to the bottom of the lowest card, which a product reserves above its drawing instead of a nominal band.
|
|
1477
|
+
*/
|
|
1478
|
+
declare function SceneCallouts({ items, className, placement, onLayout }: {
|
|
1458
1479
|
items: SceneCalloutDef[];
|
|
1459
1480
|
className?: string;
|
|
1460
1481
|
placement?: 'anchored' | 'header';
|
|
1482
|
+
onLayout?: (info: {
|
|
1483
|
+
bandHeight: number;
|
|
1484
|
+
}) => void;
|
|
1461
1485
|
}): react.JSX.Element;
|
|
1462
1486
|
|
|
1463
1487
|
interface Scene3DFrameProps {
|
|
@@ -1587,4 +1611,4 @@ declare function explodedOffsets(parts: ExplodedPart[], scale?: number): Record<
|
|
|
1587
1611
|
declare function detailTier(distance: number): DetailTier;
|
|
1588
1612
|
declare function sceneEnvironment(theme: 'light' | 'dark'): SceneEnvironment;
|
|
1589
1613
|
|
|
1590
|
-
export { BREAKPOINTS, BarChart, type BarChartProps, type BarDatum, BiText, Callout, type CalloutBox, type CalloutState, type CameraFlyOptions, type CameraFrame, type CameraPose, CardDeck, type CardDeckProps, type CaseDef, type CaseFocusConfig, type CaseGroup, type CaseKind, CaseSelector, type CaseSelectorProps, type CaseSelectorText, CaseWorkbench, type CaseWorkbenchProps, ChipGroup, type ChipGroupProps, type ChipOption, type Citation, CitationsProvider, Cite, DECK_TEXT, DOC_AREAS, type DeckCard, type DetailTier, DocPage, type DocPageProps, DocReferences, DocRefsScope, DocSection, type DocSectionProps, EXECUTIVE_SCHEMA_VERSION, Equation, type ExecutiveData, type ExecutiveProblem, ExecutiveView, type ExecutiveViewProps, type ExplodedPart, FLIP_LIMITS, FLIP_TEXT, FOCUS_PARAM, FOCUS_TEXT, FOUR_CS, Figure, FlipCard, type FlipCardProps, type FocusMode, type FocusModeConfig, type FocusModeOptions, FocusShell, type FocusShellProps, FormatOptions, Gauge, type GaugeProps, type GaugeZone, Glossary, type GlossaryEntry, InlineMath, Knob, type KnobProps, Landing, type Lane, LaneBadge, Lang, LanguageToggle, LinkedViewProvider, type LinkedViewSide, type LinkedViewState, type LinkedViewStore, MAX_PEER_TABS, MAX_WORKBENCH_GROUPS, type MotionOptions, type MotionStage, PanelBoundary, type PausedVizController, PlotCard, type PlotCardProps, type Provenance, Question, type QuestionProps, type RailSection, Readout, type ReadoutItem, type ReadoutProps, ReferenceList, Refs, Scene3DFrame, type Scene3DFrameProps, type SceneBounds, type SceneCalloutDef, SceneCallouts, type SceneEnvironment, type SelectionOrigin, Stage, type StageSize, Stat, type StatProps, type StoryAccent, StoryFrame, type StoryFrameProps, type StoryStep, type SubTabDef, SubTabs, type SubTabsProps, type SurfaceFocusConfig, SurfacePage, type SurfacePageProps, type TabDef, type TabGroupDef, TabGroups, type TabGroupsProps, Tabs, type TabsProps, type Theme, ThemeToggle, type Tone, type UsePausedVizOptions, VariantBar, type VariantBarProps, type VariantDef, type Vec3, Verdict, type VerdictProps, type ViewFocusConfig, ViewsRow, type ViewsRowProps, type VizFrame, type VizLoop, type VizLoopDeps, type VizLoopOptions, type WorkbenchGroup, WorkbenchLayout, type WorkbenchLayoutProps, type WorkbenchRoute, WorkbenchShell, type WorkbenchShellProps, type WorkbenchState, WorkbenchStateContext, applyTheme, cameraForBounds, caseKind, caseTooltip, casesInSource, createLinkedViewStore, createVizLoop, deckPosition, deckWindow, detailTier, explodedOffsets, firstAppearance, flowSpeed, flyCamera, groupByCategory, isStale, layoutSceneCallouts, makeStateKey, perPageFor, prefersReducedMotion, readCaseParam, readTheme, removeQueryParam, replaceQueryParam, runStagedTransition, sceneEnvironment, sourcesPresent, useFocusMode, useLinkedCursor, useLinkedItem, useLinkedView, usePausedViz, useStageSize, useThemeStore, useUrlView, useWorkbenchState, validateExecutive, viewParamOf, withCaseParam };
|
|
1614
|
+
export { BREAKPOINTS, BarChart, type BarChartProps, type BarDatum, BiText, Callout, type CalloutBox, type CalloutHeights, type CalloutState, type CameraFlyOptions, type CameraFrame, type CameraPose, CardDeck, type CardDeckProps, type CaseDef, type CaseFocusConfig, type CaseGroup, type CaseKind, CaseSelector, type CaseSelectorProps, type CaseSelectorText, CaseWorkbench, type CaseWorkbenchProps, ChipGroup, type ChipGroupProps, type ChipOption, type Citation, CitationsProvider, Cite, DECK_TEXT, DOC_AREAS, type DeckCard, type DetailTier, DocPage, type DocPageProps, DocReferences, DocRefsScope, DocSection, type DocSectionProps, EXECUTIVE_SCHEMA_VERSION, Equation, type ExecutiveData, type ExecutiveProblem, ExecutiveView, type ExecutiveViewProps, type ExplodedPart, FLIP_LIMITS, FLIP_TEXT, FOCUS_PARAM, FOCUS_TEXT, FOUR_CS, Figure, FlipCard, type FlipCardProps, type FocusMode, type FocusModeConfig, type FocusModeOptions, FocusShell, type FocusShellProps, FormatOptions, Gauge, type GaugeProps, type GaugeZone, Glossary, type GlossaryEntry, InlineMath, Knob, type KnobProps, Landing, type Lane, LaneBadge, Lang, LanguageToggle, LinkedViewProvider, type LinkedViewSide, type LinkedViewState, type LinkedViewStore, MAX_PEER_TABS, MAX_WORKBENCH_GROUPS, type MotionOptions, type MotionStage, PanelBoundary, type PausedVizController, PlotCard, type PlotCardProps, type Provenance, Question, type QuestionProps, type RailSection, Readout, type ReadoutItem, type ReadoutProps, ReferenceList, Refs, Scene3DFrame, type Scene3DFrameProps, type SceneBounds, type SceneCalloutDef, SceneCallouts, type SceneEnvironment, type SelectionOrigin, Stage, type StageSize, Stat, type StatProps, type StoryAccent, StoryFrame, type StoryFrameProps, type StoryStep, type SubTabDef, SubTabs, type SubTabsProps, type SurfaceFocusConfig, SurfacePage, type SurfacePageProps, type TabDef, type TabGroupDef, TabGroups, type TabGroupsProps, Tabs, type TabsProps, type Theme, ThemeToggle, type Tone, type UsePausedVizOptions, VariantBar, type VariantBarProps, type VariantDef, type Vec3, Verdict, type VerdictProps, type ViewFocusConfig, ViewsRow, type ViewsRowProps, type VizFrame, type VizLoop, type VizLoopDeps, type VizLoopOptions, type WorkbenchGroup, WorkbenchLayout, type WorkbenchLayoutProps, type WorkbenchRoute, WorkbenchShell, type WorkbenchShellProps, type WorkbenchState, WorkbenchStateContext, applyTheme, cameraForBounds, caseKind, caseTooltip, casesInSource, createLinkedViewStore, createVizLoop, deckPosition, deckWindow, detailTier, explodedOffsets, firstAppearance, flowSpeed, flyCamera, groupByCategory, isStale, layoutCalloutHeader, layoutSceneCallouts, makeStateKey, perPageFor, prefersReducedMotion, readCaseParam, readTheme, removeQueryParam, replaceQueryParam, runStagedTransition, sceneEnvironment, sourcesPresent, useFocusMode, useLinkedCursor, useLinkedItem, useLinkedView, usePausedViz, useStageSize, useThemeStore, useUrlView, useWorkbenchState, validateExecutive, viewParamOf, withCaseParam };
|
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",
|
|
@@ -4169,6 +4173,15 @@ function StoryFrame({ steps, activeId, onStepChange, onExit, labels = {}, autoAd
|
|
|
4169
4173
|
const activeIndex = Math.max(0, steps.findIndex((step) => step.id === activeId));
|
|
4170
4174
|
const active = steps[activeIndex];
|
|
4171
4175
|
const copy = useMemo(() => ({ ...DEFAULT_LABELS, ...labels }), [labels]);
|
|
4176
|
+
const stepper = useRef(null);
|
|
4177
|
+
useEffect(() => {
|
|
4178
|
+
const row = stepper.current;
|
|
4179
|
+
const link = row?.querySelector('[aria-current="step"]');
|
|
4180
|
+
if (!row || !link) return;
|
|
4181
|
+
const r = row.getBoundingClientRect(), l = link.getBoundingClientRect();
|
|
4182
|
+
if (l.left < r.left) row.scrollLeft -= r.left - l.left;
|
|
4183
|
+
else if (l.right > r.right) row.scrollLeft += Math.min(l.right - r.right, l.left - r.left);
|
|
4184
|
+
}, [activeId]);
|
|
4172
4185
|
useEffect(() => {
|
|
4173
4186
|
if (steps.length < 3 || steps.length > 6) console.error("[caos-app-shell] A showcase story has 3 to 6 steps (ADR-0082 rule 3)");
|
|
4174
4187
|
}, [steps.length]);
|
|
@@ -4232,7 +4245,7 @@ function StoryFrame({ steps, activeId, onStepChange, onExit, labels = {}, autoAd
|
|
|
4232
4245
|
}, children: pick(copy.exit, lang) })
|
|
4233
4246
|
] })
|
|
4234
4247
|
] }),
|
|
4235
|
-
/* @__PURE__ */ jsx("nav", { className: "caos-story-stepper", role: "tablist", "aria-label": pick(copy.steps, lang), children: steps.map((step) => /* @__PURE__ */ jsxs(Link, { role: "tab", "aria-selected": step.id === active.id, "aria-current": step.id === active.id ? "step" : void 0, to: step.href, "data-story-link": step.id, className: step.id === active.id ? "caos-story-step is-active" : "caos-story-step", onClick: () => {
|
|
4248
|
+
/* @__PURE__ */ jsx("nav", { ref: stepper, className: "caos-story-stepper", role: "tablist", "aria-label": pick(copy.steps, lang), children: steps.map((step) => /* @__PURE__ */ jsxs(Link, { role: "tab", "aria-selected": step.id === active.id, "aria-current": step.id === active.id ? "step" : void 0, to: step.href, "data-story-link": step.id, className: step.id === active.id ? "caos-story-step is-active" : "caos-story-step", onClick: () => {
|
|
4236
4249
|
setPlaying(false);
|
|
4237
4250
|
onStepChange(step.id);
|
|
4238
4251
|
}, children: [
|
|
@@ -4250,14 +4263,22 @@ function StoryFrame({ steps, activeId, onStepChange, onExit, labels = {}, autoAd
|
|
|
4250
4263
|
] }, active.id)
|
|
4251
4264
|
] });
|
|
4252
4265
|
}
|
|
4266
|
+
var STATE_LABELS = {
|
|
4267
|
+
alarm: { glyph: "\u25B2", en: "alarm", es: "alarma" },
|
|
4268
|
+
warning: { glyph: "\u25C6", en: "warning", es: "aviso" },
|
|
4269
|
+
normal: { glyph: "\u25CF", en: "normal", es: "normal" },
|
|
4270
|
+
selected: { glyph: "\u25CE", en: "selected", es: "seleccionado" },
|
|
4271
|
+
inactive: { glyph: "\u25CB", en: "inactive", es: "inactivo" }
|
|
4272
|
+
};
|
|
4253
4273
|
var overlaps = (a, b) => a.left < b.left + b.width && a.left + a.width > b.left && a.top < b.top + b.height && a.top + a.height > b.top;
|
|
4254
4274
|
var coversAnchor = (box, x, y, clearance = 8) => x >= box.left - clearance && x <= box.left + box.width + clearance && y >= box.top - clearance && y <= box.top + box.height + clearance;
|
|
4255
|
-
function layoutSceneCallouts(items, width, height, compact = false) {
|
|
4275
|
+
function layoutSceneCallouts(items, width, height, compact = false, heights) {
|
|
4256
4276
|
const w = compact ? 142 : 184;
|
|
4257
|
-
const
|
|
4277
|
+
const nominal = compact ? 56 : 68;
|
|
4258
4278
|
const margin = 8;
|
|
4259
4279
|
const boxes = [];
|
|
4260
4280
|
for (const item of items) {
|
|
4281
|
+
const h = heights?.[item.id] ?? nominal;
|
|
4261
4282
|
const x = Math.max(0, Math.min(width, item.anchor.x * width));
|
|
4262
4283
|
const y = Math.max(0, Math.min(height, item.anchor.y * height));
|
|
4263
4284
|
const d = compact ? 82 : 112;
|
|
@@ -4275,19 +4296,67 @@ function layoutSceneCallouts(items, width, height, compact = false) {
|
|
|
4275
4296
|
}
|
|
4276
4297
|
return boxes;
|
|
4277
4298
|
}
|
|
4278
|
-
function
|
|
4279
|
-
const
|
|
4280
|
-
const
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4299
|
+
function leadersCross(a, b) {
|
|
4300
|
+
const [x1, y1, x2, y2] = [a.anchorX, a.anchorY, a.left + a.width / 2, a.top + a.height / 2];
|
|
4301
|
+
const [x3, y3, x4, y4] = [b.anchorX, b.anchorY, b.left + b.width / 2, b.top + b.height / 2];
|
|
4302
|
+
const d = (x2 - x1) * (y4 - y3) - (y2 - y1) * (x4 - x3);
|
|
4303
|
+
if (Math.abs(d) < 1e-9) return false;
|
|
4304
|
+
const t2 = ((x3 - x1) * (y4 - y3) - (y3 - y1) * (x4 - x3)) / d;
|
|
4305
|
+
const u = ((x3 - x1) * (y2 - y1) - (y3 - y1) * (x2 - x1)) / d;
|
|
4306
|
+
return t2 > 1e-6 && t2 < 1 - 1e-6 && u > 1e-6 && u < 1 - 1e-6;
|
|
4307
|
+
}
|
|
4308
|
+
function* orders(n, prefix = []) {
|
|
4309
|
+
if (prefix.length === n) {
|
|
4310
|
+
yield prefix;
|
|
4311
|
+
return;
|
|
4312
|
+
}
|
|
4313
|
+
for (let i = 0; i < n; i += 1) if (!prefix.includes(i)) yield* orders(n, [...prefix, i]);
|
|
4286
4314
|
}
|
|
4287
|
-
function
|
|
4315
|
+
function layoutCalloutHeader(items, width, height, compact = false, heights) {
|
|
4316
|
+
const nominalW = compact ? 164 : 184, nominalH = compact ? 54 : 68, margin = 8, gap = compact ? 6 : 8;
|
|
4317
|
+
const minW = compact ? 140 : nominalW;
|
|
4318
|
+
const sorted = items.map((item, index) => ({ item, index })).sort((a, b) => a.item.anchor.x - b.item.anchor.x || a.index - b.index).map(({ item }) => item);
|
|
4319
|
+
if (!sorted.length) return [];
|
|
4320
|
+
const columns = Math.max(1, Math.min(sorted.length, Math.floor((width - margin * 2 + gap) / (minW + gap))));
|
|
4321
|
+
const place = (slots) => {
|
|
4322
|
+
const boxes = [];
|
|
4323
|
+
let top = margin;
|
|
4324
|
+
for (let start = 0; start < slots.length; start += columns) {
|
|
4325
|
+
const row = slots.slice(start, start + columns);
|
|
4326
|
+
const cell = (width - margin * 2 - gap * (row.length - 1)) / row.length;
|
|
4327
|
+
const w = Math.max(0, Math.min(nominalW, cell));
|
|
4328
|
+
const rowHeight = Math.max(...row.map((item) => heights?.[item.id] ?? nominalH));
|
|
4329
|
+
row.forEach((item, column) => boxes.push({ id: item.id, left: margin + column * (cell + gap) + (cell - w) / 2, top, width: w, height: heights?.[item.id] ?? nominalH, anchorX: item.anchor.x * width, anchorY: item.anchor.y * height }));
|
|
4330
|
+
top += rowHeight + gap;
|
|
4331
|
+
}
|
|
4332
|
+
return boxes;
|
|
4333
|
+
};
|
|
4334
|
+
const crossings = (boxes) => {
|
|
4335
|
+
let count = 0;
|
|
4336
|
+
for (let i = 0; i < boxes.length; i += 1) for (let j = i + 1; j < boxes.length; j += 1) if (leadersCross(boxes[i], boxes[j])) count += 1;
|
|
4337
|
+
return count;
|
|
4338
|
+
};
|
|
4339
|
+
let best = place(sorted);
|
|
4340
|
+
let fewest = crossings(best);
|
|
4341
|
+
if (fewest > 0 && sorted.length <= 6) {
|
|
4342
|
+
for (const order of orders(sorted.length)) {
|
|
4343
|
+
const boxes = place(order.map((i) => sorted[i]));
|
|
4344
|
+
const count = crossings(boxes);
|
|
4345
|
+
if (count < fewest) {
|
|
4346
|
+
best = boxes;
|
|
4347
|
+
fewest = count;
|
|
4348
|
+
if (count === 0) break;
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4352
|
+
const at = new Map(best.map((box) => [box.id, box]));
|
|
4353
|
+
return sorted.map((item) => at.get(item.id));
|
|
4354
|
+
}
|
|
4355
|
+
function SceneCallouts({ items, className, placement = "anchored", onLayout }) {
|
|
4288
4356
|
const lang = useShellLang();
|
|
4289
4357
|
const host = useRef(null);
|
|
4290
4358
|
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
4359
|
+
const [heights, setHeights] = useState({});
|
|
4291
4360
|
useEffect(() => {
|
|
4292
4361
|
const node = host.current;
|
|
4293
4362
|
if (!node) return;
|
|
@@ -4298,15 +4367,44 @@ function SceneCallouts({ items, className, placement = "anchored" }) {
|
|
|
4298
4367
|
observer.observe(node);
|
|
4299
4368
|
return () => observer.disconnect();
|
|
4300
4369
|
}, []);
|
|
4370
|
+
const ids = items.map((item) => item.id).join("\0");
|
|
4371
|
+
useEffect(() => {
|
|
4372
|
+
const node = host.current;
|
|
4373
|
+
if (!node) return;
|
|
4374
|
+
const observer = new ResizeObserver((entries) => setHeights((previous) => {
|
|
4375
|
+
let next = previous;
|
|
4376
|
+
for (const entry of entries) {
|
|
4377
|
+
const id = entry.target.dataset.caosCallout;
|
|
4378
|
+
const measured2 = entry.borderBoxSize?.[0]?.blockSize ?? entry.target.offsetHeight;
|
|
4379
|
+
if (id === void 0 || !(measured2 > 0) || Math.abs((previous[id] ?? -1) - measured2) <= 0.25) continue;
|
|
4380
|
+
if (next === previous) next = { ...previous };
|
|
4381
|
+
next[id] = measured2;
|
|
4382
|
+
}
|
|
4383
|
+
return next;
|
|
4384
|
+
}));
|
|
4385
|
+
node.querySelectorAll("[data-caos-callout]").forEach((card) => observer.observe(card));
|
|
4386
|
+
return () => observer.disconnect();
|
|
4387
|
+
}, [ids, placement]);
|
|
4388
|
+
const compact = size.width < 640;
|
|
4301
4389
|
const boxes = useMemo(() => {
|
|
4302
|
-
if (placement === "anchored") return layoutSceneCallouts(items, size.width, size.height,
|
|
4303
|
-
return layoutCalloutHeader(items, size.width, size.height,
|
|
4304
|
-
}, [items, placement, size]);
|
|
4305
|
-
|
|
4390
|
+
if (placement === "anchored") return layoutSceneCallouts(items, size.width, size.height, compact, heights);
|
|
4391
|
+
return layoutCalloutHeader(items, size.width, size.height, compact, heights);
|
|
4392
|
+
}, [items, placement, size, compact, heights]);
|
|
4393
|
+
const nominalH = placement === "anchored" ? compact ? 56 : 68 : compact ? 54 : 68;
|
|
4394
|
+
const bandHeight = placement === "header" && boxes.length ? Math.max(...boxes.map((box) => box.top + box.height)) : 0;
|
|
4395
|
+
const measured = items.every((item) => heights[item.id] !== void 0);
|
|
4396
|
+
const report = useRef(onLayout);
|
|
4397
|
+
report.current = onLayout;
|
|
4398
|
+
useEffect(() => {
|
|
4399
|
+
if (placement === "header" && size.width > 0 && measured) report.current?.({ bandHeight });
|
|
4400
|
+
}, [placement, size.width, measured, bandHeight]);
|
|
4401
|
+
const style = placement === "header" ? { "--caos-callout-band": `${bandHeight}px` } : void 0;
|
|
4402
|
+
return /* @__PURE__ */ jsxs("div", { ref: host, className: ["caos-callout-layer", className].filter(Boolean).join(" "), style, "aria-label": lang === "es" ? "Valores fijados a los componentes" : "Values anchored to components", "data-caos-callout-layer": "", "data-placement": placement, children: [
|
|
4306
4403
|
/* @__PURE__ */ jsx("svg", { className: "caos-callout-leaders", "aria-hidden": "true", viewBox: `0 0 ${Math.max(1, size.width)} ${Math.max(1, size.height)}`, preserveAspectRatio: "none", children: boxes.map((box) => /* @__PURE__ */ jsx("line", { x1: box.anchorX, y1: box.anchorY, x2: box.left + box.width / 2, y2: box.top + box.height / 2, "data-caos-callout-leader": box.id }, box.id)) }),
|
|
4307
4404
|
boxes.map((box) => {
|
|
4308
4405
|
const item = items.find((candidate) => candidate.id === box.id);
|
|
4309
|
-
|
|
4406
|
+
const state = Object.prototype.hasOwnProperty.call(STATE_LABELS, item.state) ? STATE_LABELS[item.state] : null;
|
|
4407
|
+
return /* @__PURE__ */ jsxs("article", { className: "caos-scene-callout", "data-caos-callout": item.id, "data-callout-value": item.value, "data-callout-unit": pick(item.unit, lang), "data-callout-state": item.state, "data-callout-source": pick(item.source, lang), "data-callout-anchor-x": item.anchor.x, "data-callout-anchor-y": item.anchor.y, style: { left: box.left, top: box.top, width: box.width, minHeight: nominalH }, children: [
|
|
4310
4408
|
/* @__PURE__ */ jsx("span", { className: "caos-callout-eyebrow", title: pick(item.eyebrow, lang), children: pick(item.eyebrow, lang) }),
|
|
4311
4409
|
/* @__PURE__ */ jsxs("strong", { title: `${item.value} ${pick(item.unit, lang)}`, children: [
|
|
4312
4410
|
item.value,
|
|
@@ -4314,6 +4412,10 @@ function SceneCallouts({ items, className, placement = "anchored" }) {
|
|
|
4314
4412
|
/* @__PURE__ */ jsx("small", { children: pick(item.unit, lang) })
|
|
4315
4413
|
] }),
|
|
4316
4414
|
item.trend && /* @__PURE__ */ jsx("span", { className: `caos-callout-trend trend-${item.trend}`, "aria-label": item.trend, children: item.trend === "up" ? "\u2191" : item.trend === "down" ? "\u2193" : "\u2192" }),
|
|
4415
|
+
state && /* @__PURE__ */ jsxs("span", { className: "caos-callout-state", "data-callout-state-label": "", children: [
|
|
4416
|
+
/* @__PURE__ */ jsx("span", { className: "caos-callout-state-glyph", "aria-hidden": "true", "data-callout-state-glyph": "", children: state.glyph }),
|
|
4417
|
+
state[lang]
|
|
4418
|
+
] }),
|
|
4317
4419
|
/* @__PURE__ */ jsx("small", { className: "caos-callout-source", title: pick(item.source, lang), children: pick(item.source, lang) })
|
|
4318
4420
|
] }, box.id);
|
|
4319
4421
|
})
|
|
@@ -4453,4 +4555,4 @@ function sceneEnvironment(theme) {
|
|
|
4453
4555
|
return theme === "dark" ? { background: "#101a22", ground: "#202b33", key: { color: "#fff0d5", intensity: 2.1, castShadow: true }, fill: { color: "#98c5db", intensity: 0.8 }, ambient: { color: "#c9d7df", intensity: 1.1 } } : { background: "#e9eef0", ground: "#d6dee1", key: { color: "#fff8e9", intensity: 1.7, castShadow: true }, fill: { color: "#8eabb9", intensity: 0.65 }, ambient: { color: "#ffffff", intensity: 1.35 } };
|
|
4454
4556
|
}
|
|
4455
4557
|
|
|
4456
|
-
export { AppShell, ArchitectureModal, BREAKPOINTS, BarChart, Callout, CardDeck, CaseSelector, CaseWorkbench, ChipGroup, CitationsProvider, Cite, DECK_TEXT, DOC_AREAS, DocPage, DocReferences, DocRefsScope, DocSection, EXECUTIVE_SCHEMA_VERSION, Equation, ExecutiveView, FLIP_LIMITS, FLIP_TEXT, FOCUS_PARAM, FOCUS_TEXT, FOUR_CS, Figure, FlipCard, FocusShell, Gauge, Glossary, InlineMath, Knob, Landing, LaneBadge, LanguageToggle, LinkedViewProvider, MAX_PEER_TABS, MAX_WORKBENCH_GROUPS, PanelBoundary, PlotCard, Question, Readout, ReferenceList, Refs, Scene3DFrame, SceneCallouts, Stat, StoryFrame, SubTabs, SurfacePage, TabGroups, Tabs, ThemeToggle, VariantBar, Verdict, ViewsRow, WorkbenchLayout, WorkbenchShell, WorkbenchStateContext, cameraForBounds, caseKind, caseTooltip, casesInSource, createLinkedViewStore, createVizLoop, deckPosition, deckWindow, detailTier, explodedOffsets, firstAppearance, flowSpeed, flyCamera, groupByCategory, isStale, layoutSceneCallouts, makeStateKey, perPageFor, prefersReducedMotion, readCaseParam, removeQueryParam, replaceQueryParam, runStagedTransition, sceneEnvironment, sourcesPresent, useFocusMode, useLinkedCursor, useLinkedItem, useLinkedView, usePausedViz, useUrlView, useWorkbenchState, validateArchitectureConfig, validateExecutive, viewParamOf, withCaseParam };
|
|
4558
|
+
export { AppShell, ArchitectureModal, BREAKPOINTS, BarChart, Callout, CardDeck, CaseSelector, CaseWorkbench, ChipGroup, CitationsProvider, Cite, DECK_TEXT, DOC_AREAS, DocPage, DocReferences, DocRefsScope, DocSection, EXECUTIVE_SCHEMA_VERSION, Equation, ExecutiveView, FLIP_LIMITS, FLIP_TEXT, FOCUS_PARAM, FOCUS_TEXT, FOUR_CS, Figure, FlipCard, FocusShell, Gauge, Glossary, InlineMath, Knob, Landing, LaneBadge, LanguageToggle, LinkedViewProvider, MAX_PEER_TABS, MAX_WORKBENCH_GROUPS, PanelBoundary, PlotCard, Question, Readout, ReferenceList, Refs, Scene3DFrame, SceneCallouts, Stat, StoryFrame, SubTabs, SurfacePage, TabGroups, Tabs, ThemeToggle, VariantBar, Verdict, ViewsRow, WorkbenchLayout, WorkbenchShell, WorkbenchStateContext, cameraForBounds, caseKind, caseTooltip, casesInSource, createLinkedViewStore, createVizLoop, deckPosition, deckWindow, detailTier, explodedOffsets, firstAppearance, flowSpeed, flyCamera, groupByCategory, isStale, layoutCalloutHeader, layoutSceneCallouts, makeStateKey, perPageFor, prefersReducedMotion, readCaseParam, removeQueryParam, replaceQueryParam, runStagedTransition, sceneEnvironment, sourcesPresent, useFocusMode, useLinkedCursor, useLinkedItem, useLinkedView, usePausedViz, useUrlView, useWorkbenchState, validateArchitectureConfig, validateExecutive, viewParamOf, withCaseParam };
|
package/gate/inpage.mjs
CHANGED
|
@@ -1583,6 +1583,8 @@ export function installLib() {
|
|
|
1583
1583
|
const callouts = [...(stage?.querySelectorAll('[data-caos-callout]') ?? [])].filter(visible).map((element) => ({
|
|
1584
1584
|
id: element.getAttribute('data-caos-callout'), value: element.getAttribute('data-callout-value'), unit: element.getAttribute('data-callout-unit'),
|
|
1585
1585
|
state: element.getAttribute('data-callout-state'), source: element.getAttribute('data-callout-source'),
|
|
1586
|
+
// the state as a reader sees it besides the colour (ADR-0082 rule 8): the visible text of the state label, empty when hidden or absent
|
|
1587
|
+
stateLabel: (() => { const label = element.querySelector('[data-callout-state-label]'); return label && visible(label) ? (label.innerText ?? label.textContent ?? '').trim() : ''; })(),
|
|
1586
1588
|
anchor: { x: Number(element.getAttribute('data-callout-anchor-x')), y: Number(element.getAttribute('data-callout-anchor-y')) }, box: box(element),
|
|
1587
1589
|
}));
|
|
1588
1590
|
const links = [...(story?.querySelectorAll('[data-story-link]') ?? [])].filter(visible).map((element) => ({ id: element.getAttribute('data-story-link'), href: element.getAttribute('href'), active: element.getAttribute('aria-selected') === 'true' }));
|
package/gate/run.mjs
CHANGED
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
// G20 chart interactions every chart is one tab stop with an accessible name, described by a table of its values and a one-line summary,
|
|
36
36
|
// and a zoomed chart resets (the + key zooms, the visible control shows, the 0 key resets); a chart that turns zoom
|
|
37
37
|
// off says why (12 letters and digits at least) and is listed among the waivers (0.19.0)
|
|
38
|
+
// G21 showcase story with --require-showcase: a directly linked 3 to 6 step story; its callouts carry value, unit, provenance and
|
|
39
|
+
// a state shown as a word (not by colour alone), leave their own anchor clear, never overlap, stay on the stage
|
|
40
|
+
// and have a leader each, at every size, theme and language of the matrix (0.24.0); interactions, motion and
|
|
41
|
+
// performance at the primary size
|
|
38
42
|
// plus the ADR-0071 and ADR-0017 measures (viewport, one-row tabs, rail without scroll, centred prose, captions,
|
|
39
43
|
// references).
|
|
40
44
|
|
|
@@ -128,6 +132,7 @@ export async function runGate(o) {
|
|
|
128
132
|
const focusReported = new Set(); // G18: an App without a Focus control is reported once per route and mode
|
|
129
133
|
const waivers = []; // ADR-0017 2.1: every waiver a documentation page declares, with its verdict
|
|
130
134
|
const docWalks = []; // the document walk's cost per state (samples and time against the budget it earned), for the report
|
|
135
|
+
const storyContexts = []; // G21: every context (size, theme, language) whose story routes had their callouts measured (0.24.0)
|
|
131
136
|
// The smallest measured values against each floor, so a pass shows its margin (and a CI font change that eats it
|
|
132
137
|
// is visible before it fails).
|
|
133
138
|
const minima = { drawnShare: null, stageFill: null };
|
|
@@ -416,12 +421,41 @@ export async function runGate(o) {
|
|
|
416
421
|
if (stories.length === 0) {
|
|
417
422
|
fail('G21', { route: '/story', mode: 'routes' }, 'no /story/<step> route is declared; ADR-0082 requires a directly addressable guided story');
|
|
418
423
|
} else {
|
|
424
|
+
const STATES = ['alarm', 'warning', 'normal', 'selected', 'inactive'];
|
|
425
|
+
// The callout checks of one story route in one context: content, state shown by more than colour, the own anchor
|
|
426
|
+
// left clear, no overlap, inside the stage, one leader each, no horizontal overflow. They run at every size, theme
|
|
427
|
+
// and language of the gate matrix (known defect 59, 0.24.0): a card grows with its text, so a layout that holds at
|
|
428
|
+
// 1280 in English can overlap on a phone or in Spanish.
|
|
429
|
+
const calloutChecks = async (page, at, f) => {
|
|
430
|
+
for (const call of f.calls) {
|
|
431
|
+
if (!call.value?.trim() || !call.unit?.trim()) fail('G21', at, `callout ${call.id} has no value or unit`);
|
|
432
|
+
if (!STATES.includes(call.state)) fail('G21', at, `callout ${call.id} has no recognised semantic state`);
|
|
433
|
+
// ADR-0082 rule 8: a state is never shown by colour alone (a word or an icon with it)
|
|
434
|
+
else if (!/[\p{L}]/u.test(call.stateLabel ?? '')) fail('G21', at, `callout ${call.id} shows its state by colour alone: no visible data-callout-state-label with its word (ADR-0082 rule 8)`);
|
|
435
|
+
if (!call.source?.trim()) fail('G21', at, `callout ${call.id} has no value provenance`);
|
|
436
|
+
if (![call.anchor?.x, call.anchor?.y].every((v) => Number.isFinite(v) && v >= 0 && v <= 1)) fail('G21', at, `callout ${call.id} has no normalized subject anchor`);
|
|
437
|
+
else if (call.box && f.stage) {
|
|
438
|
+
const x = f.stage.left + call.anchor.x * f.stage.width, y = f.stage.top + call.anchor.y * f.stage.height;
|
|
439
|
+
const clearance = 8;
|
|
440
|
+
if (x >= call.box.left - clearance && x <= call.box.right + clearance && y >= call.box.top - clearance && y <= call.box.bottom + clearance)
|
|
441
|
+
fail('G21', at, `callout ${call.id} covers its own subject anchor`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
for (let i = 0; i < f.calls.length; i += 1) for (let j = i + 1; j < f.calls.length; j += 1) {
|
|
445
|
+
const a = f.calls[i].box, b = f.calls[j].box;
|
|
446
|
+
if (a && b && a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top) fail('G21', at, `callouts ${f.calls[i].id} and ${f.calls[j].id} overlap`);
|
|
447
|
+
}
|
|
448
|
+
if (f.calls.some(({ box }) => !box || !f.stage || box.left < f.stage.left || box.right > f.stage.right || box.top < f.stage.top || box.bottom > f.stage.bottom)) fail('G21', at, 'one or more callouts escape the stage instead of following its subject');
|
|
449
|
+
if (f.calls.length && await page.locator('[data-story] [data-caos-callout-leader]').count() < f.calls.length) fail('G21', at, 'not every scene callout has a leader to its subject anchor');
|
|
450
|
+
if (f.scroll.width > f.viewport.width + 1) fail('G21', at, `story page overflows horizontally (${f.scroll.width}px in ${f.viewport.width}px)`);
|
|
451
|
+
};
|
|
419
452
|
const size = primarySize;
|
|
420
453
|
const [w, h] = size;
|
|
421
454
|
const mode = `${w}x${h} ${themes[0]} ${langs[0]} story`;
|
|
422
455
|
const ctx = await openContext(size, themes[0], langs[0]);
|
|
423
456
|
const { page, sink } = await newPage(ctx);
|
|
424
457
|
const visited = [];
|
|
458
|
+
storyContexts.push({ mode, routes: stories.length });
|
|
425
459
|
for (const route of stories) {
|
|
426
460
|
const at = { route, mode };
|
|
427
461
|
const res = await page.goto(urlFor(route), { waitUntil: 'load', timeout: 30000 }).catch((e) => e);
|
|
@@ -440,28 +474,33 @@ export async function runGate(o) {
|
|
|
440
474
|
if (share < 0.8) fail('G21', at, `story stage and docked panel cover ${(100 * share).toFixed(1)}% of the viewport (floor 80%, ADR-0082)`);
|
|
441
475
|
}
|
|
442
476
|
if (f.calls.length < 3) fail('G21', at, `stage has ${f.calls.length} visible anchored callouts; ADR-0082 requires at least 3`);
|
|
443
|
-
|
|
444
|
-
if (!call.value?.trim() || !call.unit?.trim()) fail('G21', at, `callout ${call.id} has no value or unit`);
|
|
445
|
-
if (!['alarm', 'warning', 'normal', 'selected', 'inactive'].includes(call.state)) fail('G21', at, `callout ${call.id} has no recognised semantic state`);
|
|
446
|
-
if (!call.source?.trim()) fail('G21', at, `callout ${call.id} has no value provenance`);
|
|
447
|
-
if (![call.anchor?.x, call.anchor?.y].every((v) => Number.isFinite(v) && v >= 0 && v <= 1)) fail('G21', at, `callout ${call.id} has no normalized subject anchor`);
|
|
448
|
-
else if (call.box && f.stage) {
|
|
449
|
-
const x = f.stage.left + call.anchor.x * f.stage.width, y = f.stage.top + call.anchor.y * f.stage.height;
|
|
450
|
-
const clearance = 8;
|
|
451
|
-
if (x >= call.box.left - clearance && x <= call.box.right + clearance && y >= call.box.top - clearance && y <= call.box.bottom + clearance)
|
|
452
|
-
fail('G21', at, `callout ${call.id} covers its own subject anchor`);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
for (let i = 0; i < f.calls.length; i += 1) for (let j = i + 1; j < f.calls.length; j += 1) {
|
|
456
|
-
const a = f.calls[i].box, b = f.calls[j].box;
|
|
457
|
-
if (a && b && a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top) fail('G21', at, `callouts ${f.calls[i].id} and ${f.calls[j].id} overlap`);
|
|
458
|
-
}
|
|
459
|
-
if (f.calls.some(({ box }) => !box || box.left < f.stage.left || box.right > f.stage.right || box.top < f.stage.top || box.bottom > f.stage.bottom)) fail('G21', at, 'one or more callouts escape the stage instead of following its subject');
|
|
460
|
-
if (f.calls.length && await page.locator('[data-story] [data-caos-callout-leader]').count() < f.calls.length) fail('G21', at, 'not every scene callout has a leader to its subject anchor');
|
|
461
|
-
if (f.scroll.width > f.viewport.width + 1) fail('G21', at, `story page overflows horizontally (${f.scroll.width}px in ${f.viewport.width}px)`);
|
|
477
|
+
await calloutChecks(page, at, f);
|
|
462
478
|
visited.push({ route, step: f.step, hrefs: f.links.map((x) => x.href) });
|
|
463
479
|
drain(sink, at);
|
|
464
480
|
}
|
|
481
|
+
// Every other context of the gate matrix: the callout checks only. The performance and interaction checks below stay
|
|
482
|
+
// at the primary size. Console errors of these loads are not drained here: the main walk loads the same routes in the
|
|
483
|
+
// same contexts and reports them there once.
|
|
484
|
+
for (const s of sizes) for (const theme of themes) for (const lang of langs) {
|
|
485
|
+
if (s === primarySize && theme === themes[0] && lang === langs[0]) continue;
|
|
486
|
+
const smode = `${s[0]}x${s[1]} ${theme} ${lang} story`;
|
|
487
|
+
const sctx = await openContext(s, theme, lang);
|
|
488
|
+
const { page: sp, sink: ssink } = await newPage(sctx);
|
|
489
|
+
for (const route of stories) {
|
|
490
|
+
const at = { route, mode: smode };
|
|
491
|
+
const res = await sp.goto(urlFor(route), { waitUntil: 'load', timeout: 30000 }).catch((e) => e);
|
|
492
|
+
const status = res && typeof res.status === 'function' ? res.status() : `error ${res?.message ?? res}`;
|
|
493
|
+
if (status !== 200) { fail('G21', at, `direct story link answered ${status}`); ssink.splice(0); continue; }
|
|
494
|
+
const settled = await settle(sp);
|
|
495
|
+
if (settled) fail('G21', at, `story did not settle: ${settled}`);
|
|
496
|
+
const f = await sp.evaluate(() => window.__caosGate.showcaseFacts());
|
|
497
|
+
if (!f.present) fail('G21', at, 'route does not render [data-story]');
|
|
498
|
+
else await calloutChecks(sp, at, f);
|
|
499
|
+
ssink.splice(0);
|
|
500
|
+
}
|
|
501
|
+
storyContexts.push({ mode: smode, routes: stories.length });
|
|
502
|
+
await sctx.close();
|
|
503
|
+
}
|
|
465
504
|
const first = visited.find((x) => x.hrefs.length >= 3);
|
|
466
505
|
if (first) {
|
|
467
506
|
const hrefs = [...new Set(first.hrefs.map((href) => routeOf(new URL(href, base).href)).filter(Boolean))];
|
|
@@ -1290,6 +1329,7 @@ export async function runGate(o) {
|
|
|
1290
1329
|
minima,
|
|
1291
1330
|
waivers,
|
|
1292
1331
|
docWalks,
|
|
1332
|
+
storyContexts,
|
|
1293
1333
|
checks,
|
|
1294
1334
|
captures: captures.length,
|
|
1295
1335
|
failures,
|
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",
|
package/reserved-classes.json
CHANGED
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
"caos-callout-layer",
|
|
35
35
|
"caos-callout-leaders",
|
|
36
36
|
"caos-callout-source",
|
|
37
|
+
"caos-callout-state",
|
|
38
|
+
"caos-callout-state-glyph",
|
|
37
39
|
"caos-callout-trend",
|
|
38
40
|
"caos-card",
|
|
39
41
|
"caos-chart",
|
|
@@ -392,6 +394,8 @@
|
|
|
392
394
|
"caos-callout-layer",
|
|
393
395
|
"caos-callout-leaders",
|
|
394
396
|
"caos-callout-source",
|
|
397
|
+
"caos-callout-state",
|
|
398
|
+
"caos-callout-state-glyph",
|
|
395
399
|
"caos-callout-trend",
|
|
396
400
|
"caos-card",
|
|
397
401
|
"caos-chart",
|
package/styles.css
CHANGED
|
@@ -1025,13 +1025,18 @@ svg.lucide { flex: none; }
|
|
|
1025
1025
|
.caos-scene-callout[data-callout-state="warning"] { border-left-color: var(--color-warn); }
|
|
1026
1026
|
.caos-scene-callout[data-callout-state="normal"] { border-left-color: var(--color-good); }
|
|
1027
1027
|
.caos-scene-callout[data-callout-state="inactive"] { border-left-color: var(--color-fg-faint); }
|
|
1028
|
-
|
|
1028
|
+
/* The eyebrow wraps instead of ending in an ellipsis (0.24.0): the cards are laid out on their measured heights, so a longer
|
|
1029
|
+
Spanish eyebrow ("Alimentación del molino" in a 164 px card) is read whole and the card grows. */
|
|
1030
|
+
.caos-callout-eyebrow { min-width: 0; color: var(--color-fg); font: 700 .68rem/1.25 var(--font-mono); letter-spacing: .05em; overflow-wrap: anywhere; }
|
|
1029
1031
|
.caos-scene-callout > strong { overflow: hidden; font: 700 .92rem var(--font-mono); font-variant-numeric: tabular-nums; text-overflow: ellipsis; white-space: nowrap; }
|
|
1030
1032
|
.caos-scene-callout > strong small,.caos-callout-source { color: var(--color-fg); font-size: .68rem; font-weight: 500; }
|
|
1031
1033
|
.caos-callout-trend { grid-column: 2; grid-row: 1 / span 2; align-self: center; font-size: 1.1rem; font-weight: 800; }
|
|
1032
1034
|
.caos-callout-trend.trend-up { color: var(--color-bad); }
|
|
1033
1035
|
.caos-callout-trend.trend-down { color: var(--color-good); }
|
|
1034
1036
|
.caos-callout-source { grid-column: 1 / -1; }
|
|
1037
|
+
/* The state as a word with a glyph of its own shape (ADR-0082 rule 8, known defect 58): the border colour is not the only sign. */
|
|
1038
|
+
.caos-callout-state { grid-column: 1 / -1; display: flex; align-items: center; gap: .3em; min-width: 0; color: var(--color-fg); font: 700 .64rem/1.2 var(--font-mono); letter-spacing: .04em; white-space: nowrap; }
|
|
1039
|
+
.caos-callout-state-glyph { flex: none; font-size: .95em; line-height: 1; }
|
|
1035
1040
|
@media (max-width: 760px) {
|
|
1036
1041
|
.app-shell.immersive { height: auto; min-height: 100dvh; overflow: visible; }
|
|
1037
1042
|
.app-shell.immersive > .page { display: block; overflow: visible; }
|
|
@@ -1043,7 +1048,13 @@ svg.lucide { flex: none; }
|
|
|
1043
1048
|
.caos-story-actions { width: 100%; flex-wrap: wrap; justify-content: space-between; gap: .35rem; }
|
|
1044
1049
|
.caos-story-controls { flex: 1 1 auto; justify-content: flex-start; gap: .35rem; }
|
|
1045
1050
|
.caos-story button { min-height: 34px; padding: .3rem .5rem; font-size: .78rem; white-space: nowrap; }
|
|
1046
|
-
|
|
1051
|
+
/* every verb whole on a phone (review m4 of 0.23.2): a step is at least as wide as its number and verb, the row scrolls on x and
|
|
1052
|
+
StoryFrame keeps the active step inside it */
|
|
1053
|
+
.caos-story-stepper { container-type: inline-size; grid-auto-columns: minmax(max-content, 1fr); grid-auto-flow: column; grid-template-columns: unset; overflow-x: auto; }
|
|
1054
|
+
/* a step is never wider than its row (100cqi of the stepper): a verb longer than the row wraps inside it, so End always shows the
|
|
1055
|
+
last step whole, whatever width the platform's font gives the verb (Linux CI's system font overflowed a 320 px row in 0.24.0's first candidate) */
|
|
1056
|
+
.caos-story-step { max-width: 100cqi; }
|
|
1057
|
+
.caos-story-step-verb { overflow: visible; text-overflow: clip; white-space: normal; overflow-wrap: anywhere; }
|
|
1047
1058
|
.caos-story-step-headline { display: none; }
|
|
1048
1059
|
.caos-story-layout { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(max(46dvh, var(--caos-story-stage-minimum, 300px)), auto) auto; }
|
|
1049
1060
|
.caos-story-panel { max-height: 30dvh; }
|