@adia-ai/web-components 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/components/input/input.a2ui.json +5 -0
- package/components/input/input.js +220 -34
- package/components/input/input.yaml +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,18 @@ A2UI protocol messages into live DOM.
|
|
|
9
9
|
> [`@adia-ai/a2ui-corpus`](../a2ui/corpus); the MCP server in
|
|
10
10
|
> [`@adia-ai/a2ui-mcp`](../a2ui/mcp).
|
|
11
11
|
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @adia-ai/web-components
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
For composite shells (admin / chat / editor / simple / theme clusters), pair with [`@adia-ai/web-modules`](../web-modules):
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @adia-ai/web-components @adia-ai/web-modules
|
|
22
|
+
```
|
|
23
|
+
|
|
12
24
|
## Quick start
|
|
13
25
|
|
|
14
26
|
```html
|
|
@@ -56,6 +56,11 @@
|
|
|
56
56
|
"type": "string",
|
|
57
57
|
"default": ""
|
|
58
58
|
},
|
|
59
|
+
"locale": {
|
|
60
|
+
"description": "BCP-47 locale tag for `type=\"number\"`, e.g. `de-DE`, `fr-FR`, `en-IN`. When set, the input accepts both `.` AND the locale's decimal separator (e.g. `,` in de-DE), uses `Intl.NumberFormat` for display, and groups thousands on blur (e.g. en-US `1,234,567.89`, de-DE `1.234.567,89`). On focus, the input reverts to ungrouped form for easy editing. `.value` always stores the ungrouped, locale-decimal form so `Number(#toCanonical(v))` round-trips. Default empty = en-US-equivalent path (no behavior change).",
|
|
61
|
+
"type": "string",
|
|
62
|
+
"default": ""
|
|
63
|
+
},
|
|
59
64
|
"max": {
|
|
60
65
|
"description": "Maximum numeric value. Applies when `type=\"number\"`. Clamps + drives aria-valuemax + the [+] button's disabled state.",
|
|
61
66
|
"type": "number",
|
|
@@ -53,6 +53,13 @@ class UIInput extends UIFormElement {
|
|
|
53
53
|
max: { type: Number, default: null, reflect: true },
|
|
54
54
|
step: { type: Number, default: 1, reflect: true },
|
|
55
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 },
|
|
56
63
|
};
|
|
57
64
|
|
|
58
65
|
static template = () => null;
|
|
@@ -62,15 +69,30 @@ class UIInput extends UIFormElement {
|
|
|
62
69
|
#upBtn = null;
|
|
63
70
|
#downBtn = null;
|
|
64
71
|
#valueAtFocus = '';
|
|
72
|
+
#repeatTimer = null;
|
|
73
|
+
#repeatDelayTimer = null;
|
|
74
|
+
#cachedSep = '.';
|
|
75
|
+
#cachedGroup = '';
|
|
76
|
+
#cachedSepFor = null;
|
|
65
77
|
static #labelSeq = 0;
|
|
66
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
|
+
|
|
67
85
|
get #isNativePassword() { return this.type === 'password'; }
|
|
68
86
|
get #isNumberMode() { return this.type === 'number'; }
|
|
69
87
|
|
|
70
|
-
/** Parsed numeric value. NaN when empty or unparseable.
|
|
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(…)`. */
|
|
71
91
|
get valueAsNumber() {
|
|
72
|
-
const
|
|
73
|
-
if (!
|
|
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;
|
|
74
96
|
const n = Number(s);
|
|
75
97
|
return Number.isFinite(n) ? n : NaN;
|
|
76
98
|
}
|
|
@@ -111,11 +133,15 @@ class UIInput extends UIFormElement {
|
|
|
111
133
|
}
|
|
112
134
|
|
|
113
135
|
// pointerdown.preventDefault keeps focus on the contenteditable surface
|
|
114
|
-
// when the user pokes a stepper button with a pointing device.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
this.#
|
|
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.
|
|
119
145
|
|
|
120
146
|
// In non-Vite static deploys, the icon registry loads asynchronously
|
|
121
147
|
// after the manifest fetch resolves. If our prefix/suffix were checked
|
|
@@ -273,9 +299,12 @@ class UIInput extends UIFormElement {
|
|
|
273
299
|
}
|
|
274
300
|
|
|
275
301
|
#runNumberConstraints(val) {
|
|
276
|
-
const
|
|
302
|
+
const raw = String(val ?? '').trim();
|
|
277
303
|
// Empty is handled by `required` in the base class; nothing to check here.
|
|
278
|
-
if (!
|
|
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);
|
|
279
308
|
const n = Number(s);
|
|
280
309
|
if (!Number.isFinite(n)) {
|
|
281
310
|
this.internals.setValidity(
|
|
@@ -312,12 +341,87 @@ class UIInput extends UIFormElement {
|
|
|
312
341
|
return (stepStr.split('.')[1] || '').length;
|
|
313
342
|
}
|
|
314
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). */
|
|
315
393
|
#format(n) {
|
|
316
394
|
if (!Number.isFinite(n)) return '';
|
|
317
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
|
+
}
|
|
318
405
|
return d > 0 ? n.toFixed(d) : String(Math.round(n));
|
|
319
406
|
}
|
|
320
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
|
+
|
|
321
425
|
/** Display value derived from the stored string. During focus we leave
|
|
322
426
|
* the user's raw text alone; otherwise reformat (e.g. "9.9" → "9.90"
|
|
323
427
|
* for precision=2). Non-numeric stored strings pass through unchanged
|
|
@@ -325,9 +429,16 @@ class UIInput extends UIFormElement {
|
|
|
325
429
|
#formatStored(stored) {
|
|
326
430
|
const s = String(stored ?? '');
|
|
327
431
|
if (!s) return '';
|
|
328
|
-
|
|
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));
|
|
329
435
|
if (!Number.isFinite(n)) return s;
|
|
330
|
-
|
|
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);
|
|
331
442
|
}
|
|
332
443
|
|
|
333
444
|
#snap(raw) {
|
|
@@ -404,23 +515,40 @@ class UIInput extends UIFormElement {
|
|
|
404
515
|
#isNumericProspect(s) {
|
|
405
516
|
// Permissive while typing: allow lone '-', lone '.', and trailing '.'.
|
|
406
517
|
// Reject scientific notation, multiple decimals, multiple signs.
|
|
407
|
-
|
|
408
|
-
|
|
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;
|
|
409
524
|
}
|
|
410
|
-
if (!/^-?\d*\.?\d*$/.test(
|
|
411
|
-
if (
|
|
525
|
+
if (!/^-?\d*\.?\d*$/.test(c)) return false;
|
|
526
|
+
if (c.startsWith('-') && this.min != null && this.min >= 0) return false;
|
|
412
527
|
return true;
|
|
413
528
|
}
|
|
414
529
|
|
|
415
530
|
#sanitizeNumeric(s) {
|
|
416
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();
|
|
417
544
|
let out = '';
|
|
418
|
-
let
|
|
545
|
+
let sawDecimal = false;
|
|
419
546
|
for (let i = 0; i < s.length; i++) {
|
|
420
547
|
const c = s[i];
|
|
421
548
|
if (c >= '0' && c <= '9') out += c;
|
|
422
549
|
else if (c === '-' && out === '' && (this.min == null || this.min < 0)) out += c;
|
|
423
|
-
else if (c ===
|
|
550
|
+
else if (c === sep && !sawDecimal) { out += sep; sawDecimal = true; }
|
|
551
|
+
// group separator and other punctuation silently dropped
|
|
424
552
|
}
|
|
425
553
|
return out;
|
|
426
554
|
}
|
|
@@ -487,6 +615,18 @@ class UIInput extends UIFormElement {
|
|
|
487
615
|
|
|
488
616
|
#onFocus = () => {
|
|
489
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
|
+
}
|
|
490
630
|
};
|
|
491
631
|
|
|
492
632
|
#onBlur = () => {
|
|
@@ -497,18 +637,24 @@ class UIInput extends UIFormElement {
|
|
|
497
637
|
#commitOnBlur() {
|
|
498
638
|
const raw = String(this.value ?? '').trim();
|
|
499
639
|
if (!raw) return;
|
|
500
|
-
|
|
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));
|
|
501
643
|
if (!Number.isFinite(n)) return; // leave the bad input visible for the error UX
|
|
502
644
|
const snapped = this.#snap(n);
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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);
|
|
507
653
|
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
508
654
|
}
|
|
509
|
-
if (this.#textEl.textContent !==
|
|
510
|
-
this.#textEl.textContent =
|
|
511
|
-
this.#textEl.toggleAttribute('data-empty', !
|
|
655
|
+
if (this.#textEl.textContent !== displayed) {
|
|
656
|
+
this.#textEl.textContent = displayed;
|
|
657
|
+
this.#textEl.toggleAttribute('data-empty', !displayed);
|
|
512
658
|
}
|
|
513
659
|
}
|
|
514
660
|
|
|
@@ -530,13 +676,50 @@ class UIInput extends UIFormElement {
|
|
|
530
676
|
document.execCommand('insertText', false, text);
|
|
531
677
|
};
|
|
532
678
|
|
|
533
|
-
|
|
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) {
|
|
534
688
|
// Keep focus on the editable surface when the button is pressed.
|
|
535
689
|
e.preventDefault();
|
|
536
|
-
|
|
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
|
+
}
|
|
537
708
|
|
|
538
|
-
#
|
|
539
|
-
|
|
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
|
+
}
|
|
540
723
|
|
|
541
724
|
focus() { this.#textEl?.focus(); }
|
|
542
725
|
|
|
@@ -563,10 +746,12 @@ class UIInput extends UIFormElement {
|
|
|
563
746
|
this.#textEl.removeEventListener('paste', this.#onPaste);
|
|
564
747
|
this.#textEl.removeEventListener('beforeinput', this.#onBeforeInput);
|
|
565
748
|
}
|
|
566
|
-
this.#upBtn?.removeEventListener('pointerdown', this.#
|
|
567
|
-
this.#downBtn?.removeEventListener('pointerdown', this.#
|
|
568
|
-
|
|
569
|
-
this
|
|
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();
|
|
570
755
|
this.#textEl = null;
|
|
571
756
|
this.#labelEl = null;
|
|
572
757
|
this.#upBtn = null;
|
|
@@ -576,3 +761,4 @@ class UIInput extends UIFormElement {
|
|
|
576
761
|
customElements.define('input-ui', UIInput);
|
|
577
762
|
|
|
578
763
|
export { UIInput };
|
|
764
|
+
|
|
@@ -84,6 +84,15 @@ props:
|
|
|
84
84
|
decimal-count from `step` — e.g. `step=1 precision=2` formats "10.00".
|
|
85
85
|
type: number
|
|
86
86
|
default: null
|
|
87
|
+
locale:
|
|
88
|
+
description: BCP-47 locale tag for `type="number"`, e.g. `de-DE`, `fr-FR`, `en-IN`. When set,
|
|
89
|
+
the input accepts both `.` AND the locale's decimal separator (e.g. `,` in de-DE), uses
|
|
90
|
+
`Intl.NumberFormat` for display, and groups thousands on blur (e.g. en-US `1,234,567.89`,
|
|
91
|
+
de-DE `1.234.567,89`). On focus, the input reverts to ungrouped form for easy editing.
|
|
92
|
+
`.value` always stores the ungrouped, locale-decimal form so `Number(#toCanonical(v))`
|
|
93
|
+
round-trips. Default empty = en-US-equivalent path (no behavior change).
|
|
94
|
+
type: string
|
|
95
|
+
default: ""
|
|
87
96
|
pattern:
|
|
88
97
|
description: Regex pattern for validation. Tested as ^(?:pattern)$ against the value.
|
|
89
98
|
type: string
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adia-ai/web-components",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "AdiaUI web components — vanilla custom elements. A2UI runtime (renderer, registry, streams, wiring) lives in @adia-ai/a2ui-runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|