@adia-ai/web-components 0.6.36 → 0.6.38

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 (159) hide show
  1. package/CHANGELOG.md +48 -1
  2. package/components/accordion/accordion-item.a2ui.json +3 -0
  3. package/components/accordion/accordion-item.yaml +5 -0
  4. package/components/action-list/action-item.a2ui.json +5 -1
  5. package/components/action-list/action-item.yaml +7 -0
  6. package/components/badge/badge.a2ui.json +10 -0
  7. package/components/badge/badge.css +70 -0
  8. package/components/badge/badge.yaml +20 -0
  9. package/components/blockquote/blockquote.a2ui.json +121 -0
  10. package/components/blockquote/blockquote.class.js +68 -0
  11. package/components/blockquote/blockquote.css +46 -0
  12. package/components/blockquote/blockquote.d.ts +31 -0
  13. package/components/blockquote/blockquote.js +17 -0
  14. package/components/blockquote/blockquote.yaml +124 -0
  15. package/components/button/button.css +11 -3
  16. package/components/calendar-picker/calendar-picker.a2ui.json +15 -0
  17. package/components/calendar-picker/calendar-picker.class.js +7 -1
  18. package/components/calendar-picker/calendar-picker.yaml +14 -0
  19. package/components/card/card.a2ui.json +17 -1
  20. package/components/card/card.yaml +24 -1
  21. package/components/color-input/color-input.a2ui.json +2 -2
  22. package/components/color-input/color-input.class.js +9 -2
  23. package/components/color-input/color-input.yaml +2 -2
  24. package/components/combobox/combobox.class.js +4 -0
  25. package/components/context-menu/context-menu.a2ui.json +159 -0
  26. package/components/context-menu/context-menu.class.js +275 -0
  27. package/components/context-menu/context-menu.css +56 -0
  28. package/components/context-menu/context-menu.d.ts +70 -0
  29. package/components/context-menu/context-menu.js +17 -0
  30. package/components/context-menu/context-menu.yaml +136 -0
  31. package/components/date-range-picker/date-range-picker.a2ui.json +15 -0
  32. package/components/date-range-picker/date-range-picker.class.js +2 -0
  33. package/components/date-range-picker/date-range-picker.yaml +14 -0
  34. package/components/datetime-picker/datetime-picker.a2ui.json +15 -0
  35. package/components/datetime-picker/datetime-picker.class.js +3 -1
  36. package/components/datetime-picker/datetime-picker.d.ts +2 -0
  37. package/components/datetime-picker/datetime-picker.yaml +14 -0
  38. package/components/empty-state/empty-state.a2ui.json +9 -0
  39. package/components/empty-state/empty-state.class.js +2 -0
  40. package/components/empty-state/empty-state.yaml +15 -0
  41. package/components/feed/feed-item.a2ui.json +5 -0
  42. package/components/feed/feed-item.yaml +10 -0
  43. package/components/feed/feed.class.js +13 -5
  44. package/components/feed/feed.css +14 -0
  45. package/components/field/field.a2ui.json +6 -0
  46. package/components/field/field.yaml +10 -0
  47. package/components/index.js +11 -0
  48. package/components/inline-edit/inline-edit.a2ui.json +159 -0
  49. package/components/inline-edit/inline-edit.class.js +184 -0
  50. package/components/inline-edit/inline-edit.css +62 -0
  51. package/components/inline-edit/inline-edit.d.ts +52 -0
  52. package/components/inline-edit/inline-edit.js +12 -0
  53. package/components/inline-edit/inline-edit.yaml +125 -0
  54. package/components/integration-card/integration-card.class.js +9 -0
  55. package/components/integration-card/integration-card.test.js +4 -3
  56. package/components/list/list-item.a2ui.json +8 -1
  57. package/components/list/list-item.yaml +12 -0
  58. package/components/list/list.css +36 -6
  59. package/components/mark/mark.a2ui.json +109 -0
  60. package/components/mark/mark.class.js +22 -0
  61. package/components/mark/mark.css +39 -0
  62. package/components/mark/mark.d.ts +27 -0
  63. package/components/mark/mark.js +12 -0
  64. package/components/mark/mark.yaml +87 -0
  65. package/components/modal/modal.a2ui.json +9 -0
  66. package/components/modal/modal.yaml +14 -0
  67. package/components/nav-group/nav-group.a2ui.json +3 -0
  68. package/components/nav-group/nav-group.css +7 -1
  69. package/components/nav-group/nav-group.yaml +5 -0
  70. package/components/nav-item/nav-item.a2ui.json +3 -0
  71. package/components/nav-item/nav-item.yaml +5 -0
  72. package/components/number-format/number-format.a2ui.json +180 -0
  73. package/components/number-format/number-format.class.js +96 -0
  74. package/components/number-format/number-format.css +18 -0
  75. package/components/number-format/number-format.d.ts +68 -0
  76. package/components/number-format/number-format.js +17 -0
  77. package/components/number-format/number-format.yaml +204 -0
  78. package/components/pagination/pagination.a2ui.json +19 -2
  79. package/components/pagination/pagination.class.js +90 -37
  80. package/components/pagination/pagination.css +32 -127
  81. package/components/pagination/pagination.d.ts +8 -2
  82. package/components/pagination/pagination.test.js +195 -0
  83. package/components/pagination/pagination.yaml +22 -1
  84. package/components/password-strength/password-strength.a2ui.json +152 -0
  85. package/components/password-strength/password-strength.class.js +157 -0
  86. package/components/password-strength/password-strength.css +80 -0
  87. package/components/password-strength/password-strength.d.ts +59 -0
  88. package/components/password-strength/password-strength.js +17 -0
  89. package/components/password-strength/password-strength.yaml +153 -0
  90. package/components/popover/popover.css +43 -23
  91. package/components/popover/popover.yaml +8 -4
  92. package/components/qr-code/QR-TEST.svg +4 -0
  93. package/components/qr-code/qr-code.a2ui.json +154 -0
  94. package/components/qr-code/qr-code.class.js +129 -0
  95. package/components/qr-code/qr-code.css +41 -0
  96. package/components/qr-code/qr-code.d.ts +83 -0
  97. package/components/qr-code/qr-code.js +17 -0
  98. package/components/qr-code/qr-code.yaml +203 -0
  99. package/components/qr-code/qr-encoder.js +633 -0
  100. package/components/relative-time/relative-time.a2ui.json +120 -0
  101. package/components/relative-time/relative-time.class.js +136 -0
  102. package/components/relative-time/relative-time.css +22 -0
  103. package/components/relative-time/relative-time.d.ts +51 -0
  104. package/components/relative-time/relative-time.js +17 -0
  105. package/components/relative-time/relative-time.yaml +133 -0
  106. package/components/segmented/segmented.class.js +15 -3
  107. package/components/select/select.a2ui.json +3 -0
  108. package/components/select/select.class.js +4 -0
  109. package/components/select/select.yaml +5 -0
  110. package/components/skip-nav/skip-nav.a2ui.json +92 -0
  111. package/components/skip-nav/skip-nav.class.js +45 -0
  112. package/components/skip-nav/skip-nav.css +54 -0
  113. package/components/skip-nav/skip-nav.d.ts +27 -0
  114. package/components/skip-nav/skip-nav.js +12 -0
  115. package/components/skip-nav/skip-nav.yaml +68 -0
  116. package/components/slider/slider.a2ui.json +22 -1
  117. package/components/slider/slider.class.js +264 -122
  118. package/components/slider/slider.css +82 -2
  119. package/components/slider/slider.d.ts +19 -3
  120. package/components/slider/slider.test.js +55 -0
  121. package/components/slider/slider.yaml +38 -6
  122. package/components/stat/stat.css +18 -14
  123. package/components/stepper/stepper-item.a2ui.json +3 -0
  124. package/components/stepper/stepper-item.yaml +5 -0
  125. package/components/table/table.class.js +29 -6
  126. package/components/table/table.css +31 -4
  127. package/components/table-toolbar/table-toolbar.class.js +3 -1
  128. package/components/tag/tag.a2ui.json +3 -2
  129. package/components/tag/tag.css +35 -11
  130. package/components/tag/tag.d.ts +14 -0
  131. package/components/tag/tag.test.js +35 -11
  132. package/components/tag/tag.yaml +13 -7
  133. package/components/timeline/timeline-item.a2ui.json +8 -1
  134. package/components/timeline/timeline-item.yaml +12 -0
  135. package/components/toast/toast.class.js +12 -4
  136. package/components/toc/toc.a2ui.json +159 -0
  137. package/components/toc/toc.class.js +222 -0
  138. package/components/toc/toc.css +92 -0
  139. package/components/toc/toc.d.ts +61 -0
  140. package/components/toc/toc.js +17 -0
  141. package/components/toc/toc.yaml +180 -0
  142. package/components/toolbar/toolbar.class.js +3 -0
  143. package/components/tree/tree-item.a2ui.json +5 -1
  144. package/components/tree/tree-item.yaml +7 -0
  145. package/components/tree/tree.a2ui.json +3 -0
  146. package/components/tree/tree.yaml +5 -0
  147. package/components/visually-hidden/visually-hidden.a2ui.json +71 -0
  148. package/components/visually-hidden/visually-hidden.class.js +14 -0
  149. package/components/visually-hidden/visually-hidden.css +25 -0
  150. package/components/visually-hidden/visually-hidden.d.ts +26 -0
  151. package/components/visually-hidden/visually-hidden.js +12 -0
  152. package/components/visually-hidden/visually-hidden.yaml +54 -0
  153. package/core/anchor.js +19 -3
  154. package/dist/web-components.min.css +1 -1
  155. package/dist/web-components.min.js +100 -89
  156. package/package.json +1 -1
  157. package/styles/colors/semantics.css +11 -2
  158. package/styles/components.css +11 -0
  159. package/styles/resets.css +10 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Non-side-effect class export for `<number-format-ui>`.
