stimeo-ui 0.1.0-beta.3 → 0.2.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.
Files changed (174) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +6 -6
  3. package/dist/controllers/accordion_controller.d.ts +3 -2
  4. package/dist/controllers/accordion_controller.js.map +1 -1
  5. package/dist/controllers/alert_dialog_controller.d.ts +64 -0
  6. package/dist/controllers/alert_dialog_controller.js +318 -0
  7. package/dist/controllers/alert_dialog_controller.js.map +1 -0
  8. package/dist/controllers/auto_submit_controller.js +56 -21
  9. package/dist/controllers/auto_submit_controller.js.map +1 -1
  10. package/dist/controllers/carousel_controller.d.ts +100 -0
  11. package/dist/controllers/carousel_controller.js +272 -0
  12. package/dist/controllers/carousel_controller.js.map +1 -0
  13. package/dist/controllers/character_counter_controller.js +52 -15
  14. package/dist/controllers/character_counter_controller.js.map +1 -1
  15. package/dist/controllers/clipboard_controller.d.ts +72 -0
  16. package/dist/controllers/clipboard_controller.js +144 -0
  17. package/dist/controllers/clipboard_controller.js.map +1 -0
  18. package/dist/controllers/collapsible_controller.d.ts +70 -0
  19. package/dist/controllers/collapsible_controller.js +327 -0
  20. package/dist/controllers/collapsible_controller.js.map +1 -0
  21. package/dist/controllers/color_picker_controller.d.ts +76 -0
  22. package/dist/controllers/color_picker_controller.js +213 -0
  23. package/dist/controllers/color_picker_controller.js.map +1 -0
  24. package/dist/controllers/combobox_controller.d.ts +6 -2
  25. package/dist/controllers/combobox_controller.js +64 -3
  26. package/dist/controllers/combobox_controller.js.map +1 -1
  27. package/dist/controllers/command_palette_controller.d.ts +13 -6
  28. package/dist/controllers/command_palette_controller.js +243 -39
  29. package/dist/controllers/command_palette_controller.js.map +1 -1
  30. package/dist/controllers/confirm_controller.js +94 -8
  31. package/dist/controllers/confirm_controller.js.map +1 -1
  32. package/dist/controllers/context_menu_controller.d.ts +11 -6
  33. package/dist/controllers/context_menu_controller.js +191 -13
  34. package/dist/controllers/context_menu_controller.js.map +1 -1
  35. package/dist/controllers/count_up_controller.js +8 -1
  36. package/dist/controllers/count_up_controller.js.map +1 -1
  37. package/dist/controllers/countdown_controller.d.ts +2 -2
  38. package/dist/controllers/countdown_controller.js +2 -2
  39. package/dist/controllers/countdown_controller.js.map +1 -1
  40. package/dist/controllers/currency_input_controller.d.ts +72 -0
  41. package/dist/controllers/currency_input_controller.js +147 -0
  42. package/dist/controllers/currency_input_controller.js.map +1 -0
  43. package/dist/controllers/data_grid_controller.d.ts +71 -0
  44. package/dist/controllers/data_grid_controller.js +168 -0
  45. package/dist/controllers/data_grid_controller.js.map +1 -0
  46. package/dist/controllers/date_range_picker_controller.d.ts +87 -0
  47. package/dist/controllers/date_range_picker_controller.js +417 -0
  48. package/dist/controllers/date_range_picker_controller.js.map +1 -0
  49. package/dist/controllers/dialog_controller.js +94 -8
  50. package/dist/controllers/dialog_controller.js.map +1 -1
  51. package/dist/controllers/dismissible_controller.d.ts +57 -0
  52. package/dist/controllers/dismissible_controller.js +117 -0
  53. package/dist/controllers/dismissible_controller.js.map +1 -0
  54. package/dist/controllers/drawer_controller.d.ts +96 -0
  55. package/dist/controllers/drawer_controller.js +630 -0
  56. package/dist/controllers/drawer_controller.js.map +1 -0
  57. package/dist/controllers/dropdown_controller.d.ts +9 -6
  58. package/dist/controllers/dropdown_controller.js +139 -16
  59. package/dist/controllers/dropdown_controller.js.map +1 -1
  60. package/dist/controllers/editable_controller.d.ts +73 -0
  61. package/dist/controllers/editable_controller.js +168 -0
  62. package/dist/controllers/editable_controller.js.map +1 -0
  63. package/dist/controllers/file_dropzone_controller.d.ts +92 -0
  64. package/dist/controllers/file_dropzone_controller.js +165 -0
  65. package/dist/controllers/file_dropzone_controller.js.map +1 -0
  66. package/dist/controllers/filter_controller.d.ts +60 -0
  67. package/dist/controllers/filter_controller.js +86 -0
  68. package/dist/controllers/filter_controller.js.map +1 -0
  69. package/dist/controllers/flash_controller.js +36 -5
  70. package/dist/controllers/flash_controller.js.map +1 -1
  71. package/dist/controllers/focus_controller.js +94 -8
  72. package/dist/controllers/focus_controller.js.map +1 -1
  73. package/dist/controllers/form_validation_controller.js +2 -2
  74. package/dist/controllers/form_validation_controller.js.map +1 -1
  75. package/dist/controllers/highlight_controller.js +6 -4
  76. package/dist/controllers/highlight_controller.js.map +1 -1
  77. package/dist/controllers/hover_card_controller.d.ts +15 -12
  78. package/dist/controllers/hover_card_controller.js +103 -27
  79. package/dist/controllers/hover_card_controller.js.map +1 -1
  80. package/dist/controllers/intersection_controller.js +41 -18
  81. package/dist/controllers/intersection_controller.js.map +1 -1
  82. package/dist/controllers/lazy_frame_controller.js +33 -11
  83. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  84. package/dist/controllers/listbox_controller.js +1 -0
  85. package/dist/controllers/listbox_controller.js.map +1 -1
  86. package/dist/controllers/masonry_controller.d.ts +54 -0
  87. package/dist/controllers/masonry_controller.js +142 -0
  88. package/dist/controllers/masonry_controller.js.map +1 -0
  89. package/dist/controllers/menu_controller.d.ts +18 -11
  90. package/dist/controllers/menu_controller.js +181 -8
  91. package/dist/controllers/menu_controller.js.map +1 -1
  92. package/dist/controllers/menubar_controller.d.ts +62 -0
  93. package/dist/controllers/menubar_controller.js +433 -0
  94. package/dist/controllers/menubar_controller.js.map +1 -0
  95. package/dist/controllers/multi_select_controller.d.ts +115 -0
  96. package/dist/controllers/multi_select_controller.js +472 -0
  97. package/dist/controllers/multi_select_controller.js.map +1 -0
  98. package/dist/controllers/navigation_menu_controller.d.ts +99 -0
  99. package/dist/controllers/navigation_menu_controller.js +384 -0
  100. package/dist/controllers/navigation_menu_controller.js.map +1 -0
  101. package/dist/controllers/otp_controller.js +62 -22
  102. package/dist/controllers/otp_controller.js.map +1 -1
  103. package/dist/controllers/overflow_indicator_controller.d.ts +12 -4
  104. package/dist/controllers/overflow_indicator_controller.js +178 -27
  105. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  106. package/dist/controllers/overflow_menu_controller.d.ts +3 -2
  107. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  108. package/dist/controllers/password_reveal_controller.d.ts +49 -0
  109. package/dist/controllers/password_reveal_controller.js +117 -0
  110. package/dist/controllers/password_reveal_controller.js.map +1 -0
  111. package/dist/controllers/pointer_drag_controller.js +3 -3
  112. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  113. package/dist/controllers/popover_controller.d.ts +18 -10
  114. package/dist/controllers/popover_controller.js +118 -27
  115. package/dist/controllers/popover_controller.js.map +1 -1
  116. package/dist/controllers/range_slider_controller.d.ts +87 -0
  117. package/dist/controllers/range_slider_controller.js +166 -0
  118. package/dist/controllers/range_slider_controller.js.map +1 -0
  119. package/dist/controllers/read_more_controller.d.ts +52 -0
  120. package/dist/controllers/read_more_controller.js +194 -0
  121. package/dist/controllers/read_more_controller.js.map +1 -0
  122. package/dist/controllers/scroll_area_controller.js +15 -2
  123. package/dist/controllers/scroll_area_controller.js.map +1 -1
  124. package/dist/controllers/scroll_restore_controller.d.ts +54 -0
  125. package/dist/controllers/scroll_restore_controller.js +93 -0
  126. package/dist/controllers/scroll_restore_controller.js.map +1 -0
  127. package/dist/controllers/scroll_visibility_controller.js +8 -4
  128. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  129. package/dist/controllers/scrollspy_controller.js +33 -11
  130. package/dist/controllers/scrollspy_controller.js.map +1 -1
  131. package/dist/controllers/separator_controller.d.ts +54 -0
  132. package/dist/controllers/separator_controller.js +87 -0
  133. package/dist/controllers/separator_controller.js.map +1 -0
  134. package/dist/controllers/sidebar_controller.d.ts +107 -0
  135. package/dist/controllers/sidebar_controller.js +761 -0
  136. package/dist/controllers/sidebar_controller.js.map +1 -0
  137. package/dist/controllers/stepper_controller.d.ts +4 -0
  138. package/dist/controllers/stepper_controller.js +28 -12
  139. package/dist/controllers/stepper_controller.js.map +1 -1
  140. package/dist/controllers/stick_to_bottom_controller.js +8 -4
  141. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  142. package/dist/controllers/sticky_observer_controller.d.ts +17 -4
  143. package/dist/controllers/sticky_observer_controller.js +88 -20
  144. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  145. package/dist/controllers/tabs_controller.d.ts +3 -3
  146. package/dist/controllers/tabs_controller.js.map +1 -1
  147. package/dist/controllers/tags_input_controller.d.ts +91 -0
  148. package/dist/controllers/tags_input_controller.js +275 -0
  149. package/dist/controllers/tags_input_controller.js.map +1 -0
  150. package/dist/controllers/theme_controller.js +20 -10
  151. package/dist/controllers/theme_controller.js.map +1 -1
  152. package/dist/controllers/time_picker_controller.d.ts +67 -0
  153. package/dist/controllers/time_picker_controller.js +212 -0
  154. package/dist/controllers/time_picker_controller.js.map +1 -0
  155. package/dist/controllers/toast_controller.d.ts +8 -18
  156. package/dist/controllers/toast_controller.js +144 -38
  157. package/dist/controllers/toast_controller.js.map +1 -1
  158. package/dist/controllers/tooltip_controller.d.ts +15 -18
  159. package/dist/controllers/tooltip_controller.js +148 -42
  160. package/dist/controllers/tooltip_controller.js.map +1 -1
  161. package/dist/controllers/transition_controller.d.ts +14 -8
  162. package/dist/controllers/transition_controller.js +153 -38
  163. package/dist/controllers/transition_controller.js.map +1 -1
  164. package/dist/controllers/tree_view_controller.d.ts +54 -0
  165. package/dist/controllers/tree_view_controller.js +275 -0
  166. package/dist/controllers/tree_view_controller.js.map +1 -0
  167. package/dist/index.d.ts +27 -1788
  168. package/dist/index.js +1644 -643
  169. package/dist/index.js.map +1 -1
  170. package/dist/inspector/cli.js.map +1 -1
  171. package/dist/inspector/cli_bin.js.map +1 -1
  172. package/dist/inspector/examples.json +8 -8
  173. package/dist/inspector/manifest.json +30 -5
  174. package/package.json +7 -7
