stimeo-ui 0.4.0 → 0.5.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +68 -0
- data/dist/controllers/aspect_ratio_controller.js +1 -1
- data/dist/controllers/breadcrumb_controller.js +5 -1
- data/dist/controllers/carousel_controller.js +5 -1
- data/dist/controllers/clipboard_controller.js +8 -3
- data/dist/controllers/collapsible_controller.js +4 -1
- data/dist/controllers/color_picker_controller.js +6 -2
- data/dist/controllers/context_menu_controller.js +2 -2
- data/dist/controllers/countdown_controller.js +5 -1
- data/dist/controllers/date_range_picker_controller.js +5 -1
- data/dist/controllers/direct_upload_controller.js +3 -3
- data/dist/controllers/empty_state_controller.js +107 -16
- data/dist/controllers/flash_controller.js +161 -21
- data/dist/controllers/form_validation_controller.js +8 -2
- data/dist/controllers/frame_loading_controller.js +94 -22
- data/dist/controllers/highlight_controller.js +38 -1
- data/dist/controllers/idle_controller.js +13 -2
- data/dist/controllers/local_time_controller.js +2 -0
- data/dist/controllers/masonry_controller.js +1 -1
- data/dist/controllers/meter_controller.js +3 -1
- data/dist/controllers/network_status_controller.js +1 -3
- data/dist/controllers/number_input_controller.js +191 -24
- data/dist/controllers/overflow_menu_controller.js +1 -1
- data/dist/controllers/pagination_controller.js +5 -1
- data/dist/controllers/password_strength_controller.js +1 -1
- data/dist/controllers/pointer_drag_controller.js +10 -0
- data/dist/controllers/portal_controller.js +10 -0
- data/dist/controllers/progress_controller.js +7 -3
- data/dist/controllers/range_slider_controller.js +385 -94
- data/dist/controllers/rating_controller.js +2 -0
- data/dist/controllers/relative_time_controller.js +2 -0
- data/dist/controllers/scroll_area_controller.js +1 -1
- data/dist/controllers/separator_controller.js +13 -17
- data/dist/controllers/skeleton_controller.js +71 -3
- data/dist/controllers/slider_controller.js +325 -48
- data/dist/controllers/spinner_controller.js +18 -3
- data/dist/controllers/step_indicator_controller.js +3 -1
- data/dist/controllers/stepper_controller.js +2 -0
- data/dist/controllers/switch_controller.js +162 -18
- data/dist/controllers/textarea_autosize_controller.js +1 -1
- data/dist/controllers/time_picker_controller.js +6 -3
- data/dist/controllers/tree_view_controller.js +19 -1
- data/dist/index.js +1214 -307
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -19,6 +19,61 @@ function fillTemplate(template, values) {
|
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// src/utils/detach_gate.ts
|
|
23
|
+
var DetachGate = class _DetachGate {
|
|
24
|
+
/** Set while a probe is queued, waiting for a reconnect to cancel it. */
|
|
25
|
+
#pending = false;
|
|
26
|
+
/**
|
|
27
|
+
* True while a probe is queued — the last disconnect was ambiguous and no
|
|
28
|
+
* reconnect has cancelled it yet. Read it from `connect()` to tell the
|
|
29
|
+
* reconnect half of an in-page move from a first connect: a controller whose
|
|
30
|
+
* initialisation restarts a measurement (a min-duration floor, an elapsed
|
|
31
|
+
* counter) must skip it for the move, where nothing actually restarted.
|
|
32
|
+
*/
|
|
33
|
+
get pending() {
|
|
34
|
+
return this.#pending;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* True when the disconnect is definitely a real detach — the element left
|
|
38
|
+
* the document, or `data-controller` no longer lists the identifier. False
|
|
39
|
+
* means ambiguous (in-page move or observed-root exit), NOT "alive".
|
|
40
|
+
*/
|
|
41
|
+
static isDetached(host) {
|
|
42
|
+
if (!host.element.isConnected) return true;
|
|
43
|
+
const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
|
|
44
|
+
return !tokens.includes(host.identifier);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Call from `disconnect()`: runs `teardown` synchronously on a definite
|
|
48
|
+
* detach (fast path), otherwise defers it one microtask — a reconnect
|
|
49
|
+
* ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.
|
|
50
|
+
* One microtask is the whole probe window: Stimulus reconnects a moved
|
|
51
|
+
* element within the same mutation batch, before the checkpoint drains.
|
|
52
|
+
*/
|
|
53
|
+
disconnected(host, teardown) {
|
|
54
|
+
if (_DetachGate.isDetached(host)) {
|
|
55
|
+
this.#pending = false;
|
|
56
|
+
teardown();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
this.#pending = true;
|
|
60
|
+
queueMicrotask(() => {
|
|
61
|
+
if (!this.#pending) return;
|
|
62
|
+
this.#pending = false;
|
|
63
|
+
teardown();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Disarms a pending probe. Call from `connect()` (the reconnect that proves
|
|
68
|
+
* an in-page move) and from the head of any teardown path not routed through
|
|
69
|
+
* {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can
|
|
70
|
+
* never run the teardown a second time.
|
|
71
|
+
*/
|
|
72
|
+
cancel() {
|
|
73
|
+
this.#pending = false;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
22
77
|
// src/utils/min_duration_floor.ts
|
|
23
78
|
var MinDurationFloor = class {
|
|
24
79
|
#timers;
|
|
@@ -131,14 +186,16 @@ var SkeletonController = class extends Controller {
|
|
|
131
186
|
static events = ["ready"];
|
|
132
187
|
#timers = new SafeTimeout();
|
|
133
188
|
#floor = new MinDurationFloor(this.#timers);
|
|
189
|
+
#gate = new DetachGate();
|
|
134
190
|
connect() {
|
|
135
|
-
|
|
191
|
+
const moved = this.#gate.pending;
|
|
192
|
+
this.#gate.cancel();
|
|
193
|
+
if (!moved && this.#state !== "ready") {
|
|
136
194
|
this.#enterLoading();
|
|
137
195
|
}
|
|
138
196
|
}
|
|
139
197
|
disconnect() {
|
|
140
|
-
this.#
|
|
141
|
-
this.#floor.cancel();
|
|
198
|
+
this.#gate.disconnected(this, () => this.#teardown());
|
|
142
199
|
}
|
|
143
200
|
/** Swaps to the real content. Honors `minDuration` to prevent a flash. */
|
|
144
201
|
ready() {
|
|
@@ -167,6 +224,17 @@ var SkeletonController = class extends Controller {
|
|
|
167
224
|
this.dispatch("ready", { detail: {} });
|
|
168
225
|
announce(fillTemplate(this.announceReadyTextValue, {}));
|
|
169
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Drops the held reveal on a real detach. The markup keeps whatever it last
|
|
229
|
+
* held: an element on its way out of the document has no reader left, and one
|
|
230
|
+
* whose `data-controller` dropped the identifier no longer resolves its own
|
|
231
|
+
* targets, so the rollback could only ever be partial.
|
|
232
|
+
*/
|
|
233
|
+
#teardown() {
|
|
234
|
+
this.#gate.cancel();
|
|
235
|
+
this.#timers.clearAll();
|
|
236
|
+
this.#floor.cancel();
|
|
237
|
+
}
|
|
170
238
|
/** Current lifecycle phase as reflected on `data-state`. */
|
|
171
239
|
get #state() {
|
|
172
240
|
return this.element.getAttribute("data-state") ?? "loading";
|
|
@@ -18,6 +18,92 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
18
18
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// src/utils/microtask_coalescer.ts
|
|
22
|
+
var MicrotaskCoalescer = class {
|
|
23
|
+
#run;
|
|
24
|
+
#queued = false;
|
|
25
|
+
#active = false;
|
|
26
|
+
#generation = 0;
|
|
27
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
28
|
+
constructor(run) {
|
|
29
|
+
this.#run = run;
|
|
30
|
+
}
|
|
31
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
32
|
+
activate() {
|
|
33
|
+
this.#active = true;
|
|
34
|
+
}
|
|
35
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
36
|
+
cancel() {
|
|
37
|
+
this.#active = false;
|
|
38
|
+
this.#queued = false;
|
|
39
|
+
this.#generation += 1;
|
|
40
|
+
}
|
|
41
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
42
|
+
schedule() {
|
|
43
|
+
if (!this.#active || this.#queued) return;
|
|
44
|
+
this.#queued = true;
|
|
45
|
+
const generation = this.#generation;
|
|
46
|
+
queueMicrotask(() => {
|
|
47
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
48
|
+
this.#queued = false;
|
|
49
|
+
this.#run();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// src/utils/owned_pointer_session.ts
|
|
55
|
+
var OwnedPointerSession = class {
|
|
56
|
+
pointerId;
|
|
57
|
+
#owner;
|
|
58
|
+
#handlers;
|
|
59
|
+
#abort = new AbortController();
|
|
60
|
+
#active = true;
|
|
61
|
+
constructor(start, owner, handlers) {
|
|
62
|
+
this.pointerId = start.pointerId;
|
|
63
|
+
this.#owner = owner;
|
|
64
|
+
this.#handlers = handlers;
|
|
65
|
+
const { signal } = this.#abort;
|
|
66
|
+
owner.ownerDocument.addEventListener("pointermove", this.#onMove, { signal });
|
|
67
|
+
owner.ownerDocument.addEventListener("pointerup", this.#onEndEvent, { signal });
|
|
68
|
+
owner.ownerDocument.addEventListener("pointercancel", this.#onEndEvent, { signal });
|
|
69
|
+
owner.addEventListener("lostpointercapture", this.#onLostCapture, { signal });
|
|
70
|
+
try {
|
|
71
|
+
owner.setPointerCapture?.(this.pointerId);
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Whether this session still owns its pointer and listeners. */
|
|
76
|
+
get active() {
|
|
77
|
+
return this.#active;
|
|
78
|
+
}
|
|
79
|
+
/** Whether `event` belongs to the initiating pointer of the live session. */
|
|
80
|
+
owns(event) {
|
|
81
|
+
return this.#active && event.pointerId === this.pointerId;
|
|
82
|
+
}
|
|
83
|
+
/** Releases capture/listeners and invokes the end callback exactly once. */
|
|
84
|
+
end() {
|
|
85
|
+
if (!this.#active) return;
|
|
86
|
+
this.#active = false;
|
|
87
|
+
this.#abort.abort();
|
|
88
|
+
try {
|
|
89
|
+
this.#owner.releasePointerCapture?.(this.pointerId);
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
this.#handlers.end?.();
|
|
93
|
+
}
|
|
94
|
+
#onMove = (event) => {
|
|
95
|
+
if (this.owns(event)) this.#handlers.move(event);
|
|
96
|
+
};
|
|
97
|
+
#onEndEvent = (event) => {
|
|
98
|
+
if (this.owns(event)) this.end();
|
|
99
|
+
};
|
|
100
|
+
#onLostCapture = (event) => {
|
|
101
|
+
const pointerId = event.pointerId;
|
|
102
|
+
if (typeof pointerId === "number" && pointerId !== this.pointerId) return;
|
|
103
|
+
this.end();
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
21
107
|
// src/utils/range.ts
|
|
22
108
|
function rangeFraction(value, min, max) {
|
|
23
109
|
const span = max - min;
|
|
@@ -34,6 +120,112 @@ function rangeFraction(value, min, max) {
|
|
|
34
120
|
return Math.min(1, Math.max(0, fraction));
|
|
35
121
|
}
|
|
36
122
|
|
|
123
|
+
// src/utils/stepped_value.ts
|
|
124
|
+
function effectiveStep(step) {
|
|
125
|
+
return Number.isFinite(step) && step > 0 ? step : 1;
|
|
126
|
+
}
|
|
127
|
+
function snapSteppedValue(raw, range) {
|
|
128
|
+
if (!(range.min <= range.max)) return finiteFallback(range.min, range.max);
|
|
129
|
+
const input = Number.isNaN(raw) ? finiteFallback(range.min, range.max) : raw;
|
|
130
|
+
const clamped = Math.min(range.max, Math.max(range.min, input));
|
|
131
|
+
const step = effectiveStep(range.step);
|
|
132
|
+
const base = stepBase(range);
|
|
133
|
+
const candidates = [];
|
|
134
|
+
if (Number.isFinite(range.min)) candidates.push(range.min);
|
|
135
|
+
if (Number.isFinite(range.max)) candidates.push(range.max);
|
|
136
|
+
const gridPosition = (clamped - base) / step;
|
|
137
|
+
if (Number.isFinite(gridPosition)) {
|
|
138
|
+
addGridCandidate(candidates, Math.floor(gridPosition), range, base, step);
|
|
139
|
+
addGridCandidate(candidates, Math.ceil(gridPosition), range, base, step);
|
|
140
|
+
}
|
|
141
|
+
if (candidates.length === 0) return clamped;
|
|
142
|
+
let nearest = candidates[0];
|
|
143
|
+
let nearestDistance = Math.abs(clamped - nearest);
|
|
144
|
+
for (const candidate of candidates.slice(1)) {
|
|
145
|
+
const distance = Math.abs(clamped - candidate);
|
|
146
|
+
if (distance < nearestDistance || nearlyEqual(distance, nearestDistance) && candidate > nearest) {
|
|
147
|
+
nearest = candidate;
|
|
148
|
+
nearestDistance = distance;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return nearest;
|
|
152
|
+
}
|
|
153
|
+
function stepSteppedValue(current, count, range) {
|
|
154
|
+
const value = snapSteppedValue(current, range);
|
|
155
|
+
const distance = Math.abs(Math.trunc(count));
|
|
156
|
+
if (!Number.isFinite(distance) || distance === 0) return value;
|
|
157
|
+
const direction = Math.sign(count);
|
|
158
|
+
const adjacent = adjacentSteppedValue(value, direction, range);
|
|
159
|
+
const raw = adjacent + direction * (distance - 1) * effectiveStep(range.step);
|
|
160
|
+
return snapSteppedValue(raw, range);
|
|
161
|
+
}
|
|
162
|
+
function adjacentSteppedValue(current, direction, range) {
|
|
163
|
+
const step = effectiveStep(range.step);
|
|
164
|
+
const base = stepBase(range);
|
|
165
|
+
const candidates = [];
|
|
166
|
+
if (direction > 0) {
|
|
167
|
+
const position2 = (current - base) / step;
|
|
168
|
+
if (Number.isFinite(position2)) {
|
|
169
|
+
let gridIndex = Math.floor(position2) + 1;
|
|
170
|
+
let candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
171
|
+
if (candidate < current || nearlyEqual(candidate, current)) {
|
|
172
|
+
gridIndex += 1;
|
|
173
|
+
candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
174
|
+
}
|
|
175
|
+
if (candidate > current && !nearlyEqual(candidate, current) && within(candidate, range.min, range.max)) {
|
|
176
|
+
candidates.push(clamp(candidate, range));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return clamp(Math.min(...candidates), range);
|
|
180
|
+
}
|
|
181
|
+
const position = (current - base) / step;
|
|
182
|
+
if (Number.isFinite(position)) {
|
|
183
|
+
let gridIndex = Math.ceil(position) - 1;
|
|
184
|
+
let candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
185
|
+
if (candidate > current || nearlyEqual(candidate, current)) {
|
|
186
|
+
gridIndex -= 1;
|
|
187
|
+
candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
188
|
+
}
|
|
189
|
+
if (candidate < current && !nearlyEqual(candidate, current) && within(candidate, range.min, range.max)) {
|
|
190
|
+
candidates.push(clamp(candidate, range));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return clamp(Math.max(...candidates), range);
|
|
194
|
+
}
|
|
195
|
+
function addGridCandidate(candidates, index, range, base, step) {
|
|
196
|
+
const candidate = cleanGridValue(base + index * step, base, step);
|
|
197
|
+
if (within(candidate, range.min, range.max)) candidates.push(clamp(candidate, range));
|
|
198
|
+
}
|
|
199
|
+
function stepBase(range) {
|
|
200
|
+
if (range.base !== void 0 && Number.isFinite(range.base)) return range.base;
|
|
201
|
+
return Number.isFinite(range.min) ? range.min : 0;
|
|
202
|
+
}
|
|
203
|
+
function cleanGridValue(value, base, step) {
|
|
204
|
+
const precision = Math.max(decimalPlaces(base), decimalPlaces(step));
|
|
205
|
+
return precision <= 100 ? Number(value.toFixed(precision)) : value;
|
|
206
|
+
}
|
|
207
|
+
function decimalPlaces(value) {
|
|
208
|
+
const [coefficient = "", exponentText] = Math.abs(value).toString().toLowerCase().split("e");
|
|
209
|
+
const fractionLength = coefficient.split(".")[1]?.length ?? 0;
|
|
210
|
+
const exponent = exponentText === void 0 ? 0 : Number(exponentText);
|
|
211
|
+
return Math.max(0, fractionLength - exponent);
|
|
212
|
+
}
|
|
213
|
+
function within(value, min, max) {
|
|
214
|
+
return (value > min || nearlyEqual(value, min)) && (value < max || nearlyEqual(value, max));
|
|
215
|
+
}
|
|
216
|
+
function clamp(value, range) {
|
|
217
|
+
return Math.min(range.max, Math.max(range.min, value));
|
|
218
|
+
}
|
|
219
|
+
function nearlyEqual(left, right) {
|
|
220
|
+
const scale = Math.max(Math.abs(left), Math.abs(right));
|
|
221
|
+
return Number.isFinite(left) && Number.isFinite(right) && Math.abs(left - right) <= Number.EPSILON * scale;
|
|
222
|
+
}
|
|
223
|
+
function finiteFallback(min, max) {
|
|
224
|
+
if (Number.isFinite(min)) return min;
|
|
225
|
+
if (Number.isFinite(max)) return max;
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
37
229
|
// src/controllers/slider_controller.ts
|
|
38
230
|
var FRACTION_PROPERTY = "--stimeo--slider-fraction";
|
|
39
231
|
var SliderController = class extends Controller {
|
|
@@ -47,40 +239,79 @@ var SliderController = class extends Controller {
|
|
|
47
239
|
};
|
|
48
240
|
static actions = ["onKeydown", "onPointerDown"];
|
|
49
241
|
static events = ["change"];
|
|
50
|
-
/**
|
|
51
|
-
#
|
|
242
|
+
/** One initiating pointer owns each live drag and its stable target snapshot. */
|
|
243
|
+
#drag = null;
|
|
244
|
+
/** Collapses a morph that swaps several render Values into one silent repaint. */
|
|
245
|
+
#repaint = new MicrotaskCoalescer(() => this.#render());
|
|
52
246
|
/** Whether the consumer declared a mirroring track and the direction mirrors it. */
|
|
53
247
|
get #mirrored() {
|
|
54
248
|
return this.logicalTrackValue && isRtl(this.element);
|
|
55
249
|
}
|
|
56
250
|
/** Clamps the initial value and renders the starting position. */
|
|
57
251
|
connect() {
|
|
58
|
-
this.#
|
|
252
|
+
this.#repaint.activate();
|
|
253
|
+
this.#commit(this.valueValue, false);
|
|
59
254
|
}
|
|
60
255
|
/** Cancels any active pointer drag so document listeners never leak. */
|
|
61
256
|
disconnect() {
|
|
62
|
-
this.#
|
|
63
|
-
this.#
|
|
257
|
+
this.#repaint.cancel();
|
|
258
|
+
this.#endDrag();
|
|
259
|
+
}
|
|
260
|
+
/** Silently repaints a minimum changed by application code or a Turbo morph. */
|
|
261
|
+
minValueChanged() {
|
|
262
|
+
this.#repaint.schedule();
|
|
263
|
+
}
|
|
264
|
+
/** Silently repaints a maximum changed by application code or a Turbo morph. */
|
|
265
|
+
maxValueChanged() {
|
|
266
|
+
this.#repaint.schedule();
|
|
267
|
+
}
|
|
268
|
+
/** Silently repaints a step changed by application code or a Turbo morph. */
|
|
269
|
+
stepValueChanged() {
|
|
270
|
+
this.#repaint.schedule();
|
|
271
|
+
}
|
|
272
|
+
/** Silently repaints a value changed by application code or a Turbo morph. */
|
|
273
|
+
valueValueChanged() {
|
|
274
|
+
this.#repaint.schedule();
|
|
275
|
+
}
|
|
276
|
+
/** Hydrates a thumb inserted or replaced at runtime with the current ARIA state. */
|
|
277
|
+
thumbTargetConnected(thumb) {
|
|
278
|
+
const value = this.#currentValue();
|
|
279
|
+
this.#renderThumb(thumb, value);
|
|
280
|
+
this.#renderFraction(value);
|
|
281
|
+
if (this.#drag && this.#drag.thumb === null) {
|
|
282
|
+
this.#drag.thumb = thumb;
|
|
283
|
+
thumb.focus();
|
|
284
|
+
}
|
|
285
|
+
this.#repaint.schedule();
|
|
286
|
+
}
|
|
287
|
+
/** Drops the stale focus owner while allowing a live track gesture to continue. */
|
|
288
|
+
thumbTargetDisconnected(thumb) {
|
|
289
|
+
if (this.#drag?.thumb === thumb) this.#drag.thumb = null;
|
|
290
|
+
}
|
|
291
|
+
/** Ends a gesture whose geometry target disappeared or ceased being a target. */
|
|
292
|
+
trackTargetDisconnected(track) {
|
|
293
|
+
if (this.#drag?.track === track) this.#endDrag();
|
|
64
294
|
}
|
|
65
295
|
/** Handles keyboard stepping per the APG slider model. */
|
|
66
296
|
onKeydown(event) {
|
|
67
297
|
if (isReservedArrowChord(event)) return;
|
|
68
|
-
const big = this.stepValue * 10;
|
|
69
298
|
let next = null;
|
|
299
|
+
const current = this.#currentValue();
|
|
300
|
+
const range = this.#steppedRange;
|
|
70
301
|
switch (this.#mirrored ? logicalArrowKey(event.key, this.element) : event.key) {
|
|
71
302
|
case "ArrowRight":
|
|
72
303
|
case "ArrowUp":
|
|
73
|
-
next =
|
|
304
|
+
next = stepSteppedValue(current, 1, range);
|
|
74
305
|
break;
|
|
75
306
|
case "ArrowLeft":
|
|
76
307
|
case "ArrowDown":
|
|
77
|
-
next =
|
|
308
|
+
next = stepSteppedValue(current, -1, range);
|
|
78
309
|
break;
|
|
79
310
|
case "PageUp":
|
|
80
|
-
next =
|
|
311
|
+
next = stepSteppedValue(current, 10, range);
|
|
81
312
|
break;
|
|
82
313
|
case "PageDown":
|
|
83
|
-
next =
|
|
314
|
+
next = stepSteppedValue(current, -10, range);
|
|
84
315
|
break;
|
|
85
316
|
case "Home":
|
|
86
317
|
next = this.minValue;
|
|
@@ -92,56 +323,102 @@ var SliderController = class extends Controller {
|
|
|
92
323
|
return;
|
|
93
324
|
}
|
|
94
325
|
event.preventDefault();
|
|
95
|
-
this.#
|
|
326
|
+
this.#commit(next, true);
|
|
96
327
|
}
|
|
97
328
|
/** Begins a pointer drag: sets the value and tracks subsequent movement. */
|
|
98
329
|
onPointerDown(event) {
|
|
99
|
-
if (!this.hasTrackTarget) return;
|
|
100
|
-
|
|
330
|
+
if (event.button !== 0 || this.#drag || !this.hasTrackTarget || !this.hasThumbTarget) return;
|
|
331
|
+
const track = this.trackTarget;
|
|
332
|
+
const thumb = this.thumbTarget;
|
|
101
333
|
const mirrored = this.#mirrored;
|
|
102
|
-
this.#
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
334
|
+
const value = this.#valueFromClientX(event.clientX, mirrored, track);
|
|
335
|
+
if (value === null) return;
|
|
336
|
+
event.preventDefault();
|
|
337
|
+
this.#commit(value, true);
|
|
338
|
+
thumb.focus();
|
|
339
|
+
const drag = { pointer: null, track, thumb };
|
|
340
|
+
drag.pointer = new OwnedPointerSession(event, track, {
|
|
341
|
+
move: (move) => {
|
|
342
|
+
if (!track.isConnected) {
|
|
343
|
+
this.#endDrag();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const moved = this.#valueFromClientX(move.clientX, mirrored, track);
|
|
347
|
+
if (moved !== null) this.#commit(moved, true);
|
|
348
|
+
},
|
|
349
|
+
end: () => {
|
|
350
|
+
if (this.#drag === drag) this.#drag = null;
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
this.#drag = drag;
|
|
354
|
+
}
|
|
355
|
+
/** Maps a pointer X coordinate to a raw value using a stable track snapshot. */
|
|
356
|
+
#valueFromClientX(clientX, mirrored, track) {
|
|
357
|
+
const rect = track.getBoundingClientRect();
|
|
358
|
+
if (rect.width === 0) return null;
|
|
120
359
|
const offset = (clientX - rect.left) / rect.width;
|
|
121
360
|
const fraction = mirrored ? 1 - offset : offset;
|
|
122
|
-
this
|
|
361
|
+
return this.minValue + fraction * (this.maxValue - this.minValue);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Stores a normalized value and renders synchronously for responsive input.
|
|
365
|
+
* Morph callbacks use {@link #render} instead, so they never write Values back
|
|
366
|
+
* or dispatch a user-facing change event.
|
|
367
|
+
*/
|
|
368
|
+
#commit(raw, notify) {
|
|
369
|
+
const previous = this.#currentValue();
|
|
370
|
+
const value = snapSteppedValue(raw, this.#steppedRange);
|
|
371
|
+
if (!Object.is(this.valueValue, value)) this.valueValue = value;
|
|
372
|
+
this.#renderValue(value);
|
|
373
|
+
if (notify && value !== previous) this.dispatch("change", { detail: { value } });
|
|
123
374
|
}
|
|
124
375
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* change — symmetric with `range-slider` — unless `silent` (the initial
|
|
129
|
-
* connect render, which is not a user edit).
|
|
376
|
+
* Reflects the normalized current Value without mutating or dispatching it.
|
|
377
|
+
*
|
|
378
|
+
* @stimeoRenderRoot
|
|
130
379
|
*/
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
this.valueValue = value;
|
|
380
|
+
#render() {
|
|
381
|
+
this.#renderValue(this.#currentValue());
|
|
382
|
+
}
|
|
383
|
+
/** Reflects one normalized value on the current target and CSS output. */
|
|
384
|
+
#renderValue(value) {
|
|
137
385
|
if (this.hasThumbTarget) {
|
|
138
|
-
this.thumbTarget
|
|
139
|
-
this.thumbTarget.setAttribute("aria-valuemax", String(this.maxValue));
|
|
140
|
-
this.thumbTarget.setAttribute("aria-valuenow", String(value));
|
|
386
|
+
this.#renderThumb(this.thumbTarget, value);
|
|
141
387
|
}
|
|
388
|
+
this.#renderFraction(value);
|
|
389
|
+
}
|
|
390
|
+
/** Writes only ARIA values that differ, including on a replacement target. */
|
|
391
|
+
#renderThumb(thumb, value) {
|
|
392
|
+
const attributes = {
|
|
393
|
+
"aria-valuemin": String(this.minValue),
|
|
394
|
+
"aria-valuemax": String(this.maxValue),
|
|
395
|
+
"aria-valuenow": String(value)
|
|
396
|
+
};
|
|
397
|
+
for (const [name, next] of Object.entries(attributes)) {
|
|
398
|
+
if (thumb.getAttribute(name) !== next) thumb.setAttribute(name, next);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/** Writes the behavior-only positioning hook only when its value changed. */
|
|
402
|
+
#renderFraction(value) {
|
|
142
403
|
const fraction = rangeFraction(value, this.minValue, this.maxValue);
|
|
143
|
-
|
|
144
|
-
if (
|
|
404
|
+
const next = String(fraction);
|
|
405
|
+
if (this.element.style.getPropertyValue(FRACTION_PROPERTY) !== next) {
|
|
406
|
+
this.element.style.setProperty(FRACTION_PROPERTY, next);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/** Current normalized value derived from the live declarative inputs. */
|
|
410
|
+
#currentValue() {
|
|
411
|
+
return snapSteppedValue(this.valueValue, this.#steppedRange);
|
|
412
|
+
}
|
|
413
|
+
/** Shared range configuration; finite endpoints remain allowed off the grid. */
|
|
414
|
+
get #steppedRange() {
|
|
415
|
+
return { min: this.minValue, max: this.maxValue, step: this.stepValue };
|
|
416
|
+
}
|
|
417
|
+
/** Ends the current pointer session without dispatching another change. */
|
|
418
|
+
#endDrag() {
|
|
419
|
+
const drag = this.#drag;
|
|
420
|
+
this.#drag = null;
|
|
421
|
+
drag?.pointer?.end();
|
|
145
422
|
}
|
|
146
423
|
};
|
|
147
424
|
|
|
@@ -48,10 +48,27 @@ var BeforeCacheReset = class _BeforeCacheReset {
|
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
// src/utils/default_attribute.ts
|
|
52
|
+
function setDefaultAttribute(element, name, value) {
|
|
53
|
+
if (element.hasAttribute(name)) return false;
|
|
54
|
+
element.setAttribute(name, value);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
51
58
|
// src/utils/detach_gate.ts
|
|
52
59
|
var DetachGate = class _DetachGate {
|
|
53
60
|
/** Set while a probe is queued, waiting for a reconnect to cancel it. */
|
|
54
61
|
#pending = false;
|
|
62
|
+
/**
|
|
63
|
+
* True while a probe is queued — the last disconnect was ambiguous and no
|
|
64
|
+
* reconnect has cancelled it yet. Read it from `connect()` to tell the
|
|
65
|
+
* reconnect half of an in-page move from a first connect: a controller whose
|
|
66
|
+
* initialisation restarts a measurement (a min-duration floor, an elapsed
|
|
67
|
+
* counter) must skip it for the move, where nothing actually restarted.
|
|
68
|
+
*/
|
|
69
|
+
get pending() {
|
|
70
|
+
return this.#pending;
|
|
71
|
+
}
|
|
55
72
|
/**
|
|
56
73
|
* True when the disconnect is definitely a real detach — the element left
|
|
57
74
|
* the document, or `data-controller` no longer lists the identifier. False
|
|
@@ -222,9 +239,7 @@ var SpinnerController = class extends Controller {
|
|
|
222
239
|
this.element.setAttribute("data-state", "idle");
|
|
223
240
|
return;
|
|
224
241
|
}
|
|
225
|
-
|
|
226
|
-
this.element.setAttribute("data-state", "idle");
|
|
227
|
-
}
|
|
242
|
+
setDefaultAttribute(this.element, "data-state", "idle");
|
|
228
243
|
}
|
|
229
244
|
/**
|
|
230
245
|
* Re-applies the current phase to an indicator that arrived after `connect()`.
|
|
@@ -99,6 +99,8 @@ var StepIndicatorController = class extends Controller {
|
|
|
99
99
|
* A pure function of the step set and `current`, so running it again writes the
|
|
100
100
|
* same values — which is what lets the action path paint synchronously (the event
|
|
101
101
|
* goes out after the DOM is updated) while a coalesced pass may still follow.
|
|
102
|
+
*
|
|
103
|
+
* @stimeoRenderRoot
|
|
102
104
|
*/
|
|
103
105
|
#render() {
|
|
104
106
|
const total = this.stepTargets.length;
|
|
@@ -112,7 +114,7 @@ var StepIndicatorController = class extends Controller {
|
|
|
112
114
|
}
|
|
113
115
|
});
|
|
114
116
|
const ratio = total > 1 ? current / (total - 1) : 0;
|
|
115
|
-
this.element.style.setProperty("--stimeo
|
|
117
|
+
this.element.style.setProperty("--stimeo--step-indicator-ratio", String(ratio));
|
|
116
118
|
}
|
|
117
119
|
/**
|
|
118
120
|
* Constrains an index to `[0, total-1]` (or `0` when there are no steps). A
|
|
@@ -68,6 +68,8 @@ var StepperController = class extends Controller {
|
|
|
68
68
|
* `aria-current="step"` is placed on the step's **first** `<button>`; the markup
|
|
69
69
|
* contract assumes one operable button per step. If a step needs multiple
|
|
70
70
|
* buttons, mark the navigational one first (or this would target the wrong one).
|
|
71
|
+
*
|
|
72
|
+
* @stimeoRenderRoot
|
|
71
73
|
*/
|
|
72
74
|
#render(current) {
|
|
73
75
|
this.stepTargets.forEach((step, index) => {
|