@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,178 @@
|
|
|
1
|
+
// table.ts — UITableElement, the Display-class static data table (LLD-C2, report-family.lld.md §2;
|
|
2
|
+
// report-family.spec.md SPEC-R1…R6; ADR-0111 cl.1/3). BEHAVIOUR + props + the stable-skeleton mechanism +
|
|
3
|
+
// the three-effect split + self-define ONLY; the pure cell-resolution/hardening math lives in
|
|
4
|
+
// table-model.ts (DOM-free, unit-testable) and the CSS/token geometry lives in table.css.
|
|
5
|
+
//
|
|
6
|
+
// The mechanism is a REAL native `<table>` stamped in light DOM (the ui-text `as`-stamp doctrine scaled up
|
|
7
|
+
// — ADR-0078 cl.4; ADR-0111 cl.3): header association, `th scope`, and SR table navigation come free from
|
|
8
|
+
// the platform. The host mints NO ARIA via internals (SPEC-R6) — the stamped `<table>` IS the table.
|
|
9
|
+
//
|
|
10
|
+
// SPEC-R4's re-render contract — THE load-bearing mechanism (report-family.lld.md §2, LLD-C2's own framing)
|
|
11
|
+
// — is a STABLE SKELETON built ONCE in `connected()`, held in private fields, never replaced by any later
|
|
12
|
+
// data update: `#scroll` (the component's own overflow container — SPEC-R5) › `#table` › `#thead` + `#tbody`.
|
|
13
|
+
// No code path anywhere in this file ever writes `#scroll.scrollLeft`/`.scrollTop` — the scroll offset a
|
|
14
|
+
// user leaves behind survives a `rows` update by simple absence of any write, not by special-casing it.
|
|
15
|
+
// Three INDEPENDENT effects, split by which signal(s) each reads (the fine-grained-waking discipline):
|
|
16
|
+
// • the COLUMNS effect (reads `columns` only) rebuilds `#thead`'s one header row; a `rows`-only update
|
|
17
|
+
// never re-runs it (SPEC-R4.3's identity clause) — `#table`/`#thead` node identity is untouched.
|
|
18
|
+
// • the BODY effect (reads `columns` AND `rows`) rebuilds ONLY `#tbody`'s content via `replaceChildren` —
|
|
19
|
+
// whole-array swap semantics (A2UI `updateDataModel`), deliberately NOT the ADR-0024 positional
|
|
20
|
+
// reconcile (inert text rows hold no per-node state worth reconciling, the bar-chart LLD §7 row 7
|
|
21
|
+
// precedent). `#scroll`'s node identity and scroll offsets are UNTOUCHED by construction.
|
|
22
|
+
// • the LABEL effect (reads `label` only) mints/removes exactly one `<caption>` and (un)sets the scroll
|
|
23
|
+
// region's `aria-labelledby` — touches nothing else (SPEC-R2 AC3).
|
|
24
|
+
// `render()` stays the inherited no-op (host-as-grid is not this anatomy's shape; every node here is
|
|
25
|
+
// component-built, imperatively, the sparkline/bar-chart precedent for a data-driven Display leaf).
|
|
26
|
+
//
|
|
27
|
+
// The interior scroll region (SPEC-R5 AC2) — `role="region" tabindex="0"` on `#scroll` — is the WAI-ARIA
|
|
28
|
+
// APG accessible-overflow pattern on an INTERIOR node (the Option/MenuItem interior-attribute sanction; only
|
|
29
|
+
// HOST aria rides internals). This is platform scroll AFFORDANCE, not a component keyboard contract: zero
|
|
30
|
+
// component-defined key bindings are added (no roving tabindex, no arrow-key navigation) — `keyboard: []`
|
|
31
|
+
// stands in table.md.
|
|
32
|
+
//
|
|
33
|
+
// Imports inward only (controls → dom): UIElement + prop + the typed-schema helpers from the dom barrel;
|
|
34
|
+
// the pure math + safe codecs from the co-located table-model.ts (the ADR-0065 pure-core split, ADR-0111 cl.8).
|
|
35
|
+
import { UIElement, prop } from "../../dom/index.js";
|
|
36
|
+
import { cleanColumns, cleanRows, resolveCell, tableColumnsProp, tableRowsProp } from "./table-model.js";
|
|
37
|
+
const props = {
|
|
38
|
+
columns: tableColumnsProp, // TableColumn[] · safe JSON codec (table-model.ts) · default []
|
|
39
|
+
rows: tableRowsProp, // TableRow[] · safe JSON codec (table-model.ts) · default []
|
|
40
|
+
label: { ...prop.string(''), reflect: true }, // the rendered <caption> text — the table's accessible name (SPEC-R2/R6)
|
|
41
|
+
};
|
|
42
|
+
export class UITableElement extends UIElement {
|
|
43
|
+
static props = props;
|
|
44
|
+
// The stable skeleton (SPEC-R4.1) — built ONCE EVER behind the `#built` guard (TKT-0067), held
|
|
45
|
+
// privately, NEVER replaced by any data-update effect below. `#`-private: nothing outside the host can
|
|
46
|
+
// reach in and mutate scroll state.
|
|
47
|
+
#built = false;
|
|
48
|
+
#scroll;
|
|
49
|
+
#table;
|
|
50
|
+
#thead;
|
|
51
|
+
#tbody;
|
|
52
|
+
// The mounted `<caption>` — null while `label` is empty (LLD-C2 step 4). Held so the label effect can
|
|
53
|
+
// find/remove/reuse the SAME element across reruns instead of re-minting one on every `label` change.
|
|
54
|
+
#caption = null;
|
|
55
|
+
// Live scroll-offset shadow (TKT-0067, MEASURED): node identity alone does NOT preserve scroll across
|
|
56
|
+
// a disconnect/reconnect — every engine resets a scroll container's offsets when it leaves the
|
|
57
|
+
// document (scroll state lives in the LAYOUT tree, not the DOM node; the probe measured 40 → 0 in
|
|
58
|
+
// Chromium AND WebKit even with the identical node reattached). Tracked by the scroll listener in
|
|
59
|
+
// connected() and restored on reconnect. Reading scrollLeft in disconnected() is too late — the
|
|
60
|
+
// element is already out of the document and reads 0 there.
|
|
61
|
+
#lastScrollLeft = 0;
|
|
62
|
+
#lastScrollTop = 0;
|
|
63
|
+
connected() {
|
|
64
|
+
// The skeleton — built ONCE EVER behind an idempotent guard (TKT-0067: previously rebuilt on every
|
|
65
|
+
// connect, which discarded the prior skeleton — and with it the scroll offset SPEC-R4.1 says
|
|
66
|
+
// "survives by omission" — on an ordinary disconnect/reconnect; the progress.ts field-guard shape,
|
|
67
|
+
// the fleet parts-once canon). NOT inside an effect (SPEC-R4.1's identity clause). `#table` starts
|
|
68
|
+
// with its `#thead`/`#tbody` already attached; `#scroll` starts EMPTY (the columns effect below
|
|
69
|
+
// attaches `#table` only once real columns exist — SPEC-R3 row 1's "no table is stamped" empty
|
|
70
|
+
// state). Light-DOM children persist across a disconnect, so a reconnect finds the prior skeleton —
|
|
71
|
+
// including its live `scrollLeft`/`scrollTop` and any mounted `#caption` — intact.
|
|
72
|
+
if (!this.#built) {
|
|
73
|
+
this.#built = true;
|
|
74
|
+
this.#scroll = document.createElement('div');
|
|
75
|
+
this.#scroll.setAttribute('data-part', 'scroll');
|
|
76
|
+
this.#scroll.setAttribute('role', 'region'); // SPEC-R5 AC2 — the accessible-overflow pattern
|
|
77
|
+
this.#scroll.setAttribute('tabindex', '0'); // platform scroll affordance, NOT a keyboard contract
|
|
78
|
+
this.#table = document.createElement('table');
|
|
79
|
+
this.#thead = document.createElement('thead');
|
|
80
|
+
this.#tbody = document.createElement('tbody');
|
|
81
|
+
this.#table.append(this.#thead, this.#tbody);
|
|
82
|
+
this.#caption = null;
|
|
83
|
+
this.replaceChildren(this.#scroll);
|
|
84
|
+
}
|
|
85
|
+
// Restore the pre-disconnect scroll offsets (TKT-0067) — a no-op on first connect (0/0). The
|
|
86
|
+
// synchronous write forces layout on the just-reinserted, already-populated skeleton so the value
|
|
87
|
+
// clamps against real content. Rides the connection: the listener below re-arms every connect.
|
|
88
|
+
if (this.#lastScrollLeft !== 0 || this.#lastScrollTop !== 0) {
|
|
89
|
+
this.#scroll.scrollLeft = this.#lastScrollLeft;
|
|
90
|
+
this.#scroll.scrollTop = this.#lastScrollTop;
|
|
91
|
+
}
|
|
92
|
+
this.listen(this.#scroll, 'scroll', () => {
|
|
93
|
+
this.#lastScrollLeft = this.#scroll.scrollLeft;
|
|
94
|
+
this.#lastScrollTop = this.#scroll.scrollTop;
|
|
95
|
+
});
|
|
96
|
+
// The COLUMNS effect (SPEC-R4.3) — reads ONLY `columns`. A `rows`-only update never re-runs this, so
|
|
97
|
+
// `#table`/`#thead` node identity holds across it (the identity clause proper).
|
|
98
|
+
this.effect(() => {
|
|
99
|
+
const cols = cleanColumns(this.columns);
|
|
100
|
+
if (cols.length === 0) {
|
|
101
|
+
// SPEC-R3 row 1: no valid columns ⇒ no table is stamped (the empty scroll container; the host box
|
|
102
|
+
// still paints via the CSS floor). `#table` itself is untouched — only detached, never destroyed —
|
|
103
|
+
// so a later columns write reattaches the SAME node (never a fresh one).
|
|
104
|
+
this.#scroll.replaceChildren();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// Attach `#table` only on the detached→attached transition (attach churn only at that edge, never on
|
|
108
|
+
// every columns change) — `#scroll.replaceChildren(#table)` when it is not already `#scroll`'s child.
|
|
109
|
+
if (this.#table.parentNode !== this.#scroll)
|
|
110
|
+
this.#scroll.replaceChildren(this.#table);
|
|
111
|
+
const headerRow = document.createElement('tr');
|
|
112
|
+
for (const col of cols)
|
|
113
|
+
headerRow.append(this.#headerCell(col));
|
|
114
|
+
this.#thead.replaceChildren(headerRow);
|
|
115
|
+
});
|
|
116
|
+
// The BODY effect (SPEC-R4.3) — reads `columns` AND `rows`; rebuilds ONLY `#tbody`'s content
|
|
117
|
+
// (`replaceChildren`, whole-array swap). `#scroll`/`#table`/`#thead` are never touched here, and no
|
|
118
|
+
// DATA-update path writes `#scroll.scrollLeft`/`.scrollTop` — across data updates the scroll offset
|
|
119
|
+
// survives by omission. (Across a RECONNECT it cannot — the platform resets a removed container's
|
|
120
|
+
// offsets — so connected() restores from the tracked shadow; TKT-0067.)
|
|
121
|
+
this.effect(() => {
|
|
122
|
+
const cols = cleanColumns(this.columns);
|
|
123
|
+
const rows = cleanRows(this.rows);
|
|
124
|
+
this.#tbody.replaceChildren(...rows.map((row) => this.#rowNode(cols, row)));
|
|
125
|
+
});
|
|
126
|
+
// The LABEL effect (SPEC-R2 AC3) — reads ONLY `label`; mints/removes exactly one `<caption>` as
|
|
127
|
+
// `#table`'s first child and (un)sets `#scroll`'s `aria-labelledby` (the region is named by the SAME
|
|
128
|
+
// text the caption gives the table — one text, two consumers). Touches nothing else.
|
|
129
|
+
this.effect(() => {
|
|
130
|
+
const label = this.label;
|
|
131
|
+
if (label === '') {
|
|
132
|
+
this.#caption?.remove();
|
|
133
|
+
this.#caption = null;
|
|
134
|
+
this.#scroll.removeAttribute('aria-labelledby');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!this.#caption) {
|
|
138
|
+
this.#caption = document.createElement('caption');
|
|
139
|
+
this.#caption.id = nextCaptionId();
|
|
140
|
+
this.#table.insertBefore(this.#caption, this.#table.firstChild);
|
|
141
|
+
}
|
|
142
|
+
this.#caption.textContent = label;
|
|
143
|
+
this.#scroll.setAttribute('aria-labelledby', this.#caption.id);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/** One `<th scope="col">` — `data-type='number'` set from the COLUMN's type (alignment is column-driven,
|
|
147
|
+
* table.css's `[data-type='number']` rule; SPEC-R2/R3 row 9). */
|
|
148
|
+
#headerCell(col) {
|
|
149
|
+
const th = document.createElement('th');
|
|
150
|
+
th.setAttribute('scope', 'col');
|
|
151
|
+
if (col.type === 'number')
|
|
152
|
+
th.setAttribute('data-type', 'number');
|
|
153
|
+
th.textContent = col.label;
|
|
154
|
+
return th;
|
|
155
|
+
}
|
|
156
|
+
/** One `<tr>` of `<td>`s for `row` — cell text via the pure `resolveCell` (table-model.ts); alignment
|
|
157
|
+
* rides the SAME column-driven `data-type` attribute as the header cell, regardless of the cell's own
|
|
158
|
+
* resolved value (SPEC-R3 row 9 — a mismatched string in a number column keeps the column's alignment). */
|
|
159
|
+
#rowNode(cols, row) {
|
|
160
|
+
const tr = document.createElement('tr');
|
|
161
|
+
for (const col of cols) {
|
|
162
|
+
const td = document.createElement('td');
|
|
163
|
+
if (col.type === 'number')
|
|
164
|
+
td.setAttribute('data-type', 'number');
|
|
165
|
+
td.textContent = resolveCell(col, row);
|
|
166
|
+
tr.append(td);
|
|
167
|
+
}
|
|
168
|
+
return tr;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// The caption `id` mint — a module-scoped counter suffix, collision-free in light DOM (no crypto/uuid dep).
|
|
172
|
+
let captionCounter = 0;
|
|
173
|
+
function nextCaptionId() {
|
|
174
|
+
captionCounter += 1;
|
|
175
|
+
return `ui-table-caption-${captionCounter}`;
|
|
176
|
+
}
|
|
177
|
+
if (!customElements.get('ui-table'))
|
|
178
|
+
customElements.define('ui-table', UITableElement); // idempotent self-define
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { UIElement } from '../../dom/index.js';
|
|
2
|
+
export declare class UITabPanelElement extends UIElement {
|
|
3
|
+
protected connected(): void;
|
|
4
|
+
/**
|
|
5
|
+
* Label this panel by the tab that controls it (called ONCE by the owning ui-tabs). Seeds a stable `id` when
|
|
6
|
+
* the author gave none (so `aria-controls` on the tab can resolve back), and points `aria-labelledby` at the
|
|
7
|
+
* tab via the internals element-reflection (no host attribute). Idempotent across reconnect.
|
|
8
|
+
*/
|
|
9
|
+
link(tab: Element, ownId: string): void;
|
|
10
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// tab-panel.ts — UITabPanelElement, the content region of the `ui-tabs` compound (goals.md §G9 / decomp
|
|
2
|
+
// g9-containers s8). BEHAVIOUR + the `role=tabpanel` ARIA + the parent-driven labelling + self-define ONLY;
|
|
3
|
+
// layout/colour live in tabs.css, the public contract in tabs.md.
|
|
4
|
+
//
|
|
5
|
+
// A `ui-tab-panel` is a component-native ChildList child the owning `ui-tabs` shows/hides by selection (only the
|
|
6
|
+
// selected panel is visible — the rest carry the `hidden` attribute, staying in the DOM). It extends the plain
|
|
7
|
+
// `UIElement` for the protected `internals` (`role=tabpanel` + the `aria-labelledby` element-reflection); it is
|
|
8
|
+
// not a `UIContainerElement` (no surface axes — the container.css seam lists only `ui-tabs`). The host carries NO
|
|
9
|
+
// `role`/`aria-*` attribute. `render()` stays the inherited void — the panel's body is its light-DOM children.
|
|
10
|
+
//
|
|
11
|
+
// Visibility rides the standard `hidden` attribute (a global attribute, NOT ARIA — set directly by ui-tabs), so
|
|
12
|
+
// the family "no host aria-* attribute" rule is untouched. `controls → dom` is the allowed import direction.
|
|
13
|
+
//
|
|
14
|
+
// ADR-0144 Q1 cl.4 — keyboard-scroll disposition for a `fill`-mode panel, MEASURED at build (not assumed): a
|
|
15
|
+
// focused, tabindex=0, `overflow-y:auto` region's platform default action for Arrow/Page/Home/End is NOT
|
|
16
|
+
// reliably keyboard-scrollable across engines — the identical shape already measured for `ui-card-content`
|
|
17
|
+
// (card-content.ts, ADR-0046 Amendment 6): Chromium moves it once trusted-focused, WebKit does not move it at
|
|
18
|
+
// all. tabs.browser.test.ts re-measures the SAME gap for a filled tab panel and confirms it; this panel
|
|
19
|
+
// therefore wires the SAME explicit keydown handler as `card-content.ts` rather than gamble on the default
|
|
20
|
+
// action, deterministic on every shipped engine. The handler only acts when `event.target === this` (never
|
|
21
|
+
// hijacks a focused DESCENDANT's own keys) and only when an ANCESTOR `ui-tabs` is in `fill` mode AND this
|
|
22
|
+
// panel is the visible one (`fill`-less tabs stay ordinary document flow with no added listener effect).
|
|
23
|
+
import { UIElement } from "../../dom/index.js";
|
|
24
|
+
// Set an ARIA element-reflection relationship through internals (the only host-attribute-free path for an
|
|
25
|
+
// IDREF-style relation); feature-detected — present in modern Chromium/WebKit, absent in jsdom (a no-op there,
|
|
26
|
+
// proven live in tabs.browser.test.ts). Peer copy of tab.ts's helper (the folder ships no shared module).
|
|
27
|
+
function reflectAriaElements(internals, name, elements) {
|
|
28
|
+
if (name in internals)
|
|
29
|
+
internals[name] = elements;
|
|
30
|
+
}
|
|
31
|
+
export class UITabPanelElement extends UIElement {
|
|
32
|
+
connected() {
|
|
33
|
+
this.internals.role = 'tabpanel'; // ARIA via internals — never a host role/aria-* attribute
|
|
34
|
+
// A tabpanel is focusable (APG: the panel takes tabindex=0 so a keyboard user can reach its content/scroll
|
|
35
|
+
// it). Self-set; the hidden panels are not reachable regardless (a `hidden` element is not focusable).
|
|
36
|
+
if (!this.hasAttribute('tabindex'))
|
|
37
|
+
this.tabIndex = 0;
|
|
38
|
+
// The EXPLICIT scroll (ADR-0144 Q1 cl.4, the card-content.ts precedent — see the file banner): the
|
|
39
|
+
// platform's own default action for arrow/Page/Home/End on a focused, tabindex=0, overflow-y:auto region is
|
|
40
|
+
// not reliable across engines, so a `fill`-mode panel scrolls itself deterministically instead of gambling
|
|
41
|
+
// on that default. Guarded to a no-op outside fill mode / on a hidden panel / when a focused DESCENDANT
|
|
42
|
+
// (not this panel itself) owns the key.
|
|
43
|
+
this.listen(this, 'keydown', (event) => {
|
|
44
|
+
if (event.target !== this || this.hidden)
|
|
45
|
+
return;
|
|
46
|
+
const tabs = this.closest('ui-tabs');
|
|
47
|
+
if (!tabs || !tabs.hasAttribute('fill'))
|
|
48
|
+
return;
|
|
49
|
+
const key = event.key;
|
|
50
|
+
const line = 40; // px per arrow press — an ordinary reading-line increment, density-invariant on purpose
|
|
51
|
+
const page = this.clientHeight * 0.9; // near-full-page, a small overlap (the standard page-scroll convention)
|
|
52
|
+
switch (key) {
|
|
53
|
+
case 'ArrowDown':
|
|
54
|
+
this.scrollTop += line;
|
|
55
|
+
break;
|
|
56
|
+
case 'ArrowUp':
|
|
57
|
+
this.scrollTop -= line;
|
|
58
|
+
break;
|
|
59
|
+
case 'PageDown':
|
|
60
|
+
this.scrollTop += page;
|
|
61
|
+
break;
|
|
62
|
+
case 'PageUp':
|
|
63
|
+
this.scrollTop -= page;
|
|
64
|
+
break;
|
|
65
|
+
case 'Home':
|
|
66
|
+
this.scrollTop = 0;
|
|
67
|
+
break;
|
|
68
|
+
case 'End':
|
|
69
|
+
this.scrollTop = this.scrollHeight;
|
|
70
|
+
break;
|
|
71
|
+
default:
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Label this panel by the tab that controls it (called ONCE by the owning ui-tabs). Seeds a stable `id` when
|
|
79
|
+
* the author gave none (so `aria-controls` on the tab can resolve back), and points `aria-labelledby` at the
|
|
80
|
+
* tab via the internals element-reflection (no host attribute). Idempotent across reconnect.
|
|
81
|
+
*/
|
|
82
|
+
link(tab, ownId) {
|
|
83
|
+
if (!this.id)
|
|
84
|
+
this.id = ownId;
|
|
85
|
+
reflectAriaElements(this.internals, 'ariaLabelledByElements', [tab]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!customElements.get('ui-tab-panel'))
|
|
89
|
+
customElements.define('ui-tab-panel', UITabPanelElement);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { UIElement, type ReactiveProps } from '../../dom/index.js';
|
|
2
|
+
declare const props: {
|
|
3
|
+
key: import("../../index.js").PropConfig<string>;
|
|
4
|
+
};
|
|
5
|
+
export interface UITabElement extends ReactiveProps<typeof props> {
|
|
6
|
+
}
|
|
7
|
+
export declare class UITabElement extends UIElement {
|
|
8
|
+
static props: {
|
|
9
|
+
key: import("../../index.js").PropConfig<string>;
|
|
10
|
+
};
|
|
11
|
+
protected connected(): void;
|
|
12
|
+
/**
|
|
13
|
+
* Wire this tab to the panel it controls (called ONCE by the owning ui-tabs). Seeds a stable `id` (for the
|
|
14
|
+
* reverse `aria-labelledby` on the panel) when the author gave none, and points `aria-controls` at the panel
|
|
15
|
+
* via the internals element-reflection (no host attribute). Idempotent across reconnect.
|
|
16
|
+
*/
|
|
17
|
+
link(panel: Element, ownId: string): void;
|
|
18
|
+
/**
|
|
19
|
+
* Reflect this tab's selection (called by the owning ui-tabs on every `selected` change). Drives THREE facts:
|
|
20
|
+
* `aria-selected` (via internals), the roving tabindex (0 when selected, −1 otherwise — exactly one tab is in
|
|
21
|
+
* the tab order), and the `:state(selected)` CSS hook the indicator/ink key off. `states` is optional-chained
|
|
22
|
+
* — jsdom has no CustomStateSet (the real selected-indicator paint is tabs.browser.test.ts).
|
|
23
|
+
*/
|
|
24
|
+
setSelected(selected: boolean): void;
|
|
25
|
+
}
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// tab.ts — UITabElement, the interactive tab row of the `ui-tabs` compound (goals.md §G9 / decomp
|
|
2
|
+
// g9-containers s8). BEHAVIOUR + the `role=tab` ARIA + the parent-driven coordination API + self-define ONLY;
|
|
3
|
+
// geometry/colour live in tabs.css (the family's single sheet), the public contract in tabs.md.
|
|
4
|
+
//
|
|
5
|
+
// A `ui-tab` is NOT a standalone control — it is a component-native ChildList child the owning `ui-tabs`
|
|
6
|
+
// coordinates (the ratified "regions = sub-elements" model). It extends the plain `UIElement` (it needs the
|
|
7
|
+
// protected `internals` for `role=tab` + `aria-selected` + the `:state(selected)` hook + the `aria-controls`
|
|
8
|
+
// element-reflection — none of which a sibling can reach), but it owns no surface axes (it is not a
|
|
9
|
+
// `UIContainerElement`; the container.css surface seam lists only `ui-tabs`). The host carries NO `role`/`aria-*`
|
|
10
|
+
// attribute — every ARIA fact rides `this.internals` (the family discipline, CLAUDE.md). `render()` stays the
|
|
11
|
+
// inherited void: the tab's label is its light-DOM children, placed by tabs.css (host-as-content), never clobbered.
|
|
12
|
+
//
|
|
13
|
+
// The coordination API (`link`/`setSelected`) is PUBLIC but ui-tabs-driven: the parent owns selection + the
|
|
14
|
+
// roving tabindex, so it pushes each tab's selected state in (a sibling cannot set another element's protected
|
|
15
|
+
// internals). `controls → dom` is the allowed import direction.
|
|
16
|
+
import { UIElement, prop } from "../../dom/index.js";
|
|
17
|
+
// Set an ARIA element-reflection relationship (`aria-controls`/`aria-labelledby` as ELEMENT refs, not IDREF
|
|
18
|
+
// strings) THROUGH internals — the only way to express an IDREF-style ARIA relation without a host attribute
|
|
19
|
+
// (the family bars host `aria-*`). Feature-detected: the reflection accessors landed in modern Chromium/WebKit
|
|
20
|
+
// but NOT jsdom, so this is a no-op under the jsdom inner loop (the relation is proven in tabs.browser.test.ts)
|
|
21
|
+
// and live in the real engines. Reused by tab-panel.ts (a 3-line peer copy — the folder ships no shared module).
|
|
22
|
+
function reflectAriaElements(internals, name, elements) {
|
|
23
|
+
if (name in internals)
|
|
24
|
+
internals[name] = elements;
|
|
25
|
+
}
|
|
26
|
+
const props = {
|
|
27
|
+
// `key` — the tab's optional STABLE identity (the agent's tab id), OBSERVED (its initial attribute seeds the
|
|
28
|
+
// prop) but not reflected. ui-tabs resolves `selected` against `key` first, then falls back to the DOM index,
|
|
29
|
+
// so a `key`-less tab is addressed positionally. Mirrors the text-field `value` reflect:false precedent.
|
|
30
|
+
key: prop.string(), // renamed from `value` (TKT-0069 item 1 ruling: `value` = the FACE form value, reserved)
|
|
31
|
+
};
|
|
32
|
+
export class UITabElement extends UIElement {
|
|
33
|
+
static props = props;
|
|
34
|
+
connected() {
|
|
35
|
+
this.internals.role = 'tab'; // ARIA via internals — never a host role/aria-* attribute
|
|
36
|
+
// Roving-tabindex default: a tab starts OUT of the tab order; the owning ui-tabs promotes exactly the
|
|
37
|
+
// selected one to tabindex=0 (setSelected). Self-set so a tab is never tabbable before the parent wires it.
|
|
38
|
+
if (!this.hasAttribute('tabindex'))
|
|
39
|
+
this.tabIndex = -1;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Wire this tab to the panel it controls (called ONCE by the owning ui-tabs). Seeds a stable `id` (for the
|
|
43
|
+
* reverse `aria-labelledby` on the panel) when the author gave none, and points `aria-controls` at the panel
|
|
44
|
+
* via the internals element-reflection (no host attribute). Idempotent across reconnect.
|
|
45
|
+
*/
|
|
46
|
+
link(panel, ownId) {
|
|
47
|
+
if (!this.id)
|
|
48
|
+
this.id = ownId;
|
|
49
|
+
reflectAriaElements(this.internals, 'ariaControlsElements', [panel]);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Reflect this tab's selection (called by the owning ui-tabs on every `selected` change). Drives THREE facts:
|
|
53
|
+
* `aria-selected` (via internals), the roving tabindex (0 when selected, −1 otherwise — exactly one tab is in
|
|
54
|
+
* the tab order), and the `:state(selected)` CSS hook the indicator/ink key off. `states` is optional-chained
|
|
55
|
+
* — jsdom has no CustomStateSet (the real selected-indicator paint is tabs.browser.test.ts).
|
|
56
|
+
*/
|
|
57
|
+
setSelected(selected) {
|
|
58
|
+
this.internals.ariaSelected = selected ? 'true' : 'false';
|
|
59
|
+
this.tabIndex = selected ? 0 : -1; // roving: the selected tab is the single tab-order entry
|
|
60
|
+
if (selected)
|
|
61
|
+
this.internals.states?.add('selected');
|
|
62
|
+
else
|
|
63
|
+
this.internals.states?.delete('selected');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!customElements.get('ui-tab'))
|
|
67
|
+
customElements.define('ui-tab', UITabElement);
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/* tabs.css — the SINGLE sheet for the tabs compound: ui-tabs (the surface shell + the tablist strip), ui-tab
|
|
2
|
+
* (the interactive rows), ui-tab-panel (the content regions). Two sectioned blocks (ADR-0003): a :where() TOKEN
|
|
3
|
+
* block (specificity 0 — declares the --ui-tabs-* chain from the colour roles + the dimensional ramp) then an
|
|
4
|
+
* @scope STYLES block (consumes ONLY --ui-tabs-* + the shared fleet focus-ring/motion tokens — token hygiene).
|
|
5
|
+
* Linked via the component-styles barrel (wired in decomp s12), NEVER injected from a .ts (behaviour holds no
|
|
6
|
+
* runtime style; plan §2).
|
|
7
|
+
*
|
|
8
|
+
* Geometry (geometry.md "Pattern" size-class — tabs is the named example): the interactive TAB ROWS take the
|
|
9
|
+
* CONTROL height (--md-sys-height-*), while the SHELL (strip gap, panel padding) uses the --md-sys-space layout ladder.
|
|
10
|
+
* Two distinct ledgers, never interchanged. The surface (the shell background + the elevation/brightness axes)
|
|
11
|
+
* is the shared container.css seam — this sheet does not repoint --ui-container-bg (ADR-0104): a bare <ui-tabs>
|
|
12
|
+
* is transparent by default like the rest of the container family (ADR-0015 cl.1); a plane is an asked-for
|
|
13
|
+
* intent via the already-shipped `elevation`/`brightness` axes (e.g. `<ui-tabs elevation="0">`).
|
|
14
|
+
*
|
|
15
|
+
* `[fill]` (ADR-0144 Q1) — ONE opt-in CSS-only posture: the shell becomes a flex column filling a height-bounded
|
|
16
|
+
* parent (a pinned tablist strip + an internally scrolling active panel) — the exact composition `agent-admin.css`
|
|
17
|
+
* (TKT-0085) hand-rolled per consumer for lack of a shipped variant. The panel's scrollbar-visibility seam
|
|
18
|
+
* (`--ui-tabs-panel-scrollbar-width`) is consumer-INHERITED — var()-fallback only, deliberately NOT declared in
|
|
19
|
+
* the token block below (an own declaration would beat a composing surface's inherited value — the same
|
|
20
|
+
* TKT-0065 alias-resolution lesson `split-pane.css` already records). Default (`[fill]` absent) stays
|
|
21
|
+
* byte-identical to today's document-flow tabs — negative control in tabs.browser.test.ts. */
|
|
22
|
+
|
|
23
|
+
/* ════════════════════════ [1] TOKEN BLOCK — :where() declarations (the --md-sys-color-* + ramp entry) ════════════════════════ */
|
|
24
|
+
|
|
25
|
+
:where(ui-tabs) {
|
|
26
|
+
/* Tab-row geometry — the CONTROL band (the rows are interactive; geometry.md Pattern class). Fixed at the md
|
|
27
|
+
ramp step (ui-tabs exposes no [size] axis). */
|
|
28
|
+
--ui-tabs-tab-height: var(--md-sys-height-md);
|
|
29
|
+
--ui-tabs-tab-font: var(--md-sys-font-md);
|
|
30
|
+
--ui-tabs-tab-pad-inline: var(--md-sys-space-md); /* a tab's horizontal padding — the layout ladder */
|
|
31
|
+
|
|
32
|
+
/* Shell spacing — the --md-sys-space layout ladder (density-responsive; NEVER the control frame). */
|
|
33
|
+
--ui-tabs-strip-gap: var(--md-sys-space-xs); /* gap between tabs in the strip */
|
|
34
|
+
--ui-tabs-panel-pad: var(--md-sys-space-md); /* the panel body padding */
|
|
35
|
+
|
|
36
|
+
/* The strip divider + the indicator thickness. */
|
|
37
|
+
--ui-tabs-strip-line: var(--md-sys-color-neutral-outline-variant);
|
|
38
|
+
--ui-tabs-indicator-size: 2px;
|
|
39
|
+
|
|
40
|
+
/* The tab ink ladder (SOLID roles, never a color-mix — components hold zero colour opinion; ADR-0008) +
|
|
41
|
+
the selected-tab indicator. */
|
|
42
|
+
--ui-tabs-ink: var(--md-sys-color-neutral-on-surface-variant); /* idle tab label */
|
|
43
|
+
--ui-tabs-ink-hover: var(--md-sys-color-neutral-on-surface); /* hover */
|
|
44
|
+
--ui-tabs-ink-selected: var(--md-sys-color-neutral-on-surface); /* the active tab label */
|
|
45
|
+
--ui-tabs-indicator: var(--md-sys-color-primary); /* the active-tab underline */
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* ════════════════════════ [2] STYLES BLOCK — @scope (the --ui-tabs-* + fleet consumption) ════════════════════════ */
|
|
49
|
+
|
|
50
|
+
@scope (ui-tabs) {
|
|
51
|
+
/* The shell — a block stack [ tablist · selected panel ]. The surface background is the container.css seam
|
|
52
|
+
(consumed there off --ui-container-bg); this sheet adds no background. */
|
|
53
|
+
:scope {
|
|
54
|
+
display: block;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* The tablist strip — a horizontal row of tabs with a bottom divider. role=tablist rides the part div. */
|
|
58
|
+
:scope > [data-part='tablist'] {
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-direction: row;
|
|
61
|
+
gap: var(--ui-tabs-strip-gap);
|
|
62
|
+
border-block-end: 1px solid var(--ui-tabs-strip-line);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* A tab — an interactive CONTROL-height row; the host-as-content label is its light-DOM children. */
|
|
66
|
+
ui-tab {
|
|
67
|
+
position: relative;
|
|
68
|
+
box-sizing: border-box;
|
|
69
|
+
display: inline-flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
justify-content: center;
|
|
72
|
+
block-size: var(--ui-tabs-tab-height);
|
|
73
|
+
padding-inline: var(--ui-tabs-tab-pad-inline);
|
|
74
|
+
font-size: var(--ui-tabs-tab-font);
|
|
75
|
+
color: var(--ui-tabs-ink);
|
|
76
|
+
white-space: nowrap; /* anonymous host-as-content label text — the SAME no-wrapper gap ui-button had
|
|
77
|
+
(button.css, fixed by ADR-0133) also exists here: no overflow/text-overflow, so
|
|
78
|
+
a tab label in a narrow tablist would clip/overlap with no ellipsis affordance.
|
|
79
|
+
NAMED DEFERRAL, not fixed here (ADR-0133 §Decision cl.3, TKT-0042): no evidence
|
|
80
|
+
has been submitted that a tab label actually overflows in practice, and
|
|
81
|
+
ADR-0106's Alternatives already rejected a generic fleet-wide truncate fix —
|
|
82
|
+
per-control adoption stays per-control evidence. A future report against this
|
|
83
|
+
control reopens ADR-0133's Alternatives with its own evidence, rather than
|
|
84
|
+
starting cold. */
|
|
85
|
+
cursor: pointer;
|
|
86
|
+
user-select: none;
|
|
87
|
+
-webkit-user-select: none; /* WebKit honours selection-none only via the prefix (button.browser proof) */
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
ui-tab:hover {
|
|
91
|
+
color: var(--ui-tabs-ink-hover);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* selected — aria-selected rides internals (no [aria-selected] attribute to match), so the visual cue keys
|
|
95
|
+
off the :state(selected) custom state the parent sets via the tab's internals. */
|
|
96
|
+
ui-tab:state(selected) {
|
|
97
|
+
color: var(--ui-tabs-ink-selected);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* The active-tab underline indicator — a ::after bar, transparent until selected. */
|
|
101
|
+
ui-tab::after {
|
|
102
|
+
content: '';
|
|
103
|
+
position: absolute;
|
|
104
|
+
inset-inline: 0;
|
|
105
|
+
inset-block-end: 0;
|
|
106
|
+
block-size: var(--ui-tabs-indicator-size);
|
|
107
|
+
background: transparent;
|
|
108
|
+
}
|
|
109
|
+
ui-tab:state(selected)::after {
|
|
110
|
+
background: var(--ui-tabs-indicator);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* The shared focus ring (ADR-0009) — keyboard-only :focus-visible, read DIRECTLY from the fleet tokens (the
|
|
114
|
+
one token-hygiene exception every control shares). Inset offset so the ring sits inside the strip. */
|
|
115
|
+
ui-tab:focus-visible {
|
|
116
|
+
outline: var(--md-sys-state-focus-ring-width) solid var(--md-sys-color-focus-ring);
|
|
117
|
+
outline-offset: calc(-1 * var(--md-sys-state-focus-ring-offset));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* A panel — the body region; only the selected one shows (the rest carry `hidden` and stay in the DOM). The
|
|
121
|
+
[hidden] rule must be authored here: an author `display:block` on a custom element OUTRANKS the UA
|
|
122
|
+
`[hidden]{display:none}` (author beats UA), so it would un-hide a hidden panel without this explicit rule. */
|
|
123
|
+
ui-tab-panel {
|
|
124
|
+
display: block;
|
|
125
|
+
padding: var(--ui-tabs-panel-pad);
|
|
126
|
+
}
|
|
127
|
+
ui-tab-panel[hidden] {
|
|
128
|
+
display: none;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* [fill] (ADR-0144 Q1 cl.1) — the shell becomes a flex column filling its height-bounded parent: the tablist
|
|
132
|
+
strip stays at its natural content height (an ordinary, non-grow flex item, so it stays pinned as the
|
|
133
|
+
panel scrolls beneath it), the visible panel becomes the ONE flexible, internally-scrolling item. The
|
|
134
|
+
panel gains NO content-region semantics here (Q3) — it becomes exactly what its shipped `tabindex=0`
|
|
135
|
+
already promises: a scroll container; header/body/footer composition inside it is the author's own
|
|
136
|
+
`[data-box]`/`ui-card`, never this sheet's business. */
|
|
137
|
+
:scope[fill] {
|
|
138
|
+
display: flex;
|
|
139
|
+
flex-direction: column;
|
|
140
|
+
block-size: 100%;
|
|
141
|
+
min-block-size: 0;
|
|
142
|
+
}
|
|
143
|
+
:scope[fill] ui-tab-panel:not([hidden]) {
|
|
144
|
+
flex: 1 1 auto;
|
|
145
|
+
min-block-size: 0;
|
|
146
|
+
overflow-y: auto;
|
|
147
|
+
/* Consumer-INHERITED scrollbar-visibility seam (the split-pane shape) — see the file banner; deliberately
|
|
148
|
+
NOT declared in the :where() token block, so the default rides the var() fallback. */
|
|
149
|
+
scrollbar-width: var(--ui-tabs-panel-scrollbar-width, auto);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* Motion — the state PAINT only (ink + the indicator), gated behind :state(ready) so the first paint SNAPS
|
|
153
|
+
(the selection applied synchronously at connect does not animate) and only later changes transition. Never
|
|
154
|
+
geometry, never `all`; timing from the shared motion token. */
|
|
155
|
+
:scope:state(ready) ui-tab {
|
|
156
|
+
transition: color var(--md-sys-motion-duration-fast) var(--md-sys-motion-easing-standard);
|
|
157
|
+
}
|
|
158
|
+
:scope:state(ready) ui-tab::after {
|
|
159
|
+
transition: background-color var(--md-sys-motion-duration-fast) var(--md-sys-motion-easing-standard);
|
|
160
|
+
}
|
|
161
|
+
@media (prefers-reduced-motion: reduce) {
|
|
162
|
+
:scope:state(ready) ui-tab,
|
|
163
|
+
:scope:state(ready) ui-tab::after {
|
|
164
|
+
transition: none;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* Forced-colors (WHCM) — keep the selected-tab indicator + label visible as a system colour (the surface
|
|
169
|
+
drops to Canvas via the container.css role layer; here we guarantee the SELECTED cue survives). */
|
|
170
|
+
@media (forced-colors: active) {
|
|
171
|
+
:scope > [data-part='tablist'] {
|
|
172
|
+
border-block-end-color: CanvasText;
|
|
173
|
+
}
|
|
174
|
+
ui-tab:state(selected) {
|
|
175
|
+
color: Highlight;
|
|
176
|
+
}
|
|
177
|
+
ui-tab:state(selected)::after {
|
|
178
|
+
background: Highlight;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { UIContainerElement } from '../../dom/container.js';
|
|
2
|
+
import { type ReactiveProps } from '../../dom/index.js';
|
|
3
|
+
declare const props: {
|
|
4
|
+
selected: {
|
|
5
|
+
reflect: true;
|
|
6
|
+
type: import("../../index.js").PropType<string>;
|
|
7
|
+
default: string;
|
|
8
|
+
attribute?: string | false;
|
|
9
|
+
};
|
|
10
|
+
fill: {
|
|
11
|
+
reflect: true;
|
|
12
|
+
type: import("../../index.js").PropType<boolean>;
|
|
13
|
+
default: boolean;
|
|
14
|
+
attribute?: string | false;
|
|
15
|
+
};
|
|
16
|
+
elevation: {
|
|
17
|
+
reflect: true;
|
|
18
|
+
type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
|
|
19
|
+
default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
|
|
20
|
+
attribute?: string | false;
|
|
21
|
+
};
|
|
22
|
+
brightness: {
|
|
23
|
+
reflect: true;
|
|
24
|
+
type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
|
|
25
|
+
default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
|
|
26
|
+
attribute?: string | false;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export interface UITabsElement extends ReactiveProps<typeof props> {
|
|
30
|
+
}
|
|
31
|
+
export declare class UITabsElement extends UIContainerElement {
|
|
32
|
+
#private;
|
|
33
|
+
static props: {
|
|
34
|
+
selected: {
|
|
35
|
+
reflect: true;
|
|
36
|
+
type: import("../../index.js").PropType<string>;
|
|
37
|
+
default: string;
|
|
38
|
+
attribute?: string | false;
|
|
39
|
+
};
|
|
40
|
+
fill: {
|
|
41
|
+
reflect: true;
|
|
42
|
+
type: import("../../index.js").PropType<boolean>;
|
|
43
|
+
default: boolean;
|
|
44
|
+
attribute?: string | false;
|
|
45
|
+
};
|
|
46
|
+
elevation: {
|
|
47
|
+
reflect: true;
|
|
48
|
+
type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
|
|
49
|
+
default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
|
|
50
|
+
attribute?: string | false;
|
|
51
|
+
};
|
|
52
|
+
brightness: {
|
|
53
|
+
reflect: true;
|
|
54
|
+
type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
|
|
55
|
+
default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
|
|
56
|
+
attribute?: string | false;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
protected connected(): void;
|
|
60
|
+
}
|
|
61
|
+
export {};
|