@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,115 @@
|
|
|
1
|
+
// button.ts — UIButtonElement, the reference FACE control (goals.md §G5 / Phase-1). BEHAVIOUR + props +
|
|
2
|
+
// role + self-define ONLY.
|
|
3
|
+
//
|
|
4
|
+
// Content model — host-as-grid (ADR-0006): the user's light-DOM children (an optional leading icon + the
|
|
5
|
+
// label) are placed by button.css's `:has()` grid (s7). `render()` stays the inherited no-op — there is no
|
|
6
|
+
// template, so the host render effect never commits anything of its own. ADR-0133 ADDS one adoption move
|
|
7
|
+
// beyond that: the label region (the non-adornment children — anything without `slot="leading"`/
|
|
8
|
+
// `slot="trailing"`) is wrapped in a real, persistent `<span data-part="label">` so button.css can carry
|
|
9
|
+
// `text-overflow` on it (anonymous grid text has no selector surface for the clip — button.css:99-103's own
|
|
10
|
+
// long-standing deferral comment). This is `ui-text`'s stamp/heal shape (ADR-0078 cl.4) adapted: nodes are
|
|
11
|
+
// MOVED, never cloned, and a `childList` MutationObserver re-adopts any stray label child that lands
|
|
12
|
+
// directly on the host later (parser streaming, or the A2UI `buttonFactory`'s `el.textContent = label`
|
|
13
|
+
// write, `factories.ts:117-119` — the identical clobbering pattern ui-text's heal observer exists to
|
|
14
|
+
// survive). The wrapper is UNCONDITIONAL — no opt-in prop — because the label is already forced to one line
|
|
15
|
+
// (`white-space: nowrap`, unconditional); ellipsis-on-overflow completes that existing default rather than
|
|
16
|
+
// adding a new axis. An `icon-only` button (no label content at all) never gets an empty wrapper — the heal
|
|
17
|
+
// pass only builds one when there is a stray non-adornment node to hold, so the fifth (square) structure is
|
|
18
|
+
// untouched. Geometry/colour otherwise live entirely in button.css.
|
|
19
|
+
//
|
|
20
|
+
// Keyboard activation is the `pressActivation` trait (Space/Enter → a native-parity `host.click()`),
|
|
21
|
+
// invoked from `connected()` so its listeners ride the connection AbortSignal. Focusability is the
|
|
22
|
+
// `tabbable` trait (ADR-0010): tabindex=0 while enabled, out of the tab order while disabled — the inert
|
|
23
|
+
// half that lives in a trait. ARIA `role` is set through `ElementInternals`, never a host attribute, and so
|
|
24
|
+
// is the disabled AX state (`ariaDisabled`) — a control-level effect because `internals` is protected and a
|
|
25
|
+
// trait cannot reach it. `controls → dom + traits` is the allowed import direction.
|
|
26
|
+
import { UIElement, prop } from "../../dom/index.js";
|
|
27
|
+
import { pressActivation } from "../../traits/press-activation.js";
|
|
28
|
+
import { tabbable } from "../../traits/tabbable.js";
|
|
29
|
+
/** An adornment child sits in a POSITION slot (ADR-0006/ADR-0012) — everything else is label content. */
|
|
30
|
+
const isAdornment = (node) => node instanceof Element && (node.getAttribute('slot') === 'leading' || node.getAttribute('slot') === 'trailing');
|
|
31
|
+
const props = {
|
|
32
|
+
// variant/size REFLECT so the attribute-selector styling (`[variant]`/`[size]` in button.css repointing
|
|
33
|
+
// the colour roles + dimensional ramp) applies to JS-set values too, not only author-set attributes.
|
|
34
|
+
variant: { ...prop.enum(['solid', 'soft', 'ghost'], 'solid'), reflect: true },
|
|
35
|
+
size: { ...prop.enum(['sm', 'md', 'lg'], 'md'), reflect: true },
|
|
36
|
+
// `disabled` reflects to a `disabled` attribute so CSS can render the host pointer-inert (s7); the
|
|
37
|
+
// trait already guards keyboard activation off `() => this.disabled`.
|
|
38
|
+
disabled: { ...prop.boolean(false), reflect: true },
|
|
39
|
+
// icon-only ⇒ the geometry law's fifth structure (references/geometry.md "icon-only (no label) →
|
|
40
|
+
// square"): a real slotted adornment (leading/trailing) with NO label content at all. CSS alone
|
|
41
|
+
// cannot detect an empty/text-node label — `:has()` only matches ELEMENTS, so a bare text-node label
|
|
42
|
+
// (the common `<svg slot=leading>…Download</ui-button>` pattern) is invisible to it — so this is an
|
|
43
|
+
// explicit author opt-in (button.css's `:scope[icon-only]` structure). The accessible name must then
|
|
44
|
+
// come from `aria-label` (there is no label text to read), the toast.ts close-button precedent.
|
|
45
|
+
iconOnly: { ...prop.boolean(false), reflect: true, attribute: 'icon-only' },
|
|
46
|
+
};
|
|
47
|
+
export class UIButtonElement extends UIElement {
|
|
48
|
+
static props = props;
|
|
49
|
+
// The label wrapper (ADR-0133) — the one real element the label region is adopted into, so button.css
|
|
50
|
+
// can carry `text-overflow` on it. Null whenever there is no label content to hold (e.g. `icon-only`).
|
|
51
|
+
#label = null;
|
|
52
|
+
// The childList observer that heals the wrapper invariant — the SAME shape as ui-text's `#observer`
|
|
53
|
+
// (ADR-0078 cl.4): re-adopts a stray label child (parser streaming) and rebuilds the wrapper fresh after
|
|
54
|
+
// a `textContent` clobber (the A2UI `buttonFactory` `label` write). Disconnected in `disconnected()` —
|
|
55
|
+
// the "zero residue after removal" discipline a raw platform observer needs by hand.
|
|
56
|
+
#observer = null;
|
|
57
|
+
connected() {
|
|
58
|
+
this.internals.role = 'button'; // ARIA via internals — never a host role/aria-* attribute
|
|
59
|
+
tabbable(this, { disabled: () => this.disabled }); // tabindex=0 enabled / out of tab order disabled (ADR-0010)
|
|
60
|
+
pressActivation(this, { disabled: () => this.disabled }); // Space/Enter → click; auto-cleans on disconnect
|
|
61
|
+
// The disabled AX state — control-level (internals is protected, unreachable from a trait); scope-owned,
|
|
62
|
+
// so it re-runs on the disabled signal and is disposed on disconnect (zero residue).
|
|
63
|
+
this.effect(() => {
|
|
64
|
+
this.internals.ariaDisabled = this.disabled ? 'true' : null;
|
|
65
|
+
});
|
|
66
|
+
// The label wrapper (ADR-0133) — an initial heal pass adopts whatever label content already exists
|
|
67
|
+
// (parser-streamed or set before connect), then the observer heals every later mutation. Installed
|
|
68
|
+
// AFTER the initial pass so it never observes its own synchronous setup.
|
|
69
|
+
this.#heal();
|
|
70
|
+
this.#observer = new MutationObserver(() => this.#heal());
|
|
71
|
+
this.#observer.observe(this, { childList: true });
|
|
72
|
+
// Motion gate (interaction-states standard) — flip the `ready` custom state ONE FRAME PAST first paint, so
|
|
73
|
+
// the upgrade/first-paint styling SNAPS in and only subsequent state changes animate (the button.css
|
|
74
|
+
// transition is gated behind `:state(ready)`). requestAnimationFrame — NOT updateComplete (a microtask,
|
|
75
|
+
// before paint) — to clear the first paint; `states` is optional-chained because jsdom has no CustomStateSet
|
|
76
|
+
// (the real motion behaviour is the cross-engine smoke). Idempotent (a Set), so reconnect is safe.
|
|
77
|
+
requestAnimationFrame(() => this.internals.states?.add('ready'));
|
|
78
|
+
}
|
|
79
|
+
disconnected() {
|
|
80
|
+
this.#observer?.disconnect();
|
|
81
|
+
this.#observer = null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Restore the label-wrapper invariant (ADR-0133). A DETACHED wrapper (a `textContent`/`innerHTML`
|
|
85
|
+
* clobber that replaced every child, wrapper included) is forgotten; every stray non-adornment child
|
|
86
|
+
* currently on the host is then adopted into the wrapper, building one fresh only if there is at least
|
|
87
|
+
* one such child (an empty wrapper would pollute the `icon-only` single-column anatomy with a dead grid
|
|
88
|
+
* track). The wrapper's insert point is anchored on the TRAILING adornment element itself (never on
|
|
89
|
+
* `strays[0]`) — adornments are never moved, so inserting immediately before whatever trailing element
|
|
90
|
+
* currently exists (or appending, when there is none) places the wrapper after any leading adornment and
|
|
91
|
+
* before any trailing one regardless of interspersed whitespace text nodes (component-reviewer finding:
|
|
92
|
+
* an authored, pretty-printed `<ui-button>\n <svg slot="leading">…</svg>Label\n</ui-button>` has a
|
|
93
|
+
* whitespace text node BEFORE the leading adornment — anchoring on `strays[0]` put the wrapper ahead of
|
|
94
|
+
* it, inverting the anatomy). Adopting a node re-fires this same observer, but the next delivery finds
|
|
95
|
+
* the invariant satisfied — a no-op — so this self-converges within ≤2 passes (the ui-text `#heal`
|
|
96
|
+
* precedent).
|
|
97
|
+
*/
|
|
98
|
+
#heal() {
|
|
99
|
+
if (this.#label && this.#label.parentNode !== this)
|
|
100
|
+
this.#label = null; // detached — a full clobber
|
|
101
|
+
const strays = Array.from(this.childNodes).filter((node) => node !== this.#label && !isAdornment(node));
|
|
102
|
+
if (strays.length === 0)
|
|
103
|
+
return; // nothing to wrap — keep icon-only (and empty) buttons untouched
|
|
104
|
+
if (!this.#label) {
|
|
105
|
+
this.#label = document.createElement('span');
|
|
106
|
+
this.#label.setAttribute('data-part', 'label');
|
|
107
|
+
const trailing = Array.from(this.children).find((el) => el.getAttribute('slot') === 'trailing') ?? null;
|
|
108
|
+
this.insertBefore(this.#label, trailing); // before the trailing adornment, or appended when there is none
|
|
109
|
+
}
|
|
110
|
+
for (const node of strays)
|
|
111
|
+
this.#label.appendChild(node);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!customElements.get('ui-button'))
|
|
115
|
+
customElements.define('ui-button', UIButtonElement);
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/* calendar.css — ui-calendar, the standalone month-grid date picker control (ADR-0048 / Wave-5B-1;
|
|
2
|
+
* range mode ADR-0093; fluid tracks + two-layer cell ADR-0105).
|
|
3
|
+
*
|
|
4
|
+
* Two clearly-sectioned blocks:
|
|
5
|
+
*
|
|
6
|
+
* [1] TOKEN BLOCK — `:where(ui-calendar)` (specificity 0,0,0) DECLARES the `--ui-calendar-*`
|
|
7
|
+
* chain from colour roles (`--md-sys-color-{family}-{role}`) and the dimensional ramp. Fallback values
|
|
8
|
+
* are sensible until the tokens-specialist delivers the canonical token definitions. The
|
|
9
|
+
* `[size]` repoint triples follow the button/select/text-field precedent; `[disabled]` mutes
|
|
10
|
+
* the nav buttons. `--ui-calendar-range-fill`/`-range-ink` (ADR-0093 clause 4, ERRATUM
|
|
11
|
+
* 2026-07-07) alias `--md-sys-color-primary-surface-highest` / `-primary-on-surface` —
|
|
12
|
+
* 0.0655/0.0722 OKLCH-L separation from the panel bg in light/dark (was ~0.001, effectively
|
|
13
|
+
* invisible — see the erratum on ADR-0093) and 12.70:1 / 12.16:1 ink-vs-fill contrast, well
|
|
14
|
+
* past the AA 4.5:1 floor (verified against tokens.css's OKLCH primitives).
|
|
15
|
+
*
|
|
16
|
+
* [2] STYLES BLOCK — `@scope (ui-calendar)` CONSUMES only `--ui-calendar-*` for its OWN selectors.
|
|
17
|
+
* The panel (`[data-part=panel][data-box]`) adopts the shared container box-model (inset margins
|
|
18
|
+
* + sticky header) from `_surface/container-box.css` at zero specificity. The grid is a CSS
|
|
19
|
+
* grid of 7 FLUID columns (ADR-0105: `minmax(--ui-calendar-cell-size, 1fr)` — the cell-size ramp
|
|
20
|
+
* is the track FLOOR, not a fixed track; shrink-wrapped it resolves to the floor exactly, given
|
|
21
|
+
* surplus width the grid fills it); `[role=row]` divs use `display:contents` so cells participate
|
|
22
|
+
* directly in the parent grid (a clean flat-grid layout; `[role=row]`'s ARIA role is set via
|
|
23
|
+
* attribute, not CSS, so `display:contents` does not affect the accessibility tree). Day cells are
|
|
24
|
+
* a TWO-LAYER cell (ADR-0105, CSS-only — the single `<button role=gridcell>` of calendar.ts is
|
|
25
|
+
* unchanged): the BAND layer is the button box itself (stretches to its fluid track — the square
|
|
26
|
+
* `[data-in-range]` wash, ink/disabled/outside colours, the enlarged hit target) and the POINT
|
|
27
|
+
* layer is a fixed `--ui-calendar-cell-size` circle (`::before`, absolutely centred, negative
|
|
28
|
+
* `z-index` under an `isolation:isolate` stacking context on the button so it paints ABOVE the
|
|
29
|
+
* band wash and BELOW the numeral) carrying the aria-selected/hover/today-ring/focus-ring — the
|
|
30
|
+
* things that are legitimately circular, at every track width. A second pseudo (`::after`) on
|
|
31
|
+
* `[data-range-start]`/`[data-range-end]` (mutually exclusive with the OTHER endpoint attribute
|
|
32
|
+
* via `:not()`, so a same-day range paints neither) bridges a half-track wash from the circle's
|
|
33
|
+
* edge to the band interior — `calc(50% + cell-size/2)` to the far inline edge, which is exactly
|
|
34
|
+
* 0-width at the track floor (today's rendering, byte-for-byte) and only opens up under surplus
|
|
35
|
+
* width. The [data-today] ring, [aria-selected]/[data-in-range] fills, and the half-wash are
|
|
36
|
+
* load-bearing colour values protected under forced-colors via `forced-color-adjust:none`
|
|
37
|
+
* (inherited by the pseudos from the button rules that already set it — it's an inherited
|
|
38
|
+
* property).
|
|
39
|
+
*
|
|
40
|
+
* Standalone — NOT injected from calendar.ts (behaviour-only `.ts`, no runtime style injection
|
|
41
|
+
* per plan §2). Linked via component-styles.css; foundation roles FIRST in the load order.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/* ══════════════════════ [1] TOKEN BLOCK — :where(ui-calendar) ══════════════════════════════ */
|
|
45
|
+
|
|
46
|
+
:where(ui-calendar) {
|
|
47
|
+
/* ── Cell geometry (ADR-0105: re-stated as the track FLOOR + the point-layer diameter — the
|
|
48
|
+
value is unchanged, only its meaning widens). The day cell's block-size is always this; the
|
|
49
|
+
inline-size is this at the track floor (shrink-wrapped) and grows with surplus width (fluid
|
|
50
|
+
`minmax()` track) — the fixed circular POINT layer (::before) stays this diameter regardless.
|
|
51
|
+
Aliases --md-sys-height-md so the cell rides the explicit per-[scale] height ramp (ADR-0038),
|
|
52
|
+
tracking scale automatically without a multiplier. [size] repoints below.
|
|
53
|
+
--ui-calendar-gap is the inter-cell gap; aliases --md-sys-space-xs (4px × density). */
|
|
54
|
+
--ui-calendar-cell-size: var(--md-sys-height-md);
|
|
55
|
+
--ui-calendar-gap: var(--md-sys-space-xs);
|
|
56
|
+
|
|
57
|
+
/* ── Day-cell typography — font-size at a comfortable reading size inside the cell. */
|
|
58
|
+
--ui-calendar-font: var(--md-sys-font-md, 14px);
|
|
59
|
+
|
|
60
|
+
/* ── Selected-day fill (the committed value cell).
|
|
61
|
+
--md-sys-color-primary-selected is the AA primary-on-accent role (tokens-calendar); fall back to
|
|
62
|
+
--md-sys-color-primary so the build stays green if it lands a moment after this component. */
|
|
63
|
+
--ui-calendar-selected-fill: var(--md-sys-color-primary-selected, var(--md-sys-color-primary));
|
|
64
|
+
--ui-calendar-selected-ink: var(--md-sys-color-primary-on-primary); /* white — the on-primary pair */
|
|
65
|
+
|
|
66
|
+
/* ── Today's ring (the current date, whether selected or not).
|
|
67
|
+
Aliases --md-sys-color-primary-outline; drawn as an INSET box-shadow so it sits inside the cell
|
|
68
|
+
and stays distinct from the offset-outside focus outline (see forced-colors note below). */
|
|
69
|
+
--ui-calendar-today-ring: var(--md-sys-color-primary-outline);
|
|
70
|
+
|
|
71
|
+
/* ── Disabled / out-of-range day ink (muted; WCAG exempts inactive controls). */
|
|
72
|
+
--ui-calendar-disabled-ink: var(--md-sys-color-neutral-on-surface-variant);
|
|
73
|
+
|
|
74
|
+
/* ── Adjacent-month day ink (prev/next month overflow cells — dimmed to neutral mid). */
|
|
75
|
+
--ui-calendar-outside-ink: var(--md-sys-color-neutral);
|
|
76
|
+
|
|
77
|
+
/* ── Nav button geometry — the ‹ and › inline affordances are sized = font (§4.6 law).
|
|
78
|
+
They are plain buttons; no fixed icon-cell needed (they're typography glyphs). */
|
|
79
|
+
--ui-calendar-nav-font: var(--md-sys-font-md, 14px);
|
|
80
|
+
|
|
81
|
+
/* ── Panel surface — inherits the container/surface tokens for bg/border/radius.
|
|
82
|
+
The panel is a Container/surface (NOT a Control-class surface), so it uses bg + outline. */
|
|
83
|
+
--ui-calendar-panel-bg: var(--md-sys-color-neutral-surface, #fff);
|
|
84
|
+
--ui-calendar-panel-ink: var(--md-sys-color-neutral-on-surface, #1c1c1e);
|
|
85
|
+
--ui-calendar-panel-outline: var(--md-sys-color-neutral-outline-variant, #c4c4c4);
|
|
86
|
+
--ui-calendar-panel-radius: var(--md-sys-shape-corner-base, 0.375rem);
|
|
87
|
+
|
|
88
|
+
/* ── Weekday column-header ink (muted row above the cells). */
|
|
89
|
+
--ui-calendar-header-ink: var(--md-sys-color-neutral-on-surface-variant, #767676);
|
|
90
|
+
|
|
91
|
+
/* ── Cell hover background (non-selected, non-disabled). */
|
|
92
|
+
--ui-calendar-cell-hover-bg: var(--md-sys-color-neutral-surface-high, #f0f0f0);
|
|
93
|
+
|
|
94
|
+
/* ── Range interior wash (ADR-0093 clause 4, ERRATUM 2026-07-07) — the SQUARE fill for
|
|
95
|
+
[data-in-range] cells (endpoints reuse --ui-calendar-selected-fill/-ink above, unchanged).
|
|
96
|
+
ADR-0093 clause 4 originally aliased --md-sys-color-primary-surface / -primary-on-surface —
|
|
97
|
+
measured (gallery audit) to differ from the panel's --md-sys-color-neutral-surface by only
|
|
98
|
+
~0.001 OKLCH L in BOTH schemes (light 0.9336 vs 0.9347; dark 0.2598 vs 0.2592): the band was
|
|
99
|
+
load-bearing colour with no real lightness separation, i.e. effectively invisible. Repointed
|
|
100
|
+
to --md-sys-color-primary-surface-highest, whose OKLCH L differs from the panel by 0.0655
|
|
101
|
+
(light) / 0.0722 (dark) — ~65×/~72× the old gap, a clearly-visible-but-soft wash (chroma
|
|
102
|
+
stays far below the solid endpoint fill's --md-sys-color-primary-550/600). Ink stays
|
|
103
|
+
--md-sys-color-primary-on-surface: ink-vs-fill contrast is 12.70:1 (light) / 12.16:1 (dark),
|
|
104
|
+
comfortably past the AA 4.5:1 floor a text/numeral pairing needs (see the erratum on
|
|
105
|
+
.claude/docs/adr/0093-calendar-range-selection.md for the full measurement). */
|
|
106
|
+
--ui-calendar-range-fill: var(--md-sys-color-primary-surface-highest, #dde6f4);
|
|
107
|
+
--ui-calendar-range-ink: var(--md-sys-color-primary-on-surface, #1c1c1e);
|
|
108
|
+
|
|
109
|
+
/* ── user-invalid (ADR-0051) — the danger border repoint, the text-field precedent. Appears only
|
|
110
|
+
AFTER the first interaction (the trackUserInvalid controller owns that timing; calendar.ts
|
|
111
|
+
toggles :state(user-invalid)). */
|
|
112
|
+
--ui-calendar-border-invalid: var(--md-sys-color-danger);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* [size] repoints cell size onto the §1-ramp height entries (ADR-0038: explicit per-[scale] table,
|
|
116
|
+
24/28/36px at the default ui-md scale). Pattern mirrors checkbox [size]→--md-sys-compact-* repoints. */
|
|
117
|
+
:where(ui-calendar[size='sm']) {
|
|
118
|
+
--ui-calendar-cell-size: var(--md-sys-height-sm); /* 24px default; retables under [scale] */
|
|
119
|
+
--ui-calendar-font: var(--md-sys-font-sm, 13px);
|
|
120
|
+
--ui-calendar-nav-font: var(--md-sys-font-sm, 13px);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
:where(ui-calendar[size='lg']) {
|
|
124
|
+
--ui-calendar-cell-size: var(--md-sys-height-lg); /* 36px default ≈ 2.25rem target; retables under [scale] */
|
|
125
|
+
--ui-calendar-font: var(--md-sys-font-lg, 16px);
|
|
126
|
+
--ui-calendar-nav-font: var(--md-sys-font-lg, 16px);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* Disabled host — mute the nav-button ink (day cells carry their own aria-disabled state). */
|
|
130
|
+
:where(ui-calendar[disabled]) {
|
|
131
|
+
--ui-calendar-panel-ink: var(--md-sys-color-neutral-on-surface-variant, #767676);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* ══════════════════════ [2] STYLES BLOCK — @scope (ui-calendar) ════════════════════════════ */
|
|
135
|
+
|
|
136
|
+
@scope (ui-calendar) {
|
|
137
|
+
/* Host: inline-block so ui-calendar shrink-wraps its panel by default (like a compact form
|
|
138
|
+
widget) when NOTHING gives it width. ADR-0105 — the grid's tracks are fluid (`minmax()`
|
|
139
|
+
below), so a caller that DOES grant width (a stretched flex/grid cell — blockification
|
|
140
|
+
turns this inline-block into the flex/grid item the cross-axis `stretch` default sizes,
|
|
141
|
+
ADR-0030) gets a grid that fills it, not a fixed-width grid stranded in a wide, half-empty
|
|
142
|
+
panel. The escape for a caller that wants the OLD always-compact behaviour even when
|
|
143
|
+
stretched: `max-inline-size: max-content` (one line, page-author override — ADR-0102). */
|
|
144
|
+
:scope {
|
|
145
|
+
display: inline-block;
|
|
146
|
+
vertical-align: top;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* ── Panel ([data-part=panel][data-box]) ────────────────────────────────────────────────
|
|
150
|
+
The [data-box] attribute opts this element into container-box.css's box-model:
|
|
151
|
+
• display: flow-root (BFC; child margins collapse to one inset)
|
|
152
|
+
• <header> children: INSET (not full-bleed), sticky, flex-row, padding inline=12/block=6, gap=8
|
|
153
|
+
• other direct children: margin: 6px (the uniform inset)
|
|
154
|
+
We add paint: background + border + radius. No position/size overrides needed — the
|
|
155
|
+
grid sizes the panel naturally to 7×cell-size + gaps. */
|
|
156
|
+
:scope [data-part='panel'] {
|
|
157
|
+
display: flow-root; /* BFC — child margins collapse; also set by container-box.css */
|
|
158
|
+
background-color: var(--ui-calendar-panel-bg);
|
|
159
|
+
color: var(--ui-calendar-panel-ink);
|
|
160
|
+
border: 1px solid var(--ui-calendar-panel-outline);
|
|
161
|
+
border-radius: var(--ui-calendar-panel-radius);
|
|
162
|
+
box-sizing: border-box;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* user-invalid (ADR-0051) — the danger border, appearing only AFTER the first interaction (the
|
|
166
|
+
trackUserInvalid controller owns that timing; calendar.ts toggles :state(user-invalid) on the
|
|
167
|
+
host). The panel is the calendar's one bordered surface — the text-field precedent applied to
|
|
168
|
+
the Container/surface-class part instead of a Control-class frame. */
|
|
169
|
+
:scope:state(user-invalid) [data-part='panel'] {
|
|
170
|
+
border-color: var(--ui-calendar-border-invalid);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* ── Nav header (<header data-part="nav">) ──────────────────────────────────────────────
|
|
174
|
+
container-box.css already gives the <header>: display:flex; align-items:center; gap=8px;
|
|
175
|
+
padding-inline=12px; padding-block=6px; margin=6px (INSET, not full-bleed); position:sticky;
|
|
176
|
+
inset-block-start=6px (the box's own inset — matches the at-rest margin, so nothing snaps
|
|
177
|
+
to 0 if the panel ever scrolls); z-index:1. We add: space-between so ‹ title › spreads,
|
|
178
|
+
and the title grows to fill. */
|
|
179
|
+
:scope [data-part='nav'] {
|
|
180
|
+
justify-content: space-between;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* Month/year title: grows to fill the space between the two nav buttons. Centred text. */
|
|
184
|
+
:scope [data-part='title'] {
|
|
185
|
+
flex: 1;
|
|
186
|
+
text-align: center;
|
|
187
|
+
font-size: var(--ui-calendar-nav-font);
|
|
188
|
+
font-weight: 600;
|
|
189
|
+
line-height: 1;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* Nav buttons (‹ ›): plain icon affordances — no background, no border, just the glyph. */
|
|
193
|
+
:scope [data-part='prev'],
|
|
194
|
+
:scope [data-part='next'] {
|
|
195
|
+
flex-shrink: 0;
|
|
196
|
+
padding: 0.25rem;
|
|
197
|
+
font-size: var(--ui-calendar-nav-font);
|
|
198
|
+
line-height: 1;
|
|
199
|
+
background: transparent;
|
|
200
|
+
border: none;
|
|
201
|
+
border-radius: var(--ui-calendar-panel-radius);
|
|
202
|
+
color: inherit;
|
|
203
|
+
cursor: pointer;
|
|
204
|
+
display: flex;
|
|
205
|
+
align-items: center;
|
|
206
|
+
justify-content: center;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* The injected Phosphor caret-left/caret-right (setIcon, @agent-ui-kit/icons): 1em tracks the
|
|
210
|
+
button's own font-size (--ui-calendar-nav-font above) at every [size] tier — the same
|
|
211
|
+
rendered size as the previous ‹ › text glyphs. Overrides the svg's own width/height="100%"
|
|
212
|
+
presentational attributes (resolve.ts). */
|
|
213
|
+
:scope [data-part='prev'] svg,
|
|
214
|
+
:scope [data-part='next'] svg {
|
|
215
|
+
display: block;
|
|
216
|
+
inline-size: 1em;
|
|
217
|
+
block-size: 1em;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
:scope [data-part='prev']:hover,
|
|
221
|
+
:scope [data-part='next']:hover {
|
|
222
|
+
background-color: var(--ui-calendar-cell-hover-bg);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
:scope [data-part='prev']:focus-visible,
|
|
226
|
+
:scope [data-part='next']:focus-visible {
|
|
227
|
+
outline: var(--md-sys-state-focus-ring-width, 2px) solid var(--md-sys-color-focus-ring);
|
|
228
|
+
outline-offset: var(--md-sys-state-focus-ring-offset, 2px);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* ── Grid container ([data-part="grid"]) ────────────────────────────────────────────────
|
|
232
|
+
A CSS grid of 7 FLUID columns (ADR-0105), one per day of the week. `minmax(cell-size, 1fr)`:
|
|
233
|
+
the cell-size ramp is the track FLOOR (shrink-wrapped/intrinsic sizing resolves `1fr` to its
|
|
234
|
+
base size when the grid's own inline-size is undetermined by an ancestor — today's exact
|
|
235
|
+
compact rendering, byte-identical); given a DEFINITE width from an ancestor (a stretched
|
|
236
|
+
flex/grid cell), the seven tracks share the surplus equally and the grid fills it. Row
|
|
237
|
+
heights are untouched — width adapts, the month never inflates vertically. The [role=row]
|
|
238
|
+
divs use display:contents so their children (columnheaders and gridcells) participate directly
|
|
239
|
+
in the flat 7-column grid — each week flows into the next row automatically. The
|
|
240
|
+
weekday header row and the 6 week rows are all part of the same flat grid, avoiding
|
|
241
|
+
a nested-flex layout that would need explicit sizing on every row. */
|
|
242
|
+
:scope [data-part='grid'] {
|
|
243
|
+
display: grid;
|
|
244
|
+
grid-template-columns: repeat(7, minmax(var(--ui-calendar-cell-size, 2rem), 1fr));
|
|
245
|
+
gap: var(--ui-calendar-gap, 0.125rem);
|
|
246
|
+
/* The direct-child margin from [data-box] applies here (6px all around, the box's uniform
|
|
247
|
+
inset). We keep the default inset margin — the grid sits slightly inside the panel. */
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* [role=row] is transparent to the parent CSS grid — cells participate directly.
|
|
251
|
+
The ARIA role is set via the HTML attribute, NOT by CSS, so display:contents does
|
|
252
|
+
NOT remove the row from the accessibility tree (the role attribute is authoritative). */
|
|
253
|
+
:scope [data-part='grid'] [role='row'] {
|
|
254
|
+
display: contents;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* ── Weekday column headers ──────────────────────────────────────────────────────────────
|
|
258
|
+
Non-interactive; muted ink; centred. The <span role="columnheader"> elements sit in
|
|
259
|
+
the first grid row and establish the 7-column visual header. */
|
|
260
|
+
:scope [data-part='grid'] [role='columnheader'] {
|
|
261
|
+
display: flex;
|
|
262
|
+
align-items: center;
|
|
263
|
+
justify-content: center;
|
|
264
|
+
block-size: calc(var(--ui-calendar-cell-size, 2rem) * 0.75); /* slightly smaller than cells */
|
|
265
|
+
font-size: calc(var(--ui-calendar-font, 14px) * 0.85);
|
|
266
|
+
color: var(--ui-calendar-header-ink);
|
|
267
|
+
-webkit-user-select: none;
|
|
268
|
+
user-select: none;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* ── Day cells ([role=gridcell]) — the TWO-LAYER cell (ADR-0105) ─────────────────────────
|
|
272
|
+
The single button IS the BAND layer: it stretches to its (fluid) track, carrying the square
|
|
273
|
+
[data-in-range] wash, ink/disabled/outside colours, and the enlarged hit target (a strict
|
|
274
|
+
a11y win — the click-delegation commit path already resolves through
|
|
275
|
+
closest('[role="gridcell"]'), so a wider button changes nothing behaviourally). `position:
|
|
276
|
+
relative` gives its two pseudo layers a containing block; `isolation: isolate` establishes a
|
|
277
|
+
stacking context so a NEGATIVE z-index on the pseudos still paints ABOVE the button's own
|
|
278
|
+
background (paint-order step 1) and BELOW the day-number text (an in-flow inline descendant,
|
|
279
|
+
which paints ahead of z-index:auto/0 descendants but AFTER negative-z ones — negative z-index
|
|
280
|
+
is the only way under it, CSS2.1 Appendix E). line-height:1 (ADR-0036: single-line control
|
|
281
|
+
glyph centred by the frame). */
|
|
282
|
+
:scope [data-part='grid'] [role='gridcell'] {
|
|
283
|
+
display: flex;
|
|
284
|
+
align-items: center;
|
|
285
|
+
justify-content: center;
|
|
286
|
+
box-sizing: border-box;
|
|
287
|
+
position: relative;
|
|
288
|
+
isolation: isolate;
|
|
289
|
+
block-size: var(--ui-calendar-cell-size, 2rem); /* rows never inflate — only inline-size is fluid */
|
|
290
|
+
border: none;
|
|
291
|
+
border-radius: 0; /* the BAND is square; circularity lives on the POINT layer below */
|
|
292
|
+
background: transparent;
|
|
293
|
+
color: inherit;
|
|
294
|
+
font-size: var(--ui-calendar-font, 14px);
|
|
295
|
+
line-height: var(--md-sys-control-line-height, 1);
|
|
296
|
+
cursor: pointer;
|
|
297
|
+
padding: 0;
|
|
298
|
+
-webkit-user-select: none;
|
|
299
|
+
user-select: none;
|
|
300
|
+
outline: none; /* the POINT layer's ::before carries the focus ring below — a
|
|
301
|
+
ring on the (now possibly wide) band itself would draw a
|
|
302
|
+
rectangle, not the circle ADR-0105 requires */
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* ── The POINT layer (::before) — a FIXED --ui-calendar-cell-size circle, always centred in the
|
|
306
|
+
button regardless of its (fluid) width. Carries: the aria-selected/single/endpoint fill, the
|
|
307
|
+
hover wash, the [data-today] inset ring, and the :focus-visible ring — the things that are
|
|
308
|
+
legitimately circular at every track width (ADR-0105 decision 3). `inset:0; margin:auto` +
|
|
309
|
+
a fixed inline/block-size is the "centre a fixed box inside an auto-sized positioned parent"
|
|
310
|
+
technique — it re-centres itself as the button's track grows, no JS/ResizeObserver needed. */
|
|
311
|
+
:scope [data-part='grid'] [role='gridcell']::before {
|
|
312
|
+
content: '';
|
|
313
|
+
position: absolute;
|
|
314
|
+
inset: 0;
|
|
315
|
+
margin: auto;
|
|
316
|
+
inline-size: var(--ui-calendar-cell-size, 2rem);
|
|
317
|
+
block-size: var(--ui-calendar-cell-size, 2rem);
|
|
318
|
+
border-radius: 50%;
|
|
319
|
+
z-index: -1; /* under the numeral, above the band background */
|
|
320
|
+
pointer-events: none; /* decorative — the button itself owns hit-testing */
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
:scope [data-part='grid'] [role='gridcell']:hover::before {
|
|
324
|
+
background-color: var(--ui-calendar-cell-hover-bg);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
:scope [data-part='grid'] [role='gridcell']:focus-visible::before {
|
|
328
|
+
outline: var(--md-sys-state-focus-ring-width, 2px) solid var(--md-sys-color-focus-ring);
|
|
329
|
+
outline-offset: var(--md-sys-state-focus-ring-offset, 2px);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* ── Today ring — an inset ring marking the current date (whether selected or not), on the
|
|
333
|
+
fixed-diameter POINT layer so it stays a true ring, never stretched into an oval. Uses
|
|
334
|
+
box-shadow so it stacks on top of the selected fill when the selected day = today (two
|
|
335
|
+
independent paint channels on the same pseudo — background-color + box-shadow never
|
|
336
|
+
conflict). `forced-color-adjust: none` ensures the ring survives WHCM (load-bearing colour). */
|
|
337
|
+
:scope [data-part='grid'] [role='gridcell'][data-today]::before {
|
|
338
|
+
box-shadow: inset 0 0 0 1.5px var(--ui-calendar-today-ring, currentcolor);
|
|
339
|
+
forced-color-adjust: none;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/* ── Selected fill — the committed value cell (single mode) OR a range endpoint (range mode:
|
|
343
|
+
[data-range-start]/[data-range-end] cells carry aria-selected="true" too — ADR-0093 clause 4
|
|
344
|
+
"endpoints reuse the existing circular fill"), painted on the fixed-diameter POINT layer so it
|
|
345
|
+
stays a circle at every width, never an ellipse (ADR-0105). `:not([data-in-range])` excludes
|
|
346
|
+
range mode's interior cells — they ALSO carry aria-selected="true" (clause 4's "the whole band
|
|
347
|
+
is selected") but must show the SQUARE band wash below, not an accent circle; endpoints never
|
|
348
|
+
carry [data-in-range] (rangeSelectionFor() computes them as mutually exclusive), so this
|
|
349
|
+
selector is a no-op narrowing for single mode and the real endpoint/interior split for range
|
|
350
|
+
mode. `color` stays on the BUTTON (inherited by the real day-number text, centred over the
|
|
351
|
+
circle) — `forced-color-adjust:none` keeps the fill in WHCM. */
|
|
352
|
+
:scope [data-part='grid'] [role='gridcell'][aria-selected='true']:not([data-in-range]) {
|
|
353
|
+
color: var(--ui-calendar-selected-ink, #fff);
|
|
354
|
+
forced-color-adjust: none;
|
|
355
|
+
}
|
|
356
|
+
:scope [data-part='grid'] [role='gridcell'][aria-selected='true']:not([data-in-range])::before {
|
|
357
|
+
background-color: var(--ui-calendar-selected-fill, #0057d8);
|
|
358
|
+
forced-color-adjust: none;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* ── Range interior wash ([data-in-range], ADR-0093 clause 4) — SQUARE (radius:0), painted on
|
|
362
|
+
the BAND layer (the button itself) so it spans the FULL fluid track at any width — the wash
|
|
363
|
+
never opens holes between abutting interior cells (ADR-0105 decision 2). Never co-occurs with
|
|
364
|
+
[data-range-start]/[data-range-end] (rangeSelectionFor() computes them as mutually exclusive),
|
|
365
|
+
so this rule only ever touches true interior cells; under forced-colors the explicit override
|
|
366
|
+
below lands the SAME Highlight/HighlightText pair as the endpoints — the whole band is one
|
|
367
|
+
self-delimiting run (ADR-0093 clause 4) — with the circle-vs-square shape (POINT vs BAND
|
|
368
|
+
layer, untouched by forced-colors) left as the sole endpoint-vs-interior signifier (ADR-0057
|
|
369
|
+
non-colour-signifier rule). */
|
|
370
|
+
:scope [data-part='grid'] [role='gridcell'][data-in-range] {
|
|
371
|
+
background-color: var(--ui-calendar-range-fill, #dde6f4);
|
|
372
|
+
color: var(--ui-calendar-range-ink, #1c1c1e);
|
|
373
|
+
forced-color-adjust: none;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* ── Range endpoint half-wash (::after, ADR-0105 decision 3) — bridges the fixed POINT circle to
|
|
377
|
+
the band interior on a [data-range-start]/[data-range-end] cell, so a wide track never opens a
|
|
378
|
+
gap between the endpoint circle and the neighbouring interior wash. Logical `inset-inline-*`
|
|
379
|
+
(no directional gradient) — RTL-safe by construction, the mirrored side follows for free. Spans
|
|
380
|
+
from the circle's OWN edge (`50% + cell-size/2` from centre) to the button's far inline edge —
|
|
381
|
+
at the track floor (button inline-size == cell-size) that span is exactly 0 (today's rendering,
|
|
382
|
+
byte-for-byte); it only opens under surplus width. `z-index:-2` (below the ::before circle,
|
|
383
|
+
which paints over its near portion, CSS2.1 Appendix E step 2 — most-negative-first) — only the
|
|
384
|
+
interior-facing half is ever visible. `:not([data-range-end])` / `:not([data-range-start])` on
|
|
385
|
+
the OTHER endpoint attribute excludes a same-day range (a cell that is BOTH start and end) —
|
|
386
|
+
with no interior cell on either side, no half-wash is drawn (nothing to bridge to). */
|
|
387
|
+
:scope [data-part='grid'] [role='gridcell'][data-range-start]:not([data-range-end])::after,
|
|
388
|
+
:scope [data-part='grid'] [role='gridcell'][data-range-end]:not([data-range-start])::after {
|
|
389
|
+
content: '';
|
|
390
|
+
position: absolute;
|
|
391
|
+
inset-block: 0;
|
|
392
|
+
background-color: var(--ui-calendar-range-fill, #dde6f4);
|
|
393
|
+
z-index: -2;
|
|
394
|
+
pointer-events: none;
|
|
395
|
+
forced-color-adjust: none;
|
|
396
|
+
}
|
|
397
|
+
:scope [data-part='grid'] [role='gridcell'][data-range-start]:not([data-range-end])::after {
|
|
398
|
+
inset-inline-start: calc(50% + var(--ui-calendar-cell-size, 2rem) / 2);
|
|
399
|
+
inset-inline-end: 0;
|
|
400
|
+
}
|
|
401
|
+
:scope [data-part='grid'] [role='gridcell'][data-range-end]:not([data-range-start])::after {
|
|
402
|
+
inset-inline-start: 0;
|
|
403
|
+
inset-inline-end: calc(50% + var(--ui-calendar-cell-size, 2rem) / 2);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/* ── Adjacent-month cells (data-outside) — muted ink; still clickable (navigates month). */
|
|
407
|
+
:scope [data-part='grid'] [role='gridcell'][data-outside] {
|
|
408
|
+
color: var(--ui-calendar-outside-ink, #767676);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* ── Disabled / out-of-range cells (aria-disabled=true) — pointer-inert; muted. opacity applies
|
|
412
|
+
to the whole stacking context (button + both pseudo layers together), so a disabled endpoint's
|
|
413
|
+
circle/wash mute along with its numeral — no separate pseudo overrides needed. */
|
|
414
|
+
:scope [data-part='grid'] [role='gridcell'][aria-disabled='true'] {
|
|
415
|
+
color: var(--ui-calendar-disabled-ink, #767676);
|
|
416
|
+
cursor: default;
|
|
417
|
+
pointer-events: none;
|
|
418
|
+
opacity: 0.5;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/* ── forced-colors (WHCM) ────────────────────────────────────────────────────────────────
|
|
422
|
+
Four visually distinct states must survive system-colour flattening (ADR-0093 clause 4 —
|
|
423
|
+
the ADR-0048 three-state discipline, now four; ADR-0105 re-homes the paint per layer, the
|
|
424
|
+
mapping is unchanged):
|
|
425
|
+
focus = Highlight ring on the POINT layer (::before) — free via --md-sys-color-focus-ring
|
|
426
|
+
→ Highlight; stays circular at every width because the ring lives on the fixed-
|
|
427
|
+
diameter pseudo, never the (possibly wide) band
|
|
428
|
+
selected = Highlight fill + HighlightText numeral — explicit below; in range mode this is
|
|
429
|
+
the WHOLE band (endpoints AND interior both carry aria-selected="true" — clause 4
|
|
430
|
+
"the band is self-delimiting, so endpoint-vs-interior needs no third colour"); the
|
|
431
|
+
half-wash (::after) carries the SAME Highlight so a bridged band reads as one
|
|
432
|
+
contiguous run under WHCM too
|
|
433
|
+
today = ButtonText INSET ring on the POINT layer (offset-inside, inside the circle)
|
|
434
|
+
(interior vs endpoint shape — circle (POINT layer) vs square (BAND layer) — survives
|
|
435
|
+
forced-colors unaided: border-radius is not a colour property, so it is never flattened)
|
|
436
|
+
today MUST NOT use Highlight here — it would merge with the selected/band fill AND with the
|
|
437
|
+
focus ring, collapsing every state into one indistinguishable Highlight blob.
|
|
438
|
+
The button rules above already carry `forced-color-adjust:none` per state, unconditionally
|
|
439
|
+
(not gated by this media query); it is an INHERITED property, so ::before/::after pick it up
|
|
440
|
+
from their originating button automatically — no separate declaration needed on the pseudos
|
|
441
|
+
(only the explicit system-colour paint below needs restating on the pseudo selectors, since
|
|
442
|
+
background-color/box-shadow are NOT inherited). */
|
|
443
|
+
@media (forced-colors: active) {
|
|
444
|
+
/* Panel surface: Canvas / CanvasText (standard surface mapping). */
|
|
445
|
+
:scope [data-part='panel'] {
|
|
446
|
+
background-color: Canvas;
|
|
447
|
+
color: CanvasText;
|
|
448
|
+
border-color: CanvasText;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/* Selected fill: Highlight / HighlightText (system selection pair — WCAG AA guaranteed by OS).
|
|
452
|
+
Matches range-mode endpoints too (they carry aria-selected="true"); the circle (::before) —
|
|
453
|
+
NOT the band — carries the fill, so the shape signifier survives forced-colors. */
|
|
454
|
+
:scope [data-part='grid'] [role='gridcell'][aria-selected='true']:not([data-in-range]) {
|
|
455
|
+
color: HighlightText;
|
|
456
|
+
}
|
|
457
|
+
:scope [data-part='grid'] [role='gridcell'][aria-selected='true']:not([data-in-range])::before {
|
|
458
|
+
background-color: Highlight;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/* Range interior (data-in-range): the SAME Highlight/HighlightText pair as the endpoints —
|
|
462
|
+
one self-delimiting band (ADR-0093 clause 4), painted on the BAND (button) so interior stays
|
|
463
|
+
square while endpoints (painted on the POINT layer above) stay circular. Written explicitly
|
|
464
|
+
(rather than relying on the [aria-selected='true'] rule above alone) so the four-state
|
|
465
|
+
contract stays true independent of any future reordering of this stylesheet. */
|
|
466
|
+
:scope [data-part='grid'] [role='gridcell'][data-in-range] {
|
|
467
|
+
background-color: Highlight;
|
|
468
|
+
color: HighlightText;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* Half-wash bridge: the same Highlight as the band — one contiguous run under WHCM too. */
|
|
472
|
+
:scope [data-part='grid'] [role='gridcell'][data-range-start]:not([data-range-end])::after,
|
|
473
|
+
:scope [data-part='grid'] [role='gridcell'][data-range-end]:not([data-range-start])::after {
|
|
474
|
+
background-color: Highlight;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/* Today ring: ButtonText INSET border on the fixed-diameter POINT layer — deliberately NOT
|
|
478
|
+
Highlight so focus/selected/today remain visually distinct: focus=Highlight-outside,
|
|
479
|
+
selected=Highlight-fill, today=ButtonText-inset. */
|
|
480
|
+
:scope [data-part='grid'] [role='gridcell'][data-today]::before {
|
|
481
|
+
box-shadow: inset 0 0 0 2px ButtonText;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/* Disabled cells: GrayText (the system muted role). */
|
|
485
|
+
:scope [data-part='grid'] [role='gridcell'][aria-disabled='true'] {
|
|
486
|
+
color: GrayText;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/* Nav buttons: ButtonText. */
|
|
490
|
+
:scope [data-part='prev'],
|
|
491
|
+
:scope [data-part='next'] {
|
|
492
|
+
color: ButtonText;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|