stimeo-ui 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +163 -0
- package/dist/cable/index.d.ts +16 -16
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.d.ts +3 -2
- package/dist/controllers/accordion_controller.js +10 -0
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +64 -0
- package/dist/controllers/alert_dialog_controller.js +318 -0
- package/dist/controllers/alert_dialog_controller.js.map +1 -0
- package/dist/controllers/breadcrumb_controller.d.ts +97 -10
- package/dist/controllers/breadcrumb_controller.js +225 -13
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +11 -1
- package/dist/controllers/calendar_controller.js +89 -22
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +115 -0
- package/dist/controllers/carousel_controller.js +313 -0
- package/dist/controllers/carousel_controller.js.map +1 -0
- package/dist/controllers/clipboard_controller.d.ts +72 -0
- package/dist/controllers/clipboard_controller.js +144 -0
- package/dist/controllers/clipboard_controller.js.map +1 -0
- package/dist/controllers/collapsible_controller.d.ts +70 -0
- package/dist/controllers/collapsible_controller.js +327 -0
- package/dist/controllers/collapsible_controller.js.map +1 -0
- package/dist/controllers/color_picker_controller.d.ts +87 -0
- package/dist/controllers/color_picker_controller.js +252 -0
- package/dist/controllers/color_picker_controller.js.map +1 -0
- package/dist/controllers/combobox_controller.d.ts +14 -1
- package/dist/controllers/combobox_controller.js +162 -23
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +2 -0
- package/dist/controllers/command_palette_controller.js +194 -17
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +7 -6
- package/dist/controllers/context_menu_controller.js +32 -10
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.js +8 -1
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +72 -0
- package/dist/controllers/currency_input_controller.js +147 -0
- package/dist/controllers/currency_input_controller.js.map +1 -0
- package/dist/controllers/data_grid_controller.d.ts +89 -0
- package/dist/controllers/data_grid_controller.js +246 -0
- package/dist/controllers/data_grid_controller.js.map +1 -0
- package/dist/controllers/date_range_picker_controller.d.ts +88 -0
- package/dist/controllers/date_range_picker_controller.js +441 -0
- package/dist/controllers/date_range_picker_controller.js.map +1 -0
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.d.ts +57 -0
- package/dist/controllers/dismissible_controller.js +117 -0
- package/dist/controllers/dismissible_controller.js.map +1 -0
- package/dist/controllers/drawer_controller.d.ts +96 -0
- package/dist/controllers/drawer_controller.js +630 -0
- package/dist/controllers/drawer_controller.js.map +1 -0
- package/dist/controllers/dropdown_controller.d.ts +1 -2
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.d.ts +73 -0
- package/dist/controllers/editable_controller.js +169 -0
- package/dist/controllers/editable_controller.js.map +1 -0
- package/dist/controllers/file_dropzone_controller.d.ts +92 -0
- package/dist/controllers/file_dropzone_controller.js +165 -0
- package/dist/controllers/file_dropzone_controller.js.map +1 -0
- package/dist/controllers/filter_controller.d.ts +60 -0
- package/dist/controllers/filter_controller.js +86 -0
- package/dist/controllers/filter_controller.js.map +1 -0
- package/dist/controllers/flash_controller.js +36 -5
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +1 -1
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +1 -1
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js +6 -4
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +1 -1
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js +67 -19
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js +54 -11
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +9 -2
- package/dist/controllers/listbox_controller.js +257 -53
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.js +2 -2
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +54 -0
- package/dist/controllers/masonry_controller.js +142 -0
- package/dist/controllers/masonry_controller.js.map +1 -0
- package/dist/controllers/menu_controller.d.ts +29 -11
- package/dist/controllers/menu_controller.js +104 -17
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +142 -0
- package/dist/controllers/menubar_controller.js +785 -0
- package/dist/controllers/menubar_controller.js.map +1 -0
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +141 -0
- package/dist/controllers/multi_select_controller.js +755 -0
- package/dist/controllers/multi_select_controller.js.map +1 -0
- package/dist/controllers/navigation_menu_controller.d.ts +146 -0
- package/dist/controllers/navigation_menu_controller.js +511 -0
- package/dist/controllers/navigation_menu_controller.js.map +1 -0
- package/dist/controllers/nested_form_controller.d.ts +4 -5
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +1 -1
- package/dist/controllers/number_input_controller.js +7 -0
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/otp_controller.js +18 -1
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +15 -6
- package/dist/controllers/overflow_indicator_controller.js +246 -27
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +78 -19
- package/dist/controllers/overflow_menu_controller.js +381 -57
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +36 -5
- package/dist/controllers/pagination_controller.js +163 -32
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +49 -0
- package/dist/controllers/password_reveal_controller.js +117 -0
- package/dist/controllers/password_reveal_controller.js.map +1 -0
- package/dist/controllers/persist_controller.js +6 -6
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +2 -3
- package/dist/controllers/pointer_drag_controller.js +9 -1
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +1 -2
- package/dist/controllers/popover_controller.js +2 -2
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +4 -4
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.js +22 -3
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +97 -0
- package/dist/controllers/range_slider_controller.js +192 -0
- package/dist/controllers/range_slider_controller.js.map +1 -0
- package/dist/controllers/rating_controller.js +16 -2
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +52 -0
- package/dist/controllers/read_more_controller.js +238 -0
- package/dist/controllers/read_more_controller.js.map +1 -0
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +3 -1
- package/dist/controllers/resizable_controller.js +65 -1
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.js +17 -2
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js +101 -14
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.d.ts +54 -0
- package/dist/controllers/scroll_restore_controller.js +93 -0
- package/dist/controllers/scroll_restore_controller.js.map +1 -0
- package/dist/controllers/scroll_visibility_controller.d.ts +6 -3
- package/dist/controllers/scroll_visibility_controller.js +40 -6
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +64 -15
- package/dist/controllers/scrollspy_controller.js +369 -74
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +69 -0
- package/dist/controllers/separator_controller.js +96 -0
- package/dist/controllers/separator_controller.js.map +1 -0
- package/dist/controllers/sidebar_controller.d.ts +107 -0
- package/dist/controllers/sidebar_controller.js +761 -0
- package/dist/controllers/sidebar_controller.js.map +1 -0
- package/dist/controllers/skeleton_controller.js +1 -1
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +11 -1
- package/dist/controllers/slider_controller.js +32 -6
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.d.ts +3 -1
- package/dist/controllers/sortable_controller.js +34 -3
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.js +1 -1
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +4 -0
- package/dist/controllers/stepper_controller.js +28 -12
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +5 -6
- package/dist/controllers/stick_to_bottom_controller.js +9 -4
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +17 -4
- package/dist/controllers/sticky_observer_controller.js +109 -20
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +2 -2
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.js +1 -0
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +13 -4
- package/dist/controllers/tabs_controller.js +26 -3
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +91 -0
- package/dist/controllers/tags_input_controller.js +295 -0
- package/dist/controllers/tags_input_controller.js.map +1 -0
- package/dist/controllers/theme_controller.d.ts +1 -1
- package/dist/controllers/theme_controller.js +42 -13
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +67 -0
- package/dist/controllers/time_picker_controller.js +231 -0
- package/dist/controllers/time_picker_controller.js.map +1 -0
- package/dist/controllers/toast_controller.d.ts +5 -14
- package/dist/controllers/toast_controller.js +40 -14
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.js +23 -2
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.d.ts +53 -13
- package/dist/controllers/toolbar_controller.js +230 -31
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +1 -1
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +14 -8
- package/dist/controllers/transition_controller.js +153 -38
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +135 -0
- package/dist/controllers/tree_view_controller.js +691 -0
- package/dist/controllers/tree_view_controller.js.map +1 -0
- package/dist/index.d.ts +27 -1818
- package/dist/index.js +4256 -915
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +375 -39
- package/dist/inspector/cli.js +228 -14
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +419 -48
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +26 -26
- package/dist/inspector/manifest.json +1054 -127
- package/package.json +5 -6
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/color_picker_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/logical_scroll.ts
|
|
6
|
+
function isRtl(element) {
|
|
7
|
+
return window.getComputedStyle(element).direction === "rtl";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// src/utils/arrow_step.ts
|
|
11
|
+
function logicalArrowKey(key, element) {
|
|
12
|
+
if (key !== "ArrowRight" && key !== "ArrowLeft") return key;
|
|
13
|
+
if (!isRtl(element)) return key;
|
|
14
|
+
return key === "ArrowRight" ? "ArrowLeft" : "ArrowRight";
|
|
15
|
+
}
|
|
16
|
+
function isReservedArrowChord(event, allow = []) {
|
|
17
|
+
if (!event.key.startsWith("Arrow")) return false;
|
|
18
|
+
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/utils/coerce.ts
|
|
22
|
+
function toFiniteNumber(raw) {
|
|
23
|
+
if (raw === null || raw === void 0 || raw === "") return null;
|
|
24
|
+
const value = typeof raw === "number" ? raw : Number(raw);
|
|
25
|
+
return Number.isFinite(value) ? value : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/controllers/color_picker_controller.ts
|
|
29
|
+
var COLOR_PROPERTY = "--stimeo-color";
|
|
30
|
+
var CHANNEL_RANGE = {
|
|
31
|
+
hue: [0, 360],
|
|
32
|
+
saturation: [0, 100],
|
|
33
|
+
lightness: [0, 100],
|
|
34
|
+
alpha: [0, 100]
|
|
35
|
+
};
|
|
36
|
+
var ColorPickerController = class extends Controller {
|
|
37
|
+
static targets = ["slider", "hex", "preview", "field"];
|
|
38
|
+
static values = {
|
|
39
|
+
value: { type: String, default: "#000000" },
|
|
40
|
+
alpha: { type: Boolean, default: false },
|
|
41
|
+
logicalTrack: { type: Boolean, default: false }
|
|
42
|
+
};
|
|
43
|
+
static actions = ["onHexInput", "onKeydown", "onPointerDown"];
|
|
44
|
+
static events = ["change"];
|
|
45
|
+
/** Whether the consumer declared a mirroring track and the direction mirrors it. */
|
|
46
|
+
get #mirrored() {
|
|
47
|
+
return this.logicalTrackValue && isRtl(this.element);
|
|
48
|
+
}
|
|
49
|
+
/** The current color in the editing model. */
|
|
50
|
+
#color = { hue: 0, saturation: 0, lightness: 0, alpha: 100 };
|
|
51
|
+
/** Aborts in-progress pointer-drag listeners on drag end / teardown. */
|
|
52
|
+
#dragAbort = null;
|
|
53
|
+
/** Seeds the model from the initial hex value and renders every surface. */
|
|
54
|
+
connect() {
|
|
55
|
+
const parsed = hexToHsla(this.valueValue);
|
|
56
|
+
if (parsed) this.#color = this.alphaValue ? parsed : { ...parsed, alpha: 100 };
|
|
57
|
+
this.#render();
|
|
58
|
+
}
|
|
59
|
+
/** Cancels any active pointer drag so document listeners never leak. */
|
|
60
|
+
disconnect() {
|
|
61
|
+
this.#dragAbort?.abort();
|
|
62
|
+
this.#dragAbort = null;
|
|
63
|
+
}
|
|
64
|
+
/** Keyboard stepping on the focused channel slider (APG Slider model). */
|
|
65
|
+
onKeydown(event) {
|
|
66
|
+
if (isReservedArrowChord(event)) return;
|
|
67
|
+
const slider = event.currentTarget;
|
|
68
|
+
const channel = this.#channelOf(slider);
|
|
69
|
+
if (!channel) return;
|
|
70
|
+
const [min, max] = this.#rangeOf(slider, channel);
|
|
71
|
+
const value = this.#color[channel];
|
|
72
|
+
let next = null;
|
|
73
|
+
switch (this.#mirrored ? logicalArrowKey(event.key, this.element) : event.key) {
|
|
74
|
+
case "ArrowRight":
|
|
75
|
+
case "ArrowUp":
|
|
76
|
+
next = value + 1;
|
|
77
|
+
break;
|
|
78
|
+
case "ArrowLeft":
|
|
79
|
+
case "ArrowDown":
|
|
80
|
+
next = value - 1;
|
|
81
|
+
break;
|
|
82
|
+
case "PageUp":
|
|
83
|
+
next = value + 10;
|
|
84
|
+
break;
|
|
85
|
+
case "PageDown":
|
|
86
|
+
next = value - 10;
|
|
87
|
+
break;
|
|
88
|
+
case "Home":
|
|
89
|
+
next = min;
|
|
90
|
+
break;
|
|
91
|
+
case "End":
|
|
92
|
+
next = max;
|
|
93
|
+
break;
|
|
94
|
+
default:
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
this.#setChannel(channel, next, min, max);
|
|
99
|
+
}
|
|
100
|
+
/** Begins a pointer drag on a channel slider and tracks movement. */
|
|
101
|
+
onPointerDown(event) {
|
|
102
|
+
const slider = event.currentTarget;
|
|
103
|
+
const channel = this.#channelOf(slider);
|
|
104
|
+
if (!channel) return;
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
slider.focus();
|
|
107
|
+
const [min, max] = this.#rangeOf(slider, channel);
|
|
108
|
+
const mirrored = this.#mirrored;
|
|
109
|
+
const update = (clientX) => {
|
|
110
|
+
const rect = slider.getBoundingClientRect();
|
|
111
|
+
if (rect.width === 0) return;
|
|
112
|
+
const offset = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
|
|
113
|
+
const fraction = mirrored ? 1 - offset : offset;
|
|
114
|
+
this.#setChannel(channel, min + fraction * (max - min), min, max);
|
|
115
|
+
};
|
|
116
|
+
update(event.clientX);
|
|
117
|
+
this.#dragAbort?.abort();
|
|
118
|
+
const abort = new AbortController();
|
|
119
|
+
this.#dragAbort = abort;
|
|
120
|
+
const onMove = (move) => update(move.clientX);
|
|
121
|
+
const onUp = () => {
|
|
122
|
+
abort.abort();
|
|
123
|
+
this.#dragAbort = null;
|
|
124
|
+
};
|
|
125
|
+
document.addEventListener("pointermove", onMove, { signal: abort.signal });
|
|
126
|
+
document.addEventListener("pointerup", onUp, { signal: abort.signal });
|
|
127
|
+
document.addEventListener("pointercancel", onUp, { signal: abort.signal });
|
|
128
|
+
}
|
|
129
|
+
/** Parses the hex input on confirm and syncs every channel + surface. */
|
|
130
|
+
onHexInput() {
|
|
131
|
+
if (!this.hasHexTarget) return;
|
|
132
|
+
const parsed = hexToHsla(this.hexTarget.value);
|
|
133
|
+
if (!parsed) {
|
|
134
|
+
this.hexTarget.value = this.#hexString();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
this.#color = this.alphaValue ? parsed : { ...parsed, alpha: 100 };
|
|
138
|
+
this.#render();
|
|
139
|
+
}
|
|
140
|
+
/** Clamps and snaps one channel to an integer, then re-renders + emits change. */
|
|
141
|
+
#setChannel(channel, raw, min, max) {
|
|
142
|
+
this.#color[channel] = Math.round(Math.min(max, Math.max(min, raw)));
|
|
143
|
+
this.#render();
|
|
144
|
+
}
|
|
145
|
+
/** Reflects the model onto sliders, the hex input, preview, and form field. */
|
|
146
|
+
#render() {
|
|
147
|
+
for (const slider of this.sliderTargets) {
|
|
148
|
+
const channel = this.#channelOf(slider);
|
|
149
|
+
if (!channel) continue;
|
|
150
|
+
const value = this.#color[channel];
|
|
151
|
+
slider.setAttribute("aria-valuenow", String(value));
|
|
152
|
+
slider.setAttribute("aria-valuetext", valueText(channel, value));
|
|
153
|
+
}
|
|
154
|
+
const hex = this.#hexString();
|
|
155
|
+
if (this.hasHexTarget) this.hexTarget.value = hex;
|
|
156
|
+
for (const field of this.fieldTargets) field.value = hex;
|
|
157
|
+
for (const preview of this.previewTargets) preview.style.setProperty(COLOR_PROPERTY, hex);
|
|
158
|
+
this.element.style.setProperty(COLOR_PROPERTY, hex);
|
|
159
|
+
const rgb = hslToRgb(this.#color.hue, this.#color.saturation, this.#color.lightness);
|
|
160
|
+
this.dispatch("change", {
|
|
161
|
+
detail: { value: hex, rgba: { ...rgb, a: this.#color.alpha / 100 } }
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/** The current color as `#RRGGBB`, or `#RRGGBBAA` when alpha is enabled. */
|
|
165
|
+
#hexString() {
|
|
166
|
+
const rgb = hslToRgb(this.#color.hue, this.#color.saturation, this.#color.lightness);
|
|
167
|
+
const base = `#${hex2(rgb.r)}${hex2(rgb.g)}${hex2(rgb.b)}`;
|
|
168
|
+
if (!this.alphaValue) return base;
|
|
169
|
+
return `${base}${hex2(Math.round(this.#color.alpha / 100 * 255))}`;
|
|
170
|
+
}
|
|
171
|
+
/** Reads a slider's `data-channel`, if it is a known channel. */
|
|
172
|
+
#channelOf(slider) {
|
|
173
|
+
const channel = slider.getAttribute("data-channel");
|
|
174
|
+
return channel && channel in CHANNEL_RANGE ? channel : null;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* A slider's `[min, max]` from aria-valuemin/max, falling back per channel.
|
|
178
|
+
* An absent or blank attribute means "not authored", not zero — coercing it to
|
|
179
|
+
* a number would pin the channel at `0` and make the per-channel default
|
|
180
|
+
* unreachable.
|
|
181
|
+
*/
|
|
182
|
+
#rangeOf(slider, channel) {
|
|
183
|
+
const [defMin, defMax] = CHANNEL_RANGE[channel];
|
|
184
|
+
return [
|
|
185
|
+
toFiniteNumber(slider.getAttribute("aria-valuemin")) ?? defMin,
|
|
186
|
+
toFiniteNumber(slider.getAttribute("aria-valuemax")) ?? defMax
|
|
187
|
+
];
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
function valueText(channel, value) {
|
|
191
|
+
const label = channel.charAt(0).toUpperCase() + channel.slice(1);
|
|
192
|
+
const unit = channel === "hue" ? "degrees" : "percent";
|
|
193
|
+
return `${label} ${value} ${unit}`;
|
|
194
|
+
}
|
|
195
|
+
function hex2(value) {
|
|
196
|
+
return Math.round(Math.min(255, Math.max(0, value))).toString(16).padStart(2, "0");
|
|
197
|
+
}
|
|
198
|
+
function hslToRgb(h, s, l) {
|
|
199
|
+
const sat = s / 100;
|
|
200
|
+
const light = l / 100;
|
|
201
|
+
const c = (1 - Math.abs(2 * light - 1)) * sat;
|
|
202
|
+
const hp = (h % 360 + 360) % 360 / 60;
|
|
203
|
+
const x = c * (1 - Math.abs(hp % 2 - 1));
|
|
204
|
+
let r = 0;
|
|
205
|
+
let g = 0;
|
|
206
|
+
let b = 0;
|
|
207
|
+
if (hp >= 0 && hp < 1) [r, g, b] = [c, x, 0];
|
|
208
|
+
else if (hp < 2) [r, g, b] = [x, c, 0];
|
|
209
|
+
else if (hp < 3) [r, g, b] = [0, c, x];
|
|
210
|
+
else if (hp < 4) [r, g, b] = [0, x, c];
|
|
211
|
+
else if (hp < 5) [r, g, b] = [x, 0, c];
|
|
212
|
+
else [r, g, b] = [c, 0, x];
|
|
213
|
+
const m = light - c / 2;
|
|
214
|
+
return {
|
|
215
|
+
r: Math.round((r + m) * 255),
|
|
216
|
+
g: Math.round((g + m) * 255),
|
|
217
|
+
b: Math.round((b + m) * 255)
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function rgbToHsl(r, g, b) {
|
|
221
|
+
const rn = r / 255;
|
|
222
|
+
const gn = g / 255;
|
|
223
|
+
const bn = b / 255;
|
|
224
|
+
const max = Math.max(rn, gn, bn);
|
|
225
|
+
const min = Math.min(rn, gn, bn);
|
|
226
|
+
const delta = max - min;
|
|
227
|
+
const l = (max + min) / 2;
|
|
228
|
+
let h = 0;
|
|
229
|
+
if (delta !== 0) {
|
|
230
|
+
if (max === rn) h = (gn - bn) / delta % 6;
|
|
231
|
+
else if (max === gn) h = (bn - rn) / delta + 2;
|
|
232
|
+
else h = (rn - gn) / delta + 4;
|
|
233
|
+
h = (h * 60 + 360) % 360;
|
|
234
|
+
}
|
|
235
|
+
const s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
|
|
236
|
+
return { h: Math.round(h), s: Math.round(s * 100), l: Math.round(l * 100) };
|
|
237
|
+
}
|
|
238
|
+
function hexToHsla(input) {
|
|
239
|
+
const hex = input.trim().replace(/^#/, "");
|
|
240
|
+
if (!/^(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(hex)) return null;
|
|
241
|
+
const full = hex.length <= 4 ? hex.split("").map((char) => char + char).join("") : hex;
|
|
242
|
+
const r = Number.parseInt(full.slice(0, 2), 16);
|
|
243
|
+
const g = Number.parseInt(full.slice(2, 4), 16);
|
|
244
|
+
const b = Number.parseInt(full.slice(4, 6), 16);
|
|
245
|
+
const a = full.length === 8 ? Number.parseInt(full.slice(6, 8), 16) : 255;
|
|
246
|
+
const { h, s, l } = rgbToHsl(r, g, b);
|
|
247
|
+
return { hue: h, saturation: s, lightness: l, alpha: Math.round(a / 255 * 100) };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export { ColorPickerController };
|
|
251
|
+
//# sourceMappingURL=color_picker_controller.js.map
|
|
252
|
+
//# sourceMappingURL=color_picker_controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/coerce.ts","../../src/controllers/color_picker_controller.ts"],"names":[],"mappings":"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","file":"color_picker_controller.js","sourcesContent":["/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n","/**\n * Numeric coercion shared by the value-bearing controllers (progress, meter,\n * color-picker).\n *\n * Stimulus already coerces numeric action params to numbers, but a value can also\n * arrive as a string — via a `*:set` CustomEvent `detail`, or an action param\n * whose attribute does not look numeric. Centralizing the parse keeps `setValue`\n * tolerant of either form while rejecting anything that is not a finite number.\n */\n\n/**\n * Coerces `raw` to a finite number, or returns `null` when it is absent, empty,\n * or not parseable. Empty strings are treated as \"no value\" rather than `0`, so a\n * stray blank param cannot silently reset the value.\n */\nexport function toFiniteNumber(raw: number | string | null | undefined): number | null {\n if (raw === null || raw === undefined || raw === \"\") return null;\n const value = typeof raw === \"number\" ? raw : Number(raw);\n return Number.isFinite(value) ? value : null;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord, logicalArrowKey } from \"../utils/arrow_step\";\nimport { toFiniteNumber } from \"../utils/coerce\";\nimport { isRtl } from \"../utils/logical_scroll\";\n\n/** CSS custom property exposing the current color to consumer CSS. */\nconst COLOR_PROPERTY = \"--stimeo-color\";\n\n/** A color channel slider, identified by its `data-channel` attribute. */\ntype Channel = \"hue\" | \"saturation\" | \"lightness\" | \"alpha\";\n\n/** Default `[min, max]` per channel when the slider omits aria-valuemin/max. */\nconst CHANNEL_RANGE: Record<Channel, [number, number]> = {\n hue: [0, 360],\n saturation: [0, 100],\n lightness: [0, 100],\n alpha: [0, 100],\n};\n\n/** Internal color model: HSL plus an alpha percentage (0–100). */\ninterface Hsla {\n hue: number;\n saturation: number;\n lightness: number;\n alpha: number;\n}\n\n/**\n * Headless, accessible **Color Picker** behavior.\n *\n * Markup contract (identifier: `stimeo--color-picker`):\n * <div data-controller=\"stimeo--color-picker\"\n * data-stimeo--color-picker-value-value=\"#3366cc\">\n * <div role=\"slider\" aria-label=\"Hue\" data-channel=\"hue\" tabindex=\"0\"\n * aria-valuemin=\"0\" aria-valuemax=\"360\" aria-valuenow=\"210\"\n * data-stimeo--color-picker-target=\"slider\"\n * data-action=\"keydown->stimeo--color-picker#onKeydown\n * pointerdown->stimeo--color-picker#onPointerDown\"></div>\n * <!-- saturation / lightness / alpha sliders share the same contract -->\n * <input type=\"text\" aria-label=\"Hex color\"\n * data-stimeo--color-picker-target=\"hex\"\n * data-action=\"change->stimeo--color-picker#onHexInput\" />\n * <div data-stimeo--color-picker-target=\"preview\" aria-hidden=\"true\"></div>\n * <input type=\"hidden\" data-stimeo--color-picker-target=\"field\" />\n * </div>\n *\n * Decomposes color selection into independent APG **Slider** channels (hue,\n * saturation, lightness, optional alpha) instead of a 2-D palette, so every\n * adjustment is keyboard- and screen-reader-operable. Each slider exposes\n * `aria-valuenow` and a human-readable `aria-valuetext` (e.g. \"Hue 210 degrees\");\n * the hex input stays two-way synced; the current color is published on the\n * preview and root as the `--stimeo-color` custom property and mirrored into a\n * hidden form field.\n *\n * @remarks\n * Behavior only — the swatch/gradient visuals are the consumer's CSS/canvas, fed\n * by `--stimeo-color`. Only the consumer knows whether a channel track mirrors\n * under RTL: set `logicalTrack` to declare that it does, and the pointer mapping\n * and horizontal arrow pair follow the writing direction. Left unset, nothing\n * here reads `direction`. A gradient has no logical `to` keyword, so mirroring\n * one means swapping `to right`/`to left` under a `:dir(rtl)` selector.\n *\n * Pointer-drag listeners on `document` are bound to an\n * {@link AbortController} and released on drag end and on `disconnect()` (Turbo\n * navigation included). Color is fully reconstructable from the `value` (hex), so\n * there is no transient state to restore after a Turbo cache/morph.\n *\n * The internal model is integer HSL(A), so a hex → HSL → hex round-trip is not\n * exactly bijective: a typed hex can normalize to a near (not identical) value\n * once the HSL sliders are touched. This keeps the model small and zero-dep; use a\n * dedicated color library on the consumer side if exact hex preservation matters.\n */\nexport class ColorPickerController extends Controller<HTMLElement> {\n static override targets = [\"slider\", \"hex\", \"preview\", \"field\"];\n static override values = {\n value: { type: String, default: \"#000000\" },\n alpha: { type: Boolean, default: false },\n logicalTrack: { type: Boolean, default: false },\n };\n static actions = [\"onHexInput\", \"onKeydown\", \"onPointerDown\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly sliderTargets: HTMLElement[];\n declare readonly hexTarget: HTMLInputElement;\n declare readonly hasHexTarget: boolean;\n declare readonly previewTargets: HTMLElement[];\n declare readonly fieldTargets: HTMLInputElement[];\n declare valueValue: string;\n declare alphaValue: boolean;\n declare logicalTrackValue: boolean;\n\n /** Whether the consumer declared a mirroring track and the direction mirrors it. */\n get #mirrored(): boolean {\n return this.logicalTrackValue && isRtl(this.element);\n }\n\n /** The current color in the editing model. */\n #color: Hsla = { hue: 0, saturation: 0, lightness: 0, alpha: 100 };\n /** Aborts in-progress pointer-drag listeners on drag end / teardown. */\n #dragAbort: AbortController | null = null;\n\n /** Seeds the model from the initial hex value and renders every surface. */\n override connect(): void {\n const parsed = hexToHsla(this.valueValue);\n // When alpha is disabled, drop any alpha carried by an `#RRGGBBAA` value so\n // the model stays opaque — otherwise `hexString()` would emit `#RRGGBB`\n // while `change` reported `rgba.a < 1`. Mirrors `onHexInput()`.\n if (parsed) this.#color = this.alphaValue ? parsed : { ...parsed, alpha: 100 };\n this.#render();\n }\n\n /** Cancels any active pointer drag so document listeners never leak. */\n override disconnect(): void {\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n }\n\n /** Keyboard stepping on the focused channel slider (APG Slider model). */\n onKeydown(event: KeyboardEvent): void {\n if (isReservedArrowChord(event)) return;\n const slider = event.currentTarget as HTMLElement;\n const channel = this.#channelOf(slider);\n if (!channel) return;\n\n const [min, max] = this.#rangeOf(slider, channel);\n const value = this.#color[channel];\n let next: number | null = null;\n // On a mirrored track the greater value sits at the visual left, so the\n // horizontal pair trades places; the vertical pair passes through.\n switch (this.#mirrored ? logicalArrowKey(event.key, this.element) : event.key) {\n case \"ArrowRight\":\n case \"ArrowUp\":\n next = value + 1;\n break;\n case \"ArrowLeft\":\n case \"ArrowDown\":\n next = value - 1;\n break;\n case \"PageUp\":\n next = value + 10;\n break;\n case \"PageDown\":\n next = value - 10;\n break;\n case \"Home\":\n next = min;\n break;\n case \"End\":\n next = max;\n break;\n default:\n return;\n }\n event.preventDefault();\n this.#setChannel(channel, next, min, max);\n }\n\n /** Begins a pointer drag on a channel slider and tracks movement. */\n onPointerDown(event: PointerEvent): void {\n const slider = event.currentTarget as HTMLElement;\n const channel = this.#channelOf(slider);\n if (!channel) return;\n event.preventDefault();\n slider.focus();\n\n const [min, max] = this.#rangeOf(slider, channel);\n // Resolve the direction once for the whole gesture: reading it per move\n // would query computed style on every frame, and a drag that flipped\n // mid-gesture would be incoherent anyway.\n const mirrored = this.#mirrored;\n const update = (clientX: number): void => {\n const rect = slider.getBoundingClientRect();\n if (rect.width === 0) return;\n const offset = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));\n const fraction = mirrored ? 1 - offset : offset;\n this.#setChannel(channel, min + fraction * (max - min), min, max);\n };\n update(event.clientX);\n\n this.#dragAbort?.abort();\n const abort = new AbortController();\n this.#dragAbort = abort;\n const onMove = (move: PointerEvent): void => update(move.clientX);\n const onUp = (): void => {\n abort.abort();\n this.#dragAbort = null;\n };\n document.addEventListener(\"pointermove\", onMove, { signal: abort.signal });\n document.addEventListener(\"pointerup\", onUp, { signal: abort.signal });\n document.addEventListener(\"pointercancel\", onUp, { signal: abort.signal });\n }\n\n /** Parses the hex input on confirm and syncs every channel + surface. */\n onHexInput(): void {\n if (!this.hasHexTarget) return;\n const parsed = hexToHsla(this.hexTarget.value);\n if (!parsed) {\n // Reject invalid input by restoring the last valid hex.\n this.hexTarget.value = this.#hexString();\n return;\n }\n this.#color = this.alphaValue ? parsed : { ...parsed, alpha: 100 };\n this.#render();\n }\n\n /** Clamps and snaps one channel to an integer, then re-renders + emits change. */\n #setChannel(channel: Channel, raw: number, min: number, max: number): void {\n this.#color[channel] = Math.round(Math.min(max, Math.max(min, raw)));\n this.#render();\n }\n\n /** Reflects the model onto sliders, the hex input, preview, and form field. */\n #render(): void {\n for (const slider of this.sliderTargets) {\n const channel = this.#channelOf(slider);\n if (!channel) continue;\n const value = this.#color[channel];\n slider.setAttribute(\"aria-valuenow\", String(value));\n slider.setAttribute(\"aria-valuetext\", valueText(channel, value));\n }\n\n const hex = this.#hexString();\n if (this.hasHexTarget) this.hexTarget.value = hex;\n for (const field of this.fieldTargets) field.value = hex;\n for (const preview of this.previewTargets) preview.style.setProperty(COLOR_PROPERTY, hex);\n this.element.style.setProperty(COLOR_PROPERTY, hex);\n\n const rgb = hslToRgb(this.#color.hue, this.#color.saturation, this.#color.lightness);\n this.dispatch(\"change\", {\n detail: { value: hex, rgba: { ...rgb, a: this.#color.alpha / 100 } },\n });\n }\n\n /** The current color as `#RRGGBB`, or `#RRGGBBAA` when alpha is enabled. */\n #hexString(): string {\n const rgb = hslToRgb(this.#color.hue, this.#color.saturation, this.#color.lightness);\n const base = `#${hex2(rgb.r)}${hex2(rgb.g)}${hex2(rgb.b)}`;\n if (!this.alphaValue) return base;\n return `${base}${hex2(Math.round((this.#color.alpha / 100) * 255))}`;\n }\n\n /** Reads a slider's `data-channel`, if it is a known channel. */\n #channelOf(slider: HTMLElement): Channel | null {\n const channel = slider.getAttribute(\"data-channel\");\n return channel && channel in CHANNEL_RANGE ? (channel as Channel) : null;\n }\n\n /**\n * A slider's `[min, max]` from aria-valuemin/max, falling back per channel.\n * An absent or blank attribute means \"not authored\", not zero — coercing it to\n * a number would pin the channel at `0` and make the per-channel default\n * unreachable.\n */\n #rangeOf(slider: HTMLElement, channel: Channel): [number, number] {\n const [defMin, defMax] = CHANNEL_RANGE[channel];\n return [\n toFiniteNumber(slider.getAttribute(\"aria-valuemin\")) ?? defMin,\n toFiniteNumber(slider.getAttribute(\"aria-valuemax\")) ?? defMax,\n ];\n }\n}\n\n/** Capitalizes a channel name for `aria-valuetext` (e.g. \"Hue\"). */\nfunction valueText(channel: Channel, value: number): string {\n const label = channel.charAt(0).toUpperCase() + channel.slice(1);\n const unit = channel === \"hue\" ? \"degrees\" : \"percent\";\n return `${label} ${value} ${unit}`;\n}\n\n/** Formats a 0–255 byte as a 2-digit lowercase hex pair. */\nfunction hex2(value: number): string {\n return Math.round(Math.min(255, Math.max(0, value)))\n .toString(16)\n .padStart(2, \"0\");\n}\n\n/** Converts HSL (h:0–360, s/l:0–100) to RGB bytes (0–255). */\nfunction hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } {\n const sat = s / 100;\n const light = l / 100;\n const c = (1 - Math.abs(2 * light - 1)) * sat;\n const hp = (((h % 360) + 360) % 360) / 60;\n const x = c * (1 - Math.abs((hp % 2) - 1));\n let r = 0;\n let g = 0;\n let b = 0;\n if (hp >= 0 && hp < 1) [r, g, b] = [c, x, 0];\n else if (hp < 2) [r, g, b] = [x, c, 0];\n else if (hp < 3) [r, g, b] = [0, c, x];\n else if (hp < 4) [r, g, b] = [0, x, c];\n else if (hp < 5) [r, g, b] = [x, 0, c];\n else [r, g, b] = [c, 0, x];\n const m = light - c / 2;\n return {\n r: Math.round((r + m) * 255),\n g: Math.round((g + m) * 255),\n b: Math.round((b + m) * 255),\n };\n}\n\n/** Converts RGB bytes (0–255) to HSL (h:0–360, s/l:0–100). */\nfunction rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {\n const rn = r / 255;\n const gn = g / 255;\n const bn = b / 255;\n const max = Math.max(rn, gn, bn);\n const min = Math.min(rn, gn, bn);\n const delta = max - min;\n const l = (max + min) / 2;\n\n let h = 0;\n if (delta !== 0) {\n if (max === rn) h = ((gn - bn) / delta) % 6;\n else if (max === gn) h = (bn - rn) / delta + 2;\n else h = (rn - gn) / delta + 4;\n h = (h * 60 + 360) % 360;\n }\n const s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));\n return { h: Math.round(h), s: Math.round(s * 100), l: Math.round(l * 100) };\n}\n\n/**\n * Parses `#RGB`, `#RGBA`, `#RRGGBB`, or `#RRGGBBAA` into the HSLA model, or\n * returns null when the string is not a valid hex color.\n */\nfunction hexToHsla(input: string): Hsla | null {\n const hex = input.trim().replace(/^#/, \"\");\n if (!/^(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(hex)) return null;\n\n // Expand shorthand (#RGB / #RGBA) so every channel is a full byte pair.\n const full =\n hex.length <= 4\n ? hex\n .split(\"\")\n .map((char) => char + char)\n .join(\"\")\n : hex;\n\n const r = Number.parseInt(full.slice(0, 2), 16);\n const g = Number.parseInt(full.slice(2, 4), 16);\n const b = Number.parseInt(full.slice(4, 6), 16);\n const a = full.length === 8 ? Number.parseInt(full.slice(6, 8), 16) : 255;\n\n const { h, s, l } = rgbToHsl(r, g, b);\n return { hue: h, saturation: s, lightness: l, alpha: Math.round((a / 255) * 100) };\n}\n"]}
|
|
@@ -61,6 +61,10 @@ declare class ComboboxController extends Controller<HTMLElement> {
|
|
|
61
61
|
inputTargetConnected(input: HTMLInputElement): void;
|
|
62
62
|
/** Removes composition listeners when the active input is replaced or removed. */
|
|
63
63
|
inputTargetDisconnected(input: HTMLInputElement): void;
|
|
64
|
+
/** Establishes the active-state baseline, then reconciles a runtime addition. */
|
|
65
|
+
optionTargetConnected(option: HTMLElement): void;
|
|
66
|
+
/** Clears the disconnected node's active state, then reconciles the survivors. */
|
|
67
|
+
optionTargetDisconnected(option: HTMLElement): void;
|
|
64
68
|
/** Filters confirmed input text and opens the listbox. */
|
|
65
69
|
filter(event?: InputEvent): void;
|
|
66
70
|
/**
|
|
@@ -69,7 +73,16 @@ declare class ComboboxController extends Controller<HTMLElement> {
|
|
|
69
73
|
* re-opening with a stale non-matching value still surfaces the empty state).
|
|
70
74
|
*/
|
|
71
75
|
open(): void;
|
|
72
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Closes the listbox, clears the active option, and updates ARIA state.
|
|
78
|
+
*
|
|
79
|
+
* Survives a missing input in both directions. {@link inputTargetConnected}
|
|
80
|
+
* accepts an input added after connect, so dereferencing it here would abort
|
|
81
|
+
* `connect()` before it registers the outside-click listener, and nothing
|
|
82
|
+
* re-registers it later. Symmetrically, an input removed while the popup is
|
|
83
|
+
* open must still let the popup come down — so hiding the list is
|
|
84
|
+
* unconditional and only the ARIA half is guarded.
|
|
85
|
+
*/
|
|
73
86
|
close(): void;
|
|
74
87
|
/** Routes keyboard interaction per the APG combobox model. */
|
|
75
88
|
onKeydown(event: KeyboardEvent): void;
|
|
@@ -2,6 +2,24 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/combobox_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/active_option.ts
|
|
6
|
+
function syncActiveOption(options, active) {
|
|
7
|
+
let written = 0;
|
|
8
|
+
for (const option of options) {
|
|
9
|
+
const next = option === active ? "true" : "false";
|
|
10
|
+
if (option.getAttribute("aria-selected") === next) continue;
|
|
11
|
+
option.setAttribute("aria-selected", next);
|
|
12
|
+
written += 1;
|
|
13
|
+
}
|
|
14
|
+
return written;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/utils/arrow_step.ts
|
|
18
|
+
function isReservedArrowChord(event, allow = []) {
|
|
19
|
+
if (!event.key.startsWith("Arrow")) return false;
|
|
20
|
+
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
21
|
+
}
|
|
22
|
+
|
|
5
23
|
// src/utils/composition_tracker.ts
|
|
6
24
|
var CompositionTracker = class {
|
|
7
25
|
#observedTargets = /* @__PURE__ */ new Set();
|
|
@@ -49,6 +67,39 @@ var CompositionTracker = class {
|
|
|
49
67
|
};
|
|
50
68
|
};
|
|
51
69
|
|
|
70
|
+
// src/utils/microtask_coalescer.ts
|
|
71
|
+
var MicrotaskCoalescer = class {
|
|
72
|
+
#run;
|
|
73
|
+
#queued = false;
|
|
74
|
+
#active = false;
|
|
75
|
+
#generation = 0;
|
|
76
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
77
|
+
constructor(run) {
|
|
78
|
+
this.#run = run;
|
|
79
|
+
}
|
|
80
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
81
|
+
activate() {
|
|
82
|
+
this.#active = true;
|
|
83
|
+
}
|
|
84
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
85
|
+
cancel() {
|
|
86
|
+
this.#active = false;
|
|
87
|
+
this.#queued = false;
|
|
88
|
+
this.#generation += 1;
|
|
89
|
+
}
|
|
90
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
91
|
+
schedule() {
|
|
92
|
+
if (!this.#active || this.#queued) return;
|
|
93
|
+
this.#queued = true;
|
|
94
|
+
const generation = this.#generation;
|
|
95
|
+
queueMicrotask(() => {
|
|
96
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
97
|
+
this.#queued = false;
|
|
98
|
+
this.#run();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
52
103
|
// src/utils/option_scroll.ts
|
|
53
104
|
function scrollOptionIntoView(list, option) {
|
|
54
105
|
if (list.scrollHeight <= list.clientHeight) return;
|
|
@@ -66,8 +117,11 @@ var ComboboxController = class extends Controller {
|
|
|
66
117
|
static targets = ["input", "list", "option"];
|
|
67
118
|
static actions = ["close", "filter", "onKeydown", "open", "selectByClick"];
|
|
68
119
|
static events = ["selected"];
|
|
69
|
-
/**
|
|
70
|
-
#
|
|
120
|
+
/** Stable ID of the active option; the live element is resolved before every use. */
|
|
121
|
+
#activeId = null;
|
|
122
|
+
#connected = false;
|
|
123
|
+
/** Collapses one mutation batch of target callbacks into a single pass. */
|
|
124
|
+
#reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());
|
|
71
125
|
/**
|
|
72
126
|
* Suppresses {@link open} for the duration of the programmatic re-focus in
|
|
73
127
|
* `#select`, so committing a value (which returns focus to the input)
|
|
@@ -80,21 +134,36 @@ var ComboboxController = class extends Controller {
|
|
|
80
134
|
connect() {
|
|
81
135
|
if (this.hasInputTarget) this.#composition.observe(this.inputTarget);
|
|
82
136
|
this.close();
|
|
83
|
-
document.addEventListener("click", this.#onOutsideClick);
|
|
137
|
+
document.addEventListener("click", this.#onOutsideClick, true);
|
|
138
|
+
this.#connected = true;
|
|
139
|
+
this.#reconcile.activate();
|
|
84
140
|
}
|
|
85
141
|
/** Removes the document-level listener registered in {@link connect}. */
|
|
86
142
|
disconnect() {
|
|
143
|
+
this.#connected = false;
|
|
144
|
+
this.#reconcile.cancel();
|
|
87
145
|
this.#composition.disconnect();
|
|
88
|
-
document.removeEventListener("click", this.#onOutsideClick);
|
|
146
|
+
document.removeEventListener("click", this.#onOutsideClick, true);
|
|
89
147
|
}
|
|
90
148
|
/** Tracks an input added initially or after connect. */
|
|
91
149
|
inputTargetConnected(input) {
|
|
92
150
|
this.#composition.observe(input);
|
|
151
|
+
if (this.#connected) this.#queueOptionReconciliation();
|
|
93
152
|
}
|
|
94
153
|
/** Removes composition listeners when the active input is replaced or removed. */
|
|
95
154
|
inputTargetDisconnected(input) {
|
|
96
155
|
this.#composition.unobserve(input);
|
|
97
156
|
}
|
|
157
|
+
/** Establishes the active-state baseline, then reconciles a runtime addition. */
|
|
158
|
+
optionTargetConnected(option) {
|
|
159
|
+
option.setAttribute("aria-selected", "false");
|
|
160
|
+
if (this.#connected) this.#queueOptionReconciliation();
|
|
161
|
+
}
|
|
162
|
+
/** Clears the disconnected node's active state, then reconciles the survivors. */
|
|
163
|
+
optionTargetDisconnected(option) {
|
|
164
|
+
option.setAttribute("aria-selected", "false");
|
|
165
|
+
if (this.#connected) this.#queueOptionReconciliation();
|
|
166
|
+
}
|
|
98
167
|
/** Filters confirmed input text and opens the listbox. */
|
|
99
168
|
filter(event) {
|
|
100
169
|
if (this.#composition.isComposing(event)) return;
|
|
@@ -106,7 +175,7 @@ var ComboboxController = class extends Controller {
|
|
|
106
175
|
* re-opening with a stale non-matching value still surfaces the empty state).
|
|
107
176
|
*/
|
|
108
177
|
open() {
|
|
109
|
-
if (!this.hasListTarget || this.#suppressOpen) return;
|
|
178
|
+
if (!this.hasListTarget || !this.hasInputTarget || this.#suppressOpen) return;
|
|
110
179
|
this.#applyFilter();
|
|
111
180
|
this.listTarget.hidden = false;
|
|
112
181
|
this.inputTarget.setAttribute("aria-expanded", "true");
|
|
@@ -124,24 +193,47 @@ var ComboboxController = class extends Controller {
|
|
|
124
193
|
option.hidden = query.length > 0 && !text.includes(query);
|
|
125
194
|
}
|
|
126
195
|
}
|
|
127
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* Closes the listbox, clears the active option, and updates ARIA state.
|
|
198
|
+
*
|
|
199
|
+
* Survives a missing input in both directions. {@link inputTargetConnected}
|
|
200
|
+
* accepts an input added after connect, so dereferencing it here would abort
|
|
201
|
+
* `connect()` before it registers the outside-click listener, and nothing
|
|
202
|
+
* re-registers it later. Symmetrically, an input removed while the popup is
|
|
203
|
+
* open must still let the popup come down — so hiding the list is
|
|
204
|
+
* unconditional and only the ARIA half is guarded.
|
|
205
|
+
*/
|
|
128
206
|
close() {
|
|
129
207
|
if (!this.hasListTarget) return;
|
|
130
208
|
this.listTarget.hidden = true;
|
|
131
|
-
this.inputTarget.setAttribute("aria-expanded", "false");
|
|
132
|
-
this.#setActive(-1);
|
|
133
209
|
this.element.removeAttribute("data-stimeo--combobox-empty");
|
|
210
|
+
if (this.hasInputTarget) this.inputTarget.setAttribute("aria-expanded", "false");
|
|
211
|
+
this.#setActive(-1);
|
|
134
212
|
}
|
|
135
213
|
/** Routes keyboard interaction per the APG combobox model. */
|
|
136
214
|
onKeydown(event) {
|
|
137
215
|
if (this.#composition.isComposing(event)) return;
|
|
216
|
+
if (event.defaultPrevented) return;
|
|
217
|
+
if (isReservedArrowChord(event, ["alt"])) return;
|
|
218
|
+
if (event.altKey && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
|
|
219
|
+
if (event.key === "ArrowDown" ? !this.#isClosed : this.#isClosed) return;
|
|
220
|
+
event.preventDefault();
|
|
221
|
+
if (event.key === "ArrowDown") {
|
|
222
|
+
this.open();
|
|
223
|
+
} else {
|
|
224
|
+
this.close();
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (!this.#isClosed) this.#reconcileActive();
|
|
138
229
|
switch (event.key) {
|
|
139
230
|
case "ArrowDown": {
|
|
140
231
|
event.preventDefault();
|
|
141
232
|
if (this.#isClosed) this.open();
|
|
142
233
|
const visible = this.#visibleOptions();
|
|
143
234
|
if (visible.length > 0) {
|
|
144
|
-
const
|
|
235
|
+
const activeIndex = this.#findActiveIndex(visible);
|
|
236
|
+
const next = activeIndex === -1 ? 0 : (activeIndex + 1) % visible.length;
|
|
145
237
|
this.#setActive(next);
|
|
146
238
|
}
|
|
147
239
|
break;
|
|
@@ -151,7 +243,8 @@ var ComboboxController = class extends Controller {
|
|
|
151
243
|
if (this.#isClosed) this.open();
|
|
152
244
|
const visible = this.#visibleOptions();
|
|
153
245
|
if (visible.length > 0) {
|
|
154
|
-
const
|
|
246
|
+
const activeIndex = this.#findActiveIndex(visible);
|
|
247
|
+
const next = activeIndex === -1 ? visible.length - 1 : (activeIndex - 1 + visible.length) % visible.length;
|
|
155
248
|
this.#setActive(next);
|
|
156
249
|
}
|
|
157
250
|
break;
|
|
@@ -172,7 +265,8 @@ var ComboboxController = class extends Controller {
|
|
|
172
265
|
}
|
|
173
266
|
case "Enter": {
|
|
174
267
|
const visible = this.#visibleOptions();
|
|
175
|
-
const
|
|
268
|
+
const activeIndex = this.#findActiveIndex(visible);
|
|
269
|
+
const active = activeIndex === -1 ? void 0 : visible[activeIndex];
|
|
176
270
|
if (active) {
|
|
177
271
|
event.preventDefault();
|
|
178
272
|
this.#select(active);
|
|
@@ -180,7 +274,7 @@ var ComboboxController = class extends Controller {
|
|
|
180
274
|
break;
|
|
181
275
|
}
|
|
182
276
|
case "Escape":
|
|
183
|
-
if (
|
|
277
|
+
if (this.#isClosed) break;
|
|
184
278
|
event.preventDefault();
|
|
185
279
|
this.close();
|
|
186
280
|
break;
|
|
@@ -200,11 +294,24 @@ var ComboboxController = class extends Controller {
|
|
|
200
294
|
/** Selects the clicked option. Bound via `data-action` (click). */
|
|
201
295
|
selectByClick(event) {
|
|
202
296
|
const option = event.currentTarget.closest('[role="option"]');
|
|
203
|
-
if (option) this.#select(option);
|
|
297
|
+
if (option && this.optionTargets.includes(option)) this.#select(option);
|
|
204
298
|
}
|
|
205
|
-
/**
|
|
299
|
+
/**
|
|
300
|
+
* Commits an option: fills the input, closes the listbox, notifies listeners.
|
|
301
|
+
*
|
|
302
|
+
* An input removed while the popup is open leaves the options clickable, and
|
|
303
|
+
* {@link close} already survives that state. Selection does too: the popup
|
|
304
|
+
* comes down and listeners still hear the choice, with only the field-bound
|
|
305
|
+
* half — the value write, the focus return, and the native `change` — skipped,
|
|
306
|
+
* because there is no field to carry them.
|
|
307
|
+
*/
|
|
206
308
|
#select(option) {
|
|
207
309
|
const value = option.dataset.value ?? (option.textContent ?? "").trim();
|
|
310
|
+
if (!this.hasInputTarget) {
|
|
311
|
+
this.close();
|
|
312
|
+
this.dispatch("selected", { detail: { value } });
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
208
315
|
const changed = this.inputTarget.value !== value;
|
|
209
316
|
this.inputTarget.value = value;
|
|
210
317
|
this.close();
|
|
@@ -235,19 +342,51 @@ var ComboboxController = class extends Controller {
|
|
|
235
342
|
* input's `aria-activedescendant`. Pass `-1` to clear the active option.
|
|
236
343
|
*/
|
|
237
344
|
#setActive(index) {
|
|
238
|
-
this.#activeIndex = index;
|
|
239
345
|
const visible = this.#visibleOptions();
|
|
240
|
-
const active = index === -1 ? null : visible[index];
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
346
|
+
const active = index === -1 ? null : visible[index] ?? null;
|
|
347
|
+
syncActiveOption(this.optionTargets, active);
|
|
348
|
+
this.#activeId = active?.id || null;
|
|
349
|
+
if (this.hasInputTarget) {
|
|
350
|
+
if (active?.id) {
|
|
351
|
+
this.inputTarget.setAttribute("aria-activedescendant", active.id);
|
|
352
|
+
} else {
|
|
353
|
+
this.inputTarget.removeAttribute("aria-activedescendant");
|
|
354
|
+
}
|
|
248
355
|
}
|
|
249
356
|
if (active && this.hasListTarget) scrollOptionIntoView(this.listTarget, active);
|
|
250
357
|
}
|
|
358
|
+
/** Re-applies filtering and active state after a target collection change. */
|
|
359
|
+
#reconcileOptions() {
|
|
360
|
+
if (this.hasInputTarget && !this.#isClosed) this.#applyFilter();
|
|
361
|
+
this.#reconcileActive();
|
|
362
|
+
this.#reflectEmptyState();
|
|
363
|
+
}
|
|
364
|
+
/** Preserves a surviving active ID; disappearance deliberately clears active state. */
|
|
365
|
+
#reconcileActive() {
|
|
366
|
+
if (!this.hasInputTarget || this.#isClosed) {
|
|
367
|
+
this.#setActive(-1);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const visible = this.#visibleOptions();
|
|
371
|
+
const index = this.#findActiveIndex(visible);
|
|
372
|
+
const active = index === -1 ? null : visible[index] ?? null;
|
|
373
|
+
const selected = this.optionTargets.filter(
|
|
374
|
+
(option) => option.getAttribute("aria-selected") === "true"
|
|
375
|
+
);
|
|
376
|
+
const idref = this.inputTarget.getAttribute("aria-activedescendant");
|
|
377
|
+
const stateMatches = active ? this.#activeId === (active.id || null) && selected.length === 1 && selected[0] === active && idref === (active.id || null) : this.#activeId === null && selected.length === 0 && idref === null;
|
|
378
|
+
if (!stateMatches) this.#setActive(index);
|
|
379
|
+
}
|
|
380
|
+
/** Resolves the stable ID, with `aria-selected` as the ID-less compatibility marker. */
|
|
381
|
+
#findActiveIndex(options) {
|
|
382
|
+
const activeId = (this.hasInputTarget ? this.inputTarget.getAttribute("aria-activedescendant") : null) ?? this.#activeId;
|
|
383
|
+
if (activeId) return options.findIndex((option) => option.id === activeId);
|
|
384
|
+
return options.findIndex((option) => option.getAttribute("aria-selected") === "true");
|
|
385
|
+
}
|
|
386
|
+
/** Coalesces all target callbacks from one MutationObserver batch. */
|
|
387
|
+
#queueOptionReconciliation() {
|
|
388
|
+
this.#reconcile.schedule();
|
|
389
|
+
}
|
|
251
390
|
/** The options currently shown (not filtered out). */
|
|
252
391
|
#visibleOptions() {
|
|
253
392
|
return this.optionTargets.filter((option) => !option.hidden);
|