@adia-ai/web-components 0.8.4 → 0.8.5
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/CHANGELOG.md +18 -1
- package/USAGE.md +60 -25
- package/components/adia-mark/adia-mark.a2ui.json +123 -0
- package/components/adia-mark/adia-mark.class.js +64 -0
- package/components/adia-mark/adia-mark.css +48 -0
- package/components/adia-mark/adia-mark.d.ts +20 -0
- package/components/adia-mark/adia-mark.examples.md +26 -0
- package/components/adia-mark/adia-mark.js +17 -0
- package/components/adia-mark/adia-mark.test.js +69 -0
- package/components/adia-mark/adia-mark.yaml +121 -0
- package/components/agent-questions/agent-questions.a2ui.json +2 -1
- package/components/agent-questions/agent-questions.class.js +96 -53
- package/components/agent-questions/agent-questions.css +27 -106
- package/components/agent-questions/agent-questions.yaml +1 -0
- package/components/button/button.class.js +5 -1
- package/components/calendar-grid/calendar-grid.a2ui.json +3 -1
- package/components/calendar-grid/calendar-grid.class.js +11 -6
- package/components/calendar-grid/calendar-grid.css +42 -20
- package/components/calendar-grid/calendar-grid.yaml +5 -0
- package/components/calendar-picker/calendar-picker.a2ui.json +3 -1
- package/components/calendar-picker/calendar-picker.class.js +11 -6
- package/components/calendar-picker/calendar-picker.css +39 -19
- package/components/calendar-picker/calendar-picker.yaml +5 -0
- package/components/chart/chart.class.js +17 -14
- package/components/color-input/color-input.class.js +8 -5
- package/components/color-picker/color-picker.class.js +6 -3
- package/components/field/field.class.js +10 -7
- package/components/fields/fields.class.js +15 -12
- package/components/icon/icon.class.js +9 -5
- package/components/index.js +1 -0
- package/components/nav/nav.class.js +2 -1
- package/components/noodles/noodles.class.js +14 -10
- package/components/page/page.class.js +3 -2
- package/components/select/select.class.js +3 -2
- package/components/swatch/swatch.class.js +27 -24
- package/components/swiper/swiper.class.js +4 -1
- package/components/swiper/swiper.css +13 -4
- package/components/toolbar/toolbar.class.js +11 -6
- package/components/tree/tree.class.js +5 -2
- package/core/data-stream.js +21 -15
- package/core/element.js +36 -1
- package/core/element.test.js +67 -0
- package/core/icons-phosphor.js +86 -22
- package/core/icons.js +22 -8
- package/dist/theme-provider.min.js +2 -2
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +107 -102
- package/dist/web-components.sheet.js +1 -1
- package/package.json +1 -5
- package/styles/components.css +1 -0
- package/traits/error-shake/error-shake.js +23 -17
- package/traits/fade-presence/fade-presence.js +19 -15
- package/traits/success-checkmark/success-checkmark.js +21 -16
|
@@ -41,20 +41,11 @@
|
|
|
41
41
|
|
|
42
42
|
import { UIElement } from '../../core/element.js';
|
|
43
43
|
|
|
44
|
-
function escapeText(s) {
|
|
45
|
-
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function escapeAttr(s) {
|
|
49
|
-
return String(s).replace(/"/g, '"');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
44
|
export class UIAgentQuestions extends UIElement {
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
static requiredIcons = ['check'];
|
|
45
|
+
// gh issue 276 wave 4: options now render as <option-card-ui> (which
|
|
46
|
+
// composes icon-ui itself for the leading `icon` prop). This primitive
|
|
47
|
+
// no longer stamps any icon directly, so no requiredIcons entry is
|
|
48
|
+
// needed here — see check-required-icons.mjs (slot 11).
|
|
58
49
|
|
|
59
50
|
static properties = {
|
|
60
51
|
multi: { type: Boolean, default: false, reflect: true },
|
|
@@ -87,6 +78,9 @@ export class UIAgentQuestions extends UIElement {
|
|
|
87
78
|
|
|
88
79
|
disconnected() {
|
|
89
80
|
this.#submitEl?.removeEventListener('press', this.#onSubmit);
|
|
81
|
+
this.#optionsEl?.removeEventListener('change', this.#onCardChange);
|
|
82
|
+
this.#optionsEl?.removeEventListener('click', this.#onOptionsClickCapture, true);
|
|
83
|
+
this.#optionsEl?.removeEventListener('keydown', this.#onOptionsKeydownCapture, true);
|
|
90
84
|
this.#questionEl = this.#helperEl = this.#optionsEl = this.#submitEl = null;
|
|
91
85
|
}
|
|
92
86
|
|
|
@@ -121,6 +115,20 @@ export class UIAgentQuestions extends UIElement {
|
|
|
121
115
|
|
|
122
116
|
this.#optionsEl = document.createElement('div');
|
|
123
117
|
this.#optionsEl.setAttribute('data-questions-options', '');
|
|
118
|
+
// Single-select: option-card-ui's own radiogroup machinery handles
|
|
119
|
+
// click + keyboard natively and fires `change` when a card becomes
|
|
120
|
+
// selected — we just listen for it.
|
|
121
|
+
this.#optionsEl.addEventListener('change', this.#onCardChange);
|
|
122
|
+
// Multi-select: option-card-ui is radio-only (its #select() no-ops
|
|
123
|
+
// once checked, and Enter/Space call that private method directly
|
|
124
|
+
// rather than dispatching a click), so there is no toggle-off hook
|
|
125
|
+
// to attach to. Intercept at the container's CAPTURE phase — before
|
|
126
|
+
// the card's own bubble-phase listeners ever run — and drive
|
|
127
|
+
// checked/aria state ourselves. Single mode never reaches these
|
|
128
|
+
// (guarded by `if (!this.multi) return`), so its native behavior is
|
|
129
|
+
// untouched.
|
|
130
|
+
this.#optionsEl.addEventListener('click', this.#onOptionsClickCapture, true);
|
|
131
|
+
this.#optionsEl.addEventListener('keydown', this.#onOptionsKeydownCapture, true);
|
|
124
132
|
|
|
125
133
|
this.#submitEl = document.createElement('button-ui');
|
|
126
134
|
this.#submitEl.setAttribute('text', this.submitLabel || 'Continue');
|
|
@@ -139,33 +147,28 @@ export class UIAgentQuestions extends UIElement {
|
|
|
139
147
|
this.#optionsEl.innerHTML = '';
|
|
140
148
|
|
|
141
149
|
for (const opt of this.#options) {
|
|
142
|
-
const card = document.createElement('
|
|
143
|
-
card.type = 'button';
|
|
150
|
+
const card = document.createElement('option-card-ui');
|
|
144
151
|
card.setAttribute('data-questions-option', '');
|
|
145
|
-
card.setAttribute('
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
</span>`;
|
|
157
|
-
|
|
158
|
-
card.innerHTML = `
|
|
159
|
-
${iconHTML}
|
|
160
|
-
<span data-questions-option-body>
|
|
161
|
-
<span data-questions-option-label>${escapeText(opt.label || '')}</span>
|
|
162
|
-
${opt.description ? `<span data-questions-option-desc>${escapeText(opt.description)}</span>` : ''}
|
|
163
|
-
</span>
|
|
164
|
-
${checkHTML}
|
|
165
|
-
`;
|
|
166
|
-
|
|
167
|
-
card.addEventListener('click', () => this.#pick(opt));
|
|
152
|
+
card.setAttribute('data-questions-option-id', opt.id);
|
|
153
|
+
// Shared name → siblings form option-card-ui's own radiogroup
|
|
154
|
+
// (single mode). Inert in multi mode: #select() never runs there
|
|
155
|
+
// (see the capture-phase override below), so the shared name has
|
|
156
|
+
// no sibling-uncheck effect to guard against.
|
|
157
|
+
card.name = 'questions-option';
|
|
158
|
+
card.value = opt.id;
|
|
159
|
+
card.heading = opt.label || '';
|
|
160
|
+
card.description = opt.description || '';
|
|
161
|
+
card.icon = opt.icon || '';
|
|
162
|
+
|
|
168
163
|
this.#optionsEl.appendChild(card);
|
|
164
|
+
|
|
165
|
+
// Multi mode: override the radio role option-card-ui sets on
|
|
166
|
+
// connect — this is a checkbox group, and aria-checked (which the
|
|
167
|
+
// primitive keeps in sync with `.checked` regardless of role) is
|
|
168
|
+
// valid for either.
|
|
169
|
+
if (this.multi) card.setAttribute('role', 'checkbox');
|
|
170
|
+
card.checked = this.#selected.has(opt.id);
|
|
171
|
+
card.disabled = this.#answered;
|
|
169
172
|
}
|
|
170
173
|
|
|
171
174
|
if (this.#submitEl) {
|
|
@@ -173,22 +176,62 @@ export class UIAgentQuestions extends UIElement {
|
|
|
173
176
|
}
|
|
174
177
|
}
|
|
175
178
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
179
|
+
/** Single mode — option-card-ui's own radiogroup fired `change` because
|
|
180
|
+
* a card just became selected (it never fires on becoming unselected,
|
|
181
|
+
* so this is always a genuine new pick). */
|
|
182
|
+
#onCardChange = (e) => {
|
|
183
|
+
if (this.multi || this.#answered) return;
|
|
184
|
+
const id = e.detail?.value;
|
|
185
|
+
const opt = this.#options.find((o) => o.id === id);
|
|
186
|
+
if (!opt) return;
|
|
187
|
+
this.#selected.clear();
|
|
188
|
+
this.#selected.add(opt.id);
|
|
189
|
+
this.#answered = true;
|
|
190
|
+
this.#render();
|
|
191
|
+
this.dispatchEvent(new CustomEvent('questions-answer', {
|
|
192
|
+
bubbles: true,
|
|
193
|
+
detail: { selected: [opt.id], option: opt },
|
|
194
|
+
}));
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/** Multi mode — capture-phase click override (see #build). Stops the
|
|
198
|
+
* event before option-card-ui's own bubble-phase click handler runs,
|
|
199
|
+
* since that handler can only select, never deselect. */
|
|
200
|
+
#onOptionsClickCapture = (e) => {
|
|
201
|
+
if (!this.multi) return;
|
|
202
|
+
const card = e.target.closest?.('option-card-ui[data-questions-option]');
|
|
203
|
+
if (!card || !this.#optionsEl.contains(card)) return;
|
|
204
|
+
e.stopPropagation();
|
|
205
|
+
this.#toggleMulti(card);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/** Multi mode — capture-phase keydown override. Space/Enter toggle
|
|
209
|
+
* (mirroring click); arrow keys are swallowed rather than forwarded
|
|
210
|
+
* to option-card-ui's own forced-select navigation, since checkbox
|
|
211
|
+
* groups here never had arrow-key nav (Tab-only, as before). */
|
|
212
|
+
#onOptionsKeydownCapture = (e) => {
|
|
213
|
+
if (!this.multi) return;
|
|
214
|
+
const card = e.target.closest?.('option-card-ui[data-questions-option]');
|
|
215
|
+
if (!card || !this.#optionsEl.contains(card)) return;
|
|
216
|
+
if (e.key === ' ' || e.key === 'Enter') {
|
|
217
|
+
e.preventDefault();
|
|
218
|
+
e.stopPropagation();
|
|
219
|
+
this.#toggleMulti(card);
|
|
220
|
+
return;
|
|
191
221
|
}
|
|
222
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
|
|
223
|
+
e.stopPropagation();
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
#toggleMulti(card) {
|
|
228
|
+
if (this.#answered) return;
|
|
229
|
+
const id = card.dataset.questionsOptionId;
|
|
230
|
+
const opt = this.#options.find((o) => o.id === id);
|
|
231
|
+
if (!opt) return;
|
|
232
|
+
if (this.#selected.has(opt.id)) this.#selected.delete(opt.id);
|
|
233
|
+
else this.#selected.add(opt.id);
|
|
234
|
+
this.#render();
|
|
192
235
|
}
|
|
193
236
|
|
|
194
237
|
#onSubmit = () => {
|
|
@@ -18,8 +18,6 @@
|
|
|
18
18
|
--agent-questions-option-bg-sel: var(--md-sys-color-primary-container);
|
|
19
19
|
--agent-questions-option-border: var(--md-sys-color-neutral-outline-variant);
|
|
20
20
|
--agent-questions-option-border-sel: var(--a-primary-bg);
|
|
21
|
-
--agent-questions-check-bg: var(--a-primary-bg);
|
|
22
|
-
--agent-questions-check-fg: var(--md-sys-color-primary-on-primary);
|
|
23
21
|
|
|
24
22
|
/* ── Layout ── */
|
|
25
23
|
--agent-questions-gap: var(--a-space-2);
|
|
@@ -27,9 +25,6 @@
|
|
|
27
25
|
--agent-questions-option-padding: var(--a-space-3);
|
|
28
26
|
--agent-questions-option-radius: var(--a-radius);
|
|
29
27
|
--agent-questions-q-padding: var(--a-space-1);
|
|
30
|
-
--agent-questions-option-body-gap: var(--a-space-0-5);
|
|
31
|
-
/* Component-intrinsic measurement; no --a-space-* equivalent */
|
|
32
|
-
--agent-questions-check-size: 20px;
|
|
33
28
|
|
|
34
29
|
/* ── Motion ── */
|
|
35
30
|
--agent-questions-duration: var(--a-duration-fast);
|
|
@@ -61,108 +56,34 @@
|
|
|
61
56
|
gap: var(--agent-questions-gap);
|
|
62
57
|
}
|
|
63
58
|
|
|
59
|
+
/* gh issue 276 wave 4: options render as <option-card-ui> now — it owns
|
|
60
|
+
its own box model, borders, hover/checked/disabled states, focus ring,
|
|
61
|
+
and heading/description/icon layout via its own @scope'd CSS. This
|
|
62
|
+
re-points its token surface to agent-questions' existing tokens so the
|
|
63
|
+
migration doesn't shift the look; it does not restyle the card itself.
|
|
64
|
+
One deliberate visual difference: the selected indicator is now
|
|
65
|
+
option-card-ui's authentic filled-radio-dot recipe (shared with
|
|
66
|
+
plan-picker-ui) rather than the bespoke check-glyph badge the native
|
|
67
|
+
buttons drew — that badge doesn't exist as an option-card-ui slot. */
|
|
64
68
|
[data-questions-option] {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
border:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
font-family: var(--agent-questions-font-family, var(--a-font-family-ui));
|
|
84
|
-
transition:
|
|
85
|
-
background var(--agent-questions-duration) var(--agent-questions-easing),
|
|
86
|
-
border-color var(--agent-questions-duration) var(--agent-questions-easing);
|
|
87
|
-
min-width: 0;
|
|
69
|
+
--option-card-padding-block: var(--agent-questions-option-padding);
|
|
70
|
+
--option-card-padding-inline: var(--agent-questions-option-padding);
|
|
71
|
+
--option-card-radius: var(--agent-questions-option-radius);
|
|
72
|
+
--option-card-gap-x: var(--agent-questions-option-gap);
|
|
73
|
+
--option-card-bg: var(--agent-questions-option-bg);
|
|
74
|
+
--option-card-border: var(--agent-questions-option-border);
|
|
75
|
+
--option-card-bg-hover: var(--agent-questions-option-bg-hover);
|
|
76
|
+
/* Match prior behavior: hover changed background only, never border. */
|
|
77
|
+
--option-card-border-hover: var(--agent-questions-option-border);
|
|
78
|
+
--option-card-bg-checked: var(--agent-questions-option-bg-sel);
|
|
79
|
+
--option-card-border-checked: var(--agent-questions-option-border-sel);
|
|
80
|
+
--option-card-heading-color: var(--agent-questions-label-fg);
|
|
81
|
+
--option-card-heading-weight: var(--agent-questions-label-weight);
|
|
82
|
+
--option-card-heading-size: var(--agent-questions-label-size);
|
|
83
|
+
--option-card-desc-color: var(--agent-questions-desc-fg);
|
|
84
|
+
--option-card-desc-size: var(--agent-questions-desc-size);
|
|
85
|
+
--option-card-duration: var(--agent-questions-duration);
|
|
86
|
+
--option-card-easing: var(--agent-questions-easing);
|
|
88
87
|
width: 100%;
|
|
89
88
|
}
|
|
90
|
-
|
|
91
|
-
[data-questions-option]:hover {
|
|
92
|
-
background: var(--agent-questions-option-bg-hover);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
[data-questions-option][data-selected] {
|
|
96
|
-
background: var(--agent-questions-option-bg-sel);
|
|
97
|
-
border-color: var(--agent-questions-option-border-sel);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
[data-questions-option][disabled] {
|
|
101
|
-
cursor: default;
|
|
102
|
-
opacity: 0.7;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
[data-questions-option]:focus-visible {
|
|
106
|
-
outline: 2px solid var(--agent-questions-option-border-sel);
|
|
107
|
-
outline-offset: 2px;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
[data-questions-option-icon] {
|
|
111
|
-
flex-shrink: 0;
|
|
112
|
-
/* Align the leading icon's center with the label's first-line
|
|
113
|
-
center. `1.5em` matches the label's effective line-height
|
|
114
|
-
(label is `font-size:` only — line-height inherits as 1.5).
|
|
115
|
-
`var(--a-icon-size)` is icon-ui's rendered height. Centering
|
|
116
|
-
the icon inside one line-box of the label's height puts its
|
|
117
|
-
midpoint on the label's first-line midpoint. We use em on the
|
|
118
|
-
icon's own font-size (which icon-ui keeps at the option's
|
|
119
|
-
font-size) so the offset scales with whatever the consumer
|
|
120
|
-
sets `--agent-questions-label-size` to. */
|
|
121
|
-
margin-top: calc((1.5em - var(--a-icon-size, 1em)) / 2);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
[data-questions-option-body] {
|
|
125
|
-
flex: 1;
|
|
126
|
-
min-width: 0;
|
|
127
|
-
display: flex;
|
|
128
|
-
flex-direction: column;
|
|
129
|
-
gap: var(--agent-questions-option-body-gap);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
[data-questions-option-label] {
|
|
133
|
-
font-size: var(--agent-questions-label-size);
|
|
134
|
-
font-weight: var(--agent-questions-label-weight);
|
|
135
|
-
color: var(--agent-questions-label-fg);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
[data-questions-option-desc] {
|
|
139
|
-
font-size: var(--agent-questions-desc-size);
|
|
140
|
-
color: var(--agent-questions-desc-fg);
|
|
141
|
-
line-height: 1.4;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
[data-questions-option-check] {
|
|
145
|
-
width: var(--agent-questions-check-size);
|
|
146
|
-
height: var(--agent-questions-check-size);
|
|
147
|
-
border-radius: 50%;
|
|
148
|
-
flex-shrink: 0;
|
|
149
|
-
display: flex;
|
|
150
|
-
align-items: center;
|
|
151
|
-
justify-content: center;
|
|
152
|
-
background: transparent;
|
|
153
|
-
opacity: 0;
|
|
154
|
-
transition: opacity var(--agent-questions-duration) var(--agent-questions-easing);
|
|
155
|
-
/* Match the leading-icon offset so a row of [icon · label · check]
|
|
156
|
-
reads as one horizontal line aligned to the label's first line. */
|
|
157
|
-
margin-top: calc((1.5em - var(--agent-questions-check-size)) / 2);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
[data-questions-option][data-selected] [data-questions-option-check] {
|
|
161
|
-
background: var(--agent-questions-check-bg);
|
|
162
|
-
opacity: 1;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
[data-questions-option][data-selected] [data-questions-option-check] icon-ui {
|
|
166
|
-
color: var(--agent-questions-check-fg);
|
|
167
|
-
}
|
|
168
89
|
}
|
|
@@ -50,7 +50,11 @@ export class UIButton extends UIElement {
|
|
|
50
50
|
static template = () => null;
|
|
51
51
|
|
|
52
52
|
connected() {
|
|
53
|
-
|
|
53
|
+
// Respect consumer-authored ARIA — a composite may stamp button-ui as
|
|
54
|
+
// role="tab" (swiper dots), role="gridcell"-hosted day buttons
|
|
55
|
+
// (calendar), etc. Same contract as tabindex below: default only when
|
|
56
|
+
// the author said nothing (operator ruling 2026-07-17, gh#276 wave 4).
|
|
57
|
+
if (!this.hasAttribute('role')) this.setAttribute('role', 'button');
|
|
54
58
|
// Respect a consumer-authored tabindex (e.g. a mouse-only tabindex="-1"
|
|
55
59
|
// affordance like combobox's clear x, or a roving-tabindex composite) —
|
|
56
60
|
// only default the focusable state when the author said nothing.
|
|
@@ -164,10 +164,13 @@ export class UICalendarGrid extends UIElement {
|
|
|
164
164
|
const rangeTo = (rs && re && rs <= re) ? re : null;
|
|
165
165
|
|
|
166
166
|
// Header — Prev / Title / Next
|
|
167
|
+
// <button-ui variant="ghost" icon="…"> (gh issue 276 wave 4) — button-ui
|
|
168
|
+
// respects the consumer-authored tabindex="-1", so the nav carets stay
|
|
169
|
+
// out of the roving-tabindex day-grid's tab sequence exactly as before.
|
|
167
170
|
let h = `<div data-cal-header>
|
|
168
|
-
<button data-cal-prev
|
|
171
|
+
<button-ui variant="ghost" size="xs" icon="caret-left" data-cal-prev aria-label="Previous month" tabindex="-1"></button-ui>
|
|
169
172
|
<span data-cal-title>${MONTHS_LONG[month]} ${year}</span>
|
|
170
|
-
<button data-cal-next
|
|
173
|
+
<button-ui variant="ghost" size="xs" icon="caret-right" data-cal-next aria-label="Next month" tabindex="-1"></button-ui>
|
|
171
174
|
</div>`;
|
|
172
175
|
|
|
173
176
|
// Day-of-week header
|
|
@@ -183,7 +186,7 @@ export class UICalendarGrid extends UIElement {
|
|
|
183
186
|
// Previous-month trailing days (outside, disabled)
|
|
184
187
|
for (let i = firstDay - 1; i >= 0; i--) {
|
|
185
188
|
const day = daysInPrev - i;
|
|
186
|
-
h += `<button
|
|
189
|
+
h += `<button-ui variant="ghost" data-cal-day data-outside disabled tabindex="-1" text="${day}"></button-ui>`;
|
|
187
190
|
}
|
|
188
191
|
|
|
189
192
|
// Current-month days
|
|
@@ -206,7 +209,6 @@ export class UICalendarGrid extends UIElement {
|
|
|
206
209
|
const isRangeEnd = !!(rangeTo && sameDay(date, rangeTo));
|
|
207
210
|
|
|
208
211
|
const attrs = [
|
|
209
|
-
'type="button"',
|
|
210
212
|
'data-cal-day',
|
|
211
213
|
`data-date="${iso}"`,
|
|
212
214
|
isToday ? 'data-today' : '',
|
|
@@ -219,14 +221,17 @@ export class UICalendarGrid extends UIElement {
|
|
|
219
221
|
`tabindex="${isFocused ? '0' : '-1'}"`,
|
|
220
222
|
].filter(Boolean).join(' ');
|
|
221
223
|
|
|
222
|
-
|
|
224
|
+
// <button-ui variant="ghost" text="…"> (gh issue 276 wave 4) — the day
|
|
225
|
+
// number moves from element text content to the `text` attribute,
|
|
226
|
+
// which button-ui renders via CSS attr(text) on its ::after.
|
|
227
|
+
h += `<button-ui variant="ghost" ${attrs} text="${d}"></button-ui>`;
|
|
223
228
|
}
|
|
224
229
|
|
|
225
230
|
// Next-month leading days
|
|
226
231
|
const totalCells = firstDay + daysInMonth;
|
|
227
232
|
const remaining = (7 - (totalCells % 7)) % 7;
|
|
228
233
|
for (let d = 1; d <= remaining; d++) {
|
|
229
|
-
h += `<button
|
|
234
|
+
h += `<button-ui variant="ghost" data-cal-day data-outside disabled tabindex="-1" text="${d}"></button-ui>`;
|
|
230
235
|
}
|
|
231
236
|
|
|
232
237
|
h += '</div>';
|
|
@@ -115,14 +115,23 @@
|
|
|
115
115
|
color: var(--calendar-grid-title-fg);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
/* Stamped <button-ui variant="ghost" size="xs" icon="caret-…"> (gh issue
|
|
119
|
+
276 wave 4) — icon-only (no [text], no child text) so button-ui's own
|
|
120
|
+
[data-icon-only] rule already squares it to --button-height; pointing
|
|
121
|
+
--button-height at the nav size (not just literal width/height) avoids
|
|
122
|
+
the min-width/min-height floor trap. Literal background/color below
|
|
123
|
+
out-specify button.css's own (via the `button-ui` type-selector bump —
|
|
124
|
+
same technique as swiper's dots), so the button-ui type-selector prefix
|
|
125
|
+
is load-bearing, not decorative. */
|
|
126
|
+
button-ui[data-cal-prev],
|
|
127
|
+
button-ui[data-cal-next] {
|
|
128
|
+
--button-height: var(--calendar-grid-nav-size);
|
|
129
|
+
--a-icon-size: var(--calendar-grid-nav-icon-size);
|
|
130
|
+
|
|
120
131
|
display: inline-flex;
|
|
121
132
|
align-items: center;
|
|
122
133
|
justify-content: center;
|
|
123
|
-
width: var(--calendar-grid-nav-size);
|
|
124
134
|
height: var(--calendar-grid-nav-size);
|
|
125
|
-
padding: 0;
|
|
126
135
|
border: none;
|
|
127
136
|
border-radius: var(--calendar-grid-nav-radius);
|
|
128
137
|
background: var(--calendar-grid-nav-bg);
|
|
@@ -131,10 +140,9 @@
|
|
|
131
140
|
transition:
|
|
132
141
|
background var(--calendar-grid-duration) var(--calendar-grid-easing),
|
|
133
142
|
color var(--calendar-grid-duration) var(--calendar-grid-easing);
|
|
134
|
-
--a-icon-size: var(--calendar-grid-nav-icon-size);
|
|
135
143
|
}
|
|
136
|
-
[data-cal-prev]:hover,
|
|
137
|
-
[data-cal-next]:hover {
|
|
144
|
+
button-ui[data-cal-prev]:hover,
|
|
145
|
+
button-ui[data-cal-next]:hover {
|
|
138
146
|
background: var(--calendar-grid-nav-bg-hover);
|
|
139
147
|
color: var(--calendar-grid-nav-fg-hover);
|
|
140
148
|
}
|
|
@@ -164,7 +172,18 @@
|
|
|
164
172
|
gap: var(--calendar-grid-day-grid-gap);
|
|
165
173
|
}
|
|
166
174
|
|
|
167
|
-
|
|
175
|
+
/* Stamped <button-ui variant="ghost" text="…"> (gh issue 276 wave 4).
|
|
176
|
+
button-ui floors min-width/min-height at --button-height — point it at
|
|
177
|
+
the day-cell size (not just the literal width/height below) so the
|
|
178
|
+
floor never clamps a smaller [size] tier. The literal geometry/color
|
|
179
|
+
declarations below out-specify button-ui's own equivalents via the
|
|
180
|
+
`button-ui` type-selector bump (same technique as swiper's dots).
|
|
181
|
+
button-ui renders its label via `::after { content: attr(text) }` — the
|
|
182
|
+
today-indicator dot below therefore uses `::before` (kept free), not
|
|
183
|
+
`::after`, to avoid the two fighting over the same pseudo-element. */
|
|
184
|
+
button-ui[data-cal-day] {
|
|
185
|
+
--button-height: var(--calendar-grid-day-size);
|
|
186
|
+
|
|
168
187
|
display: flex;
|
|
169
188
|
align-items: center;
|
|
170
189
|
justify-content: center;
|
|
@@ -188,25 +207,25 @@
|
|
|
188
207
|
}
|
|
189
208
|
|
|
190
209
|
/* ── State: hover ── */
|
|
191
|
-
[data-cal-day]:not([disabled]):not([data-outside]):hover {
|
|
210
|
+
button-ui[data-cal-day]:not([disabled]):not([data-outside]):hover {
|
|
192
211
|
background: var(--calendar-grid-day-bg-hover);
|
|
193
212
|
color: var(--calendar-grid-day-fg-hover);
|
|
194
213
|
}
|
|
195
214
|
|
|
196
215
|
/* ── State: focus ── */
|
|
197
|
-
[data-cal-day]:not([disabled]):not([data-outside]):focus-visible,
|
|
198
|
-
[data-cal-day][data-focused] {
|
|
216
|
+
button-ui[data-cal-day]:not([disabled]):not([data-outside]):focus-visible,
|
|
217
|
+
button-ui[data-cal-day][data-focused] {
|
|
199
218
|
outline: none;
|
|
200
219
|
box-shadow: var(--calendar-grid-day-focus-ring);
|
|
201
220
|
}
|
|
202
221
|
|
|
203
222
|
/* ── State: selected ── */
|
|
204
|
-
[data-cal-day][data-selected] {
|
|
223
|
+
button-ui[data-cal-day][data-selected] {
|
|
205
224
|
background: var(--calendar-grid-day-bg-selected);
|
|
206
225
|
color: var(--calendar-grid-day-fg-selected);
|
|
207
226
|
font-weight: var(--calendar-grid-selected-weight);
|
|
208
227
|
}
|
|
209
|
-
[data-cal-day][data-selected]:not([disabled]):hover {
|
|
228
|
+
button-ui[data-cal-day][data-selected]:not([disabled]):hover {
|
|
210
229
|
background: var(--calendar-grid-day-bg-selected);
|
|
211
230
|
color: var(--calendar-grid-day-fg-selected);
|
|
212
231
|
}
|
|
@@ -217,21 +236,21 @@
|
|
|
217
236
|
fill); the in-range middle is visually subordinate. `:not([data-selected])`
|
|
218
237
|
guard prevents the muted fill from overriding the endpoint cells in
|
|
219
238
|
consumer code that sets both `value` AND `range-start`/`range-end`. */
|
|
220
|
-
[data-cal-day][data-in-range]:not([data-selected]) {
|
|
239
|
+
button-ui[data-cal-day][data-in-range]:not([data-selected]) {
|
|
221
240
|
background: var(--calendar-grid-day-bg-in-range);
|
|
222
241
|
color: var(--calendar-grid-day-fg-in-range);
|
|
223
242
|
}
|
|
224
|
-
[data-cal-day][data-in-range]:not([data-selected]):not([disabled]):hover {
|
|
243
|
+
button-ui[data-cal-day][data-in-range]:not([data-selected]):not([disabled]):hover {
|
|
225
244
|
background: var(--calendar-grid-day-bg-in-range);
|
|
226
245
|
}
|
|
227
246
|
|
|
228
247
|
/* ── State: today (unselected) ── */
|
|
229
|
-
[data-cal-day][data-today]:not([data-selected]) {
|
|
248
|
+
button-ui[data-cal-day][data-today]:not([data-selected]) {
|
|
230
249
|
position: relative;
|
|
231
250
|
color: var(--calendar-grid-day-today-color);
|
|
232
251
|
font-weight: var(--calendar-grid-today-weight);
|
|
233
252
|
}
|
|
234
|
-
[data-cal-day][data-today]:not([data-selected])::
|
|
253
|
+
button-ui[data-cal-day][data-today]:not([data-selected])::before {
|
|
235
254
|
content: "";
|
|
236
255
|
position: absolute;
|
|
237
256
|
left: 50%;
|
|
@@ -245,13 +264,16 @@
|
|
|
245
264
|
}
|
|
246
265
|
|
|
247
266
|
/* ── State: outside month ── */
|
|
248
|
-
[data-cal-day][data-outside] {
|
|
267
|
+
button-ui[data-cal-day][data-outside] {
|
|
249
268
|
color: var(--calendar-grid-day-fg-outside);
|
|
250
269
|
cursor: default;
|
|
251
270
|
}
|
|
252
271
|
|
|
253
|
-
/* ── State: disabled (excluding outside) ──
|
|
254
|
-
|
|
272
|
+
/* ── State: disabled (excluding outside) ──
|
|
273
|
+
`[disabled]` — NOT the `:disabled` pseudo-class, which only matches
|
|
274
|
+
native form controls; button-ui is a custom element, so `:disabled`
|
|
275
|
+
silently never matches it. */
|
|
276
|
+
button-ui[data-cal-day][disabled]:not([data-outside]) {
|
|
255
277
|
color: var(--calendar-grid-day-fg-disabled);
|
|
256
278
|
cursor: not-allowed;
|
|
257
279
|
}
|
|
@@ -16,6 +16,11 @@ description: >-
|
|
|
16
16
|
(two side-by-side grids), and datetime-picker-ui (one grid + time pane).
|
|
17
17
|
For a complete date-picker affordance (trigger + popover + form
|
|
18
18
|
participation), use calendar-picker-ui.
|
|
19
|
+
# Per ADR-0027 — primitives that programmatically create other primitives
|
|
20
|
+
# in their render() do NOT auto-import them. Consumer (or demo shell) must
|
|
21
|
+
# explicitly import each composed primitive.
|
|
22
|
+
composes:
|
|
23
|
+
- button-ui # day cells + month nav (gh issue 276 wave 4)
|
|
19
24
|
props:
|
|
20
25
|
value:
|
|
21
26
|
description: Selected date in ISO format (YYYY-MM-DD).
|
|
@@ -191,10 +191,14 @@ export class UICalendarPicker extends UIFormElement {
|
|
|
191
191
|
const maxDate = parseISO(this.max);
|
|
192
192
|
|
|
193
193
|
// Header
|
|
194
|
+
// <button-ui variant="ghost" icon="…"> (gh issue 276 wave 4) — no
|
|
195
|
+
// tabindex here, matching the native version: these nav buttons stay in
|
|
196
|
+
// the default tab order (unlike calendar-grid-ui's, which are pulled out
|
|
197
|
+
// via tabindex="-1" for its inline roving-tabindex day grid).
|
|
194
198
|
let h = `<div data-cal-header>
|
|
195
|
-
<button data-cal-prev
|
|
199
|
+
<button-ui variant="ghost" size="xs" icon="caret-left" data-cal-prev aria-label="Previous month"></button-ui>
|
|
196
200
|
<span data-cal-title>${MONTHS_LONG[month]} ${year}</span>
|
|
197
|
-
<button data-cal-next
|
|
201
|
+
<button-ui variant="ghost" size="xs" icon="caret-right" data-cal-next aria-label="Next month"></button-ui>
|
|
198
202
|
</div>`;
|
|
199
203
|
|
|
200
204
|
// Day-of-week header
|
|
@@ -210,7 +214,7 @@ export class UICalendarPicker extends UIFormElement {
|
|
|
210
214
|
// Previous month trailing days
|
|
211
215
|
for (let i = firstDay - 1; i >= 0; i--) {
|
|
212
216
|
const day = daysInPrev - i;
|
|
213
|
-
h += `<button
|
|
217
|
+
h += `<button-ui variant="ghost" data-cal-day data-outside disabled tabindex="-1" text="${day}"></button-ui>`;
|
|
214
218
|
}
|
|
215
219
|
|
|
216
220
|
// Current month days
|
|
@@ -223,7 +227,6 @@ export class UICalendarPicker extends UIFormElement {
|
|
|
223
227
|
const isFocused = this.#focusedDay === d;
|
|
224
228
|
|
|
225
229
|
const attrs = [
|
|
226
|
-
'type="button"',
|
|
227
230
|
'data-cal-day',
|
|
228
231
|
`data-date="${iso}"`,
|
|
229
232
|
isToday ? 'data-today' : '',
|
|
@@ -233,14 +236,16 @@ export class UICalendarPicker extends UIFormElement {
|
|
|
233
236
|
`tabindex="${isFocused ? '0' : '-1'}"`,
|
|
234
237
|
].filter(Boolean).join(' ');
|
|
235
238
|
|
|
236
|
-
|
|
239
|
+
// <button-ui variant="ghost" text="…"> (gh issue 276 wave 4) — the day
|
|
240
|
+
// number moves from element text content to the `text` attribute.
|
|
241
|
+
h += `<button-ui variant="ghost" ${attrs} text="${d}"></button-ui>`;
|
|
237
242
|
}
|
|
238
243
|
|
|
239
244
|
// Next month leading days
|
|
240
245
|
const totalCells = firstDay + daysInMonth;
|
|
241
246
|
const remaining = (7 - (totalCells % 7)) % 7;
|
|
242
247
|
for (let d = 1; d <= remaining; d++) {
|
|
243
|
-
h += `<button
|
|
248
|
+
h += `<button-ui variant="ghost" data-cal-day data-outside disabled tabindex="-1" text="${d}"></button-ui>`;
|
|
244
249
|
}
|
|
245
250
|
|
|
246
251
|
h += '</div>';
|