stimeo-ui 0.17.0 → 0.18.1
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 +136 -2
- package/README.md +19 -10
- package/dist/cable/index.d.ts +11 -9
- package/dist/cable/index.js +22 -14
- package/dist/cable/index.js.map +1 -1
- package/dist/coerce-JtZVNG5h.d.ts +20 -0
- package/dist/controllers/accordion_controller.js +14 -6
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.js +145 -118
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +5 -3
- package/dist/controllers/announcer_controller.js +113 -17
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.js +185 -60
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +55 -43
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +3 -3
- package/dist/controllers/bulk_select_controller.js +64 -50
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +15 -12
- package/dist/controllers/calendar_controller.js +55 -53
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +9 -5
- package/dist/controllers/carousel_controller.js +124 -86
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +27 -6
- package/dist/controllers/character_counter_controller.js +291 -95
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.js +6 -0
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.js +118 -27
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +1 -1
- package/dist/controllers/collapsible_controller.js +52 -49
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.js +29 -7
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +1 -1
- package/dist/controllers/combobox_controller.js +210 -71
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +9 -9
- package/dist/controllers/command_palette_controller.js +290 -140
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.js +145 -58
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.js +145 -118
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +49 -14
- package/dist/controllers/context_menu_controller.js +138 -73
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +2 -2
- package/dist/controllers/countdown_controller.js +109 -21
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +75 -11
- package/dist/controllers/currency_input_controller.js +446 -72
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +10 -8
- package/dist/controllers/data_grid_controller.js +134 -28
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +12 -9
- package/dist/controllers/date_range_picker_controller.js +93 -72
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +4 -5
- package/dist/controllers/dialog_controller.js +145 -118
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +1 -1
- package/dist/controllers/direct_upload_controller.js +24 -14
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.d.ts +6 -1
- package/dist/controllers/dirty_form_controller.js +68 -31
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.js +66 -46
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.js +145 -118
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.js +76 -63
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.js +200 -62
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.js +13 -6
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +3 -0
- package/dist/controllers/file_dropzone_controller.js +55 -48
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.js +9 -2
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +4 -4
- package/dist/controllers/flash_controller.js +66 -33
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +3 -3
- package/dist/controllers/focus_controller.js +39 -14
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.d.ts +2 -1
- package/dist/controllers/form_field_controller.js +52 -43
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.d.ts +3 -3
- package/dist/controllers/form_validation_controller.js +18 -11
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +13 -12
- package/dist/controllers/frame_loading_controller.js +55 -48
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +5 -4
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +4 -1
- package/dist/controllers/hover_card_controller.js +67 -61
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +6 -5
- package/dist/controllers/idle_controller.js +10 -6
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +31 -13
- package/dist/controllers/input_mask_controller.js +434 -24
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js +3 -0
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +3 -2
- package/dist/controllers/lazy_frame_controller.js +15 -3
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +10 -7
- package/dist/controllers/listbox_controller.js +260 -79
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +8 -6
- package/dist/controllers/local_time_controller.js +48 -13
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.js +11 -4
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +24 -5
- package/dist/controllers/menu_controller.js +138 -82
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +49 -7
- package/dist/controllers/menubar_controller.js +65 -19
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +13 -20
- package/dist/controllers/meter_controller.js +270 -24
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +19 -16
- package/dist/controllers/multi_select_controller.js +272 -95
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +19 -5
- package/dist/controllers/navigation_menu_controller.js +67 -19
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.js +98 -64
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.js +70 -52
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +24 -6
- package/dist/controllers/number_input_controller.js +344 -94
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.d.ts +8 -5
- package/dist/controllers/optimistic_controller.js +29 -11
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +35 -4
- package/dist/controllers/otp_controller.js +502 -104
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.js +6 -0
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +10 -9
- package/dist/controllers/overflow_menu_controller.js +136 -21
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +3 -2
- package/dist/controllers/pagination_controller.js +47 -14
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +5 -4
- package/dist/controllers/password_reveal_controller.js +52 -49
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +1 -1
- package/dist/controllers/password_strength_controller.js +13 -6
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.js +48 -128
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +20 -8
- package/dist/controllers/pointer_drag_controller.js +255 -24
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +9 -5
- package/dist/controllers/popover_controller.js +91 -65
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.js +6 -0
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.js +14 -3
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +9 -19
- package/dist/controllers/progress_controller.js +271 -27
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +11 -8
- package/dist/controllers/radio_group_controller.js +167 -80
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +4 -3
- package/dist/controllers/range_slider_controller.js +27 -7
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.js +93 -57
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.js +10 -6
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.js +22 -5
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +4 -4
- package/dist/controllers/relative_time_controller.js +75 -11
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/reset_on_restore_controller.d.ts +3 -3
- package/dist/controllers/reset_on_restore_controller.js +6 -0
- package/dist/controllers/reset_on_restore_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +3 -0
- package/dist/controllers/resizable_controller.js +55 -22
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +3 -3
- package/dist/controllers/roving_controller.js +10 -4
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js +129 -88
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.d.ts +3 -3
- package/dist/controllers/scroll_restore_controller.js +14 -2
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.js +93 -60
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.js +11 -5
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +1 -1
- package/dist/controllers/separator_controller.js +59 -43
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +1 -1
- package/dist/controllers/sidebar_controller.js +150 -122
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.js +55 -48
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +2 -1
- package/dist/controllers/slider_controller.js +27 -6
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.d.ts +1 -1
- package/dist/controllers/smart_sticky_header_controller.js +13 -5
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.d.ts +4 -4
- package/dist/controllers/sortable_controller.js +27 -14
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.js +55 -48
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +4 -4
- package/dist/controllers/step_indicator_controller.js +6 -0
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +2 -3
- package/dist/controllers/stepper_controller.js +6 -0
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +3 -1
- package/dist/controllers/stick_to_bottom_controller.js +19 -8
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.js +49 -43
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +2 -1
- package/dist/controllers/submit_once_controller.js +145 -85
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +7 -4
- package/dist/controllers/switch_controller.js +172 -28
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +7 -4
- package/dist/controllers/tabs_controller.js +6 -1
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +4 -3
- package/dist/controllers/tags_input_controller.js +157 -28
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +1 -1
- package/dist/controllers/textarea_autosize_controller.js +40 -19
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +3 -2
- package/dist/controllers/theme_controller.js +10 -4
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +38 -5
- package/dist/controllers/time_picker_controller.js +192 -54
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +4 -5
- package/dist/controllers/toast_controller.js +58 -27
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +2 -3
- package/dist/controllers/toggle_group_controller.js +158 -56
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.d.ts +8 -6
- package/dist/controllers/toolbar_controller.js +150 -21
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +4 -1
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +1 -1
- package/dist/controllers/tree_view_controller.js +134 -25
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2515 -1315
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +25 -12
- package/dist/inspector/cli.js +21 -3
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.d.ts +1 -1
- package/dist/inspector/cli_bin.js +62 -8
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +13 -13
- package/dist/inspector/manifest.json +19 -5
- package/dist/positioning/index.d.ts +9 -8
- package/dist/positioning/index.js +6 -0
- package/dist/positioning/index.js.map +1 -1
- package/package.json +11 -10
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
+
function setAttributeIfChanged(element, name, value, beforeWrite) {
|
|
4
|
+
if (element.getAttribute(name) === value) return false;
|
|
5
|
+
if (value === null) element.removeAttribute(name);
|
|
6
|
+
else element.setAttribute(name, value);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
function sharedRegistry(key, create) {
|
|
4
11
|
const symbol = Symbol.for(key);
|
|
5
12
|
const scope = globalThis;
|
|
6
13
|
const existing = scope[symbol];
|
|
7
14
|
if (existing !== void 0) return existing;
|
|
8
|
-
const
|
|
9
|
-
Object.defineProperty(globalThis, symbol, { value:
|
|
10
|
-
return
|
|
15
|
+
const registry2 = create();
|
|
16
|
+
Object.defineProperty(globalThis, symbol, { value: registry2 });
|
|
17
|
+
return registry2;
|
|
11
18
|
}
|
|
12
19
|
|
|
20
|
+
var registry = sharedRegistry(
|
|
21
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
22
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
23
|
+
);
|
|
13
24
|
function isAttributeValue(value) {
|
|
14
25
|
return value === null || typeof value === "string";
|
|
15
26
|
}
|
|
@@ -26,24 +37,46 @@ function readLeaseRecord(element, record) {
|
|
|
26
37
|
return void 0;
|
|
27
38
|
}
|
|
28
39
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
);
|
|
40
|
+
function attributeStack(element, attribute) {
|
|
41
|
+
const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
42
|
+
registry.stacks.set(element, byAttribute);
|
|
43
|
+
const known = byAttribute.get(attribute);
|
|
44
|
+
if (known) return known;
|
|
45
|
+
const found = [];
|
|
46
|
+
for (const record of registry.names.get(attribute) ?? []) {
|
|
47
|
+
const read = readLeaseRecord(element, record);
|
|
48
|
+
if (read) found.push({ ...read, record });
|
|
49
|
+
}
|
|
50
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
51
|
+
const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
52
|
+
lease: null,
|
|
53
|
+
record,
|
|
54
|
+
written,
|
|
55
|
+
original,
|
|
56
|
+
linked: linked && index > 0
|
|
57
|
+
}));
|
|
58
|
+
byAttribute.set(attribute, stack);
|
|
59
|
+
return stack;
|
|
60
|
+
}
|
|
61
|
+
function findHold(stack, lease, record) {
|
|
62
|
+
return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
|
|
63
|
+
}
|
|
64
|
+
var AttributeLease = class {
|
|
34
65
|
#attribute;
|
|
35
66
|
#record;
|
|
36
67
|
#base;
|
|
68
|
+
#preserveAuthor;
|
|
37
69
|
#held = /* @__PURE__ */ new Set();
|
|
38
70
|
constructor(attribute, owner, options = {}) {
|
|
39
71
|
this.#attribute = attribute;
|
|
40
72
|
this.#record = `data-${owner}-${attribute}-lease`;
|
|
41
73
|
this.#base = options.base === true;
|
|
42
|
-
|
|
74
|
+
this.#preserveAuthor = options.preserveAuthor === true;
|
|
75
|
+
const names = registry.names;
|
|
43
76
|
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
44
77
|
}
|
|
45
78
|
write(element, value) {
|
|
46
|
-
const stack = this.#
|
|
79
|
+
const stack = attributeStack(element, this.#attribute);
|
|
47
80
|
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
48
81
|
this.#held.add(element);
|
|
49
82
|
const shows = !this.#covered(element, stack, hold);
|
|
@@ -53,8 +86,8 @@ var AttributeLease = class _AttributeLease {
|
|
|
53
86
|
}
|
|
54
87
|
return(element) {
|
|
55
88
|
const held = this.#held.delete(element);
|
|
56
|
-
const known =
|
|
57
|
-
const stack = held || known || element.hasAttribute(this.#record) ? this.#
|
|
89
|
+
const known = registry.stacks.get(element)?.has(this.#attribute);
|
|
90
|
+
const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
|
|
58
91
|
const hold = this.#take(stack);
|
|
59
92
|
if (!hold) return false;
|
|
60
93
|
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
@@ -72,31 +105,8 @@ var AttributeLease = class _AttributeLease {
|
|
|
72
105
|
returnAll() {
|
|
73
106
|
for (const element of Array.from(this.#held)) this.return(element);
|
|
74
107
|
}
|
|
75
|
-
#stack(element) {
|
|
76
|
-
const stacks = _AttributeLease.#registry.stacks;
|
|
77
|
-
const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
78
|
-
stacks.set(element, byAttribute);
|
|
79
|
-
const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
|
|
80
|
-
byAttribute.set(this.#attribute, stack);
|
|
81
|
-
return stack;
|
|
82
|
-
}
|
|
83
|
-
#seed(element) {
|
|
84
|
-
const found = [];
|
|
85
|
-
for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
|
|
86
|
-
const read = readLeaseRecord(element, record);
|
|
87
|
-
if (read) found.push({ ...read, record });
|
|
88
|
-
}
|
|
89
|
-
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
90
|
-
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
91
|
-
lease: null,
|
|
92
|
-
record,
|
|
93
|
-
written,
|
|
94
|
-
original,
|
|
95
|
-
linked: linked && index > 0
|
|
96
|
-
}));
|
|
97
|
-
}
|
|
98
108
|
#take(stack) {
|
|
99
|
-
const hold = stack
|
|
109
|
+
const hold = findHold(stack, this, this.#record);
|
|
100
110
|
if (hold) hold.lease = this;
|
|
101
111
|
return hold;
|
|
102
112
|
}
|
|
@@ -105,7 +115,7 @@ var AttributeLease = class _AttributeLease {
|
|
|
105
115
|
const top = stack.at(-1);
|
|
106
116
|
const bottom = stack[0];
|
|
107
117
|
const over = top?.written === current ? top : void 0;
|
|
108
|
-
const from = this.#base ? bottom : over;
|
|
118
|
+
const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
|
|
109
119
|
const hold = {
|
|
110
120
|
lease: this,
|
|
111
121
|
record: this.#record,
|
|
@@ -135,15 +145,11 @@ var AttributeLease = class _AttributeLease {
|
|
|
135
145
|
);
|
|
136
146
|
});
|
|
137
147
|
for (const [name, recorded] of records) {
|
|
138
|
-
|
|
139
|
-
if (recorded === null) element.removeAttribute(name);
|
|
140
|
-
else element.setAttribute(name, recorded);
|
|
148
|
+
setAttributeIfChanged(element, name, recorded);
|
|
141
149
|
}
|
|
142
150
|
}
|
|
143
151
|
#reflect(element, value) {
|
|
144
|
-
|
|
145
|
-
if (value === null) element.removeAttribute(this.#attribute);
|
|
146
|
-
else element.setAttribute(this.#attribute, value);
|
|
152
|
+
setAttributeIfChanged(element, this.#attribute, value);
|
|
147
153
|
}
|
|
148
154
|
};
|
|
149
155
|
|
|
@@ -174,6 +180,9 @@ var MicrotaskCoalescer = class {
|
|
|
174
180
|
this.#queued = false;
|
|
175
181
|
this.#generation += 1;
|
|
176
182
|
}
|
|
183
|
+
get pending() {
|
|
184
|
+
return this.#queued;
|
|
185
|
+
}
|
|
177
186
|
schedule() {
|
|
178
187
|
if (!this.#active || this.#queued) return;
|
|
179
188
|
this.#queued = true;
|
|
@@ -204,6 +213,9 @@ var MorphRenderWatcher = class {
|
|
|
204
213
|
schedule() {
|
|
205
214
|
this.#pass.schedule();
|
|
206
215
|
}
|
|
216
|
+
get pending() {
|
|
217
|
+
return this.#pass.pending;
|
|
218
|
+
}
|
|
207
219
|
disconnect() {
|
|
208
220
|
this.#listeners.dispose();
|
|
209
221
|
this.#pass.cancel();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/shared_registry.ts","../../src/utils/attribute_lease.ts","../../src/utils/listener_set.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/morph_render_watcher.ts","../../src/controllers/avatar_controller.ts"],"names":[],"mappings":";;AA4BO,SAAS,cAAA,CAAiC,KAAa,MAAA,EAAoB;AAChF,EAAA,MAAM,MAAA,GAAS,MAAA,CAAO,GAAA,CAAI,GAAG,CAAA;AAC7B,EAAA,MAAM,KAAA,GAAQ,UAAA;AACd,EAAA,MAAM,QAAA,GAAW,MAAM,MAAM,CAAA;AAC7B,EAAA,IAAI,QAAA,KAAa,QAAW,OAAO,QAAA;AACnC,EAAA,MAAM,WAAW,MAAA,EAAO;AACxB,EAAA,MAAA,CAAO,eAAe,UAAA,EAAY,MAAA,EAAQ,EAAE,KAAA,EAAO,UAAU,CAAA;AAC7D,EAAA,OAAO,QAAA;AACT;;ACLA,SAAS,iBAAiB,KAAA,EAAwC;AAChE,EAAA,OAAO,KAAA,KAAU,IAAA,IAAQ,OAAO,KAAA,KAAU,QAAA;AAC5C;AAOA,SAAS,eAAA,CAAgB,SAAkB,MAAA,EAAkD;AAC3F,EAAA,IAAI;AACF,IAAA,MAAM,SAAkB,IAAA,CAAK,KAAA,CAAM,QAAQ,YAAA,CAAa,MAAM,KAAK,MAAM,CAAA;AACzE,IAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,MAAM,GAAG,OAAO,KAAA,CAAA;AACnC,IAAA,MAAM,CAAC,QAAA,EAAU,OAAA,EAAS,GAAG,IAAI,CAAA,GAAI,MAAA;AACrC,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,CAAC,CAAA,KAAM,KAAA;AAC3B,IAAA,MAAM,OAAA,GAAU,MAAA,GAAS,IAAA,GAAO,IAAA,CAAK,MAAM,CAAC,CAAA;AAC5C,IAAA,IAAI,CAAC,QAAQ,KAAA,CAAM,CAAC,UAA2B,OAAO,KAAA,KAAU,QAAQ,CAAA,EAAG,OAAO,KAAA,CAAA;AAClF,IAAA,OAAO,gBAAA,CAAiB,QAAQ,CAAA,IAAK,gBAAA,CAAiB,OAAO,CAAA,GACzD,EAAE,QAAA,EAAU,OAAA,EAAS,MAAA,EAAQ,OAAA,EAAQ,GACrC,KAAA,CAAA;AAAA,EACN,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,MAAA;AAAA,EACT;AACF;AA4EO,IAAM,cAAA,GAAN,MAAM,eAAA,CAA4C;AAAA,EAEvD,OAAgB,SAAA,GAAY,cAAA;AAAA,IAC1B,uCAAA;AAAA,IACA,OAA+B,EAAE,MAAA,kBAAQ,IAAI,SAAQ,EAAG,KAAA,kBAAO,IAAI,GAAA,EAAI,EAAE;AAAA,GAC3E;AAAA,EACS,UAAA;AAAA,EACA,OAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA,uBAAY,GAAA,EAAO;AAAA,EAO5B,WAAA,CAAY,SAAA,EAAmB,KAAA,EAAe,OAAA,GAAuC,EAAC,EAAG;AACvF,IAAA,IAAA,CAAK,UAAA,GAAa,SAAA;AAClB,IAAA,IAAA,CAAK,OAAA,GAAU,CAAA,KAAA,EAAQ,KAAK,CAAA,CAAA,EAAI,SAAS,CAAA,MAAA,CAAA;AACzC,IAAA,IAAA,CAAK,KAAA,GAAQ,QAAQ,IAAA,KAAS,IAAA;AAC9B,IAAA,MAAM,KAAA,GAAQ,gBAAe,SAAA,CAAU,KAAA;AACvC,IAAA,KAAA,CAAM,GAAA,CAAI,SAAA,EAAA,CAAY,KAAA,CAAM,GAAA,CAAI,SAAS,CAAA,oBAAK,IAAI,GAAA,EAAY,EAAG,GAAA,CAAI,IAAA,CAAK,OAAO,CAAC,CAAA;AAAA,EACpF;AAAA,EAGA,KAAA,CAAM,SAAY,KAAA,EAA4B;AAC5C,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,MAAA,CAAO,OAAO,CAAA;AACjC,IAAA,MAAM,IAAA,GAAO,KAAK,KAAA,CAAM,KAAK,KAAK,IAAA,CAAK,QAAA,CAAS,SAAS,KAAK,CAAA;AAC9D,IAAA,IAAA,CAAK,KAAA,CAAM,IAAI,OAAO,CAAA;AACtB,IAAA,MAAM,QAAQ,CAAC,IAAA,CAAK,QAAA,CAAS,OAAA,EAAS,OAAO,IAAI,CAAA;AACjD,IAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,IAAA,IAAA,CAAK,KAAA,CAAM,SAAS,KAAK,CAAA;AACzB,IAAA,IAAI,KAAA,EAAO,IAAA,CAAK,QAAA,CAAS,OAAA,EAAS,KAAK,CAAA;AAAA,EACzC;AAAA,EAQA,OAAO,OAAA,EAAqB;AAC1B,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,MAAA,CAAO,OAAO,CAAA;AACtC,IAAA,MAAM,KAAA,GAAQ,gBAAe,SAAA,CAAU,MAAA,CAAO,IAAI,OAAO,CAAA,EAAG,GAAA,CAAI,IAAA,CAAK,UAAU,CAAA;AAC/E,IAAA,MAAM,KAAA,GAAQ,IAAA,IAAQ,KAAA,IAAS,OAAA,CAAQ,YAAA,CAAa,IAAA,CAAK,OAAO,CAAA,GAAI,IAAA,CAAK,MAAA,CAAO,OAAO,CAAA,GAAI,EAAC;AAC5F,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,KAAK,CAAA;AAC7B,IAAA,IAAI,CAAC,MAAM,OAAO,KAAA;AAClB,IAAA,MAAM,UAAU,OAAA,CAAQ,YAAA,CAAa,IAAA,CAAK,UAAU,MAAM,IAAA,CAAK,OAAA;AAC/D,IAAA,MAAM,OAAO,OAAA,IAAW,CAAC,KAAK,QAAA,CAAS,OAAA,EAAS,OAAO,IAAI,CAAA;AAC3D,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,IAAI,CAAA;AAChC,IAAA,KAAA,CAAM,MAAA,CAAO,OAAO,CAAC,CAAA;AACrB,IAAA,MAAM,KAAA,GAAQ,MAAM,KAAK,CAAA;AACzB,IAAA,IAAI,KAAA,EAAO,KAAA,CAAM,MAAA,KAAW,IAAA,CAAK,MAAA;AACjC,IAAA,IAAA,CAAK,KAAA,CAAM,SAAS,KAAK,CAAA;AACzB,IAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAClB,IAAA,MAAM,QAAQ,IAAA,CAAK,MAAA,GAAS,KAAA,CAAM,KAAA,GAAQ,CAAC,CAAA,GAAI,MAAA;AAC/C,IAAA,IAAA,CAAK,SAAS,OAAA,EAAS,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,KAAK,QAAQ,CAAA;AAC5D,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAGA,SAAA,GAAkB;AAChB,IAAA,KAAA,MAAW,OAAA,IAAW,MAAM,IAAA,CAAK,IAAA,CAAK,KAAK,CAAA,EAAG,IAAA,CAAK,OAAO,OAAO,CAAA;AAAA,EACnE;AAAA,EAGA,OAAO,OAAA,EAA6B;AAClC,IAAA,MAAM,MAAA,GAAS,gBAAe,SAAA,CAAU,MAAA;AACxC,IAAA,MAAM,cAAc,MAAA,CAAO,GAAA,CAAI,OAAO,CAAA,wBAAS,GAAA,EAA6B;AAC5E,IAAA,MAAA,CAAO,GAAA,CAAI,SAAS,WAAW,CAAA;AAC/B,IAAA,MAAM,KAAA,GAAQ,YAAY,GAAA,CAAI,IAAA,CAAK,UAAU,CAAA,IAAK,IAAA,CAAK,MAAM,OAAO,CAAA;AACpE,IAAA,WAAA,CAAY,GAAA,CAAI,IAAA,CAAK,UAAA,EAAY,KAAK,CAAA;AACtC,IAAA,OAAO,KAAA;AAAA,EACT;AAAA,EAGA,MAAM,OAAA,EAA6B;AACjC,IAAA,MAAM,QAAgE,EAAC;AACvE,IAAA,KAAA,MAAW,MAAA,IAAU,gBAAe,SAAA,CAAU,KAAA,CAAM,IAAI,IAAA,CAAK,UAAU,CAAA,IAAK,EAAC,EAAG;AAC9E,MAAA,MAAM,IAAA,GAAO,eAAA,CAAgB,OAAA,EAAS,MAAM,CAAA;AAC5C,MAAA,IAAI,MAAM,KAAA,CAAM,IAAA,CAAK,EAAE,GAAG,IAAA,EAAM,QAAQ,CAAA;AAAA,IAC1C;AACA,IAAA,MAAM,QAAQ,CAAC,EAAE,OAAA,EAAQ,KACvB,QAAQ,MAAA,CAAO,CAAC,IAAA,KAAS,KAAA,CAAM,KAAK,CAAC,KAAA,KAAU,MAAM,MAAA,KAAW,IAAI,CAAC,CAAA,CAAE,MAAA;AACzE,IAAA,OAAO,KAAA,CACJ,KAAK,CAAC,GAAA,EAAK,UAAU,KAAA,CAAM,GAAG,IAAI,KAAA,CAAM,KAAK,CAAC,CAAA,CAC9C,GAAA,CAAI,CAAC,EAAE,MAAA,EAAQ,SAAS,QAAA,EAAU,MAAA,IAAU,KAAA,MAAW;AAAA,MACtD,KAAA,EAAO,IAAA;AAAA,MACP,MAAA;AAAA,MACA,OAAA;AAAA,MACA,QAAA;AAAA,MACA,MAAA,EAAQ,UAAU,KAAA,GAAQ;AAAA,KAC5B,CAAE,CAAA;AAAA,EACN;AAAA,EAGA,MAAM,KAAA,EAAmD;AACvD,IAAA,MAAM,OACJ,KAAA,CAAM,IAAA,CAAK,CAAC,IAAA,KAAS,IAAA,CAAK,UAAU,IAAI,CAAA,IACxC,MAAM,IAAA,CAAK,CAAC,SAAS,IAAA,CAAK,KAAA,KAAU,QAAQ,IAAA,CAAK,MAAA,KAAW,KAAK,OAAO,CAAA;AAC1E,IAAA,IAAI,IAAA,OAAW,KAAA,GAAQ,IAAA;AACvB,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAGA,QAAA,CAAS,SAAY,KAAA,EAAuC;AAC1D,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,YAAA,CAAa,IAAA,CAAK,UAAU,CAAA;AACpD,IAAA,MAAM,GAAA,GAAM,KAAA,CAAM,EAAA,CAAG,EAAE,CAAA;AACvB,IAAA,MAAM,MAAA,GAAS,MAAM,CAAC,CAAA;AACtB,IAAA,MAAM,IAAA,GAAO,GAAA,EAAK,OAAA,KAAY,OAAA,GAAU,GAAA,GAAM,MAAA;AAC9C,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,GAAQ,MAAA,GAAS,IAAA;AACnC,IAAA,MAAM,IAAA,GAAsB;AAAA,MAC1B,KAAA,EAAO,IAAA;AAAA,MACP,QAAQ,IAAA,CAAK,OAAA;AAAA,MACb,OAAA,EAAS,OAAA;AAAA,MACT,QAAA,EAAU,IAAA,GAAO,IAAA,CAAK,QAAA,GAAW,OAAA;AAAA,MACjC,MAAA,EAAQ,CAAC,IAAA,CAAK,KAAA,IAAS,IAAA,KAAS;AAAA,KAClC;AACA,IAAA,IAAI,IAAA,CAAK,SAAS,MAAA,EAAQ;AACxB,MAAA,MAAA,CAAO,MAAA,GAAS,IAAA;AAChB,MAAA,KAAA,CAAM,QAAQ,IAAI,CAAA;AAAA,IACpB,CAAA,MAAO,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA;AACtB,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAGA,QAAA,CAAS,OAAA,EAAY,KAAA,EAAiC,IAAA,EAA8B;AAClF,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,YAAA,CAAa,IAAA,CAAK,UAAU,CAAA;AACpD,IAAA,OAAO,KAAA,CAAM,KAAA,CAAM,KAAA,CAAM,OAAA,CAAQ,IAAI,CAAA,GAAI,CAAC,CAAA,CAAE,IAAA,CAAK,CAAC,KAAA,KAAU,KAAA,CAAM,YAAY,OAAO,CAAA;AAAA,EACvF;AAAA,EAGA,KAAA,CAAM,SAAY,KAAA,EAAuC;AACvD,IAAA,MAAM,OAAA,uBAAc,GAAA,CAA2B,CAAC,CAAC,IAAA,CAAK,OAAA,EAAS,IAAI,CAAC,CAAC,CAAA;AACrE,IAAA,KAAA,CAAM,OAAA,CAAQ,CAAC,IAAA,EAAM,KAAA,KAAU;AAC7B,MAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,CAAA,EAAG,KAAK,EAAE,GAAA,CAAI,CAAC,KAAA,KAAU,KAAA,CAAM,MAAM,CAAA;AAC/D,MAAA,MAAM,OAAA,GAAU,CAAC,GAAG,IAAI,IAAI,KAAA,CAAM,OAAA,EAAS,CAAC,EAAE,MAAA,CAAO,CAAC,IAAA,KAAS,IAAA,KAAS,KAAK,MAAM,CAAA;AACnF,MAAA,MAAM,MAAA,GAAS,KAAK,MAAA,IAAU,OAAA,CAAQ,WAAW,CAAA,GAAI,EAAC,GAAI,CAAC,KAAK,CAAA;AAChE,MAAA,OAAA,CAAQ,GAAA;AAAA,QACN,IAAA,CAAK,MAAA;AAAA,QACL,IAAA,CAAK,YAAY,IAAA,CAAK,QAAA,IAAY,QAAQ,MAAA,KAAW,CAAA,IAAK,KAAA,KAAU,KAAA,CAAM,MAAA,GAAS,CAAA,GAC/E,OACA,IAAA,CAAK,SAAA,CAAU,CAAC,IAAA,CAAK,QAAA,EAAU,IAAA,CAAK,SAAS,GAAG,MAAA,EAAQ,GAAG,OAAO,CAAC;AAAA,OACzE;AAAA,IACF,CAAC,CAAA;AACD,IAAA,KAAA,MAAW,CAAC,IAAA,EAAM,QAAQ,CAAA,IAAK,OAAA,EAAS;AACtC,MAAA,IAAI,OAAA,CAAQ,YAAA,CAAa,IAAI,CAAA,KAAM,QAAA,EAAU;AAC7C,MAAA,IAAI,QAAA,KAAa,IAAA,EAAM,OAAA,CAAQ,eAAA,CAAgB,IAAI,CAAA;AAAA,WAC9C,OAAA,CAAQ,YAAA,CAAa,IAAA,EAAM,QAAQ,CAAA;AAAA,IAC1C;AAAA,EACF;AAAA,EAGA,QAAA,CAAS,SAAY,KAAA,EAA4B;AAC/C,IAAA,IAAI,OAAA,CAAQ,YAAA,CAAa,IAAA,CAAK,UAAU,MAAM,KAAA,EAAO;AACrD,IAAA,IAAI,KAAA,KAAU,IAAA,EAAM,OAAA,CAAQ,eAAA,CAAgB,KAAK,UAAU,CAAA;AAAA,SACtD,OAAA,CAAQ,YAAA,CAAa,IAAA,CAAK,UAAA,EAAY,KAAK,CAAA;AAAA,EAClD;AACF,CAAA;;AC9OO,IAAM,cAAN,MAAkB;AAAA,EAEvB,MAAA,GAAS,IAAI,eAAA,EAAgB;AAAA,EAS7B,GAAA,CACE,MAAA,EACA,IAAA,EACA,OAAA,EACA,OAAA,EACM;AACN,IAAA,MAAA,CAAO,gBAAA,CAAiB,IAAA,EAAM,OAAA,EAAS,EAAE,GAAG,SAAS,MAAA,EAAQ,IAAA,CAAK,MAAA,CAAO,MAAA,EAAQ,CAAA;AAAA,EACnF;AAAA,EAMA,OAAA,GAAgB;AACd,IAAA,IAAA,CAAK,OAAO,KAAA,EAAM;AAClB,IAAA,IAAA,CAAK,MAAA,GAAS,IAAI,eAAA,EAAgB;AAAA,EACpC;AACF,CAAA;;AC7BO,IAAM,qBAAN,MAAyB;AAAA,EACrB,IAAA;AAAA,EACT,OAAA,GAAU,KAAA;AAAA,EACV,OAAA,GAAU,KAAA;AAAA,EACV,WAAA,GAAc,CAAA;AAAA,EAGd,YAAY,GAAA,EAAiB;AAC3B,IAAA,IAAA,CAAK,IAAA,GAAO,GAAA;AAAA,EACd;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AAAA,EACjB;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,IAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,IAAA,IAAA,CAAK,WAAA,IAAe,CAAA;AAAA,EACtB;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,IAAI,CAAC,IAAA,CAAK,OAAA,IAAW,IAAA,CAAK,OAAA,EAAS;AACnC,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AACf,IAAA,MAAM,aAAa,IAAA,CAAK,WAAA;AACxB,IAAA,cAAA,CAAe,MAAM;AAEnB,MAAA,IAAI,UAAA,KAAe,KAAK,WAAA,IAAe,CAAC,KAAK,OAAA,IAAW,CAAC,KAAK,OAAA,EAAS;AACvE,MAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,MAAA,IAAA,CAAK,IAAA,EAAK;AAAA,IACZ,CAAC,CAAA;AAAA,EACH;AACF,CAAA;;ACxEO,IAAM,qBAAN,MAAyB;AAAA,EACrB,UAAA,GAAa,IAAI,WAAA,EAAY;AAAA,EAC7B,KAAA;AAAA,EACT,QAAA,GAA2B,IAAA;AAAA,EAClB,QAAA,GAAW,MAAY,IAAA,CAAK,KAAA,CAAM,QAAA,EAAS;AAAA,EAGpD,YAAY,GAAA,EAAiB;AAC3B,IAAA,IAAA,CAAK,KAAA,GAAQ,IAAI,kBAAA,CAAmB,GAAG,CAAA;AAAA,EACzC;AAAA,EAGA,QAAQ,OAAA,EAAwB;AAC9B,IAAA,IAAI,IAAA,CAAK,aAAa,OAAA,EAAS;AAC/B,IAAA,IAAA,CAAK,UAAA,EAAW;AAChB,IAAA,IAAA,CAAK,QAAA,GAAW,OAAA;AAChB,IAAA,IAAA,CAAK,MAAM,QAAA,EAAS;AACpB,IAAA,IAAA,CAAK,UAAA,CAAW,GAAA,CAAI,OAAA,EAAS,qBAAA,EAAuB,KAAK,QAAQ,CAAA;AAAA,EACnE;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,IAAA,CAAK,MAAM,QAAA,EAAS;AAAA,EACtB;AAAA,EAGA,UAAA,GAAmB;AACjB,IAAA,IAAA,CAAK,WAAW,OAAA,EAAQ;AACxB,IAAA,IAAA,CAAK,MAAM,MAAA,EAAO;AAClB,IAAA,IAAA,CAAK,QAAA,GAAW,IAAA;AAAA,EAClB;AACF,CAAA;;ACNO,IAAM,gBAAA,GAAN,cAA+B,UAAA,CAAwB;AAAA,EAC5D,OAAgB,OAAA,GAAU,CAAC,OAAA,EAAS,UAAU,CAAA;AAAA,EAC9C,OAAgB,MAAA,GAAS,EAAE,GAAA,EAAK,MAAA,EAAO;AAAA,EACvC,OAAO,OAAA,GAAU,CAAC,SAAA,EAAW,QAAQ,CAAA;AAAA,EACrC,OAAO,MAAA,GAAS,CAAC,OAAO,CAAA;AAAA,EASf,aAAa,IAAI,kBAAA,CAAmB,MAAM,IAAA,CAAK,SAAS,CAAA;AAAA,EAExD,IAAA,GAAO,IAAI,cAAA,CAAiC,KAAA,EAAO,KAAK,UAAU,CAAA;AAAA,EAClE,YAAA,GAAe,IAAI,cAAA,CAAiC,QAAA,EAAU,KAAK,UAAU,CAAA;AAAA,EAC7E,eAAA,GAAkB,IAAI,cAAA,CAA4B,QAAA,EAAU,KAAK,UAAU,CAAA;AAAA,EAEpF,UAAA,GAAa,KAAA;AAAA,EAEJ,YAAA,GAAe,IAAI,gBAAA,CAAiB,CAAC,OAAA,KAAY;AACxD,IAAA,IAAI,OAAA,CAAQ,IAAA,CAAK,CAAC,MAAA,KAAW,IAAA,CAAK,kBAAkB,MAAA,CAAO,MAAA,KAAW,IAAA,CAAK,WAAW,CAAA,EAAG;AACvF,MAAA,IAAA,CAAK,WAAW,QAAA,EAAS;AAAA,IAC3B;AAAA,EACF,CAAC,CAAA;AAAA,EAGQ,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAClB,IAAA,IAAA,CAAK,UAAA,CAAW,OAAA,CAAQ,IAAA,CAAK,OAAO,CAAA;AACpC,IAAA,IAAA,CAAK,YAAA,CAAa,OAAA,CAAQ,IAAA,CAAK,OAAA,EAAS;AAAA,MACtC,UAAA,EAAY,IAAA;AAAA,MACZ,eAAA,EAAiB,CAAC,KAAK,CAAA;AAAA,MACvB,OAAA,EAAS;AAAA,KACV,CAAA;AACD,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA,EAGS,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,UAAA,GAAa,KAAA;AAClB,IAAA,IAAA,CAAK,WAAW,UAAA,EAAW;AAC3B,IAAA,IAAA,CAAK,aAAa,UAAA,EAAW;AAAA,EAC/B;AAAA,EAGA,eAAA,GAAwB;AACtB,IAAA,IAAA,CAAK,WAAW,QAAA,EAAS;AAAA,EAC3B;AAAA,EAGA,oBAAA,GAA6B;AAC3B,IAAA,IAAA,CAAK,WAAW,QAAA,EAAS;AAAA,EAC3B;AAAA,EAGA,wBAAwB,KAAA,EAA+B;AACrD,IAAA,IAAI,KAAK,UAAA,EAAY;AACnB,MAAA,IAAA,CAAK,IAAA,CAAK,OAAO,KAAK,CAAA;AACtB,MAAA,IAAA,CAAK,YAAA,CAAa,OAAO,KAAK,CAAA;AAAA,IAChC;AACA,IAAA,IAAA,CAAK,WAAW,QAAA,EAAS;AAAA,EAC3B;AAAA,EAGA,uBAAA,GAAgC;AAC9B,IAAA,IAAA,CAAK,WAAW,QAAA,EAAS;AAAA,EAC3B;AAAA,EAGA,2BAA2B,QAAA,EAA6B;AACtD,IAAA,IAAI,IAAA,CAAK,UAAA,EAAY,IAAA,CAAK,eAAA,CAAgB,OAAO,QAAQ,CAAA;AACzD,IAAA,IAAA,CAAK,WAAW,QAAA,EAAS;AAAA,EAC3B;AAAA,EAGA,OAAO,KAAA,EAAoB;AACzB,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,WAAA,CAAY,KAAK,CAAA;AACpC,IAAA,IAAI,CAAC,KAAA,EAAO;AACZ,IAAA,IAAI,CAAC,KAAA,CAAM,YAAA,CAAa,KAAK,CAAA,EAAG;AAC9B,MAAA,IAAA,CAAK,OAAA,EAAQ;AACb,MAAA;AAAA,IACF;AACA,IAAA,IAAA,CAAK,SAAS,QAAQ,CAAA;AAAA,EACxB;AAAA,EAQA,QAAQ,KAAA,EAAoB;AAC1B,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,WAAA,CAAY,KAAK,CAAA;AACpC,IAAA,IAAI,CAAC,KAAA,EAAO;AACZ,IAAA,MAAM,GAAA,GAAM,KAAA,CAAM,YAAA,CAAa,KAAK,CAAA;AACpC,IAAA,IAAI,CAAC,GAAA,EAAK;AACR,MAAA,IAAA,CAAK,OAAA,EAAQ;AACb,MAAA;AAAA,IACF;AACA,IAAA,IAAA,CAAK,SAAS,OAAO,CAAA;AACrB,IAAA,IAAA,CAAK,SAAS,OAAA,EAAS,EAAE,QAAQ,EAAE,GAAA,IAAO,CAAA;AAAA,EAC5C;AAAA,EAUA,OAAA,GAAgB;AACd,IAAA,IAAI,CAAC,KAAK,cAAA,EAAgB;AACxB,MAAA,IAAA,CAAK,SAAS,OAAO,CAAA;AACrB,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,QAAQ,IAAA,CAAK,WAAA;AACnB,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,YAAA,CAAa,KAAK,CAAA;AACnC,IAAA,IAAI,CAAC,GAAA,EAAK;AACR,MAAA,IAAA,CAAK,SAAS,OAAO,CAAA;AACrB,MAAA;AAAA,IACF;AACA,IAAA,IAAI,CAAC,MAAM,QAAA,EAAU;AACnB,MAAA,IAAA,CAAK,SAAS,SAAS,CAAA;AACvB,MAAA;AAAA,IACF;AACA,IAAA,IAAA,CAAK,QAAA,CAAS,KAAA,CAAM,YAAA,GAAe,CAAA,GAAI,WAAW,OAAO,CAAA;AAAA,EAC3D;AAAA,EAGA,aAAa,KAAA,EAAwC;AACnD,IAAA,IAAI,CAAC,KAAK,OAAA,CAAQ,YAAA,CAAa,QAAQ,IAAA,CAAK,UAAU,YAAY,CAAA,EAAG;AACnE,MAAA,IAAA,CAAK,IAAA,CAAK,OAAO,KAAK,CAAA;AACtB,MAAA,OAAO,KAAA,CAAM,aAAa,KAAK,CAAA;AAAA,IACjC;AAEA,IAAA,MAAM,UAAU,IAAA,CAAK,QAAA,CAAS,MAAA,GAAS,CAAA,GAAI,KAAK,QAAA,GAAW,IAAA;AAC3D,IAAA,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,KAAA,EAAO,OAAO,CAAA;AAC9B,IAAA,OAAO,OAAA;AAAA,EACT;AAAA,EAGA,YAAY,KAAA,EAAuC;AACjD,IAAA,MAAM,YAAY,KAAA,CAAM,aAAA;AACxB,IAAA,IAAI,EAAE,SAAA,YAAqB,gBAAA,CAAA,EAAmB,OAAO,IAAA;AACrD,IAAA,IAAI,CAAC,IAAA,CAAK,cAAA,IAAkB,SAAA,KAAc,IAAA,CAAK,aAAa,OAAO,IAAA;AACnE,IAAA,OAAO,SAAA;AAAA,EACT;AAAA,EAGA,SAAS,KAAA,EAA0B;AACjC,IAAA,MAAM,SAAA,GAAY,KAAA,KAAU,SAAA,IAAa,KAAA,KAAU,QAAA;AACnD,IAAA,IAAI,KAAK,cAAA,EAAgB;AACvB,MAAA,IAAA,CAAK,aAAa,KAAA,CAAM,IAAA,CAAK,WAAA,EAAa,SAAA,GAAY,OAAO,EAAE,CAAA;AAAA,IACjE;AACA,IAAA,IAAI,KAAK,iBAAA,EAAmB;AAC1B,MAAA,IAAA,CAAK,gBAAgB,KAAA,CAAM,IAAA,CAAK,cAAA,EAAgB,SAAA,GAAY,KAAK,IAAI,CAAA;AAAA,IACvE;AACA,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,YAAA,EAAc,KAAK,CAAA;AAAA,EAC/C;AACF","file":"avatar_controller.js","sourcesContent":["/**\n * One registry per page for every copy of the library.\n *\n * Each `stimeo-ui/controllers/*` file, and each opt-in entry, is built on its own and carries\n * its own copy of every utility it uses, so a page that imports controllers one by one, or one\n * of them next to the barrel, evaluates a utility several times. A registry that coordinates\n * instances of different controllers — the stacks of leased values, the loans of a\n * `tabindex`, the Escape layers, the focus traps — must not be split by that: a dialog inside\n * a drawer, or a character counter and a form field on one input, coordinate whichever files\n * they came from. So such a registry lives once on `globalThis`, under a `Symbol.for` key the\n * copies agree on, and the first copy to ask for it creates it.\n *\n * The key names the registry and the version of its shape\n * (`stimeo-ui.<utility>.registry.v<n>`). Copies whose registries have the same shape share\n * one; a release that changes the shape changes the version, so a copy of another release\n * keeps a registry of its own instead of reading entries it does not understand — the two\n * then coordinate within each release only. An entry another copy put in a registry is read\n * through its fields alone, never through `#private` members, which belong to the class of\n * the copy that created it.\n */\n\n/**\n * The registry stored under `key`, created by `create` on first use.\n *\n * @param key - `stimeo-ui.<utility>.registry.v<n>`: the version is that of the shape `create`\n * returns.\n * @param create - Builds the empty registry.\n */\nexport function sharedRegistry<T extends object>(key: string, create: () => T): T {\n const symbol = Symbol.for(key);\n const scope = globalThis as unknown as Record<symbol, T | undefined>;\n const existing = scope[symbol];\n if (existing !== undefined) return existing;\n const registry = create();\n Object.defineProperty(globalThis, symbol, { value: registry });\n return registry;\n}\n","import { sharedRegistry } from \"./shared_registry\";\n\n/** What a lease's record says: the author's value, its last write, the leases beneath. */\ninterface AttributeLeaseRecord {\n readonly original: string | null;\n readonly written: string | null;\n /** Whether it hands back what the lease right beneath it wrote last. */\n readonly linked: boolean;\n /** The records of the other owners' leases beneath it, nearest first. */\n readonly beneath: readonly string[];\n}\n\n/** One lease's hold on one element's attribute, in the stack every lease of it shares. */\ninterface AttributeHold {\n /** The lease holding it; `null` for a hold read from a copy's record until its lease takes it. */\n lease: object | null;\n readonly record: string;\n written: string | null;\n /** What it hands back with nothing beneath it: the author's value. */\n readonly original: string | null;\n /** Whether it hands back what the hold beneath it wrote last instead. */\n linked: boolean;\n}\n\n/** The holds on each element by attribute, and every lease's record name by attribute. */\ninterface AttributeLeaseRegistry {\n readonly stacks: WeakMap<Element, Map<string, AttributeHold[]>>;\n readonly names: Map<string, Set<string>>;\n}\n\n/** Whether a parsed record entry is an attribute value: a string, or `null` for absence. */\nfunction isAttributeValue(value: unknown): value is string | null {\n return value === null || typeof value === \"string\";\n}\n\n/**\n * Reads the record a lease left on `element`: the author's value, the value the lease wrote\n * last, whether it hands back the value beneath it (`false` before the names when it does not)\n * and the records beneath it. `undefined` when the record is missing or holds anything else.\n */\nfunction readLeaseRecord(element: Element, record: string): AttributeLeaseRecord | undefined {\n try {\n const parsed: unknown = JSON.parse(element.getAttribute(record) ?? \"null\");\n if (!Array.isArray(parsed)) return undefined;\n const [original, written, ...rest] = parsed as unknown[];\n const linked = rest[0] !== false;\n const beneath = linked ? rest : rest.slice(1);\n if (!beneath.every((under): under is string => typeof under === \"string\")) return undefined;\n return isAttributeValue(original) && isAttributeValue(written)\n ? { original, written, linked, beneath }\n : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * The value `element`'s author wrote for `attribute`, as the record a lease of any owner leaves\n * on the element carries it; `undefined` while no lease leaves one, as when nothing has moved\n * the attribute off the author's value. A field a reveal turned from `password` into `text`\n * still reads `\"password\"` here.\n */\nexport function leasedAuthorValue(element: Element, attribute: string): string | null | undefined {\n const suffix = `-${attribute}-lease`;\n for (const name of element.getAttributeNames()) {\n if (!name.startsWith(\"data-\") || !name.endsWith(suffix)) continue;\n const record = readLeaseRecord(element, name);\n if (record) return record.original;\n }\n return undefined;\n}\n\n/**\n * Temporarily controls one attribute across a changing set of elements.\n *\n * The first write remembers the authored value, including the distinction between\n * an absent attribute and an authored empty string. Returning a lease restores that\n * value only while the attribute still matches the controller's last write. If a\n * consumer changed it in the meantime, the consumer owns the new value and teardown\n * leaves it alone.\n *\n * A `null` write deliberately removes the attribute while retaining the lease. This\n * is useful for derived ARIA whose valid absence is itself controller state, such as\n * an unbounded `aria-valuemin` or a blank spinbutton's `aria-valuenow`.\n *\n * **Every lease of an attribute shares one stack of holds per element**, whatever its owner\n * and whichever file of the package constructed it (the stacks are one per page): the holds\n * sit in the order their leases first wrote the element, each with the value it wrote last.\n * The element's value is the highest hold's that carries it, and the page's when none does.\n * A write reaches the element unless a hold above the writer carries the element's value: the\n * top hold always shows its writes, a hold beneath it shows nothing while a hold above keeps\n * its value on the element, and once the page has put a value there that no hold carries, the\n * next write of any hold replaces it. A return gives a value back only from the hold the\n * element's value is: the value the hold beneath it wrote last, or the author's once nothing\n * is beneath it; any other return only leaves the stack, and the page's value stays. Holds\n * that wrote the same value are told apart by the stack, never by the value: the higher one\n * keeps it. A first write over a top hold whose value the page has replaced takes the\n * element's value as the author's. Two instances of one controller on one element — an\n * element outside their scope — stack the same way.\n *\n * A **base lease** (`{ base: true }`) holds the element's own state rather than a value over\n * it — a form field's ARIA on its control is one — so its first write goes to the bottom of\n * the stack, beneath the leases of other owners already there: a temporary value another\n * owner leased stays shown and returns to the base lease's latest value.\n *\n * **The stack travels with the element.** While a hold shows a value other than the author's,\n * lies on another hold or is covered by one, the element carries its owner's record\n * `data-<owner>-<attribute>-lease`: JSON of the author's value and the value the hold wrote\n * last (each a string, or `null` for an absent attribute), `false` where the hold hands back\n * the author's value rather than the latest value of the hold beneath it, and the records of\n * every hold beneath it, nearest first. Returning the last lease of an owner removes its\n * record. A copy of the element — a page Turbo restores from its cache is one — therefore\n * carries the stack, and the first lease to touch the copy reads it back under the record\n * names of the leases constructed so far, which include every lease that wrote a record the\n * cache holds, and orders the holds by the records each one lists beneath it. Each lease takes\n * the hold its owner's record describes over in place. Complete records for distinct owners\n * preserve their order across a copy. The records describe holds by owner, not by instance:\n * one owner holding the element more than once leaves only its topmost hold's record, which\n * the first of its leases to write or return on the copy takes. Lower holds of that owner and\n * their positions are absent; mutually listed owners cannot express their full live order.\n * A hold that showed the author's value alone carries no record and is taken by its first\n * write on the copy. Missing or unreadable records also leave their holds unknown, so copying\n * the DOM does not promise identical decisions for every live stack. Without a record it can\n * read, a new stack takes the current value as the author's. A Turbo morph removes records\n * absent from the server HTML; existing holds remain in memory until their leases return.\n *\n * The lease has no lifecycle of its own, so it never subscribes to document events,\n * and nothing rewinds a live page before Turbo caches it: `turbo:before-cache` also\n * fires on pages that stay.\n */\nexport class AttributeLease<T extends Element = Element> {\n /** The holds on each element, bottom first, and the record names; one per page. */\n static readonly #registry = sharedRegistry(\n \"stimeo-ui.attribute-lease.registry.v1\",\n (): AttributeLeaseRegistry => ({ stacks: new WeakMap(), names: new Map() }),\n );\n readonly #attribute: string;\n readonly #record: string;\n readonly #base: boolean;\n readonly #held = new Set<T>();\n\n /**\n * @param attribute - The attribute whose temporary values this lease owns.\n * @param owner - Names the record on the element; the controller's identifier.\n * @param options - `base`: written beneath the other owners' leases.\n */\n constructor(attribute: string, owner: string, options: { readonly base?: boolean } = {}) {\n this.#attribute = attribute;\n this.#record = `data-${owner}-${attribute}-lease`;\n this.#base = options.base === true;\n const names = AttributeLease.#registry.names;\n names.set(attribute, (names.get(attribute) ?? new Set<string>()).add(this.#record));\n }\n\n /** Writes or removes the leased attribute while preserving its authored value. */\n write(element: T, value: string | null): void {\n const stack = this.#stack(element);\n const hold = this.#take(stack) ?? this.#acquire(element, stack);\n this.#held.add(element);\n const shows = !this.#covered(element, stack, hold);\n hold.written = value;\n this.#mark(element, stack);\n if (shows) this.#reflect(element, value);\n }\n\n /**\n * Returns one lease, also on a restored copy, to the value the hold beneath it wrote last or\n * the author's, without overwriting a value a consumer wrote since. Says whether there was a\n * lease to return: a hold this lease has, or the hold a copy's record describes while no live\n * lease of the owner has taken it — a copy of a page an earlier connection wrote carries one.\n */\n return(element: T): boolean {\n const held = this.#held.delete(element);\n const known = AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);\n const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];\n const hold = this.#take(stack);\n if (!hold) return false;\n const carries = element.getAttribute(this.#attribute) === hold.written;\n const owns = carries && !this.#covered(element, stack, hold);\n const index = stack.indexOf(hold);\n stack.splice(index, 1);\n const above = stack[index];\n if (above) above.linked &&= hold.linked;\n this.#mark(element, stack);\n if (!owns) return true;\n const below = hold.linked ? stack[index - 1] : undefined;\n this.#reflect(element, below ? below.written : hold.original);\n return true;\n }\n\n /** Returns every lease it holds, with the same ownership check as {@link return}. */\n returnAll(): void {\n for (const element of Array.from(this.#held)) this.return(element);\n }\n\n /** The element's stack, read from its records the first time. */\n #stack(element: T): AttributeHold[] {\n const stacks = AttributeLease.#registry.stacks;\n const byAttribute = stacks.get(element) ?? new Map<string, AttributeHold[]>();\n stacks.set(element, byAttribute);\n const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);\n byAttribute.set(this.#attribute, stack);\n return stack;\n }\n\n /** The holds a copy's records describe, each above every hold its record lists beneath. */\n #seed(element: T): AttributeHold[] {\n const found: (AttributeLeaseRecord & { readonly record: string })[] = [];\n for (const record of AttributeLease.#registry.names.get(this.#attribute) ?? []) {\n const read = readLeaseRecord(element, record);\n if (read) found.push({ ...read, record });\n }\n const depth = ({ beneath }: (typeof found)[number]) =>\n beneath.filter((name) => found.some((other) => other.record === name)).length;\n return found\n .sort((one, other) => depth(one) - depth(other))\n .map(({ record, written, original, linked }, index) => ({\n lease: null,\n record,\n written,\n original,\n linked: linked && index > 0,\n }));\n }\n\n /** Its hold in `stack`, taking over one read from its record. */\n #take(stack: AttributeHold[]): AttributeHold | undefined {\n const hold =\n stack.find((held) => held.lease === this) ??\n stack.find((held) => held.lease === null && held.record === this.#record);\n if (hold) hold.lease = this;\n return hold;\n }\n\n /** A new hold: on top, over the top hold while the element shows it; a base one at the bottom. */\n #acquire(element: T, stack: AttributeHold[]): AttributeHold {\n const current = element.getAttribute(this.#attribute);\n const top = stack.at(-1);\n const bottom = stack[0];\n const over = top?.written === current ? top : undefined;\n const from = this.#base ? bottom : over;\n const hold: AttributeHold = {\n lease: this,\n record: this.#record,\n written: current,\n original: from ? from.original : current,\n linked: !this.#base && over !== undefined,\n };\n if (this.#base && bottom) {\n bottom.linked = true;\n stack.unshift(hold);\n } else stack.push(hold);\n return hold;\n }\n\n /** Whether a hold above `hold` carries the element's value, which is then that hold's. */\n #covered(element: T, stack: readonly AttributeHold[], hold: AttributeHold): boolean {\n const current = element.getAttribute(this.#attribute);\n return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);\n }\n\n /** Leaves each owner's record of its topmost hold, or none where nothing needs one. */\n #mark(element: T, stack: readonly AttributeHold[]): void {\n const records = new Map<string, string | null>([[this.#record, null]]);\n stack.forEach((hold, index) => {\n const names = stack.slice(0, index).map((under) => under.record);\n const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);\n const marker = hold.linked || beneath.length === 0 ? [] : [false];\n records.set(\n hold.record,\n hold.written === hold.original && beneath.length === 0 && index === stack.length - 1\n ? null\n : JSON.stringify([hold.original, hold.written, ...marker, ...beneath]),\n );\n });\n for (const [name, recorded] of records) {\n if (element.getAttribute(name) === recorded) continue;\n if (recorded === null) element.removeAttribute(name);\n else element.setAttribute(name, recorded);\n }\n }\n\n /** Reflects only a real value transition, avoiding self-triggered mutation work. */\n #reflect(element: T, value: string | null): void {\n if (element.getAttribute(this.#attribute) === value) return;\n if (value === null) element.removeAttribute(this.#attribute);\n else element.setAttribute(this.#attribute, value);\n }\n}\n","/**\n * Owns every listener a controller attaches for one connected lifetime and\n * releases them together, so an event name is spelled once and the release\n * cannot drift from the registration.\n *\n * Detaching by hand repeats three things at the release site — the type, the\n * handler reference and the capture flag — and any one of them drifting leaves\n * a listener alive on an element the controller no longer drives. Aborting the\n * signal a registration was created with releases it whatever it was made of,\n * so the release site names nothing.\n *\n * One set per lifetime, held as a field and never rebuilt. {@link\n * ListenerSet.dispose} is the only thing that closes a generation, and it opens\n * the next one in the same step, so the lifecycle hooks can call `add` and\n * `dispose` in any order any number of times, and no `add` ever meets an\n * already aborted signal — a DOM that registers a listener on such a signal\n * would keep it forever. Rebuilding the set instead would leak:\n * `addEventListener` matches an existing registration on target, type, callback\n * and capture alone, so re-adding the same handler while the first registration\n * is live is discarded, and that live registration keeps the signal it was\n * attached with — a set built by a second `connect()` can never release what it\n * appears to own.\n *\n * Scope is the registration only. Timers, observers, leases and the order they\n * are unwound in stay with the controller, which places `dispose()` in its\n * cleanup sequence. A receiver that changes at runtime (a\n * swapped target) is rebound by calling `dispose()` and then `add()` again from\n * the rebind, not by a second set.\n *\n * The set must own every registration of a tuple it holds: a listener the same\n * target already carries for the same type, callback and capture flag keeps the\n * right to remove it, and this set's `add` for that tuple is discarded. A\n * subscription shared between instances behind a participant count, or one that\n * lives for less than the connection, keeps its own pair of calls.\n *\n * @example\n * ```ts\n * readonly #listeners = new ListenerSet();\n *\n * connect(): void {\n * this.#listeners.add(this.element, \"submit\", this.#onNativeSubmit, { capture: true });\n * this.#listeners.add(this.element, \"turbo:submit-start\", this.#onSubmitStart);\n * this.#listeners.add(this.element, \"turbo:submit-end\", this.#onSubmitEnd);\n * }\n *\n * disconnect(): void {\n * this.#listeners.dispose();\n * }\n * ```\n */\nexport class ListenerSet {\n /** The generation every `add` joins until the next `dispose()`. */\n #abort = new AbortController();\n\n /**\n * Attaches `handler` to the open generation, exactly as the caller spelled it.\n *\n * The set supplies the signal, so `options` carries everything else the DOM\n * accepts — `capture` included, which has to match at release time and no\n * longer has a second place to drift from.\n */\n add(\n target: EventTarget,\n type: string,\n handler: EventListener,\n options?: Omit<AddEventListenerOptions, \"signal\">,\n ): void {\n target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });\n }\n\n /**\n * Releases every listener of the open generation, synchronously, and opens the\n * next one. Idempotent, and safe before anything has been added.\n */\n dispose(): void {\n this.#abort.abort();\n this.#abort = new AbortController();\n }\n}\n","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","import { ListenerSet } from \"./listener_set\";\nimport { MicrotaskCoalescer } from \"./microtask_coalescer\";\n\n/**\n * Coalesces retained-element morphs and explicit lifecycle requests into one pass.\n *\n * Observe the controller's element after its initial state is settled. Morphs on\n * that element and bubbling morphs on its descendants share the same microtask\n * as {@link schedule}. The callback repairs derived output; the controller owns\n * any logical transition reporting. {@link disconnect} releases the listener and\n * cancels pending work, including work from an earlier connected lifetime.\n */\nexport class MorphRenderWatcher {\n readonly #listeners = new ListenerSet();\n readonly #pass: MicrotaskCoalescer;\n #element: Element | null = null;\n readonly #onMorph = (): void => this.#pass.schedule();\n\n /** @param run - the single pass that repairs the settled controller's output. */\n constructor(run: () => void) {\n this.#pass = new MicrotaskCoalescer(run);\n }\n\n /** Opens observation; observing the same element again preserves queued work. */\n observe(element: Element): void {\n if (this.#element === element) return;\n this.disconnect();\n this.#element = element;\n this.#pass.activate();\n this.#listeners.add(element, \"turbo:morph-element\", this.#onMorph);\n }\n\n /** Requests one pass; requests outside an observed lifetime are ignored. */\n schedule(): void {\n this.#pass.schedule();\n }\n\n /** Releases observation and pending work synchronously; safe to repeat. */\n disconnect(): void {\n this.#listeners.dispose();\n this.#pass.cancel();\n this.#element = null;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { AttributeLease } from \"../utils/attribute_lease\";\nimport { MorphRenderWatcher } from \"../utils/morph_render_watcher\";\n\n/** Public rendering phases exposed through the root's `data-state`. */\ntype AvatarState = \"empty\" | \"loading\" | \"loaded\" | \"error\";\n\n/**\n * Headless **Avatar** behavior with image-load fallback. No dedicated APG\n * pattern; it follows the non-text-content practice (WCAG 1.1.1).\n *\n * Markup contract (identifier: `stimeo--avatar`):\n * <span data-controller=\"stimeo--avatar\" role=\"img\" aria-label=\"Jane Doe\"\n * data-stimeo--avatar-src-value=\"/u/123.jpg\">\n * <img alt=\"\" aria-hidden=\"true\"\n * data-stimeo--avatar-target=\"image\"\n * data-action=\"load->stimeo--avatar#onLoad error->stimeo--avatar#onError\" />\n * <span aria-hidden=\"true\" hidden data-stimeo--avatar-target=\"fallback\">JD</span>\n * </span>\n *\n * The `image` and `fallback` targets are optional. A present `src` Value owns the\n * current image's `src`, including an empty Value that explicitly clears it. When\n * the Value is absent, a directly authored `<img src>` is used instead. Runtime\n * Value changes, direct `src` changes, and target replacements all repaint.\n *\n * The accessible name lives on the container (`role=\"img\"` + `aria-label`); the\n * inner image and fallback are `aria-hidden` so assistive technology reads the\n * name once in every visual state.\n *\n * @remarks\n * Behavior only — shape, size, and colour are the consumer's CSS. Initials or\n * colour generation are out of scope: fallback content comes from markup. The\n * `empty`, `loading`, `loaded`, and `error` phases are exposed on `data-state`.\n * A native error from the current image dispatches `stimeo--avatar:error` with\n * `{ src: string }`; reconnecting or reconciling an already-broken image is\n * silent because no new failure event occurred.\n */\nexport class AvatarController extends Controller<HTMLElement> {\n static override targets = [\"image\", \"fallback\"];\n static override values = { src: String };\n static actions = [\"onError\", \"onLoad\"] as const;\n static events = [\"error\"] as const;\n\n declare readonly imageTarget: HTMLImageElement;\n declare readonly fallbackTarget: HTMLElement;\n declare readonly hasImageTarget: boolean;\n declare readonly hasFallbackTarget: boolean;\n declare srcValue: string;\n\n /** Collapses one morph's Value, target, and direct-attribute signals into one pass. */\n readonly #reconcile = new MorphRenderWatcher(() => this.#render());\n /** Preserves directly-authored source and visibility while targets are controlled. */\n readonly #src = new AttributeLease<HTMLImageElement>(\"src\", this.identifier);\n readonly #imageHidden = new AttributeLease<HTMLImageElement>(\"hidden\", this.identifier);\n readonly #fallbackHidden = new AttributeLease<HTMLElement>(\"hidden\", this.identifier);\n /** True only while target departure means ownership was actually relinquished. */\n #connected = false;\n /** Watches the non-Value render input used by the direct `<img src>` form. */\n readonly #srcObserver = new MutationObserver((records) => {\n if (records.some((record) => this.hasImageTarget && record.target === this.imageTarget)) {\n this.#reconcile.schedule();\n }\n });\n\n /** Starts retained-DOM observation and silently derives the current phase. */\n override connect(): void {\n this.#connected = true;\n this.#reconcile.observe(this.element);\n this.#srcObserver.observe(this.element, {\n attributes: true,\n attributeFilter: [\"src\"],\n subtree: true,\n });\n this.#render();\n }\n\n /** Cancels asynchronous work while retaining the last materialized visual state. */\n override disconnect(): void {\n this.#connected = false;\n this.#reconcile.disconnect();\n this.#srcObserver.disconnect();\n }\n\n /** Repaints when application code or a Turbo morph changes the source Value. */\n srcValueChanged(): void {\n this.#reconcile.schedule();\n }\n\n /** Applies the current source and phase to an image inserted or replaced at runtime. */\n imageTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Returns attributes borrowed from an image that leaves this live avatar. */\n imageTargetDisconnected(image: HTMLImageElement): void {\n if (this.#connected) {\n this.#src.return(image);\n this.#imageHidden.return(image);\n }\n this.#reconcile.schedule();\n }\n\n /** Applies the current phase to a fallback inserted or replaced at runtime. */\n fallbackTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Returns authored visibility when a fallback leaves this live avatar. */\n fallbackTargetDisconnected(fallback: HTMLElement): void {\n if (this.#connected) this.#fallbackHidden.return(fallback);\n this.#reconcile.schedule();\n }\n\n /** Reveals the current image after its native `load` event. */\n onLoad(event: Event): void {\n const image = this.#eventImage(event);\n if (!image) return;\n if (!image.getAttribute(\"src\")) {\n this.#render();\n return;\n }\n this.#reflect(\"loaded\");\n }\n\n /**\n * Reveals the fallback after the current image's native `error` event.\n *\n * Dispatches `stimeo--avatar:error` with `{ src: string }`, where `src` is\n * the attempted attribute value. Empty/stale/detached image events are ignored.\n */\n onError(event: Event): void {\n const image = this.#eventImage(event);\n if (!image) return;\n const src = image.getAttribute(\"src\");\n if (!src) {\n this.#render();\n return;\n }\n this.#reflect(\"error\");\n this.dispatch(\"error\", { detail: { src } });\n }\n\n /**\n * Materializes the phase from the final source/target DOM after one mutation batch.\n *\n * Cached success and failure are state reconstruction, not new load events, so\n * this pass never dispatches the public `error` event.\n *\n * @stimeoRenderRoot\n */\n #render(): void {\n if (!this.hasImageTarget) {\n this.#reflect(\"empty\");\n return;\n }\n\n const image = this.imageTarget;\n const src = this.#applySource(image);\n if (!src) {\n this.#reflect(\"empty\");\n return;\n }\n if (!image.complete) {\n this.#reflect(\"loading\");\n return;\n }\n this.#reflect(image.naturalWidth > 0 ? \"loaded\" : \"error\");\n }\n\n /** Applies Value precedence and returns the effective raw `src` attribute. */\n #applySource(image: HTMLImageElement): string | null {\n if (!this.element.hasAttribute(`data-${this.identifier}-src-value`)) {\n this.#src.return(image);\n return image.getAttribute(\"src\");\n }\n\n const desired = this.srcValue.length > 0 ? this.srcValue : null;\n this.#src.write(image, desired);\n return desired;\n }\n\n /** Accepts an action only from the singular image target currently controlled. */\n #eventImage(event: Event): HTMLImageElement | null {\n const candidate = event.currentTarget;\n if (!(candidate instanceof HTMLImageElement)) return null;\n if (!this.hasImageTarget || candidate !== this.imageTarget) return null;\n return candidate;\n }\n\n /** Writes the visibility pair and public root state as one controller-owned output. */\n #reflect(state: AvatarState): void {\n const showImage = state === \"loading\" || state === \"loaded\";\n if (this.hasImageTarget) {\n this.#imageHidden.write(this.imageTarget, showImage ? null : \"\");\n }\n if (this.hasFallbackTarget) {\n this.#fallbackHidden.write(this.fallbackTarget, showImage ? \"\" : null);\n }\n this.element.setAttribute(\"data-state\", state);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/attribute_write.ts","../../src/utils/shared_registry.ts","../../src/utils/attribute_lease.ts","../../src/utils/listener_set.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/morph_render_watcher.ts","../../src/controllers/avatar_controller.ts"],"names":["registry"],"mappings":";;AAaO,SAAS,qBAAA,CACd,OAAA,EACA,IAAA,EACA,KAAA,EACA,WAAA,EACS;AACT,EAAA,IAAI,OAAA,CAAQ,YAAA,CAAa,IAAI,CAAA,KAAM,OAAO,OAAO,KAAA;AAEjD,EAAA,IAAI,KAAA,KAAU,IAAA,EAAM,OAAA,CAAQ,eAAA,CAAgB,IAAI,CAAA;AAAA,OAC3C,OAAA,CAAQ,YAAA,CAAa,IAAA,EAAM,KAAK,CAAA;AACrC,EAAA,OAAO,IAAA;AACT;;ACIO,SAAS,cAAA,CAAiC,KAAa,MAAA,EAAoB;AAChF,EAAA,MAAM,MAAA,GAAS,MAAA,CAAO,GAAA,CAAI,GAAG,CAAA;AAC7B,EAAA,MAAM,KAAA,GAAQ,UAAA;AACd,EAAA,MAAM,QAAA,GAAW,MAAM,MAAM,CAAA;AAC7B,EAAA,IAAI,QAAA,KAAa,QAAW,OAAO,QAAA;AACnC,EAAA,MAAMA,YAAW,MAAA,EAAO;AACxB,EAAA,MAAA,CAAO,eAAe,UAAA,EAAY,MAAA,EAAQ,EAAE,KAAA,EAAOA,WAAU,CAAA;AAC7D,EAAA,OAAOA,SAAAA;AACT;;ACJA,IAAM,QAAA,GAAW,cAAA;AAAA,EACf,uCAAA;AAAA,EACA,OAA+B,EAAE,MAAA,kBAAQ,IAAI,SAAQ,EAAG,KAAA,kBAAO,IAAI,GAAA,EAAI,EAAE;AAC3E,CAAA;AAGA,SAAS,iBAAiB,KAAA,EAAwC;AAChE,EAAA,OAAO,KAAA,KAAU,IAAA,IAAQ,OAAO,KAAA,KAAU,QAAA;AAC5C;AAOA,SAAS,eAAA,CAAgB,SAAkB,MAAA,EAAkD;AAC3F,EAAA,IAAI;AACF,IAAA,MAAM,SAAkB,IAAA,CAAK,KAAA,CAAM,QAAQ,YAAA,CAAa,MAAM,KAAK,MAAM,CAAA;AACzE,IAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,MAAM,GAAG,OAAO,KAAA,CAAA;AACnC,IAAA,MAAM,CAAC,QAAA,EAAU,OAAA,EAAS,GAAG,IAAI,CAAA,GAAI,MAAA;AACrC,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,CAAC,CAAA,KAAM,KAAA;AAC3B,IAAA,MAAM,OAAA,GAAU,MAAA,GAAS,IAAA,GAAO,IAAA,CAAK,MAAM,CAAC,CAAA;AAC5C,IAAA,IAAI,CAAC,QAAQ,KAAA,CAAM,CAAC,UAA2B,OAAO,KAAA,KAAU,QAAQ,CAAA,EAAG,OAAO,KAAA,CAAA;AAClF,IAAA,OAAO,gBAAA,CAAiB,QAAQ,CAAA,IAAK,gBAAA,CAAiB,OAAO,CAAA,GACzD,EAAE,QAAA,EAAU,OAAA,EAAS,MAAA,EAAQ,OAAA,EAAQ,GACrC,KAAA,CAAA;AAAA,EACN,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,MAAA;AAAA,EACT;AACF;AAGA,SAAS,cAAA,CAAe,SAAkB,SAAA,EAAoC;AAC5E,EAAA,MAAM,cAAc,QAAA,CAAS,MAAA,CAAO,IAAI,OAAO,CAAA,wBAAS,GAAA,EAA6B;AACrF,EAAA,QAAA,CAAS,MAAA,CAAO,GAAA,CAAI,OAAA,EAAS,WAAW,CAAA;AACxC,EAAA,MAAM,KAAA,GAAQ,WAAA,CAAY,GAAA,CAAI,SAAS,CAAA;AACvC,EAAA,IAAI,OAAO,OAAO,KAAA;AAClB,EAAA,MAAM,QAAgE,EAAC;AACvE,EAAA,KAAA,MAAW,UAAU,QAAA,CAAS,KAAA,CAAM,IAAI,SAAS,CAAA,IAAK,EAAC,EAAG;AACxD,IAAA,MAAM,IAAA,GAAO,eAAA,CAAgB,OAAA,EAAS,MAAM,CAAA;AAC5C,IAAA,IAAI,MAAM,KAAA,CAAM,IAAA,CAAK,EAAE,GAAG,IAAA,EAAM,QAAQ,CAAA;AAAA,EAC1C;AACA,EAAA,MAAM,QAAQ,CAAC,EAAE,OAAA,EAAQ,KACvB,QAAQ,MAAA,CAAO,CAAC,IAAA,KAAS,KAAA,CAAM,KAAK,CAAC,KAAA,KAAU,MAAM,MAAA,KAAW,IAAI,CAAC,CAAA,CAAE,MAAA;AACzE,EAAA,MAAM,KAAA,GAAQ,MACX,IAAA,CAAK,CAAC,KAAK,KAAA,KAAU,KAAA,CAAM,GAAG,CAAA,GAAI,KAAA,CAAM,KAAK,CAAC,CAAA,CAC9C,IAAI,CAAC,EAAE,QAAQ,OAAA,EAAS,QAAA,EAAU,MAAA,EAAO,EAAG,KAAA,MAAW;AAAA,IACtD,KAAA,EAAO,IAAA;AAAA,IACP,MAAA;AAAA,IACA,OAAA;AAAA,IACA,QAAA;AAAA,IACA,MAAA,EAAQ,UAAU,KAAA,GAAQ;AAAA,GAC5B,CAAE,CAAA;AACJ,EAAA,WAAA,CAAY,GAAA,CAAI,WAAW,KAAK,CAAA;AAChC,EAAA,OAAO,KAAA;AACT;AAGA,SAAS,QAAA,CACP,KAAA,EACA,KAAA,EACA,MAAA,EAC2B;AAC3B,EAAA,OACE,MAAM,IAAA,CAAK,CAAC,IAAA,KAAS,IAAA,CAAK,UAAU,KAAK,CAAA,IACzC,KAAA,CAAM,IAAA,CAAK,CAAC,IAAA,KAAS,IAAA,CAAK,UAAU,IAAA,IAAQ,IAAA,CAAK,WAAW,MAAM,CAAA;AAEtE;AAwGO,IAAM,iBAAN,MAAkD;AAAA,EAC9C,UAAA;AAAA,EACA,OAAA;AAAA,EACA,KAAA;AAAA,EACA,eAAA;AAAA,EACA,KAAA,uBAAY,GAAA,EAAO;AAAA,EAQ5B,WAAA,CACE,SAAA,EACA,KAAA,EACA,OAAA,GAA0E,EAAC,EAC3E;AACA,IAAA,IAAA,CAAK,UAAA,GAAa,SAAA;AAClB,IAAA,IAAA,CAAK,OAAA,GAAU,CAAA,KAAA,EAAQ,KAAK,CAAA,CAAA,EAAI,SAAS,CAAA,MAAA,CAAA;AACzC,IAAA,IAAA,CAAK,KAAA,GAAQ,QAAQ,IAAA,KAAS,IAAA;AAC9B,IAAA,IAAA,CAAK,eAAA,GAAkB,QAAQ,cAAA,KAAmB,IAAA;AAClD,IAAA,MAAM,QAAQ,QAAA,CAAS,KAAA;AACvB,IAAA,KAAA,CAAM,GAAA,CAAI,SAAA,EAAA,CAAY,KAAA,CAAM,GAAA,CAAI,SAAS,CAAA,oBAAK,IAAI,GAAA,EAAY,EAAG,GAAA,CAAI,IAAA,CAAK,OAAO,CAAC,CAAA;AAAA,EACpF;AAAA,EAGA,KAAA,CAAM,SAAY,KAAA,EAA4B;AAC5C,IAAA,MAAM,KAAA,GAAQ,cAAA,CAAe,OAAA,EAAS,IAAA,CAAK,UAAU,CAAA;AACrD,IAAA,MAAM,IAAA,GAAO,KAAK,KAAA,CAAM,KAAK,KAAK,IAAA,CAAK,QAAA,CAAS,SAAS,KAAK,CAAA;AAC9D,IAAA,IAAA,CAAK,KAAA,CAAM,IAAI,OAAO,CAAA;AACtB,IAAA,MAAM,QAAQ,CAAC,IAAA,CAAK,QAAA,CAAS,OAAA,EAAS,OAAO,IAAI,CAAA;AACjD,IAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,IAAA,IAAA,CAAK,KAAA,CAAM,SAAS,KAAK,CAAA;AACzB,IAAA,IAAI,KAAA,EAAO,IAAA,CAAK,QAAA,CAAS,OAAA,EAAS,KAAK,CAAA;AAAA,EACzC;AAAA,EAQA,OAAO,OAAA,EAAqB;AAC1B,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,MAAA,CAAO,OAAO,CAAA;AACtC,IAAA,MAAM,KAAA,GAAQ,SAAS,MAAA,CAAO,GAAA,CAAI,OAAO,CAAA,EAAG,GAAA,CAAI,KAAK,UAAU,CAAA;AAC/D,IAAA,MAAM,KAAA,GACJ,IAAA,IAAQ,KAAA,IAAS,OAAA,CAAQ,YAAA,CAAa,IAAA,CAAK,OAAO,CAAA,GAC9C,cAAA,CAAe,OAAA,EAAS,IAAA,CAAK,UAAU,IACvC,EAAC;AACP,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,KAAK,CAAA;AAC7B,IAAA,IAAI,CAAC,MAAM,OAAO,KAAA;AAClB,IAAA,MAAM,UAAU,OAAA,CAAQ,YAAA,CAAa,IAAA,CAAK,UAAU,MAAM,IAAA,CAAK,OAAA;AAC/D,IAAA,MAAM,OAAO,OAAA,IAAW,CAAC,KAAK,QAAA,CAAS,OAAA,EAAS,OAAO,IAAI,CAAA;AAC3D,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,IAAI,CAAA;AAChC,IAAA,KAAA,CAAM,MAAA,CAAO,OAAO,CAAC,CAAA;AACrB,IAAA,MAAM,KAAA,GAAQ,MAAM,KAAK,CAAA;AACzB,IAAA,IAAI,KAAA,EAAO,KAAA,CAAM,MAAA,KAAW,IAAA,CAAK,MAAA;AACjC,IAAA,IAAA,CAAK,KAAA,CAAM,SAAS,KAAK,CAAA;AACzB,IAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAClB,IAAA,MAAM,QAAQ,IAAA,CAAK,MAAA,GAAS,KAAA,CAAM,KAAA,GAAQ,CAAC,CAAA,GAAI,MAAA;AAC/C,IAAA,IAAA,CAAK,SAAS,OAAA,EAAS,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,KAAK,QAAQ,CAAA;AAC5D,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAGA,SAAA,GAAkB;AAChB,IAAA,KAAA,MAAW,OAAA,IAAW,MAAM,IAAA,CAAK,IAAA,CAAK,KAAK,CAAA,EAAG,IAAA,CAAK,OAAO,OAAO,CAAA;AAAA,EACnE;AAAA,EAGA,MAAM,KAAA,EAAmD;AACvD,IAAA,MAAM,IAAA,GAAO,QAAA,CAAS,KAAA,EAAO,IAAA,EAAM,KAAK,OAAO,CAAA;AAC/C,IAAA,IAAI,IAAA,OAAW,KAAA,GAAQ,IAAA;AACvB,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAGA,QAAA,CAAS,SAAY,KAAA,EAAuC;AAC1D,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,YAAA,CAAa,IAAA,CAAK,UAAU,CAAA;AACpD,IAAA,MAAM,GAAA,GAAM,KAAA,CAAM,EAAA,CAAG,EAAE,CAAA;AACvB,IAAA,MAAM,MAAA,GAAS,MAAM,CAAC,CAAA;AACtB,IAAA,MAAM,IAAA,GAAO,GAAA,EAAK,OAAA,KAAY,OAAA,GAAU,GAAA,GAAM,MAAA;AAC9C,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,GACd,IAAA,CAAK,mBAAmB,CAAC,KAAA,CAAM,IAAA,CAAK,CAAC,SAAS,IAAA,CAAK,OAAA,KAAY,OAAO,CAAA,GACpE,SACA,MAAA,GACF,IAAA;AACJ,IAAA,MAAM,IAAA,GAAsB;AAAA,MAC1B,KAAA,EAAO,IAAA;AAAA,MACP,QAAQ,IAAA,CAAK,OAAA;AAAA,MACb,OAAA,EAAS,OAAA;AAAA,MACT,QAAA,EAAU,IAAA,GAAO,IAAA,CAAK,QAAA,GAAW,OAAA;AAAA,MACjC,MAAA,EAAQ,CAAC,IAAA,CAAK,KAAA,IAAS,IAAA,KAAS;AAAA,KAClC;AACA,IAAA,IAAI,IAAA,CAAK,SAAS,MAAA,EAAQ;AACxB,MAAA,MAAA,CAAO,MAAA,GAAS,IAAA;AAChB,MAAA,KAAA,CAAM,QAAQ,IAAI,CAAA;AAAA,IACpB,CAAA,MAAO,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA;AACtB,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAGA,QAAA,CAAS,OAAA,EAAY,KAAA,EAAiC,IAAA,EAA8B;AAClF,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,YAAA,CAAa,IAAA,CAAK,UAAU,CAAA;AACpD,IAAA,OAAO,KAAA,CAAM,KAAA,CAAM,KAAA,CAAM,OAAA,CAAQ,IAAI,CAAA,GAAI,CAAC,CAAA,CAAE,IAAA,CAAK,CAAC,KAAA,KAAU,KAAA,CAAM,YAAY,OAAO,CAAA;AAAA,EACvF;AAAA,EAGA,KAAA,CAAM,SAAY,KAAA,EAAuC;AACvD,IAAA,MAAM,OAAA,uBAAc,GAAA,CAA2B,CAAC,CAAC,IAAA,CAAK,OAAA,EAAS,IAAI,CAAC,CAAC,CAAA;AACrE,IAAA,KAAA,CAAM,OAAA,CAAQ,CAAC,IAAA,EAAM,KAAA,KAAU;AAC7B,MAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,CAAA,EAAG,KAAK,EAAE,GAAA,CAAI,CAAC,KAAA,KAAU,KAAA,CAAM,MAAM,CAAA;AAC/D,MAAA,MAAM,OAAA,GAAU,CAAC,GAAG,IAAI,IAAI,KAAA,CAAM,OAAA,EAAS,CAAC,EAAE,MAAA,CAAO,CAAC,IAAA,KAAS,IAAA,KAAS,KAAK,MAAM,CAAA;AACnF,MAAA,MAAM,MAAA,GAAS,KAAK,MAAA,IAAU,OAAA,CAAQ,WAAW,CAAA,GAAI,EAAC,GAAI,CAAC,KAAK,CAAA;AAChE,MAAA,OAAA,CAAQ,GAAA;AAAA,QACN,IAAA,CAAK,MAAA;AAAA,QACL,IAAA,CAAK,YAAY,IAAA,CAAK,QAAA,IAAY,QAAQ,MAAA,KAAW,CAAA,IAAK,KAAA,KAAU,KAAA,CAAM,MAAA,GAAS,CAAA,GAC/E,OACA,IAAA,CAAK,SAAA,CAAU,CAAC,IAAA,CAAK,QAAA,EAAU,IAAA,CAAK,SAAS,GAAG,MAAA,EAAQ,GAAG,OAAO,CAAC;AAAA,OACzE;AAAA,IACF,CAAC,CAAA;AACD,IAAA,KAAA,MAAW,CAAC,IAAA,EAAM,QAAQ,CAAA,IAAK,OAAA,EAAS;AACtC,MAAA,qBAAA,CAAsB,OAAA,EAAS,MAAM,QAAQ,CAAA;AAAA,IAC/C;AAAA,EACF;AAAA,EAGA,QAAA,CAAS,SAAY,KAAA,EAA4B;AAC/C,IAAA,qBAAA,CAAsB,OAAA,EAAS,IAAA,CAAK,UAAA,EAAY,KAAK,CAAA;AAAA,EACvD;AACF,CAAA;;AChQO,IAAM,cAAN,MAAkB;AAAA,EAEvB,MAAA,GAAS,IAAI,eAAA,EAAgB;AAAA,EAqB7B,GAAA,CAAI,MAAA,EAAqB,IAAA,EAAc,OAAA,EAAwB,OAAA,EAAiC;AAC9F,IAAA,MAAA,CAAO,gBAAA,CAAiB,IAAA,EAAM,OAAA,EAAS,EAAE,GAAG,SAAS,MAAA,EAAQ,IAAA,CAAK,MAAA,CAAO,MAAA,EAAQ,CAAA;AAAA,EACnF;AAAA,EAMA,OAAA,GAAgB;AACd,IAAA,IAAA,CAAK,OAAO,KAAA,EAAM;AAClB,IAAA,IAAA,CAAK,MAAA,GAAS,IAAI,eAAA,EAAgB;AAAA,EACpC;AACF,CAAA;;AChEO,IAAM,qBAAN,MAAyB;AAAA,EACrB,IAAA;AAAA,EACT,OAAA,GAAU,KAAA;AAAA,EACV,OAAA,GAAU,KAAA;AAAA,EACV,WAAA,GAAc,CAAA;AAAA,EAGd,YAAY,GAAA,EAAiB;AAC3B,IAAA,IAAA,CAAK,IAAA,GAAO,GAAA;AAAA,EACd;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AAAA,EACjB;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,IAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,IAAA,IAAA,CAAK,WAAA,IAAe,CAAA;AAAA,EACtB;AAAA,EAGA,IAAI,OAAA,GAAmB;AACrB,IAAA,OAAO,IAAA,CAAK,OAAA;AAAA,EACd;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,IAAI,CAAC,IAAA,CAAK,OAAA,IAAW,IAAA,CAAK,OAAA,EAAS;AACnC,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AACf,IAAA,MAAM,aAAa,IAAA,CAAK,WAAA;AACxB,IAAA,cAAA,CAAe,MAAM;AAEnB,MAAA,IAAI,UAAA,KAAe,KAAK,WAAA,IAAe,CAAC,KAAK,OAAA,IAAW,CAAC,KAAK,OAAA,EAAS;AACvE,MAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,MAAA,IAAA,CAAK,IAAA,EAAK;AAAA,IACZ,CAAC,CAAA;AAAA,EACH;AACF,CAAA;;AC7EO,IAAM,qBAAN,MAAyB;AAAA,EACrB,UAAA,GAAa,IAAI,WAAA,EAAY;AAAA,EAC7B,KAAA;AAAA,EACT,QAAA,GAA2B,IAAA;AAAA,EAClB,QAAA,GAAW,MAAY,IAAA,CAAK,KAAA,CAAM,QAAA,EAAS;AAAA,EAGpD,YAAY,GAAA,EAAiB;AAC3B,IAAA,IAAA,CAAK,KAAA,GAAQ,IAAI,kBAAA,CAAmB,GAAG,CAAA;AAAA,EACzC;AAAA,EAGA,QAAQ,OAAA,EAAwB;AAC9B,IAAA,IAAI,IAAA,CAAK,aAAa,OAAA,EAAS;AAC/B,IAAA,IAAA,CAAK,UAAA,EAAW;AAChB,IAAA,IAAA,CAAK,QAAA,GAAW,OAAA;AAChB,IAAA,IAAA,CAAK,MAAM,QAAA,EAAS;AACpB,IAAA,IAAA,CAAK,UAAA,CAAW,GAAA,CAAI,OAAA,EAAS,qBAAA,EAAuB,KAAK,QAAQ,CAAA;AAAA,EACnE;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,IAAA,CAAK,MAAM,QAAA,EAAS;AAAA,EACtB;AAAA,EAGA,IAAI,OAAA,GAAmB;AACrB,IAAA,OAAO,KAAK,KAAA,CAAM,OAAA;AAAA,EACpB;AAAA,EAGA,UAAA,GAAmB;AACjB,IAAA,IAAA,CAAK,WAAW,OAAA,EAAQ;AACxB,IAAA,IAAA,CAAK,MAAM,MAAA,EAAO;AAClB,IAAA,IAAA,CAAK,QAAA,GAAW,IAAA;AAAA,EAClB;AACF,CAAA;;ACXO,IAAM,gBAAA,GAAN,cAA+B,UAAA,CAAwB;AAAA,EAC5D,OAAgB,OAAA,GAAU,CAAC,OAAA,EAAS,UAAU,CAAA;AAAA,EAC9C,OAAgB,MAAA,GAAS,EAAE,GAAA,EAAK,MAAA,EAAO;AAAA,EACvC,OAAO,OAAA,GAAU,CAAC,SAAA,EAAW,QAAQ,CAAA;AAAA,EACrC,OAAO,MAAA,GAAS,CAAC,OAAO,CAAA;AAAA,EASf,aAAa,IAAI,kBAAA,CAAmB,MAAM,IAAA,CAAK,SAAS,CAAA;AAAA,EAExD,IAAA,GAAO,IAAI,cAAA,CAAiC,KAAA,EAAO,KAAK,UAAU,CAAA;AAAA,EAClE,YAAA,GAAe,IAAI,cAAA,CAAiC,QAAA,EAAU,KAAK,UAAU,CAAA;AAAA,EAC7E,eAAA,GAAkB,IAAI,cAAA,CAA4B,QAAA,EAAU,KAAK,UAAU,CAAA;AAAA,EAEpF,UAAA,GAAa,KAAA;AAAA,EAEJ,YAAA,GAAe,IAAI,gBAAA,CAAiB,CAAC,OAAA,KAAY;AACxD,IAAA,IAAI,OAAA,CAAQ,IAAA,CAAK,CAAC,MAAA,KAAW,IAAA,CAAK,kBAAkB,MAAA,CAAO,MAAA,KAAW,IAAA,CAAK,WAAW,CAAA,EAAG;AACvF,MAAA,IAAA,CAAK,WAAW,QAAA,EAAS;AAAA,IAC3B;AAAA,EACF,CAAC,CAAA;AAAA,EAGQ,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAClB,IAAA,IAAA,CAAK,UAAA,CAAW,OAAA,CAAQ,IAAA,CAAK,OAAO,CAAA;AACpC,IAAA,IAAA,CAAK,YAAA,CAAa,OAAA,CAAQ,IAAA,CAAK,OAAA,EAAS;AAAA,MACtC,UAAA,EAAY,IAAA;AAAA,MACZ,eAAA,EAAiB,CAAC,KAAK,CAAA;AAAA,MACvB,OAAA,EAAS;AAAA,KACV,CAAA;AACD,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA,EAGS,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,UAAA,GAAa,KAAA;AAClB,IAAA,IAAA,CAAK,WAAW,UAAA,EAAW;AAC3B,IAAA,IAAA,CAAK,aAAa,UAAA,EAAW;AAAA,EAC/B;AAAA,EAGA,eAAA,GAAwB;AACtB,IAAA,IAAA,CAAK,WAAW,QAAA,EAAS;AAAA,EAC3B;AAAA,EAGA,oBAAA,GAA6B;AAC3B,IAAA,IAAA,CAAK,WAAW,QAAA,EAAS;AAAA,EAC3B;AAAA,EAGA,wBAAwB,KAAA,EAA+B;AACrD,IAAA,IAAI,KAAK,UAAA,EAAY;AACnB,MAAA,IAAA,CAAK,IAAA,CAAK,OAAO,KAAK,CAAA;AACtB,MAAA,IAAA,CAAK,YAAA,CAAa,OAAO,KAAK,CAAA;AAAA,IAChC;AACA,IAAA,IAAA,CAAK,WAAW,QAAA,EAAS;AAAA,EAC3B;AAAA,EAGA,uBAAA,GAAgC;AAC9B,IAAA,IAAA,CAAK,WAAW,QAAA,EAAS;AAAA,EAC3B;AAAA,EAGA,2BAA2B,QAAA,EAA6B;AACtD,IAAA,IAAI,IAAA,CAAK,UAAA,EAAY,IAAA,CAAK,eAAA,CAAgB,OAAO,QAAQ,CAAA;AACzD,IAAA,IAAA,CAAK,WAAW,QAAA,EAAS;AAAA,EAC3B;AAAA,EAGA,OAAO,KAAA,EAAoB;AACzB,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,WAAA,CAAY,KAAK,CAAA;AACpC,IAAA,IAAI,CAAC,KAAA,EAAO;AACZ,IAAA,IAAI,CAAC,KAAA,CAAM,YAAA,CAAa,KAAK,CAAA,EAAG;AAC9B,MAAA,IAAA,CAAK,OAAA,EAAQ;AACb,MAAA;AAAA,IACF;AACA,IAAA,IAAA,CAAK,SAAS,QAAQ,CAAA;AAAA,EACxB;AAAA,EAQA,QAAQ,KAAA,EAAoB;AAC1B,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,WAAA,CAAY,KAAK,CAAA;AACpC,IAAA,IAAI,CAAC,KAAA,EAAO;AACZ,IAAA,MAAM,GAAA,GAAM,KAAA,CAAM,YAAA,CAAa,KAAK,CAAA;AACpC,IAAA,IAAI,CAAC,GAAA,EAAK;AACR,MAAA,IAAA,CAAK,OAAA,EAAQ;AACb,MAAA;AAAA,IACF;AACA,IAAA,IAAA,CAAK,SAAS,OAAO,CAAA;AACrB,IAAA,IAAA,CAAK,SAAS,OAAA,EAAS,EAAE,QAAQ,EAAE,GAAA,IAAO,CAAA;AAAA,EAC5C;AAAA,EAUA,OAAA,GAAgB;AACd,IAAA,IAAI,CAAC,KAAK,cAAA,EAAgB;AACxB,MAAA,IAAA,CAAK,SAAS,OAAO,CAAA;AACrB,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,QAAQ,IAAA,CAAK,WAAA;AACnB,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,YAAA,CAAa,KAAK,CAAA;AACnC,IAAA,IAAI,CAAC,GAAA,EAAK;AACR,MAAA,IAAA,CAAK,SAAS,OAAO,CAAA;AACrB,MAAA;AAAA,IACF;AACA,IAAA,IAAI,CAAC,MAAM,QAAA,EAAU;AACnB,MAAA,IAAA,CAAK,SAAS,SAAS,CAAA;AACvB,MAAA;AAAA,IACF;AACA,IAAA,IAAA,CAAK,QAAA,CAAS,KAAA,CAAM,YAAA,GAAe,CAAA,GAAI,WAAW,OAAO,CAAA;AAAA,EAC3D;AAAA,EAGA,aAAa,KAAA,EAAwC;AACnD,IAAA,IAAI,CAAC,KAAK,OAAA,CAAQ,YAAA,CAAa,QAAQ,IAAA,CAAK,UAAU,YAAY,CAAA,EAAG;AACnE,MAAA,IAAA,CAAK,IAAA,CAAK,OAAO,KAAK,CAAA;AACtB,MAAA,OAAO,KAAA,CAAM,aAAa,KAAK,CAAA;AAAA,IACjC;AAEA,IAAA,MAAM,UAAU,IAAA,CAAK,QAAA,CAAS,MAAA,GAAS,CAAA,GAAI,KAAK,QAAA,GAAW,IAAA;AAC3D,IAAA,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,KAAA,EAAO,OAAO,CAAA;AAC9B,IAAA,OAAO,OAAA;AAAA,EACT;AAAA,EAGA,YAAY,KAAA,EAAuC;AACjD,IAAA,MAAM,YAAY,KAAA,CAAM,aAAA;AACxB,IAAA,IAAI,EAAE,SAAA,YAAqB,gBAAA,CAAA,EAAmB,OAAO,IAAA;AACrD,IAAA,IAAI,CAAC,IAAA,CAAK,cAAA,IAAkB,SAAA,KAAc,IAAA,CAAK,aAAa,OAAO,IAAA;AACnE,IAAA,OAAO,SAAA;AAAA,EACT;AAAA,EAGA,SAAS,KAAA,EAA0B;AACjC,IAAA,MAAM,SAAA,GAAY,KAAA,KAAU,SAAA,IAAa,KAAA,KAAU,QAAA;AACnD,IAAA,IAAI,KAAK,cAAA,EAAgB;AACvB,MAAA,IAAA,CAAK,aAAa,KAAA,CAAM,IAAA,CAAK,WAAA,EAAa,SAAA,GAAY,OAAO,EAAE,CAAA;AAAA,IACjE;AACA,IAAA,IAAI,KAAK,iBAAA,EAAmB;AAC1B,MAAA,IAAA,CAAK,gBAAgB,KAAA,CAAM,IAAA,CAAK,cAAA,EAAgB,SAAA,GAAY,KAAK,IAAI,CAAA;AAAA,IACvE;AACA,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,YAAA,EAAc,KAAK,CAAA;AAAA,EAC/C;AACF","file":"avatar_controller.js","sourcesContent":["/**\n * Reflects a serialized attribute value only when the current value differs.\n *\n * A same-value `setAttribute` still queues a mutation record for every observer, so an\n * unchanged value writes nothing.\n *\n * @param element - The element whose attribute is reflected.\n * @param name - The attribute name, using the non-namespaced DOM API.\n * @param value - The serialized value, or null to remove the attribute.\n * @param beforeWrite - Called only for a change, before the DOM mutation, so the\n * caller can record ownership or identify its own observer records.\n * @returns Whether an attribute was written or removed.\n */\nexport function setAttributeIfChanged(\n element: Element,\n name: string,\n value: string | null,\n beforeWrite?: () => void,\n): boolean {\n if (element.getAttribute(name) === value) return false;\n beforeWrite?.();\n if (value === null) element.removeAttribute(name);\n else element.setAttribute(name, value);\n return true;\n}\n","/**\n * One registry per page for every copy of the library.\n *\n * Each `stimeo-ui/controllers/*` file, and each opt-in entry, is built on its own and carries\n * its own copy of every utility it uses, so a page that imports controllers one by one, or one\n * of them next to the barrel, evaluates a utility several times. A registry that coordinates\n * instances of different controllers — the stacks of leased values, the loans of a\n * `tabindex`, the Escape layers, the focus traps — must not be split by that: a dialog inside\n * a drawer, or a character counter and a form field on one input, coordinate whichever files\n * they came from. So such a registry lives once on `globalThis`, under a `Symbol.for` key the\n * copies agree on, and the first copy to ask for it creates it.\n *\n * The key names the registry and the version of its shape\n * (`stimeo-ui.<utility>.registry.v<n>`). Copies whose registries have the same shape share\n * one; a release that changes the shape changes the version, so a copy of another release\n * keeps a registry of its own instead of reading entries it does not understand — the two\n * then coordinate within each release only. An entry another copy put in a registry is read\n * through its fields alone, never through `#private` members, which belong to the class of\n * the copy that created it.\n */\n\n/**\n * The registry stored under `key`, created by `create` on first use.\n *\n * @param key - `stimeo-ui.<utility>.registry.v<n>`: the version is that of the shape `create`\n * returns.\n * @param create - Builds the empty registry.\n */\nexport function sharedRegistry<T extends object>(key: string, create: () => T): T {\n const symbol = Symbol.for(key);\n const scope = globalThis as unknown as Record<symbol, T | undefined>;\n const existing = scope[symbol];\n if (existing !== undefined) return existing;\n const registry = create();\n Object.defineProperty(globalThis, symbol, { value: registry });\n return registry;\n}\n","import { setAttributeIfChanged } from \"./attribute_write\";\nimport { sharedRegistry } from \"./shared_registry\";\n\n/** What a lease's record says: the author's value, its last write, the leases beneath. */\ninterface AttributeLeaseRecord {\n readonly original: string | null;\n readonly written: string | null;\n /** Whether it hands back what the lease right beneath it wrote last. */\n readonly linked: boolean;\n /** The records of the other owners' leases beneath it, nearest first. */\n readonly beneath: readonly string[];\n}\n\n/** One lease's hold on one element's attribute, in the stack every lease of it shares. */\ninterface AttributeHold {\n /** The lease holding it; `null` for a hold read from a copy's record until its lease takes it. */\n lease: object | null;\n readonly record: string;\n written: string | null;\n /** What it hands back with nothing beneath it: the author's value. */\n readonly original: string | null;\n /** Whether it hands back what the hold beneath it wrote last instead. */\n linked: boolean;\n}\n\n/** The holds on each element by attribute, and every lease's record name by attribute. */\ninterface AttributeLeaseRegistry {\n readonly stacks: WeakMap<Element, Map<string, AttributeHold[]>>;\n readonly names: Map<string, Set<string>>;\n}\n\n/** Holds and record names coordinate across separately built package entries. */\nconst registry = sharedRegistry(\n \"stimeo-ui.attribute-lease.registry.v1\",\n (): AttributeLeaseRegistry => ({ stacks: new WeakMap(), names: new Map() }),\n);\n\n/** Whether a parsed record entry is an attribute value: a string, or `null` for absence. */\nfunction isAttributeValue(value: unknown): value is string | null {\n return value === null || typeof value === \"string\";\n}\n\n/**\n * Reads the record a lease left on `element`: the author's value, the value the lease wrote\n * last, whether it hands back the value beneath it (`false` before the names when it does not)\n * and the records beneath it. `undefined` when the record is missing or holds anything else.\n */\nfunction readLeaseRecord(element: Element, record: string): AttributeLeaseRecord | undefined {\n try {\n const parsed: unknown = JSON.parse(element.getAttribute(record) ?? \"null\");\n if (!Array.isArray(parsed)) return undefined;\n const [original, written, ...rest] = parsed as unknown[];\n const linked = rest[0] !== false;\n const beneath = linked ? rest : rest.slice(1);\n if (!beneath.every((under): under is string => typeof under === \"string\")) return undefined;\n return isAttributeValue(original) && isAttributeValue(written)\n ? { original, written, linked, beneath }\n : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** The element's stack, seeded from complete ownership records once. */\nfunction attributeStack(element: Element, attribute: string): AttributeHold[] {\n const byAttribute = registry.stacks.get(element) ?? new Map<string, AttributeHold[]>();\n registry.stacks.set(element, byAttribute);\n const known = byAttribute.get(attribute);\n if (known) return known;\n const found: (AttributeLeaseRecord & { readonly record: string })[] = [];\n for (const record of registry.names.get(attribute) ?? []) {\n const read = readLeaseRecord(element, record);\n if (read) found.push({ ...read, record });\n }\n const depth = ({ beneath }: (typeof found)[number]) =>\n beneath.filter((name) => found.some((other) => other.record === name)).length;\n const stack = found\n .sort((one, other) => depth(one) - depth(other))\n .map(({ record, written, original, linked }, index) => ({\n lease: null,\n record,\n written,\n original,\n linked: linked && index > 0,\n }));\n byAttribute.set(attribute, stack);\n return stack;\n}\n\n/** The lease's live hold or the unclaimed hold described by its owner's record. */\nfunction findHold(\n stack: readonly AttributeHold[],\n lease: object,\n record: string,\n): AttributeHold | undefined {\n return (\n stack.find((held) => held.lease === lease) ??\n stack.find((held) => held.lease === null && held.record === record)\n );\n}\n\n/**\n * Inspects a derived writer's continuation without acquiring or returning a hold.\n * A covered hold may update beneath its cover. Without a hold, the expected authored\n * value is compared with the original beneath any temporary cover. The original\n * distinguishes an absent attribute from an empty one.\n */\nexport function readAttributeLease(\n element: Element,\n attribute: string,\n owner: string,\n lease: object,\n expected: string | null,\n): { readonly allowed: boolean; readonly original: string | null } {\n const stack = attributeStack(element, attribute);\n const hold = findHold(stack, lease, `data-${owner}-${attribute}-lease`);\n const current = element.getAttribute(attribute);\n const covering = [...stack].reverse().find((other) => other.written === current);\n const original = (hold ?? covering)?.original;\n const authored = original === undefined ? current : original;\n return {\n original: authored,\n allowed: hold\n ? current === hold.written ||\n stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current)\n : authored === expected,\n };\n}\n\n/**\n * The value `element`'s author wrote for `attribute`, as the record a lease of any owner leaves\n * on the element carries it; `undefined` while no lease leaves one, as when nothing has moved\n * the attribute off the author's value. A field a reveal turned from `password` into `text`\n * still reads `\"password\"` here.\n */\nexport function leasedAuthorValue(element: Element, attribute: string): string | null | undefined {\n const suffix = `-${attribute}-lease`;\n for (const name of element.getAttributeNames()) {\n if (!name.startsWith(\"data-\") || !name.endsWith(suffix)) continue;\n const record = readLeaseRecord(element, name);\n if (record) return record.original;\n }\n return undefined;\n}\n\n/**\n * Temporarily controls one attribute across a changing set of elements.\n *\n * The first write remembers the authored value, including the distinction between\n * an absent attribute and an authored empty string. Returning a lease restores that\n * value only while the attribute still matches the controller's last write. If a\n * consumer changed it in the meantime, the consumer owns the new value and teardown\n * leaves it alone.\n *\n * A `null` write deliberately removes the attribute while retaining the lease. This\n * is useful for derived ARIA whose valid absence is itself controller state, such as\n * an unbounded `aria-valuemin` or a blank spinbutton's `aria-valuenow`.\n *\n * **Every lease of an attribute shares one stack of holds per element**, whatever its owner\n * and whichever file of the package constructed it (the stacks are one per page): the holds\n * sit in the order their leases first wrote the element, each with the value it wrote last.\n * The element's value is the highest hold's that carries it, and the page's when none does.\n * A write reaches the element unless a hold above the writer carries the element's value: the\n * top hold always shows its writes, a hold beneath it shows nothing while a hold above keeps\n * its value on the element, and once the page has put a value there that no hold carries, the\n * next write of any hold replaces it. A return gives a value back only from the hold the\n * element's value is: the value the hold beneath it wrote last, or the author's once nothing\n * is beneath it; any other return only leaves the stack, and the page's value stays. Holds\n * that wrote the same value are told apart by the stack, never by the value: the higher one\n * keeps it. A first write over a top hold whose value the page has replaced takes the\n * element's value as the author's. Two instances of one controller on one element — an\n * element outside their scope — stack the same way.\n *\n * A **base lease** (`{ base: true }`) holds the element's own state rather than a value over\n * it — a form field's ARIA on its control is one — so its first write goes to the bottom of\n * the stack, beneath the leases of other owners already there: a temporary value another\n * owner leased stays shown and returns to the base lease's latest value.\n *\n * **The stack travels with the element.** While a hold shows a value other than the author's,\n * lies on another hold or is covered by one, the element carries its owner's record\n * `data-<owner>-<attribute>-lease`: JSON of the author's value and the value the hold wrote\n * last (each a string, or `null` for an absent attribute), `false` where the hold hands back\n * the author's value rather than the latest value of the hold beneath it, and the records of\n * every hold beneath it, nearest first. Returning the last lease of an owner removes its\n * record. A copy of the element — a page Turbo restores from its cache is one — therefore\n * carries the stack, and the first lease to touch the copy reads it back under the record\n * names of the leases constructed so far, which include every lease that wrote a record the\n * cache holds, and orders the holds by the records each one lists beneath it. Each lease takes\n * the hold its owner's record describes over in place. Complete records for distinct owners\n * preserve their order across a copy. The records describe holds by owner, not by instance:\n * one owner holding the element more than once leaves only its topmost hold's record, which\n * the first of its leases to write or return on the copy takes. Lower holds of that owner and\n * their positions are absent; mutually listed owners cannot express their full live order.\n * A hold that showed the author's value alone carries no record and is taken by its first\n * write on the copy. Missing or unreadable records also leave their holds unknown, so copying\n * the DOM does not promise identical decisions for every live stack. Without a record it can\n * read, a new stack takes the current value as the author's. A Turbo morph removes records\n * absent from the server HTML; existing holds remain in memory until their leases return.\n *\n * The lease has no lifecycle of its own, so it never subscribes to document events,\n * and nothing rewinds a live page before Turbo caches it: `turbo:before-cache` also\n * fires on pages that stay.\n */\nexport class AttributeLease<T extends Element = Element> {\n readonly #attribute: string;\n readonly #record: string;\n readonly #base: boolean;\n readonly #preserveAuthor: boolean;\n readonly #held = new Set<T>();\n\n /**\n * @param attribute - The attribute whose temporary values this lease owns.\n * @param owner - Names the record on the element; the controller's identifier.\n * @param options - `base`: written beneath the other owners' leases;\n * `preserveAuthor`: a new base hold keeps a current value that no hold carries as authored.\n */\n constructor(\n attribute: string,\n owner: string,\n options: { readonly base?: boolean; readonly preserveAuthor?: boolean } = {},\n ) {\n this.#attribute = attribute;\n this.#record = `data-${owner}-${attribute}-lease`;\n this.#base = options.base === true;\n this.#preserveAuthor = options.preserveAuthor === true;\n const names = registry.names;\n names.set(attribute, (names.get(attribute) ?? new Set<string>()).add(this.#record));\n }\n\n /** Writes or removes the leased attribute while preserving its authored value. */\n write(element: T, value: string | null): void {\n const stack = attributeStack(element, this.#attribute);\n const hold = this.#take(stack) ?? this.#acquire(element, stack);\n this.#held.add(element);\n const shows = !this.#covered(element, stack, hold);\n hold.written = value;\n this.#mark(element, stack);\n if (shows) this.#reflect(element, value);\n }\n\n /**\n * Returns one lease, also on a restored copy, to the value the hold beneath it wrote last or\n * the author's, without overwriting a value a consumer wrote since. Says whether there was a\n * lease to return: a hold this lease has, or the hold a copy's record describes while no live\n * lease of the owner has taken it — a copy of a page an earlier connection wrote carries one.\n */\n return(element: T): boolean {\n const held = this.#held.delete(element);\n const known = registry.stacks.get(element)?.has(this.#attribute);\n const stack =\n held || known || element.hasAttribute(this.#record)\n ? attributeStack(element, this.#attribute)\n : [];\n const hold = this.#take(stack);\n if (!hold) return false;\n const carries = element.getAttribute(this.#attribute) === hold.written;\n const owns = carries && !this.#covered(element, stack, hold);\n const index = stack.indexOf(hold);\n stack.splice(index, 1);\n const above = stack[index];\n if (above) above.linked &&= hold.linked;\n this.#mark(element, stack);\n if (!owns) return true;\n const below = hold.linked ? stack[index - 1] : undefined;\n this.#reflect(element, below ? below.written : hold.original);\n return true;\n }\n\n /** Returns every lease it holds, with the same ownership check as {@link return}. */\n returnAll(): void {\n for (const element of Array.from(this.#held)) this.return(element);\n }\n\n /** Its hold in `stack`, taking over one read from its record. */\n #take(stack: AttributeHold[]): AttributeHold | undefined {\n const hold = findHold(stack, this, this.#record);\n if (hold) hold.lease = this;\n return hold;\n }\n\n /** A new hold: on top, over the top hold while the element shows it; a base one at the bottom. */\n #acquire(element: T, stack: AttributeHold[]): AttributeHold {\n const current = element.getAttribute(this.#attribute);\n const top = stack.at(-1);\n const bottom = stack[0];\n const over = top?.written === current ? top : undefined;\n const from = this.#base\n ? this.#preserveAuthor && !stack.some((held) => held.written === current)\n ? undefined\n : bottom\n : over;\n const hold: AttributeHold = {\n lease: this,\n record: this.#record,\n written: current,\n original: from ? from.original : current,\n linked: !this.#base && over !== undefined,\n };\n if (this.#base && bottom) {\n bottom.linked = true;\n stack.unshift(hold);\n } else stack.push(hold);\n return hold;\n }\n\n /** Whether a hold above `hold` carries the element's value, which is then that hold's. */\n #covered(element: T, stack: readonly AttributeHold[], hold: AttributeHold): boolean {\n const current = element.getAttribute(this.#attribute);\n return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);\n }\n\n /** Leaves each owner's record of its topmost hold, or none where nothing needs one. */\n #mark(element: T, stack: readonly AttributeHold[]): void {\n const records = new Map<string, string | null>([[this.#record, null]]);\n stack.forEach((hold, index) => {\n const names = stack.slice(0, index).map((under) => under.record);\n const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);\n const marker = hold.linked || beneath.length === 0 ? [] : [false];\n records.set(\n hold.record,\n hold.written === hold.original && beneath.length === 0 && index === stack.length - 1\n ? null\n : JSON.stringify([hold.original, hold.written, ...marker, ...beneath]),\n );\n });\n for (const [name, recorded] of records) {\n setAttributeIfChanged(element, name, recorded);\n }\n }\n\n /** Reflects only a real value transition, avoiding self-triggered mutation work. */\n #reflect(element: T, value: string | null): void {\n setAttributeIfChanged(element, this.#attribute, value);\n }\n}\n","/** What {@link ListenerSet.add} passes on besides the signal it supplies itself. */\nexport type ListenerOptions = Omit<AddEventListenerOptions, \"signal\">;\n\n/**\n * The event map a target's own `addEventListener` types its handlers with: the\n * window's, a document's, an HTML or SVG element's, any other element's, or a\n * media query list's. Other targets name none.\n */\ntype EventMapOf<T extends EventTarget> = T extends Window\n ? WindowEventMap\n : T extends Document\n ? DocumentEventMap\n : T extends HTMLElement\n ? HTMLElementEventMap\n : T extends SVGElement\n ? SVGElementEventMap\n : T extends Element\n ? ElementEventMap\n : T extends MediaQueryList\n ? MediaQueryListEventMap\n : Record<never, never>;\n\n/**\n * Owns every listener a controller attaches for one connected lifetime and\n * releases them together, so an event name is spelled once and the release\n * cannot drift from the registration.\n *\n * Detaching by hand repeats three things at the release site — the type, the\n * handler reference and the capture flag — and any one of them drifting leaves\n * a listener alive on an element the controller no longer drives. Aborting the\n * signal a registration was created with releases it whatever it was made of,\n * so the release site names nothing.\n *\n * One set per lifetime, held as a field and never rebuilt.\n * {@link ListenerSet.dispose} is the only thing that closes a generation, and it opens\n * the next one in the same step, so the lifecycle hooks can call `add` and\n * `dispose` in any order any number of times, and no `add` ever meets an\n * already aborted signal — a DOM that registers a listener on such a signal\n * would keep it forever. Rebuilding the set instead would leak:\n * `addEventListener` matches an existing registration on target, type, callback\n * and capture alone, so re-adding the same handler while the first registration\n * is live is discarded, and that live registration keeps the signal it was\n * attached with — a set built by a second `connect()` can never release what it\n * appears to own.\n *\n * Scope is the registration only. Timers, observers, leases and the order they\n * are unwound in stay with the controller, which places `dispose()` in its\n * cleanup sequence. A receiver that changes at runtime (a\n * swapped target) is rebound by calling `dispose()` and then `add()` again from\n * the rebind, not by a second set.\n *\n * A lifetime shorter than the connection — while a Value holds, until a first\n * load, while a subscription is open — gets a set of its own, which the end of\n * that lifetime disposes. A controller therefore holds one set per lifetime: the\n * connection's, and one for each swapped receiver or shorter span.\n *\n * The set must own every registration of a tuple it holds: a listener the same\n * target already carries for the same type, callback and capture flag keeps the\n * right to remove it, and this set's `add` for that tuple is discarded. A\n * subscription shared between instances behind a participant count, listeners of\n * several elements that come and go one at a time, and those one gesture holds\n * keep their own pair of calls or a signal of their own.\n *\n * @example\n * ```ts\n * readonly #listeners = new ListenerSet();\n *\n * connect(): void {\n * this.#listeners.add(this.element, \"submit\", this.#onNativeSubmit, { capture: true });\n * this.#listeners.add(this.element, \"turbo:submit-start\", this.#onSubmitStart);\n * this.#listeners.add(this.element, \"turbo:submit-end\", this.#onSubmitEnd);\n * }\n *\n * disconnect(): void {\n * this.#listeners.dispose();\n * }\n * ```\n */\nexport class ListenerSet {\n /** The generation every `add` joins until the next `dispose()`. */\n #abort = new AbortController();\n\n /**\n * Attaches `handler` to the open generation, exactly as the caller spelled it.\n *\n * The set supplies the signal, so `options` carries everything else the DOM\n * accepts — `capture` included. A removal by hand would have to repeat that\n * flag; aborting the signal releases the listener without it.\n *\n * A `type` the target's own event map names types `handler` with that event,\n * as the target's `addEventListener` does, so a handler written for a\n * `KeyboardEvent` is passed as it is; any other `type` takes an\n * `EventListener`.\n */\n add<T extends EventTarget, K extends keyof EventMapOf<T> & string>(\n target: T,\n type: K,\n handler: (event: EventMapOf<T>[K]) => void,\n options?: ListenerOptions,\n ): void;\n add(target: EventTarget, type: string, handler: EventListener, options?: ListenerOptions): void;\n add(target: EventTarget, type: string, handler: EventListener, options?: ListenerOptions): void {\n target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });\n }\n\n /**\n * Releases every listener of the open generation, synchronously, and opens the\n * next one. Idempotent, and safe before anything has been added.\n */\n dispose(): void {\n this.#abort.abort();\n this.#abort = new AbortController();\n }\n}\n","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Whether a requested pass has yet to run. */\n get pending(): boolean {\n return this.#queued;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","import { ListenerSet } from \"./listener_set\";\nimport { MicrotaskCoalescer } from \"./microtask_coalescer\";\n\n/**\n * Coalesces retained-element morphs and explicit lifecycle requests into one pass.\n *\n * Observe the controller's element after its initial state is settled. Morphs on\n * that element and bubbling morphs on its descendants share the same microtask\n * as {@link schedule}. The callback repairs derived output; the controller owns\n * any logical transition reporting. {@link disconnect} releases the listener and\n * cancels pending work, including work from an earlier connected lifetime.\n */\nexport class MorphRenderWatcher {\n readonly #listeners = new ListenerSet();\n readonly #pass: MicrotaskCoalescer;\n #element: Element | null = null;\n readonly #onMorph = (): void => this.#pass.schedule();\n\n /** @param run - the single pass that repairs the settled controller's output. */\n constructor(run: () => void) {\n this.#pass = new MicrotaskCoalescer(run);\n }\n\n /** Opens observation; observing the same element again preserves queued work. */\n observe(element: Element): void {\n if (this.#element === element) return;\n this.disconnect();\n this.#element = element;\n this.#pass.activate();\n this.#listeners.add(element, \"turbo:morph-element\", this.#onMorph);\n }\n\n /** Requests one pass; requests outside an observed lifetime are ignored. */\n schedule(): void {\n this.#pass.schedule();\n }\n\n /** Whether a pass requested by a morph or {@link schedule} has yet to run. */\n get pending(): boolean {\n return this.#pass.pending;\n }\n\n /** Releases observation and pending work synchronously; safe to repeat. */\n disconnect(): void {\n this.#listeners.dispose();\n this.#pass.cancel();\n this.#element = null;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { AttributeLease } from \"../utils/attribute_lease\";\nimport { MorphRenderWatcher } from \"../utils/morph_render_watcher\";\n\n/** Public rendering phases exposed through the root's `data-state`. */\ntype AvatarState = \"empty\" | \"loading\" | \"loaded\" | \"error\";\n\n/**\n * Headless **Avatar** behavior with image-load fallback. No dedicated APG\n * pattern; it follows the non-text-content practice (WCAG 1.1.1).\n *\n * Markup contract (identifier: `stimeo--avatar`):\n * <span data-controller=\"stimeo--avatar\" role=\"img\" aria-label=\"Jane Doe\"\n * data-stimeo--avatar-src-value=\"/u/123.jpg\">\n * <img alt=\"\" aria-hidden=\"true\"\n * data-stimeo--avatar-target=\"image\"\n * data-action=\"load->stimeo--avatar#onLoad error->stimeo--avatar#onError\" />\n * <span aria-hidden=\"true\" hidden data-stimeo--avatar-target=\"fallback\">JD</span>\n * </span>\n *\n * The `image` and `fallback` targets are optional. A present `src` Value owns the\n * current image's `src`, including an empty Value that explicitly clears it. When\n * the Value is absent, a directly authored `<img src>` is used instead. Runtime\n * Value changes, direct `src` changes, and target replacements all repaint.\n *\n * The accessible name lives on the container (`role=\"img\"` + `aria-label`); the\n * inner image and fallback are `aria-hidden` so assistive technology reads the\n * name once in every visual state.\n *\n * @remarks\n * Behavior only — shape, size, and colour are the consumer's CSS. Initials or\n * colour generation are out of scope: fallback content comes from markup. The\n * `empty`, `loading`, `loaded`, and `error` phases are exposed on `data-state`.\n * A native error from the current image dispatches `stimeo--avatar:error` with\n * `{ src: string }`; reconnecting or reconciling an already-broken image is\n * silent because no new failure event occurred.\n */\nexport class AvatarController extends Controller<HTMLElement> {\n static override targets = [\"image\", \"fallback\"];\n static override values = { src: String };\n static actions = [\"onError\", \"onLoad\"] as const;\n static events = [\"error\"] as const;\n\n declare readonly imageTarget: HTMLImageElement;\n declare readonly fallbackTarget: HTMLElement;\n declare readonly hasImageTarget: boolean;\n declare readonly hasFallbackTarget: boolean;\n declare srcValue: string;\n\n /** Collapses one morph's Value, target, and direct-attribute signals into one pass. */\n readonly #reconcile = new MorphRenderWatcher(() => this.#render());\n /** Preserves directly-authored source and visibility while targets are controlled. */\n readonly #src = new AttributeLease<HTMLImageElement>(\"src\", this.identifier);\n readonly #imageHidden = new AttributeLease<HTMLImageElement>(\"hidden\", this.identifier);\n readonly #fallbackHidden = new AttributeLease<HTMLElement>(\"hidden\", this.identifier);\n /** True only while target departure means ownership was actually relinquished. */\n #connected = false;\n /** Watches the non-Value render input used by the direct `<img src>` form. */\n readonly #srcObserver = new MutationObserver((records) => {\n if (records.some((record) => this.hasImageTarget && record.target === this.imageTarget)) {\n this.#reconcile.schedule();\n }\n });\n\n /** Starts retained-DOM observation and silently derives the current phase. */\n override connect(): void {\n this.#connected = true;\n this.#reconcile.observe(this.element);\n this.#srcObserver.observe(this.element, {\n attributes: true,\n attributeFilter: [\"src\"],\n subtree: true,\n });\n this.#render();\n }\n\n /** Cancels asynchronous work while retaining the last materialized visual state. */\n override disconnect(): void {\n this.#connected = false;\n this.#reconcile.disconnect();\n this.#srcObserver.disconnect();\n }\n\n /** Repaints when application code or a Turbo morph changes the source Value. */\n srcValueChanged(): void {\n this.#reconcile.schedule();\n }\n\n /** Applies the current source and phase to an image inserted or replaced at runtime. */\n imageTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Returns attributes borrowed from an image that leaves this live avatar. */\n imageTargetDisconnected(image: HTMLImageElement): void {\n if (this.#connected) {\n this.#src.return(image);\n this.#imageHidden.return(image);\n }\n this.#reconcile.schedule();\n }\n\n /** Applies the current phase to a fallback inserted or replaced at runtime. */\n fallbackTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Returns authored visibility when a fallback leaves this live avatar. */\n fallbackTargetDisconnected(fallback: HTMLElement): void {\n if (this.#connected) this.#fallbackHidden.return(fallback);\n this.#reconcile.schedule();\n }\n\n /** Reveals the current image after its native `load` event. */\n onLoad(event: Event): void {\n const image = this.#eventImage(event);\n if (!image) return;\n if (!image.getAttribute(\"src\")) {\n this.#render();\n return;\n }\n this.#reflect(\"loaded\");\n }\n\n /**\n * Reveals the fallback after the current image's native `error` event.\n *\n * Dispatches `stimeo--avatar:error` with `{ src: string }`, where `src` is\n * the attempted attribute value. Empty/stale/detached image events are ignored.\n */\n onError(event: Event): void {\n const image = this.#eventImage(event);\n if (!image) return;\n const src = image.getAttribute(\"src\");\n if (!src) {\n this.#render();\n return;\n }\n this.#reflect(\"error\");\n this.dispatch(\"error\", { detail: { src } });\n }\n\n /**\n * Materializes the phase from the final source/target DOM after one mutation batch.\n *\n * Cached success and failure are state reconstruction, not new load events, so\n * this pass never dispatches the public `error` event.\n *\n * @stimeoRenderRoot\n */\n #render(): void {\n if (!this.hasImageTarget) {\n this.#reflect(\"empty\");\n return;\n }\n\n const image = this.imageTarget;\n const src = this.#applySource(image);\n if (!src) {\n this.#reflect(\"empty\");\n return;\n }\n if (!image.complete) {\n this.#reflect(\"loading\");\n return;\n }\n this.#reflect(image.naturalWidth > 0 ? \"loaded\" : \"error\");\n }\n\n /** Applies Value precedence and returns the effective raw `src` attribute. */\n #applySource(image: HTMLImageElement): string | null {\n if (!this.element.hasAttribute(`data-${this.identifier}-src-value`)) {\n this.#src.return(image);\n return image.getAttribute(\"src\");\n }\n\n const desired = this.srcValue.length > 0 ? this.srcValue : null;\n this.#src.write(image, desired);\n return desired;\n }\n\n /** Accepts an action only from the singular image target currently controlled. */\n #eventImage(event: Event): HTMLImageElement | null {\n const candidate = event.currentTarget;\n if (!(candidate instanceof HTMLImageElement)) return null;\n if (!this.hasImageTarget || candidate !== this.imageTarget) return null;\n return candidate;\n }\n\n /** Writes the visibility pair and public root state as one controller-owned output. */\n #reflect(state: AvatarState): void {\n const showImage = state === \"loading\" || state === \"loaded\";\n if (this.hasImageTarget) {\n this.#imageHidden.write(this.imageTarget, showImage ? null : \"\");\n }\n if (this.hasFallbackTarget) {\n this.#fallbackHidden.write(this.fallbackTarget, showImage ? \"\" : null);\n }\n this.element.setAttribute(\"data-state\", state);\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/blur_deferral.ts","../../src/utils/layout_observer.ts","../../src/controllers/breadcrumb_controller.ts"],"names":[],"mappings":"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","file":"breadcrumb_controller.js","sourcesContent":["/**\n * Shared bookkeeping for updates that must wait until an element loses focus.\n *\n * A control that becomes redundant while the user is standing on it cannot be\n * removed from the page immediately: hiding a focused element (or turning it\n * into a native `disabled` one) drops focus to `<body>`, stranding a keyboard\n * user mid-widget. The fix every affected controller reached for is the same —\n * keep the element reachable, hold the destructive update, listen for `blur`,\n * and apply it then.\n *\n * {@link BlurDeferral} owns *only* the registry part of that: which elements are\n * currently holding an update back, attaching and detaching the `blur` listener,\n * and guaranteeing teardown. It is deliberately **policy-free** — what the\n * interim state looks like (`aria-disabled`, an ownership marker, nothing at\n * all), and what the deferred update actually is, stay in the controller, which\n * receives the element back through its release callback.\n *\n * Three contracts are worth stating up front, because each one is a hazard the\n * consumers would otherwise hit:\n *\n * - **The release callback fires only on a real `blur`.** `release()` /\n * `releaseAll()` *cancel* a deferral; they do not complete it. Conflating the\n * two makes `overflow-indicator` disable a button that just left the boundary.\n * - **`elements` is a snapshot.** Iterating the live key set while releasing\n * inside the loop is the failure this registry exists to prevent.\n * - **The entry is detached before the callback runs**, so a controller that\n * decides the update is still unsafe can simply defer again.\n *\n * @example\n * ```ts\n * readonly #deferred = new BlurDeferral<HTMLElement>((trigger) => {\n * if (this.#connected) this.#evaluate();\n * });\n *\n * #hide(trigger: HTMLElement) {\n * if (document.activeElement === trigger) {\n * this.#deferred.deferOnly(trigger); // stays visible until blur\n * return;\n * }\n * this.#deferred.releaseAll();\n * trigger.hidden = true;\n * }\n *\n * disconnect() {\n * this.#deferred.releaseAll();\n * }\n * ```\n */\nexport class BlurDeferral<T extends HTMLElement = HTMLElement> {\n /** Elements currently holding an update back, mapped to their `blur` listener. */\n readonly #pending = new Map<T, () => void>();\n /** Called after a pending element blurs and has been detached. */\n readonly #onRelease: (element: T) => void;\n\n /** @param onRelease - Invoked once `element` actually blurs; never on `release`. */\n constructor(onRelease: (element: T) => void) {\n this.#onRelease = onRelease;\n }\n\n /** Number of elements currently holding an update back. */\n get size(): number {\n return this.#pending.size;\n }\n\n /** Snapshot of the pending elements, safe to iterate while releasing them. */\n get elements(): T[] {\n return [...this.#pending.keys()];\n }\n\n /** Whether `element` is currently holding an update back. */\n has(element: T): boolean {\n return this.#pending.has(element);\n }\n\n /** Holds an update back until `element` blurs. Idempotent (no stacked listeners). */\n defer(element: T): void {\n if (this.#pending.has(element)) return;\n const onBlur = (): void => {\n this.#detach(element);\n this.#onRelease(element);\n };\n this.#pending.set(element, onBlur);\n element.addEventListener(\"blur\", onBlur);\n }\n\n /** Defers `element` as the only pending entry, cancelling any others. */\n deferOnly(element: T): void {\n for (const pending of this.elements) {\n if (pending !== element) this.#detach(pending);\n }\n this.defer(element);\n }\n\n /** Cancels `element`'s deferral without completing it; no-ops when not pending. */\n release(element: T): void {\n this.#detach(element);\n }\n\n /** Cancels every deferral without completing any of them. */\n releaseAll(): void {\n for (const element of this.elements) this.#detach(element);\n }\n\n /** Removes the `blur` listener for `element` and forgets it. */\n #detach(element: T): void {\n const onBlur = this.#pending.get(element);\n if (onBlur) element.removeEventListener(\"blur\", onBlur);\n this.#pending.delete(element);\n }\n}\n","/**\n * Unified layout observation for Stimeo controllers.\n *\n * Widgets whose output is measured — an overflow boundary, a masonry column count,\n * an autosized textarea — have three sources that move the layout under them: their\n * *own* box changing — via {@link ResizeObserver} — the *viewport* changing — via the\n * `window` `resize` event — and a descendant *resource settling*, because an image or\n * a frame reports a height of zero until it has loaded. Wiring those three by hand in\n * every controller risks leaked listeners on `disconnect()`. {@link LayoutObserver}\n * owns all three behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * this.#layout.observeDescendantLoads(this.panelTarget);\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n #loadContainer: Element | null = null;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n /** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */\n readonly #handleDescendantLoad = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Starts reporting a `load` from anywhere inside `container` — an image or a\n * frame settling changes the box it sits in, and it measures as zero high until\n * then. `load` does not bubble, so the subscription is a capture-phase listener\n * on the container itself and nothing the caller spells.\n *\n * **One container at a time.** A further call moves the observation, so a widget\n * whose content element is swapped at runtime releases the element it let go by\n * naming the new one — there is no second place for the release to drift from.\n */\n observeDescendantLoads(container: Element): void {\n this.unobserveDescendantLoads();\n this.#loadContainer = container;\n container.addEventListener(\"load\", this.#handleDescendantLoad, true);\n }\n\n /** Stops reporting descendant loads without affecting element or viewport observation. */\n unobserveDescendantLoads(): void {\n this.#loadContainer?.removeEventListener(\"load\", this.#handleDescendantLoad, true);\n this.#loadContainer = null;\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and removes\n * the viewport and descendant-load listeners. Safe to call multiple times. Call\n * this from a controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n this.unobserveDescendantLoads();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BlurDeferral } from \"../utils/blur_deferral\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\n\n/**\n * Sub-pixel tolerance (px) applied to the overflow comparison.\n *\n * Fractional layout metrics (zoom, `border-box` rounding, fractional font\n * metrics) routinely make `scrollWidth` exceed `clientWidth` by a fraction of a\n * pixel on a trail that visually fits, which would collapse it for no reason.\n */\nconst OVERFLOW_EPSILON = 1;\n\n/**\n * Headless, accessible responsive breadcrumb behavior.\n *\n * Markup contract (identifier: `stimeo--breadcrumb`):\n * <nav data-controller=\"stimeo--breadcrumb\" aria-label=\"Breadcrumb\">\n * <ol data-stimeo--breadcrumb-target=\"list\">\n * <li><a href=\"/\">Home</a></li>\n * <li data-stimeo--breadcrumb-target=\"ellipsis\" hidden>\n * <button type=\"button\" aria-expanded=\"false\" aria-controls=\"bc-a bc-b\"\n * aria-label=\"Show full path\"\n * data-stimeo--breadcrumb-target=\"trigger\"\n * data-action=\"click->stimeo--breadcrumb#toggle\">…</button>\n * </li>\n * <li id=\"bc-a\" data-stimeo--breadcrumb-target=\"collapsible\"><a href=\"/a\">Section A</a></li>\n * <li id=\"bc-b\" data-stimeo--breadcrumb-target=\"collapsible\"><a href=\"/a/b\">Sub B</a></li>\n * <li><a href=\"/a/b/c\" aria-current=\"page\">Item C</a></li>\n * </ol>\n * </nav>\n *\n * Implements the WAI-ARIA APG **Breadcrumb** pattern. The base structure (`nav`\n * + `ol` + `aria-current=\"page\"`) lives in the markup; this controller adds the\n * responsive behavior: when the trail overflows its container it collapses the\n * author-marked middle items behind a disclosure (`…`) button, which expands\n * them back on demand.\n *\n * `toggle` dispatches `{ expanded: boolean }`.\n *\n * @remarks\n * Behavior only — the consumer owns separators (CSS) and the look. The items to\n * collapse are the author-marked `collapsible` targets (the source of truth),\n * placed between the leading and trailing items that must always stay visible.\n *\n * Behavior provided:\n * - Detects overflow via a shared layout observer (element + viewport resize) and a\n * `MutationObserver` on the list (content edits, Turbo Stream / morph item\n * swaps); the public `update` action re-measures on demand.\n * - While overflowing and not expanded, hides the `collapsible` items and shows\n * the `ellipsis` item; when it fits, shows everything, hides the ellipsis and\n * resets the expanded state (so a later collapse starts collapsed).\n * - The disclosure `trigger` toggles `aria-expanded` and the collapsed items,\n * dispatching `stimeo--breadcrumb:toggle`. `toggle` is a no-op (no state\n * change, no event) while the trail fits, so a hidden trigger can never be\n * left reporting `aria-expanded=\"true\"`.\n *\n * Layout requirements the behavior depends on:\n * - **The list must not wrap** (`white-space: nowrap` / `flex-wrap: nowrap`). A\n * wrapping list grows in height instead of scroll width, so\n * `scrollWidth === clientWidth` always holds and overflow is never detected.\n * - **The list width must follow its container** (no shrink-to-fit / intrinsic\n * width). Overflow is re-measured synchronously inside the resize\n * notification, so a list that widens to its content would never report\n * overflow. There is deliberately no debounce: the pass is one write → one\n * forced read → one write on a single element.\n * - Consumers must not neutralize `[hidden]` (e.g. `display: inline-flex` on the\n * items wins over the UA's `[hidden] { display: none }`); collapsing is\n * expressed exclusively through the `hidden` attribute.\n *\n * Ownership of `hidden`: this controller owns the attribute on an element **for as\n * long as that element is a `collapsible` target and the controller is connected**.\n * Outside that window it does not write it:\n * - lose the marker while connected → the element becomes an always-visible item\n * and the controller clears the `hidden` it owned;\n * - controller `disconnect()` → the collapsed state is left in the DOM on purpose,\n * because that DOM is what Turbo caches and restores.\n *\n * A consumer that also wants to own `hidden` on the same element (for a reason of\n * its own) is not supported: the overflow measurement clears it on every pass to\n * read the expanded width.\n *\n * Focus safety: hiding the element the user is standing on would drop focus to\n * `<body>`. When a collapse would hide the focused item, focus moves to the\n * disclosure `trigger` first; when the ellipsis would be hidden while it holds\n * focus, the hide waits for `blur`.\n *\n * **Fail-safe when the disclosure is incomplete.** Collapsing needs all of\n * `list`, one `collapsible`, `ellipsis`, and `trigger`; miss any and the trail\n * degrades to a plain APG breadcrumb (every item visible, ellipsis hidden,\n * `aria-expanded=\"false\"`, `toggle` a silent no-op). Hiding the middle items\n * with no control that can reveal them puts the path out of reach for good — a\n * too-wide trail is the cheaper failure. Watched at runtime, so a swap that\n * breaks the set releases the items and one that completes it starts collapsing.\n */\nexport class BreadcrumbController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"collapsible\", \"ellipsis\", \"trigger\"];\n static actions = [\"toggle\", \"update\"] as const;\n static events = [\"toggle\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly collapsibleTargets: HTMLElement[];\n declare readonly ellipsisTarget: HTMLElement;\n declare readonly triggerTarget: HTMLElement;\n declare readonly hasListTarget: boolean;\n declare readonly hasEllipsisTarget: boolean;\n declare readonly hasTriggerTarget: boolean;\n\n /** Guards target callbacks that can fire outside the connected window. */\n #connected = false;\n /** Whether the trail currently overflows its container. */\n #overflowing = false;\n /** Whether the user has expanded the collapsed items via the disclosure. */\n #expanded = false;\n /** The list element currently observed (resize + mutations), if any. */\n #observedList: HTMLElement | null = null;\n #listMutations: MutationObserver | null = null;\n readonly #layout = new LayoutObserver(() => this.update());\n\n readonly #onListMutation = (): void => {\n this.update();\n };\n\n /** Holds the ellipsis hide back while it contains focus; re-renders on blur. */\n readonly #deferredHide = new BlurDeferral(() => {\n this.#render();\n });\n\n /** Starts observing for overflow and renders the initial state. */\n override connect(): void {\n this.#connected = true;\n // The DOM is the source of truth on reconnect (Turbo cache restore / morph):\n // the trigger's `aria-expanded` is where the expanded state lives, so a trail\n // the user opened survives a back-navigation instead of silently re-collapsing.\n this.#expanded = this.#initialExpanded();\n this.#layout.observeViewport();\n this.#syncListObservation();\n this.update();\n }\n\n /** Releases the resize/mutation observation (Turbo navigation included). */\n override disconnect(): void {\n this.#connected = false;\n this.#deferredHide.releaseAll();\n this.#stopObservingList();\n this.#layout.disconnect();\n }\n\n listTargetConnected(): void {\n this.#resync();\n }\n\n listTargetDisconnected(): void {\n this.#resync();\n }\n\n collapsibleTargetConnected(): void {\n this.#resync();\n }\n\n /**\n * Re-measures when an `ellipsis` target arrives. The `ellipsis` and `trigger`\n * targets form the disclosure set, a precondition for collapsing at all, so they\n * need the same watching the list and items get: without their callbacks a swap\n * that breaks or completes the set would only take effect at the next resize or\n * list mutation, which may never come.\n */\n ellipsisTargetConnected(): void {\n this.#resync();\n }\n\n /** Re-measures when an `ellipsis` target leaves the disclosure set. */\n ellipsisTargetDisconnected(): void {\n this.#resync();\n }\n\n /** Re-measures when a `trigger` target joins the disclosure set. */\n triggerTargetConnected(): void {\n this.#resync();\n }\n\n /** Re-measures when a `trigger` target leaves the disclosure set. */\n triggerTargetDisconnected(): void {\n this.#resync();\n }\n\n /**\n * Hands `hidden` back when an element stops being a `collapsible` target.\n *\n * The marker is the source of truth for what may be collapsed, so an element\n * that loses it while the controller is live is an always-visible item again —\n * and rendering only walks the *current* targets, so nothing else would ever\n * clear the `hidden` this controller put there.\n *\n * The `#connected` guard is load-bearing, not defensive. Stimulus fires this\n * callback for **every** target during teardown as well (`Context.disconnect()`\n * stops the target observer after `disconnect()` has run), and there the\n * collapsed DOM must survive untouched so Turbo's cache restores the trail in\n * the state the user left it (see the ownership note in {@link BreadcrumbController}).\n *\n * @param element - the element that just left the `collapsible` target set\n */\n collapsibleTargetDisconnected(element: HTMLElement): void {\n if (this.#connected) element.hidden = false;\n this.#resync();\n }\n\n /**\n * Expands or re-collapses the trail and dispatches `toggle`.\n *\n * No-op while the trail fits: there is nothing collapsed to reveal, and the\n * (hidden) trigger would otherwise keep a stale `aria-expanded=\"true\"` that\n * surfaces pre-expanded the next time the trail overflows. No event is\n * dispatched in that case.\n */\n toggle(): void {\n if (!this.#connected || !this.#overflowing) return;\n this.#expanded = !this.#expanded;\n this.#render();\n this.dispatch(\"toggle\", { detail: { expanded: this.#expanded } });\n }\n\n /**\n * Re-measures overflow and re-renders. Wired to resizes and list mutations;\n * exposed as an action so consumers can re-measure after a change the\n * observers cannot see (e.g. a web font swap that only alters text width).\n */\n update(): void {\n if (!this.#connected) return;\n this.#overflowing = this.#measureOverflow();\n if (!this.#overflowing) this.#expanded = false;\n this.#render();\n }\n\n /** Re-attaches the list observation (targets may have been swapped) and re-measures. */\n #resync(): void {\n if (!this.#connected) return;\n this.#syncListObservation();\n this.update();\n }\n\n /** Reads the expanded state back from its DOM home (the trigger's `aria-expanded`). */\n #initialExpanded(): boolean {\n return this.hasTriggerTarget && this.triggerTarget.getAttribute(\"aria-expanded\") === \"true\";\n }\n\n /**\n * Points the resize + mutation observation at the current `list` target.\n *\n * The list is re-resolved rather than captured at connect so a target swapped\n * at runtime (Turbo Stream replacement of the `<ol>`) is observed too. Only\n * `childList` / `subtree` / `characterData` are watched — deliberately **not**\n * `attributes`, which the controller's own `hidden` writes would re-trigger.\n */\n #syncListObservation(): void {\n const next = this.hasListTarget ? this.listTarget : null;\n if (next === this.#observedList) return;\n\n this.#stopObservingList();\n if (!next) return;\n\n this.#observedList = next;\n this.#layout.observe(next);\n if (typeof MutationObserver !== \"undefined\") {\n this.#listMutations = new MutationObserver(this.#onListMutation);\n this.#listMutations.observe(next, { childList: true, subtree: true, characterData: true });\n }\n }\n\n #stopObservingList(): void {\n if (this.#observedList) this.#layout.unobserve(this.#observedList);\n this.#observedList = null;\n this.#listMutations?.disconnect();\n this.#listMutations = null;\n }\n\n /**\n * Measures overflow against the **fully expanded** layout so hiding items does\n * not make the condition oscillate. Reveals every item, then compares the list's\n * own scroll width to its own client width; `#render` immediately re-applies\n * the correct hidden state afterward.\n *\n * Both widths are read from the list element itself (not the host `nav`) so the\n * check is independent of any padding/border on the host — comparing against the\n * host's `clientWidth` would over-report the available space by its padding and\n * miss real overflow in a padded container.\n *\n * With no `collapsible` items there is nothing to collapse, so the trail is\n * never reported as overflowing (which also keeps the disclosure out of the\n * tab order — a button controlling nothing).\n *\n * The full disclosure set is a precondition: collapsing without an `ellipsis`\n * *and* a `trigger` would hide items behind a control that does not exist, so\n * an incomplete set reports \"not overflowing\" and `#render` degrades the trail\n * to a plain breadcrumb. Losing layout is recoverable; losing the path is not.\n */\n #measureOverflow(): boolean {\n if (!this.hasListTarget || this.collapsibleTargets.length === 0) return false;\n if (!this.hasEllipsisTarget || !this.hasTriggerTarget) return false;\n for (const item of this.collapsibleTargets) item.hidden = false;\n // No `hasEllipsisTarget` check: the line above already returned without one.\n this.ellipsisTarget.hidden = true;\n return this.listTarget.scrollWidth > this.listTarget.clientWidth + OVERFLOW_EPSILON;\n }\n\n /**\n * Applies the collapsed/expanded state to the items, ellipsis, and trigger.\n */\n #render(): void {\n const collapsed = this.#overflowing && !this.#expanded;\n const showEllipsis = this.#overflowing && this.collapsibleTargets.length > 0;\n\n // Decided *before* the writes below: once a focused item is hidden the browser\n // drops focus to <body> and the information is gone.\n const rescueFocus =\n collapsed &&\n this.hasTriggerTarget &&\n this.collapsibleTargets.some((i) => this.#holdsFocus(i));\n\n for (const item of this.collapsibleTargets) item.hidden = collapsed;\n if (this.hasEllipsisTarget) this.#applyEllipsisVisibility(showEllipsis);\n if (this.hasTriggerTarget) {\n // Never report expanded while the trail fits: nothing is collapsed to reveal.\n const expanded = this.#overflowing && this.#expanded;\n this.triggerTarget.setAttribute(\"aria-expanded\", expanded ? \"true\" : \"false\");\n }\n if (rescueFocus) this.triggerTarget.focus();\n }\n\n /**\n * Shows or hides the ellipsis item, deferring a hide that would blur the user.\n *\n * Hiding the element that currently holds focus drops focus to `<body>`, so the\n * hide waits for that element's `blur` and is re-applied then (the shared\n * `BlurDeferral` registry).\n */\n #applyEllipsisVisibility(show: boolean): void {\n const ellipsis = this.ellipsisTarget;\n if (show) {\n this.#deferredHide.releaseAll();\n ellipsis.hidden = false;\n return;\n }\n\n const active = document.activeElement;\n if (active instanceof HTMLElement && ellipsis.contains(active)) {\n ellipsis.hidden = false;\n this.#deferredHide.deferOnly(active);\n return;\n }\n\n this.#deferredHide.releaseAll();\n ellipsis.hidden = true;\n }\n\n /** Whether `element` contains (or is) the currently focused element. */\n #holdsFocus(element: HTMLElement): boolean {\n const active = document.activeElement;\n return active instanceof HTMLElement && element.contains(active);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/blur_deferral.ts","../../src/utils/layout_observer.ts","../../src/controllers/breadcrumb_controller.ts"],"names":[],"mappings":"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","file":"breadcrumb_controller.js","sourcesContent":["/**\n * Shared bookkeeping for updates that must wait until an element loses focus.\n *\n * A control that becomes redundant while the user is standing on it cannot be\n * removed from the page immediately: hiding a focused element (or turning it\n * into a native `disabled` one) drops focus to `<body>`, stranding a keyboard\n * user mid-widget. Every affected controller handles it the same way — keep the\n * element reachable, hold the destructive update, listen for `blur`, and apply\n * it then.\n *\n * {@link BlurDeferral} owns *only* the registry part of that: which elements are\n * currently holding an update back, attaching and detaching the `blur` listener,\n * and guaranteeing teardown. It is deliberately **policy-free** — what the\n * interim state looks like (`aria-disabled`, an ownership marker, nothing at\n * all), and what the deferred update actually is, stay in the controller, which\n * receives the element back through its release callback.\n *\n * Three contracts are worth stating up front, because each one is a hazard the\n * consumers would otherwise hit:\n *\n * - **The release callback fires only on a real `blur`.** `release()` /\n * `releaseAll()` *cancel* a deferral; they do not complete it. Conflating the\n * two makes `overflow-indicator` disable a button that just left the boundary.\n * - **`elements` is a snapshot.** Iterating the live key set while releasing\n * inside the loop is the failure this registry exists to prevent.\n * - **The entry is detached before the callback runs**, so a controller that\n * decides the update is still unsafe can simply defer again.\n *\n * @example\n * ```ts\n * readonly #deferred = new BlurDeferral<HTMLElement>((trigger) => {\n * if (this.#connected) this.#evaluate();\n * });\n *\n * #hide(trigger: HTMLElement) {\n * if (document.activeElement === trigger) {\n * this.#deferred.deferOnly(trigger); // stays visible until blur\n * return;\n * }\n * this.#deferred.releaseAll();\n * trigger.hidden = true;\n * }\n *\n * disconnect() {\n * this.#deferred.releaseAll();\n * }\n * ```\n */\nexport class BlurDeferral<T extends HTMLElement = HTMLElement> {\n /** Elements currently holding an update back, mapped to their `blur` listener. */\n readonly #pending = new Map<T, () => void>();\n /** Called after a pending element blurs and has been detached. */\n readonly #onRelease: (element: T) => void;\n\n /** @param onRelease - Invoked once `element` actually blurs; never on `release`. */\n constructor(onRelease: (element: T) => void) {\n this.#onRelease = onRelease;\n }\n\n /** Number of elements currently holding an update back. */\n get size(): number {\n return this.#pending.size;\n }\n\n /** Snapshot of the pending elements, safe to iterate while releasing them. */\n get elements(): T[] {\n return [...this.#pending.keys()];\n }\n\n /** Whether `element` is currently holding an update back. */\n has(element: T): boolean {\n return this.#pending.has(element);\n }\n\n /** Holds an update back until `element` blurs. Idempotent (no stacked listeners). */\n defer(element: T): void {\n if (this.#pending.has(element)) return;\n const onBlur = (): void => {\n this.#detach(element);\n this.#onRelease(element);\n };\n this.#pending.set(element, onBlur);\n element.addEventListener(\"blur\", onBlur);\n }\n\n /** Defers `element` as the only pending entry, cancelling any others. */\n deferOnly(element: T): void {\n for (const pending of this.elements) {\n if (pending !== element) this.#detach(pending);\n }\n this.defer(element);\n }\n\n /** Cancels `element`'s deferral without completing it; no-ops when not pending. */\n release(element: T): void {\n this.#detach(element);\n }\n\n /** Cancels every deferral without completing any of them. */\n releaseAll(): void {\n for (const element of this.elements) this.#detach(element);\n }\n\n /** Removes the `blur` listener for `element` and forgets it. */\n #detach(element: T): void {\n const onBlur = this.#pending.get(element);\n if (onBlur) element.removeEventListener(\"blur\", onBlur);\n this.#pending.delete(element);\n }\n}\n","/**\n * Unified layout observation for Stimeo controllers.\n *\n * Widgets whose output is measured — an overflow boundary, a masonry column count,\n * an autosized textarea — have three sources that move the layout under them: their\n * *own* box changing — via {@link ResizeObserver} — the *viewport* changing — via the\n * `window` `resize` event — and a descendant *resource settling*, because an image or\n * a frame reports a height of zero until it has loaded. Wiring those three by hand in\n * every controller risks leaked listeners on `disconnect()`. {@link LayoutObserver}\n * owns all three behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * this.#layout.observeDescendantLoads(this.panelTarget);\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n #loadContainer: Element | null = null;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n /** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */\n readonly #handleDescendantLoad = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Starts reporting a `load` from anywhere inside `container` — an image or a\n * frame settling changes the box it sits in, and it measures as zero high until\n * then. `load` does not bubble, so the subscription is a capture-phase listener\n * on the container itself and nothing the caller spells.\n *\n * **One container at a time.** A further call moves the observation, so a widget\n * whose content element is swapped at runtime releases the element it let go by\n * naming the new one — there is no second place for the release to drift from.\n */\n observeDescendantLoads(container: Element): void {\n this.unobserveDescendantLoads();\n this.#loadContainer = container;\n container.addEventListener(\"load\", this.#handleDescendantLoad, true);\n }\n\n /** Stops reporting descendant loads without affecting element or viewport observation. */\n unobserveDescendantLoads(): void {\n this.#loadContainer?.removeEventListener(\"load\", this.#handleDescendantLoad, true);\n this.#loadContainer = null;\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and removes\n * the viewport and descendant-load listeners. Safe to call multiple times. Call\n * this from a controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n this.unobserveDescendantLoads();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BlurDeferral } from \"../utils/blur_deferral\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\n\n/**\n * Sub-pixel tolerance (px) applied to the overflow comparison.\n *\n * Fractional layout metrics (zoom, `border-box` rounding, fractional font\n * metrics) routinely make `scrollWidth` exceed `clientWidth` by a fraction of a\n * pixel on a trail that visually fits, which would collapse it for no reason.\n */\nconst OVERFLOW_EPSILON = 1;\n\n/**\n * Headless, accessible responsive breadcrumb behavior.\n *\n * Markup contract (identifier: `stimeo--breadcrumb`):\n * <nav data-controller=\"stimeo--breadcrumb\" aria-label=\"Breadcrumb\">\n * <ol data-stimeo--breadcrumb-target=\"list\">\n * <li><a href=\"/\">Home</a></li>\n * <li data-stimeo--breadcrumb-target=\"ellipsis\" hidden>\n * <button type=\"button\" aria-expanded=\"false\" aria-controls=\"bc-a bc-b\"\n * aria-label=\"Show full path\"\n * data-stimeo--breadcrumb-target=\"trigger\"\n * data-action=\"click->stimeo--breadcrumb#toggle\">…</button>\n * </li>\n * <li id=\"bc-a\" data-stimeo--breadcrumb-target=\"collapsible\"><a href=\"/a\">Section A</a></li>\n * <li id=\"bc-b\" data-stimeo--breadcrumb-target=\"collapsible\"><a href=\"/a/b\">Sub B</a></li>\n * <li><a href=\"/a/b/c\" aria-current=\"page\">Item C</a></li>\n * </ol>\n * </nav>\n *\n * Implements the WAI-ARIA APG **Breadcrumb** pattern. The base structure (`nav`\n * + `ol` + `aria-current=\"page\"`) lives in the markup; this controller adds the\n * responsive behavior: when the trail overflows its container it collapses the\n * author-marked middle items behind a disclosure (`…`) button, which expands\n * them back on demand.\n *\n * `toggle` dispatches `{ expanded: boolean }`.\n *\n * @remarks\n * Behavior only — the consumer owns separators (CSS) and the look. The items to\n * collapse are the author-marked `collapsible` targets (the source of truth),\n * placed between the leading and trailing items that must always stay visible.\n *\n * Behavior provided:\n * - Detects overflow via a shared layout observer (element + viewport resize) and a\n * `MutationObserver` on the list (content edits, Turbo Stream / morph item\n * swaps); the public `update` action re-measures on demand.\n * - While overflowing and not expanded, hides the `collapsible` items and shows\n * the `ellipsis` item; when it fits, shows everything, hides the ellipsis and\n * resets the expanded state (so a later collapse starts collapsed).\n * - The disclosure `trigger` toggles `aria-expanded` and the collapsed items,\n * dispatching `stimeo--breadcrumb:toggle`. `toggle` is a no-op (no state\n * change, no event) while the trail fits, so a hidden trigger can never be\n * left reporting `aria-expanded=\"true\"`.\n *\n * Layout requirements the behavior depends on:\n * - **The list must not wrap** (`white-space: nowrap` / `flex-wrap: nowrap`). A\n * wrapping list grows in height instead of scroll width, so\n * `scrollWidth === clientWidth` always holds and overflow is never detected.\n * - **The list width must follow its container** (no shrink-to-fit / intrinsic\n * width). Overflow is re-measured synchronously inside the resize\n * notification, so a list that widens to its content would never report\n * overflow. There is deliberately no debounce: the pass is one write → one\n * forced read → one write on a single element.\n * - Consumers must not neutralize `[hidden]` (e.g. `display: inline-flex` on the\n * items wins over the UA's `[hidden] { display: none }`); collapsing is\n * expressed exclusively through the `hidden` attribute.\n *\n * Ownership of `hidden`: this controller owns the attribute on an element **for as\n * long as that element is a `collapsible` target and the controller is connected**.\n * Outside that window it does not write it:\n * - lose the marker while connected → the element becomes an always-visible item\n * and the controller clears the `hidden` it owned;\n * - controller `disconnect()` → the collapsed state is left in the DOM on purpose,\n * because that DOM is what Turbo caches and restores.\n *\n * A consumer that also wants to own `hidden` on the same element (for a reason of\n * its own) is not supported: the overflow measurement clears it on every pass to\n * read the expanded width.\n *\n * Focus safety: hiding the element the user is standing on would drop focus to\n * `<body>`. When a collapse would hide the focused item, focus moves to the\n * disclosure `trigger` first; when the ellipsis would be hidden while it holds\n * focus, the hide waits for `blur`.\n *\n * **Fail-safe when the disclosure is incomplete.** Collapsing needs all of\n * `list`, one `collapsible`, `ellipsis`, and `trigger`; miss any and the trail\n * degrades to a plain APG breadcrumb (every item visible, ellipsis hidden,\n * `aria-expanded=\"false\"`, `toggle` a silent no-op). Hiding the middle items\n * with no control that can reveal them puts the path out of reach for good — a\n * too-wide trail is the cheaper failure. Watched at runtime, so a swap that\n * breaks the set releases the items and one that completes it starts collapsing.\n */\nexport class BreadcrumbController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"collapsible\", \"ellipsis\", \"trigger\"];\n static actions = [\"toggle\", \"update\"] as const;\n static events = [\"toggle\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly collapsibleTargets: HTMLElement[];\n declare readonly ellipsisTarget: HTMLElement;\n declare readonly triggerTarget: HTMLElement;\n declare readonly hasListTarget: boolean;\n declare readonly hasEllipsisTarget: boolean;\n declare readonly hasTriggerTarget: boolean;\n\n /** Guards target callbacks that can fire outside the connected window. */\n #connected = false;\n /** Whether the trail currently overflows its container. */\n #overflowing = false;\n /** Whether the user has expanded the collapsed items via the disclosure. */\n #expanded = false;\n /** The list element currently observed (resize + mutations), if any. */\n #observedList: HTMLElement | null = null;\n #listMutations: MutationObserver | null = null;\n readonly #layout = new LayoutObserver(() => this.update());\n\n readonly #onListMutation = (): void => {\n this.update();\n };\n\n /** Holds the ellipsis hide back while it contains focus; re-renders on blur. */\n readonly #deferredHide = new BlurDeferral(() => {\n this.#render();\n });\n\n /** Starts observing for overflow and renders the initial state. */\n override connect(): void {\n this.#connected = true;\n // The DOM is the source of truth on reconnect (Turbo cache restore / morph):\n // the trigger's `aria-expanded` is where the expanded state lives, so a trail\n // the user opened survives a back-navigation instead of silently re-collapsing.\n this.#expanded = this.#initialExpanded();\n this.#layout.observeViewport();\n this.#syncListObservation();\n this.update();\n }\n\n /** Releases the resize/mutation observation (Turbo navigation included). */\n override disconnect(): void {\n this.#connected = false;\n this.#deferredHide.releaseAll();\n this.#stopObservingList();\n this.#layout.disconnect();\n }\n\n listTargetConnected(): void {\n this.#resync();\n }\n\n listTargetDisconnected(): void {\n this.#resync();\n }\n\n collapsibleTargetConnected(): void {\n this.#resync();\n }\n\n /**\n * Re-measures when an `ellipsis` target arrives. The `ellipsis` and `trigger`\n * targets form the disclosure set, a precondition for collapsing at all, so they\n * need the same watching the list and items get: without their callbacks a swap\n * that breaks or completes the set would only take effect at the next resize or\n * list mutation, which may never come.\n */\n ellipsisTargetConnected(): void {\n this.#resync();\n }\n\n /** Re-measures when an `ellipsis` target leaves the disclosure set. */\n ellipsisTargetDisconnected(): void {\n this.#resync();\n }\n\n /** Re-measures when a `trigger` target joins the disclosure set. */\n triggerTargetConnected(): void {\n this.#resync();\n }\n\n /** Re-measures when a `trigger` target leaves the disclosure set. */\n triggerTargetDisconnected(): void {\n this.#resync();\n }\n\n /**\n * Hands `hidden` back when an element stops being a `collapsible` target.\n *\n * The marker is the source of truth for what may be collapsed, so an element\n * that loses it while the controller is live is an always-visible item again —\n * and rendering only walks the *current* targets, so nothing else would ever\n * clear the `hidden` this controller put there.\n *\n * The `#connected` guard is load-bearing, not defensive. Stimulus fires this\n * callback for **every** target during teardown as well (`Context.disconnect()`\n * stops the target observer after `disconnect()` has run), and there the\n * collapsed DOM must survive untouched so Turbo's cache restores the trail in\n * the state the user left it (see the ownership note in {@link BreadcrumbController}).\n *\n * @param element - the element that just left the `collapsible` target set\n */\n collapsibleTargetDisconnected(element: HTMLElement): void {\n if (this.#connected) element.hidden = false;\n this.#resync();\n }\n\n /**\n * Expands or re-collapses the trail and dispatches `toggle`.\n *\n * No-op while the trail fits: there is nothing collapsed to reveal, and the\n * (hidden) trigger would otherwise keep a stale `aria-expanded=\"true\"` that\n * surfaces pre-expanded the next time the trail overflows. No event is\n * dispatched in that case.\n */\n toggle(): void {\n if (!this.#connected || !this.#overflowing) return;\n this.#expanded = !this.#expanded;\n this.#render();\n this.dispatch(\"toggle\", { detail: { expanded: this.#expanded } });\n }\n\n /**\n * Re-measures overflow and re-renders. Wired to resizes and list mutations;\n * exposed as an action so consumers can re-measure after a change the\n * observers cannot see (e.g. a web font swap that only alters text width).\n */\n update(): void {\n if (!this.#connected) return;\n this.#overflowing = this.#measureOverflow();\n if (!this.#overflowing) this.#expanded = false;\n this.#render();\n }\n\n /** Re-attaches the list observation (targets may have been swapped) and re-measures. */\n #resync(): void {\n if (!this.#connected) return;\n this.#syncListObservation();\n this.update();\n }\n\n /** Reads the expanded state back from its DOM home (the trigger's `aria-expanded`). */\n #initialExpanded(): boolean {\n return this.hasTriggerTarget && this.triggerTarget.getAttribute(\"aria-expanded\") === \"true\";\n }\n\n /**\n * Points the resize + mutation observation at the current `list` target.\n *\n * The list is re-resolved rather than captured at connect so a target swapped\n * at runtime (Turbo Stream replacement of the `<ol>`) is observed too. Only\n * `childList` / `subtree` / `characterData` are watched — deliberately **not**\n * `attributes`, which the controller's own `hidden` writes would re-trigger.\n */\n #syncListObservation(): void {\n const next = this.hasListTarget ? this.listTarget : null;\n if (next === this.#observedList) return;\n\n this.#stopObservingList();\n if (!next) return;\n\n this.#observedList = next;\n this.#layout.observe(next);\n if (typeof MutationObserver !== \"undefined\") {\n this.#listMutations = new MutationObserver(this.#onListMutation);\n this.#listMutations.observe(next, { childList: true, subtree: true, characterData: true });\n }\n }\n\n #stopObservingList(): void {\n if (this.#observedList) this.#layout.unobserve(this.#observedList);\n this.#observedList = null;\n this.#listMutations?.disconnect();\n this.#listMutations = null;\n }\n\n /**\n * Measures overflow against the **fully expanded** layout so hiding items does\n * not make the condition oscillate. Reveals every item, then compares the list's\n * own scroll width to its own client width; `#render` immediately re-applies\n * the correct hidden state afterward.\n *\n * Both widths are read from the list element itself (not the host `nav`) so the\n * check is independent of any padding/border on the host — comparing against the\n * host's `clientWidth` would over-report the available space by its padding and\n * miss real overflow in a padded container.\n *\n * With no `collapsible` items there is nothing to collapse, so the trail is\n * never reported as overflowing (which also keeps the disclosure out of the\n * tab order — a button controlling nothing).\n *\n * The full disclosure set is a precondition: collapsing without an `ellipsis`\n * *and* a `trigger` would hide items behind a control that does not exist, so\n * an incomplete set reports \"not overflowing\" and `#render` degrades the trail\n * to a plain breadcrumb. Losing layout is recoverable; losing the path is not.\n */\n #measureOverflow(): boolean {\n if (!this.hasListTarget || this.collapsibleTargets.length === 0) return false;\n if (!this.hasEllipsisTarget || !this.hasTriggerTarget) return false;\n for (const item of this.collapsibleTargets) item.hidden = false;\n // No `hasEllipsisTarget` check: the line above already returned without one.\n this.ellipsisTarget.hidden = true;\n return this.listTarget.scrollWidth > this.listTarget.clientWidth + OVERFLOW_EPSILON;\n }\n\n /**\n * Applies the collapsed/expanded state to the items, ellipsis, and trigger.\n */\n #render(): void {\n const collapsed = this.#overflowing && !this.#expanded;\n const showEllipsis = this.#overflowing && this.collapsibleTargets.length > 0;\n\n // Decided *before* the writes below: once a focused item is hidden the browser\n // drops focus to <body> and the information is gone.\n const rescueFocus =\n collapsed &&\n this.hasTriggerTarget &&\n this.collapsibleTargets.some((i) => this.#holdsFocus(i));\n\n for (const item of this.collapsibleTargets) item.hidden = collapsed;\n if (this.hasEllipsisTarget) this.#applyEllipsisVisibility(showEllipsis);\n if (this.hasTriggerTarget) {\n // Never report expanded while the trail fits: nothing is collapsed to reveal.\n const expanded = this.#overflowing && this.#expanded;\n this.triggerTarget.setAttribute(\"aria-expanded\", expanded ? \"true\" : \"false\");\n }\n if (rescueFocus) this.triggerTarget.focus();\n }\n\n /**\n * Shows or hides the ellipsis item, deferring a hide that would blur the user.\n *\n * Hiding the element that currently holds focus drops focus to `<body>`, so the\n * hide waits for that element's `blur` and is re-applied then (the shared\n * `BlurDeferral` registry).\n */\n #applyEllipsisVisibility(show: boolean): void {\n const ellipsis = this.ellipsisTarget;\n if (show) {\n this.#deferredHide.releaseAll();\n ellipsis.hidden = false;\n return;\n }\n\n const active = document.activeElement;\n if (active instanceof HTMLElement && ellipsis.contains(active)) {\n ellipsis.hidden = false;\n this.#deferredHide.deferOnly(active);\n return;\n }\n\n this.#deferredHide.releaseAll();\n ellipsis.hidden = true;\n }\n\n /** Whether `element` contains (or is) the currently focused element. */\n #holdsFocus(element: HTMLElement): boolean {\n const active = document.activeElement;\n return active instanceof HTMLElement && element.contains(active);\n }\n}\n"]}
|
|
@@ -58,8 +58,8 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
58
58
|
* the select-all box first when the bar holds it, so the keyboard user keeps a Tab
|
|
59
59
|
* position instead of falling back to the document.
|
|
60
60
|
*
|
|
61
|
-
* A non-finite `totalCount` counts as the Value's default `0`, so the
|
|
62
|
-
* renders as `NaN
|
|
61
|
+
* A non-finite or negative `totalCount` counts as the Value's default `0`, so the
|
|
62
|
+
* count never renders as `NaN` or below zero. The attribute keeps what the page wrote.
|
|
63
63
|
*
|
|
64
64
|
* The delegated listener and the pending repair are both released on `disconnect()`.
|
|
65
65
|
*/
|
|
@@ -117,7 +117,7 @@ declare class BulkSelectController extends Controller<HTMLElement> {
|
|
|
117
117
|
countTargetDisconnected(): void;
|
|
118
118
|
/** Repaints the count for a total that changed at runtime. */
|
|
119
119
|
totalCountValueChanged(): void;
|
|
120
|
-
/**
|
|
120
|
+
/** Schedules a repair pass; the next announcement reads the current `announceText` itself. */
|
|
121
121
|
announceTextValueChanged(): void;
|
|
122
122
|
/** Clears every selection (rows + select-all) and exits all-pages mode. */
|
|
123
123
|
clear(): void;
|