@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,360 @@
|
|
|
1
|
+
// timeline-item.ts — UITimelineItemElement, the timeline family's shared INERT visual atom
|
|
2
|
+
// (timeline-family.lld.md §2 · SPEC-R1…R5 · ADR-0122 F1/F2/F3/F6; recursive nesting + the
|
|
3
|
+
// collapsed-summary preview — ADR-0143 F1-F3/F7, TKT-0091). BEHAVIOUR + props + the control-built
|
|
4
|
+
// marker/content/detail/nested anatomy + self-define ONLY. Anatomy/geometry per the LLD; styling lives
|
|
5
|
+
// in timeline-item.css, the public contract in timeline-item.md.
|
|
6
|
+
//
|
|
7
|
+
// One rail row = a marker (dot/ring/pulse via CSS, OR a built-in check/x glyph, OR a consumer-slotted
|
|
8
|
+
// marker) + the content roles (label · description · timestamp · trailing) + an optional collapsible
|
|
9
|
+
// detail/nested pair. Inert: holds no transport, no live-region role, emits ONLY `toggle` (the composed
|
|
10
|
+
// detail's own event — never a bespoke one). `internals.role = 'listitem'` is set in the CONSTRUCTOR
|
|
11
|
+
// (the toast role precedent — semantics before insertion), extends `UIElement` (NOT form-associated).
|
|
12
|
+
// Hosted by BOTH `ui-timeline` (durable) and `ui-status-stream` (live) — authored once, shared everywhere.
|
|
13
|
+
//
|
|
14
|
+
// Anatomy (#ensureAnatomy(), idempotent — ONCE, persists across reconnect, the toast/disclosure part-
|
|
15
|
+
// persistence precedent): a `<span data-part="marker">` (dot/connector painted by timeline-item.css;
|
|
16
|
+
// suppressed and replaced by a real glyph child when `icon` is set, a `[data-role="marker"]` consumer
|
|
17
|
+
// child is present, or `status` is `done`/`error` — SPEC-R3/R4) + four content cells keyed by
|
|
18
|
+
// `data-role` (`label`/`description`/`timestamp`/`trailing`). A pre-existing light-DOM child carrying one
|
|
19
|
+
// of those `[data-role]`s is ADOPTED (kept, never cloned — ADR-0022) and marked consumer-owned so
|
|
20
|
+
// `#renderContent()` never stamps over it (the adia wrapper-trap regression this build guards against);
|
|
21
|
+
// otherwise a fresh cell is created and the reactive effect stamps it from the matching prop. `trailing`
|
|
22
|
+
// has no prop source by default — it is consumer-content-only UNLESS the collapsed-summary preview
|
|
23
|
+
// auto-fills it (ADR-0143 F3, below), which backs off the instant a consumer adopts it themselves.
|
|
24
|
+
//
|
|
25
|
+
// A pre-existing `[data-role="detail"]` AND/OR `[data-role="nested"]` child are moved into ONE shared
|
|
26
|
+
// composed `ui-disclosure` (§2.3, ADR-0143 F1/F2) — NOT a bespoke caret+hidden reimplementation, and NOT
|
|
27
|
+
// two independent disclosures: `detail` (free consumer content) is appended first, `nested` (a genuine
|
|
28
|
+
// `<ui-timeline>` child — ADR-0143 F1, reusing `ui-timeline` itself rather than inventing a second
|
|
29
|
+
// nesting primitive) second, into the SAME disclosure host, which materializes only when either exists.
|
|
30
|
+
// A LIVE host (ui-status-stream's grouping, ADR-0146 F5) that needs a nested slot on an ALREADY-connected
|
|
31
|
+
// item calls `ensureNestedSlot(factory)` — the ONE public method the ADR-0143 2026-07-18 amendment adds
|
|
32
|
+
// (Kim's narrow, additive exception): it composes the EXACT SAME disclosure + nested slot + preview
|
|
33
|
+
// mechanism lazily, on first call, instead of eagerly at connect. Dead code for the authored-markup case.
|
|
34
|
+
// Recursion is authored-markup depth, not runtime-generated: a nested item can carry its OWN
|
|
35
|
+
// `[data-role="nested"]` child, arbitrary levels deep, with zero extra code — `ui-timeline`'s own
|
|
36
|
+
// `#markLastItem()` is already `:scope > ui-timeline-item`-scoped (verified against source, ADR-0143's
|
|
37
|
+
// own finding), so a nested timeline's terminal-connector marking self-scopes per level with NO change
|
|
38
|
+
// here; likewise `size` never cascades into a nested `<ui-timeline>` (ADR-0143 F7) — each level reads
|
|
39
|
+
// its OWN `[size]` attribute, no forwarding mechanism exists or is added.
|
|
40
|
+
// `this.append(marker, ...cells, [disclosure])` re-parents every node (fresh or adopted) into anatomy
|
|
41
|
+
// order in one call — `Node.append` on an already-connected child simply repositions it, so adoption and
|
|
42
|
+
// creation are handled uniformly by one assembly line.
|
|
43
|
+
//
|
|
44
|
+
// The marker glyph (#renderMarkerGlyph(), a SEPARATE effect from content — status/icon changes should
|
|
45
|
+
// never re-stamp label/description/timestamp, and vice versa): a consumer-adopted `[data-role="marker"]`
|
|
46
|
+
// child is NEVER touched. Otherwise: `icon` non-empty wins (F3, "a free marker coexists with status,
|
|
47
|
+
// orthogonal") — `resolveIcon(icon)` is injected and tagged `data-role="marker"` so the SAME CSS
|
|
48
|
+
// suppression rule fires; else a resolved-outcome `status` (`done`/`error`/`warning`, ADR-0146 F7) injects
|
|
49
|
+
// its built-in glyph (`check`/`x`/`warning` — @agent-ui-kit/icons, already fleet-adopted; disclosure's chevron
|
|
50
|
+
// / toast's close-x precedent) tagged the same way, each a DISTINCT shape (ADR-0057: warning's triangle is
|
|
51
|
+
// never error's `x` recoloured); else (`''`/`pending`/`active`) the marker is cleared and CSS
|
|
52
|
+
// `::before`/`::after` paint the dot/ring/pulse — the non-color SHAPE signifier (ADR-0057, SPEC-R4).
|
|
53
|
+
//
|
|
54
|
+
// The collapsed-summary preview (#renderTrailingPreview(), ADR-0143 F3): when `[data-role="nested"]`
|
|
55
|
+
// content exists, a `MutationObserver` on that nested subtree tracks the LAST `<ui-timeline-item>` in
|
|
56
|
+
// DOM order — recursing to the deepest leaf when THAT item itself nests further (last child wins, no
|
|
57
|
+
// status-priority, matching the ticket's own resolved rule) — and a reactive effect reads the shared
|
|
58
|
+
// disclosure's own `open` prop (a DIFFERENT element's reactive signal; reading it inside `this.effect()`
|
|
59
|
+
// still tracks it — signals are not scoped to their declaring element) to gate whether the resolved
|
|
60
|
+
// label+status is PAINTED into the item's own EXISTING `trailing` cell (closed) or left untouched
|
|
61
|
+
// (open, where the real nested content is already visible — no duplicate). `trailing` remains
|
|
62
|
+
// `#consumerOwned`-gated exactly as today: a consumer-authored `[data-role="trailing"]` child is NEVER
|
|
63
|
+
// overwritten by this effect, at any open/closed state.
|
|
64
|
+
//
|
|
65
|
+
// `controls → dom + controls/disclosure/disclosure.ts + @agent-ui-kit/icons` — the allowed import direction
|
|
66
|
+
// (cross-control, the segmented-control/radio + disclosure/icons precedents). The nested slot's content
|
|
67
|
+
// is treated as generic `Element`/DOM (querySelector/MutationObserver/attributes only) — never importing
|
|
68
|
+
// `ui-timeline`'s own module or type, so no `timeline-item → timeline → timeline-item` cycle is created
|
|
69
|
+
// (timeline.ts already imports timeline-item.ts for registration).
|
|
70
|
+
import { UIElement, prop } from "../../dom/index.js";
|
|
71
|
+
import "../disclosure/disclosure.js"; // the collapse mechanism (F6) — composed, not reinvented
|
|
72
|
+
import { resolveIcon } from '@agent-ui-kit/icons';
|
|
73
|
+
const STATUS = ['', 'pending', 'active', 'done', 'error', 'warning'];
|
|
74
|
+
const SIZE = ['sm', 'md', 'lg'];
|
|
75
|
+
// The resolved-outcome states that inject a built-in marker glyph (the in-progress '' / pending / active
|
|
76
|
+
// states paint a pure-CSS dot/ring/pulse instead). `warning` (ADR-0146 F7) joins done/error here with its
|
|
77
|
+
// OWN distinct triangle-exclamation glyph — shape-coded, never hue-alone (ADR-0057): the shape alone,
|
|
78
|
+
// never the colour, distinguishes warning from error's `x`.
|
|
79
|
+
const STATUS_GLYPH = { done: 'check', error: 'x', warning: 'warning' };
|
|
80
|
+
// A GROUP item's OWN glyph set (an item hosting a nested sub-timeline, `#nested` — ADR-0146 F5) — the
|
|
81
|
+
// Figma "Claude Code Gateway" reasoning-chain card (node 21:1641-1643): a card-level marker reads as a
|
|
82
|
+
// distinct, prominent status glyph (a spinning ring while working, a circled check when done, a circled
|
|
83
|
+
// X on error), never the small plain dot/bare-check/cross a LEAF step's marker uses. `error` gets its OWN
|
|
84
|
+
// `x-circle` glyph, NOT `warning`'s triangle reused (component-review finding, ADR-0057): the two states
|
|
85
|
+
// must stay SHAPE-distinct, not ink-hue-distinct only, the same law the leaf glyph set already honors —
|
|
86
|
+
// reusing `warning`'s shape for `error` at the group level would make them distinguishable by colour alone.
|
|
87
|
+
const GROUP_STATUS_GLYPH = { active: 'circle-notch', done: 'check-circle', error: 'x-circle', warning: 'warning' };
|
|
88
|
+
const props = {
|
|
89
|
+
// reflected (SPEC-R2 AC2: a JS-set status/size round-trips through getAttribute) — the LLD's frozen
|
|
90
|
+
// interface names the constructors; the CSS [status]/[size] repoint (and the AC2 round-trip itself)
|
|
91
|
+
// requires `reflect: true`, the container.ts/disclosure.ts convention for every attribute-selector-driven prop.
|
|
92
|
+
status: { ...prop.enum(STATUS, ''), reflect: true }, // '' = neutral marker (F3)
|
|
93
|
+
label: { ...prop.string(''), reflect: true },
|
|
94
|
+
description: prop.string(''),
|
|
95
|
+
timestamp: prop.string(''), // the consumer's string — NO codec (F6)
|
|
96
|
+
icon: prop.string(''), // a marker glyph name replacing the dot (adia icon-mode)
|
|
97
|
+
size: { ...prop.enum(SIZE, 'md'), reflect: true }, // first-class geometry (F2)
|
|
98
|
+
};
|
|
99
|
+
const CONTENT_ROLES = ['label', 'description', 'timestamp', 'trailing'];
|
|
100
|
+
const STAMPED_ROLES = new Set(['label', 'description', 'timestamp']);
|
|
101
|
+
export class UITimelineItemElement extends UIElement {
|
|
102
|
+
static props = props;
|
|
103
|
+
// The control-built parts — created ONCE (idempotent guard in #ensureAnatomy); persist through
|
|
104
|
+
// disconnect/reconnect (the toast/disclosure precedent). `#`-private: nothing outside can observe them.
|
|
105
|
+
#marker = null;
|
|
106
|
+
#cells = new Map();
|
|
107
|
+
#consumerOwned = new Set(); // roles adopted from a pre-existing consumer child — never stamped
|
|
108
|
+
#consumerMarker = false; // a consumer supplied a [data-role="marker"] child at connect — the item's own glyph logic backs off entirely
|
|
109
|
+
#disclosure = null;
|
|
110
|
+
#nested = null; // the adopted [data-role="nested"] child (ADR-0143 F1 — a genuine <ui-timeline>, treated as generic DOM)
|
|
111
|
+
#nestedObserver = null; // recomputes the collapsed-summary preview on any change to #nested's subtree
|
|
112
|
+
constructor() {
|
|
113
|
+
super();
|
|
114
|
+
this.internals.role = 'listitem'; // set in the CONSTRUCTOR (the toast role precedent) — semantics before insertion
|
|
115
|
+
}
|
|
116
|
+
connected() {
|
|
117
|
+
this.#ensureAnatomy(); // idempotent, ONCE (the toast/modal/disclosure part-persistence guard)
|
|
118
|
+
this.effect(() => this.#renderContent()); // re-stamps label/description/timestamp on prop change
|
|
119
|
+
this.effect(() => this.#renderMarkerGlyph()); // re-paints the marker glyph on status/icon change — a SEPARATE effect
|
|
120
|
+
if (this.#nested) {
|
|
121
|
+
this.#nestedObserver = new MutationObserver(() => this.#renderTrailingPreview());
|
|
122
|
+
this.#nestedObserver.observe(this.#nested, { subtree: true, childList: true, attributes: true, attributeFilter: ['status', 'label'], characterData: true });
|
|
123
|
+
// Reads `this.#disclosure!.open` — a DIFFERENT element's reactive prop — so this effect re-runs on
|
|
124
|
+
// every open/close (ADR-0143 F3's paint gate) in addition to every observer-triggered recompute above.
|
|
125
|
+
this.effect(() => this.#renderTrailingPreview());
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
disconnected() {
|
|
129
|
+
this.#nestedObserver?.disconnect();
|
|
130
|
+
this.#nestedObserver = null;
|
|
131
|
+
}
|
|
132
|
+
/** Reveal/collapse the detail region (used by ui-status-stream's update({detail})); no-op if no detail. */
|
|
133
|
+
toggleDetail(open) {
|
|
134
|
+
const disclosure = this.#disclosure;
|
|
135
|
+
if (!disclosure)
|
|
136
|
+
return;
|
|
137
|
+
disclosure.open = open ?? !disclosure.open;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* ADR-0146 F5 (the ADR-0143 2026-07-18 amendment — Kim's narrow, additive exception) — LAZILY ensure a
|
|
141
|
+
* `[data-role="nested"]` slot exists on an ALREADY-CONNECTED item, for ui-status-stream's GROUPED entries.
|
|
142
|
+
* Returns the nested host, composing it into the SAME shared `ui-disclosure` `#ensureAnatomy()` uses at
|
|
143
|
+
* connect (ADR-0143 F1/F2) — never a second nesting mechanism (the family-coherence law the amendment
|
|
144
|
+
* preserves). The collapsed-summary preview + its `MutationObserver` (ADR-0143 F3) are armed here EXACTLY as
|
|
145
|
+
* `connected()` arms them for an authored nested slot, so the group's collapse + preview work with ZERO
|
|
146
|
+
* bespoke nesting code. This is the ONLY new public surface the amendment adds; it is a dead code path for
|
|
147
|
+
* the durable authored-markup host (which composes `nested` eagerly at connect) — a live host is its sole
|
|
148
|
+
* caller. `factory` mints the host (ui-status-stream passes a fresh `<ui-timeline>`) so THIS file stays
|
|
149
|
+
* import-free of `ui-timeline` (the cycle guard, file header). Idempotent: a second call returns the
|
|
150
|
+
* existing slot untouched. Must run while connected — it registers a scope-owned effect (`this.effect`
|
|
151
|
+
* throws outside the connected lifetime); ui-status-stream only calls it on an item already appended live.
|
|
152
|
+
*/
|
|
153
|
+
ensureNestedSlot(factory) {
|
|
154
|
+
if (this.#nested)
|
|
155
|
+
return this.#nested;
|
|
156
|
+
const nested = factory();
|
|
157
|
+
nested.setAttribute('data-role', 'nested');
|
|
158
|
+
if (this.#disclosure) {
|
|
159
|
+
// The item already composed a disclosure (it was authored WITH `detail`) — append `nested` onto the
|
|
160
|
+
// disclosure host; its own childList heal observer (disclosure.ts `#heal`) relocates it into the body
|
|
161
|
+
// part (the ADR-0143 F2 two-hop adoption, just deferred to first-group time).
|
|
162
|
+
this.#disclosure.appendChild(nested);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
// No disclosure yet — compose one, adopting `nested` BEFORE the disclosure connects so its own
|
|
166
|
+
// `#ensureParts()` moves it into the body SYNCHRONOUSLY (the exact composition `#ensureAnatomy` runs at
|
|
167
|
+
// connect, deferred to the moment the first grouped child arrives).
|
|
168
|
+
const disclosure = document.createElement('ui-disclosure');
|
|
169
|
+
disclosure.setAttribute('data-part', 'detail');
|
|
170
|
+
disclosure.appendChild(nested);
|
|
171
|
+
this.#disclosure = disclosure;
|
|
172
|
+
this.append(disclosure);
|
|
173
|
+
}
|
|
174
|
+
this.#nested = nested;
|
|
175
|
+
// Re-evaluate the marker glyph NOW — this item just became a group parent, and GROUP_STATUS_GLYPH
|
|
176
|
+
// (#renderMarkerGlyph) only applies once `#nested` is set. The reactive effect that normally drives
|
|
177
|
+
// this reruns on a status/icon CHANGE, which may not happen at the moment nesting begins (a status
|
|
178
|
+
// unchanged by the new child's escalation would leave the OLD leaf-style glyph painted).
|
|
179
|
+
this.#renderMarkerGlyph();
|
|
180
|
+
// Arm the preview observer + paint-gate effect exactly as `connected()` does for an authored nested slot
|
|
181
|
+
// (ADR-0143 F3). On a later disconnect/reconnect, `connected()` re-arms both because `#nested` is now set
|
|
182
|
+
// — so the lazily-mounted slot survives reconnect identically to an authored one.
|
|
183
|
+
this.#nestedObserver = new MutationObserver(() => this.#renderTrailingPreview());
|
|
184
|
+
this.#nestedObserver.observe(nested, { subtree: true, childList: true, attributes: true, attributeFilter: ['status', 'label'], characterData: true });
|
|
185
|
+
this.effect(() => this.#renderTrailingPreview());
|
|
186
|
+
return nested;
|
|
187
|
+
}
|
|
188
|
+
/** Mark/unmark this item TRUNCATED — ui-status-stream's completion invariant (SPEC-R11): a distinct,
|
|
189
|
+
* non-color-only interrupted affordance via a real custom state (`:state(truncated)` in CSS), never a
|
|
190
|
+
* silent forever-spinner. `internals.states` is optional-chained (absent in jsdom, the fleet convention —
|
|
191
|
+
* button.ts/checkbox.ts/text-field.ts). Imperative/CSS-state, not a `static props` field (LLD §7/§8). */
|
|
192
|
+
markTruncated(truncated) {
|
|
193
|
+
if (truncated)
|
|
194
|
+
this.internals.states?.add('truncated');
|
|
195
|
+
else
|
|
196
|
+
this.internals.states?.delete('truncated');
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Build the light-DOM anatomy ONCE (idempotent — the toast/disclosure part-persistence precedent).
|
|
200
|
+
* Pre-existing `[data-role]` children are ADOPTED (kept in place, marked consumer-owned); everything
|
|
201
|
+
* else is created fresh. `this.append(...)` re-parents every node — adopted or fresh — into the final
|
|
202
|
+
* anatomy order in one call (a no-op reposition for an already-connected child, ADR-0022's move
|
|
203
|
+
* semantics via the platform's own `Node.append`).
|
|
204
|
+
*/
|
|
205
|
+
#ensureAnatomy() {
|
|
206
|
+
if (this.#marker)
|
|
207
|
+
return;
|
|
208
|
+
const marker = document.createElement('span');
|
|
209
|
+
marker.setAttribute('data-part', 'marker');
|
|
210
|
+
const consumerMarker = this.querySelector(':scope > [data-role="marker"]');
|
|
211
|
+
if (consumerMarker) {
|
|
212
|
+
marker.appendChild(consumerMarker); // adopted, moved (ADR-0022)
|
|
213
|
+
this.#consumerMarker = true;
|
|
214
|
+
}
|
|
215
|
+
for (const role of CONTENT_ROLES) {
|
|
216
|
+
const existing = this.querySelector(`:scope > [data-role="${role}"]`);
|
|
217
|
+
if (existing instanceof HTMLElement) {
|
|
218
|
+
this.#cells.set(role, existing); // adopted in place — the wrapper-trap guard: never re-stamped
|
|
219
|
+
this.#consumerOwned.add(role);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
const cell = document.createElement('span');
|
|
223
|
+
cell.setAttribute('data-role', role);
|
|
224
|
+
this.#cells.set(role, cell);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// ONE shared disclosure adopts detail THEN nested, in that order (ADR-0143 F1/F2) — materializes
|
|
228
|
+
// only when either exists; an item with neither composes no disclosure at all (unchanged from today).
|
|
229
|
+
const detailContent = this.querySelector(':scope > [data-role="detail"]');
|
|
230
|
+
const nestedContent = this.querySelector(':scope > [data-role="nested"]');
|
|
231
|
+
if (detailContent || nestedContent) {
|
|
232
|
+
const disclosure = document.createElement('ui-disclosure');
|
|
233
|
+
disclosure.setAttribute('data-part', 'detail');
|
|
234
|
+
if (detailContent)
|
|
235
|
+
disclosure.appendChild(detailContent); // moved (ADR-0022)
|
|
236
|
+
if (nestedContent instanceof HTMLElement) {
|
|
237
|
+
disclosure.appendChild(nestedContent); // moved (ADR-0022) — the SAME two-hop adoption `detail` already used
|
|
238
|
+
this.#nested = nestedContent;
|
|
239
|
+
}
|
|
240
|
+
this.#disclosure = disclosure;
|
|
241
|
+
// NO re-emit — the composed ui-disclosure's own `toggle` (bubbles+composed, element.ts's `emit()`)
|
|
242
|
+
// already surfaces on the item host via natural light-DOM bubbling (the SAME event object, not a
|
|
243
|
+
// second one); an explicit re-emit here would double-fire. Allowlisted, no new event name (F6).
|
|
244
|
+
}
|
|
245
|
+
this.append(marker, ...CONTENT_ROLES.map((r) => this.#cells.get(r)), ...(this.#disclosure ? [this.#disclosure] : []));
|
|
246
|
+
this.#marker = marker;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* ADR-0143 F3 — resolve the LAST `<ui-timeline-item>` in DOM order under `#nested`, recursing to the
|
|
250
|
+
* deepest leaf when that item itself carries a further `[data-role="nested"]` child (last child wins,
|
|
251
|
+
* no status-priority — the ticket's own resolved rule). Returns `null` when `#nested` has no items yet
|
|
252
|
+
* (a plain no-op source, never an error). Pure DOM traversal — `#nested`'s content is never assumed to
|
|
253
|
+
* be a real `<ui-timeline>` beyond "an element whose direct children may be `ui-timeline-item`s."
|
|
254
|
+
*/
|
|
255
|
+
#resolveLastDescendant(root) {
|
|
256
|
+
const items = root.querySelectorAll(':scope > ui-timeline-item');
|
|
257
|
+
const last = items[items.length - 1];
|
|
258
|
+
if (!(last instanceof HTMLElement))
|
|
259
|
+
return null;
|
|
260
|
+
// NOT :scope-restricted to a DIRECT child: by the time this runs, `last`'s OWN connected() may already
|
|
261
|
+
// have moved its [data-role="nested"] child into its composed <ui-disclosure data-part="detail"> body
|
|
262
|
+
// (connectedCallback fires parent-before-child on a bulk insert, but this resolution runs from a
|
|
263
|
+
// deferred effect, which fires after the whole subtree's synchronous connect pass — so a deeper item can
|
|
264
|
+
// already be relocated). The second branch below still finds it as a direct child of the disclosure's
|
|
265
|
+
// OWN `[data-part="body"]` — narrower than a bare subtree query, so a consumer who (misusing the
|
|
266
|
+
// reserved `nested` role) plants `[data-role="nested"]` somewhere inside free-form `detail` prose is
|
|
267
|
+
// never mistaken for `last`'s real nested slot.
|
|
268
|
+
const deeper = last.querySelector(':scope > [data-role="nested"], :scope > [data-part="detail"] [data-part="body"] > [data-role="nested"]');
|
|
269
|
+
if (deeper instanceof HTMLElement) {
|
|
270
|
+
const recursed = this.#resolveLastDescendant(deeper);
|
|
271
|
+
if (recursed)
|
|
272
|
+
return recursed;
|
|
273
|
+
}
|
|
274
|
+
return last;
|
|
275
|
+
}
|
|
276
|
+
/** The SAME text-based, non-color status-shape glyph the fleet already uses elsewhere for a compact
|
|
277
|
+
* inline status echo (the agent-admin client-message-echo precedent) — a distinct SHAPE per status,
|
|
278
|
+
* never color alone (ADR-0057), with no new CSS/icon-injection machinery for a one-line preview cell. */
|
|
279
|
+
#previewGlyph(status) {
|
|
280
|
+
if (status === 'done')
|
|
281
|
+
return '✓ ';
|
|
282
|
+
if (status === 'error')
|
|
283
|
+
return '✕ ';
|
|
284
|
+
if (status === 'warning')
|
|
285
|
+
return '▲ '; // ADR-0146 F7 — a distinct SHAPE (a triangle), never hue alone (ADR-0057)
|
|
286
|
+
if (status === 'active')
|
|
287
|
+
return '● ';
|
|
288
|
+
if (status === 'pending')
|
|
289
|
+
return '○ ';
|
|
290
|
+
return '';
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* ADR-0143 F3 — the collapsed-summary paint gate: while the shared disclosure is CLOSED (or absent —
|
|
294
|
+
* a no-op then, since there is nothing to summarize), paint the resolved last-descendant's status-shape
|
|
295
|
+
* glyph + label into the item's OWN `trailing` cell, unless a consumer has adopted `trailing`
|
|
296
|
+
* themselves (`#consumerOwned`, checked exactly as `#renderContent()` already does — never overwritten).
|
|
297
|
+
* While OPEN, clear the auto-fill (the real nested content is already visible; no duplicate). A no-op
|
|
298
|
+
* entirely when `#nested` is absent.
|
|
299
|
+
*/
|
|
300
|
+
#renderTrailingPreview() {
|
|
301
|
+
const nested = this.#nested;
|
|
302
|
+
if (!nested)
|
|
303
|
+
return;
|
|
304
|
+
const trailing = this.#cells.get('trailing');
|
|
305
|
+
if (!trailing || this.#consumerOwned.has('trailing'))
|
|
306
|
+
return;
|
|
307
|
+
const isOpen = this.#disclosure?.open ?? false;
|
|
308
|
+
if (isOpen) {
|
|
309
|
+
trailing.textContent = '';
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const last = this.#resolveLastDescendant(nested);
|
|
313
|
+
trailing.textContent = last ? `${this.#previewGlyph(last.getAttribute('status') ?? '')}${last.getAttribute('label') ?? ''}` : '';
|
|
314
|
+
}
|
|
315
|
+
/** Stamp label/description/timestamp from props onto their cells — SKIPPING any consumer-owned cell
|
|
316
|
+
* (the wrapper-trap regression: a wrapped consumer child must suppress the default stamp, not duplicate). */
|
|
317
|
+
#renderContent() {
|
|
318
|
+
for (const role of STAMPED_ROLES) {
|
|
319
|
+
if (this.#consumerOwned.has(role))
|
|
320
|
+
continue;
|
|
321
|
+
this.#cells.get(role).textContent = this[role];
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/** Paint the marker glyph: a consumer-adopted marker is untouched; `icon` wins over `status`; `done`/
|
|
325
|
+
* `error` get a built-in glyph; everything else clears to let CSS `::before`/`::after` paint the
|
|
326
|
+
* dot/ring/pulse (SPEC-R4). Every injected glyph is tagged `data-role="marker"` so ONE CSS suppression
|
|
327
|
+
* rule (`:has([data-role="marker"])`) fires uniformly for consumer/icon/status-glyph markers alike. */
|
|
328
|
+
/** Inject `name` as the marker glyph, tagged the way BOTH resolution paths below need it (`data-role`
|
|
329
|
+
* for the shared plain-dot suppression rule, `data-glyph` for the CSS spin-animation hook) — a single
|
|
330
|
+
* paint site so the icon-prop path and the status/group path can never silently diverge on how an
|
|
331
|
+
* injected glyph is tagged (code-review finding: the two branches had drifted into a hand-duplicated
|
|
332
|
+
* 4-line block each, already edited in lockstep once for `data-glyph`). */
|
|
333
|
+
#paintMarkerGlyph(name) {
|
|
334
|
+
const marker = this.#marker;
|
|
335
|
+
const svg = resolveIcon(name);
|
|
336
|
+
svg.setAttribute('data-role', 'marker');
|
|
337
|
+
svg.setAttribute('data-glyph', name);
|
|
338
|
+
marker.replaceChildren(svg);
|
|
339
|
+
}
|
|
340
|
+
#renderMarkerGlyph() {
|
|
341
|
+
const marker = this.#marker;
|
|
342
|
+
if (!marker || this.#consumerMarker)
|
|
343
|
+
return;
|
|
344
|
+
if (this.icon !== '') {
|
|
345
|
+
this.#paintMarkerGlyph(this.icon);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
// A group parent (`#nested` set — ADR-0146 F5) reads its OWN glyph set (GROUP_STATUS_GLYPH); a leaf
|
|
349
|
+
// step keeps the existing STATUS_GLYPH (bare check/x) + CSS dot/ring/pulse fallback.
|
|
350
|
+
const glyphMap = this.#nested ? GROUP_STATUS_GLYPH : STATUS_GLYPH;
|
|
351
|
+
const glyph = glyphMap[this.status];
|
|
352
|
+
if (glyph !== undefined) {
|
|
353
|
+
this.#paintMarkerGlyph(glyph);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
marker.replaceChildren(); // '' / pending / active(leaf) — pure CSS paints the dot/ring/pulse
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (!customElements.get('ui-timeline-item'))
|
|
360
|
+
customElements.define('ui-timeline-item', UITimelineItemElement);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/* toast-region.css — ui-toast-region, the single-file component stylesheet (ADR-0003; feed-family.
|
|
2
|
+
* lld.md LLD-C8 · SPEC-R12). Two clearly-SECTIONED blocks:
|
|
3
|
+
*
|
|
4
|
+
* [1] TOKEN BLOCK — `:where(ui-toast-region)` declares the `--ui-toast-region-*` chain: `inset`
|
|
5
|
+
* (block-end/inline-end fixed inset off `--md-sys-space-lg`) + `gap` (the inter-toast stacking gap
|
|
6
|
+
* off `--md-sys-space-sm`). No surface/radius/colour tokens AT ALL — the region paints NOTHING of
|
|
7
|
+
* its own (LLD-C8; the ui-form-provider "layout, no colour voice" precedent) — each `ui-toast`
|
|
8
|
+
* child supplies its own surface.
|
|
9
|
+
* [2] STYLES BLOCK — `@scope (ui-toast-region)` consumes ONLY `--ui-toast-region-*`. The host IS
|
|
10
|
+
* its own Popover-API top-layer element (`popover="manual"`, set in toast-region.ts) — the
|
|
11
|
+
* `:popover-open` state selector overrides the UA's default popover centering with a
|
|
12
|
+
* block-end/inline-end anchored FIXED position, then lays its `ui-toast` children out as a
|
|
13
|
+
* normal-flow flex column (oldest→newest, top→bottom — newest nearest the block-end inset edge,
|
|
14
|
+
* zero JS positioning; logical properties mirror RTL for free). `pointer-events: none` on the
|
|
15
|
+
* empty region area means it never intercepts page clicks (SPEC-R12) — re-enabled per-toast so
|
|
16
|
+
* each card (and its affordances) stays fully interactive.
|
|
17
|
+
*
|
|
18
|
+
* Standalone stylesheet — linked via the host page / component-styles barrel; NEVER injected from
|
|
19
|
+
* toast-region.ts (behaviour-only `.ts`, no runtime style injection — plan §2).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/* ══════════ [1] TOKEN BLOCK — :where(ui-toast-region) declares --ui-toast-region-* ══════════ */
|
|
23
|
+
|
|
24
|
+
:where(ui-toast-region) {
|
|
25
|
+
--ui-toast-region-inset: var(--md-sys-space-lg); /* block-end/inline-end fixed inset off the shell ramp */
|
|
26
|
+
--ui-toast-region-gap: var(--md-sys-space-sm); /* the inter-toast stacking gap */
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* ══════════ [2] STYLES BLOCK — @scope (ui-toast-region) consumes ONLY --ui-toast-region-* ══════════ */
|
|
30
|
+
|
|
31
|
+
@scope (ui-toast-region) {
|
|
32
|
+
/* The host IS the popover (not a separate part) — override the UA's default popover centering with
|
|
33
|
+
a block-end/inline-end anchored fixed position once it enters the top layer. */
|
|
34
|
+
:scope:popover-open {
|
|
35
|
+
position: fixed;
|
|
36
|
+
inset: auto; /* clear the UA popover default before setting the two edges below */
|
|
37
|
+
inset-block-end: var(--ui-toast-region-inset);
|
|
38
|
+
inset-inline-end: var(--ui-toast-region-inset);
|
|
39
|
+
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column; /* normal-flow stacking: oldest→newest, top→bottom (SPEC-R12 AC3) */
|
|
42
|
+
gap: var(--ui-toast-region-gap);
|
|
43
|
+
|
|
44
|
+
margin: 0;
|
|
45
|
+
border: 0;
|
|
46
|
+
padding: 0;
|
|
47
|
+
background: transparent; /* the region paints nothing of its own — each ui-toast is its own surface */
|
|
48
|
+
overflow: visible;
|
|
49
|
+
|
|
50
|
+
pointer-events: none; /* the empty region area never intercepts page clicks (SPEC-R12) */
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
:scope:popover-open > ui-toast {
|
|
54
|
+
pointer-events: auto; /* re-enabled per-toast — each card stays fully interactive */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { UIElement } from '../../dom/index.js';
|
|
2
|
+
import { UIToastElement } from './toast.js';
|
|
3
|
+
/** The `show()` input — a bare string shorthand normalizes to `{ message }`. */
|
|
4
|
+
export interface ToastOptions {
|
|
5
|
+
message: string;
|
|
6
|
+
urgent?: boolean;
|
|
7
|
+
duration?: number;
|
|
8
|
+
action?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare class UIToastRegionElement extends UIElement {
|
|
11
|
+
#private;
|
|
12
|
+
static props: {};
|
|
13
|
+
protected connected(): void;
|
|
14
|
+
protected disconnected(): void;
|
|
15
|
+
/**
|
|
16
|
+
* Create and show a toast (SPEC-R13). See the class doc for the full sequence. Throws if the region
|
|
17
|
+
* is not connected — a dev error, never a silent queue (ledger #10).
|
|
18
|
+
*/
|
|
19
|
+
show(options: ToastOptions | string): UIToastElement;
|
|
20
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// toast-region.ts — UIToastRegionElement, the top-layer host `ui-toast` instances stack inside
|
|
2
|
+
// (feed-family.lld.md LLD-C8 · SPEC-R12/R13/R17 · ADR-0112 fork F1/F2). BEHAVIOUR + the `show()`
|
|
3
|
+
// convenience + self-define ONLY; stacking geometry lives in toast-region.css, the public contract in
|
|
4
|
+
// toast-region.md. Same folder as toast.ts (the radio/radio-group same-folder precedent) — NOT its own
|
|
5
|
+
// package. NOT catalogued (ADR-0112 cl.6).
|
|
6
|
+
//
|
|
7
|
+
// Surface (fork F1): a platform `popover="manual"` attribute (set on the HOST at connect if absent —
|
|
8
|
+
// a platform attribute, never ARIA) rides the top layer above ANY page stacking context — a completion
|
|
9
|
+
// arriving while a modal is open must still show — while toasts stack inside via plain CSS flex/gap,
|
|
10
|
+
// zero JS positioning (unlike the overlay-controller's anchored popups, this region has no anchor to
|
|
11
|
+
// measure against). A childList `MutationObserver` drives visibility off child COUNT: ≥1 ⇒
|
|
12
|
+
// showPopover() (guarded by a local `#isOpen` flag — the overlay.ts open()/close() idiom, jsdom-safe
|
|
13
|
+
// since `:popover-open` matching is unavailable there); 0 ⇒ hidePopover() (SPEC-R12 AC1).
|
|
14
|
+
//
|
|
15
|
+
// Ownership (fork F2, "the ownership ruling" — feed-family.lld.md §5, resolved not left open): v1
|
|
16
|
+
// ownership is CONSUMER-mounted — a page/app-shell declares `<ui-toast-region>` and holds the reference
|
|
17
|
+
// it calls `show()` on. NO static singleton exists anywhere (ADR-0082's per-instance isolation); the
|
|
18
|
+
// app-shell does NOT compose a default region this wave (a scope allocation inside F2's ratified
|
|
19
|
+
// architecture, not a new fork — no ADR owed).
|
|
20
|
+
//
|
|
21
|
+
// `show()` (SPEC-R13): normalizes a string shorthand to `{ message }`, creates a `<ui-toast>`, assigns
|
|
22
|
+
// urgent/duration/action, sets its message text BEFORE append (announcement-correct — SPEC-R15 AC2's
|
|
23
|
+
// pre-insertion role covers the live-region HALF; this covers the CONTENT half), re-asserts top-layer
|
|
24
|
+
// order (SPEC-R12 AC2 — a completion arriving while a LATER modal is open still paints above it:
|
|
25
|
+
// hidePopover();showPopover() back-to-back, synchronous, one frame, no focus implications — manual
|
|
26
|
+
// popovers never hold focus), appends, and returns the element. Throws on a disconnected region (a dev
|
|
27
|
+
// error, never a silent queue — ledger #10).
|
|
28
|
+
//
|
|
29
|
+
// `controls → dom + controls/toast/toast.ts` — the allowed import direction (same-folder sibling).
|
|
30
|
+
import { UIElement } from "../../dom/index.js";
|
|
31
|
+
import { UIToastElement } from "./toast.js";
|
|
32
|
+
export class UIToastRegionElement extends UIElement {
|
|
33
|
+
// EMPTY by design — placement is tokens in v1 (a `placement` prop is the named foreseen extension,
|
|
34
|
+
// LLD-C8). Present (not omitted) for the fleet convention + the descriptor trip-wire's empty
|
|
35
|
+
// bijection (the ui-form-provider precedent).
|
|
36
|
+
static props = {};
|
|
37
|
+
#isOpen = false;
|
|
38
|
+
#observer = null;
|
|
39
|
+
connected() {
|
|
40
|
+
if (!this.hasAttribute('popover'))
|
|
41
|
+
this.setAttribute('popover', 'manual'); // a platform attribute, never ARIA
|
|
42
|
+
this.#observer = new MutationObserver(() => this.#syncVisibility());
|
|
43
|
+
this.#observer.observe(this, { childList: true });
|
|
44
|
+
this.#syncVisibility(); // seed from any children already present (declarative markup)
|
|
45
|
+
}
|
|
46
|
+
disconnected() {
|
|
47
|
+
this.#observer?.disconnect();
|
|
48
|
+
this.#observer = null;
|
|
49
|
+
this.#isOpen = false;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Create and show a toast (SPEC-R13). See the class doc for the full sequence. Throws if the region
|
|
53
|
+
* is not connected — a dev error, never a silent queue (ledger #10).
|
|
54
|
+
*/
|
|
55
|
+
show(options) {
|
|
56
|
+
if (!this.isConnected)
|
|
57
|
+
throw new Error('ui-toast-region: show() called while disconnected');
|
|
58
|
+
const opts = typeof options === 'string' ? { message: options } : options;
|
|
59
|
+
const toast = document.createElement('ui-toast');
|
|
60
|
+
if (opts.urgent !== undefined)
|
|
61
|
+
toast.urgent = opts.urgent;
|
|
62
|
+
if (opts.duration !== undefined)
|
|
63
|
+
toast.duration = opts.duration;
|
|
64
|
+
if (opts.action !== undefined)
|
|
65
|
+
toast.action = opts.action;
|
|
66
|
+
toast.textContent = opts.message; // BEFORE append — announcement-correct (SPEC-R13)
|
|
67
|
+
this.#reassertTopLayer();
|
|
68
|
+
this.appendChild(toast);
|
|
69
|
+
return toast;
|
|
70
|
+
}
|
|
71
|
+
#syncVisibility() {
|
|
72
|
+
if (this.childElementCount > 0)
|
|
73
|
+
this.#show();
|
|
74
|
+
else
|
|
75
|
+
this.#hide();
|
|
76
|
+
}
|
|
77
|
+
#show() {
|
|
78
|
+
if (this.#isOpen)
|
|
79
|
+
return;
|
|
80
|
+
this.#isOpen = true;
|
|
81
|
+
this.showPopover();
|
|
82
|
+
}
|
|
83
|
+
#hide() {
|
|
84
|
+
if (!this.#isOpen)
|
|
85
|
+
return;
|
|
86
|
+
this.#isOpen = false;
|
|
87
|
+
try {
|
|
88
|
+
this.hidePopover();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// already hidden — nothing more to do (a platform-drift guard, the overlay.ts idiom)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* SPEC-R12 AC2 — when already open, re-enter the top layer so a completion arriving while a LATER
|
|
96
|
+
* modal is open still paints above it. A no-op when not currently open (the first-ever show path,
|
|
97
|
+
* where the childList observer's own showPopover() call — triggered by the append below — is the
|
|
98
|
+
* one that actually opens the region).
|
|
99
|
+
*/
|
|
100
|
+
#reassertTopLayer() {
|
|
101
|
+
if (!this.#isOpen)
|
|
102
|
+
return;
|
|
103
|
+
try {
|
|
104
|
+
this.hidePopover();
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// already hidden — proceed to re-show regardless
|
|
108
|
+
}
|
|
109
|
+
this.showPopover();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!customElements.get('ui-toast-region'))
|
|
113
|
+
customElements.define('ui-toast-region', UIToastRegionElement);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/* toast.css — ui-toast, the single-file component stylesheet (ADR-0003; feed-family.lld.md LLD-C7 ·
|
|
2
|
+
* SPEC-R14/R15/R19). Two clearly-SECTIONED blocks:
|
|
3
|
+
*
|
|
4
|
+
* [1] TOKEN BLOCK — `:where(ui-toast)` (specificity 0,0,0) declares the `--ui-toast-*` chain: a
|
|
5
|
+
* FIXED `--ui-toast-inline-size` (20em — a notification card is not a control on the height
|
|
6
|
+
* ramp; `max-inline-size: 100%` floors small viewports) + the surface roles (neutral
|
|
7
|
+
* surface-brightest / outline / an inline elevation shadow). No `[variant]`/`[size]` repoint —
|
|
8
|
+
* toast carries neither axis (SPEC-R19; a `[size]` selector would need a declared `size`
|
|
9
|
+
* attribute — family-coherence A2b).
|
|
10
|
+
* [2] STYLES BLOCK — `@scope (ui-toast)` consumes ONLY `--ui-toast-*` (∪ the shared `--md-sys-space-*`/
|
|
11
|
+
* `--md-sys-shape-corner-base` shell-spacing tokens, the ui-menu panel precedent). A three-column grid
|
|
12
|
+
* (`1fr auto auto` — message | action | close) laid out by the control-built parts in toast.ts;
|
|
13
|
+
* `[data-part='message']` gets `min-inline-size: 0` (the flex/grid text-overflow floor — an
|
|
14
|
+
* unbounded message must not blow out the fixed card width) + `overflow-wrap: anywhere` (an
|
|
15
|
+
* unbroken token, e.g. a URL, must wrap inside the fixed inline-size rather than overflow it).
|
|
16
|
+
* Forced-colors keeps the border in CanvasText once the box-shadow (a `rgb()` literal — WHCM
|
|
17
|
+
* flattens arbitrary alpha colours) vanishes under high-contrast — the bar-chart fill lesson:
|
|
18
|
+
* the border alone must carry the card's edge.
|
|
19
|
+
*
|
|
20
|
+
* No custom-state motion gate, no transitions — the LLD ships no entrance/exit animation in v1 (a
|
|
21
|
+
* fade/slide-in is a named foreseen extension, not this wave's contract). Standalone stylesheet —
|
|
22
|
+
* linked via the host page / component-styles barrel; NEVER injected from toast.ts (behaviour-only
|
|
23
|
+
* `.ts`, no runtime style injection — plan §2).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/* ════════════════ [1] TOKEN BLOCK — :where(ui-toast) declares --ui-toast-* ════════════════ */
|
|
27
|
+
|
|
28
|
+
:where(ui-toast) {
|
|
29
|
+
/* A notification card is a FIXED-width surface, not a control on the height ramp (geometry.md's
|
|
30
|
+
Pattern band — the ui-menu panel precedent, not a Control-band `--md-sys-height-*` lookup). */
|
|
31
|
+
--ui-toast-inline-size: 20em;
|
|
32
|
+
|
|
33
|
+
/* Surface — neutral roles (a toast carries no [intent]/[variant] colour channel in v1). */
|
|
34
|
+
--ui-toast-surface: var(--md-sys-color-neutral-surface-brightest);
|
|
35
|
+
--ui-toast-ink: var(--md-sys-color-neutral-on-surface); /* TKT-0066: was a direct role read in @scope */
|
|
36
|
+
--ui-toast-border: var(--md-sys-color-neutral-outline);
|
|
37
|
+
--ui-toast-shadow: 0 4px 12px rgb(0 0 0 / 0.15);
|
|
38
|
+
|
|
39
|
+
/* TKT-0066 item 5 ruling: dimensional constants route through the own chain, never read raw in @scope. */
|
|
40
|
+
--ui-toast-gap: var(--md-sys-space-sm); /* shared shell-spacing step — the ui-menu panel precedent */
|
|
41
|
+
--ui-toast-pad-inline: var(--md-sys-space-md);
|
|
42
|
+
--ui-toast-pad-block: var(--md-sys-space-sm);
|
|
43
|
+
--ui-toast-radius: var(--md-sys-shape-corner-base); /* the shared fleet radius (ui-menu/ui-card precedent) */
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* ════════════════ [2] STYLES BLOCK — @scope (ui-toast) consumes ONLY --ui-toast-* ════════════════ */
|
|
47
|
+
|
|
48
|
+
@scope (ui-toast) {
|
|
49
|
+
:scope {
|
|
50
|
+
display: grid;
|
|
51
|
+
grid-template-columns: 1fr auto auto; /* message | action | close — the control-built parts (toast.ts) */
|
|
52
|
+
align-items: center;
|
|
53
|
+
gap: var(--ui-toast-gap);
|
|
54
|
+
box-sizing: border-box; /* the fixed inline-size below INCLUDES padding + border (button.css precedent) */
|
|
55
|
+
|
|
56
|
+
inline-size: var(--ui-toast-inline-size);
|
|
57
|
+
max-inline-size: 100%; /* floors small viewports — the card never overflows a narrow region */
|
|
58
|
+
|
|
59
|
+
padding-inline: var(--ui-toast-pad-inline);
|
|
60
|
+
padding-block: var(--ui-toast-pad-block);
|
|
61
|
+
|
|
62
|
+
background: var(--ui-toast-surface);
|
|
63
|
+
color: var(--ui-toast-ink);
|
|
64
|
+
border: 1px solid var(--ui-toast-border);
|
|
65
|
+
border-radius: var(--ui-toast-radius);
|
|
66
|
+
box-shadow: var(--ui-toast-shadow);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* The message part — the fixed-width floor (an unbounded message must not blow out the card) + a
|
|
70
|
+
wrap escape for an unbroken token (e.g. a URL) that would otherwise overflow the fixed width. */
|
|
71
|
+
:scope [data-part='message'] {
|
|
72
|
+
min-inline-size: 0;
|
|
73
|
+
overflow-wrap: anywhere;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* The close part is PINNED to the grid's third (last) column, always — the `action` part is only
|
|
77
|
+
appended when non-empty (toast.ts), so a non-actionable toast (the common case) has only TWO real
|
|
78
|
+
children (message, close) landing in auto-placement's columns 1 and 2, leaving the third (intended
|
|
79
|
+
close) column empty. Left unpinned, the close button rendered in the MIDDLE (action) track and the
|
|
80
|
+
genuinely-empty trailing track still consumed a `gap` between it and the padding edge — an extra
|
|
81
|
+
`--md-sys-space-sm` of phantom inset past the close button's own edge, reading as an off-standard
|
|
82
|
+
double-inset against the message's own left inset (TKT-0014). Explicit placement fixes both the
|
|
83
|
+
actionable AND non-actionable cases uniformly (a 3-child toast already lands here by auto-placement,
|
|
84
|
+
so this is a no-op for it). */
|
|
85
|
+
:scope [data-part='close'] {
|
|
86
|
+
grid-column: 3;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* forced-colors — the box-shadow (an arbitrary-alpha rgb() literal) is flattened away under WHCM;
|
|
90
|
+
the border alone must carry the card's edge (the bar-chart fill lesson). */
|
|
91
|
+
@media (forced-colors: active) {
|
|
92
|
+
:scope {
|
|
93
|
+
border-color: CanvasText;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|