@adia-ai/web-components 0.4.5 → 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 (316) hide show
  1. package/README.md +63 -24
  2. package/USAGE.md +604 -0
  3. package/components/accordion/accordion.d.ts +17 -0
  4. package/components/accordion/accordion.js +10 -117
  5. package/components/accordion/class.js +132 -0
  6. package/components/action-list/action-list.d.ts +15 -0
  7. package/components/action-list/action-list.js +9 -140
  8. package/components/action-list/class.js +156 -0
  9. package/components/agent-artifact/agent-artifact.d.ts +25 -0
  10. package/components/agent-artifact/agent-artifact.js +8 -181
  11. package/components/agent-artifact/class.js +200 -0
  12. package/components/agent-feedback-bar/agent-feedback-bar.d.ts +21 -0
  13. package/components/agent-feedback-bar/agent-feedback-bar.js +8 -143
  14. package/components/agent-feedback-bar/class.js +162 -0
  15. package/components/agent-questions/agent-questions.d.ts +23 -0
  16. package/components/agent-questions/agent-questions.js +8 -180
  17. package/components/agent-questions/class.js +199 -0
  18. package/components/agent-reasoning/agent-reasoning.d.ts +23 -0
  19. package/components/agent-reasoning/agent-reasoning.js +8 -494
  20. package/components/agent-reasoning/class.js +513 -0
  21. package/components/agent-suggestions/agent-suggestions.d.ts +21 -0
  22. package/components/agent-suggestions/agent-suggestions.js +8 -78
  23. package/components/agent-suggestions/class.js +97 -0
  24. package/components/agent-trace/agent-trace.d.ts +19 -0
  25. package/components/alert/alert.d.ts +29 -0
  26. package/components/alert/alert.js +8 -175
  27. package/components/alert/class.js +194 -0
  28. package/components/avatar/avatar.d.ts +27 -0
  29. package/components/avatar/avatar.js +9 -159
  30. package/components/avatar/class.js +173 -0
  31. package/components/badge/badge.d.ts +27 -0
  32. package/components/badge/badge.js +9 -75
  33. package/components/badge/class.js +93 -0
  34. package/components/block/block.d.ts +19 -0
  35. package/components/block/block.js +9 -15
  36. package/components/block/class.js +33 -0
  37. package/components/breadcrumb/breadcrumb.d.ts +23 -0
  38. package/components/breadcrumb/breadcrumb.js +8 -113
  39. package/components/breadcrumb/class.js +132 -0
  40. package/components/button/button.d.ts +34 -0
  41. package/components/button/button.js +15 -66
  42. package/components/button/class.js +80 -0
  43. package/components/calendar-picker/calendar-picker.a2ui.json +6 -1
  44. package/components/calendar-picker/calendar-picker.d.ts +27 -0
  45. package/components/calendar-picker/calendar-picker.js +8 -332
  46. package/components/calendar-picker/calendar-picker.yaml +51 -177
  47. package/components/calendar-picker/class.js +351 -0
  48. package/components/canvas/canvas.a2ui.json +6 -1
  49. package/components/canvas/canvas.d.ts +17 -0
  50. package/components/canvas/canvas.yaml +19 -36
  51. package/components/card/card.a2ui.json +3 -0
  52. package/components/card/card.d.ts +27 -0
  53. package/components/card/card.js +9 -50
  54. package/components/card/card.yaml +171 -433
  55. package/components/card/class.js +68 -0
  56. package/components/chart/chart.d.ts +41 -0
  57. package/components/chart/chart.js +8 -2131
  58. package/components/chart/class.js +2150 -0
  59. package/components/chart-legend/chart-legend.d.ts +27 -0
  60. package/components/chart-legend/chart-legend.js +8 -197
  61. package/components/chart-legend/class.js +215 -0
  62. package/components/chat-thread/chat-thread.d.ts +17 -0
  63. package/components/chat-thread/chat-thread.js +8 -157
  64. package/components/chat-thread/class.js +176 -0
  65. package/components/check/check.d.ts +30 -0
  66. package/components/check/check.js +11 -52
  67. package/components/check/class.js +68 -0
  68. package/components/code/class.js +501 -0
  69. package/components/code/code.d.ts +39 -0
  70. package/components/code/code.js +8 -482
  71. package/components/col/class.js +30 -0
  72. package/components/col/col.d.ts +23 -0
  73. package/components/col/col.js +10 -13
  74. package/components/color-picker/class.js +550 -0
  75. package/components/color-picker/color-picker.d.ts +37 -0
  76. package/components/color-picker/color-picker.js +8 -531
  77. package/components/command/class.js +364 -0
  78. package/components/command/command.a2ui.json +3 -0
  79. package/components/command/command.d.ts +19 -0
  80. package/components/command/command.js +8 -345
  81. package/components/command/command.yaml +105 -124
  82. package/components/demo-toggle/class.js +153 -0
  83. package/components/demo-toggle/demo-toggle.d.ts +23 -0
  84. package/components/demo-toggle/demo-toggle.js +8 -135
  85. package/components/description-list/class.js +86 -0
  86. package/components/description-list/description-list.d.ts +21 -0
  87. package/components/description-list/description-list.js +8 -67
  88. package/components/divider/class.js +57 -0
  89. package/components/divider/divider.d.ts +19 -0
  90. package/components/divider/divider.js +10 -40
  91. package/components/drawer/class.js +306 -0
  92. package/components/drawer/drawer.d.ts +25 -0
  93. package/components/drawer/drawer.js +8 -287
  94. package/components/embed/class.js +73 -0
  95. package/components/embed/embed.d.ts +23 -0
  96. package/components/embed/embed.js +9 -55
  97. package/components/empty-state/class.js +108 -0
  98. package/components/empty-state/empty-state.d.ts +21 -0
  99. package/components/empty-state/empty-state.js +9 -90
  100. package/components/feed/class.js +381 -0
  101. package/components/feed/feed.d.ts +19 -0
  102. package/components/feed/feed.js +9 -367
  103. package/components/field/class.js +266 -0
  104. package/components/field/field.d.ts +23 -0
  105. package/components/field/field.js +8 -247
  106. package/components/fields/class.js +106 -0
  107. package/components/fields/fields.d.ts +19 -0
  108. package/components/fields/fields.js +8 -87
  109. package/components/grid/class.js +31 -0
  110. package/components/grid/grid.d.ts +23 -0
  111. package/components/grid/grid.js +10 -14
  112. package/components/heatmap/class.js +305 -0
  113. package/components/heatmap/heatmap.d.ts +31 -0
  114. package/components/heatmap/heatmap.js +8 -286
  115. package/components/icon/class.js +54 -0
  116. package/components/icon/icon.d.ts +23 -0
  117. package/components/icon/icon.js +13 -40
  118. package/components/image/class.js +112 -0
  119. package/components/image/image.d.ts +33 -0
  120. package/components/image/image.js +9 -94
  121. package/components/index.js +1 -0
  122. package/components/input/class.js +773 -0
  123. package/components/input/input.a2ui.json +3 -0
  124. package/components/input/input.d.ts +61 -0
  125. package/components/input/input.js +8 -755
  126. package/components/input/input.yaml +171 -442
  127. package/components/inspector/class.js +142 -0
  128. package/components/inspector/inspector.a2ui.json +8 -1
  129. package/components/inspector/inspector.d.ts +17 -0
  130. package/components/inspector/inspector.js +8 -124
  131. package/components/inspector/inspector.yaml +15 -30
  132. package/components/kbd/class.js +34 -0
  133. package/components/kbd/kbd.a2ui.json +3 -0
  134. package/components/kbd/kbd.d.ts +17 -0
  135. package/components/kbd/kbd.js +10 -17
  136. package/components/kbd/kbd.yaml +54 -185
  137. package/components/link/class.js +187 -0
  138. package/components/link/link.d.ts +55 -0
  139. package/components/link/link.js +8 -168
  140. package/components/list/class.js +249 -0
  141. package/components/list/list.d.ts +23 -0
  142. package/components/list/list.js +9 -231
  143. package/components/menu/class.js +332 -0
  144. package/components/menu/menu.d.ts +21 -0
  145. package/components/menu/menu.js +11 -316
  146. package/components/modal/class.js +231 -0
  147. package/components/modal/modal.a2ui.json +5 -1
  148. package/components/modal/modal.d.ts +23 -0
  149. package/components/modal/modal.js +8 -212
  150. package/components/modal/modal.yaml +19 -39
  151. package/components/nav/class.js +150 -0
  152. package/components/nav/nav.d.ts +31 -0
  153. package/components/nav/nav.js +8 -131
  154. package/components/nav-group/class.js +152 -0
  155. package/components/nav-group/nav-group.d.ts +35 -0
  156. package/components/nav-group/nav-group.js +9 -134
  157. package/components/nav-item/class.js +86 -0
  158. package/components/nav-item/nav-item.d.ts +37 -0
  159. package/components/nav-item/nav-item.js +10 -69
  160. package/components/noodles/class.js +510 -0
  161. package/components/noodles/noodles.d.ts +33 -0
  162. package/components/noodles/noodles.js +9 -493
  163. package/components/option-card/class.js +167 -0
  164. package/components/option-card/option-card.d.ts +30 -0
  165. package/components/option-card/option-card.js +8 -149
  166. package/components/otp-input/class.js +180 -0
  167. package/components/otp-input/otp-input.a2ui.json +5 -1
  168. package/components/otp-input/otp-input.d.ts +25 -0
  169. package/components/otp-input/otp-input.js +9 -162
  170. package/components/otp-input/otp-input.yaml +45 -174
  171. package/components/page/class.js +97 -0
  172. package/components/page/page.d.ts +46 -0
  173. package/components/page/page.js +8 -79
  174. package/components/pagination/class.js +195 -0
  175. package/components/pagination/pagination.d.ts +23 -0
  176. package/components/pagination/pagination.js +9 -177
  177. package/components/pane/class.js +186 -0
  178. package/components/pane/pane.a2ui.json +12 -1
  179. package/components/pane/pane.css +10 -0
  180. package/components/pane/pane.d.ts +31 -0
  181. package/components/pane/pane.js +8 -143
  182. package/components/pane/pane.yaml +57 -157
  183. package/components/pipeline-status/class.js +189 -0
  184. package/components/pipeline-status/pipeline-status.a2ui.json +7 -1
  185. package/components/pipeline-status/pipeline-status.d.ts +21 -0
  186. package/components/pipeline-status/pipeline-status.js +9 -172
  187. package/components/pipeline-status/pipeline-status.yaml +34 -72
  188. package/components/popover/class.js +194 -0
  189. package/components/popover/popover.d.ts +23 -0
  190. package/components/popover/popover.js +9 -176
  191. package/components/progress/class.js +74 -0
  192. package/components/progress/progress.a2ui.json +3 -0
  193. package/components/progress/progress.d.ts +19 -0
  194. package/components/progress/progress.js +10 -57
  195. package/components/progress/progress.yaml +124 -287
  196. package/components/progress-row/class.js +110 -0
  197. package/components/progress-row/progress-row.d.ts +23 -0
  198. package/components/progress-row/progress-row.js +8 -92
  199. package/components/radio/class.js +83 -0
  200. package/components/radio/radio.d.ts +28 -0
  201. package/components/radio/radio.js +11 -67
  202. package/components/range/class.js +194 -0
  203. package/components/range/range.d.ts +31 -0
  204. package/components/range/range.js +9 -176
  205. package/components/rating/class.js +148 -0
  206. package/components/rating/rating.d.ts +33 -0
  207. package/components/rating/rating.js +9 -130
  208. package/components/richtext/class.js +87 -0
  209. package/components/richtext/richtext.a2ui.json +7 -1
  210. package/components/richtext/richtext.d.ts +19 -0
  211. package/components/richtext/richtext.js +8 -68
  212. package/components/richtext/richtext.yaml +30 -65
  213. package/components/row/class.js +50 -0
  214. package/components/row/row.d.ts +27 -0
  215. package/components/row/row.js +10 -33
  216. package/components/search/class.js +134 -0
  217. package/components/search/search.d.ts +35 -0
  218. package/components/search/search.js +10 -117
  219. package/components/segment/class.js +62 -0
  220. package/components/segment/segment.d.ts +25 -0
  221. package/components/segment/segment.js +10 -45
  222. package/components/segmented/class.js +165 -0
  223. package/components/segmented/segmented.a2ui.json +4 -0
  224. package/components/segmented/segmented.d.ts +24 -0
  225. package/components/segmented/segmented.js +10 -148
  226. package/components/segmented/segmented.yaml +41 -59
  227. package/components/select/class.js +408 -0
  228. package/components/select/select.d.ts +57 -0
  229. package/components/select/select.js +15 -396
  230. package/components/skeleton/class.js +52 -0
  231. package/components/skeleton/skeleton.d.ts +23 -0
  232. package/components/skeleton/skeleton.js +8 -34
  233. package/components/slider/class.js +184 -0
  234. package/components/slider/slider.d.ts +31 -0
  235. package/components/slider/slider.js +9 -166
  236. package/components/stack/class.js +28 -0
  237. package/components/stack/stack.d.ts +17 -0
  238. package/components/stack/stack.js +10 -11
  239. package/components/step-progress/class.js +98 -0
  240. package/components/step-progress/step-progress.d.ts +27 -0
  241. package/components/step-progress/step-progress.js +8 -79
  242. package/components/stepper/class.js +126 -0
  243. package/components/stepper/stepper.d.ts +19 -0
  244. package/components/stepper/stepper.js +9 -112
  245. package/components/stream/class.js +109 -0
  246. package/components/stream/stream.d.ts +19 -0
  247. package/components/stream/stream.js +8 -90
  248. package/components/swatch/class.js +131 -0
  249. package/components/swatch/swatch.d.ts +28 -0
  250. package/components/swatch/swatch.js +8 -112
  251. package/components/swiper/class.js +373 -0
  252. package/components/swiper/swiper.a2ui.json +4 -0
  253. package/components/swiper/swiper.d.ts +31 -0
  254. package/components/swiper/swiper.js +8 -354
  255. package/components/swiper/swiper.yaml +68 -212
  256. package/components/switch/class.js +63 -0
  257. package/components/switch/switch.a2ui.json +6 -1
  258. package/components/switch/switch.d.ts +30 -0
  259. package/components/switch/switch.js +11 -47
  260. package/components/switch/switch.yaml +70 -265
  261. package/components/table/class.js +1453 -0
  262. package/components/table/table.d.ts +37 -0
  263. package/components/table/table.js +8 -1435
  264. package/components/table-toolbar/class.js +680 -0
  265. package/components/table-toolbar/table-toolbar.d.ts +33 -0
  266. package/components/table-toolbar/table-toolbar.js +8 -689
  267. package/components/tabs/class.js +242 -0
  268. package/components/tabs/tabs.d.ts +21 -0
  269. package/components/tabs/tabs.js +8 -223
  270. package/components/tag/class.js +99 -0
  271. package/components/tag/tag.d.ts +27 -0
  272. package/components/tag/tag.js +8 -80
  273. package/components/text/class.js +46 -0
  274. package/components/text/text.d.ts +25 -0
  275. package/components/text/text.js +9 -28
  276. package/components/textarea/class.js +134 -0
  277. package/components/textarea/textarea.d.ts +31 -0
  278. package/components/textarea/textarea.js +11 -118
  279. package/components/timeline/class.js +176 -0
  280. package/components/timeline/timeline.d.ts +19 -0
  281. package/components/timeline/timeline.js +9 -162
  282. package/components/toast/class.js +92 -0
  283. package/components/toast/toast.d.ts +23 -0
  284. package/components/toast/toast.js +9 -76
  285. package/components/toggle-group/class.js +154 -0
  286. package/components/toggle-group/toggle-group.d.ts +19 -0
  287. package/components/toggle-group/toggle-group.js +11 -140
  288. package/components/toggle-scheme/class.js +286 -0
  289. package/components/toggle-scheme/toggle-scheme.a2ui.json +197 -0
  290. package/components/toggle-scheme/toggle-scheme.css +20 -0
  291. package/components/toggle-scheme/toggle-scheme.d.ts +41 -0
  292. package/components/toggle-scheme/toggle-scheme.js +17 -0
  293. package/components/toggle-scheme/toggle-scheme.yaml +173 -0
  294. package/components/toolbar/class.js +388 -0
  295. package/components/toolbar/toolbar.d.ts +23 -0
  296. package/components/toolbar/toolbar.js +10 -376
  297. package/components/tooltip/class.js +299 -0
  298. package/components/tooltip/tooltip.d.ts +27 -0
  299. package/components/tooltip/tooltip.js +8 -280
  300. package/components/tree/class.js +245 -0
  301. package/components/tree/tree.d.ts +15 -0
  302. package/components/tree/tree.js +9 -244
  303. package/components/upload/class.js +199 -0
  304. package/components/upload/upload.d.ts +27 -0
  305. package/components/upload/upload.js +11 -183
  306. package/core/element.d.ts +174 -0
  307. package/core/form.d.ts +108 -0
  308. package/core/index.d.ts +11 -0
  309. package/core/index.js +1 -0
  310. package/core/register.d.ts +25 -0
  311. package/core/register.js +58 -0
  312. package/core/signals.d.ts +94 -0
  313. package/core/template.d.ts +70 -0
  314. package/index.d.ts +315 -0
  315. package/package.json +25 -6
  316. package/traits/CATEGORIES.md +1 -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
-