stimeo-ui 0.5.0 → 0.7.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 +178 -0
- data/dist/controllers/alert_dialog_controller.js +75 -4
- data/dist/controllers/aspect_ratio_controller.js +19 -11
- data/dist/controllers/avatar_controller.js +237 -40
- data/dist/controllers/carousel_controller.js +85 -9
- data/dist/controllers/character_counter_controller.js +338 -63
- data/dist/controllers/checkbox_controller.js +136 -25
- data/dist/controllers/color_picker_controller.js +35 -9
- data/dist/controllers/command_palette_controller.js +75 -4
- data/dist/controllers/conditional_fields_controller.js +345 -51
- data/dist/controllers/confirm_controller.js +75 -4
- data/dist/controllers/date_range_picker_controller.js +157 -30
- data/dist/controllers/dialog_controller.js +75 -4
- data/dist/controllers/direct_upload_controller.js +201 -45
- data/dist/controllers/dirty_form_controller.js +192 -29
- data/dist/controllers/dismissible_controller.js +83 -18
- data/dist/controllers/drawer_controller.js +75 -4
- data/dist/controllers/file_dropzone_controller.js +26 -3
- data/dist/controllers/focus_controller.js +75 -4
- data/dist/controllers/form_field_controller.js +280 -62
- data/dist/controllers/form_validation_controller.js +208 -83
- data/dist/controllers/idle_controller.js +27 -5
- data/dist/controllers/menubar_controller.js +5 -3
- data/dist/controllers/multi_select_controller.js +460 -151
- data/dist/controllers/number_input_controller.js +317 -51
- data/dist/controllers/overflow_menu_controller.js +6 -1
- data/dist/controllers/pagination_controller.js +35 -1
- data/dist/controllers/password_strength_controller.js +20 -2
- data/dist/controllers/persist_controller.js +449 -120
- data/dist/controllers/popover_controller.js +77 -4
- data/dist/controllers/radio_group_controller.js +540 -56
- data/dist/controllers/rating_controller.js +276 -89
- data/dist/controllers/resizable_controller.js +33 -0
- data/dist/controllers/roving_controller.js +60 -5
- data/dist/controllers/scroll_area_controller.js +557 -125
- data/dist/controllers/scroll_visibility_controller.js +33 -0
- data/dist/controllers/separator_controller.js +354 -38
- data/dist/controllers/sidebar_controller.js +83 -10
- data/dist/controllers/submit_once_controller.js +399 -121
- data/dist/controllers/tags_input_controller.js +356 -120
- data/dist/controllers/theme_controller.js +8 -6
- data/dist/controllers/time_picker_controller.js +296 -107
- data/dist/controllers/toggle_group_controller.js +378 -55
- data/dist/controllers/toolbar_controller.js +5 -3
- data/dist/controllers/tree_view_controller.js +7 -4
- data/dist/index.js +4963 -1581
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -7,10 +7,41 @@ function prefersReducedMotion() {
|
|
|
7
7
|
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
// src/utils/before_cache_reset.ts
|
|
11
|
+
var BeforeCacheReset = class _BeforeCacheReset {
|
|
12
|
+
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
13
|
+
static #subscribers = /* @__PURE__ */ new Set();
|
|
14
|
+
/** The shared listener; installed while at least one instance is subscribed. */
|
|
15
|
+
static #onBeforeCache = () => {
|
|
16
|
+
for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
|
|
17
|
+
};
|
|
18
|
+
#rewind;
|
|
19
|
+
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
20
|
+
constructor(rewind) {
|
|
21
|
+
this.#rewind = rewind;
|
|
22
|
+
}
|
|
23
|
+
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
24
|
+
activate() {
|
|
25
|
+
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
26
|
+
_BeforeCacheReset.#subscribers.add(this);
|
|
27
|
+
if (first) {
|
|
28
|
+
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
|
|
32
|
+
deactivate() {
|
|
33
|
+
_BeforeCacheReset.#subscribers.delete(this);
|
|
34
|
+
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
35
|
+
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
10
39
|
// src/utils/tabindex_loan.ts
|
|
11
40
|
var TabindexLoan = class {
|
|
12
41
|
#value;
|
|
13
42
|
#lent = /* @__PURE__ */ new Set();
|
|
43
|
+
/** Returns live loans before Turbo can copy them into its page snapshot. */
|
|
44
|
+
#beforeCache = new BeforeCacheReset(() => this.returnAll());
|
|
14
45
|
/**
|
|
15
46
|
* @param value - the `tabindex` to lend. `"-1"` (the default) is
|
|
16
47
|
* programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
|
|
@@ -24,6 +55,7 @@ var TabindexLoan = class {
|
|
|
24
55
|
if (element.hasAttribute("tabindex")) return;
|
|
25
56
|
element.setAttribute("tabindex", this.#value);
|
|
26
57
|
this.#lent.add(element);
|
|
58
|
+
this.#beforeCache.activate();
|
|
27
59
|
}
|
|
28
60
|
/** Takes back every loan whose value is still the one that was lent. */
|
|
29
61
|
returnAll() {
|
|
@@ -31,6 +63,7 @@ var TabindexLoan = class {
|
|
|
31
63
|
if (element.getAttribute("tabindex") === this.#value) element.removeAttribute("tabindex");
|
|
32
64
|
}
|
|
33
65
|
this.#lent.clear();
|
|
66
|
+
this.#beforeCache.deactivate();
|
|
34
67
|
}
|
|
35
68
|
};
|
|
36
69
|
|
|
@@ -8,82 +8,398 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
8
8
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
// src/utils/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
// src/utils/attribute_lease.ts
|
|
12
|
+
var AttributeLease = class {
|
|
13
|
+
#attribute;
|
|
14
|
+
#records = /* @__PURE__ */ new Map();
|
|
15
|
+
/** @param attribute - The attribute whose temporary values this lease owns. */
|
|
16
|
+
constructor(attribute) {
|
|
17
|
+
this.#attribute = attribute;
|
|
18
|
+
}
|
|
19
|
+
/** Writes or removes the leased attribute while preserving its authored value. */
|
|
20
|
+
write(element, value) {
|
|
21
|
+
const existing = this.#records.get(element);
|
|
22
|
+
if (existing) {
|
|
23
|
+
existing.written = value;
|
|
24
|
+
} else {
|
|
25
|
+
this.#records.set(element, {
|
|
26
|
+
original: element.getAttribute(this.#attribute),
|
|
27
|
+
written: value
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
this.#reflect(element, value);
|
|
31
|
+
}
|
|
32
|
+
/** Returns one lease without overwriting a value subsequently authored by a consumer. */
|
|
33
|
+
return(element) {
|
|
34
|
+
const record = this.#records.get(element);
|
|
35
|
+
if (!record) return;
|
|
36
|
+
this.#records.delete(element);
|
|
37
|
+
const stillOwned = element.getAttribute(this.#attribute) === record.written;
|
|
38
|
+
if (stillOwned) this.#reflect(element, record.original);
|
|
39
|
+
}
|
|
40
|
+
/** Reflects only a real value transition, avoiding self-triggered mutation work. */
|
|
41
|
+
#reflect(element, value) {
|
|
42
|
+
if (element.getAttribute(this.#attribute) === value) return;
|
|
43
|
+
if (value === null) element.removeAttribute(this.#attribute);
|
|
44
|
+
else element.setAttribute(this.#attribute, value);
|
|
45
|
+
}
|
|
46
|
+
/** Returns every outstanding lease using the same ownership check as {@link return}. */
|
|
47
|
+
returnAll() {
|
|
48
|
+
for (const element of Array.from(this.#records.keys())) this.return(element);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// src/utils/before_cache_reset.ts
|
|
53
|
+
var BeforeCacheReset = class _BeforeCacheReset {
|
|
54
|
+
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
55
|
+
static #subscribers = /* @__PURE__ */ new Set();
|
|
56
|
+
/** The shared listener; installed while at least one instance is subscribed. */
|
|
57
|
+
static #onBeforeCache = () => {
|
|
58
|
+
for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
|
|
59
|
+
};
|
|
60
|
+
#rewind;
|
|
61
|
+
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
62
|
+
constructor(rewind) {
|
|
63
|
+
this.#rewind = rewind;
|
|
64
|
+
}
|
|
65
|
+
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
66
|
+
activate() {
|
|
67
|
+
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
68
|
+
_BeforeCacheReset.#subscribers.add(this);
|
|
69
|
+
if (first) {
|
|
70
|
+
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
|
|
74
|
+
deactivate() {
|
|
75
|
+
_BeforeCacheReset.#subscribers.delete(this);
|
|
76
|
+
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
77
|
+
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// src/utils/microtask_coalescer.ts
|
|
82
|
+
var MicrotaskCoalescer = class {
|
|
83
|
+
#run;
|
|
84
|
+
#queued = false;
|
|
85
|
+
#active = false;
|
|
86
|
+
#generation = 0;
|
|
87
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
88
|
+
constructor(run) {
|
|
89
|
+
this.#run = run;
|
|
90
|
+
}
|
|
91
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
92
|
+
activate() {
|
|
93
|
+
this.#active = true;
|
|
94
|
+
}
|
|
95
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
96
|
+
cancel() {
|
|
97
|
+
this.#active = false;
|
|
98
|
+
this.#queued = false;
|
|
99
|
+
this.#generation += 1;
|
|
100
|
+
}
|
|
101
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
102
|
+
schedule() {
|
|
103
|
+
if (!this.#active || this.#queued) return;
|
|
104
|
+
this.#queued = true;
|
|
105
|
+
const generation = this.#generation;
|
|
106
|
+
queueMicrotask(() => {
|
|
107
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
108
|
+
this.#queued = false;
|
|
109
|
+
this.#run();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// src/utils/stepped_value.ts
|
|
115
|
+
function effectiveStep(step) {
|
|
116
|
+
return Number.isFinite(step) && step > 0 ? step : 1;
|
|
117
|
+
}
|
|
118
|
+
function snapSteppedValue(raw, range) {
|
|
119
|
+
if (!(range.min <= range.max)) return finiteFallback(range.min, range.max);
|
|
120
|
+
const input = Number.isNaN(raw) ? finiteFallback(range.min, range.max) : raw;
|
|
121
|
+
const clamped = Math.min(range.max, Math.max(range.min, input));
|
|
122
|
+
const step = effectiveStep(range.step);
|
|
123
|
+
const base = stepBase(range);
|
|
124
|
+
const candidates = [];
|
|
125
|
+
if (Number.isFinite(range.min)) candidates.push(range.min);
|
|
126
|
+
if (Number.isFinite(range.max)) candidates.push(range.max);
|
|
127
|
+
const gridPosition = (clamped - base) / step;
|
|
128
|
+
if (Number.isFinite(gridPosition)) {
|
|
129
|
+
addGridCandidate(candidates, Math.floor(gridPosition), range, base, step);
|
|
130
|
+
addGridCandidate(candidates, Math.ceil(gridPosition), range, base, step);
|
|
131
|
+
}
|
|
132
|
+
if (candidates.length === 0) return clamped;
|
|
133
|
+
let nearest = candidates[0];
|
|
134
|
+
let nearestDistance = Math.abs(clamped - nearest);
|
|
135
|
+
for (const candidate of candidates.slice(1)) {
|
|
136
|
+
const distance = Math.abs(clamped - candidate);
|
|
137
|
+
if (distance < nearestDistance || nearlyEqual(distance, nearestDistance) && candidate > nearest) {
|
|
138
|
+
nearest = candidate;
|
|
139
|
+
nearestDistance = distance;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return nearest;
|
|
143
|
+
}
|
|
144
|
+
function stepSteppedValue(current, count, range) {
|
|
145
|
+
const value = snapSteppedValue(current, range);
|
|
146
|
+
const distance = Math.abs(Math.trunc(count));
|
|
147
|
+
if (!Number.isFinite(distance) || distance === 0) return value;
|
|
148
|
+
const direction = Math.sign(count);
|
|
149
|
+
const adjacent = adjacentSteppedValue(value, direction, range);
|
|
150
|
+
const raw = adjacent + direction * (distance - 1) * effectiveStep(range.step);
|
|
151
|
+
return snapSteppedValue(raw, range);
|
|
152
|
+
}
|
|
153
|
+
function adjacentSteppedValue(current, direction, range) {
|
|
154
|
+
const step = effectiveStep(range.step);
|
|
155
|
+
const base = stepBase(range);
|
|
156
|
+
const candidates = [];
|
|
157
|
+
if (direction > 0) {
|
|
158
|
+
const position2 = (current - base) / step;
|
|
159
|
+
if (Number.isFinite(position2)) {
|
|
160
|
+
let gridIndex = Math.floor(position2) + 1;
|
|
161
|
+
let candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
162
|
+
if (candidate < current || nearlyEqual(candidate, current)) {
|
|
163
|
+
gridIndex += 1;
|
|
164
|
+
candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
165
|
+
}
|
|
166
|
+
if (candidate > current && !nearlyEqual(candidate, current) && within(candidate, range.min, range.max)) {
|
|
167
|
+
candidates.push(clamp(candidate, range));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return clamp(Math.min(...candidates), range);
|
|
171
|
+
}
|
|
172
|
+
const position = (current - base) / step;
|
|
173
|
+
if (Number.isFinite(position)) {
|
|
174
|
+
let gridIndex = Math.ceil(position) - 1;
|
|
175
|
+
let candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
176
|
+
if (candidate > current || nearlyEqual(candidate, current)) {
|
|
177
|
+
gridIndex -= 1;
|
|
178
|
+
candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
179
|
+
}
|
|
180
|
+
if (candidate < current && !nearlyEqual(candidate, current) && within(candidate, range.min, range.max)) {
|
|
181
|
+
candidates.push(clamp(candidate, range));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return clamp(Math.max(...candidates), range);
|
|
185
|
+
}
|
|
186
|
+
function addGridCandidate(candidates, index, range, base, step) {
|
|
187
|
+
const candidate = cleanGridValue(base + index * step, base, step);
|
|
188
|
+
if (within(candidate, range.min, range.max)) candidates.push(clamp(candidate, range));
|
|
189
|
+
}
|
|
190
|
+
function stepBase(range) {
|
|
191
|
+
if (range.base !== void 0 && Number.isFinite(range.base)) return range.base;
|
|
192
|
+
return Number.isFinite(range.min) ? range.min : 0;
|
|
193
|
+
}
|
|
194
|
+
function cleanGridValue(value, base, step) {
|
|
195
|
+
const precision = Math.max(decimalPlaces(base), decimalPlaces(step));
|
|
196
|
+
return precision <= 100 ? Number(value.toFixed(precision)) : value;
|
|
197
|
+
}
|
|
198
|
+
function decimalPlaces(value) {
|
|
199
|
+
const [coefficient = "", exponentText] = Math.abs(value).toString().toLowerCase().split("e");
|
|
200
|
+
const fractionLength = coefficient.split(".")[1]?.length ?? 0;
|
|
201
|
+
const exponent = exponentText === void 0 ? 0 : Number(exponentText);
|
|
202
|
+
return Math.max(0, fractionLength - exponent);
|
|
203
|
+
}
|
|
204
|
+
function within(value, min, max) {
|
|
205
|
+
return (value > min || nearlyEqual(value, min)) && (value < max || nearlyEqual(value, max));
|
|
206
|
+
}
|
|
207
|
+
function clamp(value, range) {
|
|
208
|
+
return Math.min(range.max, Math.max(range.min, value));
|
|
209
|
+
}
|
|
210
|
+
function nearlyEqual(left, right) {
|
|
211
|
+
const scale = Math.max(Math.abs(left), Math.abs(right));
|
|
212
|
+
return Number.isFinite(left) && Number.isFinite(right) && Math.abs(left - right) <= Number.EPSILON * scale;
|
|
213
|
+
}
|
|
214
|
+
function finiteFallback(min, max) {
|
|
215
|
+
if (Number.isFinite(min)) return min;
|
|
216
|
+
if (Number.isFinite(max)) return max;
|
|
217
|
+
return 0;
|
|
16
218
|
}
|
|
17
219
|
|
|
18
220
|
// src/controllers/separator_controller.ts
|
|
221
|
+
var DEFAULT_MIN = 0;
|
|
222
|
+
var DEFAULT_MAX = 100;
|
|
19
223
|
var SeparatorController = class extends Controller {
|
|
20
224
|
static values = {
|
|
21
225
|
orientation: { type: String, default: "horizontal" },
|
|
22
226
|
focusable: { type: Boolean, default: false },
|
|
23
|
-
|
|
227
|
+
min: { type: Number, default: DEFAULT_MIN },
|
|
228
|
+
max: { type: Number, default: DEFAULT_MAX },
|
|
229
|
+
step: { type: Number, default: 1 },
|
|
230
|
+
value: { type: Number, default: DEFAULT_MIN }
|
|
24
231
|
};
|
|
25
232
|
static actions = ["onKeydown"];
|
|
26
233
|
static events = ["change"];
|
|
234
|
+
#role = new AttributeLease("role");
|
|
235
|
+
#tabindex = new AttributeLease("tabindex");
|
|
236
|
+
#orientation = new AttributeLease("aria-orientation");
|
|
237
|
+
#minimum = new AttributeLease("aria-valuemin");
|
|
238
|
+
#maximum = new AttributeLease("aria-valuemax");
|
|
239
|
+
#current = new AttributeLease("aria-valuenow");
|
|
240
|
+
/** Collapses a morph that changes several render Values into one silent repaint. */
|
|
241
|
+
#repaint = new MicrotaskCoalescer(() => this.#render());
|
|
242
|
+
/** Restores authored semantics before Turbo snapshots the element. */
|
|
243
|
+
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
244
|
+
/** Hydrates legacy ARIA input once, normalizes the current Value, and renders. */
|
|
27
245
|
connect() {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
246
|
+
this.#hydrateValues();
|
|
247
|
+
this.#repaint.activate();
|
|
248
|
+
this.#beforeCache.activate();
|
|
249
|
+
const value = this.#currentValue();
|
|
250
|
+
if (!Object.is(this.valueValue, value)) this.valueValue = value;
|
|
251
|
+
this.#render();
|
|
252
|
+
}
|
|
253
|
+
/** Cancels repaint work and returns every semantic attribute this instance controlled. */
|
|
254
|
+
disconnect() {
|
|
255
|
+
this.#repaint.cancel();
|
|
256
|
+
this.#beforeCache.deactivate();
|
|
257
|
+
this.#returnAttributes();
|
|
258
|
+
}
|
|
259
|
+
/** Silently repaints an orientation changed by application code or a Turbo morph. */
|
|
260
|
+
orientationValueChanged() {
|
|
261
|
+
this.#repaint.schedule();
|
|
262
|
+
}
|
|
263
|
+
/** Silently adds or removes the focusable range semantics at runtime. */
|
|
264
|
+
focusableValueChanged() {
|
|
265
|
+
this.#repaint.schedule();
|
|
266
|
+
}
|
|
267
|
+
/** Silently repaints a minimum changed by application code or a Turbo morph. */
|
|
268
|
+
minValueChanged() {
|
|
269
|
+
this.#repaint.schedule();
|
|
270
|
+
}
|
|
271
|
+
/** Silently repaints a maximum changed by application code or a Turbo morph. */
|
|
272
|
+
maxValueChanged() {
|
|
273
|
+
this.#repaint.schedule();
|
|
274
|
+
}
|
|
275
|
+
/** Silently repaints a step changed by application code or a Turbo morph. */
|
|
276
|
+
stepValueChanged() {
|
|
277
|
+
this.#repaint.schedule();
|
|
278
|
+
}
|
|
279
|
+
/** Silently repaints a current value changed by application code or a Turbo morph. */
|
|
280
|
+
valueValueChanged() {
|
|
281
|
+
this.#repaint.schedule();
|
|
36
282
|
}
|
|
37
283
|
/** Adjusts the value on arrow / Home / End keys (focusable variant only). */
|
|
38
284
|
onKeydown(event) {
|
|
39
|
-
if (isReservedArrowChord(event)) return;
|
|
40
|
-
|
|
41
|
-
const
|
|
285
|
+
if (isReservedArrowChord(event) || !this.focusableValue) return;
|
|
286
|
+
const horizontal = this.#effectiveOrientation === "horizontal";
|
|
287
|
+
const range = this.#effectiveRange;
|
|
288
|
+
const current = snapSteppedValue(this.valueValue, range);
|
|
42
289
|
let next = null;
|
|
43
290
|
switch (event.key) {
|
|
44
291
|
case "ArrowUp":
|
|
45
|
-
if (horizontal) next =
|
|
292
|
+
if (horizontal) next = stepSteppedValue(current, 1, range);
|
|
46
293
|
break;
|
|
47
294
|
case "ArrowDown":
|
|
48
|
-
if (horizontal) next =
|
|
295
|
+
if (horizontal) next = stepSteppedValue(current, -1, range);
|
|
49
296
|
break;
|
|
50
297
|
case "ArrowRight":
|
|
51
|
-
if (!horizontal) next =
|
|
298
|
+
if (!horizontal) next = stepSteppedValue(current, 1, range);
|
|
52
299
|
break;
|
|
53
300
|
case "ArrowLeft":
|
|
54
|
-
if (!horizontal) next =
|
|
301
|
+
if (!horizontal) next = stepSteppedValue(current, -1, range);
|
|
55
302
|
break;
|
|
56
303
|
case "Home":
|
|
57
|
-
next =
|
|
304
|
+
next = range.min;
|
|
58
305
|
break;
|
|
59
306
|
case "End":
|
|
60
|
-
next =
|
|
307
|
+
next = range.max;
|
|
61
308
|
break;
|
|
62
309
|
default:
|
|
63
310
|
return;
|
|
64
311
|
}
|
|
65
312
|
if (next === null) return;
|
|
66
313
|
event.preventDefault();
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
314
|
+
this.#commit(next, current);
|
|
315
|
+
}
|
|
316
|
+
/** Stores a normalized user value, renders immediately, and reports real changes. */
|
|
317
|
+
#commit(raw, previous) {
|
|
318
|
+
const value = snapSteppedValue(raw, this.#effectiveRange);
|
|
319
|
+
if (!Object.is(this.valueValue, value)) this.valueValue = value;
|
|
320
|
+
this.#render();
|
|
321
|
+
if (value !== previous) this.dispatch("change", { detail: { value } });
|
|
71
322
|
}
|
|
72
|
-
|
|
73
|
-
|
|
323
|
+
/**
|
|
324
|
+
* Reflects live Values into the connected element without dispatching.
|
|
325
|
+
*
|
|
326
|
+
* @stimeoRenderRoot
|
|
327
|
+
*/
|
|
328
|
+
#render() {
|
|
329
|
+
const range = this.#effectiveRange;
|
|
330
|
+
const value = snapSteppedValue(this.valueValue, range);
|
|
331
|
+
this.#role.write(this.element, "separator");
|
|
332
|
+
this.#orientation.write(this.element, this.#effectiveOrientation);
|
|
333
|
+
if (!this.focusableValue) {
|
|
334
|
+
this.#tabindex.write(this.element, null);
|
|
335
|
+
this.#minimum.write(this.element, null);
|
|
336
|
+
this.#maximum.write(this.element, null);
|
|
337
|
+
this.#current.write(this.element, null);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
this.#tabindex.write(this.element, "0");
|
|
341
|
+
this.#minimum.write(this.element, String(range.min));
|
|
342
|
+
this.#maximum.write(this.element, String(range.max));
|
|
343
|
+
this.#current.write(this.element, String(value));
|
|
344
|
+
}
|
|
345
|
+
/** Effective orientation; unknown strings fall back to the horizontal contract. */
|
|
346
|
+
get #effectiveOrientation() {
|
|
347
|
+
return this.orientationValue === "vertical" ? "vertical" : "horizontal";
|
|
348
|
+
}
|
|
349
|
+
/** Finite, ordered range shared by rendering and keyboard stepping. */
|
|
350
|
+
get #effectiveRange() {
|
|
351
|
+
const min = Number.isFinite(this.minValue) ? this.minValue : DEFAULT_MIN;
|
|
352
|
+
const candidateMax = Number.isFinite(this.maxValue) ? this.maxValue : DEFAULT_MAX;
|
|
353
|
+
return {
|
|
354
|
+
min,
|
|
355
|
+
max: Math.max(min, candidateMax),
|
|
356
|
+
step: this.stepValue,
|
|
357
|
+
base: min
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
/** Current normalized value derived from the live declarative inputs. */
|
|
361
|
+
#currentValue() {
|
|
362
|
+
return snapSteppedValue(this.valueValue, this.#effectiveRange);
|
|
363
|
+
}
|
|
364
|
+
/** Copies valid authored ARIA into Values only when no explicit Value exists. */
|
|
365
|
+
#hydrateValues() {
|
|
366
|
+
if (!this.#hasInput("orientation")) {
|
|
367
|
+
const orientation = this.element.getAttribute("aria-orientation");
|
|
368
|
+
if (orientation === "horizontal" || orientation === "vertical") {
|
|
369
|
+
this.orientationValue = orientation;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const min = this.#finiteAttribute("aria-valuemin");
|
|
373
|
+
if (!this.#hasInput("min") && min !== null) this.minValue = min;
|
|
374
|
+
const max = this.#finiteAttribute("aria-valuemax");
|
|
375
|
+
if (!this.#hasInput("max") && max !== null) this.maxValue = max;
|
|
376
|
+
const value = this.#finiteAttribute("aria-valuenow");
|
|
377
|
+
if (!this.#hasInput("value") && value !== null) this.valueValue = value;
|
|
74
378
|
}
|
|
75
|
-
|
|
76
|
-
|
|
379
|
+
/** Whether the author supplied the namespaced Value instead of using its default. */
|
|
380
|
+
#hasInput(name) {
|
|
381
|
+
return this.element.hasAttribute(`data-${this.identifier}-${name}-value`);
|
|
77
382
|
}
|
|
78
|
-
|
|
79
|
-
|
|
383
|
+
/** Strict finite-number parser for the one-time progressive-enhancement bridge. */
|
|
384
|
+
#finiteAttribute(name) {
|
|
385
|
+
const raw = this.element.getAttribute(name);
|
|
386
|
+
if (raw === null || raw.trim() === "") return null;
|
|
387
|
+
const value = Number(raw);
|
|
388
|
+
return Number.isFinite(value) ? value : null;
|
|
80
389
|
}
|
|
81
|
-
|
|
82
|
-
|
|
390
|
+
/** Returns attributes without overwriting values a consumer changed after render. */
|
|
391
|
+
#returnAttributes() {
|
|
392
|
+
this.#role.return(this.element);
|
|
393
|
+
this.#tabindex.return(this.element);
|
|
394
|
+
this.#orientation.return(this.element);
|
|
395
|
+
this.#minimum.return(this.element);
|
|
396
|
+
this.#maximum.return(this.element);
|
|
397
|
+
this.#current.return(this.element);
|
|
83
398
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
399
|
+
/** Prevents a queued Value repaint from racing Turbo's restored snapshot. */
|
|
400
|
+
#rewindForCache() {
|
|
401
|
+
this.#repaint.cancel();
|
|
402
|
+
this.#returnAttributes();
|
|
87
403
|
}
|
|
88
404
|
};
|
|
89
405
|
|
|
@@ -115,8 +115,81 @@ var EscapeLayer = class _EscapeLayer {
|
|
|
115
115
|
}
|
|
116
116
|
};
|
|
117
117
|
|
|
118
|
+
// src/utils/focus_candidate.ts
|
|
119
|
+
function inheritsFieldsetDisabled(control) {
|
|
120
|
+
let fieldset = control.closest("fieldset[disabled]");
|
|
121
|
+
while (fieldset) {
|
|
122
|
+
const legend = Array.from(fieldset.children).find((child) => child.tagName === "LEGEND");
|
|
123
|
+
if (!legend?.contains(control)) return true;
|
|
124
|
+
fieldset = fieldset.parentElement?.closest("fieldset[disabled]") ?? null;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
function canTakeFocus(element) {
|
|
129
|
+
if (element.closest("[hidden], [inert]")) return false;
|
|
130
|
+
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
131
|
+
if (!("disabled" in element)) return true;
|
|
132
|
+
if (element.disabled) return false;
|
|
133
|
+
return !inheritsFieldsetDisabled(element);
|
|
134
|
+
}
|
|
135
|
+
var TAB_STOP_CANDIDATE_SELECTOR = [
|
|
136
|
+
"a[href]",
|
|
137
|
+
"area[href]",
|
|
138
|
+
"button",
|
|
139
|
+
"input",
|
|
140
|
+
"select",
|
|
141
|
+
"textarea",
|
|
142
|
+
"summary",
|
|
143
|
+
"iframe",
|
|
144
|
+
"audio[controls]",
|
|
145
|
+
"video[controls]",
|
|
146
|
+
"[tabindex]",
|
|
147
|
+
"[contenteditable]"
|
|
148
|
+
].join(",");
|
|
149
|
+
function isRenderedForFocus(element) {
|
|
150
|
+
const check = element.checkVisibility;
|
|
151
|
+
return typeof check === "function" ? check.call(element, { visibilityProperty: true }) : true;
|
|
152
|
+
}
|
|
153
|
+
function authoredTabindex(element) {
|
|
154
|
+
const value = element.getAttribute("tabindex");
|
|
155
|
+
if (value === null || !/^[+-]?\d+$/.test(value.trim())) return null;
|
|
156
|
+
return Number(value);
|
|
157
|
+
}
|
|
158
|
+
function hasNativeTabStop(element) {
|
|
159
|
+
if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {
|
|
160
|
+
return element.hasAttribute("href");
|
|
161
|
+
}
|
|
162
|
+
if (element instanceof HTMLButtonElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
if (element instanceof HTMLInputElement) return element.type !== "hidden";
|
|
166
|
+
if (element instanceof HTMLIFrameElement) return true;
|
|
167
|
+
if (element.tagName === "AUDIO" || element.tagName === "VIDEO") {
|
|
168
|
+
return element.hasAttribute("controls");
|
|
169
|
+
}
|
|
170
|
+
if (element instanceof HTMLElement && element.tagName === "SUMMARY") {
|
|
171
|
+
const details = element.parentElement;
|
|
172
|
+
return details instanceof HTMLDetailsElement && Array.from(details.children).find((child) => child.tagName === "SUMMARY") === element;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
function hasEditableTabStop(element) {
|
|
177
|
+
const value = element.getAttribute("contenteditable")?.toLowerCase();
|
|
178
|
+
return value === "" || value === "true" || value === "plaintext-only";
|
|
179
|
+
}
|
|
180
|
+
function isTabStop(element) {
|
|
181
|
+
if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;
|
|
182
|
+
const tabindex = authoredTabindex(element);
|
|
183
|
+
if (tabindex !== null) return tabindex >= 0;
|
|
184
|
+
return hasNativeTabStop(element) || hasEditableTabStop(element);
|
|
185
|
+
}
|
|
186
|
+
function tabStopsWithin(root) {
|
|
187
|
+
return Array.from(root.querySelectorAll(TAB_STOP_CANDIDATE_SELECTOR)).filter(
|
|
188
|
+
isTabStop
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
118
192
|
// src/utils/focus_trap.ts
|
|
119
|
-
var FOCUSABLE = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
120
193
|
var FocusTrap = class {
|
|
121
194
|
/** The element focused before activation, restored on deactivation. */
|
|
122
195
|
#previouslyFocused = null;
|
|
@@ -278,24 +351,24 @@ var FocusTrap = class {
|
|
|
278
351
|
}
|
|
279
352
|
/** Collects the container's currently focusable descendants in DOM order. */
|
|
280
353
|
#focusableElements() {
|
|
281
|
-
return
|
|
282
|
-
(el) => !el.hidden
|
|
283
|
-
);
|
|
354
|
+
return tabStopsWithin(this.#getContainer());
|
|
284
355
|
}
|
|
285
356
|
};
|
|
286
357
|
|
|
287
358
|
// src/utils/safe_storage.ts
|
|
288
359
|
function readLocalStorage(key) {
|
|
289
360
|
try {
|
|
290
|
-
return window.localStorage.getItem(key);
|
|
291
|
-
} catch {
|
|
292
|
-
return
|
|
361
|
+
return { ok: true, value: window.localStorage.getItem(key) };
|
|
362
|
+
} catch (error) {
|
|
363
|
+
return { ok: false, error };
|
|
293
364
|
}
|
|
294
365
|
}
|
|
295
366
|
function writeLocalStorage(key, value) {
|
|
296
367
|
try {
|
|
297
368
|
window.localStorage.setItem(key, value);
|
|
298
|
-
|
|
369
|
+
return { ok: true, value: void 0 };
|
|
370
|
+
} catch (error) {
|
|
371
|
+
return { ok: false, error };
|
|
299
372
|
}
|
|
300
373
|
}
|
|
301
374
|
|
|
@@ -754,8 +827,8 @@ var SidebarController = class extends Controller {
|
|
|
754
827
|
#restoreCollapsed() {
|
|
755
828
|
const key = this.#storageKey;
|
|
756
829
|
if (key) {
|
|
757
|
-
const
|
|
758
|
-
if (
|
|
830
|
+
const result = readLocalStorage(key);
|
|
831
|
+
if (result.ok && result.value !== null) return result.value === "1";
|
|
759
832
|
}
|
|
760
833
|
const domState = this.hasPanelTarget ? this.panelTarget.getAttribute("data-state") : null;
|
|
761
834
|
if (domState === "collapsed") return true;
|