@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,417 @@
|
|
|
1
|
+
// status-stream.ts — UIStatusStreamElement, the timeline family's LIVE host (timeline-family.lld.md §4 ·
|
|
2
|
+
// SPEC-R8…R12 · ADR-0122 F1/F2/F4/F6 · ADR-0146 F6/F7/F8). BEHAVIOUR + the imperative appendEntry/update/
|
|
3
|
+
// finalize/fail API + the tail-follow guard + the completion invariant + the opt-in streaming header +
|
|
4
|
+
// self-define ONLY. Anatomy/geometry per the LLD; styling lives in status-stream.css, the public contract
|
|
5
|
+
// in status-stream.md.
|
|
6
|
+
//
|
|
7
|
+
// NAMED LLD DEVIATION (build-time, mechanical, flagged for the design seat's amendment): the LLD/SPEC-R9
|
|
8
|
+
// name this method `append(entry): UITimelineItemElement`. That name is UNBUILDABLE as specified — every
|
|
9
|
+
// element (via Node.prototype) already has a native, INCOMPATIBLE `append(...nodes: (string|Node)[])`, and
|
|
10
|
+
// `UIStatusStreamElement extends UIContainerElement extends UIElement extends HTMLElement`, so a same-name
|
|
11
|
+
// override with an incompatible signature fails `tsc` outright (TS2416/TS2430 — verified, not a maybe).
|
|
12
|
+
// Renamed to `appendEntry` — IDENTICAL signature/behaviour/return, name only; zero other contract change.
|
|
13
|
+
// LLD-C-timeline-family.lld.md §4/§7 and SPEC-R9 need the same rename before ratification.
|
|
14
|
+
//
|
|
15
|
+
// The live "what the system is doing now" strip: entries arrive imperatively (`appendEntry`), transition
|
|
16
|
+
// state in place (`update`, keyed by string identity), and the stream's own scroll region tail-follows the
|
|
17
|
+
// newest arrival unless the user has scrolled up to read history (the stick-to-bottom guard). Hosts the
|
|
18
|
+
// SAME `ui-timeline-item` children `ui-timeline` does — created by THIS host's own API, never authored by
|
|
19
|
+
// the consumer. `internals.role = 'log'` (a POLITE live region — ARIA's role=log carries an implicit
|
|
20
|
+
// aria-relevant="additions" default, which is exactly the discipline this control needs: a genuinely NEW
|
|
21
|
+
// entry (`appendEntry`, a real DOM addition) announces, while a `text` patch that MUTATES an existing
|
|
22
|
+
// node's textContent does NOT re-trigger the addition-relevant announcement — state transitions announce,
|
|
23
|
+
// token spam does not, for free, by choosing role=log's own default semantics rather than a bespoke
|
|
24
|
+
// aria-live wiring). `internals.role` is set in the CONSTRUCTOR (the toast role precedent) — before insertion.
|
|
25
|
+
//
|
|
26
|
+
// The opt-in streaming header (ADR-0146 F8): a reflected boolean `header` prop, default false — every
|
|
27
|
+
// shipped consumer renders byte-identically (NO header DOM at all). When set, a `[data-part="header"]` row
|
|
28
|
+
// renders the `label` VISIBLY (today it is aria-only) plus a live overall-status glyph. The header is
|
|
29
|
+
// PINNED (position: sticky, status-stream.css) so it never scrolls away as entries overflow — the "chrome
|
|
30
|
+
// outside the scroll region" F8 rejects a faked first-entry for (a faked entry would scroll away). The
|
|
31
|
+
// overall status follows ONE rule (F8): while un-finalized, the F6 escalation over the strip's TOP-LEVEL
|
|
32
|
+
// entries whenever that escalation OUTRANKS `active` (a mid-turn error/warning child flips the header
|
|
33
|
+
// immediately), and `active` otherwise — so an EMPTY un-finalized stream reads WORKING from construction
|
|
34
|
+
// (the blank-bubble ROOT fix: a header shows "working" the instant a consumer sets header, before any entry
|
|
35
|
+
// or wire signal). `finalize()` settles it to the escalated final status (still-active/pending entries, now
|
|
36
|
+
// truncated by the completion invariant, contribute `warning` — matching the warning-coloured truncated
|
|
37
|
+
// ring); `fail()` settles it `error`. Because THIS host owns every mutation of its top-level entries
|
|
38
|
+
// (appendEntry/update/finalize/fail), the header is recomputed imperatively at each — no MutationObserver
|
|
39
|
+
// is needed for the stream level.
|
|
40
|
+
//
|
|
41
|
+
// GROUPED entries (ADR-0146 F5, via the ADR-0143 2026-07-18 amendment): a `StatusEntry.parent?: string` (an
|
|
42
|
+
// existing entry's key) nests the child UNDER that group instead of as a top-level sibling — the host lazily
|
|
43
|
+
// mounts a nested `<ui-timeline>` into the parent item's `[data-role="nested"]` slot through
|
|
44
|
+
// `ui-timeline-item.ensureNestedSlot()`, REUSING ADR-0143's shared `ui-disclosure` + collapsed-summary
|
|
45
|
+
// preview (never a second nesting primitive — the family-coherence law). The keyed registry stays FLAT:
|
|
46
|
+
// keys are unique across the whole strip, so `update(childKey, patch)` reaches a nested entry identically,
|
|
47
|
+
// and `finalize()`/`fail()` truncation already walk every entry (`#byKey.values()`), nested included. The
|
|
48
|
+
// nested `<ui-timeline>` is `role=list` (durable) inside the outer `role=log` — ONE live region, one
|
|
49
|
+
// addition-announcement path, no bespoke aria-live on the nested host (the F6/aria discipline).
|
|
50
|
+
//
|
|
51
|
+
// GROUP-LEVEL escalation (ADR-0146 F6): a group parent's status = the WORST of its children over the SAME
|
|
52
|
+
// closed ladder `error > warning > active > pending > done` (the exported `escalateStatus` reduce). It is
|
|
53
|
+
// recomputed the SAME mediated way the stream-level header is — imperatively, from THIS host's own
|
|
54
|
+
// `appendEntry`/`update` calls (every grouped entry's status change flows through `update`), bubbling up any
|
|
55
|
+
// enclosing groups — NOT via a MutationObserver. The nested-slot observer `ensureNestedSlot` installs serves
|
|
56
|
+
// ONLY the collapsed-summary preview (ADR-0143 F3), never escalation; adding a second, observer-driven
|
|
57
|
+
// escalation path would be redundant and would leave durable-timeline nesting byte-changed for no reason.
|
|
58
|
+
//
|
|
59
|
+
// The completion invariant (#markTruncated/finalize, SPEC-R11, the B7 tracked-completion doctrine applied
|
|
60
|
+
// to display): `finalize()` marks every still-`pending`/`active` entry TRUNCATED via the item's own
|
|
61
|
+
// `markTruncated()` escape hatch (a `:state(truncated)` custom state, timeline-item.ts) — fail-closed, a
|
|
62
|
+
// torn stream never shows "still working."
|
|
63
|
+
//
|
|
64
|
+
// `controls → dom + controls/timeline-item/timeline-item.ts + @agent-ui-kit/icons` — the allowed import
|
|
65
|
+
// direction (cross-folder sibling + the zero-dep icons pack, the timeline-item precedent). Holds NO
|
|
66
|
+
// transport of its own (a standing negative-control grep — SPEC-R9 AC3): no fetch/ReadableStream/
|
|
67
|
+
// readNdjsonLines reference anywhere in this file.
|
|
68
|
+
import { UIContainerElement, prop } from "../../dom/index.js";
|
|
69
|
+
import { UITimelineItemElement } from "../timeline-item/timeline-item.js"; // constructs items via its own API (F4)
|
|
70
|
+
import "../timeline/timeline.js"; // registers <ui-timeline> — the nested group host (ADR-0146 F5, ADR-0143's mechanism)
|
|
71
|
+
import { resolveIcon } from '@agent-ui-kit/icons'; // the header's overall-status glyph (done/error/warning) — the timeline-item glyph precedent
|
|
72
|
+
// The total severity order (ADR-0146 F6): error > warning > active > pending > done; neutral '' contributes
|
|
73
|
+
// nothing (rank 0 — a group/strip of only-neutral entries escalates to '' itself). One closed ladder, the
|
|
74
|
+
// SAME order the group-header and the stream-header both reduce over.
|
|
75
|
+
const STATUS_RANK = { '': 0, done: 1, pending: 2, active: 3, warning: 4, error: 5 };
|
|
76
|
+
/** The resolved-outcome header glyphs (mirrors timeline-item's STATUS_GLYPH) — the in-progress '' /
|
|
77
|
+
* pending / active states paint a pure-CSS dot/ring/pulse in status-stream.css instead. */
|
|
78
|
+
const HEADER_STATUS_GLYPH = { done: 'check', error: 'x', warning: 'warning' };
|
|
79
|
+
/**
|
|
80
|
+
* Worst-child-wins over the closed ADR-0146 F6 ladder — the single reduce both the (future) group header
|
|
81
|
+
* and the stream header use. Monotone-truthful: the result never reads calmer than the worst contributor
|
|
82
|
+
* (a group with one `error` child and one still-`active` child reads `error`, the truth that something
|
|
83
|
+
* already failed outranking "still working"). Neutral '' entries contribute nothing. Pure + exported so
|
|
84
|
+
* it is directly unit-tested (the ladder's `error`-beats-`active` and `done`+`warning`→`warning` cases).
|
|
85
|
+
*/
|
|
86
|
+
export function escalateStatus(statuses) {
|
|
87
|
+
let worst = '';
|
|
88
|
+
for (const s of statuses)
|
|
89
|
+
if (STATUS_RANK[s] > STATUS_RANK[worst])
|
|
90
|
+
worst = s;
|
|
91
|
+
return worst;
|
|
92
|
+
}
|
|
93
|
+
const SIZE = ['sm', 'md', 'lg'];
|
|
94
|
+
const props = {
|
|
95
|
+
size: { ...prop.enum(SIZE, 'md'), reflect: true },
|
|
96
|
+
label: { ...prop.string(''), reflect: true },
|
|
97
|
+
// ADR-0146 F8 — the opt-in visible streaming header. Reflected, default false: every shipped consumer
|
|
98
|
+
// renders byte-identically (no header DOM) until it opts in. When true, the header shows the label + the
|
|
99
|
+
// live escalated overall status, pinned outside the scroll region.
|
|
100
|
+
header: { ...prop.boolean(false), reflect: true },
|
|
101
|
+
};
|
|
102
|
+
// The stick-to-bottom threshold (px) — "at/near the bottom" tolerates sub-pixel/rounding scroll noise
|
|
103
|
+
// without falsely dropping the follow guard.
|
|
104
|
+
const STICK_THRESHOLD_PX = 24;
|
|
105
|
+
export class UIStatusStreamElement extends UIContainerElement {
|
|
106
|
+
static props = props;
|
|
107
|
+
#byKey = new Map();
|
|
108
|
+
#stuckToBottom = true; // the tail-follow guard (§4.3)
|
|
109
|
+
// ── grouping (ADR-0146 F5) — the FLAT registry: keys stay unique across the whole strip ─────────────────
|
|
110
|
+
#parentOf = new Map(); // childKey → parentKey
|
|
111
|
+
#childrenOf = new Map(); // parentKey → ordered childKeys
|
|
112
|
+
#nestedByParent = new Map(); // parentKey → its once-per-parent nested <ui-timeline> host
|
|
113
|
+
// ── the opt-in streaming header (ADR-0146 F8) ────────────────────────────────────────────────────────
|
|
114
|
+
#header = null;
|
|
115
|
+
#headerMarker = null;
|
|
116
|
+
#headerLabel = null;
|
|
117
|
+
// The turn's completion state — drives the header's F8 rule (working-floor while un-finalized; the
|
|
118
|
+
// settled escalation once finalized; forced `error` once failed).
|
|
119
|
+
#finalized = false;
|
|
120
|
+
#failed = false;
|
|
121
|
+
// Entries the completion invariant truncated — they contribute `warning` to the settled header (the
|
|
122
|
+
// warning-coloured truncated ring's header-level face). A WeakSet: no key bookkeeping, GC-friendly.
|
|
123
|
+
#truncated = new WeakSet();
|
|
124
|
+
constructor() {
|
|
125
|
+
super();
|
|
126
|
+
this.internals.role = 'log'; // a POLITE live region via internals.role (the toast role='status' precedent)
|
|
127
|
+
}
|
|
128
|
+
connected() {
|
|
129
|
+
this.effect(() => {
|
|
130
|
+
this.internals.ariaLabel = this.label === '' ? null : this.label;
|
|
131
|
+
this.#syncHeaderLabel(); // the header shows the SAME label VISIBLY when opted in (F8)
|
|
132
|
+
});
|
|
133
|
+
// Create/remove the header as `header` toggles — default false renders byte-identically to a headerless
|
|
134
|
+
// strip (no header DOM at all, the F8 zero-regression guarantee).
|
|
135
|
+
this.effect(() => {
|
|
136
|
+
if (this.header)
|
|
137
|
+
this.#ensureHeader();
|
|
138
|
+
else
|
|
139
|
+
this.#removeHeader();
|
|
140
|
+
});
|
|
141
|
+
this.listen(this, 'scroll', () => this.#trackStickToBottom());
|
|
142
|
+
}
|
|
143
|
+
/** Append a new entry, tail-follow to it, return the created item (the toast-region.show() return
|
|
144
|
+
* precedent). Named `appendEntry`, NOT `append` — the native Node.prototype.append() every element
|
|
145
|
+
* inherits is incompatible; see the file-header NAMED LLD DEVIATION note. ADR-0146 F5: an entry carrying
|
|
146
|
+
* a KNOWN `parent` nests under that group's `[data-role="nested"]` slot instead of as a flat sibling.
|
|
147
|
+
*
|
|
148
|
+
* Duplicate-key guard (issue #37): a key already in `#byKey` is REJECTED — returns the EXISTING item
|
|
149
|
+
* untouched, never creating a second element. This is the same "graceful, never throw, no-op the
|
|
150
|
+
* invalid case" posture the rest of this file already applies to creation-adjacent edge cases (an
|
|
151
|
+
* unknown `parent` degrades to a flat append rather than throwing; a self-referencing `parent` does the
|
|
152
|
+
* same) — appendEntry's contract is to CREATE, so a key collision is the invalid case here, symmetric
|
|
153
|
+
* with update()'s own no-op on an UNKNOWN key. The alternative (tear down the old item and re-key it)
|
|
154
|
+
* was rejected: the old item may itself be a GROUP PARENT with live nested children (#childrenOf/
|
|
155
|
+
* #nestedByParent), and re-parenting those children's DOM + registry links onto a freshly-created
|
|
156
|
+
* replacement item is a second, much larger surgery this fix does not need — silently keeping the
|
|
157
|
+
* first-registered item is sufficient to close the acceptance criteria (never two live elements for one
|
|
158
|
+
* key) without inventing a re-parent path the design never specified. A consumer that truly needs to
|
|
159
|
+
* replace an entry's fields still can, via `update(key, patch)` on the SAME key. */
|
|
160
|
+
appendEntry(entry) {
|
|
161
|
+
const existing = this.#byKey.get(entry.key);
|
|
162
|
+
if (existing !== undefined)
|
|
163
|
+
return existing; // duplicate key — reject, the old element/links are left intact
|
|
164
|
+
const item = document.createElement('ui-timeline-item');
|
|
165
|
+
item.dataset.key = entry.key;
|
|
166
|
+
this.#assign(item, entry);
|
|
167
|
+
// Resolve the parent BEFORE registering `entry.key` in `#byKey` — a self-referencing `parent`
|
|
168
|
+
// (entry.key === entry.parent) must resolve to "unknown parent", never to this not-yet-connected
|
|
169
|
+
// item itself (which would route it into #ensureNested and throw: no connection scope).
|
|
170
|
+
const parentKey = entry.parent;
|
|
171
|
+
const parentItem = parentKey !== undefined ? this.#byKey.get(parentKey) : undefined;
|
|
172
|
+
this.#byKey.set(entry.key, item);
|
|
173
|
+
if (parentKey !== undefined && parentItem !== undefined) {
|
|
174
|
+
// GROUPED (F5) — mount the child into the parent's nested <ui-timeline> (created lazily, once per parent
|
|
175
|
+
// via ADR-0143's shared disclosure/preview composition). The registry stays FLAT (keyed by `key`).
|
|
176
|
+
this.#ensureNested(parentKey, parentItem).appendChild(item);
|
|
177
|
+
this.#parentOf.set(entry.key, parentKey);
|
|
178
|
+
const kids = this.#childrenOf.get(parentKey) ?? [];
|
|
179
|
+
kids.push(entry.key);
|
|
180
|
+
this.#childrenOf.set(parentKey, kids);
|
|
181
|
+
this.#recomputeGroups(entry.key); // F6 — escalate the new child's group chain (mediated, no observer)
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// TOP-LEVEL (or an unknown parent — a graceful flat fallback, never a throw): append into the strip,
|
|
185
|
+
// after the pinned header when present (appendChild lands at the end, past the first-child header).
|
|
186
|
+
this.appendChild(item);
|
|
187
|
+
}
|
|
188
|
+
this.#refreshHeader(); // the escalation may have changed (a mid-turn error/warning flips the header)
|
|
189
|
+
this.#tailFollow(item);
|
|
190
|
+
return item;
|
|
191
|
+
}
|
|
192
|
+
/** Keyed, in-place mutation — transition status / grow text / reveal detail. No-op if the key is unknown.
|
|
193
|
+
* Reaches a NESTED entry identically (the flat registry), and a status transition re-escalates its
|
|
194
|
+
* enclosing group chain + the header (ADR-0146 F6, mediated — every child status change flows through here). */
|
|
195
|
+
update(key, patch) {
|
|
196
|
+
const item = this.#byKey.get(key);
|
|
197
|
+
if (item === undefined)
|
|
198
|
+
return; // a late update after truncation is tolerated (never a throw) — SPEC-R9 AC2
|
|
199
|
+
this.#assign(item, patch);
|
|
200
|
+
if (patch.status !== undefined) {
|
|
201
|
+
this.#recomputeGroups(key); // F6 — a child's status change re-escalates its group chain (mediated, no observer)
|
|
202
|
+
this.#refreshHeader(); // a status transition may re-escalate the header
|
|
203
|
+
}
|
|
204
|
+
if (this.#growsTail(patch))
|
|
205
|
+
this.#tailFollow(item);
|
|
206
|
+
}
|
|
207
|
+
/** The completion invariant — mark every still-pending/active entry TRUNCATED (SPEC-R11), then settle the
|
|
208
|
+
* header to the escalated FINAL status (ADR-0146 F8). */
|
|
209
|
+
finalize() {
|
|
210
|
+
this.#settle(false);
|
|
211
|
+
}
|
|
212
|
+
/** A failed stream (ADR-0146 F8): the completion invariant PLUS a header forced to `error` — the
|
|
213
|
+
* completion invariant now has a header-level face. Marks still-pending/active entries truncated exactly
|
|
214
|
+
* as `finalize()` does (a failed turn is also torn), then paints the header `error` regardless of the
|
|
215
|
+
* entries' own escalation. */
|
|
216
|
+
fail() {
|
|
217
|
+
this.#settle(true);
|
|
218
|
+
}
|
|
219
|
+
/** Shared settle path for finalize()/fail(): truncate the unresolved entries, flip the completion state,
|
|
220
|
+
* repaint the header to its settled (or forced-error) face. */
|
|
221
|
+
#settle(failed) {
|
|
222
|
+
for (const item of this.#byKey.values()) {
|
|
223
|
+
if (item.status === 'active' || item.status === 'pending') {
|
|
224
|
+
this.#markTruncated(item);
|
|
225
|
+
this.#truncated.add(item);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Re-escalate every group chain — a truncated child now contributes `warning` via `#effectiveStatus`,
|
|
229
|
+
// but only a recompute actually repaints the group parent's `.status` with it (issue #26: the stream
|
|
230
|
+
// header already reads truncation-aware via `#topLevelStatuses()`, but a GROUP parent's own `.status`
|
|
231
|
+
// was never recomputed at settle time, so it kept its stale pre-truncation face). Every key, not just
|
|
232
|
+
// the truncated ones — a group with no truncated child is a harmless no-op walk, and a group-of-groups
|
|
233
|
+
// needs its own parent's chain walked too, exactly like the live `update()` path already does per key.
|
|
234
|
+
for (const key of this.#byKey.keys())
|
|
235
|
+
this.#recomputeGroups(key);
|
|
236
|
+
this.#finalized = true;
|
|
237
|
+
this.#failed = failed;
|
|
238
|
+
this.#refreshHeader();
|
|
239
|
+
}
|
|
240
|
+
/** The entry → item projection — sets only the provided fields onto the item's typed props; `text`
|
|
241
|
+
* grows/replaces the item's streamed-text cell in place (a `[data-role="text"]` content-column cell,
|
|
242
|
+
* never re-parsed — the item's own residual imperative fact, timeline-item.md). `parent` is deliberately
|
|
243
|
+
* NOT projected — it is a routing fact consumed by `appendEntry`, never a timeline-item prop. */
|
|
244
|
+
#assign(item, patch) {
|
|
245
|
+
if (patch.status !== undefined)
|
|
246
|
+
item.status = patch.status;
|
|
247
|
+
if (patch.label !== undefined)
|
|
248
|
+
item.label = patch.label;
|
|
249
|
+
if (patch.description !== undefined)
|
|
250
|
+
item.description = patch.description;
|
|
251
|
+
if (patch.timestamp !== undefined)
|
|
252
|
+
item.timestamp = patch.timestamp;
|
|
253
|
+
if (patch.icon !== undefined)
|
|
254
|
+
item.icon = patch.icon;
|
|
255
|
+
if (patch.text !== undefined)
|
|
256
|
+
this.#growText(item, patch.text);
|
|
257
|
+
}
|
|
258
|
+
/** ADR-0146 F5 — the once-per-parent nested `<ui-timeline>` host, composed into the parent item's shared
|
|
259
|
+
* `ui-disclosure` via `ensureNestedSlot` (ADR-0143's mechanism, reused — never a second nesting primitive).
|
|
260
|
+
* Cached per parent key so every grouped child mounts into the SAME nested timeline. */
|
|
261
|
+
#ensureNested(parentKey, parentItem) {
|
|
262
|
+
const existing = this.#nestedByParent.get(parentKey);
|
|
263
|
+
if (existing !== undefined)
|
|
264
|
+
return existing;
|
|
265
|
+
const nested = parentItem.ensureNestedSlot(() => document.createElement('ui-timeline'));
|
|
266
|
+
this.#nestedByParent.set(parentKey, nested);
|
|
267
|
+
return nested;
|
|
268
|
+
}
|
|
269
|
+
/** ADR-0146 F6 — worst-child-wins escalation, driven the SAME mediated way the stream header is: walk the
|
|
270
|
+
* changed key's ANCESTOR group chain and set each group parent's status to `escalateStatus` over its own
|
|
271
|
+
* children (bubbling if the group is itself nested). NO MutationObserver — every grouped entry's status
|
|
272
|
+
* change flows through `appendEntry`/`update`, so a mediated recompute is complete, and the nested-slot
|
|
273
|
+
* observer `ensureNestedSlot` installs is left to serve ONLY the collapsed-summary preview (ADR-0143 F3).
|
|
274
|
+
* A top-level key (no parent) walks zero steps — the header repaint stays the caller's own #refreshHeader.
|
|
275
|
+
* A child's contribution is its `#effectiveStatus` (truncation-aware), not its raw `.status` — so a group
|
|
276
|
+
* whose child was truncated by `#settle()` correctly reads `warning`, the SAME rule the stream header
|
|
277
|
+
* already applies via `#topLevelStatuses()` (issue #26 — this was previously the raw `.status`, so a
|
|
278
|
+
* truncated nested child's group parent kept reading its stale pre-truncation escalation).
|
|
279
|
+
* Cycle-guarded (issue #27): `#parentOf` is normally acyclic (an entry nests under an ALREADY-registered
|
|
280
|
+
* parent, appendEntry's own ordering), but a duplicate-keyed re-append can still wire a cycle back into
|
|
281
|
+
* its own former descendant chain — a `visited` set bounds the walk so a malformed chain terminates
|
|
282
|
+
* instead of hanging the main thread, rather than trusting the registry's shape. */
|
|
283
|
+
#recomputeGroups(key) {
|
|
284
|
+
const visited = new Set();
|
|
285
|
+
let cursor = this.#parentOf.get(key);
|
|
286
|
+
while (cursor !== undefined && !visited.has(cursor)) {
|
|
287
|
+
visited.add(cursor);
|
|
288
|
+
const parent = this.#byKey.get(cursor);
|
|
289
|
+
const kids = this.#childrenOf.get(cursor);
|
|
290
|
+
if (parent !== undefined && kids !== undefined) {
|
|
291
|
+
parent.status = escalateStatus(kids.map((k) => {
|
|
292
|
+
const kid = this.#byKey.get(k);
|
|
293
|
+
return kid !== undefined ? this.#effectiveStatus(kid) : '';
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
cursor = this.#parentOf.get(cursor); // keep bubbling if the group is itself nested
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/** Find-or-create the item's `[data-role="text"]` cell and set its text (plain assignment — the
|
|
300
|
+
* consumer sends the cumulative text each call; the host never concatenates/tokenizes it). */
|
|
301
|
+
#growText(item, text) {
|
|
302
|
+
let cell = item.querySelector(':scope > [data-role="text"]');
|
|
303
|
+
if (!cell) {
|
|
304
|
+
cell = document.createElement('span');
|
|
305
|
+
cell.setAttribute('data-role', 'text');
|
|
306
|
+
item.appendChild(cell);
|
|
307
|
+
}
|
|
308
|
+
cell.textContent = text;
|
|
309
|
+
}
|
|
310
|
+
/** Whether a patch grew visible tail content — gates a re-scroll so a metadata-only update (icon alone)
|
|
311
|
+
* does not needlessly re-trigger tail-follow. */
|
|
312
|
+
#growsTail(patch) {
|
|
313
|
+
return patch.text !== undefined || patch.description !== undefined || patch.label !== undefined;
|
|
314
|
+
}
|
|
315
|
+
/** Mark an item TRUNCATED via its own escape hatch (SPEC-R11) — a distinct, non-color-only interrupted
|
|
316
|
+
* affordance, fail-closed: an unresolved-at-end entry is truncated, never left silently "still working." */
|
|
317
|
+
#markTruncated(item) {
|
|
318
|
+
item.markTruncated(true);
|
|
319
|
+
}
|
|
320
|
+
// ── the streaming header (ADR-0146 F8) + escalation (F6) ─────────────────────────────────────────────
|
|
321
|
+
/** Build the header anatomy ONCE (idempotent — the part-persistence precedent) and prepend it so it is
|
|
322
|
+
* the strip's FIRST child (appendEntry always appends AFTER, so entries never displace it). Sticky
|
|
323
|
+
* positioning (status-stream.css) pins it outside the visual scroll region. */
|
|
324
|
+
#ensureHeader() {
|
|
325
|
+
if (this.#header)
|
|
326
|
+
return;
|
|
327
|
+
const header = document.createElement('div');
|
|
328
|
+
header.dataset.part = 'header';
|
|
329
|
+
const marker = document.createElement('span');
|
|
330
|
+
marker.dataset.part = 'header-marker';
|
|
331
|
+
const label = document.createElement('span');
|
|
332
|
+
label.dataset.part = 'header-label';
|
|
333
|
+
header.append(marker, label);
|
|
334
|
+
this.#header = header;
|
|
335
|
+
this.#headerMarker = marker;
|
|
336
|
+
this.#headerLabel = label;
|
|
337
|
+
this.insertBefore(header, this.firstChild); // FIRST child — before any already-appended entry
|
|
338
|
+
this.#syncHeaderLabel();
|
|
339
|
+
this.#refreshHeader();
|
|
340
|
+
}
|
|
341
|
+
/** Remove the header entirely (the `header=false` path) — no header DOM lingers, the byte-identical
|
|
342
|
+
* headerless rendering the F8 default guarantees. */
|
|
343
|
+
#removeHeader() {
|
|
344
|
+
this.#header?.remove();
|
|
345
|
+
this.#header = null;
|
|
346
|
+
this.#headerMarker = null;
|
|
347
|
+
this.#headerLabel = null;
|
|
348
|
+
}
|
|
349
|
+
/** Paint the CURRENT label into the header (a no-op before the header exists / when not opted in). */
|
|
350
|
+
#syncHeaderLabel() {
|
|
351
|
+
if (this.#headerLabel)
|
|
352
|
+
this.#headerLabel.textContent = this.label;
|
|
353
|
+
}
|
|
354
|
+
/** Recompute + paint the header's overall status (a no-op before the header exists). Called at every
|
|
355
|
+
* mutation the host owns (appendEntry/update-status/finalize/fail) — no observer needed at the stream
|
|
356
|
+
* level since the host is the sole mutator of its top-level entries. */
|
|
357
|
+
#refreshHeader() {
|
|
358
|
+
const header = this.#header;
|
|
359
|
+
const marker = this.#headerMarker;
|
|
360
|
+
if (!header || !marker)
|
|
361
|
+
return;
|
|
362
|
+
const status = this.#overallStatus();
|
|
363
|
+
header.dataset.status = status;
|
|
364
|
+
const glyph = HEADER_STATUS_GLYPH[status];
|
|
365
|
+
if (glyph !== undefined) {
|
|
366
|
+
const svg = resolveIcon(glyph);
|
|
367
|
+
svg.setAttribute('data-role', 'marker');
|
|
368
|
+
marker.replaceChildren(svg);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
marker.replaceChildren(); // '' / pending / active — pure CSS paints the dot/ring/pulse
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/** The header's overall status under the F8 rule: forced `error` on fail(); the settled escalation once
|
|
375
|
+
* finalized; otherwise the working-floored escalation (the F6 reduce when it OUTRANKS `active`, else
|
|
376
|
+
* `active` — so an empty un-finalized strip reads WORKING from construction). */
|
|
377
|
+
#overallStatus() {
|
|
378
|
+
if (this.#failed)
|
|
379
|
+
return 'error';
|
|
380
|
+
const escalated = escalateStatus(this.#topLevelStatuses());
|
|
381
|
+
if (this.#finalized)
|
|
382
|
+
return escalated; // settled — no working floor
|
|
383
|
+
return STATUS_RANK[escalated] > STATUS_RANK['active'] ? escalated : 'active';
|
|
384
|
+
}
|
|
385
|
+
/** The escalation domain (F6/F8): the strip's TOP-LEVEL entries only — direct `ui-timeline-item`
|
|
386
|
+
* children of the host, so a future nested (grouped) entry never double-counts at the stream level. A
|
|
387
|
+
* truncated entry contributes `warning` (its settled, torn-outcome face), not its frozen `active`. */
|
|
388
|
+
#topLevelStatuses() {
|
|
389
|
+
const items = this.querySelectorAll(':scope > ui-timeline-item');
|
|
390
|
+
return Array.from(items).map((el) => this.#effectiveStatus(el));
|
|
391
|
+
}
|
|
392
|
+
#effectiveStatus(item) {
|
|
393
|
+
return this.#truncated.has(item) ? 'warning' : item.status;
|
|
394
|
+
}
|
|
395
|
+
/** Scroll the item's END into view IFF the guard holds — smooth by default, instant under reduced-
|
|
396
|
+
* motion (the a2a-artifact-feed `revealScroll` mechanism, TKT-0004, promoted to component behaviour).
|
|
397
|
+
* Deferred past a second rAF so a lazily-laid-out entry has settled layout before the target is read.
|
|
398
|
+
* `scrollIntoView` is guarded (absent in jsdom, the test environment's own gap — a real browser always
|
|
399
|
+
* has it) so the imperative API stays jsdom-safe without changing browser behaviour at all. */
|
|
400
|
+
#tailFollow(item) {
|
|
401
|
+
if (!this.#stuckToBottom)
|
|
402
|
+
return;
|
|
403
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
404
|
+
if (typeof item.scrollIntoView !== 'function')
|
|
405
|
+
return;
|
|
406
|
+
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
407
|
+
item.scrollIntoView({ behavior: reduce ? 'auto' : 'smooth', block: 'end' });
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
410
|
+
/** Recompute the stick-to-bottom guard on user scroll — scrolling UP (away from the bottom) pins the
|
|
411
|
+
* viewport against new arrivals; scrolling back down within the threshold resumes follow (SPEC-R10 AC2). */
|
|
412
|
+
#trackStickToBottom() {
|
|
413
|
+
this.#stuckToBottom = this.scrollHeight - this.scrollTop - this.clientHeight <= STICK_THRESHOLD_PX;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (!customElements.get('ui-status-stream'))
|
|
417
|
+
customElements.define('ui-status-stream', UIStatusStreamElement);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/* swatch.css — ui-swatch, the Display-class color-identity leaf (LLD-C3, token-surfaces.lld.md §3.1;
|
|
2
|
+
* SPEC-R2/R13/R14/R16). Two clearly-SECTIONED blocks:
|
|
3
|
+
*
|
|
4
|
+
* [1] TOKEN BLOCK — `:where(ui-swatch)` (specificity 0,0,0) declares the `--ui-swatch-*` chain: the
|
|
5
|
+
* whole-shape floor (SPEC-R13 AC1 — an EXPLICIT `2rem` box, never `auto`: a swatch has no intrinsic
|
|
6
|
+
* content, so `auto` would collapse to 0 in a shrink-to-fit flex item), the hairline border (SPEC-R2
|
|
7
|
+
* AC3 — so a surface-colored swatch never disappears into the page), and the density-RESPONSIVE
|
|
8
|
+
* box↔text gap (rides `--md-sys-space` × `[density]` for free).
|
|
9
|
+
* [2] STYLES BLOCK — `@scope (ui-swatch)` consumes ONLY `--ui-swatch-*` (∪ the shared `--md-sys-color-*`/
|
|
10
|
+
* `--md-sys-typescale-*` namespaces). Display-class (SPEC-R16): no `[size]`/`[scale]` row, no control
|
|
11
|
+
* height — the box is a fixed token size, the value text reads the body-medium type row directly.
|
|
12
|
+
*
|
|
13
|
+
* WHCM (SPEC-R14): a color box cannot paint under `forced-colors: active` — the box degrades to a
|
|
14
|
+
* `CanvasText` hairline (never a fake system color); the value/label text is real text and survives
|
|
15
|
+
* untouched. `!important` because a color the author set INLINE (`box.style.background`, swatch.ts) would
|
|
16
|
+
* otherwise win over the media block (inline styles beat an author-origin stylesheet rule).
|
|
17
|
+
*
|
|
18
|
+
* RTL is free: `column-gap` + logical inline-grid flow mirror under `dir="rtl"` — no physical property
|
|
19
|
+
* anywhere in this file.
|
|
20
|
+
*
|
|
21
|
+
* Standalone stylesheet — linked via the component-styles barrel; NEVER injected from swatch.ts
|
|
22
|
+
* (behaviour-only `.ts`, no runtime style injection — plan §2).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/* ════════════════ [1] TOKEN BLOCK — :where(ui-swatch) declares --ui-swatch-* ════════════════ */
|
|
26
|
+
|
|
27
|
+
:where(ui-swatch) {
|
|
28
|
+
--ui-swatch-box-size: 2rem; /* the deterministic default box (whole-shape, SPEC-R13 AC1) — NOT auto */
|
|
29
|
+
--ui-swatch-border: 1px solid var(--md-sys-color-neutral-outline-variant); /* the hairline so a surface color never vanishes (SPEC-R2 AC3) */
|
|
30
|
+
--ui-swatch-radius: var(--md-sys-shape-corner-base);
|
|
31
|
+
--ui-swatch-gap: var(--md-sys-space-xs); /* box↔text gap — rides [density] for free (ADR-0103) */
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* ════════════════ [2] STYLES BLOCK — @scope (ui-swatch) consumes ONLY --ui-swatch-* ════════════════ */
|
|
35
|
+
|
|
36
|
+
@scope (ui-swatch) {
|
|
37
|
+
:scope {
|
|
38
|
+
display: inline-grid;
|
|
39
|
+
grid-auto-flow: column;
|
|
40
|
+
align-items: center;
|
|
41
|
+
column-gap: var(--ui-swatch-gap);
|
|
42
|
+
color: inherit;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
:scope [data-part='box'] {
|
|
46
|
+
inline-size: var(--ui-swatch-box-size);
|
|
47
|
+
block-size: var(--ui-swatch-box-size);
|
|
48
|
+
border: var(--ui-swatch-border);
|
|
49
|
+
border-radius: var(--ui-swatch-radius);
|
|
50
|
+
flex: none;
|
|
51
|
+
box-sizing: border-box;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
:scope [data-part='value'] {
|
|
55
|
+
font-size: var(--md-sys-typescale-body-medium-size);
|
|
56
|
+
line-height: var(--md-sys-typescale-body-medium-line-height);
|
|
57
|
+
overflow-wrap: anywhere;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@media (forced-colors: active) {
|
|
61
|
+
:scope [data-part='box'] {
|
|
62
|
+
background: transparent !important; /* an inline author-set background otherwise wins over this rule */
|
|
63
|
+
border-color: CanvasText;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { UIElement, type ReactiveProps } from '../../dom/index.js';
|
|
2
|
+
declare const props: {
|
|
3
|
+
color: import("../../index.js").PropConfig<string>;
|
|
4
|
+
label: {
|
|
5
|
+
reflect: true;
|
|
6
|
+
type: import("../../index.js").PropType<string>;
|
|
7
|
+
default: string;
|
|
8
|
+
attribute?: string | false;
|
|
9
|
+
};
|
|
10
|
+
scheme: import("../../index.js").PropConfig<"auto" | "light" | "dark">;
|
|
11
|
+
};
|
|
12
|
+
export interface UISwatchElement extends ReactiveProps<typeof props> {
|
|
13
|
+
}
|
|
14
|
+
export declare class UISwatchElement extends UIElement {
|
|
15
|
+
#private;
|
|
16
|
+
static props: {
|
|
17
|
+
color: import("../../index.js").PropConfig<string>;
|
|
18
|
+
label: {
|
|
19
|
+
reflect: true;
|
|
20
|
+
type: import("../../index.js").PropType<string>;
|
|
21
|
+
default: string;
|
|
22
|
+
attribute?: string | false;
|
|
23
|
+
};
|
|
24
|
+
scheme: import("../../index.js").PropConfig<"auto" | "light" | "dark">;
|
|
25
|
+
};
|
|
26
|
+
protected connected(): void;
|
|
27
|
+
}
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// swatch.ts — UISwatchElement, the Display-class color-identity leaf (LLD-C2, token-surfaces.lld.md §3.1;
|
|
2
|
+
// SPEC-R1…R4; ADR-0118 cl.1/2/4). BEHAVIOUR + props + the composed-name ARIA + the two component-built child
|
|
3
|
+
// nodes + self-define ONLY — the value-lane/hardening math lives in the shared `_token-surface/token-surface.ts`
|
|
4
|
+
// (LLD-C1), the box/token geometry in swatch.css.
|
|
5
|
+
//
|
|
6
|
+
// A swatch is data, not decoration (ADR-0118 cl.4): the host carries `role=img` via `ElementInternals` (never a
|
|
7
|
+
// host attribute) — a CONSTANT semantic, set once in `connected()` (the `ui-icon`/`ui-bar-chart` precedent). Its
|
|
8
|
+
// accessible name is a COMPOSED string (`label`, `', '`, `value` — see `#composeName`): there is no silent
|
|
9
|
+
// state, with or without `label` — the color itself is never announced (it cannot be spoken), only its name/
|
|
10
|
+
// value. The SAME composed string is also the visible `[data-part='value']` text content (SPEC-R2: "the label/
|
|
11
|
+
// value are real DOM text… AT-visible") — one string, two surfaces (the printed datum and the accessible name
|
|
12
|
+
// stay in lockstep by construction, never two independently-maintained copies).
|
|
13
|
+
//
|
|
14
|
+
// Content model — TWO component-built nodes, built ONCE and mutated in place (not a `replaceChildren` rebuild
|
|
15
|
+
// per change, unlike the chart family's whole-array row lists): a swatch has a fixed 2-node shape
|
|
16
|
+
// (`[data-part='box']` + `[data-part='value']`), so the render effect builds them lazily on first run and only
|
|
17
|
+
// ever WRITES into them thereafter — `render()` stays the inherited no-op.
|
|
18
|
+
//
|
|
19
|
+
// Imports inward only (controls → dom + the sibling shared helper): UIElement + prop + the typed-schema helpers
|
|
20
|
+
// from the dom barrel; `cssValue` from the shared `_token-surface` value-lane module (LLD-C1).
|
|
21
|
+
import { UIElement, prop } from "../../dom/index.js";
|
|
22
|
+
import { cssValue } from "../_token-surface/token-surface.js";
|
|
23
|
+
const props = {
|
|
24
|
+
color: prop.string(''), // a literal CSS color OR a --var name (cssValue routes it) — SPEC-R1. Renamed from `value` (TKT-0069 item 1 ruling: `value` = the FACE form value, reserved; the A2UI catalog keeps wire `value`, mapped in its bespoke factory)
|
|
25
|
+
label: { ...prop.string(''), reflect: true }, // the token name / caption — SPEC-R1
|
|
26
|
+
scheme: prop.enum(['auto', 'light', 'dark'], 'auto'), // the color-scheme pin — SPEC-R2
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* The composed accessible name (SPEC-R4): `label` (when non-empty) + `', '` + `value`; the value alone with no
|
|
30
|
+
* label; `'swatch'` when neither — never nameless, never a silent state. ALSO the visible `[data-part='value']`
|
|
31
|
+
* text content (SPEC-R2) — the printed datum and the accessible name are the SAME string by construction.
|
|
32
|
+
*/
|
|
33
|
+
function composeName(label, value) {
|
|
34
|
+
if (label && value)
|
|
35
|
+
return `${label}, ${value}`;
|
|
36
|
+
if (label)
|
|
37
|
+
return label;
|
|
38
|
+
if (value)
|
|
39
|
+
return value;
|
|
40
|
+
return 'swatch';
|
|
41
|
+
}
|
|
42
|
+
export class UISwatchElement extends UIElement {
|
|
43
|
+
static props = props;
|
|
44
|
+
#box;
|
|
45
|
+
#valueText;
|
|
46
|
+
connected() {
|
|
47
|
+
// A swatch is data, not decoration (SPEC-R4, ADR-0118 cl.4) — a constant semantic, set directly (not
|
|
48
|
+
// inside an effect); re-set on each connect (idempotent).
|
|
49
|
+
this.internals.role = 'img';
|
|
50
|
+
// The one render effect (reads value/label/scheme): builds the 2-node shape lazily on first run, then
|
|
51
|
+
// only ever WRITES into the existing nodes on every subsequent change (SPEC-R2/R3/R4).
|
|
52
|
+
this.effect(() => {
|
|
53
|
+
if (!this.#box) {
|
|
54
|
+
this.#box = document.createElement('span');
|
|
55
|
+
this.#box.setAttribute('data-part', 'box');
|
|
56
|
+
this.#valueText = document.createElement('span');
|
|
57
|
+
this.#valueText.setAttribute('data-part', 'value');
|
|
58
|
+
this.replaceChildren(this.#box, this.#valueText);
|
|
59
|
+
}
|
|
60
|
+
const box = this.#box;
|
|
61
|
+
const valueText = this.#valueText;
|
|
62
|
+
// SPEC-R2/R3: empty/invalid/undefined-var value → the browser resolves `background` to transparent (or
|
|
63
|
+
// drops the declaration); the box's own hairline border (swatch.css) always carries the shape.
|
|
64
|
+
box.style.background = cssValue(this.color);
|
|
65
|
+
// SPEC-R2: the scheme pin — 'auto' sets nothing (ambient inheritance); light/dark pins color-scheme.
|
|
66
|
+
box.style.colorScheme = this.scheme === 'auto' ? '' : this.scheme;
|
|
67
|
+
const name = composeName(this.label, this.color);
|
|
68
|
+
valueText.textContent = name;
|
|
69
|
+
this.internals.ariaLabel = name;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!customElements.get('ui-swatch'))
|
|
74
|
+
customElements.define('ui-swatch', UISwatchElement); // idempotent self-define
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* swiper-item.css — ui-swiper-item's own sheet (ADR-0124 Consequences, the family-root resolution;
|
|
2
|
+
* swiper-family.lld.md LLD-C4/§7 REV). No `--ui-swiper-item-*` tokens of its own — a slide is sized
|
|
3
|
+
* ENTIRELY by the owning track, so this file DECLARES nothing (an empty :where() token block would be
|
|
4
|
+
* vacuous; there is none) and only CONSUMES the family `--ui-swiper-*` prefix swiper.css declares
|
|
5
|
+
* (`--ui-swiper-align`) — the family-root consumption rule the amended family-coherence.test.ts invariant
|
|
6
|
+
* B allows for a non-primary descriptor in a multi-descriptor folder. Standalone; never injected from a
|
|
7
|
+
* .ts (behaviour-only .ts, no runtime style injection — plan §2). */
|
|
8
|
+
|
|
9
|
+
@scope (ui-swiper-item) {
|
|
10
|
+
/* Sized by the track; snap-align/stop off the family tokens; no geometry of its own (SPEC-R9). */
|
|
11
|
+
:scope {
|
|
12
|
+
min-inline-size: 0;
|
|
13
|
+
min-block-size: 0;
|
|
14
|
+
scroll-snap-align: var(--ui-swiper-align);
|
|
15
|
+
scroll-snap-stop: always;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { UIElement } from '../../dom/element.js';
|
|
2
|
+
import { type ReactiveProps } from '../../dom/index.js';
|
|
3
|
+
declare const props: {
|
|
4
|
+
key: {
|
|
5
|
+
reflect: true;
|
|
6
|
+
type: import("../../index.js").PropType<string>;
|
|
7
|
+
default: string;
|
|
8
|
+
attribute?: string | false;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
export interface UISwiperItemElement extends ReactiveProps<typeof props> {
|
|
12
|
+
}
|
|
13
|
+
export declare class UISwiperItemElement extends UIElement {
|
|
14
|
+
static props: {
|
|
15
|
+
key: {
|
|
16
|
+
reflect: true;
|
|
17
|
+
type: import("../../index.js").PropType<string>;
|
|
18
|
+
default: string;
|
|
19
|
+
attribute?: string | false;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
protected connected(): void;
|
|
23
|
+
/**
|
|
24
|
+
* Label this REAL slide (called by the owning `ui-swiper`, never a sibling — `internals` is protected).
|
|
25
|
+
* Sets `role=group`, `aria-roledescription='slide'`, and `aria-label` to the supplied position string
|
|
26
|
+
* (`"{n} of {realCount}"`, SPEC-R9/R11) — all via `ElementInternals`, never a host attribute. Idempotent:
|
|
27
|
+
* safe to call again with a new position string as the real-slide set shifts (resize/mutation rebuilds).
|
|
28
|
+
*/
|
|
29
|
+
labelAs(position: string): void;
|
|
30
|
+
}
|
|
31
|
+
export {};
|