3
+ *
4
+ * Importing this file gives you the class without auto-registering the
5
+ * tag. Useful for test isolation, subclassing with tag-name override,
6
+ * or selective composition.
7
+ *
8
+ * The auto-register path stays at `@adia-ai/web-components/components/number-format`
9
+ * (which imports this file + calls `defineIfFree()`).
10
+ *
11
+ * @see ../../USAGE.md#registration--auto-vs-explicit
12
+ */
13
+
14
+ /**
15
+ * <number-format-ui value="1234567.89"></number-format-ui> → 1,234,567.89
16
+ * <number-format-ui value="1299.99" number-style="currency" currency="USD"></number-format-ui> → $1,299.99
17
+ * <number-format-ui value="0.876" number-style="percent"></number-format-ui> → 87.6%
18
+ * <number-format-ui value="1234567" notation="compact"></number-format-ui> → 1.2M
19
+ *
20
+ * Read-only display primitive wrapping `Intl.NumberFormat`. Distinct
21
+ * from `<input-ui type="number">` (a form-input primitive).
22
+ */
23
+
24
+ import { UIElement } from '../../core/element.js';
25
+
26
+ export class UINumberFormat extends UIElement {
27
+ static properties = {
28
+ value: { type: Number, default: 0, reflect: true },
29
+ numberStyle: { type: String, default: 'decimal', reflect: true, attribute: 'number-style' },
30
+ currency: { type: String, default: '', reflect: true },
31
+ unit: { type: String, default: '', reflect: true },
32
+ notation: { type: String, default: 'standard', reflect: true },
33
+ compactDisplay: { type: String, default: 'short', reflect: true, attribute: 'compact-display' },
34
+ minimumFractionDigits: { type: Number, default: 0, reflect: true, attribute: 'minimum-fraction-digits' },
35
+ maximumFractionDigits: { type: Number, default: 2, reflect: true, attribute: 'maximum-fraction-digits' },
36
+ locale: { type: String, default: '', reflect: true },
37
+ signDisplay: { type: String, default: 'auto', reflect: true, attribute: 'sign-display' },
38
+ };
39
+
40
+ static template = () => null;
41
+
42
+ #resolveLocale() {
43
+ if (this.locale) return this.locale;
44
+ return this.ownerDocument?.documentElement?.lang || undefined;
45
+ }
46
+
47
+ #format() {
48
+ const v = Number(this.value);
49
+ if (!Number.isFinite(v)) return '';
50
+
51
+ // Validate required dependent props per style.
52
+ if (this.numberStyle === 'currency' && !this.currency) return '';
53
+ if (this.numberStyle === 'unit' && !this.unit) return '';
54
+
55
+ /** @type {Intl.NumberFormatOptions} */
56
+ const opts = {
57
+ style: this.numberStyle,
58
+ notation: this.notation,
59
+ signDisplay: this.signDisplay,
60
+ };
61
+ if (this.numberStyle === 'currency') opts.currency = this.currency;
62
+ if (this.numberStyle === 'unit') opts.unit = this.unit;
63
+ if (this.notation === 'compact') opts.compactDisplay = this.compactDisplay;
64
+ // Honor min/max fraction digits when explicitly set. The defaults
65
+ // (0 / 2) are reasonable for decimal+currency; compact notation
66
+ // typically wants 1 max but we let consumers override.
67
+ if (Number.isFinite(this.minimumFractionDigits)) opts.minimumFractionDigits = this.minimumFractionDigits;
68
+ if (Number.isFinite(this.maximumFractionDigits)) opts.maximumFractionDigits = this.maximumFractionDigits;
69
+
70
+ try {
71
+ return new Intl.NumberFormat(this.#resolveLocale(), opts).format(v);
72
+ } catch {
73
+ return String(v);
74
+ }
75
+ }
76
+
77
+ connected() {
78
+ super.connected();
79
+ // No special role — the rendered text is read by AT naturally; we
80
+ // expose the raw value via aria-label for screen-reader clarity in
81
+ // contexts where the formatted glyphs ("€", "M") might be parsed
82
+ // unpredictably by different readers.
83
+ }
84
+
85
+ render() {
86
+ const text = this.#format();
87
+ this.textContent = text;
88
+ // Mirror raw value via aria-label so screen readers can read the
89
+ // underlying number alongside the formatted glyphs.
90
+ if (text && Number.isFinite(Number(this.value))) {
91
+ this.setAttribute('aria-label', `${text} (raw: ${this.value})`);
92
+ } else {
93
+ this.removeAttribute('aria-label');
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,18 @@
1
+ /* ═══════════════════════════════════════════════════════════════
2
+ NUMBER-FORMAT-UI — Read-only formatted numeric display.
3
+ ═══════════════════════════════════════════════════════════════ */
4
+
5
+ @scope (number-format-ui) {
6
+ :where(:scope) {
7
+ --number-format-fg-default: inherit;
8
+ --number-format-font-default: inherit;
9
+ }
10
+
11
+ :scope {
12
+ display: inline;
13
+ color: var(--number-format-fg, var(--number-format-fg-default));
14
+ font-family: var(--number-format-font, var(--number-format-font-default));
15
+ /* Tabular figures so column-aligned numeric displays line up cleanly. */
16
+ font-variant-numeric: tabular-nums;
17
+ }
18
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * `<number-format-ui>` — Display a numeric value with locale-aware formatting — currency,
3
+ percentage, compact (1.2K / 3.4M), unit, or plain decimal. Wraps
4
+ `Intl.NumberFormat`. Distinct from `<input-ui type="number">`
5
+ (an INPUT primitive); this is a DISPLAY primitive — read-only, no
6
+ form participation, no keyboard handling. Pair with `<stat-ui>` for
7
+ KPI surfaces or use standalone inline within prose.
8
+
9
+ *
10
+ * @see https://ui-kit.exe.xyz/site/components/number-format
11
+ *
12
+ * Type declarations generated by scripts/build/dts-codegen.mjs from
13
+ * the component's `.a2ui.json` sidecar(s). Edit the source `.yaml`,
14
+ * run `npm run build:components`, then `npm run codegen:dts` to
15
+ * regenerate; or hand-author this file fully if rich event types are
16
+ * needed beyond what the yaml `events:` block can express.
17
+ */
18
+
19
+ import { UIElement } from '../../core/element.js';
20
+
21
+ export class UINumberFormat extends UIElement {
22
+ /** When [notation="compact"], controls the compact-form length.
23
+ `short` = "1.2M" (default); `long` = "1.2 million".
24
+ */
25
+ compactDisplay: 'short' | 'long';
26
+ /** ISO 4217 currency code (e.g. "USD", "EUR", "JPY"). Required when
27
+ [number-style="currency"]; ignored otherwise.
28
+ */
29
+ currency: string;
30
+ /** BCP-47 locale tag for the formatter. Empty defaults to the
31
+ document locale (`<html lang>`) then to browser default.
32
+ */
33
+ locale: string;
34
+ /** Maximum fractional digits (0–20). Default `2` for decimal/
35
+ currency/percent, `1` for compact notation.
36
+ */
37
+ maximumFractionDigits: number;
38
+ /** Minimum fractional digits (0–20). Padded with trailing zeros.
39
+ Useful for currency display to force ".00" suffix.
40
+ */
41
+ minimumFractionDigits: number;
42
+ /** `Intl.NumberFormat` `notation` option. `standard` is the
43
+ thousands-grouped form (1,234,567); `compact` is the abbreviated
44
+ form (1.2M); `scientific` and `engineering` are the exponent
45
+ forms. Defaults to `standard`.
46
+ */
47
+ notation: 'standard' | 'compact' | 'scientific' | 'engineering';
48
+ /** `Intl.NumberFormat` `style` option. `decimal` (default) renders a
49
+ plain number with locale-aware grouping. `currency` requires
50
+ [currency] to be set. `percent` formats 0–1 as 0%–100%. `unit`
51
+ requires [unit] to be set (e.g. "kilobyte", "celsius").
52
+ */
53
+ numberStyle: 'decimal' | 'currency' | 'percent' | 'unit';
54
+ /** `Intl.NumberFormat` `signDisplay` option. `auto` (default) shows
55
+ "−" for negatives only; `always` shows "+" / "−"; `exceptZero`
56
+ shows sign for non-zero only; `never` hides signs.
57
+ */
58
+ signDisplay: 'auto' | 'always' | 'exceptZero' | 'never';
59
+ /** `Intl.NumberFormat` unit identifier (e.g. "kilometer-per-hour",
60
+ "celsius", "byte"). Required when [number-style="unit"]; ignored
61
+ otherwise. See MDN's NumberFormat docs for the valid set.
62
+ */
63
+ unit: string;
64
+ /** The numeric value to format. Empty string or non-numeric value
65
+ renders nothing.
66
+ */
67
+ value: number;
68
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `<number-format-ui>` — auto-registers the tag on import.
3
+ *
4
+ * For non-side-effect class import (test isolation, tag override), use
5
+ * the `class` subpath:
6
+ *
7
+ * import { UINumberFormat } from '@adia-ai/web-components/components/number-format/class';
8
+ *
9
+ * @see ../../USAGE.md#registration--auto-vs-explicit
10
+ */
11
+
12
+ import { defineIfFree } from '../../core/register.js';
13
+ import { UINumberFormat } from './number-format.class.js';
14
+
15
+ defineIfFree('number-format-ui', UINumberFormat);
16
+
17
+ export { UINumberFormat };
@@ -0,0 +1,204 @@
1
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
2
+ name: UINumberFormat
3
+ tag: number-format-ui
4
+ status: stable
5
+ component: NumberFormat
6
+ category: display
7
+ version: 1
8
+ description: |
9
+ Display a numeric value with locale-aware formatting — currency,
10
+ percentage, compact (1.2K / 3.4M), unit, or plain decimal. Wraps
11
+ `Intl.NumberFormat`. Distinct from `<input-ui type="number">`
12
+ (an INPUT primitive); this is a DISPLAY primitive — read-only, no
13
+ form participation, no keyboard handling. Pair with `<stat-ui>` for
14
+ KPI surfaces or use standalone inline within prose.
15
+ props:
16
+ value:
17
+ description: |
18
+ The numeric value to format. Empty string or non-numeric value
19
+ renders nothing.
20
+ type: number
21
+ default: 0
22
+ reflect: true
23
+ numberStyle:
24
+ description: |
25
+ `Intl.NumberFormat` `style` option. `decimal` (default) renders a
26
+ plain number with locale-aware grouping. `currency` requires
27
+ [currency] to be set. `percent` formats 0–1 as 0%–100%. `unit`
28
+ requires [unit] to be set (e.g. "kilobyte", "celsius").
29
+ type: string
30
+ default: decimal
31
+ enum: [decimal, currency, percent, unit]
32
+ reflect: true
33
+ attribute: number-style
34
+ currency:
35
+ description: |
36
+ ISO 4217 currency code (e.g. "USD", "EUR", "JPY"). Required when
37
+ [number-style="currency"]; ignored otherwise.
38
+ type: string
39
+ default: ""
40
+ reflect: true
41
+ unit:
42
+ description: |
43
+ `Intl.NumberFormat` unit identifier (e.g. "kilometer-per-hour",
44
+ "celsius", "byte"). Required when [number-style="unit"]; ignored
45
+ otherwise. See MDN's NumberFormat docs for the valid set.
46
+ type: string
47
+ default: ""
48
+ reflect: true
49
+ notation:
50
+ description: |
51
+ `Intl.NumberFormat` `notation` option. `standard` is the
52
+ thousands-grouped form (1,234,567); `compact` is the abbreviated
53
+ form (1.2M); `scientific` and `engineering` are the exponent
54
+ forms. Defaults to `standard`.
55
+ type: string
56
+ default: standard
57
+ enum: [standard, compact, scientific, engineering]
58
+ reflect: true
59
+ compactDisplay:
60
+ description: |
61
+ When [notation="compact"], controls the compact-form length.
62
+ `short` = "1.2M" (default); `long` = "1.2 million".
63
+ type: string
64
+ default: short
65
+ enum: [short, long]
66
+ reflect: true
67
+ attribute: compact-display
68
+ minimumFractionDigits:
69
+ description: |
70
+ Minimum fractional digits (0–20). Padded with trailing zeros.
71
+ Useful for currency display to force ".00" suffix.
72
+ type: number
73
+ default: 0
74
+ reflect: true
75
+ attribute: minimum-fraction-digits
76
+ maximumFractionDigits:
77
+ description: |
78
+ Maximum fractional digits (0–20). Default `2` for decimal/
79
+ currency/percent, `1` for compact notation.
80
+ type: number
81
+ default: 2
82
+ reflect: true
83
+ attribute: maximum-fraction-digits
84
+ locale:
85
+ description: |
86
+ BCP-47 locale tag for the formatter. Empty defaults to the
87
+ document locale (`<html lang>`) then to browser default.
88
+ type: string
89
+ default: ""
90
+ reflect: true
91
+ signDisplay:
92
+ description: |
93
+ `Intl.NumberFormat` `signDisplay` option. `auto` (default) shows
94
+ "−" for negatives only; `always` shows "+" / "−"; `exceptZero`
95
+ shows sign for non-zero only; `never` hides signs.
96
+ type: string
97
+ default: auto
98
+ enum: [auto, always, exceptZero, never]
99
+ reflect: true
100
+ attribute: sign-display
101
+ events: {}
102
+ slots: {}
103
+ states:
104
+ - name: idle
105
+ description: Default, displaying the formatted value.
106
+ traits: []
107
+ tokens: {}
108
+ a2ui:
109
+ rules:
110
+ - rule: "Use for read-only numeric display with locale-aware formatting. For numeric INPUT use <input-ui type=number>; for KPI big-number display use <stat-ui>."
111
+ reason: "Display vs input vs metric separation."
112
+ - rule: "[number-style=currency] REQUIRES [currency] to be set to a valid ISO 4217 code. Without it the element renders nothing rather than producing malformed output."
113
+ reason: "Currency contract."
114
+ - rule: "[number-style=percent] treats the [value] as a fraction (0.5 → 50%). To display 50 as '50%' pass value=0.5 OR keep [number-style=decimal] and append '%' manually."
115
+ reason: "Percent semantics — Intl.NumberFormat convention."
116
+ - rule: "Compact notation auto-defaults maximumFractionDigits to 1. Override only when extra precision is needed (rare for compact display — defeats the purpose)."
117
+ reason: "Sensible defaults."
118
+ anti_patterns:
119
+ - wrong: |
120
+ <number-format-ui value="50" number-style="percent"></number-format-ui>
121
+ why: |
122
+ Renders "5,000%" — Intl.NumberFormat percent style multiplies the
123
+ input by 100. value=50 means "5000%".
124
+ fix: |
125
+ <number-format-ui value="0.5" number-style="percent"></number-format-ui>
126
+ - wrong: |
127
+ <number-format-ui value="9.99" number-style="currency"></number-format-ui>
128
+ why: |
129
+ Missing [currency] code. Renders nothing rather than producing
130
+ malformed currency output.
131
+ fix: |
132
+ <number-format-ui value="9.99" number-style="currency" currency="USD"></number-format-ui>
133
+ examples:
134
+ - name: decimal
135
+ description: Plain locale-grouped number — default style.
136
+ a2ui: |
137
+ [
138
+ {
139
+ "id": "n",
140
+ "component": "NumberFormat",
141
+ "value": 1234567.89
142
+ }
143
+ ]
144
+ - name: currency
145
+ description: USD currency display.
146
+ a2ui: |
147
+ [
148
+ {
149
+ "id": "n",
150
+ "component": "NumberFormat",
151
+ "value": 1299.99,
152
+ "numberStyle": "currency",
153
+ "currency": "USD"
154
+ }
155
+ ]
156
+ - name: percent
157
+ description: Fraction → percent (0.876 → 87.6%).
158
+ a2ui: |
159
+ [
160
+ {
161
+ "id": "n",
162
+ "component": "NumberFormat",
163
+ "value": 0.876,
164
+ "numberStyle": "percent",
165
+ "maximumFractionDigits": 1
166
+ }
167
+ ]
168
+ - name: compact
169
+ description: Compact-form (1.2M, 3.4K).
170
+ a2ui: |
171
+ [
172
+ {
173
+ "id": "n",
174
+ "component": "NumberFormat",
175
+ "value": 1234567,
176
+ "notation": "compact"
177
+ }
178
+ ]
179
+ keywords:
180
+ - number-format
181
+ - format
182
+ - currency
183
+ - percent
184
+ - percentage
185
+ - compact
186
+ - number
187
+ - locale
188
+ - intl
189
+ synonyms:
190
+ number:
191
+ - number-format
192
+ - format
193
+ currency:
194
+ - number-format
195
+ - money
196
+ - price
197
+ percent:
198
+ - number-format
199
+ - percentage
200
+ related:
201
+ - stat
202
+ - text
203
+ - badge
204
+ - input
@@ -26,14 +26,28 @@
26
26
  "type": "number",
27
27
  "default": 1
28
28
  },
29
+ "size": {
30
+ "description": "Universal size — threads through to every nested `<button-ui size=…>`\nso pagination honors the substrate's 24/30/36 px size system\n(with [density] modifier). Default `md` matches `<button-ui>`'s\ndefault; pass `size=\"sm\"` for a denser numbered row.\n",
31
+ "type": "string",
32
+ "enum": [
33
+ "sm",
34
+ "md",
35
+ "lg"
36
+ ],
37
+ "default": "md"
38
+ },
29
39
  "total": {
30
40
  "description": "Total number of pages.",
31
41
  "type": "number",
32
42
  "default": 1
33
43
  },
34
44
  "variant": {
35
- "description": "Visual variant",
45
+ "description": "Visual variant — `default` (ghost buttons w/ hover bg) or `button` (1×1 bordered cells; active page filled).",
36
46
  "type": "string",
47
+ "enum": [
48
+ "default",
49
+ "button"
50
+ ],
37
51
  "default": "default"
38
52
  }
39
53
  },
@@ -44,7 +58,10 @@
44
58
  "x-adiaui": {
45
59
  "anti_patterns": [],
46
60
  "category": "navigation",
47
- "composes": [],
61
+ "composes": [
62
+ "button-ui",
63
+ "icon-ui"
64
+ ],
48
65
  "events": {
49
66
  "page-change": {
50
67
  "description": "Fired when a page button is clicked. detail contains { page }.",
@@ -29,8 +29,18 @@ export class UIPagination extends UIElement {
29
29
  total: { type: Number, default: 1, reflect: true },
30
30
  siblings: { type: Number, default: 1, reflect: true },
31
31
  variant: { type: String, default: 'default', reflect: true },
32
+ // Threads through to every nested <button-ui size=…> so pagination
33
+ // honors the universal [size] system (sm=24 / md=30 / lg=36 px with
34
+ // density modifier). Default `md` matches <button-ui>'s default —
35
+ // pagination is a button-ui composite, so the canonical size is the
36
+ // same. Authors who want a denser numbered row pass [size="sm"].
37
+ size: { type: String, default: 'md', reflect: true },
32
38
  };
33
39
 
40
+ // Phosphor icons stamped by this primitive (prev/next chevrons inside
41
+ // the nested <button-ui>). Audited by check-required-icons.mjs.
42
+ static requiredIcons = ['caret-left', 'caret-right'];
43
+
34
44
  static template = () => null;
35
45
 
36
46
  #nav = null;
@@ -46,12 +56,15 @@ export class UIPagination extends UIElement {
46
56
 
47
57
  if (!this.#bound) {
48
58
  this.#bound = true;
49
- this.#nav.addEventListener('click', this.#onClick);
59
+ // `press` is the canonical button-ui event — fires only when not
60
+ // disabled (button-ui stops native click propagation on disabled
61
+ // state), so we get the right gating for free.
62
+ this.#nav.addEventListener('press', this.#onPress);
50
63
  }
51
64
  }
52
65
 
53
66
  disconnected() {
54
- this.#nav?.removeEventListener('click', this.#onClick);
67
+ this.#nav?.removeEventListener('press', this.#onPress);
55
68
  this.#nav = null;
56
69
  this.#bound = false;
57
70
  }
@@ -81,42 +94,70 @@ export class UIPagination extends UIElement {
81
94
 
82
95
  #buildRange(page, total, siblings) {
83
96
  const items = [];
84
-
85
- // Prev button
86
97
  items.push({ key: 'prev', type: 'prev', value: page - 1 });
87
98
 
88
- // Always show first
89
- items.push({ key: 'page-1', type: 'page', value: 1 });
90
-
91
- const start = Math.max(2, page - siblings);
92
- const end = Math.min(total - 1, page + siblings);
93
-
94
- // Left ellipsis
95
- if (start > 2) {
96
- items.push({ key: 'ellipsis-start', type: 'ellipsis' });
97
- }
98
-
99
- // Sibling pages
100
- for (let i = start; i <= end; i++) {
101
- items.push({ key: `page-${i}`, type: 'page', value: i });
102
- }
103
-
104
- // Right ellipsis
105
- if (end < total - 1) {
106
- items.push({ key: 'ellipsis-end', type: 'ellipsis' });
107
- }
108
-
109
- // Always show last (if more than 1 page)
110
- if (total > 1) {
111
- items.push({ key: `page-${total}`, type: 'page', value: total });
99
+ // W = constant compact-mode width (visible page cells, incl ellipses).
100
+ // = 2 bookends + (2*siblings + 1 sibling window) + 2 ellipsis slots
101
+ // siblings=1 → W=7; siblings=2 → W=9.
102
+ // Holding W invariant across page positions prevents the layout from
103
+ // jumping when the current page advances by one — fixes the wobble
104
+ // where page=3 showed 6 cells but page=4 showed 7.
105
+ const W = 2 * siblings + 5;
106
+
107
+ if (total <= W) {
108
+ // Small total — show every page, never ellipsis. No need to compact
109
+ // when compacting wouldn't save horizontal slots.
110
+ for (let i = 1; i <= total; i++) {
111
+ items.push({ key: `page-${i}`, type: 'page', value: i });
112
+ }
113
+ } else {
114
+ // Compact mode. Three layouts, each yielding exactly W cells so the
115
+ // row width stays constant under any page advance.
116
+ const nearStart = page <= siblings + 3;
117
+ const nearEnd = page >= total - siblings - 2;
118
+
119
+ if (nearStart) {
120
+ // 1..(W-2), ellipsis, total. The left window expands rightward to
121
+ // fill the slot a left ellipsis would have used.
122
+ const leftEnd = W - 2;
123
+ for (let i = 1; i <= leftEnd; i++) {
124
+ items.push({ key: `page-${i}`, type: 'page', value: i });
125
+ }
126
+ items.push({ key: 'ellipsis-end', type: 'ellipsis' });
127
+ items.push({ key: `page-${total}`, type: 'page', value: total });
128
+ } else if (nearEnd) {
129
+ // 1, ellipsis, (total-W+3)..total. Right window expands leftward.
130
+ items.push({ key: 'page-1', type: 'page', value: 1 });
131
+ items.push({ key: 'ellipsis-start', type: 'ellipsis' });
132
+ const rightStart = total - W + 3;
133
+ for (let i = rightStart; i <= total; i++) {
134
+ items.push({ key: `page-${i}`, type: 'page', value: i });
135
+ }
136
+ } else {
137
+ // Middle. 1, ellipsis, current±siblings, ellipsis, total.
138
+ items.push({ key: 'page-1', type: 'page', value: 1 });
139
+ items.push({ key: 'ellipsis-start', type: 'ellipsis' });
140
+ for (let i = page - siblings; i <= page + siblings; i++) {
141
+ items.push({ key: `page-${i}`, type: 'page', value: i });
142
+ }
143
+ items.push({ key: 'ellipsis-end', type: 'ellipsis' });
144
+ items.push({ key: `page-${total}`, type: 'page', value: total });
145
+ }
112
146
  }
113
147
 
114
- // Next button
115
148
  items.push({ key: 'next', type: 'next', value: page + 1 });
116
-
117
149
  return items;
118
150
  }
119
151
 
152
+ // The non-active button variant \u2014 `ghost` for default mode (chrome-less
153
+ // hover), `outline` for variant="button" mode (1\u00D71 bordered cells).
154
+ // Active items always use `primary` so the active-state token chain
155
+ // comes from button-ui's primary surface matrix (no re-implementing
156
+ // a separate accent fill at the pagination tier).
157
+ #restVariant() {
158
+ return this.variant === 'button' ? 'outline' : 'ghost';
159
+ }
160
+
120
161
  #createItem(item, page) {
121
162
  if (item.type === 'ellipsis') {
122
163
  const span = document.createElement('span');
@@ -125,21 +166,21 @@ export class UIPagination extends UIElement {
125
166
  return span;
126
167
  }
127
168
 
128
- const btn = document.createElement('button');
129
- btn.setAttribute('type', 'button');
169
+ const btn = document.createElement('button-ui');
170
+ btn.setAttribute('size', this.size);
130
171
 
131
172
  if (item.type === 'prev') {
132
173
  btn.setAttribute('data-prev', '');
174
+ btn.setAttribute('icon', 'caret-left');
133
175
  btn.setAttribute('aria-label', 'Previous page');
134
- btn.textContent = '\u2039';
135
176
  } else if (item.type === 'next') {
136
177
  btn.setAttribute('data-next', '');
178
+ btn.setAttribute('icon', 'caret-right');
137
179
  btn.setAttribute('aria-label', 'Next page');
138
- btn.textContent = '\u203A';
139
180
  } else {
140
181
  btn.setAttribute('data-page', '');
141
182
  btn.dataset.value = String(item.value);
142
- btn.textContent = String(item.value);
183
+ btn.setAttribute('text', String(item.value));
143
184
  btn.setAttribute('aria-label', `Page ${item.value}`);
144
185
  }
145
186
 
@@ -150,27 +191,39 @@ export class UIPagination extends UIElement {
150
191
  #updateItem(el, item, page) {
151
192
  if (item.type === 'ellipsis') return;
152
193
 
194
+ // Keep size in sync \u2014 the host's [size] may change between renders.
195
+ el.setAttribute('size', this.size);
196
+
153
197
  if (item.type === 'prev') {
198
+ el.setAttribute('variant', this.#restVariant());
154
199
  if (page <= 1) { el.setAttribute('disabled', ''); el.setAttribute('tabindex', '-1'); }
155
200
  else { el.removeAttribute('disabled'); el.setAttribute('tabindex', '0'); }
156
201
  } else if (item.type === 'next') {
202
+ el.setAttribute('variant', this.#restVariant());
157
203
  if (page >= this.total) { el.setAttribute('disabled', ''); el.setAttribute('tabindex', '-1'); }
158
204
  else { el.removeAttribute('disabled'); el.setAttribute('tabindex', '0'); }
159
205
  } else {
160
206
  el.dataset.value = String(item.value);
161
- el.textContent = String(item.value);
207
+ el.setAttribute('text', String(item.value));
162
208
  el.setAttribute('aria-label', `Page ${item.value}`);
163
209
  if (item.value === page) {
210
+ // Active page reads as `variant="primary"` so the filled-accent
211
+ // state comes from button-ui's primary surface matrix (the
212
+ // canonical token chain) \u2014 not a pagination-tier re-impl.
213
+ el.setAttribute('variant', 'primary');
164
214
  el.setAttribute('data-active', '');
165
215
  el.setAttribute('aria-current', 'page');
166
216
  } else {
217
+ el.setAttribute('variant', this.#restVariant());
167
218
  el.removeAttribute('data-active');
168
219
  el.removeAttribute('aria-current');
169
220
  }
170
221
  }
171
222
  }
172
223
 
173
- #onClick = (e) => {
224
+ #onPress = (e) => {
225
+ // `press` fires on the <button-ui> itself, which is exactly the
226
+ // element carrying the data-prev / data-next / data-page marker.
174
227
  const btn = e.target.closest('[data-prev], [data-next], [data-page]');
175
228
  if (!btn || btn.hasAttribute('disabled') || !this.#nav.contains(btn)) return;
176
229