@@ -0,0 +1,213 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // src/controllers/color_picker_controller.ts
4
+ var COLOR_PROPERTY = "--stimeo-color";
5
+ var CHANNEL_RANGE = {
6
+ hue: [0, 360],
7
+ saturation: [0, 100],
8
+ lightness: [0, 100],
9
+ alpha: [0, 100]
10
+ };
11
+ var ColorPickerController = class extends Controller {
12
+ static targets = ["slider", "hex", "preview", "field"];
13
+ static values = {
14
+ value: { type: String, default: "#000000" },
15
+ alpha: { type: Boolean, default: false }
16
+ };
17
+ static actions = ["onHexInput", "onKeydown", "onPointerDown"];
18
+ static events = ["change"];
19
+ /** The current color in the editing model. */
20
+ #color = { hue: 0, saturation: 0, lightness: 0, alpha: 100 };
21
+ /** Aborts in-progress pointer-drag listeners on drag end / teardown. */
22
+ #dragAbort = null;
23
+ /** Seeds the model from the initial hex value and renders every surface. */
24
+ connect() {
25
+ const parsed = hexToHsla(this.valueValue);
26
+ if (parsed) this.#color = this.alphaValue ? parsed : { ...parsed, alpha: 100 };
27
+ this.#render();
28
+ }
29
+ /** Cancels any active pointer drag so document listeners never leak. */
30
+ disconnect() {
31
+ this.#dragAbort?.abort();
32
+ this.#dragAbort = null;
33
+ }
34
+ /** Keyboard stepping on the focused channel slider (APG Slider model). */
35
+ onKeydown(event) {
36
+ const slider = event.currentTarget;
37
+ const channel = this.#channelOf(slider);
38
+ if (!channel) return;
39
+ const [min, max] = this.#rangeOf(slider, channel);
40
+ const value = this.#color[channel];
41
+ let next = null;
42
+ switch (event.key) {
43
+ case "ArrowRight":
44
+ case "ArrowUp":
45
+ next = value + 1;
46
+ break;
47
+ case "ArrowLeft":
48
+ case "ArrowDown":
49
+ next = value - 1;
50
+ break;
51
+ case "PageUp":
52
+ next = value + 10;
53
+ break;
54
+ case "PageDown":
55
+ next = value - 10;
56
+ break;
57
+ case "Home":
58
+ next = min;
59
+ break;
60
+ case "End":
61
+ next = max;
62
+ break;
63
+ default:
64
+ return;
65
+ }
66
+ event.preventDefault();
67
+ this.#setChannel(channel, next, min, max);
68
+ }
69
+ /** Begins a pointer drag on a channel slider and tracks movement. */
70
+ onPointerDown(event) {
71
+ const slider = event.currentTarget;
72
+ const channel = this.#channelOf(slider);
73
+ if (!channel) return;
74
+ event.preventDefault();
75
+ slider.focus();
76
+ const [min, max] = this.#rangeOf(slider, channel);
77
+ const update = (clientX) => {
78
+ const rect = slider.getBoundingClientRect();
79
+ if (rect.width === 0) return;
80
+ const fraction = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
81
+ this.#setChannel(channel, min + fraction * (max - min), min, max);
82
+ };
83
+ update(event.clientX);
84
+ this.#dragAbort?.abort();
85
+ const abort = new AbortController();
86
+ this.#dragAbort = abort;
87
+ const onMove = (move) => update(move.clientX);
88
+ const onUp = () => {
89
+ abort.abort();
90
+ this.#dragAbort = null;
91
+ };
92
+ document.addEventListener("pointermove", onMove, { signal: abort.signal });
93
+ document.addEventListener("pointerup", onUp, { signal: abort.signal });
94
+ document.addEventListener("pointercancel", onUp, { signal: abort.signal });
95
+ }
96
+ /** Parses the hex input on confirm and syncs every channel + surface. */
97
+ onHexInput() {
98
+ if (!this.hasHexTarget) return;
99
+ const parsed = hexToHsla(this.hexTarget.value);
100
+ if (!parsed) {
101
+ this.hexTarget.value = this.#hexString();
102
+ return;
103
+ }
104
+ this.#color = this.alphaValue ? parsed : { ...parsed, alpha: 100 };
105
+ this.#render();
106
+ }
107
+ /** Clamps and snaps one channel to an integer, then re-renders + emits change. */
108
+ #setChannel(channel, raw, min, max) {
109
+ this.#color[channel] = Math.round(Math.min(max, Math.max(min, raw)));
110
+ this.#render();
111
+ }
112
+ /** Reflects the model onto sliders, the hex input, preview, and form field. */
113
+ #render() {
114
+ for (const slider of this.sliderTargets) {
115
+ const channel = this.#channelOf(slider);
116
+ if (!channel) continue;
117
+ const value = this.#color[channel];
118
+ slider.setAttribute("aria-valuenow", String(value));
119
+ slider.setAttribute("aria-valuetext", valueText(channel, value));
120
+ }
121
+ const hex = this.#hexString();
122
+ if (this.hasHexTarget) this.hexTarget.value = hex;
123
+ for (const field of this.fieldTargets) field.value = hex;
124
+ for (const preview of this.previewTargets) preview.style.setProperty(COLOR_PROPERTY, hex);
125
+ this.element.style.setProperty(COLOR_PROPERTY, hex);
126
+ const rgb = hslToRgb(this.#color.hue, this.#color.saturation, this.#color.lightness);
127
+ this.dispatch("change", {
128
+ detail: { value: hex, rgba: { ...rgb, a: this.#color.alpha / 100 } }
129
+ });
130
+ }
131
+ /** The current color as `#RRGGBB`, or `#RRGGBBAA` when alpha is enabled. */
132
+ #hexString() {
133
+ const rgb = hslToRgb(this.#color.hue, this.#color.saturation, this.#color.lightness);
134
+ const base = `#${hex2(rgb.r)}${hex2(rgb.g)}${hex2(rgb.b)}`;
135
+ if (!this.alphaValue) return base;
136
+ return `${base}${hex2(Math.round(this.#color.alpha / 100 * 255))}`;
137
+ }
138
+ /** Reads a slider's `data-channel`, if it is a known channel. */
139
+ #channelOf(slider) {
140
+ const channel = slider.getAttribute("data-channel");
141
+ return channel && channel in CHANNEL_RANGE ? channel : null;
142
+ }
143
+ /** A slider's `[min, max]` from aria-valuemin/max, falling back per channel. */
144
+ #rangeOf(slider, channel) {
145
+ const [defMin, defMax] = CHANNEL_RANGE[channel];
146
+ const min = Number(slider.getAttribute("aria-valuemin"));
147
+ const max = Number(slider.getAttribute("aria-valuemax"));
148
+ return [Number.isFinite(min) ? min : defMin, Number.isFinite(max) ? max : defMax];
149
+ }
150
+ };
151
+ function valueText(channel, value) {
152
+ const label = channel.charAt(0).toUpperCase() + channel.slice(1);
153
+ const unit = channel === "hue" ? "degrees" : "percent";
154
+ return `${label} ${value} ${unit}`;
155
+ }
156
+ function hex2(value) {
157
+ return Math.round(Math.min(255, Math.max(0, value))).toString(16).padStart(2, "0");
158
+ }
159
+ function hslToRgb(h, s, l) {
160
+ const sat = s / 100;
161
+ const light = l / 100;
162
+ const c = (1 - Math.abs(2 * light - 1)) * sat;
163
+ const hp = (h % 360 + 360) % 360 / 60;
164
+ const x = c * (1 - Math.abs(hp % 2 - 1));
165
+ let r = 0;
166
+ let g = 0;
167
+ let b = 0;
168
+ if (hp >= 0 && hp < 1) [r, g, b] = [c, x, 0];
169
+ else if (hp < 2) [r, g, b] = [x, c, 0];
170
+ else if (hp < 3) [r, g, b] = [0, c, x];
171
+ else if (hp < 4) [r, g, b] = [0, x, c];
172
+ else if (hp < 5) [r, g, b] = [x, 0, c];
173
+ else [r, g, b] = [c, 0, x];
174
+ const m = light - c / 2;
175
+ return {
176
+ r: Math.round((r + m) * 255),
177
+ g: Math.round((g + m) * 255),
178
+ b: Math.round((b + m) * 255)
179
+ };
180
+ }
181
+ function rgbToHsl(r, g, b) {
182
+ const rn = r / 255;
183
+ const gn = g / 255;
184
+ const bn = b / 255;
185
+ const max = Math.max(rn, gn, bn);
186
+ const min = Math.min(rn, gn, bn);
187
+ const delta = max - min;
188
+ const l = (max + min) / 2;
189
+ let h = 0;
190
+ if (delta !== 0) {
191
+ if (max === rn) h = (gn - bn) / delta % 6;
192
+ else if (max === gn) h = (bn - rn) / delta + 2;
193
+ else h = (rn - gn) / delta + 4;
194
+ h = (h * 60 + 360) % 360;
195
+ }
196
+ const s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
197
+ return { h: Math.round(h), s: Math.round(s * 100), l: Math.round(l * 100) };
198
+ }
199
+ function hexToHsla(input) {
200
+ const hex = input.trim().replace(/^#/, "");
201
+ if (!/^(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(hex)) return null;
202
+ const full = hex.length <= 4 ? hex.split("").map((char) => char + char).join("") : hex;
203
+ const r = Number.parseInt(full.slice(0, 2), 16);
204
+ const g = Number.parseInt(full.slice(2, 4), 16);
205
+ const b = Number.parseInt(full.slice(4, 6), 16);
206
+ const a = full.length === 8 ? Number.parseInt(full.slice(6, 8), 16) : 255;
207
+ const { h, s, l } = rgbToHsl(r, g, b);
208
+ return { hue: h, saturation: s, lightness: l, alpha: Math.round(a / 255 * 100) };
209
+ }
210
+
211
+ export { ColorPickerController };
212
+ //# sourceMappingURL=color_picker_controller.js.map
213
+ //# sourceMappingURL=color_picker_controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/controllers/color_picker_controller.ts"],"names":[],"mappings":"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","file":"color_picker_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\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`. 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 };\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\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 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 switch (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 const update = (clientX: number): void => {\n const rect = slider.getBoundingClientRect();\n if (rect.width === 0) return;\n const fraction = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));\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 /** A slider's `[min, max]` from aria-valuemin/max, falling back per channel. */\n #rangeOf(slider: HTMLElement, channel: Channel): [number, number] {\n const [defMin, defMax] = CHANNEL_RANGE[channel];\n const min = Number(slider.getAttribute(\"aria-valuemin\"));\n const max = Number(slider.getAttribute(\"aria-valuemax\"));\n return [Number.isFinite(min) ? min : defMin, Number.isFinite(max) ? max : defMax];\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"]}
@@ -57,8 +57,12 @@ declare class ComboboxController extends Controller<HTMLElement> {
57
57
  connect(): void;
58
58
  /** Removes the document-level listener registered in {@link connect}. */
59
59
  disconnect(): void;
60
- /** Filters options by the current input value and opens the listbox. */
61
- filter(): void;
60
+ /** Tracks an input added initially or after connect. */
61
+ inputTargetConnected(input: HTMLInputElement): void;
62
+ /** Removes composition listeners when the active input is replaced or removed. */
63
+ inputTargetDisconnected(input: HTMLInputElement): void;
64
+ /** Filters confirmed input text and opens the listbox. */
65
+ filter(event?: InputEvent): void;
62
66
  /**
63
67
  * Opens the listbox, re-filtering the options against the current input value
64
68
  * so the visible options and empty state always match what is typed (e.g.
@@ -2,6 +2,53 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/combobox_controller.ts
4
4
 
5
+ // src/utils/composition_tracker.ts
6
+ var CompositionTracker = class {
7
+ #observedTargets = /* @__PURE__ */ new Set();
8
+ #activeTargets = /* @__PURE__ */ new Set();
9
+ #onStart;
10
+ #onEnd;
11
+ constructor(options = {}) {
12
+ this.#onStart = options.onStart;
13
+ this.#onEnd = options.onEnd;
14
+ }
15
+ /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
16
+ observe(target) {
17
+ if (this.#observedTargets.has(target)) return;
18
+ target.addEventListener("compositionstart", this.#handleStart);
19
+ target.addEventListener("compositionend", this.#handleEnd);
20
+ this.#observedTargets.add(target);
21
+ }
22
+ /** Stops tracking one target and clears any active composition it owned. */
23
+ unobserve(target) {
24
+ if (!this.#observedTargets.delete(target)) return;
25
+ target.removeEventListener("compositionstart", this.#handleStart);
26
+ target.removeEventListener("compositionend", this.#handleEnd);
27
+ this.#activeTargets.delete(target);
28
+ }
29
+ /** Releases every listener and clears state so reconnect starts cleanly. */
30
+ disconnect() {
31
+ for (const target of this.#observedTargets) {
32
+ target.removeEventListener("compositionstart", this.#handleStart);
33
+ target.removeEventListener("compositionend", this.#handleEnd);
34
+ }
35
+ this.#observedTargets.clear();
36
+ this.#activeTargets.clear();
37
+ }
38
+ /** True when lifecycle tracking or the current event reports composition. */
39
+ isComposing(event) {
40
+ return this.#activeTargets.size > 0 || event?.isComposing === true;
41
+ }
42
+ #handleStart = (event) => {
43
+ if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
44
+ this.#onStart?.(event);
45
+ };
46
+ #handleEnd = (event) => {
47
+ if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
48
+ this.#onEnd?.(event);
49
+ };
50
+ };
51
+
5
52
  // src/utils/option_scroll.ts
6
53
  function scrollOptionIntoView(list, option) {
7
54
  if (list.scrollHeight <= list.clientHeight) return;
@@ -27,17 +74,30 @@ var ComboboxController = class extends Controller {
27
74
  * does not immediately re-open the listbox via a `focus`-bound action.
28
75
  */
29
76
  #suppressOpen = false;
77
+ /** Owns IME lifecycle state; confirmed text re-filters the list once. */
78
+ #composition = new CompositionTracker({ onEnd: () => this.filter() });
30
79
  /** Starts closed with no active option and registers the outside-click listener. */
31
80
  connect() {
81
+ if (this.hasInputTarget) this.#composition.observe(this.inputTarget);
32
82
  this.close();
33
83
  document.addEventListener("click", this.#onOutsideClick);
34
84
  }
35
85
  /** Removes the document-level listener registered in {@link connect}. */
36
86
  disconnect() {
87
+ this.#composition.disconnect();
37
88
  document.removeEventListener("click", this.#onOutsideClick);
38
89
  }
39
- /** Filters options by the current input value and opens the listbox. */
40
- filter() {
90
+ /** Tracks an input added initially or after connect. */
91
+ inputTargetConnected(input) {
92
+ this.#composition.observe(input);
93
+ }
94
+ /** Removes composition listeners when the active input is replaced or removed. */
95
+ inputTargetDisconnected(input) {
96
+ this.#composition.unobserve(input);
97
+ }
98
+ /** Filters confirmed input text and opens the listbox. */
99
+ filter(event) {
100
+ if (this.#composition.isComposing(event)) return;
41
101
  this.open();
42
102
  }
43
103
  /**
@@ -74,7 +134,7 @@ var ComboboxController = class extends Controller {
74
134
  }
75
135
  /** Routes keyboard interaction per the APG combobox model. */
76
136
  onKeydown(event) {
77
- if (event.isComposing || event.keyCode === 229) return;
137
+ if (this.#composition.isComposing(event)) return;
78
138
  switch (event.key) {
79
139
  case "ArrowDown": {
80
140
  event.preventDefault();
@@ -120,6 +180,7 @@ var ComboboxController = class extends Controller {
120
180
  break;
121
181
  }
122
182
  case "Escape":
183
+ if (event.defaultPrevented || this.#isClosed) break;
123
184
  event.preventDefault();
124
185
  this.close();
125
186
  break;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/option_scroll.ts","../../src/controllers/combobox_controller.ts"],"names":[],"mappings":"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","file":"combobox_controller.js","sourcesContent":["/**\n * Scroll-follow for virtually-focused listbox options.\n *\n * Listbox-style widgets that track their active option with\n * `aria-activedescendant` keep DOM focus on the input/trigger, so the\n * browser's native \"scroll the focused element into view\" never runs — in a\n * scrollable list (`max-height` + `overflow`) the active option can walk right\n * out of sight on ArrowDown/ArrowUp. This helper keeps it visible.\n *\n * It adjusts the LIST's own `scrollTop` only — deliberately not\n * `scrollIntoView({ block: \"nearest\" })`, which may also scroll ancestor\n * scrolling boxes (the page) when a floating popup pokes past a viewport\n * edge, desyncing anchored/marker-composed placements. (Command-palette keeps\n * its `scrollIntoView` because its page is scroll-locked behind the modal;\n * page-floating popups must use this helper instead.)\n */\n\n/**\n * Scrolls `option` into view within `list` by minimally adjusting\n * `list.scrollTop`. No-ops when the list does not actually scroll. Never\n * touches any other scrolling box.\n *\n * @param list - The scrollable listbox container.\n * @param option - The (virtually focused) option to keep visible.\n */\nexport function scrollOptionIntoView(list: HTMLElement, option: HTMLElement): void {\n if (list.scrollHeight <= list.clientHeight) return;\n const listRect = list.getBoundingClientRect();\n const optionRect = option.getBoundingClientRect();\n if (optionRect.top < listRect.top) {\n list.scrollTop -= listRect.top - optionRect.top;\n } else if (optionRect.bottom > listRect.bottom) {\n list.scrollTop += optionRect.bottom - listRect.bottom;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { scrollOptionIntoView } from \"../utils/option_scroll\";\n\n/**\n * Headless, accessible combobox behavior (list autocomplete).\n *\n * Markup contract (identifier: `stimeo--combobox`):\n * <div data-controller=\"stimeo--combobox\">\n * <input type=\"text\" role=\"combobox\" aria-expanded=\"false\"\n * aria-autocomplete=\"list\" aria-controls=\"listbox\"\n * data-stimeo--combobox-target=\"input\"\n * data-action=\"input->stimeo--combobox#filter\n * keydown->stimeo--combobox#onKeydown\n * focus->stimeo--combobox#open\n * click->stimeo--combobox#open\" />\n * <ul id=\"listbox\" role=\"listbox\" data-stimeo--combobox-target=\"list\" hidden>\n * <li role=\"option\" id=\"opt-apple\" data-value=\"apple\"\n * data-stimeo--combobox-target=\"option\"\n * data-action=\"click->stimeo--combobox#selectByClick\">Apple</li>\n * <!-- more options -->\n * </ul>\n * </div>\n *\n * Implements the WAI-ARIA APG **Combobox** pattern with a listbox popup and\n * list-autocomplete. Focus stays in the input; the active option is tracked with\n * `aria-activedescendant` rather than by moving DOM focus.\n *\n * @remarks\n * Behavior only. Options are authored in the DOM; the controller filters them by\n * toggling each option's `hidden` attribute (case-insensitive substring match on\n * its text). The consumer owns styling, typically keyed off `[aria-selected]`.\n * When an open listbox has no matching options, the root element gets\n * `data-stimeo--combobox-empty` so the consumer can style the empty state (hide\n * the list, show a \"no results\" node, …) — the library imposes no visuals.\n *\n * Behavior provided:\n * - Typing filters the options and opens the listbox.\n * - Focusing or clicking the input opens the listbox, re-filtered against the\n * current value (so re-opening with a non-matching value keeps the empty state).\n * - `ArrowDown`/`ArrowUp` move the active option (wrapping); `Enter` selects it;\n * `Escape` closes the listbox; `Home`/`End` jump to the first/last visible\n * option.\n * - Selecting an option fills the input (with the option's `data-value` if set,\n * otherwise its text) and closes the listbox.\n * - A click outside the combobox closes the listbox.\n */\nexport class ComboboxController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"list\", \"option\"];\n static actions = [\"close\", \"filter\", \"onKeydown\", \"open\", \"selectByClick\"] as const;\n static events = [\"selected\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly listTarget: HTMLElement;\n declare readonly optionTargets: HTMLElement[];\n declare readonly hasInputTarget: boolean;\n declare readonly hasListTarget: boolean;\n\n /** Index into the *visible* options of the active option, or -1 if none. */\n #activeIndex = -1;\n /**\n * Suppresses {@link open} for the duration of the programmatic re-focus in\n * `#select`, so committing a value (which returns focus to the input)\n * does not immediately re-open the listbox via a `focus`-bound action.\n */\n #suppressOpen = false;\n\n /** Starts closed with no active option and registers the outside-click listener. */\n override connect(): void {\n this.close();\n document.addEventListener(\"click\", this.#onOutsideClick);\n }\n\n /** Removes the document-level listener registered in {@link connect}. */\n override disconnect(): void {\n document.removeEventListener(\"click\", this.#onOutsideClick);\n }\n\n /** Filters options by the current input value and opens the listbox. */\n filter(): void {\n this.open();\n }\n\n /**\n * Opens the listbox, re-filtering the options against the current input value\n * so the visible options and empty state always match what is typed (e.g.\n * re-opening with a stale non-matching value still surfaces the empty state).\n */\n open(): void {\n if (!this.hasListTarget || this.#suppressOpen) return;\n this.#applyFilter();\n this.listTarget.hidden = false;\n this.inputTarget.setAttribute(\"aria-expanded\", \"true\");\n this.#setActive(-1);\n this.#reflectEmptyState();\n }\n\n /**\n * Hides options that don't match the current input value (case-insensitive\n * substring). An empty query shows every option. Does not change open state.\n */\n #applyFilter(): void {\n const query = this.inputTarget.value.trim().toLowerCase();\n for (const option of this.optionTargets) {\n const text = (option.textContent ?? \"\").trim().toLowerCase();\n option.hidden = query.length > 0 && !text.includes(query);\n }\n }\n\n /** Closes the listbox, clears the active option, and updates ARIA state. */\n close(): void {\n if (!this.hasListTarget) return;\n this.listTarget.hidden = true;\n this.inputTarget.setAttribute(\"aria-expanded\", \"false\");\n this.#setActive(-1);\n this.element.removeAttribute(\"data-stimeo--combobox-empty\");\n }\n\n /** Routes keyboard interaction per the APG combobox model. */\n onKeydown(event: KeyboardEvent): void {\n // Ignore keys fired during IME composition: the `Enter` that confirms a\n // candidate (and arrows that move within it) must not select an option or\n // close the popup. `keyCode === 229` covers browsers that omit `isComposing`\n // on the confirming keydown. Aligns with the library's IME composition-guard\n // policy (the keydown-level equivalent of the input-path guards in\n // character-counter / auto-submit).\n if (event.isComposing || event.keyCode === 229) return;\n switch (event.key) {\n case \"ArrowDown\": {\n event.preventDefault();\n // open() re-filters, so read the visible set afterwards.\n if (this.#isClosed) this.open();\n const visible = this.#visibleOptions();\n if (visible.length > 0) {\n const next = this.#activeIndex === -1 ? 0 : (this.#activeIndex + 1) % visible.length;\n this.#setActive(next);\n }\n break;\n }\n case \"ArrowUp\": {\n event.preventDefault();\n if (this.#isClosed) this.open();\n const visible = this.#visibleOptions();\n if (visible.length > 0) {\n // From the input (no active option) ArrowUp jumps to the last option,\n // per the APG; otherwise it wraps backwards.\n const next =\n this.#activeIndex === -1\n ? visible.length - 1\n : (this.#activeIndex - 1 + visible.length) % visible.length;\n this.#setActive(next);\n }\n break;\n }\n case \"Home\":\n if (!this.#isClosed && this.#visibleOptions().length > 0) {\n event.preventDefault();\n this.#setActive(0);\n }\n break;\n case \"End\": {\n const visible = this.#visibleOptions();\n if (!this.#isClosed && visible.length > 0) {\n event.preventDefault();\n this.#setActive(visible.length - 1);\n }\n break;\n }\n case \"Enter\": {\n const visible = this.#visibleOptions();\n const active = this.#activeIndex === -1 ? undefined : visible[this.#activeIndex];\n if (active) {\n event.preventDefault();\n this.#select(active);\n }\n break;\n }\n case \"Escape\":\n event.preventDefault();\n this.close();\n break;\n case \"Tab\":\n // Let focus leave naturally, but don't keep a stale popup open.\n this.close();\n break;\n default:\n break;\n }\n }\n\n /**\n * Closes the listbox when a click lands outside the combobox. Mirrors the menu\n * button's outside-click behavior; clicks on an option are inside the element,\n * so `#select` (not this handler) closes the popup after committing.\n */\n readonly #onOutsideClick = (event: MouseEvent): void => {\n if (!this.#isClosed && !this.element.contains(event.target as Node)) this.close();\n };\n\n /** Selects the clicked option. Bound via `data-action` (click). */\n selectByClick(event: Event): void {\n const option = (event.currentTarget as HTMLElement).closest<HTMLElement>('[role=\"option\"]');\n if (option) this.#select(option);\n }\n\n /** Commits an option: fills the input, closes the listbox, notifies listeners. */\n #select(option: HTMLElement): void {\n const value = option.dataset.value ?? (option.textContent ?? \"\").trim();\n const changed = this.inputTarget.value !== value;\n this.inputTarget.value = value;\n this.close();\n // Returning focus to the input would re-trigger a `focus`-bound open(); guard\n // it so the listbox stays closed after a selection.\n this.#suppressOpen = true;\n this.inputTarget.focus();\n this.#suppressOpen = false;\n if (changed) {\n // A native bubbling `change` (matching <select>/listbox semantics: only on\n // an actual value change) so form-level behaviors — validation re-checks,\n // auto-submit — hear the commit without knowing this widget. Deliberately\n // NOT `input`: that is this combobox's own filter trigger and would reopen\n // the popup on every selection.\n this.inputTarget.dispatchEvent(new Event(\"change\", { bubbles: true }));\n }\n this.dispatch(\"selected\", { detail: { value } });\n }\n\n /**\n * Reflects whether the open listbox currently has zero matching options by\n * toggling `data-stimeo--combobox-empty` on the root element. Behavior only:\n * consumers decide how to present the empty state (hide the list, show a\n * \"no results\" node, etc.) via CSS keyed off this attribute.\n */\n #reflectEmptyState(): void {\n const empty = !this.#isClosed && this.#visibleOptions().length === 0;\n if (empty) {\n this.element.setAttribute(\"data-stimeo--combobox-empty\", \"\");\n } else {\n this.element.removeAttribute(\"data-stimeo--combobox-empty\");\n }\n }\n\n /**\n * Marks the visible option at `index` active via `aria-selected` and the\n * input's `aria-activedescendant`. Pass `-1` to clear the active option.\n */\n #setActive(index: number): void {\n this.#activeIndex = index;\n const visible = this.#visibleOptions();\n const active = index === -1 ? null : visible[index];\n // Clear aria-selected on every option (not just the visible ones) so a\n // previously-active option that became hidden by filtering doesn't keep a\n // stale selected state.\n for (const option of this.optionTargets) {\n option.setAttribute(\"aria-selected\", option === active ? \"true\" : \"false\");\n }\n if (active?.id) {\n this.inputTarget.setAttribute(\"aria-activedescendant\", active.id);\n } else {\n this.inputTarget.removeAttribute(\"aria-activedescendant\");\n }\n // Virtual focus never triggers the browser's native focus-scrolling, so a\n // scrollable list must follow the active option itself (list-only scroll).\n if (active && this.hasListTarget) scrollOptionIntoView(this.listTarget, active);\n }\n\n /** The options currently shown (not filtered out). */\n #visibleOptions(): HTMLElement[] {\n return this.optionTargets.filter((option) => !option.hidden);\n }\n\n /** Whether the listbox is currently hidden. */\n get #isClosed(): boolean {\n return !this.hasListTarget || this.listTarget.hidden !== false;\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/composition_tracker.ts","../../src/utils/option_scroll.ts","../../src/controllers/combobox_controller.ts"],"names":[],"mappings":"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","file":"combobox_controller.js","sourcesContent":["/** Minimal event shape carrying the platform's per-event composition signal. */\nexport interface CompositionSignal {\n readonly isComposing?: boolean;\n}\n\n/** Hooks that keep component-specific work outside {@link CompositionTracker}. */\nexport interface CompositionTrackerOptions {\n /** Runs after lifecycle state is set for a `compositionstart` event. */\n readonly onStart?: (event: Event) => void;\n /** Runs after lifecycle state is cleared for a `compositionend` event. */\n readonly onEnd?: (event: Event) => void;\n}\n\n/**\n * Owns IME composition listeners and transient state for one or more event targets.\n *\n * Some browsers omit `KeyboardEvent.isComposing` on the keydown that confirms a\n * conversion. Tracking `compositionstart` through `compositionend` supplies the\n * missing lifecycle signal without relying on deprecated numeric key codes.\n * Component policy stays with the consumer: filtering, validation, and submission\n * belong in the optional {@link CompositionTrackerOptions.onEnd} callback.\n *\n * @example\n * ```ts\n * #composition = new CompositionTracker({ onEnd: () => this.filter() });\n *\n * connect(): void {\n * this.#composition.observe(this.inputTarget);\n * }\n *\n * disconnect(): void {\n * this.#composition.disconnect();\n * }\n *\n * onKeydown(event: KeyboardEvent): void {\n * if (this.#composition.isComposing(event)) return;\n * }\n * ```\n */\nexport class CompositionTracker {\n readonly #observedTargets = new Set<EventTarget>();\n readonly #activeTargets = new Set<EventTarget>();\n readonly #onStart: ((event: Event) => void) | undefined;\n readonly #onEnd: ((event: Event) => void) | undefined;\n\n constructor(options: CompositionTrackerOptions = {}) {\n this.#onStart = options.onStart;\n this.#onEnd = options.onEnd;\n }\n\n /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */\n observe(target: EventTarget): void {\n if (this.#observedTargets.has(target)) return;\n target.addEventListener(\"compositionstart\", this.#handleStart);\n target.addEventListener(\"compositionend\", this.#handleEnd);\n this.#observedTargets.add(target);\n }\n\n /** Stops tracking one target and clears any active composition it owned. */\n unobserve(target: EventTarget): void {\n if (!this.#observedTargets.delete(target)) return;\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n this.#activeTargets.delete(target);\n }\n\n /** Releases every listener and clears state so reconnect starts cleanly. */\n disconnect(): void {\n for (const target of this.#observedTargets) {\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n }\n this.#observedTargets.clear();\n this.#activeTargets.clear();\n }\n\n /** True when lifecycle tracking or the current event reports composition. */\n isComposing(event?: CompositionSignal): boolean {\n return this.#activeTargets.size > 0 || event?.isComposing === true;\n }\n\n readonly #handleStart = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.add(event.currentTarget);\n this.#onStart?.(event);\n };\n\n readonly #handleEnd = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);\n this.#onEnd?.(event);\n };\n}\n","/**\n * Scroll-follow for virtually-focused listbox options.\n *\n * Listbox-style widgets that track their active option with\n * `aria-activedescendant` keep DOM focus on the input/trigger, so the\n * browser's native \"scroll the focused element into view\" never runs — in a\n * scrollable list (`max-height` + `overflow`) the active option can walk right\n * out of sight on ArrowDown/ArrowUp. This helper keeps it visible.\n *\n * It adjusts the LIST's own `scrollTop` only — deliberately not\n * `scrollIntoView({ block: \"nearest\" })`, which may also scroll ancestor\n * scrolling boxes (the page) when a floating popup pokes past a viewport\n * edge, desyncing anchored/marker-composed placements. (Command-palette keeps\n * its `scrollIntoView` because its page is scroll-locked behind the modal;\n * page-floating popups must use this helper instead.)\n */\n\n/**\n * Scrolls `option` into view within `list` by minimally adjusting\n * `list.scrollTop`. No-ops when the list does not actually scroll. Never\n * touches any other scrolling box.\n *\n * @param list - The scrollable listbox container.\n * @param option - The (virtually focused) option to keep visible.\n */\nexport function scrollOptionIntoView(list: HTMLElement, option: HTMLElement): void {\n if (list.scrollHeight <= list.clientHeight) return;\n const listRect = list.getBoundingClientRect();\n const optionRect = option.getBoundingClientRect();\n if (optionRect.top < listRect.top) {\n list.scrollTop -= listRect.top - optionRect.top;\n } else if (optionRect.bottom > listRect.bottom) {\n list.scrollTop += optionRect.bottom - listRect.bottom;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { CompositionTracker } from \"../utils/composition_tracker\";\nimport { scrollOptionIntoView } from \"../utils/option_scroll\";\n\n/**\n * Headless, accessible combobox behavior (list autocomplete).\n *\n * Markup contract (identifier: `stimeo--combobox`):\n * <div data-controller=\"stimeo--combobox\">\n * <input type=\"text\" role=\"combobox\" aria-expanded=\"false\"\n * aria-autocomplete=\"list\" aria-controls=\"listbox\"\n * data-stimeo--combobox-target=\"input\"\n * data-action=\"input->stimeo--combobox#filter\n * keydown->stimeo--combobox#onKeydown\n * focus->stimeo--combobox#open\n * click->stimeo--combobox#open\" />\n * <ul id=\"listbox\" role=\"listbox\" data-stimeo--combobox-target=\"list\" hidden>\n * <li role=\"option\" id=\"opt-apple\" data-value=\"apple\"\n * data-stimeo--combobox-target=\"option\"\n * data-action=\"click->stimeo--combobox#selectByClick\">Apple</li>\n * <!-- more options -->\n * </ul>\n * </div>\n *\n * Implements the WAI-ARIA APG **Combobox** pattern with a listbox popup and\n * list-autocomplete. Focus stays in the input; the active option is tracked with\n * `aria-activedescendant` rather than by moving DOM focus.\n *\n * @remarks\n * Behavior only. Options are authored in the DOM; the controller filters them by\n * toggling each option's `hidden` attribute (case-insensitive substring match on\n * its text). The consumer owns styling, typically keyed off `[aria-selected]`.\n * When an open listbox has no matching options, the root element gets\n * `data-stimeo--combobox-empty` so the consumer can style the empty state (hide\n * the list, show a \"no results\" node, …) — the library imposes no visuals.\n *\n * Behavior provided:\n * - Typing filters the options and opens the listbox.\n * - Focusing or clicking the input opens the listbox, re-filtered against the\n * current value (so re-opening with a non-matching value keeps the empty state).\n * - `ArrowDown`/`ArrowUp` move the active option (wrapping); `Enter` selects it;\n * `Escape` closes the listbox; `Home`/`End` jump to the first/last visible\n * option.\n * - Selecting an option fills the input (with the option's `data-value` if set,\n * otherwise its text) and closes the listbox.\n * - A click outside the combobox closes the listbox.\n */\nexport class ComboboxController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"list\", \"option\"];\n static actions = [\"close\", \"filter\", \"onKeydown\", \"open\", \"selectByClick\"] as const;\n static events = [\"selected\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly listTarget: HTMLElement;\n declare readonly optionTargets: HTMLElement[];\n declare readonly hasInputTarget: boolean;\n declare readonly hasListTarget: boolean;\n\n /** Index into the *visible* options of the active option, or -1 if none. */\n #activeIndex = -1;\n /**\n * Suppresses {@link open} for the duration of the programmatic re-focus in\n * `#select`, so committing a value (which returns focus to the input)\n * does not immediately re-open the listbox via a `focus`-bound action.\n */\n #suppressOpen = false;\n /** Owns IME lifecycle state; confirmed text re-filters the list once. */\n readonly #composition = new CompositionTracker({ onEnd: () => this.filter() });\n\n /** Starts closed with no active option and registers the outside-click listener. */\n override connect(): void {\n if (this.hasInputTarget) this.#composition.observe(this.inputTarget);\n this.close();\n document.addEventListener(\"click\", this.#onOutsideClick);\n }\n\n /** Removes the document-level listener registered in {@link connect}. */\n override disconnect(): void {\n this.#composition.disconnect();\n document.removeEventListener(\"click\", this.#onOutsideClick);\n }\n\n /** Tracks an input added initially or after connect. */\n inputTargetConnected(input: HTMLInputElement): void {\n this.#composition.observe(input);\n }\n\n /** Removes composition listeners when the active input is replaced or removed. */\n inputTargetDisconnected(input: HTMLInputElement): void {\n this.#composition.unobserve(input);\n }\n\n /** Filters confirmed input text and opens the listbox. */\n filter(event?: InputEvent): void {\n if (this.#composition.isComposing(event)) return;\n this.open();\n }\n\n /**\n * Opens the listbox, re-filtering the options against the current input value\n * so the visible options and empty state always match what is typed (e.g.\n * re-opening with a stale non-matching value still surfaces the empty state).\n */\n open(): void {\n if (!this.hasListTarget || this.#suppressOpen) return;\n this.#applyFilter();\n this.listTarget.hidden = false;\n this.inputTarget.setAttribute(\"aria-expanded\", \"true\");\n this.#setActive(-1);\n this.#reflectEmptyState();\n }\n\n /**\n * Hides options that don't match the current input value (case-insensitive\n * substring). An empty query shows every option. Does not change open state.\n */\n #applyFilter(): void {\n const query = this.inputTarget.value.trim().toLowerCase();\n for (const option of this.optionTargets) {\n const text = (option.textContent ?? \"\").trim().toLowerCase();\n option.hidden = query.length > 0 && !text.includes(query);\n }\n }\n\n /** Closes the listbox, clears the active option, and updates ARIA state. */\n close(): void {\n if (!this.hasListTarget) return;\n this.listTarget.hidden = true;\n this.inputTarget.setAttribute(\"aria-expanded\", \"false\");\n this.#setActive(-1);\n this.element.removeAttribute(\"data-stimeo--combobox-empty\");\n }\n\n /** Routes keyboard interaction per the APG combobox model. */\n onKeydown(event: KeyboardEvent): void {\n // Ignore keys fired during IME composition: the `Enter` that confirms a\n // candidate (and arrows that move within it) must not select an option or\n // close the popup. Controller-owned lifecycle state covers confirming events\n // that omit the standard per-event signal.\n if (this.#composition.isComposing(event)) return;\n switch (event.key) {\n case \"ArrowDown\": {\n event.preventDefault();\n // open() re-filters, so read the visible set afterwards.\n if (this.#isClosed) this.open();\n const visible = this.#visibleOptions();\n if (visible.length > 0) {\n const next = this.#activeIndex === -1 ? 0 : (this.#activeIndex + 1) % visible.length;\n this.#setActive(next);\n }\n break;\n }\n case \"ArrowUp\": {\n event.preventDefault();\n if (this.#isClosed) this.open();\n const visible = this.#visibleOptions();\n if (visible.length > 0) {\n // From the input (no active option) ArrowUp jumps to the last option,\n // per the APG; otherwise it wraps backwards.\n const next =\n this.#activeIndex === -1\n ? visible.length - 1\n : (this.#activeIndex - 1 + visible.length) % visible.length;\n this.#setActive(next);\n }\n break;\n }\n case \"Home\":\n if (!this.#isClosed && this.#visibleOptions().length > 0) {\n event.preventDefault();\n this.#setActive(0);\n }\n break;\n case \"End\": {\n const visible = this.#visibleOptions();\n if (!this.#isClosed && visible.length > 0) {\n event.preventDefault();\n this.#setActive(visible.length - 1);\n }\n break;\n }\n case \"Enter\": {\n const visible = this.#visibleOptions();\n const active = this.#activeIndex === -1 ? undefined : visible[this.#activeIndex];\n if (active) {\n event.preventDefault();\n this.#select(active);\n }\n break;\n }\n case \"Escape\":\n // Layered-Escape contract: leave a press an inner handler already\n // owned (rule 1), and consume only while owning a dismissable state —\n // with the list already closed the press stays free for the shared\n // resolver, so an enclosing dialog still closes from a focused input.\n if (event.defaultPrevented || this.#isClosed) break;\n event.preventDefault();\n this.close();\n break;\n case \"Tab\":\n // Let focus leave naturally, but don't keep a stale popup open.\n this.close();\n break;\n default:\n break;\n }\n }\n\n /**\n * Closes the listbox when a click lands outside the combobox. Mirrors the menu\n * button's outside-click behavior; clicks on an option are inside the element,\n * so `#select` (not this handler) closes the popup after committing.\n */\n readonly #onOutsideClick = (event: MouseEvent): void => {\n if (!this.#isClosed && !this.element.contains(event.target as Node)) this.close();\n };\n\n /** Selects the clicked option. Bound via `data-action` (click). */\n selectByClick(event: Event): void {\n const option = (event.currentTarget as HTMLElement).closest<HTMLElement>('[role=\"option\"]');\n if (option) this.#select(option);\n }\n\n /** Commits an option: fills the input, closes the listbox, notifies listeners. */\n #select(option: HTMLElement): void {\n const value = option.dataset.value ?? (option.textContent ?? \"\").trim();\n const changed = this.inputTarget.value !== value;\n this.inputTarget.value = value;\n this.close();\n // Returning focus to the input would re-trigger a `focus`-bound open(); guard\n // it so the listbox stays closed after a selection.\n this.#suppressOpen = true;\n this.inputTarget.focus();\n this.#suppressOpen = false;\n if (changed) {\n // A native bubbling `change` (matching <select>/listbox semantics: only on\n // an actual value change) so form-level behaviors — validation re-checks,\n // auto-submit — hear the commit without knowing this widget. Deliberately\n // NOT `input`: that is this combobox's own filter trigger and would reopen\n // the popup on every selection.\n this.inputTarget.dispatchEvent(new Event(\"change\", { bubbles: true }));\n }\n this.dispatch(\"selected\", { detail: { value } });\n }\n\n /**\n * Reflects whether the open listbox currently has zero matching options by\n * toggling `data-stimeo--combobox-empty` on the root element. Behavior only:\n * consumers decide how to present the empty state (hide the list, show a\n * \"no results\" node, etc.) via CSS keyed off this attribute.\n */\n #reflectEmptyState(): void {\n const empty = !this.#isClosed && this.#visibleOptions().length === 0;\n if (empty) {\n this.element.setAttribute(\"data-stimeo--combobox-empty\", \"\");\n } else {\n this.element.removeAttribute(\"data-stimeo--combobox-empty\");\n }\n }\n\n /**\n * Marks the visible option at `index` active via `aria-selected` and the\n * input's `aria-activedescendant`. Pass `-1` to clear the active option.\n */\n #setActive(index: number): void {\n this.#activeIndex = index;\n const visible = this.#visibleOptions();\n const active = index === -1 ? null : visible[index];\n // Clear aria-selected on every option (not just the visible ones) so a\n // previously-active option that became hidden by filtering doesn't keep a\n // stale selected state.\n for (const option of this.optionTargets) {\n option.setAttribute(\"aria-selected\", option === active ? \"true\" : \"false\");\n }\n if (active?.id) {\n this.inputTarget.setAttribute(\"aria-activedescendant\", active.id);\n } else {\n this.inputTarget.removeAttribute(\"aria-activedescendant\");\n }\n // Virtual focus never triggers the browser's native focus-scrolling, so a\n // scrollable list must follow the active option itself (list-only scroll).\n if (active && this.hasListTarget) scrollOptionIntoView(this.listTarget, active);\n }\n\n /** The options currently shown (not filtered out). */\n #visibleOptions(): HTMLElement[] {\n return this.optionTargets.filter((option) => !option.hidden);\n }\n\n /** Whether the listbox is currently hidden. */\n get #isClosed(): boolean {\n return !this.hasListTarget || this.listTarget.hidden !== false;\n }\n}\n"]}
@@ -15,11 +15,10 @@ import { Controller } from '@hotwired/stimulus';
15
15
  * data-action="input->stimeo--command-palette#filter
16
16
  * keydown->stimeo--command-palette#onKeydown" />
17
17
  * <ul id="cmdk-list" data-stimeo--command-palette-target="list" role="listbox">
18
- * <li id="cmd-new" role="option"
19
- * data-stimeo--command-palette-target="option"
18
+ * <li id="cmd-new" role="option" data-stimeo--command-palette-target="option"
20
19
  * data-action="click->stimeo--command-palette#selectByClick">New…</li>
21
- * <li role="option" data-stimeo--command-palette-target="option"
22
- * data-disabled="true">Section heading (shown but not selectable)</li>
20
+ * <li id="cmd-heading" role="option" data-stimeo--command-palette-target="option"
21
+ * data-disabled="true">Disabled command (shown but not selectable)</li>
23
22
  * </ul>
24
23
  * <p data-stimeo--command-palette-target="empty" hidden>No commands</p>
25
24
  * </div>
@@ -37,7 +36,9 @@ import { Controller } from '@hotwired/stimulus';
37
36
  * primitive (also used by dialog / alert-dialog / drawer). `Escape` closes and
38
37
  * `Tab`/`Shift+Tab` cycle focus regardless of which element inside the dialog holds
39
38
  * focus (input, close button, …), because the trap listens at the document level.
40
- * Styling, transitions, and the actual command handlers remain the consumer's.
39
+ * IME composition is tracked from start to end: intermediate input does not filter,
40
+ * and conversion-confirming keys never select a command. Styling, transitions, and
41
+ * the actual command handlers remain the consumer's.
41
42
  */
42
43
  declare class CommandPaletteController extends Controller<HTMLElement> {
43
44
  #private;
@@ -79,6 +80,12 @@ declare class CommandPaletteController extends Controller<HTMLElement> {
79
80
  connect(): void;
80
81
  /** Tears down the global hotkey listener and reverts the modal side effects. */
81
82
  disconnect(): void;
83
+ /** Initializes semantics for an option added before or after the controller connects. */
84
+ optionTargetConnected(option: HTMLElement): void;
85
+ /** Tracks an input added initially or after connect without extra consumer actions. */
86
+ inputTargetConnected(input: HTMLInputElement): void;
87
+ /** Removes controller-owned listeners when the input target is replaced or removed. */
88
+ inputTargetDisconnected(input: HTMLInputElement): void;
82
89
  /** Toggles the open state of the command palette. */
83
90
  toggle(): void;
84
91
  /** Opens the palette, traps focus, and shifts focus to the input. */
@@ -86,7 +93,7 @@ declare class CommandPaletteController extends Controller<HTMLElement> {
86
93
  /** Closes the palette and restores focus back to the opener. */
87
94
  close(): void;
88
95
  /** Filters option elements in-memory matching the input value. Bound to input target. */
89
- filter(): void;
96
+ filter(event?: InputEvent): void;
90
97
  /** Selects the clicked option. Bound to option targets. */
91
98
  selectByClick(event: MouseEvent): void;
92
99
  /** Closes when the backdrop (the dialog target itself) is clicked, ignoring inner clicks. */