@adia-ai/web-components 0.4.6 → 0.4.7

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 (284) hide show
  1. package/USAGE.md +29 -9
  2. package/components/accordion/accordion.d.ts +17 -0
  3. package/components/accordion/accordion.js +10 -117
  4. package/components/accordion/class.js +132 -0
  5. package/components/action-list/action-list.d.ts +15 -0
  6. package/components/action-list/action-list.js +9 -140
  7. package/components/action-list/class.js +156 -0
  8. package/components/agent-artifact/agent-artifact.d.ts +25 -0
  9. package/components/agent-artifact/agent-artifact.js +8 -181
  10. package/components/agent-artifact/class.js +200 -0
  11. package/components/agent-feedback-bar/agent-feedback-bar.d.ts +21 -0
  12. package/components/agent-feedback-bar/agent-feedback-bar.js +8 -143
  13. package/components/agent-feedback-bar/class.js +162 -0
  14. package/components/agent-questions/agent-questions.d.ts +23 -0
  15. package/components/agent-questions/agent-questions.js +8 -180
  16. package/components/agent-questions/class.js +199 -0
  17. package/components/agent-reasoning/agent-reasoning.d.ts +23 -0
  18. package/components/agent-reasoning/agent-reasoning.js +8 -494
  19. package/components/agent-reasoning/class.js +513 -0
  20. package/components/agent-suggestions/agent-suggestions.d.ts +21 -0
  21. package/components/agent-suggestions/agent-suggestions.js +8 -78
  22. package/components/agent-suggestions/class.js +97 -0
  23. package/components/agent-trace/agent-trace.d.ts +19 -0
  24. package/components/alert/alert.d.ts +29 -0
  25. package/components/alert/alert.js +8 -175
  26. package/components/alert/class.js +194 -0
  27. package/components/avatar/avatar.d.ts +27 -0
  28. package/components/avatar/avatar.js +9 -159
  29. package/components/avatar/class.js +173 -0
  30. package/components/badge/badge.d.ts +27 -0
  31. package/components/badge/badge.js +9 -75
  32. package/components/badge/class.js +93 -0
  33. package/components/block/block.d.ts +19 -0
  34. package/components/block/block.js +9 -15
  35. package/components/block/class.js +33 -0
  36. package/components/breadcrumb/breadcrumb.d.ts +23 -0
  37. package/components/breadcrumb/breadcrumb.js +8 -113
  38. package/components/breadcrumb/class.js +132 -0
  39. package/components/button/button.d.ts +34 -0
  40. package/components/button/button.js +15 -66
  41. package/components/button/class.js +80 -0
  42. package/components/calendar-picker/calendar-picker.a2ui.json +6 -1
  43. package/components/calendar-picker/calendar-picker.js +8 -332
  44. package/components/calendar-picker/calendar-picker.yaml +51 -177
  45. package/components/calendar-picker/class.js +351 -0
  46. package/components/canvas/canvas.a2ui.json +6 -1
  47. package/components/canvas/canvas.d.ts +17 -0
  48. package/components/canvas/canvas.yaml +19 -36
  49. package/components/card/card.a2ui.json +3 -0
  50. package/components/card/card.d.ts +27 -0
  51. package/components/card/card.js +9 -50
  52. package/components/card/card.yaml +171 -433
  53. package/components/card/class.js +68 -0
  54. package/components/chart/chart.d.ts +41 -0
  55. package/components/chart/chart.js +8 -2131
  56. package/components/chart/class.js +2150 -0
  57. package/components/chart-legend/chart-legend.d.ts +27 -0
  58. package/components/chart-legend/chart-legend.js +8 -197
  59. package/components/chart-legend/class.js +215 -0
  60. package/components/chat-thread/chat-thread.d.ts +17 -0
  61. package/components/chat-thread/chat-thread.js +8 -157
  62. package/components/chat-thread/class.js +176 -0
  63. package/components/check/check.js +11 -52
  64. package/components/check/class.js +68 -0
  65. package/components/code/class.js +501 -0
  66. package/components/code/code.js +8 -482
  67. package/components/col/class.js +30 -0
  68. package/components/col/col.d.ts +23 -0
  69. package/components/col/col.js +10 -13
  70. package/components/color-picker/class.js +550 -0
  71. package/components/color-picker/color-picker.js +8 -531
  72. package/components/command/class.js +364 -0
  73. package/components/command/command.a2ui.json +3 -0
  74. package/components/command/command.d.ts +19 -0
  75. package/components/command/command.js +8 -345
  76. package/components/command/command.yaml +105 -124
  77. package/components/demo-toggle/class.js +153 -0
  78. package/components/demo-toggle/demo-toggle.d.ts +23 -0
  79. package/components/demo-toggle/demo-toggle.js +8 -135
  80. package/components/description-list/class.js +86 -0
  81. package/components/description-list/description-list.d.ts +21 -0
  82. package/components/description-list/description-list.js +8 -67
  83. package/components/divider/class.js +57 -0
  84. package/components/divider/divider.d.ts +19 -0
  85. package/components/divider/divider.js +10 -40
  86. package/components/drawer/class.js +306 -0
  87. package/components/drawer/drawer.d.ts +25 -0
  88. package/components/drawer/drawer.js +8 -287
  89. package/components/embed/class.js +73 -0
  90. package/components/embed/embed.d.ts +23 -0
  91. package/components/embed/embed.js +9 -55
  92. package/components/empty-state/class.js +108 -0
  93. package/components/empty-state/empty-state.d.ts +21 -0
  94. package/components/empty-state/empty-state.js +9 -90
  95. package/components/feed/class.js +381 -0
  96. package/components/feed/feed.d.ts +19 -0
  97. package/components/feed/feed.js +9 -367
  98. package/components/field/class.js +266 -0
  99. package/components/field/field.d.ts +23 -0
  100. package/components/field/field.js +8 -247
  101. package/components/fields/class.js +106 -0
  102. package/components/fields/fields.d.ts +19 -0
  103. package/components/fields/fields.js +8 -87
  104. package/components/grid/class.js +31 -0
  105. package/components/grid/grid.d.ts +23 -0
  106. package/components/grid/grid.js +10 -14
  107. package/components/heatmap/class.js +305 -0
  108. package/components/heatmap/heatmap.d.ts +31 -0
  109. package/components/heatmap/heatmap.js +8 -286
  110. package/components/icon/class.js +54 -0
  111. package/components/icon/icon.d.ts +23 -0
  112. package/components/icon/icon.js +13 -40
  113. package/components/image/class.js +112 -0
  114. package/components/image/image.d.ts +33 -0
  115. package/components/image/image.js +9 -94
  116. package/components/input/class.js +773 -0
  117. package/components/input/input.a2ui.json +3 -0
  118. package/components/input/input.js +8 -755
  119. package/components/input/input.yaml +171 -442
  120. package/components/inspector/class.js +142 -0
  121. package/components/inspector/inspector.a2ui.json +8 -1
  122. package/components/inspector/inspector.d.ts +17 -0
  123. package/components/inspector/inspector.js +8 -124
  124. package/components/inspector/inspector.yaml +15 -30
  125. package/components/kbd/class.js +34 -0
  126. package/components/kbd/kbd.a2ui.json +3 -0
  127. package/components/kbd/kbd.d.ts +17 -0
  128. package/components/kbd/kbd.js +10 -17
  129. package/components/kbd/kbd.yaml +54 -185
  130. package/components/link/class.js +187 -0
  131. package/components/link/link.d.ts +55 -0
  132. package/components/link/link.js +8 -168
  133. package/components/list/class.js +249 -0
  134. package/components/list/list.d.ts +23 -0
  135. package/components/list/list.js +9 -231
  136. package/components/menu/class.js +332 -0
  137. package/components/menu/menu.d.ts +21 -0
  138. package/components/menu/menu.js +11 -316
  139. package/components/modal/class.js +231 -0
  140. package/components/modal/modal.a2ui.json +5 -1
  141. package/components/modal/modal.d.ts +23 -0
  142. package/components/modal/modal.js +8 -212
  143. package/components/modal/modal.yaml +19 -39
  144. package/components/nav/class.js +150 -0
  145. package/components/nav/nav.d.ts +31 -0
  146. package/components/nav/nav.js +8 -131
  147. package/components/nav-group/class.js +152 -0
  148. package/components/nav-group/nav-group.d.ts +35 -0
  149. package/components/nav-group/nav-group.js +9 -134
  150. package/components/nav-item/class.js +86 -0
  151. package/components/nav-item/nav-item.d.ts +37 -0
  152. package/components/nav-item/nav-item.js +10 -69
  153. package/components/noodles/class.js +510 -0
  154. package/components/noodles/noodles.d.ts +33 -0
  155. package/components/noodles/noodles.js +9 -493
  156. package/components/option-card/class.js +167 -0
  157. package/components/option-card/option-card.js +8 -149
  158. package/components/otp-input/class.js +180 -0
  159. package/components/otp-input/otp-input.a2ui.json +5 -1
  160. package/components/otp-input/otp-input.js +9 -162
  161. package/components/otp-input/otp-input.yaml +45 -174
  162. package/components/page/class.js +97 -0
  163. package/components/page/page.d.ts +46 -0
  164. package/components/page/page.js +8 -79
  165. package/components/pagination/class.js +195 -0
  166. package/components/pagination/pagination.d.ts +23 -0
  167. package/components/pagination/pagination.js +9 -177
  168. package/components/pane/class.js +186 -0
  169. package/components/pane/pane.a2ui.json +12 -1
  170. package/components/pane/pane.d.ts +31 -0
  171. package/components/pane/pane.js +8 -167
  172. package/components/pane/pane.yaml +57 -157
  173. package/components/pipeline-status/class.js +189 -0
  174. package/components/pipeline-status/pipeline-status.a2ui.json +7 -1
  175. package/components/pipeline-status/pipeline-status.d.ts +21 -0
  176. package/components/pipeline-status/pipeline-status.js +9 -172
  177. package/components/pipeline-status/pipeline-status.yaml +34 -72
  178. package/components/popover/class.js +194 -0
  179. package/components/popover/popover.d.ts +23 -0
  180. package/components/popover/popover.js +9 -176
  181. package/components/progress/class.js +74 -0
  182. package/components/progress/progress.a2ui.json +3 -0
  183. package/components/progress/progress.d.ts +19 -0
  184. package/components/progress/progress.js +10 -57
  185. package/components/progress/progress.yaml +124 -287
  186. package/components/progress-row/class.js +110 -0
  187. package/components/progress-row/progress-row.d.ts +23 -0
  188. package/components/progress-row/progress-row.js +8 -92
  189. package/components/radio/class.js +83 -0
  190. package/components/radio/radio.js +11 -67
  191. package/components/range/class.js +194 -0
  192. package/components/range/range.js +9 -176
  193. package/components/rating/class.js +148 -0
  194. package/components/rating/rating.js +9 -130
  195. package/components/richtext/class.js +87 -0
  196. package/components/richtext/richtext.a2ui.json +7 -1
  197. package/components/richtext/richtext.d.ts +19 -0
  198. package/components/richtext/richtext.js +8 -68
  199. package/components/richtext/richtext.yaml +30 -65
  200. package/components/row/class.js +50 -0
  201. package/components/row/row.d.ts +27 -0
  202. package/components/row/row.js +10 -33
  203. package/components/search/class.js +134 -0
  204. package/components/search/search.js +10 -117
  205. package/components/segment/class.js +62 -0
  206. package/components/segment/segment.d.ts +25 -0
  207. package/components/segment/segment.js +10 -45
  208. package/components/segmented/class.js +165 -0
  209. package/components/segmented/segmented.a2ui.json +4 -0
  210. package/components/segmented/segmented.js +10 -148
  211. package/components/segmented/segmented.yaml +41 -59
  212. package/components/select/class.js +408 -0
  213. package/components/select/select.js +15 -396
  214. package/components/skeleton/class.js +52 -0
  215. package/components/skeleton/skeleton.d.ts +23 -0
  216. package/components/skeleton/skeleton.js +8 -34
  217. package/components/slider/class.js +184 -0
  218. package/components/slider/slider.js +9 -166
  219. package/components/stack/class.js +28 -0
  220. package/components/stack/stack.d.ts +17 -0
  221. package/components/stack/stack.js +10 -11
  222. package/components/step-progress/class.js +98 -0
  223. package/components/step-progress/step-progress.d.ts +27 -0
  224. package/components/step-progress/step-progress.js +8 -79
  225. package/components/stepper/class.js +126 -0
  226. package/components/stepper/stepper.d.ts +19 -0
  227. package/components/stepper/stepper.js +9 -112
  228. package/components/stream/class.js +109 -0
  229. package/components/stream/stream.d.ts +19 -0
  230. package/components/stream/stream.js +8 -90
  231. package/components/swatch/class.js +131 -0
  232. package/components/swatch/swatch.d.ts +28 -0
  233. package/components/swatch/swatch.js +8 -112
  234. package/components/swiper/class.js +373 -0
  235. package/components/swiper/swiper.a2ui.json +4 -0
  236. package/components/swiper/swiper.d.ts +31 -0
  237. package/components/swiper/swiper.js +8 -354
  238. package/components/swiper/swiper.yaml +68 -212
  239. package/components/switch/class.js +63 -0
  240. package/components/switch/switch.a2ui.json +6 -1
  241. package/components/switch/switch.js +11 -47
  242. package/components/switch/switch.yaml +70 -265
  243. package/components/table/class.js +1453 -0
  244. package/components/table/table.d.ts +37 -0
  245. package/components/table/table.js +8 -1435
  246. package/components/table-toolbar/class.js +680 -0
  247. package/components/table-toolbar/table-toolbar.d.ts +33 -0
  248. package/components/table-toolbar/table-toolbar.js +8 -689
  249. package/components/tabs/class.js +242 -0
  250. package/components/tabs/tabs.d.ts +21 -0
  251. package/components/tabs/tabs.js +8 -223
  252. package/components/tag/class.js +99 -0
  253. package/components/tag/tag.d.ts +27 -0
  254. package/components/tag/tag.js +8 -80
  255. package/components/text/class.js +46 -0
  256. package/components/text/text.d.ts +25 -0
  257. package/components/text/text.js +9 -28
  258. package/components/textarea/class.js +134 -0
  259. package/components/textarea/textarea.js +11 -118
  260. package/components/timeline/class.js +176 -0
  261. package/components/timeline/timeline.d.ts +19 -0
  262. package/components/timeline/timeline.js +9 -162
  263. package/components/toast/class.js +92 -0
  264. package/components/toast/toast.d.ts +23 -0
  265. package/components/toast/toast.js +9 -76
  266. package/components/toggle-group/class.js +154 -0
  267. package/components/toggle-group/toggle-group.d.ts +19 -0
  268. package/components/toggle-group/toggle-group.js +11 -140
  269. package/components/toggle-scheme/class.js +286 -0
  270. package/components/toggle-scheme/toggle-scheme.d.ts +41 -0
  271. package/components/toggle-scheme/toggle-scheme.js +8 -268
  272. package/components/toolbar/class.js +388 -0
  273. package/components/toolbar/toolbar.d.ts +23 -0
  274. package/components/toolbar/toolbar.js +10 -376
  275. package/components/tooltip/class.js +299 -0
  276. package/components/tooltip/tooltip.d.ts +27 -0
  277. package/components/tooltip/tooltip.js +8 -280
  278. package/components/tree/class.js +245 -0
  279. package/components/tree/tree.d.ts +15 -0
  280. package/components/tree/tree.js +9 -244
  281. package/components/upload/class.js +199 -0
  282. package/components/upload/upload.js +11 -183
  283. package/index.d.ts +159 -5
  284. package/package.json +5 -1
