@adia-ai/web-components 0.6.36 → 0.6.37
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 +28 -1
- package/components/badge/badge.a2ui.json +10 -0
- package/components/badge/badge.css +70 -0
- package/components/badge/badge.yaml +20 -0
- package/components/blockquote/blockquote.a2ui.json +121 -0
- package/components/blockquote/blockquote.class.js +68 -0
- package/components/blockquote/blockquote.css +46 -0
- package/components/blockquote/blockquote.d.ts +31 -0
- package/components/blockquote/blockquote.js +17 -0
- package/components/blockquote/blockquote.yaml +124 -0
- package/components/button/button.css +11 -3
- package/components/calendar-picker/calendar-picker.a2ui.json +15 -0
- package/components/calendar-picker/calendar-picker.class.js +7 -1
- package/components/calendar-picker/calendar-picker.yaml +14 -0
- package/components/color-input/color-input.a2ui.json +2 -2
- package/components/color-input/color-input.class.js +9 -2
- package/components/color-input/color-input.yaml +2 -2
- package/components/combobox/combobox.class.js +4 -0
- package/components/context-menu/context-menu.a2ui.json +159 -0
- package/components/context-menu/context-menu.class.js +275 -0
- package/components/context-menu/context-menu.css +56 -0
- package/components/context-menu/context-menu.d.ts +70 -0
- package/components/context-menu/context-menu.js +17 -0
- package/components/context-menu/context-menu.yaml +136 -0
- package/components/date-range-picker/date-range-picker.a2ui.json +15 -0
- package/components/date-range-picker/date-range-picker.class.js +2 -0
- package/components/date-range-picker/date-range-picker.yaml +14 -0
- package/components/datetime-picker/datetime-picker.a2ui.json +15 -0
- package/components/datetime-picker/datetime-picker.class.js +3 -1
- package/components/datetime-picker/datetime-picker.d.ts +2 -0
- package/components/datetime-picker/datetime-picker.yaml +14 -0
- package/components/empty-state/empty-state.class.js +2 -0
- package/components/feed/feed.class.js +13 -5
- package/components/feed/feed.css +14 -0
- package/components/index.js +9 -0
- package/components/integration-card/integration-card.class.js +9 -0
- package/components/integration-card/integration-card.test.js +4 -3
- package/components/nav-group/nav-group.css +7 -1
- package/components/number-format/number-format.a2ui.json +180 -0
- package/components/number-format/number-format.class.js +96 -0
- package/components/number-format/number-format.css +18 -0
- package/components/number-format/number-format.d.ts +68 -0
- package/components/number-format/number-format.js +17 -0
- package/components/number-format/number-format.yaml +204 -0
- package/components/pagination/pagination.a2ui.json +19 -2
- package/components/pagination/pagination.class.js +90 -37
- package/components/pagination/pagination.css +32 -127
- package/components/pagination/pagination.d.ts +8 -2
- package/components/pagination/pagination.test.js +195 -0
- package/components/pagination/pagination.yaml +22 -1
- package/components/password-strength/password-strength.a2ui.json +152 -0
- package/components/password-strength/password-strength.class.js +157 -0
- package/components/password-strength/password-strength.css +80 -0
- package/components/password-strength/password-strength.d.ts +59 -0
- package/components/password-strength/password-strength.js +17 -0
- package/components/password-strength/password-strength.yaml +153 -0
- package/components/popover/popover.css +43 -23
- package/components/popover/popover.yaml +8 -4
- package/components/qr-code/QR-TEST.svg +4 -0
- package/components/qr-code/qr-code.a2ui.json +154 -0
- package/components/qr-code/qr-code.class.js +129 -0
- package/components/qr-code/qr-code.css +41 -0
- package/components/qr-code/qr-code.d.ts +83 -0
- package/components/qr-code/qr-code.js +17 -0
- package/components/qr-code/qr-code.yaml +203 -0
- package/components/qr-code/qr-encoder.js +633 -0
- package/components/relative-time/relative-time.a2ui.json +120 -0
- package/components/relative-time/relative-time.class.js +136 -0
- package/components/relative-time/relative-time.css +22 -0
- package/components/relative-time/relative-time.d.ts +51 -0
- package/components/relative-time/relative-time.js +17 -0
- package/components/relative-time/relative-time.yaml +133 -0
- package/components/segmented/segmented.class.js +5 -1
- package/components/select/select.class.js +4 -0
- package/components/skip-nav/skip-nav.a2ui.json +92 -0
- package/components/skip-nav/skip-nav.class.js +45 -0
- package/components/skip-nav/skip-nav.css +54 -0
- package/components/skip-nav/skip-nav.d.ts +27 -0
- package/components/skip-nav/skip-nav.js +12 -0
- package/components/skip-nav/skip-nav.yaml +68 -0
- package/components/slider/slider.a2ui.json +16 -1
- package/components/slider/slider.class.js +264 -122
- package/components/slider/slider.css +82 -2
- package/components/slider/slider.d.ts +19 -3
- package/components/slider/slider.test.js +55 -0
- package/components/slider/slider.yaml +28 -6
- package/components/table/table.class.js +29 -6
- package/components/table/table.css +31 -4
- package/components/table-toolbar/table-toolbar.class.js +3 -1
- package/components/tag/tag.a2ui.json +3 -2
- package/components/tag/tag.css +35 -11
- package/components/tag/tag.d.ts +14 -0
- package/components/tag/tag.test.js +35 -11
- package/components/tag/tag.yaml +13 -7
- package/components/toast/toast.class.js +12 -4
- package/components/toc/toc.a2ui.json +159 -0
- package/components/toc/toc.class.js +222 -0
- package/components/toc/toc.css +92 -0
- package/components/toc/toc.d.ts +61 -0
- package/components/toc/toc.js +17 -0
- package/components/toc/toc.yaml +180 -0
- package/components/toolbar/toolbar.class.js +3 -0
- package/components/visually-hidden/visually-hidden.a2ui.json +71 -0
- package/components/visually-hidden/visually-hidden.class.js +14 -0
- package/components/visually-hidden/visually-hidden.css +25 -0
- package/components/visually-hidden/visually-hidden.d.ts +26 -0
- package/components/visually-hidden/visually-hidden.js +12 -0
- package/components/visually-hidden/visually-hidden.yaml +54 -0
- package/core/anchor.js +19 -3
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +100 -89
- package/package.json +1 -1
- package/styles/colors/semantics.css +11 -2
- package/styles/components.css +9 -0
- package/styles/resets.css +10 -0
|
@@ -14,10 +14,14 @@
|
|
|
14
14
|
/**
|
|
15
15
|
* <slider-ui label="Width" value="63" min="0" max="200" step="1" suffix="rem"></slider-ui>
|
|
16
16
|
*
|
|
17
|
-
*
|
|
17
|
+
* Single-thumb layout:
|
|
18
18
|
* [label] [value] [suffix]
|
|
19
19
|
* [=====fill=====●─────────────────track───────────────────]
|
|
20
20
|
*
|
|
21
|
+
* Dual-thumb layout (set [dual] + [lower-value] / [upper-value]):
|
|
22
|
+
* [label] [lower–upper] [suffix]
|
|
23
|
+
* [───────●═════════════════●──────────────────────────────]
|
|
24
|
+
*
|
|
21
25
|
* The `label` attribute renders as a first-class in-component caption in
|
|
22
26
|
* the slider header and is mirrored to `aria-label` on the host for
|
|
23
27
|
* screen-reader announcement. Wrap in `<field-ui>` only when you need
|
|
@@ -35,24 +39,20 @@ export class UISlider extends UIFormElement {
|
|
|
35
39
|
|
|
36
40
|
static properties = {
|
|
37
41
|
...UIFormElement.properties,
|
|
38
|
-
/** value: Number —
|
|
39
|
-
value:
|
|
42
|
+
/** value: Number — single-thumb mode; ignored when [dual]. */
|
|
43
|
+
value: { type: Number, default: 50, reflect: true },
|
|
44
|
+
/** dual: when true, two-thumb mode; lowerValue / upperValue authoritative. */
|
|
45
|
+
dual: { type: Boolean, default: false, reflect: true },
|
|
46
|
+
/** lowerValue: dual mode; clamped to ≤ upperValue. */
|
|
47
|
+
lowerValue: { type: Number, default: 0, reflect: true, attribute: 'lower-value' },
|
|
48
|
+
/** upperValue: dual mode; clamped to ≥ lowerValue. */
|
|
49
|
+
upperValue: { type: Number, default: 100, reflect: true, attribute: 'upper-value' },
|
|
40
50
|
min: { type: Number, default: 0, reflect: true },
|
|
41
51
|
max: { type: Number, default: 100, reflect: true },
|
|
42
52
|
step: { type: Number, default: 1, reflect: true },
|
|
43
53
|
label: { type: String, default: '', reflect: true },
|
|
44
54
|
suffix: { type: String, default: '', reflect: true },
|
|
45
|
-
// §184
|
|
46
|
-
// declarative trailing-debounce on `input` for expensive consumers
|
|
47
|
-
// (palette regen, shader compile, large list reflow). When > 0,
|
|
48
|
-
// value updates + visual feedback are immediate but `input` is
|
|
49
|
-
// collapsed across the window. `change` fires unthrottled on
|
|
50
|
-
// pointerup / track click / keyboard; any pending `input` flushes
|
|
51
|
-
// BEFORE `change` so consumers always see
|
|
52
|
-
// input→input→…→input→change ordering. throttle="0" (default)
|
|
53
|
-
// preserves the pre-§184 every-pointer-move-fires-input behavior.
|
|
54
|
-
// The mechanism graduated to UIFormElement at v0.5.9 §220 — slider
|
|
55
|
-
// delegates via scheduleThrottledInput() + flushPendingInput().
|
|
55
|
+
// §184/§220 throttle inherited from UIFormElement.
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
static template = () => null;
|
|
@@ -62,12 +62,19 @@ export class UISlider extends UIFormElement {
|
|
|
62
62
|
|
|
63
63
|
#trackEl = null;
|
|
64
64
|
#thumbEl = null;
|
|
65
|
+
#thumbLowerEl = null;
|
|
66
|
+
#thumbUpperEl = null;
|
|
65
67
|
#dragging = false;
|
|
68
|
+
#draggingThumb = null; // 'lower' | 'upper' | null (dual only)
|
|
66
69
|
#dragOffset = 0;
|
|
70
|
+
// Previous-render values used by the dual-mode constraint to decide
|
|
71
|
+
// which thumb to clamp when lower > upper. NaN sentinel = first render.
|
|
72
|
+
#prevLowerValue = NaN;
|
|
73
|
+
#prevUpperValue = NaN;
|
|
67
74
|
|
|
68
|
-
|
|
75
|
+
#pctOf(v) {
|
|
69
76
|
const range = this.max - this.min;
|
|
70
|
-
return range > 0 ? (
|
|
77
|
+
return range > 0 ? (v - this.min) / range : 0;
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
#format(v) {
|
|
@@ -77,27 +84,42 @@ export class UISlider extends UIFormElement {
|
|
|
77
84
|
|
|
78
85
|
connected() {
|
|
79
86
|
super.connected();
|
|
80
|
-
this.
|
|
87
|
+
const isDual = this.dual;
|
|
88
|
+
this.setAttribute('role', isDual ? 'group' : 'slider');
|
|
81
89
|
this.setAttribute('tabindex', '0');
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
if (!isDual) {
|
|
91
|
+
this.setAttribute('aria-valuemin', this.min);
|
|
92
|
+
this.setAttribute('aria-valuemax', this.max);
|
|
93
|
+
} else {
|
|
94
|
+
// Group host should not carry valuemin/max — the per-thumb sliders do.
|
|
95
|
+
this.removeAttribute('aria-valuemin');
|
|
96
|
+
this.removeAttribute('aria-valuemax');
|
|
97
|
+
}
|
|
84
98
|
if (this.label) this.setAttribute('aria-label', this.label);
|
|
85
99
|
|
|
86
100
|
if (!this.querySelector('[slot="track"]')) {
|
|
87
101
|
// §184 (v0.5.5, FEEDBACK-08 §7): hint slot stamped underneath
|
|
88
102
|
// the track when [hint] is set. Wired to aria-describedby below.
|
|
89
103
|
const hintId = this.hint ? `slider-hint-${++UISlider.#hintSeq}` : '';
|
|
104
|
+
const lowerLabel = this.label ? `${this.label} lower bound` : 'Lower bound';
|
|
105
|
+
const upperLabel = this.label ? `${this.label} upper bound` : 'Upper bound';
|
|
106
|
+
const readout = isDual
|
|
107
|
+
? `<span slot="value-lower">${this.#format(this.lowerValue)}</span><span slot="value-sep" aria-hidden="true">–</span><span slot="value-upper">${this.#format(this.upperValue)}</span>`
|
|
108
|
+
: `<span slot="value">${this.#format(this.value)}</span>`;
|
|
109
|
+
const thumbs = isDual
|
|
110
|
+
? `<div slot="thumb" data-thumb="lower" tabindex="0" role="slider" aria-label="${lowerLabel}" aria-valuemin="${this.min}" aria-valuemax="${this.max}"></div><div slot="thumb" data-thumb="upper" tabindex="0" role="slider" aria-label="${upperLabel}" aria-valuemin="${this.min}" aria-valuemax="${this.max}"></div>`
|
|
111
|
+
: `<div slot="thumb" tabindex="0"></div>`;
|
|
90
112
|
this.innerHTML = `
|
|
91
113
|
<div slot="header">
|
|
92
114
|
${this.label ? `<span slot="label">${this.label}</span>` : ''}
|
|
93
115
|
<span slot="readout">
|
|
94
|
-
|
|
116
|
+
${readout}
|
|
95
117
|
${this.suffix ? `<span slot="suffix">${this.suffix}</span>` : ''}
|
|
96
118
|
</span>
|
|
97
119
|
</div>
|
|
98
120
|
<div slot="track">
|
|
99
121
|
<div slot="fill"></div>
|
|
100
|
-
|
|
122
|
+
${thumbs}
|
|
101
123
|
</div>
|
|
102
124
|
${this.hint ? `<span slot="hint" id="${hintId}">${this.hint}</span>` : ''}
|
|
103
125
|
`;
|
|
@@ -105,32 +127,28 @@ export class UISlider extends UIFormElement {
|
|
|
105
127
|
}
|
|
106
128
|
|
|
107
129
|
this.#trackEl = this.querySelector('[slot="track"]');
|
|
108
|
-
|
|
130
|
+
if (isDual) {
|
|
131
|
+
this.#thumbLowerEl = this.querySelector('[slot="thumb"][data-thumb="lower"]');
|
|
132
|
+
this.#thumbUpperEl = this.querySelector('[slot="thumb"][data-thumb="upper"]');
|
|
133
|
+
this.#thumbLowerEl?.addEventListener('pointerdown', this.#onPointerDown);
|
|
134
|
+
this.#thumbUpperEl?.addEventListener('pointerdown', this.#onPointerDown);
|
|
135
|
+
} else {
|
|
136
|
+
this.#thumbEl = this.querySelector('[slot="thumb"]');
|
|
137
|
+
this.#thumbEl?.addEventListener('pointerdown', this.#onPointerDown);
|
|
138
|
+
}
|
|
109
139
|
|
|
110
|
-
|
|
111
|
-
if (this.#trackEl) this.#trackEl.addEventListener('click', this.#onTrackClick);
|
|
140
|
+
this.#trackEl?.addEventListener('click', this.#onTrackClick);
|
|
112
141
|
this.addEventListener('keydown', this.#onKey);
|
|
113
142
|
}
|
|
114
143
|
|
|
115
144
|
render() {
|
|
116
145
|
if (!this.#trackEl) return;
|
|
117
146
|
|
|
118
|
-
// §153
|
|
119
|
-
|
|
120
|
-
// auto-subscribe; AdiaUI's reactive system (template.js `isFn` branch)
|
|
121
|
-
// does wrap functions in effects + call them per dep change, but the
|
|
122
|
-
// result of `v()` must be a number for `#pct` / `#format` to work.
|
|
123
|
-
// When the function returns non-number (e.g. doesn't read a signal,
|
|
124
|
-
// returns object/string) OR when consumers bypass the template engine
|
|
125
|
-
// (manual `sliderEl.value = someFunction`), `this.value` ends up as
|
|
126
|
-
// the function itself — `#pct` does NaN math, thumb stays at 0%,
|
|
127
|
-
// silent fail. Warn loudly + skip render so the bug class is
|
|
128
|
-
// diagnosable in dev. See USAGE.md "Reactive binding" for the
|
|
129
|
-
// documented patterns.
|
|
130
|
-
if (typeof this.value === 'function') {
|
|
147
|
+
// §153 function-typed value guard (extended to dual props).
|
|
148
|
+
if (typeof this.value === 'function' || typeof this.lowerValue === 'function' || typeof this.upperValue === 'function') {
|
|
131
149
|
// eslint-disable-next-line no-console
|
|
132
150
|
console.warn(
|
|
133
|
-
'[slider-ui] .value received a function. Did you mean:\n' +
|
|
151
|
+
'[slider-ui] .value / .lowerValue / .upperValue received a function. Did you mean:\n' +
|
|
134
152
|
' .value=${fn()} ← call the function to get the current value\n' +
|
|
135
153
|
' .value=${signal.value} ← read the signal\'s current value\n' +
|
|
136
154
|
'Functions are not auto-invoked at render time; the slider reads\n' +
|
|
@@ -140,18 +158,56 @@ export class UISlider extends UIFormElement {
|
|
|
140
158
|
return;
|
|
141
159
|
}
|
|
142
160
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
161
|
+
if (this.dual) {
|
|
162
|
+
// Bidirectional constraint: lower ≤ upper, but clamp the prop that
|
|
163
|
+
// was just changed (so direct programmatic prop assignment behaves
|
|
164
|
+
// intuitively in both directions).
|
|
165
|
+
// - lower bumped up past upper → clamp lower DOWN to upper
|
|
166
|
+
// - upper bumped down past lower → clamp upper UP to lower
|
|
167
|
+
// - both/neither (first render) → fall back to clamp lower DOWN
|
|
168
|
+
if (this.lowerValue > this.upperValue) {
|
|
169
|
+
const lowerChanged = !Number.isNaN(this.#prevLowerValue) && this.lowerValue !== this.#prevLowerValue;
|
|
170
|
+
const upperChanged = !Number.isNaN(this.#prevUpperValue) && this.upperValue !== this.#prevUpperValue;
|
|
171
|
+
if (upperChanged && !lowerChanged) {
|
|
172
|
+
this.upperValue = this.lowerValue;
|
|
173
|
+
} else {
|
|
174
|
+
this.lowerValue = this.upperValue;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
this.#prevLowerValue = this.lowerValue;
|
|
178
|
+
this.#prevUpperValue = this.upperValue;
|
|
179
|
+
const pctL = this.#pctOf(this.lowerValue);
|
|
180
|
+
const pctU = this.#pctOf(this.upperValue);
|
|
181
|
+
this.style.setProperty('--slider-pct-lower', String(pctL));
|
|
182
|
+
this.style.setProperty('--slider-pct-upper', String(pctU));
|
|
148
183
|
|
|
149
|
-
|
|
150
|
-
|
|
184
|
+
const valueLEl = this.querySelector('[slot="value-lower"]');
|
|
185
|
+
const valueUEl = this.querySelector('[slot="value-upper"]');
|
|
186
|
+
if (valueLEl) valueLEl.textContent = this.#format(this.lowerValue);
|
|
187
|
+
if (valueUEl) valueUEl.textContent = this.#format(this.upperValue);
|
|
188
|
+
|
|
189
|
+
if (this.#thumbLowerEl) {
|
|
190
|
+
this.#thumbLowerEl.setAttribute('aria-valuenow', this.lowerValue);
|
|
191
|
+
this.#thumbLowerEl.setAttribute('aria-valuetext', `${this.#format(this.lowerValue)}${this.suffix ? ' ' + this.suffix : ''}`);
|
|
192
|
+
}
|
|
193
|
+
if (this.#thumbUpperEl) {
|
|
194
|
+
this.#thumbUpperEl.setAttribute('aria-valuenow', this.upperValue);
|
|
195
|
+
this.#thumbUpperEl.setAttribute('aria-valuetext', `${this.#format(this.upperValue)}${this.suffix ? ' ' + this.suffix : ''}`);
|
|
196
|
+
}
|
|
197
|
+
this.syncValue(`${this.lowerValue},${this.upperValue}`);
|
|
198
|
+
} else {
|
|
199
|
+
const pct = this.#pctOf(this.value);
|
|
200
|
+
this.style.setProperty('--slider-pct', String(pct));
|
|
201
|
+
|
|
202
|
+
const valueEl = this.querySelector('[slot="value"]');
|
|
203
|
+
if (valueEl) valueEl.textContent = this.#format(this.value);
|
|
204
|
+
|
|
205
|
+
this.setAttribute('aria-valuenow', this.value);
|
|
206
|
+
this.setAttribute('aria-valuetext', `${this.#format(this.value)}${this.suffix ? ' ' + this.suffix : ''}`);
|
|
207
|
+
this.syncValue(String(this.value));
|
|
208
|
+
}
|
|
151
209
|
|
|
152
|
-
// §FB-45: label
|
|
153
|
-
// by the template engine (which first writes {{p:N}} then resolves).
|
|
154
|
-
// Re-sync both slots on every render so reactive bindings work.
|
|
210
|
+
// §FB-45: label + suffix late-binding re-sync (shared across modes).
|
|
155
211
|
const labelEl = this.querySelector('[slot="label"]');
|
|
156
212
|
if (labelEl) {
|
|
157
213
|
labelEl.textContent = this.label;
|
|
@@ -163,44 +219,68 @@ export class UISlider extends UIFormElement {
|
|
|
163
219
|
|
|
164
220
|
const suffixEl = this.querySelector('[slot="suffix"]');
|
|
165
221
|
if (suffixEl) suffixEl.textContent = this.suffix;
|
|
166
|
-
|
|
167
|
-
this.setAttribute('aria-valuenow', this.value);
|
|
168
|
-
this.setAttribute('aria-valuetext', `${this.#format(this.value)}${this.suffix ? ' ' + this.suffix : ''}`);
|
|
169
|
-
this.syncValue(String(this.value));
|
|
170
222
|
}
|
|
171
223
|
|
|
172
224
|
/**
|
|
173
225
|
* Inverse geometry: given a *desired* thumb-center viewport-x, compute
|
|
174
|
-
* the slider value such that the thumb center lands
|
|
175
|
-
* coordinate (clamped
|
|
226
|
+
* the slider value such that the named thumb's visual center lands at
|
|
227
|
+
* that coordinate (clamped to min/max).
|
|
176
228
|
*
|
|
177
|
-
* Forward geometry (
|
|
178
|
-
*
|
|
229
|
+
* Forward geometry differs by mode (see slider.css):
|
|
230
|
+
* single → center = t/2 + p · (W − t)
|
|
231
|
+
* dual lower (data-thumb=lower) → center = t/2 + p · (W − 2t)
|
|
232
|
+
* dual upper (data-thumb=upper) → center = 1.5t + p · (W − 2t)
|
|
179
233
|
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
234
|
+
* The dual model reserves 2t of travel-space so the two thumbs can
|
|
235
|
+
* never overlap — they touch edge-to-edge at equal values and span the
|
|
236
|
+
* full track at opposite extremes.
|
|
182
237
|
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* • #onPointerMove — (clientX − dragOffset) is the *intended* thumb
|
|
187
|
-
* center (offset preserves where the user originally grabbed the
|
|
188
|
-
* thumb, so dragging feels relative rather than snap-to-cursor).
|
|
238
|
+
* @param {number} clientX viewport x of the desired thumb center
|
|
239
|
+
* @param {'lower'|'upper'|null} which which thumb's geometry to invert
|
|
240
|
+
* (null = single-thumb mode); ignored when this.dual is false
|
|
189
241
|
*/
|
|
190
|
-
#valueFromX(clientX) {
|
|
191
|
-
|
|
242
|
+
#valueFromX(clientX, which = null) {
|
|
243
|
+
const refThumb = this.#thumbEl || this.#thumbLowerEl || this.#thumbUpperEl;
|
|
244
|
+
if (!this.#trackEl || !refThumb) return this.min;
|
|
192
245
|
const trackRect = this.#trackEl.getBoundingClientRect();
|
|
193
|
-
const
|
|
246
|
+
const t = refThumb.getBoundingClientRect().width;
|
|
194
247
|
const W = trackRect.width;
|
|
195
|
-
|
|
196
|
-
|
|
248
|
+
let travel, offset;
|
|
249
|
+
if (this.dual) {
|
|
250
|
+
travel = W - 2 * t;
|
|
251
|
+
offset = which === 'upper' ? 1.5 * t : 0.5 * t;
|
|
252
|
+
} else {
|
|
253
|
+
travel = W - t;
|
|
254
|
+
offset = 0.5 * t;
|
|
255
|
+
}
|
|
197
256
|
if (travel <= 0) return this.min;
|
|
198
|
-
const target = clientX - trackRect.left;
|
|
199
|
-
const ratio = Math.max(0, Math.min(1, (target -
|
|
257
|
+
const target = clientX - trackRect.left;
|
|
258
|
+
const ratio = Math.max(0, Math.min(1, (target - offset) / travel));
|
|
200
259
|
const raw = this.min + ratio * (this.max - this.min);
|
|
201
260
|
return this.#snap(raw);
|
|
202
261
|
}
|
|
203
262
|
|
|
263
|
+
// Visual-center of the named thumb in viewport coords. Used by drag
|
|
264
|
+
// logic to capture the cursor offset relative to the thumb's logical
|
|
265
|
+
// (transition-immune) center. Mirrors the forward geometry in
|
|
266
|
+
// slider.css per-mode.
|
|
267
|
+
#centerOfThumb(which) {
|
|
268
|
+
if (!this.#trackEl) return 0;
|
|
269
|
+
const refThumb = this.#thumbEl || this.#thumbLowerEl || this.#thumbUpperEl;
|
|
270
|
+
if (!refThumb) return 0;
|
|
271
|
+
const trackRect = this.#trackEl.getBoundingClientRect();
|
|
272
|
+
const t = refThumb.getBoundingClientRect().width;
|
|
273
|
+
const W = trackRect.width;
|
|
274
|
+
if (this.dual) {
|
|
275
|
+
const travel = W - 2 * t;
|
|
276
|
+
const p = which === 'upper' ? this.#pctOf(this.upperValue) : this.#pctOf(this.lowerValue);
|
|
277
|
+
const offset = which === 'upper' ? 1.5 * t : 0.5 * t;
|
|
278
|
+
return trackRect.left + offset + p * travel;
|
|
279
|
+
} else {
|
|
280
|
+
return trackRect.left + 0.5 * t + this.#pctOf(this.value) * (W - t);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
204
284
|
#snap(raw) {
|
|
205
285
|
const stepped = Math.round((raw - this.min) / this.step) * this.step + this.min;
|
|
206
286
|
return Math.max(this.min, Math.min(this.max, parseFloat(stepped.toFixed(10))));
|
|
@@ -209,98 +289,160 @@ export class UISlider extends UIFormElement {
|
|
|
209
289
|
#setValue(v) {
|
|
210
290
|
if (v === this.value) return;
|
|
211
291
|
this.value = v;
|
|
212
|
-
// §220 (v0.5.9): delegate to UIFormElement's shared throttle helper.
|
|
213
|
-
// When `this.throttle > 0`, the dispatch is trailing-debounced
|
|
214
|
-
// (collapses pointer-move bursts to one emission). When 0, fires
|
|
215
|
-
// synchronously.
|
|
216
292
|
this.scheduleThrottledInput();
|
|
217
293
|
}
|
|
218
294
|
|
|
295
|
+
#setLowerValue(v) {
|
|
296
|
+
const clamped = Math.min(Math.max(v, this.min), this.upperValue);
|
|
297
|
+
if (clamped === this.lowerValue) return;
|
|
298
|
+
this.lowerValue = clamped;
|
|
299
|
+
this.scheduleThrottledInput();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#setUpperValue(v) {
|
|
303
|
+
const clamped = Math.max(Math.min(v, this.max), this.lowerValue);
|
|
304
|
+
if (clamped === this.upperValue) return;
|
|
305
|
+
this.upperValue = clamped;
|
|
306
|
+
this.scheduleThrottledInput();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#emitChange() {
|
|
310
|
+
const detail = this.dual
|
|
311
|
+
? { lower: this.lowerValue, upper: this.upperValue }
|
|
312
|
+
: { value: this.value };
|
|
313
|
+
this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail }));
|
|
314
|
+
}
|
|
315
|
+
|
|
219
316
|
#onPointerDown = (e) => {
|
|
220
317
|
if (this.disabled) return;
|
|
221
318
|
e.preventDefault();
|
|
222
319
|
this.#dragging = true;
|
|
223
320
|
this.setAttribute('data-dragging', '');
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
this.#dragOffset = e.clientX -
|
|
321
|
+
|
|
322
|
+
let thumb;
|
|
323
|
+
if (this.dual) {
|
|
324
|
+
const which = e.currentTarget?.dataset?.thumb;
|
|
325
|
+
this.#draggingThumb = which === 'upper' ? 'upper' : 'lower';
|
|
326
|
+
thumb = this.#draggingThumb === 'lower' ? this.#thumbLowerEl : this.#thumbUpperEl;
|
|
327
|
+
// Lift the dragged thumb above its sibling so it stays visually
|
|
328
|
+
// anchored even when both thumbs land at the same value.
|
|
329
|
+
thumb?.setAttribute('data-active', '');
|
|
330
|
+
} else {
|
|
331
|
+
thumb = this.#thumbEl;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (this.#trackEl && thumb) {
|
|
335
|
+
// Logical center derived from the forward geometry (transition-immune),
|
|
336
|
+
// not from getBoundingClientRect which can be mid-animation.
|
|
337
|
+
const which = this.dual ? this.#draggingThumb : null;
|
|
338
|
+
this.#dragOffset = e.clientX - this.#centerOfThumb(which);
|
|
242
339
|
} else {
|
|
243
340
|
this.#dragOffset = 0;
|
|
244
341
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
342
|
+
|
|
343
|
+
thumb.setPointerCapture(e.pointerId);
|
|
344
|
+
thumb.addEventListener('pointermove', this.#onPointerMove);
|
|
345
|
+
thumb.addEventListener('pointerup', this.#onPointerUp);
|
|
248
346
|
};
|
|
249
347
|
|
|
250
348
|
#onPointerMove = (e) => {
|
|
251
349
|
if (!this.#dragging) return;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
350
|
+
const adjustedX = e.clientX - this.#dragOffset;
|
|
351
|
+
if (this.dual) {
|
|
352
|
+
const v = this.#valueFromX(adjustedX, this.#draggingThumb);
|
|
353
|
+
if (this.#draggingThumb === 'lower') this.#setLowerValue(v);
|
|
354
|
+
else this.#setUpperValue(v);
|
|
355
|
+
} else {
|
|
356
|
+
this.#setValue(this.#valueFromX(adjustedX));
|
|
357
|
+
}
|
|
256
358
|
};
|
|
257
359
|
|
|
258
360
|
#onPointerUp = (e) => {
|
|
259
361
|
this.#dragging = false;
|
|
260
362
|
this.#dragOffset = 0;
|
|
261
363
|
this.removeAttribute('data-dragging');
|
|
262
|
-
this
|
|
263
|
-
|
|
264
|
-
|
|
364
|
+
const thumb = this.dual
|
|
365
|
+
? (this.#draggingThumb === 'lower' ? this.#thumbLowerEl : this.#thumbUpperEl)
|
|
366
|
+
: this.#thumbEl;
|
|
367
|
+
thumb?.releasePointerCapture(e.pointerId);
|
|
368
|
+
thumb?.removeEventListener('pointermove', this.#onPointerMove);
|
|
369
|
+
thumb?.removeEventListener('pointerup', this.#onPointerUp);
|
|
370
|
+
// Drop the active marker — but only after emitting change, so any
|
|
371
|
+
// mid-drag z-index lift stays painted through the up event.
|
|
372
|
+
thumb?.removeAttribute('data-active');
|
|
265
373
|
this.flushPendingInput(); // §220 (was §184 #flushInput): pending throttled input fires before change
|
|
266
|
-
this
|
|
374
|
+
this.#emitChange();
|
|
375
|
+
this.#draggingThumb = null;
|
|
267
376
|
};
|
|
268
377
|
|
|
269
378
|
#onTrackClick = (e) => {
|
|
270
|
-
if (this.disabled
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
379
|
+
if (this.disabled) return;
|
|
380
|
+
if (this.dual) {
|
|
381
|
+
if (e.target === this.#thumbLowerEl || e.target === this.#thumbUpperEl) return;
|
|
382
|
+
// Pick the thumb closer to the click in pixel space (using each
|
|
383
|
+
// thumb's own forward-geometry center), then re-invert with that
|
|
384
|
+
// thumb's geometry so the chosen thumb lands precisely under the
|
|
385
|
+
// cursor.
|
|
386
|
+
const distLower = Math.abs(e.clientX - this.#centerOfThumb('lower'));
|
|
387
|
+
const distUpper = Math.abs(e.clientX - this.#centerOfThumb('upper'));
|
|
388
|
+
if (distLower <= distUpper) {
|
|
389
|
+
this.#setLowerValue(this.#valueFromX(e.clientX, 'lower'));
|
|
390
|
+
} else {
|
|
391
|
+
this.#setUpperValue(this.#valueFromX(e.clientX, 'upper'));
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
if (e.target === this.#thumbEl) return;
|
|
395
|
+
this.#setValue(this.#valueFromX(e.clientX));
|
|
396
|
+
}
|
|
397
|
+
this.flushPendingInput();
|
|
398
|
+
this.#emitChange();
|
|
274
399
|
};
|
|
275
400
|
|
|
276
401
|
#onKey = (e) => {
|
|
277
402
|
if (this.disabled) return;
|
|
278
|
-
let
|
|
403
|
+
let delta = 0;
|
|
404
|
+
let absolute = null;
|
|
279
405
|
switch (e.key) {
|
|
280
|
-
case 'ArrowRight': case 'ArrowUp':
|
|
281
|
-
case 'ArrowLeft': case 'ArrowDown':
|
|
282
|
-
case 'Home':
|
|
283
|
-
case 'End':
|
|
284
|
-
case 'PageUp':
|
|
285
|
-
case 'PageDown':
|
|
406
|
+
case 'ArrowRight': case 'ArrowUp': delta = this.step; break;
|
|
407
|
+
case 'ArrowLeft': case 'ArrowDown': delta = -this.step; break;
|
|
408
|
+
case 'Home': absolute = this.min; break;
|
|
409
|
+
case 'End': absolute = this.max; break;
|
|
410
|
+
case 'PageUp': delta = this.step * 10; break;
|
|
411
|
+
case 'PageDown': delta = -this.step * 10; break;
|
|
286
412
|
default: return;
|
|
287
413
|
}
|
|
288
414
|
e.preventDefault();
|
|
289
|
-
|
|
290
|
-
this.
|
|
291
|
-
|
|
415
|
+
|
|
416
|
+
if (this.dual) {
|
|
417
|
+
// Move whichever thumb is focused. Default to lower when neither.
|
|
418
|
+
const active = this.ownerDocument?.activeElement;
|
|
419
|
+
const targetThumb = active === this.#thumbUpperEl ? 'upper' : 'lower';
|
|
420
|
+
const current = targetThumb === 'lower' ? this.lowerValue : this.upperValue;
|
|
421
|
+
let next = absolute !== null ? absolute : current + delta;
|
|
422
|
+
next = this.#snap(next);
|
|
423
|
+
if (targetThumb === 'lower') this.#setLowerValue(next);
|
|
424
|
+
else this.#setUpperValue(next);
|
|
425
|
+
} else {
|
|
426
|
+
let v = absolute !== null ? absolute : this.value + delta;
|
|
427
|
+
this.#setValue(this.#snap(v));
|
|
428
|
+
}
|
|
429
|
+
this.flushPendingInput();
|
|
430
|
+
this.#emitChange();
|
|
292
431
|
};
|
|
293
432
|
|
|
294
433
|
disconnected() {
|
|
295
434
|
super.disconnected();
|
|
296
435
|
this.#thumbEl?.removeEventListener('pointerdown', this.#onPointerDown);
|
|
436
|
+
this.#thumbLowerEl?.removeEventListener('pointerdown', this.#onPointerDown);
|
|
437
|
+
this.#thumbUpperEl?.removeEventListener('pointerdown', this.#onPointerDown);
|
|
297
438
|
this.#trackEl?.removeEventListener('click', this.#onTrackClick);
|
|
298
|
-
|
|
299
|
-
|
|
439
|
+
// Pointermove/up listeners are attached transiently per-drag to the
|
|
440
|
+
// active thumb; #onPointerUp removes them. If disconnect happens
|
|
441
|
+
// mid-drag the browser tears down listeners with the element anyway.
|
|
300
442
|
this.removeEventListener('keydown', this.#onKey);
|
|
301
|
-
// §220 (v0.5.9): UIFormElement.disconnected() auto-drops the pending
|
|
302
|
-
// throttled input dispatch via super.disconnected().
|
|
303
443
|
this.#trackEl = null;
|
|
304
444
|
this.#thumbEl = null;
|
|
445
|
+
this.#thumbLowerEl = null;
|
|
446
|
+
this.#thumbUpperEl = null;
|
|
305
447
|
}
|
|
306
448
|
}
|
|
@@ -86,13 +86,17 @@
|
|
|
86
86
|
/* Progress fraction (0.0 → 1.0) written by JS. */
|
|
87
87
|
--slider-pct-default: 0;
|
|
88
88
|
|
|
89
|
+
/* Dual-thumb progress fractions (0.0 → 1.0) written by JS when [dual]. */
|
|
90
|
+
--slider-pct-lower-default: 0;
|
|
91
|
+
--slider-pct-upper-default: 1;
|
|
92
|
+
|
|
89
93
|
/* ── Colors ──
|
|
90
94
|
Track: dim recessed surface | Fill: primary | Thumb: white chrome */
|
|
91
95
|
--slider-track-bg-default: var(--a-bg-muted);
|
|
92
96
|
--slider-fill-bg-default: var(--a-primary-bg);
|
|
93
97
|
--slider-thumb-bg-default: var(--a-chrome-light);
|
|
94
|
-
--slider-fill-bg-disabled-default: var(--a-
|
|
95
|
-
--slider-thumb-bg-disabled-default: var(--a-
|
|
98
|
+
--slider-fill-bg-disabled-default: var(--a-canvas-1-scrim);
|
|
99
|
+
--slider-thumb-bg-disabled-default: var(--a-canvas-2-scrim);
|
|
96
100
|
|
|
97
101
|
/* ── Typography ── */
|
|
98
102
|
--slider-font-size-default: var(--a-ui-size);
|
|
@@ -172,6 +176,43 @@
|
|
|
172
176
|
+ var(--slider-pct, var(--slider-pct-default)) * (100% - var(--slider-thumb-width, var(--slider-thumb-width-default))));
|
|
173
177
|
}
|
|
174
178
|
|
|
179
|
+
/* Dual-thumb geometry: each thumb has an EFFECTIVE travel of (W − 2t),
|
|
180
|
+
not (W − t) — the reservation of 2t guarantees the thumbs never
|
|
181
|
+
overlap. At equal values they touch edge-to-edge; at extremes the
|
|
182
|
+
gap between thumb visual-edges represents 100% of the value range.
|
|
183
|
+
|
|
184
|
+
Forward equations (track-relative pixel positions):
|
|
185
|
+
lower_visual_left (p_l) = p_l · (W − 2t)
|
|
186
|
+
lower_visual_right (p_l) = t + p_l · (W − 2t)
|
|
187
|
+
upper_visual_left (p_u) = t + p_u · (W − 2t)
|
|
188
|
+
upper_visual_right (p_u) = 2t + p_u · (W − 2t)
|
|
189
|
+
|
|
190
|
+
CSS thumb `left` (visual center, since transform: translate(-50%)):
|
|
191
|
+
lower center = t/2 + p_l · (W − 2t)
|
|
192
|
+
upper center = 1.5t + p_u · (W − 2t)
|
|
193
|
+
|
|
194
|
+
Fill (lower-edge → upper-edge):
|
|
195
|
+
fill_left = p_l · (W − 2t)
|
|
196
|
+
fill_width = 2t + (p_u − p_l) · (W − 2t)
|
|
197
|
+
|
|
198
|
+
Sanity:
|
|
199
|
+
p_l = p_u → fill_width = 2t (both thumbs touching)
|
|
200
|
+
p_l = 0, p_u = 1 → fill_width = W (full track filled)
|
|
201
|
+
p_l = 0.5, p_u = 0.5 → pair centered at W/2 (thumbs touch at midpoint)
|
|
202
|
+
p_l = 0.2, p_u = 0.8, W=716, t=36 →
|
|
203
|
+
travel = W − 2t = 644
|
|
204
|
+
fill_left = 0.2·644 = 128.8
|
|
205
|
+
fill_width = 72 + 0.6·644 = 458.4
|
|
206
|
+
fill_right = 587.2 (= upper visual right) */
|
|
207
|
+
:scope[dual] [slot="fill"] {
|
|
208
|
+
left: calc(var(--slider-pct-lower, var(--slider-pct-lower-default))
|
|
209
|
+
* (100% - 2 * var(--slider-thumb-width, var(--slider-thumb-width-default))));
|
|
210
|
+
width: calc(2 * var(--slider-thumb-width, var(--slider-thumb-width-default))
|
|
211
|
+
+ (var(--slider-pct-upper, var(--slider-pct-upper-default))
|
|
212
|
+
- var(--slider-pct-lower, var(--slider-pct-lower-default)))
|
|
213
|
+
* (100% - 2 * var(--slider-thumb-width, var(--slider-thumb-width-default))));
|
|
214
|
+
}
|
|
215
|
+
|
|
175
216
|
/* Thumb CONTAINER: full track height, geometry width.
|
|
176
217
|
Transparent background; the white pill is rendered by ::before.
|
|
177
218
|
This element provides a generous vertical grab area. */
|
|
@@ -211,6 +252,45 @@
|
|
|
211
252
|
transition: transform var(--slider-duration, var(--slider-duration-default)) var(--slider-easing, var(--slider-easing-default));
|
|
212
253
|
}
|
|
213
254
|
|
|
255
|
+
/* Dual-mode thumb positions: each thumb travels along (W − 2t) so they
|
|
256
|
+
can never overlap. Lower's CSS center starts at t/2 (visual left
|
|
257
|
+
edge at 0); upper's CSS center starts at 1.5t (visual left edge at
|
|
258
|
+
t, leaving room for lower). At equal values the two thumbs touch
|
|
259
|
+
edge-to-edge — the 2t reservation between them is exactly the
|
|
260
|
+
combined thumb-width.
|
|
261
|
+
|
|
262
|
+
z-index nudges keep both thumbs paintable at boundary positions.
|
|
263
|
+
The "active" (currently-dragging) thumb lifts above its sibling. */
|
|
264
|
+
:scope[dual] [slot="thumb"][data-thumb="lower"] {
|
|
265
|
+
left: calc(var(--slider-thumb-width, var(--slider-thumb-width-default)) / 2
|
|
266
|
+
+ var(--slider-pct-lower, var(--slider-pct-lower-default))
|
|
267
|
+
* (100% - 2 * var(--slider-thumb-width, var(--slider-thumb-width-default))));
|
|
268
|
+
z-index: 2;
|
|
269
|
+
}
|
|
270
|
+
:scope[dual] [slot="thumb"][data-thumb="upper"] {
|
|
271
|
+
left: calc(var(--slider-thumb-width, var(--slider-thumb-width-default)) * 1.5
|
|
272
|
+
+ var(--slider-pct-upper, var(--slider-pct-upper-default))
|
|
273
|
+
* (100% - 2 * var(--slider-thumb-width, var(--slider-thumb-width-default))));
|
|
274
|
+
z-index: 3;
|
|
275
|
+
}
|
|
276
|
+
/* When the user is dragging a thumb, lift it above its sibling so the
|
|
277
|
+
dragged one stays visually anchored even if they cross. */
|
|
278
|
+
:scope[dual] [slot="thumb"][data-active] {
|
|
279
|
+
z-index: 4;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* Dual-mode readout: separator + numeric pair */
|
|
283
|
+
:scope[dual] [slot="value-sep"] {
|
|
284
|
+
color: var(--a-fg-muted);
|
|
285
|
+
padding: 0 var(--a-space-0-5);
|
|
286
|
+
}
|
|
287
|
+
:scope[dual] [slot="value-lower"],
|
|
288
|
+
:scope[dual] [slot="value-upper"] {
|
|
289
|
+
color: var(--a-fg);
|
|
290
|
+
font-weight: var(--slider-value-weight, var(--slider-value-weight-default));
|
|
291
|
+
font-variant-numeric: tabular-nums;
|
|
292
|
+
}
|
|
293
|
+
|
|
214
294
|
[slot="thumb"]:hover {
|
|
215
295
|
transform: translate(-50%, -50%) scale(1.05);
|
|
216
296
|
}
|