@agent-ui-kit/components 0.0.3
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/base-styles.css +8 -0
- package/dist/component-styles.css +147 -0
- package/dist/controls/_base/index.d.ts +3 -0
- package/dist/controls/_base/index.js +9 -0
- package/dist/controls/_base/indicator-element.d.ts +132 -0
- package/dist/controls/_base/indicator-element.js +158 -0
- package/dist/controls/_base/listbox-element.d.ts +70 -0
- package/dist/controls/_base/listbox-element.js +74 -0
- package/dist/controls/_base/range-element.d.ts +120 -0
- package/dist/controls/_base/range-element.js +158 -0
- package/dist/controls/_surface/container-box.css +301 -0
- package/dist/controls/_surface/container.css +156 -0
- package/dist/controls/_token-surface/token-surface.d.ts +35 -0
- package/dist/controls/_token-surface/token-surface.js +97 -0
- package/dist/controls/attachment/attachment-meta.d.ts +8 -0
- package/dist/controls/attachment/attachment-meta.js +84 -0
- package/dist/controls/attachment/attachment.css +91 -0
- package/dist/controls/attachment/attachment.d.ts +40 -0
- package/dist/controls/attachment/attachment.js +70 -0
- package/dist/controls/avatar/avatar-initials.d.ts +4 -0
- package/dist/controls/avatar/avatar-initials.js +30 -0
- package/dist/controls/avatar/avatar.css +87 -0
- package/dist/controls/avatar/avatar.d.ts +40 -0
- package/dist/controls/avatar/avatar.js +77 -0
- package/dist/controls/badge/badge.css +175 -0
- package/dist/controls/badge/badge.d.ts +36 -0
- package/dist/controls/badge/badge.js +71 -0
- package/dist/controls/bar-chart/bar-chart.css +110 -0
- package/dist/controls/bar-chart/bar-chart.d.ts +26 -0
- package/dist/controls/bar-chart/bar-chart.js +80 -0
- package/dist/controls/bar-chart/bar-math.d.ts +31 -0
- package/dist/controls/bar-chart/bar-math.js +106 -0
- package/dist/controls/button/button.css +263 -0
- package/dist/controls/button/button.d.ts +61 -0
- package/dist/controls/button/button.js +115 -0
- package/dist/controls/calendar/calendar.css +495 -0
- package/dist/controls/calendar/calendar.d.ts +161 -0
- package/dist/controls/calendar/calendar.js +1004 -0
- package/dist/controls/card/card-content.d.ts +24 -0
- package/dist/controls/card/card-content.js +133 -0
- package/dist/controls/card/card-footer.d.ts +3 -0
- package/dist/controls/card/card-footer.js +16 -0
- package/dist/controls/card/card-header.d.ts +3 -0
- package/dist/controls/card/card-header.js +18 -0
- package/dist/controls/card/card.css +445 -0
- package/dist/controls/card/card.d.ts +51 -0
- package/dist/controls/card/card.js +61 -0
- package/dist/controls/checkbox/checkbox.css +204 -0
- package/dist/controls/checkbox/checkbox.d.ts +114 -0
- package/dist/controls/checkbox/checkbox.js +130 -0
- package/dist/controls/code/code.css +72 -0
- package/dist/controls/code/code.d.ts +23 -0
- package/dist/controls/code/code.js +40 -0
- package/dist/controls/color-picker/color-picker.css +209 -0
- package/dist/controls/color-picker/color-picker.d.ts +84 -0
- package/dist/controls/color-picker/color-picker.js +545 -0
- package/dist/controls/color-picker/color.d.ts +40 -0
- package/dist/controls/color-picker/color.js +159 -0
- package/dist/controls/column/column.css +151 -0
- package/dist/controls/column/column.d.ts +107 -0
- package/dist/controls/column/column.js +57 -0
- package/dist/controls/combo-box/combo-box.css +397 -0
- package/dist/controls/combo-box/combo-box.d.ts +133 -0
- package/dist/controls/combo-box/combo-box.js +680 -0
- package/dist/controls/command-modal/command-modal.css +226 -0
- package/dist/controls/command-modal/command-modal.d.ts +42 -0
- package/dist/controls/command-modal/command-modal.js +315 -0
- package/dist/controls/disclosure/disclosure.css +162 -0
- package/dist/controls/disclosure/disclosure.d.ts +37 -0
- package/dist/controls/disclosure/disclosure.js +207 -0
- package/dist/controls/field/field.css +140 -0
- package/dist/controls/field/field.d.ts +27 -0
- package/dist/controls/field/field.js +228 -0
- package/dist/controls/form-provider/form-provider.css +40 -0
- package/dist/controls/form-provider/form-provider.d.ts +42 -0
- package/dist/controls/form-provider/form-provider.js +105 -0
- package/dist/controls/grid/grid.css +72 -0
- package/dist/controls/grid/grid.d.ts +60 -0
- package/dist/controls/grid/grid.js +56 -0
- package/dist/controls/icon/icon.css +37 -0
- package/dist/controls/icon/icon.d.ts +25 -0
- package/dist/controls/icon/icon.js +55 -0
- package/dist/controls/index.d.ts +57 -0
- package/dist/controls/index.js +136 -0
- package/dist/controls/ladder/ladder.css +89 -0
- package/dist/controls/ladder/ladder.d.ts +27 -0
- package/dist/controls/ladder/ladder.js +78 -0
- package/dist/controls/list/list.css +118 -0
- package/dist/controls/list/list.d.ts +84 -0
- package/dist/controls/list/list.js +48 -0
- package/dist/controls/menu/menu.css +195 -0
- package/dist/controls/menu/menu.d.ts +44 -0
- package/dist/controls/menu/menu.js +248 -0
- package/dist/controls/modal/modal.css +99 -0
- package/dist/controls/modal/modal.d.ts +61 -0
- package/dist/controls/modal/modal.js +155 -0
- package/dist/controls/popover/popover.css +83 -0
- package/dist/controls/popover/popover.d.ts +44 -0
- package/dist/controls/popover/popover.js +164 -0
- package/dist/controls/progress/progress.css +106 -0
- package/dist/controls/progress/progress.d.ts +28 -0
- package/dist/controls/progress/progress.js +80 -0
- package/dist/controls/radio/radio-group.css +84 -0
- package/dist/controls/radio/radio-group.d.ts +152 -0
- package/dist/controls/radio/radio-group.js +350 -0
- package/dist/controls/radio/radio.css +219 -0
- package/dist/controls/radio/radio.d.ts +19 -0
- package/dist/controls/radio/radio.js +75 -0
- package/dist/controls/ramp/ramp.css +75 -0
- package/dist/controls/ramp/ramp.d.ts +29 -0
- package/dist/controls/ramp/ramp.js +72 -0
- package/dist/controls/row/row.css +152 -0
- package/dist/controls/row/row.d.ts +95 -0
- package/dist/controls/row/row.js +45 -0
- package/dist/controls/segment/segment.css +66 -0
- package/dist/controls/segment/segment.d.ts +3 -0
- package/dist/controls/segment/segment.js +26 -0
- package/dist/controls/segmented-control/segmented-control.css +189 -0
- package/dist/controls/segmented-control/segmented-control.d.ts +20 -0
- package/dist/controls/segmented-control/segmented-control.js +64 -0
- package/dist/controls/select/select.css +467 -0
- package/dist/controls/select/select.d.ts +147 -0
- package/dist/controls/select/select.js +538 -0
- package/dist/controls/slider/slider.css +231 -0
- package/dist/controls/slider/slider.d.ts +111 -0
- package/dist/controls/slider/slider.js +60 -0
- package/dist/controls/slider-multi/slider-multi.css +197 -0
- package/dist/controls/slider-multi/slider-multi.d.ts +149 -0
- package/dist/controls/slider-multi/slider-multi.js +380 -0
- package/dist/controls/sparkline/sparkline-math.d.ts +23 -0
- package/dist/controls/sparkline/sparkline-math.js +79 -0
- package/dist/controls/sparkline/sparkline.css +61 -0
- package/dist/controls/sparkline/sparkline.d.ts +27 -0
- package/dist/controls/sparkline/sparkline.js +74 -0
- package/dist/controls/split/constrain.d.ts +44 -0
- package/dist/controls/split/constrain.js +126 -0
- package/dist/controls/split/split-pane.css +63 -0
- package/dist/controls/split/split-pane.d.ts +49 -0
- package/dist/controls/split/split-pane.js +42 -0
- package/dist/controls/split/split.css +127 -0
- package/dist/controls/split/split.d.ts +42 -0
- package/dist/controls/split/split.js +364 -0
- package/dist/controls/stat/stat-model.d.ts +22 -0
- package/dist/controls/stat/stat-model.js +69 -0
- package/dist/controls/stat/stat.css +101 -0
- package/dist/controls/stat/stat.d.ts +30 -0
- package/dist/controls/stat/stat.js +73 -0
- package/dist/controls/status-stream/status-stream.css +151 -0
- package/dist/controls/status-stream/status-stream.d.ts +108 -0
- package/dist/controls/status-stream/status-stream.js +417 -0
- package/dist/controls/swatch/swatch.css +66 -0
- package/dist/controls/swatch/swatch.d.ts +28 -0
- package/dist/controls/swatch/swatch.js +74 -0
- package/dist/controls/swiper/swiper-item.css +17 -0
- package/dist/controls/swiper/swiper-item.d.ts +31 -0
- package/dist/controls/swiper/swiper-item.js +47 -0
- package/dist/controls/swiper/swiper-label.css +10 -0
- package/dist/controls/swiper/swiper-label.d.ts +4 -0
- package/dist/controls/swiper/swiper-label.js +20 -0
- package/dist/controls/swiper/swiper-paddles.css +26 -0
- package/dist/controls/swiper/swiper-paddles.d.ts +10 -0
- package/dist/controls/swiper/swiper-paddles.js +62 -0
- package/dist/controls/swiper/swiper-pagination.css +60 -0
- package/dist/controls/swiper/swiper-pagination.d.ts +31 -0
- package/dist/controls/swiper/swiper-pagination.js +78 -0
- package/dist/controls/swiper/swiper.css +153 -0
- package/dist/controls/swiper/swiper.d.ts +158 -0
- package/dist/controls/swiper/swiper.js +659 -0
- package/dist/controls/switch/switch.css +194 -0
- package/dist/controls/switch/switch.d.ts +5 -0
- package/dist/controls/switch/switch.js +18 -0
- package/dist/controls/table/table-model.d.ts +43 -0
- package/dist/controls/table/table-model.js +124 -0
- package/dist/controls/table/table.css +90 -0
- package/dist/controls/table/table.d.ts +29 -0
- package/dist/controls/table/table.js +178 -0
- package/dist/controls/tabs/tab-panel.d.ts +10 -0
- package/dist/controls/tabs/tab-panel.js +89 -0
- package/dist/controls/tabs/tab.d.ts +26 -0
- package/dist/controls/tabs/tab.js +67 -0
- package/dist/controls/tabs/tabs.css +181 -0
- package/dist/controls/tabs/tabs.d.ts +61 -0
- package/dist/controls/tabs/tabs.js +176 -0
- package/dist/controls/text/href.d.ts +26 -0
- package/dist/controls/text/href.js +48 -0
- package/dist/controls/text/text.css +328 -0
- package/dist/controls/text/text.d.ts +85 -0
- package/dist/controls/text/text.js +273 -0
- package/dist/controls/text-field/text-field.css +498 -0
- package/dist/controls/text-field/text-field.d.ts +226 -0
- package/dist/controls/text-field/text-field.js +905 -0
- package/dist/controls/textarea/textarea.css +251 -0
- package/dist/controls/textarea/textarea.d.ts +136 -0
- package/dist/controls/textarea/textarea.js +312 -0
- package/dist/controls/theme-provider/theme-provider.css +48 -0
- package/dist/controls/theme-provider/theme-provider.d.ts +59 -0
- package/dist/controls/theme-provider/theme-provider.js +40 -0
- package/dist/controls/timeline/timeline.css +33 -0
- package/dist/controls/timeline/timeline.d.ts +39 -0
- package/dist/controls/timeline/timeline.js +52 -0
- package/dist/controls/timeline-item/timeline-item.css +495 -0
- package/dist/controls/timeline-item/timeline-item.d.ts +79 -0
- package/dist/controls/timeline-item/timeline-item.js +360 -0
- package/dist/controls/toast/toast-region.css +56 -0
- package/dist/controls/toast/toast-region.d.ts +20 -0
- package/dist/controls/toast/toast-region.js +113 -0
- package/dist/controls/toast/toast.css +96 -0
- package/dist/controls/toast/toast.d.ts +28 -0
- package/dist/controls/toast/toast.js +191 -0
- package/dist/controls/toolbar/toolbar.css +125 -0
- package/dist/controls/toolbar/toolbar.d.ts +108 -0
- package/dist/controls/toolbar/toolbar.js +68 -0
- package/dist/controls/tooltip/tooltip.css +80 -0
- package/dist/controls/tooltip/tooltip.d.ts +57 -0
- package/dist/controls/tooltip/tooltip.js +205 -0
- package/dist/descriptor/component-descriptor.d.ts +112 -0
- package/dist/descriptor/component-descriptor.js +492 -0
- package/dist/descriptor/index.d.ts +1 -0
- package/dist/descriptor/index.js +5 -0
- package/dist/dom/container.d.ts +45 -0
- package/dist/dom/container.js +60 -0
- package/dist/dom/element.d.ts +98 -0
- package/dist/dom/element.js +221 -0
- package/dist/dom/form.d.ts +240 -0
- package/dist/dom/form.js +399 -0
- package/dist/dom/index.d.ts +10 -0
- package/dist/dom/index.js +31 -0
- package/dist/dom/props.d.ts +56 -0
- package/dist/dom/props.js +231 -0
- package/dist/dom/repeat.d.ts +10 -0
- package/dist/dom/repeat.js +202 -0
- package/dist/dom/template.d.ts +161 -0
- package/dist/dom/template.js +634 -0
- package/dist/dom/watch.d.ts +11 -0
- package/dist/dom/watch.js +82 -0
- package/dist/foundation-styles.css +9 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/reactive/graph.d.ts +65 -0
- package/dist/reactive/graph.js +310 -0
- package/dist/reactive/index.d.ts +3 -0
- package/dist/reactive/index.js +4 -0
- package/dist/reactive/scheduler.d.ts +13 -0
- package/dist/reactive/scheduler.js +49 -0
- package/dist/traits/area-drag.d.ts +12 -0
- package/dist/traits/area-drag.js +70 -0
- package/dist/traits/form-registry.d.ts +36 -0
- package/dist/traits/form-registry.js +108 -0
- package/dist/traits/index.d.ts +24 -0
- package/dist/traits/index.js +19 -0
- package/dist/traits/overlay.d.ts +50 -0
- package/dist/traits/overlay.js +277 -0
- package/dist/traits/pane-resize.d.ts +26 -0
- package/dist/traits/pane-resize.js +103 -0
- package/dist/traits/press-activation.d.ts +11 -0
- package/dist/traits/press-activation.js +55 -0
- package/dist/traits/roving-focus.d.ts +52 -0
- package/dist/traits/roving-focus.js +214 -0
- package/dist/traits/scroll-fade.d.ts +28 -0
- package/dist/traits/scroll-fade.js +120 -0
- package/dist/traits/selection-commit.d.ts +30 -0
- package/dist/traits/selection-commit.js +146 -0
- package/dist/traits/tabbable.d.ts +12 -0
- package/dist/traits/tabbable.js +59 -0
- package/dist/traits/track-user-invalid.d.ts +36 -0
- package/dist/traits/track-user-invalid.js +52 -0
- package/dist/traits/value-codec.d.ts +120 -0
- package/dist/traits/value-codec.js +497 -0
- package/dist/traits/value-drag.d.ts +18 -0
- package/dist/traits/value-drag.js +84 -0
- package/package.json +263 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { UIContainerElement, type ReactiveProps } from '../../dom/index.js';
|
|
2
|
+
declare const splitProps: {
|
|
3
|
+
axis: {
|
|
4
|
+
reflect: true;
|
|
5
|
+
type: import("../../index.js").PropType<"horizontal" | "vertical">;
|
|
6
|
+
default: "horizontal" | "vertical";
|
|
7
|
+
attribute?: string | false;
|
|
8
|
+
};
|
|
9
|
+
sizes: {
|
|
10
|
+
attribute: false;
|
|
11
|
+
type: import("../../index.js").PropType<number[] | undefined>;
|
|
12
|
+
default: number[] | undefined;
|
|
13
|
+
reflect?: boolean;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export interface UISplitElement extends ReactiveProps<typeof splitProps> {
|
|
17
|
+
}
|
|
18
|
+
export declare class UISplitElement extends UIContainerElement {
|
|
19
|
+
#private;
|
|
20
|
+
static props: {
|
|
21
|
+
axis: {
|
|
22
|
+
reflect: true;
|
|
23
|
+
type: import("../../index.js").PropType<"horizontal" | "vertical">;
|
|
24
|
+
default: "horizontal" | "vertical";
|
|
25
|
+
attribute?: string | false;
|
|
26
|
+
};
|
|
27
|
+
sizes: {
|
|
28
|
+
attribute: false;
|
|
29
|
+
type: import("../../index.js").PropType<number[] | undefined>;
|
|
30
|
+
default: number[] | undefined;
|
|
31
|
+
reflect?: boolean;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
protected connected(): void;
|
|
35
|
+
protected disconnected(): void;
|
|
36
|
+
/** Expose the live separator elements for test probes (structural ARIA/keyboard-target assertions). */
|
|
37
|
+
protected get separatorElsSeam(): HTMLElement[];
|
|
38
|
+
/** Expose the CURRENT effective ratio vector (controlled `sizes`, reconciled, or the internal uncontrolled
|
|
39
|
+
* vector) for test probes — the sum-invariance / clamp / dynamic-pane assertions read this directly. */
|
|
40
|
+
protected get ratiosSeam(): number[];
|
|
41
|
+
}
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
// split.ts — UISplitElement, the multi-pane (N-slot) resizable split container (app-surfaces-m4.lld.md
|
|
2
|
+
// LLD-C1/C4, SPEC-R1..R5; ADR-0120 cl.2 — the wave's hardest contract). A components-tier Container/layout
|
|
3
|
+
// primitive (sibling to `ui-row`/`ui-column`/`ui-grid`, `tier: layout`, NO control height): lays its
|
|
4
|
+
// `ui-split-pane` children out along one axis, control-managing exactly N−1 draggable + keyboard-resizable
|
|
5
|
+
// ARIA `separator` elements between adjacent pairs (the slider-thumb precedent — the author authors panes,
|
|
6
|
+
// never separators). `formAssociated: false` — a layout container contributes nothing to a form.
|
|
7
|
+
//
|
|
8
|
+
// Sizing model (SPEC-R2): controlled (`sizes` present) — the control RENDERS `sizes` and EMITS the
|
|
9
|
+
// proposed ratios on user resize but never self-mutates it (prop-as-source-of-truth, ADR-0102); uncontrolled
|
|
10
|
+
// (`sizes` undefined) — an internal `#ratios` signal drives the render directly, seeded from each pane's
|
|
11
|
+
// `initial` prop at connect and re-derived on every pane-count change (SPEC-R2 AC5, the dynamic-panes contract
|
|
12
|
+
// — a MutationObserver on `childList`, the `toast-region.ts` precedent). Mid-drag pane-count mutation
|
|
13
|
+
// ABORTS the in-flight drag FIRST (SPEC-R2 M2) via the pane-resize handle's distinct `abortDrag()`.
|
|
14
|
+
//
|
|
15
|
+
// Geometry: a `display: flex` row/column (axis picks the CSS identity via `[axis=vertical]`, split.css);
|
|
16
|
+
// each pane's proportional share rides a JS-written `--_pane-flex` custom property (the slider-multi
|
|
17
|
+
// `--value-pct-lo/-hi` geometry-seam precedent — this file NEVER writes a real layout style property
|
|
18
|
+
// directly, only namespaced custom properties CSS consumes). Two-neighbor redistribution + bounds-in-ratio-
|
|
19
|
+
// space math is entirely `constrain.ts` (DOM-free, pure); this file's job is measuring the live container/
|
|
20
|
+
// pane extents (`getBoundingClientRect`/`getComputedStyle`) and feeding ratio-space bounds into it.
|
|
21
|
+
//
|
|
22
|
+
// `controls → dom + traits + controls/split/split-pane.ts` — inward-only (no deep `packages/**/src`).
|
|
23
|
+
import { signal } from "../../reactive/index.js";
|
|
24
|
+
import { UIContainerElement, prop } from "../../dom/index.js";
|
|
25
|
+
import { paneResize } from "../../traits/pane-resize.js";
|
|
26
|
+
import { UISplitPaneElement } from "./split-pane.js";
|
|
27
|
+
import { redistribute, seedRatios, rederiveRatios, reconcileRatios } from "./constrain.js";
|
|
28
|
+
const splitProps = {
|
|
29
|
+
axis: { ...prop.enum(['horizontal', 'vertical'], 'horizontal'), reflect: true },
|
|
30
|
+
// `sizes` is a PROPERTY, not an attribute — too structured (a JS number[]) to reflect. `attribute: false`
|
|
31
|
+
// means it is never observed/coerced from markup; `prop.json` supplies a codec ONLY so the descriptor
|
|
32
|
+
// trip-wire's structural `kindOf` probe can classify it ('json' — the same array/structured-value
|
|
33
|
+
// classification bar-chart's `data`/sparkline's `values` codecs already establish). undefined ⇒
|
|
34
|
+
// UNCONTROLLED (internal ratios drive the render); present ⇒ CONTROLLED (SPEC-R2 §2).
|
|
35
|
+
sizes: { ...prop.json(undefined), attribute: false },
|
|
36
|
+
};
|
|
37
|
+
let paneIdSeq = 0;
|
|
38
|
+
export class UISplitElement extends UIContainerElement {
|
|
39
|
+
static props = splitProps;
|
|
40
|
+
// Uncontrolled internal ratio vector — a kernel signal so reading it inside `this.effect` tracks it.
|
|
41
|
+
#ratios = signal([]);
|
|
42
|
+
// A structural-change poke the render/ARIA effect reads UNCONDITIONALLY (component-reviewer MEDIUM fix):
|
|
43
|
+
// in UNCONTROLLED mode, #syncPanes's `#ratios` write already re-triggers the effect on its own (a fresh
|
|
44
|
+
// array reference), but in CONTROLLED mode #syncPanes touches NO signal at all when the pane COUNT
|
|
45
|
+
// changes (sizes itself is unchanged) — new separators would render bare (no aria-valuenow/-controls) and
|
|
46
|
+
// the new pane would get no --_pane-flex until `sizes` next changed for an unrelated reason. Bumping this
|
|
47
|
+
// on every real #syncPanes pass (both branches) forces the effect to re-run regardless of which mode is
|
|
48
|
+
// active — the SPEC-R2 × SPEC-R4 intersection dynamic panes must hold in BOTH sizing modes.
|
|
49
|
+
#version = signal(0);
|
|
50
|
+
#observer = null;
|
|
51
|
+
#resizeHandle = null;
|
|
52
|
+
#separatorEls = [];
|
|
53
|
+
#lastPaneCount = null; // null = never synced (connect-time seed path)
|
|
54
|
+
#warnedSizesMismatch = false;
|
|
55
|
+
#collapseMemory = new Map();
|
|
56
|
+
// TKT-0015: the ORIGINAL effective ratios at a drag's first live move, held fixed for every subsequent
|
|
57
|
+
// onResize call in the SAME drag. `pane-resize.ts`'s `deltaRatio` is measured SINCE THE PRESS POINT — an
|
|
58
|
+
// absolute offset from drag start, re-derived fresh on every pointermove, NOT an increment since the last
|
|
59
|
+
// move (see its own doc comment). Applying that press-relative delta against the LIVE, already-mutated
|
|
60
|
+
// ratio vector (as the pre-fix code did) double-counts every prior move's contribution each time — a
|
|
61
|
+
// resize that compounds every pointermove instead of tracking the pointer 1:1 (the perceived "animation").
|
|
62
|
+
// `null` ⟺ no drag currently in flight — doubles as the drag-active flag for `:state(dragging)` below.
|
|
63
|
+
#dragBaseline = null;
|
|
64
|
+
connected() {
|
|
65
|
+
this.#lastPaneCount = null;
|
|
66
|
+
this.#warnedSizesMismatch = false;
|
|
67
|
+
this.#collapseMemory.clear();
|
|
68
|
+
this.#observer = new MutationObserver(() => this.#syncPanes());
|
|
69
|
+
this.#observer.observe(this, { childList: true });
|
|
70
|
+
this.#syncPanes(); // seed from any children already present (declarative markup)
|
|
71
|
+
this.#resizeHandle = paneResize(this, {
|
|
72
|
+
separators: () => this.#separatorEls,
|
|
73
|
+
axis: () => this.axis,
|
|
74
|
+
rtl: () => getComputedStyle(this).direction === 'rtl',
|
|
75
|
+
onResize: (sepIndex, deltaRatio, commit) => this.#applyPointerDelta(sepIndex, deltaRatio, commit),
|
|
76
|
+
});
|
|
77
|
+
this.listen(this, 'keydown', (event) => this.#handleKeydown(event));
|
|
78
|
+
// The one geometry+ARIA effect: re-runs on `axis`/`sizes` changes, on any pane's own `min`/`max` (read
|
|
79
|
+
// per-pane inside the loop below, tracking each one individually), AND on `#version` (bumped by every
|
|
80
|
+
// real #syncPanes pass regardless of sizing mode — the single render pass.
|
|
81
|
+
this.effect(() => this.#render());
|
|
82
|
+
}
|
|
83
|
+
disconnected() {
|
|
84
|
+
this.#observer?.disconnect();
|
|
85
|
+
this.#observer = null;
|
|
86
|
+
this.#resizeHandle?.release(); // ends any in-flight drag WITHOUT a commit — clear the baseline/state to match
|
|
87
|
+
this.#resizeHandle = null;
|
|
88
|
+
this.#dragBaseline = null;
|
|
89
|
+
this.internals.states?.delete('dragging');
|
|
90
|
+
for (const sep of this.#separatorEls)
|
|
91
|
+
sep.remove();
|
|
92
|
+
this.#separatorEls = [];
|
|
93
|
+
}
|
|
94
|
+
// ── pane discovery + separator lifecycle (SPEC-R1, SPEC-R2 AC5) ────────────────────────────────────
|
|
95
|
+
#panes() {
|
|
96
|
+
return [...this.children].filter((el) => el instanceof UISplitPaneElement);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Re-derive the separator set (N−1) + (uncontrolled) ratio vector from the CURRENT pane list. Guarded
|
|
100
|
+
* against re-entrant noise from the separator DOM mutations THIS function itself performs (the
|
|
101
|
+
* MutationObserver watches `childList` on the host, so inserting/removing separators — non-pane children
|
|
102
|
+
* — re-triggers the same callback; since `#panes()` filters to `UISplitPaneElement` only, a separator-
|
|
103
|
+
* only mutation reports an UNCHANGED pane count and the guard below no-ops it, terminating the recursion).
|
|
104
|
+
*/
|
|
105
|
+
#syncPanes() {
|
|
106
|
+
const panes = this.#panes();
|
|
107
|
+
const n = panes.length;
|
|
108
|
+
const isFirst = this.#lastPaneCount === null;
|
|
109
|
+
const countChanged = !isFirst && this.#lastPaneCount !== n;
|
|
110
|
+
if (!isFirst && !countChanged)
|
|
111
|
+
return; // separator-only noise — nothing to re-derive
|
|
112
|
+
for (const pane of panes) {
|
|
113
|
+
if (!pane.id)
|
|
114
|
+
pane.id = `ui-split-pane-${++paneIdSeq}`;
|
|
115
|
+
}
|
|
116
|
+
// Mid-drag mutation (SPEC-R2 M2): abort BEFORE re-deriving — the captured separator index is stale.
|
|
117
|
+
// `abortDrag()` is silent (no `onResize` call, SPEC-R2 M2), so the commit branch in `#applyPointerDelta`
|
|
118
|
+
// never runs to clear `#dragBaseline`/`:state(dragging)` — clear both here instead.
|
|
119
|
+
if (countChanged) {
|
|
120
|
+
this.#resizeHandle?.abortDrag();
|
|
121
|
+
this.#dragBaseline = null;
|
|
122
|
+
this.internals.states?.delete('dragging');
|
|
123
|
+
}
|
|
124
|
+
for (const sep of this.#separatorEls)
|
|
125
|
+
sep.remove();
|
|
126
|
+
this.#separatorEls = [];
|
|
127
|
+
for (let i = 0; i < n - 1; i++) {
|
|
128
|
+
const sep = document.createElement('div');
|
|
129
|
+
sep.setAttribute('data-separator', '');
|
|
130
|
+
sep.setAttribute('role', 'separator');
|
|
131
|
+
sep.setAttribute('tabindex', '0');
|
|
132
|
+
panes[i].after(sep);
|
|
133
|
+
this.#separatorEls.push(sep);
|
|
134
|
+
}
|
|
135
|
+
if (this.sizes === undefined) {
|
|
136
|
+
const seeds = panes.map((p) => p.initial);
|
|
137
|
+
this.#ratios.value = isFirst ? seedRatios(seeds) : rederiveRatios(this.#ratios.value, seeds);
|
|
138
|
+
}
|
|
139
|
+
// Unconditional poke (component-reviewer MEDIUM fix): the uncontrolled branch above already re-triggers
|
|
140
|
+
// the render/ARIA effect via the `#ratios` write, but CONTROLLED mode touches no signal on a pane-count
|
|
141
|
+
// change — bump this every real sync (both branches) so fresh separators always get their ARIA + the
|
|
142
|
+
// new pane always gets its --_pane-flex, regardless of sizing mode.
|
|
143
|
+
this.#version.value++;
|
|
144
|
+
this.#lastPaneCount = n;
|
|
145
|
+
}
|
|
146
|
+
// ── ratio resolution (controlled vs uncontrolled, SPEC-R2 AC3/AC4) ─────────────────────────────────
|
|
147
|
+
#effectiveRatios(panes) {
|
|
148
|
+
const sizes = this.sizes;
|
|
149
|
+
if (sizes !== undefined) {
|
|
150
|
+
if (sizes.length !== panes.length) {
|
|
151
|
+
if (!this.#warnedSizesMismatch) {
|
|
152
|
+
console.warn(`ui-split: "sizes" has ${sizes.length} entries but ${panes.length} pane(s) are present — reconciling`);
|
|
153
|
+
this.#warnedSizesMismatch = true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
this.#warnedSizesMismatch = false;
|
|
158
|
+
}
|
|
159
|
+
return reconcileRatios(sizes, panes.length);
|
|
160
|
+
}
|
|
161
|
+
return this.#ratios.value;
|
|
162
|
+
}
|
|
163
|
+
/** Physical [min,max] bounds in RATIO space, resolved from each pane's live CSS min/max (`getComputedStyle`
|
|
164
|
+
* — the used-value px resolution; degrades gracefully to `[0,1]` when unresolvable, e.g. jsdom's no-layout
|
|
165
|
+
* environment or a zero-extent container, per constrain.ts's own contradictory-bounds tolerance). */
|
|
166
|
+
#boundsFor(panes) {
|
|
167
|
+
const rect = this.getBoundingClientRect();
|
|
168
|
+
const isH = this.axis === 'horizontal';
|
|
169
|
+
const extent = isH ? rect.width : rect.height;
|
|
170
|
+
return panes.map((pane) => {
|
|
171
|
+
if (extent <= 0)
|
|
172
|
+
return [0, 1];
|
|
173
|
+
const cs = getComputedStyle(pane);
|
|
174
|
+
const minRaw = isH ? cs.minWidth : cs.minHeight;
|
|
175
|
+
const maxRaw = isH ? cs.maxWidth : cs.maxHeight;
|
|
176
|
+
const minPx = Number.parseFloat(minRaw) || 0;
|
|
177
|
+
const maxPx = maxRaw === 'none' || maxRaw === '' ? Infinity : Number.parseFloat(maxRaw);
|
|
178
|
+
const minR = Math.min(1, minPx / extent);
|
|
179
|
+
const maxR = Number.isFinite(maxPx) ? Math.min(1, maxPx / extent) : 1;
|
|
180
|
+
return [minR, Math.max(minR, maxR)];
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// ── resize application (keyboard path — see #applyPointerDelta below for the pointer path) ────────────
|
|
184
|
+
/** The keyboard path's delta resolver: a discrete key press is an INCREMENT against the CURRENT/live
|
|
185
|
+
* ratios (correct — unlike the pointer path's since-press delta, TKT-0015), so it reads `#effectiveRatios`
|
|
186
|
+
* fresh on every call rather than a drag-start baseline. */
|
|
187
|
+
#resolveKeyDelta(sepIndex, deltaRatio) {
|
|
188
|
+
const panes = this.#panes();
|
|
189
|
+
return redistribute(this.#effectiveRatios(panes), sepIndex, deltaRatio, this.#boundsFor(panes));
|
|
190
|
+
}
|
|
191
|
+
#commitRatios(next) {
|
|
192
|
+
if (this.sizes === undefined)
|
|
193
|
+
this.#ratios.value = next;
|
|
194
|
+
}
|
|
195
|
+
/** The pane-resize trait's onResize callback (pointer path) — `input` per live move, `change` on commit
|
|
196
|
+
* (SPEC-R3 AC1). Controlled mode never self-mutates `sizes`; the proposed ratios ride the event detail
|
|
197
|
+
* (SPEC-R2 AC3 — the consumer reads `event.detail` and writes `sizes` back to move the rendered layout).
|
|
198
|
+
*
|
|
199
|
+
* TKT-0015 (1:1 pointer tracking): `deltaRatio` is measured SINCE THE PRESS POINT (pane-resize.ts), so it
|
|
200
|
+
* MUST be applied against the ratios captured at the drag's FIRST live move (`#dragBaseline`) — NOT the
|
|
201
|
+
* live/current `#effectiveRatios()` vector (the keyboard path's `#resolveKeyDelta` correctly uses THAT,
|
|
202
|
+
* since a key press is a fresh increment), which already reflects every prior move's application and
|
|
203
|
+
* would double-count it on each subsequent move (the compounding bug: N moves overshoot far past the pointer).
|
|
204
|
+
* The baseline is snapshotted lazily on the first call of a drag and cleared on commit (drag end) — its
|
|
205
|
+
* nullness also gates `:state(dragging)` (TKT-0015 pt.2 — split.css suspends selection while it holds). */
|
|
206
|
+
#applyPointerDelta(sepIndex, deltaRatio, commit) {
|
|
207
|
+
const panes = this.#panes();
|
|
208
|
+
if (this.#dragBaseline === null) {
|
|
209
|
+
this.#dragBaseline = this.#effectiveRatios(panes);
|
|
210
|
+
this.internals.states?.add('dragging');
|
|
211
|
+
}
|
|
212
|
+
const next = redistribute(this.#dragBaseline, sepIndex, deltaRatio, this.#boundsFor(panes));
|
|
213
|
+
this.#commitRatios(next);
|
|
214
|
+
this.emit(commit ? 'change' : 'input', next);
|
|
215
|
+
if (commit) {
|
|
216
|
+
this.#dragBaseline = null;
|
|
217
|
+
this.internals.states?.delete('dragging');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** The keyboard path — a discrete key press is both the live update AND the commit in one atomic action,
|
|
221
|
+
* so it emits BOTH `input` and `change` (the native `<input type=range>` per-keystep parity). */
|
|
222
|
+
#applyKeyDelta(sepIndex, deltaRatio) {
|
|
223
|
+
const next = this.#resolveKeyDelta(sepIndex, deltaRatio);
|
|
224
|
+
this.#commitRatios(next);
|
|
225
|
+
this.emit('input', next);
|
|
226
|
+
this.emit('change', next);
|
|
227
|
+
}
|
|
228
|
+
// ── keyboard (SPEC-R4) ──────────────────────────────────────────────────────────────────────────────
|
|
229
|
+
#keyStepRatio() {
|
|
230
|
+
const raw = getComputedStyle(this).getPropertyValue('--ui-split-key-step').trim();
|
|
231
|
+
if (raw.endsWith('%')) {
|
|
232
|
+
const n = Number.parseFloat(raw);
|
|
233
|
+
if (Number.isFinite(n))
|
|
234
|
+
return n / 100;
|
|
235
|
+
}
|
|
236
|
+
return 0.05; // the fallback default (5%) — also what --ui-split-key-step declares
|
|
237
|
+
}
|
|
238
|
+
#handleKeydown(event) {
|
|
239
|
+
const target = event.target;
|
|
240
|
+
if (!(target instanceof HTMLElement) || !target.hasAttribute('data-separator'))
|
|
241
|
+
return;
|
|
242
|
+
const index = this.#separatorEls.indexOf(target);
|
|
243
|
+
if (index === -1)
|
|
244
|
+
return;
|
|
245
|
+
if (event.key === 'Enter') {
|
|
246
|
+
const pane = this.#panes()[index];
|
|
247
|
+
if (pane?.collapsible) {
|
|
248
|
+
event.preventDefault();
|
|
249
|
+
this.#toggleCollapse(index);
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const axis = this.axis;
|
|
254
|
+
const rtl = getComputedStyle(this).direction === 'rtl';
|
|
255
|
+
const isH = axis === 'horizontal';
|
|
256
|
+
const growKey = isH ? (rtl ? 'ArrowLeft' : 'ArrowRight') : 'ArrowDown';
|
|
257
|
+
const shrinkKey = isH ? (rtl ? 'ArrowRight' : 'ArrowLeft') : 'ArrowUp';
|
|
258
|
+
// `--ui-split-key-step` is a percentage of the two-neighbor PAIR (SPEC-R4), matching aria-valuenow's
|
|
259
|
+
// own normalization (split.md/split.css) — a screen-reader user hears aria-valuenow move by roughly the
|
|
260
|
+
// step's percentage on every press, regardless of how large the pair is relative to the whole track.
|
|
261
|
+
// `redistribute()` expects a whole-container-ratio delta, so the pair-relative step is scaled by the
|
|
262
|
+
// pair's own combined extent before being passed in.
|
|
263
|
+
const panes = this.#panes();
|
|
264
|
+
const ratios = this.#effectiveRatios(panes);
|
|
265
|
+
const pairExtent = (ratios[index] ?? 0) + (ratios[index + 1] ?? 0);
|
|
266
|
+
const step = this.#keyStepRatio() * pairExtent;
|
|
267
|
+
let delta = null;
|
|
268
|
+
switch (event.key) {
|
|
269
|
+
case growKey:
|
|
270
|
+
delta = step;
|
|
271
|
+
break;
|
|
272
|
+
case shrinkKey:
|
|
273
|
+
delta = -step;
|
|
274
|
+
break;
|
|
275
|
+
case 'Home':
|
|
276
|
+
delta = -1;
|
|
277
|
+
break; // drive the leading pane to its minimum — redistribute clamps it
|
|
278
|
+
case 'End':
|
|
279
|
+
delta = 1;
|
|
280
|
+
break; // drive the leading pane to its maximum — redistribute clamps it
|
|
281
|
+
default: return;
|
|
282
|
+
}
|
|
283
|
+
event.preventDefault();
|
|
284
|
+
this.#applyKeyDelta(index, delta);
|
|
285
|
+
}
|
|
286
|
+
#toggleCollapse(index) {
|
|
287
|
+
const panes = this.#panes();
|
|
288
|
+
const current = this.#effectiveRatios(panes);
|
|
289
|
+
const bounds = this.#boundsFor(panes);
|
|
290
|
+
const [minI] = bounds[index] ?? [0, 1];
|
|
291
|
+
const isCollapsed = current[index] <= minI + 1e-6;
|
|
292
|
+
if (isCollapsed) {
|
|
293
|
+
const restore = this.#collapseMemory.get(index) ?? 1 / panes.length;
|
|
294
|
+
this.#collapseMemory.delete(index);
|
|
295
|
+
this.#applyKeyDelta(index, restore - current[index]);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
this.#collapseMemory.set(index, current[index]);
|
|
299
|
+
this.#applyKeyDelta(index, minI - current[index]);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// ── render: geometry seam + ARIA (SPEC-R1/R4) ───────────────────────────────────────────────────────
|
|
303
|
+
#render() {
|
|
304
|
+
void this.#version.value; // tracked unconditionally — see #version's own doc for why (the CONTROLLED-mode dynamic-panes fix)
|
|
305
|
+
const axis = this.axis;
|
|
306
|
+
const isH = axis === 'horizontal';
|
|
307
|
+
const panes = this.#panes();
|
|
308
|
+
for (const pane of panes) {
|
|
309
|
+
pane.toggleAttribute('data-axis-vertical', !isH);
|
|
310
|
+
const min = pane.min; // tracked — a per-pane min/max change re-runs this whole effect
|
|
311
|
+
const max = pane.max;
|
|
312
|
+
// TKT-0045: `setProperty(name, '')` REMOVES a custom property (CSSOM spec) rather than setting it
|
|
313
|
+
// to an empty value — an unset `--_pane-min`/`--_pane-max` would then INHERIT from the nearest
|
|
314
|
+
// ancestor that set a real one, which leaks THIS split's per-pane geometry into any independent
|
|
315
|
+
// nested `ui-split` further down a pane's own content (e.g. `ui-master-detail`'s internal split)
|
|
316
|
+
// whose own panes don't set `min`/`max` themselves. The literal `'initial'` keyword is a real
|
|
317
|
+
// (non-removed) declaration that resets to the custom property's guaranteed-invalid value —
|
|
318
|
+
// blocking inheritance while still tripping `var(--_pane-min, var(--ui-split-pane-min))`'s
|
|
319
|
+
// fallback exactly as an actually-unset property would.
|
|
320
|
+
pane.style.setProperty('--_pane-min', min || 'initial');
|
|
321
|
+
pane.style.setProperty('--_pane-max', max || 'initial');
|
|
322
|
+
}
|
|
323
|
+
const ratios = this.#effectiveRatios(panes);
|
|
324
|
+
panes.forEach((pane, i) => {
|
|
325
|
+
pane.style.setProperty('--_pane-flex', String(Math.max(0, ratios[i] ?? 0)));
|
|
326
|
+
});
|
|
327
|
+
this.#updateAria(panes, ratios);
|
|
328
|
+
}
|
|
329
|
+
#updateAria(panes, ratios) {
|
|
330
|
+
const axis = this.axis;
|
|
331
|
+
const bounds = this.#boundsFor(panes);
|
|
332
|
+
this.#separatorEls.forEach((sep, i) => {
|
|
333
|
+
const j = i + 1;
|
|
334
|
+
sep.setAttribute('aria-orientation', axis);
|
|
335
|
+
sep.setAttribute('aria-controls', panes[i]?.id ?? '');
|
|
336
|
+
if (!sep.hasAttribute('aria-label'))
|
|
337
|
+
sep.setAttribute('aria-label', 'Resize panel');
|
|
338
|
+
const ri = ratios[i] ?? 0;
|
|
339
|
+
const rj = ratios[j] ?? 0;
|
|
340
|
+
const pairExtent = ri + rj;
|
|
341
|
+
const [minI] = bounds[i] ?? [0, 1];
|
|
342
|
+
const [minJ] = bounds[j] ?? [0, 1];
|
|
343
|
+
const hiI = Math.max(minI, pairExtent - minJ);
|
|
344
|
+
const now = pairExtent > 0 ? Math.round((ri / pairExtent) * 100) : 0;
|
|
345
|
+
const lo = pairExtent > 0 ? Math.round((minI / pairExtent) * 100) : 0;
|
|
346
|
+
const hi = pairExtent > 0 ? Math.round((hiI / pairExtent) * 100) : 100;
|
|
347
|
+
sep.setAttribute('aria-valuenow', String(now));
|
|
348
|
+
sep.setAttribute('aria-valuemin', String(lo));
|
|
349
|
+
sep.setAttribute('aria-valuemax', String(hi));
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
// ── protected test seams ────────────────────────────────────────────────────────────────────────────
|
|
353
|
+
/** Expose the live separator elements for test probes (structural ARIA/keyboard-target assertions). */
|
|
354
|
+
get separatorElsSeam() {
|
|
355
|
+
return this.#separatorEls;
|
|
356
|
+
}
|
|
357
|
+
/** Expose the CURRENT effective ratio vector (controlled `sizes`, reconciled, or the internal uncontrolled
|
|
358
|
+
* vector) for test probes — the sum-invariance / clamp / dynamic-pane assertions read this directly. */
|
|
359
|
+
get ratiosSeam() {
|
|
360
|
+
return this.#effectiveRatios(this.#panes());
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (!customElements.get('ui-split'))
|
|
364
|
+
customElements.define('ui-split', UISplitElement);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PropConfig } from '../../dom/props.js';
|
|
2
|
+
/** The SPEC-R7 value rendering: finite number → Intl-formatted; non-finite number → the placeholder `—`
|
|
3
|
+
* (U+2014); string → verbatim passthrough (the author controls formatting for non-numeric values). */
|
|
4
|
+
export declare function formatStatValue(value: string | number): string;
|
|
5
|
+
export interface DeltaParts {
|
|
6
|
+
dir: 'up' | 'down' | 'flat';
|
|
7
|
+
word: 'up' | 'down' | 'unchanged';
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
/** null for any non-number / non-finite input — the delta region is not rendered at all (SPEC-R7 AC3). */
|
|
11
|
+
export declare function deltaParts(delta: unknown): DeltaParts | null;
|
|
12
|
+
/**
|
|
13
|
+
* The safe `value` codec (SPEC-R7): `from(attr)`: `null` (attribute absent) → `''`; a TRIMMED attribute
|
|
14
|
+
* that parses to a FINITE number → that number (so `value="48200"` formats); anything else → the
|
|
15
|
+
* verbatim, untrimmed attribute string (so `value="$1.2M"` passes through unchanged). Property writes
|
|
16
|
+
* keep whatever runtime type the author assigns — this codec only guards the ATTRIBUTE crossing.
|
|
17
|
+
*/
|
|
18
|
+
export declare const statValueProp: PropConfig<string | number>;
|
|
19
|
+
/** The safe `delta` codec (SPEC-R7): `from(attr)`: `null` → `null`; `parseFloat`; non-finite → `null`
|
|
20
|
+
* (never `NaN` reaches the render path — `deltaParts` would already guard it, but the codec stays
|
|
21
|
+
* honest at the boundary too, the chart-family safe-codec discipline). */
|
|
22
|
+
export declare const statDeltaProp: PropConfig<number | null>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// stat-model.ts — the pure, DOM-free math for `ui-stat` (LLD-C4, report-family.lld.md §3; SPEC-R7/R9;
|
|
2
|
+
// ADR-0111). Everything here is a plain function over strings/numbers — no DOM, no signals,
|
|
3
|
+
// unit-testable without a browser (the sparkline-math.ts / bar-math.ts pure-core precedent).
|
|
4
|
+
//
|
|
5
|
+
// `formatStatValue` (SPEC-R7): a finite number formats via the module-memoized default-locale
|
|
6
|
+
// Intl.NumberFormat (the chart-family printed-value discipline); a non-finite number renders the
|
|
7
|
+
// placeholder em dash; a string passes through verbatim (the author's own pre-formatted text, e.g.
|
|
8
|
+
// "$1.2M" — no coercion, ever).
|
|
9
|
+
//
|
|
10
|
+
// `deltaParts` (SPEC-R9): the delta region announces DIRECTION, never valence — a `dir` (drives the CSS
|
|
11
|
+
// glyph orientation), a `word` (the real, visually-hidden-but-announced text: "up"/"down"/"unchanged"),
|
|
12
|
+
// and `text` (the Intl-formatted signed number, `signDisplay: 'exceptZero'` so 0 prints bare and ±N carry
|
|
13
|
+
// an explicit sign). `null` for any non-number/non-finite input — the delta region is not rendered at all
|
|
14
|
+
// (never a silent throw, never a fabricated direction for garbage input).
|
|
15
|
+
const numberFormat = new Intl.NumberFormat(); // module-memoized default-locale formatter (SPEC-R7)
|
|
16
|
+
const signedNumberFormat = new Intl.NumberFormat(undefined, { signDisplay: 'exceptZero' }); // SPEC-R9
|
|
17
|
+
/** The SPEC-R7 value rendering: finite number → Intl-formatted; non-finite number → the placeholder `—`
|
|
18
|
+
* (U+2014); string → verbatim passthrough (the author controls formatting for non-numeric values). */
|
|
19
|
+
export function formatStatValue(value) {
|
|
20
|
+
if (typeof value === 'number')
|
|
21
|
+
return Number.isFinite(value) ? numberFormat.format(value) : '—';
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
/** null for any non-number / non-finite input — the delta region is not rendered at all (SPEC-R7 AC3). */
|
|
25
|
+
export function deltaParts(delta) {
|
|
26
|
+
if (typeof delta !== 'number' || !Number.isFinite(delta))
|
|
27
|
+
return null;
|
|
28
|
+
const dir = delta > 0 ? 'up' : delta < 0 ? 'down' : 'flat';
|
|
29
|
+
const word = delta > 0 ? 'up' : delta < 0 ? 'down' : 'unchanged';
|
|
30
|
+
return { dir, word, text: signedNumberFormat.format(delta) };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The safe `value` codec (SPEC-R7): `from(attr)`: `null` (attribute absent) → `''`; a TRIMMED attribute
|
|
34
|
+
* that parses to a FINITE number → that number (so `value="48200"` formats); anything else → the
|
|
35
|
+
* verbatim, untrimmed attribute string (so `value="$1.2M"` passes through unchanged). Property writes
|
|
36
|
+
* keep whatever runtime type the author assigns — this codec only guards the ATTRIBUTE crossing.
|
|
37
|
+
*/
|
|
38
|
+
export const statValueProp = {
|
|
39
|
+
type: {
|
|
40
|
+
from(attr) {
|
|
41
|
+
if (attr === null)
|
|
42
|
+
return '';
|
|
43
|
+
const trimmed = attr.trim();
|
|
44
|
+
const n = Number(trimmed);
|
|
45
|
+
return trimmed !== '' && Number.isFinite(n) ? n : attr;
|
|
46
|
+
},
|
|
47
|
+
to(value) {
|
|
48
|
+
return String(value);
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
default: '',
|
|
52
|
+
};
|
|
53
|
+
/** The safe `delta` codec (SPEC-R7): `from(attr)`: `null` → `null`; `parseFloat`; non-finite → `null`
|
|
54
|
+
* (never `NaN` reaches the render path — `deltaParts` would already guard it, but the codec stays
|
|
55
|
+
* honest at the boundary too, the chart-family safe-codec discipline). */
|
|
56
|
+
export const statDeltaProp = {
|
|
57
|
+
type: {
|
|
58
|
+
from(attr) {
|
|
59
|
+
if (attr === null)
|
|
60
|
+
return null;
|
|
61
|
+
const n = Number.parseFloat(attr);
|
|
62
|
+
return Number.isFinite(n) ? n : null;
|
|
63
|
+
},
|
|
64
|
+
to(value) {
|
|
65
|
+
return value === null ? null : String(value);
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
default: null,
|
|
69
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/* stat.css — ui-stat, the Display-class metric tile (LLD-C6, report-family.lld.md §3; SPEC-R8/R9/R10/
|
|
2
|
+
* R14/R15/R16/R17; ADR-0111). Two sectioned blocks:
|
|
3
|
+
*
|
|
4
|
+
* [1] TOKEN BLOCK — `:where(ui-stat)` declares the tile's rhythm/typography as `--ui-stat-*`: interior
|
|
5
|
+
* gap + label/caption ink ride the space ladder + a supporting-text role (density-responsive for
|
|
6
|
+
* free, ADR-0103); the delta glyph's mark geometry is density-invariant px/em (badge/sparkline
|
|
7
|
+
* precedent). Display class (SPEC-R17): no [size]/[scale] control-band lookup, no `--md-sys-height-*`,
|
|
8
|
+
* no `h/2` law — the lever is the type matrix (`--md-sys-typescale-*`) + the space ladder.
|
|
9
|
+
* [2] STYLES BLOCK — `@scope (ui-stat)` consumes ONLY `--ui-stat-*`.
|
|
10
|
+
*
|
|
11
|
+
* The value register is the tile the fake-`h3` idiom faked: `--md-sys-typescale-headline-small-*` gives
|
|
12
|
+
* it real heading-scale typography WITHOUT a heading element (ADR-0078's semantics/style split honored —
|
|
13
|
+
* stat.ts never stamps h1-h6).
|
|
14
|
+
*
|
|
15
|
+
* SPEC-R9 AC2 — no valence channel exists to remove: the delta inherits `color: inherit`; no `[data-dir]`
|
|
16
|
+
* rule in this file touches ink. Direction lives ONLY in the glyph's clip-path orientation + the signed
|
|
17
|
+
* number + the hidden word (stat.ts). `neutral` isn't a concept here — up/down/flat share one ink.
|
|
18
|
+
*
|
|
19
|
+
* Forced colors (SPEC-R15): the delta glyph is `background: currentColor` (a mask shape, not text) — an
|
|
20
|
+
* explicit `forced-colors` block repoints it to `CanvasText` (the bar-chart/badge fill lesson: a
|
|
21
|
+
* background-drawn shape is otherwise forced to `Canvas` and vanishes). Every other part is real text and
|
|
22
|
+
* survives untouched — no dedicated block needed for label/value/caption.
|
|
23
|
+
*
|
|
24
|
+
* RTL: no dedicated mirroring rule needed — the tile is a single-column block stack (`display:
|
|
25
|
+
* inline-grid`, one column), so there is no inline-axis slot order to flip; the delta's glyph→word→number
|
|
26
|
+
* sequence is DOM order (stat.ts), which logical inline layout already reads correctly in both directions.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/* ════════════════ [1] TOKEN BLOCK — :where(ui-stat) declares --ui-stat-* ════════════════ */
|
|
30
|
+
|
|
31
|
+
:where(ui-stat) {
|
|
32
|
+
--ui-stat-min-inline-size: 8em; /* the whole-shape floor (SPEC-R10) */
|
|
33
|
+
--ui-stat-gap: var(--md-sys-space-xs); /* interior rhythm — rides [density] free (ADR-0103) */
|
|
34
|
+
--ui-stat-label-ink: var(--md-sys-color-neutral-on-surface-variant);
|
|
35
|
+
--ui-stat-caption-ink: var(--md-sys-color-neutral-on-surface-variant);
|
|
36
|
+
--ui-stat-delta-glyph-size: 0.6em; /* density-invariant mark geometry */
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* ════════════════ [2] STYLES BLOCK — @scope (ui-stat) consumes ONLY --ui-stat-* ════════════════ */
|
|
40
|
+
|
|
41
|
+
@scope (ui-stat) {
|
|
42
|
+
:scope {
|
|
43
|
+
display: inline-grid;
|
|
44
|
+
gap: var(--ui-stat-gap);
|
|
45
|
+
min-inline-size: var(--ui-stat-min-inline-size);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
:scope [data-part='label'] {
|
|
49
|
+
color: var(--ui-stat-label-ink);
|
|
50
|
+
font-size: var(--md-sys-typescale-label-medium-size);
|
|
51
|
+
line-height: var(--md-sys-typescale-label-medium-line-height);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
:scope [data-part='value'] {
|
|
55
|
+
/* the tile register the fake-h3 idiom faked — real heading-scale STYLE without heading SEMANTICS
|
|
56
|
+
(ADR-0078's split honored; row names verified against dimensions.css at build) */
|
|
57
|
+
font-size: var(--md-sys-typescale-headline-small-size);
|
|
58
|
+
line-height: var(--md-sys-typescale-headline-small-line-height);
|
|
59
|
+
font-weight: var(--md-sys-typescale-headline-small-weight);
|
|
60
|
+
font-variant-numeric: tabular-nums;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
:scope [data-part='delta'] {
|
|
64
|
+
font-size: var(--md-sys-typescale-body-medium-size);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
:scope [data-part='caption'] {
|
|
68
|
+
color: var(--ui-stat-caption-ink);
|
|
69
|
+
font-size: var(--md-sys-typescale-body-small-size);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
:scope [data-part='delta-glyph'] {
|
|
73
|
+
display: inline-block;
|
|
74
|
+
background: currentColor;
|
|
75
|
+
inline-size: var(--ui-stat-delta-glyph-size);
|
|
76
|
+
block-size: var(--ui-stat-delta-glyph-size);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
:scope [data-dir='up'] [data-part='delta-glyph'] {
|
|
80
|
+
clip-path: polygon(50% 0, 100% 100%, 0 100%);
|
|
81
|
+
}
|
|
82
|
+
:scope [data-dir='down'] [data-part='delta-glyph'] {
|
|
83
|
+
clip-path: polygon(0 0, 100% 0, 50% 100%);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
:scope [data-part='delta-word'] {
|
|
87
|
+
/* visually hidden, announced (SPEC-R9) — the direction word carries the meaning as REAL text */
|
|
88
|
+
position: absolute;
|
|
89
|
+
inline-size: 1px;
|
|
90
|
+
block-size: 1px;
|
|
91
|
+
overflow: hidden;
|
|
92
|
+
clip-path: inset(50%);
|
|
93
|
+
white-space: nowrap;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@media (forced-colors: active) {
|
|
97
|
+
:scope [data-part='delta-glyph'] {
|
|
98
|
+
background: CanvasText; /* SPEC-R15 — the bar-fill/badge lesson: a masked shape needs a system ink */
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { UIElement, type ReactiveProps } from '../../dom/index.js';
|
|
2
|
+
declare const props: {
|
|
3
|
+
label: {
|
|
4
|
+
reflect: true;
|
|
5
|
+
type: import("../../index.js").PropType<string>;
|
|
6
|
+
default: string;
|
|
7
|
+
attribute?: string | false;
|
|
8
|
+
};
|
|
9
|
+
figure: import("../../index.js").PropConfig<string | number>;
|
|
10
|
+
delta: import("../../index.js").PropConfig<number | null>;
|
|
11
|
+
caption: import("../../index.js").PropConfig<string>;
|
|
12
|
+
};
|
|
13
|
+
export interface UIStatElement extends ReactiveProps<typeof props> {
|
|
14
|
+
}
|
|
15
|
+
export declare class UIStatElement extends UIElement {
|
|
16
|
+
#private;
|
|
17
|
+
static props: {
|
|
18
|
+
label: {
|
|
19
|
+
reflect: true;
|
|
20
|
+
type: import("../../index.js").PropType<string>;
|
|
21
|
+
default: string;
|
|
22
|
+
attribute?: string | false;
|
|
23
|
+
};
|
|
24
|
+
figure: import("../../index.js").PropConfig<string | number>;
|
|
25
|
+
delta: import("../../index.js").PropConfig<number | null>;
|
|
26
|
+
caption: import("../../index.js").PropConfig<string>;
|
|
27
|
+
};
|
|
28
|
+
protected connected(): void;
|
|
29
|
+
}
|
|
30
|
+
export {};
|