@@ -1,764 +1,17 @@
1
1
  /**
2
- * <input-ui>Text input. The host IS the interactive surface.
3
- * Uses contenteditable for text entry, ElementInternals for form participation.
2
+ * `<input-ui>`auto-registers the tag on import.
4
3
  *
5
- * Slots inside [slot="field"]:
6
- * prefix label → text → suffix → controls (number mode)
4
+ * For non-side-effect class import (test isolation, tag override), use
5
+ * the `class` subpath:
7
6
  *
8
- * <input-ui label="Email" placeholder="you@acme.com"></input-ui>
9
- * <input-ui label="Email" prefix="user" placeholder="you@acme.com"></input-ui>
10
- * <input-ui placeholder="Search" prefix="magnifying-glass"></input-ui>
11
- * <input-ui prefix="@" value="kim"></input-ui>
7
+ * import { UIInput } from '@adia-ai/web-components/components/input/class';
12
8
  *
13
- * <input-ui type="number" value="42" min="0" max="100" step="1"></input-ui>
14
- * <input-ui type="number" value="9.99" step="0.01" precision="2" prefix="$"></input-ui>
15
- *
16
- * type="number" renders a contenteditable surface + [+]/[-] stepper buttons,
17
- * filters input to digits / minus / decimal, snaps to step, clamps to min/max,
18
- * and exposes ARIA spinbutton semantics. No native <input type=number>.
19
- *
20
- * type="password" still wraps a native <input> — only path that needs
21
- * `-webkit-text-security` disc masking, which only works on native inputs.
22
- *
23
- * label renders as a dim leading caption inside the chrome (next to the
24
- * value, sharing the input's border) — for stacked label / hint / error
25
- * compositions, wrap with field-ui.
9
+ * @see ../../USAGE.md#registration--auto-vs-explicit
26
10
  */
27
11
 
28
- import { UIFormElement } from '../../core/form.js';
29
- import { isIconName, whenIconRegistryReady } from '../../core/icons.js';
30
-
31
- const renderAffix = (v) => isIconName(v)
32
- ? `<icon-ui name="${v}"></icon-ui>`
33
- : v;
34
-
35
- class UIInput extends UIFormElement {
36
- // Opt out of UIFormElement's per-control `label` deprecation warning.
37
- // input-ui's `label` is a first-class API rendering an inline-leading
38
- // caption inside the chrome with `aria-labelledby` wiring on the
39
- // editable surface — not the inert above-the-field rendering that
40
- // motivated the deprecation.
41
- static labelDeprecated = false;
42
-
43
- static properties = {
44
- ...UIFormElement.properties,
45
- placeholder: { type: String, default: '', reflect: true },
46
- type: { type: String, default: 'text', reflect: true },
47
- label: { type: String, default: '', reflect: true },
48
- prefix: { type: String, default: '', reflect: true },
49
- suffix: { type: String, default: '', reflect: true },
50
- raw: { type: Boolean, default: false, reflect: true },
51
- // ── Number mode ──
52
- min: { type: Number, default: null, reflect: true },
53
- max: { type: Number, default: null, reflect: true },
54
- step: { type: Number, default: 1, reflect: true },
55
- precision: { type: Number, default: null, reflect: true },
56
- // BCP-47 locale tag, e.g. "de-DE" / "fr-FR" / "en-IN". Default empty =
57
- // en-US (`.` decimal separator, no thousands grouping). When set, the
58
- // input accepts both `.` AND the locale's decimal separator (so en-US-
59
- // formatted programmatic values still parse), and `#format` uses
60
- // `Intl.NumberFormat` for display. Internal storage stays in JS-Number
61
- // canonical form so `.value` round-trips through `Number(v)` unchanged.
62
- locale: { type: String, default: '', reflect: true },
63
- };
64
-
65
- static template = () => null;
66
-
67
- #textEl = null;
68
- #labelEl = null;
69
- #upBtn = null;
70
- #downBtn = null;
71
- #valueAtFocus = '';
72
- #repeatTimer = null;
73
- #repeatDelayTimer = null;
74
- #cachedSep = '.';
75
- #cachedGroup = '';
76
- #cachedSepFor = null;
77
- static #labelSeq = 0;
78
-
79
- // Hold-to-repeat tuning. Initial delay before autorepeat begins, and the
80
- // interval between repeats. Values match the cadence of the native
81
- // <input type="number"> spinner behavior in Chromium/Safari.
82
- static #REPEAT_INITIAL_MS = 400;
83
- static #REPEAT_INTERVAL_MS = 60;
84
-
85
- get #isNativePassword() { return this.type === 'password'; }
86
- get #isNumberMode() { return this.type === 'number'; }
87
-
88
- /** Parsed numeric value. NaN when empty or unparseable. When `locale` is
89
- * set, the value may carry the locale's decimal separator (e.g. "1,5" in
90
- * de-DE); we canonicalize to JS form before `Number(…)`. */
91
- get valueAsNumber() {
92
- const raw = String(this.value ?? '').trim();
93
- if (!raw) return NaN;
94
- const s = this.#toCanonical(raw);
95
- if (s === '-' || s === '.' || s === '-.') return NaN;
96
- const n = Number(s);
97
- return Number.isFinite(n) ? n : NaN;
98
- }
99
- set valueAsNumber(n) {
100
- if (!Number.isFinite(n)) { this.value = ''; return; }
101
- this.value = this.#format(n);
102
- }
103
-
104
- connected() {
105
- super.connected();
106
- this.setAttribute('role', this.#isNumberMode ? 'spinbutton' : 'textbox');
107
-
108
- if (!this.querySelector('[slot="text"]')) {
109
- const labelId = this.label ? `input-label-${++UIInput.#labelSeq}` : '';
110
- this.innerHTML = this.#shellHTML(labelId);
111
- }
112
-
113
- this.#textEl = this.querySelector('[slot="text"]');
114
- this.#labelEl = this.querySelector('[slot="label"]');
115
- this.#upBtn = this.querySelector('[data-step="up"]');
116
- this.#downBtn = this.querySelector('[data-step="down"]');
117
-
118
- if (!this.#isNativePassword && this.value) {
119
- this.#textEl.textContent = this.#isNumberMode
120
- ? this.#formatStored(this.value)
121
- : this.value;
122
- }
123
-
124
- if (this.#textEl) {
125
- this.#textEl.addEventListener('input', this.#onInput);
126
- this.#textEl.addEventListener('keydown', this.#onKeydown);
127
- this.#textEl.addEventListener('blur', this.#onBlur);
128
- this.#textEl.addEventListener('focus', this.#onFocus);
129
- this.#textEl.addEventListener('paste', this.#onPaste);
130
- if (this.#isNumberMode) {
131
- this.#textEl.addEventListener('beforeinput', this.#onBeforeInput);
132
- }
133
- }
134
-
135
- // pointerdown.preventDefault keeps focus on the contenteditable surface
136
- // when the user pokes a stepper button with a pointing device. Same
137
- // handler fires the initial step + arms hold-to-repeat; pointerup/leave/
138
- // cancel on document stops it (the user can drag off the button to
139
- // abort the repeat without lifting their finger first).
140
- this.#upBtn?.addEventListener('pointerdown', this.#onStepperUpDown);
141
- this.#downBtn?.addEventListener('pointerdown', this.#onStepperDownDown);
142
- // Stop autorepeat on any pointer release, anywhere — captures the
143
- // "drag-off-then-lift" abort path without per-button leave/cancel
144
- // bookkeeping. Cheap; runs only while a stepper is held.
145
-
146
- // In non-Vite static deploys, the icon registry loads asynchronously
147
- // after the manifest fetch resolves. If our prefix/suffix were checked
148
- // by isIconName() during that window, kebab-case icon names like
149
- // "magnifying-glass" got baked into the DOM as literal text. Re-evaluate
150
- // once the registry is ready and promote text-rendered affixes to
151
- // <icon-ui>. (No-op on Vite dev where the promise resolves synchronously.)
152
- if (this.prefix || this.suffix) {
153
- whenIconRegistryReady.then(() => this.#promoteAffixes());
154
- }
155
- }
156
-
157
- #shellHTML(labelId) {
158
- const prefix = this.prefix ? `<span slot="prefix">${renderAffix(this.prefix)}</span>` : '';
159
- const label = this.label ? `<span slot="label" id="${labelId}">${this.label}</span>` : '';
160
- const suffix = this.suffix ? `<span slot="suffix">${renderAffix(this.suffix)}</span>` : '';
161
- const labelby = labelId ? `aria-labelledby="${labelId}"` : '';
162
-
163
- if (this.#isNativePassword) {
164
- return `
165
- <div slot="field">
166
- ${prefix}${label}
167
- <input slot="text" type="password" tabindex="0"
168
- placeholder="${this.placeholder}" value="${this.value || ''}"
169
- autocomplete="current-password" ${labelby}
170
- ${this.disabled ? 'disabled' : ''} ${this.readonly ? 'readonly' : ''} />
171
- ${suffix}
172
- </div>
173
- `;
174
- }
175
-
176
- const editable = `
177
- <span slot="text" contenteditable="plaintext-only" tabindex="0"
178
- ${this.value ? '' : 'data-empty=""'}
179
- ${labelby}
180
- data-placeholder="${this.placeholder}"
181
- ${this.#isNumberMode ? 'inputmode="decimal"' : ''}></span>`;
182
-
183
- const controls = this.#isNumberMode ? `
184
- <span slot="controls" data-controls aria-hidden="true">
185
- <button-ui type="button" tabindex="-1" variant="ghost" size="xs"
186
- icon="caret-up" data-step="up" aria-label="Increase"></button-ui>
187
- <button-ui type="button" tabindex="-1" variant="ghost" size="xs"
188
- icon="caret-down" data-step="down" aria-label="Decrease"></button-ui>
189
- </span>` : '';
190
-
191
- return `
192
- <div slot="field"${this.#isNumberMode ? ' data-number' : ''}>
193
- ${prefix}${label}${editable}${suffix}${controls}
194
- </div>
195
- `;
196
- }
197
-
198
- #promoteAffixes() {
199
- if (!this.isConnected) return;
200
- for (const which of ['prefix', 'suffix']) {
201
- const value = this[which];
202
- if (!value) continue;
203
- const slot = this.querySelector(`:scope [slot="${which}"]`);
204
- if (!slot) continue;
205
- // Already an <icon-ui> — nothing to do.
206
- if (slot.querySelector(':scope > icon-ui')) continue;
207
- // Was rendered as text and the value is now a known icon — replace.
208
- if (isIconName(value)) {
209
- slot.replaceChildren();
210
- const icon = document.createElement('icon-ui');
211
- icon.setAttribute('name', value);
212
- slot.appendChild(icon);
213
- }
214
- }
215
- }
216
-
217
- render() {
218
- if (!this.#textEl) return;
219
-
220
- const text = this.value ?? '';
221
-
222
- if (this.#isNativePassword) {
223
- this.#textEl.placeholder = this.placeholder;
224
- this.#textEl.disabled = this.disabled;
225
- this.#textEl.readOnly = this.readonly;
226
- if (this.#textEl.value !== text) this.#textEl.value = text;
227
- } else {
228
- this.#textEl.setAttribute('data-placeholder', this.placeholder);
229
- if (this.disabled || this.readonly) {
230
- this.#textEl.contentEditable = 'false';
231
- } else {
232
- this.#textEl.contentEditable = 'plaintext-only';
233
- }
234
- // Sync programmatic value writes into the contenteditable surface.
235
- // Skip when already in sync to avoid clobbering an in-flight edit's
236
- // caret position. For number mode, render the formatted display, but
237
- // only when the surface DOESN'T have focus (mid-edit reformat would
238
- // wipe caret + lose the user's transient state like "9." → "9").
239
- const display = this.#isNumberMode && document.activeElement !== this.#textEl
240
- ? this.#formatStored(text)
241
- : String(text);
242
- if (this.#textEl.textContent !== display) {
243
- this.#textEl.textContent = display;
244
- this.#textEl.toggleAttribute('data-empty', !display);
245
- }
246
- }
247
-
248
- if (this.#labelEl) this.#labelEl.textContent = this.label || '';
249
-
250
- if (this.label) {
251
- this.removeAttribute('aria-label');
252
- } else if (this.placeholder) {
253
- this.setAttribute('aria-label', this.placeholder);
254
- } else {
255
- this.removeAttribute('aria-label');
256
- }
257
-
258
- if (this.#isNumberMode) {
259
- const n = this.valueAsNumber;
260
- if (Number.isFinite(n)) {
261
- this.setAttribute('aria-valuenow', String(n));
262
- this.setAttribute('aria-valuetext', `${this.#format(n)}${this.suffix ? ' ' + this.suffix : ''}`);
263
- } else {
264
- this.removeAttribute('aria-valuenow');
265
- this.removeAttribute('aria-valuetext');
266
- }
267
- if (this.min != null) this.setAttribute('aria-valuemin', String(this.min));
268
- else this.removeAttribute('aria-valuemin');
269
- if (this.max != null) this.setAttribute('aria-valuemax', String(this.max));
270
- else this.removeAttribute('aria-valuemax');
271
-
272
- const disableUp = this.disabled || this.readonly || (this.max != null && Number.isFinite(n) && n >= this.max);
273
- const disableDown = this.disabled || this.readonly || (this.min != null && Number.isFinite(n) && n <= this.min);
274
- this.#upBtn?.toggleAttribute('disabled', !!disableUp);
275
- this.#downBtn?.toggleAttribute('disabled', !!disableDown);
276
- }
277
- }
278
-
279
- // ── Value sync + validation override ──
280
-
281
- syncValue(val) {
282
- val = val ?? this.value ?? '';
283
- super.syncValue(String(val));
284
- if (this.#isNumberMode) this.#runNumberConstraints(String(val));
285
- }
12
+ import { defineIfFree } from '../../core/register.js';
13
+ import { UIInput } from './class.js';
286
14
 
287
- validate() {
288
- const baseValid = super.validate();
289
- if (!this.#isNumberMode) return baseValid;
290
- // super.validate cleared validity if all base constraints passed; layer
291
- // number-specific checks on top.
292
- if (!baseValid) return false;
293
- const numValid = this.#runNumberConstraints(this.value ?? '');
294
- if (!numValid) {
295
- this.setAttribute('aria-invalid', 'true');
296
- this.error = this.validationMessage;
297
- }
298
- return numValid;
299
- }
300
-
301
- #runNumberConstraints(val) {
302
- const raw = String(val ?? '').trim();
303
- // Empty is handled by `required` in the base class; nothing to check here.
304
- if (!raw) return true;
305
- // Canonicalize for `Number(…)` parse — when `locale` is set the raw
306
- // value may carry the locale's decimal separator.
307
- const s = this.#toCanonical(raw);
308
- const n = Number(s);
309
- if (!Number.isFinite(n)) {
310
- this.internals.setValidity(
311
- { badInput: true },
312
- this.getAttribute('data-msg-bad-input') || 'Please enter a valid number.',
313
- this,
314
- );
315
- return false;
316
- }
317
- if (this.min != null && n < this.min) {
318
- this.internals.setValidity(
319
- { rangeUnderflow: true },
320
- this.getAttribute('data-msg-min') || `Value must be ${this.min} or greater.`,
321
- this,
322
- );
323
- return false;
324
- }
325
- if (this.max != null && n > this.max) {
326
- this.internals.setValidity(
327
- { rangeOverflow: true },
328
- this.getAttribute('data-msg-max') || `Value must be ${this.max} or less.`,
329
- this,
330
- );
331
- return false;
332
- }
333
- return true;
334
- }
335
-
336
- // ── Number helpers ──
337
-
338
- #decimals() {
339
- if (this.precision != null) return Math.max(0, this.precision | 0);
340
- const stepStr = String(this.step ?? 1);
341
- return (stepStr.split('.')[1] || '').length;
342
- }
343
-
344
- /** Locale's decimal separator, or '.' for the default en-US-equivalent path.
345
- * Result cached per-locale on the host so `Intl.NumberFormat.formatToParts`
346
- * isn't called per keystroke. */
347
- #decimalSep() {
348
- if (!this.locale) return '.';
349
- if (this.#cachedSepFor === this.locale) return this.#cachedSep;
350
- this.#refreshSepCache();
351
- return this.#cachedSep;
352
- }
353
-
354
- /** Locale's thousands/grouping separator (e.g. `,` in en-US, `.` in de-DE).
355
- * Returns '' for the default path (no locale → no grouping). Cached
356
- * alongside the decimal separator. */
357
- #groupSep() {
358
- if (!this.locale) return '';
359
- if (this.#cachedSepFor === this.locale) return this.#cachedGroup;
360
- this.#refreshSepCache();
361
- return this.#cachedGroup;
362
- }
363
-
364
- #refreshSepCache() {
365
- try {
366
- const parts = new Intl.NumberFormat(this.locale).formatToParts(1234567.89);
367
- this.#cachedSep = parts.find((p) => p.type === 'decimal')?.value || '.';
368
- this.#cachedGroup = parts.find((p) => p.type === 'group')?.value || '';
369
- } catch {
370
- this.#cachedSep = '.';
371
- this.#cachedGroup = '';
372
- }
373
- this.#cachedSepFor = this.locale;
374
- }
375
-
376
- /** Convert a locale-formatted numeric string to the JS-canonical form
377
- * (decimal `.`, no thousands grouping). Strips group separators first so
378
- * "1.234,5" (de-DE) → "1234.5", "1,234.5" (en-US) → "1234.5". Pure string
379
- * transform; no validation. */
380
- #toCanonical(s) {
381
- const sep = this.#decimalSep();
382
- const group = this.#groupSep();
383
- let out = String(s);
384
- if (group) out = out.split(group).join('');
385
- if (sep !== '.') out = out.replace(new RegExp(`\\${sep}`, 'g'), '.');
386
- return out;
387
- }
388
-
389
- /** Internal/edit-mode format: locale decimal separator, NO thousands
390
- * grouping. Used for `this.value` storage and for the textContent
391
- * rendering while the input is focused (so the user can edit without
392
- * the group separator jumping around as they type). */
393
- #format(n) {
394
- if (!Number.isFinite(n)) return '';
395
- const d = this.#decimals();
396
- if (this.locale) {
397
- try {
398
- return new Intl.NumberFormat(this.locale, {
399
- minimumFractionDigits: d,
400
- maximumFractionDigits: d,
401
- useGrouping: false,
402
- }).format(n);
403
- } catch { /* fall through to JS toFixed */ }
404
- }
405
- return d > 0 ? n.toFixed(d) : String(Math.round(n));
406
- }
407
-
408
- /** Display-mode format: locale decimal separator + thousands grouping when
409
- * the locale supports it. Used for the textContent rendering when the
410
- * input is NOT focused (initial render + post-blur). Returns the same as
411
- * `#format` when no `locale` is set. */
412
- #formatDisplay(n) {
413
- if (!Number.isFinite(n)) return '';
414
- if (!this.locale) return this.#format(n);
415
- const d = this.#decimals();
416
- try {
417
- return new Intl.NumberFormat(this.locale, {
418
- minimumFractionDigits: d,
419
- maximumFractionDigits: d,
420
- useGrouping: true,
421
- }).format(n);
422
- } catch { return this.#format(n); }
423
- }
424
-
425
- /** Display value derived from the stored string. During focus we leave
426
- * the user's raw text alone; otherwise reformat (e.g. "9.9" → "9.90"
427
- * for precision=2). Non-numeric stored strings pass through unchanged
428
- * so error-state visuals can echo what the user typed. */
429
- #formatStored(stored) {
430
- const s = String(stored ?? '');
431
- if (!s) return '';
432
- // Canonicalize before Number() — `.value` may carry the locale's
433
- // decimal separator if the host has `locale` set.
434
- const n = Number(this.#toCanonical(s));
435
- if (!Number.isFinite(n)) return s;
436
- // If the input is currently focused, render without grouping so the
437
- // user can edit naturally; otherwise group when locale is set. Falls
438
- // back to #format (ungrouped) when there's no locale.
439
- return document.activeElement === this.#textEl
440
- ? this.#format(n)
441
- : this.#formatDisplay(n);
442
- }
443
-
444
- #snap(raw) {
445
- const step = this.step || 1;
446
- const base = this.min != null ? this.min : 0;
447
- const stepped = Math.round((raw - base) / step) * step + base;
448
- const clamped = Math.max(
449
- this.min != null ? this.min : -Infinity,
450
- Math.min(this.max != null ? this.max : Infinity, stepped),
451
- );
452
- return parseFloat(clamped.toFixed(10));
453
- }
454
-
455
- #stepBy(multiplier) {
456
- if (this.disabled || this.readonly) return;
457
- const step = (this.step || 1) * multiplier;
458
- const current = Number.isFinite(this.valueAsNumber)
459
- ? this.valueAsNumber
460
- : (this.min != null ? this.min : 0);
461
- const next = this.#snap(current + step);
462
- if (next === this.valueAsNumber) return;
463
- this.value = this.#format(next);
464
- this.syncValue(this.value);
465
- this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: this.value } }));
466
- this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
467
- }
468
-
469
- // ── Event handlers ──
470
-
471
- #onInput = () => {
472
- let text;
473
- if (this.#isNativePassword) {
474
- text = this.#textEl.value || '';
475
- } else if (this.#isNumberMode) {
476
- // beforeinput filtered the keystroke; some browsers still let through
477
- // composition or paste events that bypass beforeinput. Re-sanitize.
478
- const raw = this.#textEl.textContent || '';
479
- text = this.#sanitizeNumeric(raw);
480
- if (text !== raw) {
481
- // Soft-revert: restore filtered text + put caret at end. Rare path.
482
- this.#textEl.textContent = text;
483
- this.#placeCaretAtEnd();
484
- }
485
- } else {
486
- text = this.#textEl.textContent || '';
487
- }
488
- this.value = text;
489
- if (!this.#isNativePassword) this.#textEl.toggleAttribute('data-empty', !text);
490
- this.syncValue(text);
491
- this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: this.value } }));
492
- };
493
-
494
- #onBeforeInput = (e) => {
495
- // Allow deletions, formatting, composition — only gate text insertions.
496
- const t = e.inputType;
497
- if (!t || !t.startsWith('insert')) return;
498
- if (t === 'insertCompositionText') return; // IME — let through, #onInput cleans up
499
- const incoming = (e.data ?? '');
500
- if (!incoming) return;
501
- const current = this.#textEl.textContent || '';
502
- const sel = window.getSelection();
503
- // Build prospective string: replace selection (or insert at caret).
504
- let start = current.length, end = current.length;
505
- if (sel && sel.rangeCount && this.#textEl.contains(sel.anchorNode)) {
506
- const r = sel.getRangeAt(0);
507
- start = this.#offsetFromTextStart(r.startContainer, r.startOffset);
508
- end = this.#offsetFromTextStart(r.endContainer, r.endOffset);
509
- if (start > end) [start, end] = [end, start];
510
- }
511
- const prospective = current.slice(0, start) + incoming + current.slice(end);
512
- if (!this.#isNumericProspect(prospective)) e.preventDefault();
513
- };
514
-
515
- #isNumericProspect(s) {
516
- // Permissive while typing: allow lone '-', lone '.', and trailing '.'.
517
- // Reject scientific notation, multiple decimals, multiple signs.
518
- // When `locale` is set, accept both '.' AND the locale's decimal
519
- // separator, and silently strip thousands-group separators (paste of
520
- // "1,234.5" or "1.234,5" both validate).
521
- const c = this.#toCanonical(s);
522
- if (c === '' || c === '-' || c === '.' || c === '-.') {
523
- return c === '' || c === '-' || (this.min == null || this.min < 0) ? true : false;
524
- }
525
- if (!/^-?\d*\.?\d*$/.test(c)) return false;
526
- if (c.startsWith('-') && this.min != null && this.min >= 0) return false;
527
- return true;
528
- }
529
-
530
- #sanitizeNumeric(s) {
531
- // Strip everything but digits / one leading minus / one decimal point.
532
- // The decimal mark is the locale's separator; characters that match the
533
- // locale's group separator (e.g. `.` in de-DE, `,` in en-US) are silently
534
- // dropped — never preserved in `this.value`. The blur handler re-renders
535
- // with grouping for display via `#formatDisplay`.
536
- //
537
- // Note on programmatic `.value = "1.5"` in de-DE: that path doesn't run
538
- // through sanitization (UIFormElement.value setter is string-only), so
539
- // canonical-form programmatic values still parse correctly via
540
- // `valueAsNumber` (which canonicalizes through `#toCanonical`). Only
541
- // user-typed/-pasted input flows through this sanitizer, and there the
542
- // locale interpretation (`.` = group when sep=`,`) is the correct read.
543
- const sep = this.#decimalSep();
544
- let out = '';
545
- let sawDecimal = false;
546
- for (let i = 0; i < s.length; i++) {
547
- const c = s[i];
548
- if (c >= '0' && c <= '9') out += c;
549
- else if (c === '-' && out === '' && (this.min == null || this.min < 0)) out += c;
550
- else if (c === sep && !sawDecimal) { out += sep; sawDecimal = true; }
551
- // group separator and other punctuation silently dropped
552
- }
553
- return out;
554
- }
555
-
556
- #offsetFromTextStart(node, offset) {
557
- // Walk the text descendants until we reach `node`, accumulating chars.
558
- if (!this.#textEl.contains(node)) return 0;
559
- let acc = 0;
560
- const walker = document.createTreeWalker(this.#textEl, NodeFilter.SHOW_TEXT);
561
- let n;
562
- while ((n = walker.nextNode())) {
563
- if (n === node) return acc + offset;
564
- acc += n.textContent.length;
565
- }
566
- return node === this.#textEl ? offset : acc;
567
- }
568
-
569
- #placeCaretAtEnd() {
570
- const sel = window.getSelection();
571
- const range = document.createRange();
572
- range.selectNodeContents(this.#textEl);
573
- range.collapse(false);
574
- sel.removeAllRanges();
575
- sel.addRange(range);
576
- }
577
-
578
- #onKeydown = (e) => {
579
- if (this.#isNumberMode) {
580
- switch (e.key) {
581
- case 'ArrowUp': e.preventDefault(); this.#stepBy( 1); return;
582
- case 'ArrowDown': e.preventDefault(); this.#stepBy(-1); return;
583
- case 'PageUp': e.preventDefault(); this.#stepBy( 10); return;
584
- case 'PageDown': e.preventDefault(); this.#stepBy(-10); return;
585
- case 'Home':
586
- if (this.min != null) { e.preventDefault(); this.#commitNumeric(this.min); }
587
- return;
588
- case 'End':
589
- if (this.max != null) { e.preventDefault(); this.#commitNumeric(this.max); }
590
- return;
591
- case 'Escape':
592
- e.preventDefault();
593
- this.value = this.#valueAtFocus;
594
- this.#textEl.textContent = this.#formatStored(this.value);
595
- this.#textEl.toggleAttribute('data-empty', !this.value);
596
- this.syncValue(this.value);
597
- this.#textEl.blur();
598
- return;
599
- case 'Enter':
600
- e.preventDefault();
601
- // Commit normalized value before firing form events.
602
- this.#commitOnBlur();
603
- this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
604
- this.dispatchEvent(new Event('submit', { bubbles: true }));
605
- return;
606
- }
607
- return;
608
- }
609
- if (e.key === 'Enter') {
610
- e.preventDefault();
611
- this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
612
- this.dispatchEvent(new Event('submit', { bubbles: true }));
613
- }
614
- };
615
-
616
- #onFocus = () => {
617
- this.#valueAtFocus = this.value ?? '';
618
- // When focused: re-render textContent without thousands grouping so the
619
- // user can edit naturally — group separators jumping mid-keystroke is
620
- // disorienting. Only matters when `locale` is set AND the post-blur
621
- // render added grouping; no-op for the default `.` path.
622
- if (this.#isNumberMode && this.locale) {
623
- const raw = String(this.value ?? '').trim();
624
- if (!raw) return;
625
- const n = Number(this.#toCanonical(raw));
626
- if (!Number.isFinite(n)) return;
627
- const ungrouped = this.#format(n);
628
- if (this.#textEl.textContent !== ungrouped) this.#textEl.textContent = ungrouped;
629
- }
630
- };
631
-
632
- #onBlur = () => {
633
- if (this.#isNumberMode) this.#commitOnBlur();
634
- this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
635
- };
636
-
637
- #commitOnBlur() {
638
- const raw = String(this.value ?? '').trim();
639
- if (!raw) return;
640
- // Canonicalize before Number() — `this.value` may carry the locale's
641
- // decimal separator (e.g. "1,5" in de-DE).
642
- const n = Number(this.#toCanonical(raw));
643
- if (!Number.isFinite(n)) return; // leave the bad input visible for the error UX
644
- const snapped = this.#snap(n);
645
- // `this.value` stores the ungrouped, locale-decimal form (round-trippable
646
- // through #toCanonical → Number → #format). textContent shows the
647
- // grouped display form when `locale` is set.
648
- const stored = this.#format(snapped);
649
- const displayed = this.#formatDisplay(snapped);
650
- if (this.value !== stored) {
651
- this.value = stored;
652
- this.syncValue(stored);
653
- this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: this.value } }));
654
- }
655
- if (this.#textEl.textContent !== displayed) {
656
- this.#textEl.textContent = displayed;
657
- this.#textEl.toggleAttribute('data-empty', !displayed);
658
- }
659
- }
660
-
661
- #commitNumeric(n) {
662
- const snapped = this.#snap(n);
663
- if (snapped === this.valueAsNumber) return;
664
- this.value = this.#format(snapped);
665
- this.syncValue(this.value);
666
- this.#textEl.textContent = this.value;
667
- this.#textEl.toggleAttribute('data-empty', !this.value);
668
- this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: this.value } }));
669
- this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
670
- }
671
-
672
- #onPaste = (e) => {
673
- e.preventDefault();
674
- const raw = e.clipboardData?.getData('text/plain') || '';
675
- const text = this.#isNumberMode ? this.#sanitizeNumeric(raw) : raw;
676
- document.execCommand('insertText', false, text);
677
- };
678
-
679
- // Hold-to-repeat: pointerdown fires the initial step + arms an autorepeat
680
- // timer. The first repeat fires after REPEAT_INITIAL_MS; subsequent ones
681
- // every REPEAT_INTERVAL_MS. pointerup on document stops everything. We
682
- // also stop on a stale value (disabled at min/max boundary) so the
683
- // browser doesn't keep firing input events for no-op increments.
684
- #onStepperUpDown = (e) => this.#startStepperHold(e, 1);
685
- #onStepperDownDown = (e) => this.#startStepperHold(e, -1);
686
-
687
- #startStepperHold(e, multiplier) {
688
- // Keep focus on the editable surface when the button is pressed.
689
- e.preventDefault();
690
- if (this.disabled || this.readonly) return;
691
- // Initial step fires immediately on press.
692
- this.#stepBy(multiplier);
693
- this.#stopStepperHold();
694
- // Listen for release on document (cheap; only while held).
695
- document.addEventListener('pointerup', this.#onStepperRelease, { once: true });
696
- document.addEventListener('pointercancel', this.#onStepperRelease, { once: true });
697
- // Initial delay → then continuous repeat.
698
- this.#repeatDelayTimer = window.setTimeout(() => {
699
- this.#repeatDelayTimer = null;
700
- this.#repeatTimer = window.setInterval(() => {
701
- const before = this.valueAsNumber;
702
- this.#stepBy(multiplier);
703
- // Boundary hit → no-op; cancel to avoid wasted intervals + event spam.
704
- if (this.valueAsNumber === before) this.#stopStepperHold();
705
- }, UIInput.#REPEAT_INTERVAL_MS);
706
- }, UIInput.#REPEAT_INITIAL_MS);
707
- }
708
-
709
- #onStepperRelease = () => this.#stopStepperHold();
710
-
711
- #stopStepperHold() {
712
- if (this.#repeatDelayTimer != null) {
713
- window.clearTimeout(this.#repeatDelayTimer);
714
- this.#repeatDelayTimer = null;
715
- }
716
- if (this.#repeatTimer != null) {
717
- window.clearInterval(this.#repeatTimer);
718
- this.#repeatTimer = null;
719
- }
720
- document.removeEventListener('pointerup', this.#onStepperRelease);
721
- document.removeEventListener('pointercancel', this.#onStepperRelease);
722
- }
723
-
724
- focus() { this.#textEl?.focus(); }
725
-
726
- clear() {
727
- this.value = '';
728
- if (this.#textEl) {
729
- if (this.#isNativePassword) {
730
- this.#textEl.value = '';
731
- } else {
732
- this.#textEl.textContent = '';
733
- this.#textEl.setAttribute('data-empty', '');
734
- }
735
- }
736
- this.syncValue('');
737
- }
738
-
739
- disconnected() {
740
- super.disconnected();
741
- if (this.#textEl) {
742
- this.#textEl.removeEventListener('input', this.#onInput);
743
- this.#textEl.removeEventListener('keydown', this.#onKeydown);
744
- this.#textEl.removeEventListener('blur', this.#onBlur);
745
- this.#textEl.removeEventListener('focus', this.#onFocus);
746
- this.#textEl.removeEventListener('paste', this.#onPaste);
747
- this.#textEl.removeEventListener('beforeinput', this.#onBeforeInput);
748
- }
749
- this.#upBtn?.removeEventListener('pointerdown', this.#onStepperUpDown);
750
- this.#downBtn?.removeEventListener('pointerdown', this.#onStepperDownDown);
751
- // Cancel any in-flight hold (the document-level pointerup listener
752
- // is `{once: true}` so it self-cleans on fire; this also clears the
753
- // timers if the host disconnects mid-hold).
754
- this.#stopStepperHold();
755
- this.#textEl = null;
756
- this.#labelEl = null;
757
- this.#upBtn = null;
758
- this.#downBtn = null;
759
- }
760
- }
761
- customElements.define('input-ui', UIInput);
15
+ defineIfFree('input-ui', UIInput);
762
16
 
763
17
  export { UIInput };
764
-