@adia-ai/web-components 0.6.36 → 0.6.37

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 (115) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/components/badge/badge.a2ui.json +10 -0
  3. package/components/badge/badge.css +70 -0
  4. package/components/badge/badge.yaml +20 -0
  5. package/components/blockquote/blockquote.a2ui.json +121 -0
  6. package/components/blockquote/blockquote.class.js +68 -0
  7. package/components/blockquote/blockquote.css +46 -0
  8. package/components/blockquote/blockquote.d.ts +31 -0
  9. package/components/blockquote/blockquote.js +17 -0
  10. package/components/blockquote/blockquote.yaml +124 -0
  11. package/components/button/button.css +11 -3
  12. package/components/calendar-picker/calendar-picker.a2ui.json +15 -0
  13. package/components/calendar-picker/calendar-picker.class.js +7 -1
  14. package/components/calendar-picker/calendar-picker.yaml +14 -0
  15. package/components/color-input/color-input.a2ui.json +2 -2
  16. package/components/color-input/color-input.class.js +9 -2
  17. package/components/color-input/color-input.yaml +2 -2
  18. package/components/combobox/combobox.class.js +4 -0
  19. package/components/context-menu/context-menu.a2ui.json +159 -0
  20. package/components/context-menu/context-menu.class.js +275 -0
  21. package/components/context-menu/context-menu.css +56 -0
  22. package/components/context-menu/context-menu.d.ts +70 -0
  23. package/components/context-menu/context-menu.js +17 -0
  24. package/components/context-menu/context-menu.yaml +136 -0
  25. package/components/date-range-picker/date-range-picker.a2ui.json +15 -0
  26. package/components/date-range-picker/date-range-picker.class.js +2 -0
  27. package/components/date-range-picker/date-range-picker.yaml +14 -0
  28. package/components/datetime-picker/datetime-picker.a2ui.json +15 -0
  29. package/components/datetime-picker/datetime-picker.class.js +3 -1
  30. package/components/datetime-picker/datetime-picker.d.ts +2 -0
  31. package/components/datetime-picker/datetime-picker.yaml +14 -0
  32. package/components/empty-state/empty-state.class.js +2 -0
  33. package/components/feed/feed.class.js +13 -5
  34. package/components/feed/feed.css +14 -0
  35. package/components/index.js +9 -0
  36. package/components/integration-card/integration-card.class.js +9 -0
  37. package/components/integration-card/integration-card.test.js +4 -3
  38. package/components/nav-group/nav-group.css +7 -1
  39. package/components/number-format/number-format.a2ui.json +180 -0
  40. package/components/number-format/number-format.class.js +96 -0
  41. package/components/number-format/number-format.css +18 -0
  42. package/components/number-format/number-format.d.ts +68 -0
  43. package/components/number-format/number-format.js +17 -0
  44. package/components/number-format/number-format.yaml +204 -0
  45. package/components/pagination/pagination.a2ui.json +19 -2
  46. package/components/pagination/pagination.class.js +90 -37
  47. package/components/pagination/pagination.css +32 -127
  48. package/components/pagination/pagination.d.ts +8 -2
  49. package/components/pagination/pagination.test.js +195 -0
  50. package/components/pagination/pagination.yaml +22 -1
  51. package/components/password-strength/password-strength.a2ui.json +152 -0
  52. package/components/password-strength/password-strength.class.js +157 -0
  53. package/components/password-strength/password-strength.css +80 -0
  54. package/components/password-strength/password-strength.d.ts +59 -0
  55. package/components/password-strength/password-strength.js +17 -0
  56. package/components/password-strength/password-strength.yaml +153 -0
  57. package/components/popover/popover.css +43 -23
  58. package/components/popover/popover.yaml +8 -4
  59. package/components/qr-code/QR-TEST.svg +4 -0
  60. package/components/qr-code/qr-code.a2ui.json +154 -0
  61. package/components/qr-code/qr-code.class.js +129 -0
  62. package/components/qr-code/qr-code.css +41 -0
  63. package/components/qr-code/qr-code.d.ts +83 -0
  64. package/components/qr-code/qr-code.js +17 -0
  65. package/components/qr-code/qr-code.yaml +203 -0
  66. package/components/qr-code/qr-encoder.js +633 -0
  67. package/components/relative-time/relative-time.a2ui.json +120 -0
  68. package/components/relative-time/relative-time.class.js +136 -0
  69. package/components/relative-time/relative-time.css +22 -0
  70. package/components/relative-time/relative-time.d.ts +51 -0
  71. package/components/relative-time/relative-time.js +17 -0
  72. package/components/relative-time/relative-time.yaml +133 -0
  73. package/components/segmented/segmented.class.js +5 -1
  74. package/components/select/select.class.js +4 -0
  75. package/components/skip-nav/skip-nav.a2ui.json +92 -0
  76. package/components/skip-nav/skip-nav.class.js +45 -0
  77. package/components/skip-nav/skip-nav.css +54 -0
  78. package/components/skip-nav/skip-nav.d.ts +27 -0
  79. package/components/skip-nav/skip-nav.js +12 -0
  80. package/components/skip-nav/skip-nav.yaml +68 -0
  81. package/components/slider/slider.a2ui.json +16 -1
  82. package/components/slider/slider.class.js +264 -122
  83. package/components/slider/slider.css +82 -2
  84. package/components/slider/slider.d.ts +19 -3
  85. package/components/slider/slider.test.js +55 -0
  86. package/components/slider/slider.yaml +28 -6
  87. package/components/table/table.class.js +29 -6
  88. package/components/table/table.css +31 -4
  89. package/components/table-toolbar/table-toolbar.class.js +3 -1
  90. package/components/tag/tag.a2ui.json +3 -2
  91. package/components/tag/tag.css +35 -11
  92. package/components/tag/tag.d.ts +14 -0
  93. package/components/tag/tag.test.js +35 -11
  94. package/components/tag/tag.yaml +13 -7
  95. package/components/toast/toast.class.js +12 -4
  96. package/components/toc/toc.a2ui.json +159 -0
  97. package/components/toc/toc.class.js +222 -0
  98. package/components/toc/toc.css +92 -0
  99. package/components/toc/toc.d.ts +61 -0
  100. package/components/toc/toc.js +17 -0
  101. package/components/toc/toc.yaml +180 -0
  102. package/components/toolbar/toolbar.class.js +3 -0
  103. package/components/visually-hidden/visually-hidden.a2ui.json +71 -0
  104. package/components/visually-hidden/visually-hidden.class.js +14 -0
  105. package/components/visually-hidden/visually-hidden.css +25 -0
  106. package/components/visually-hidden/visually-hidden.d.ts +26 -0
  107. package/components/visually-hidden/visually-hidden.js +12 -0
  108. package/components/visually-hidden/visually-hidden.yaml +54 -0
  109. package/core/anchor.js +19 -3
  110. package/dist/web-components.min.css +1 -1
  111. package/dist/web-components.min.js +100 -89
  112. package/package.json +1 -1
  113. package/styles/colors/semantics.css +11 -2
  114. package/styles/components.css +9 -0
  115. package/styles/resets.css +10 -0
@@ -1,42 +1,24 @@
1
1
  /* ═══════════════════════════════════════════════════════════════
2
- PAGINATION-N — Page navigation with numbers and prev/next.
2
+ PAGINATION-UI — Page navigation with prev/next + numbered cells.
3
+ Composes <button-ui> for every interactive cell, so chrome, size,
4
+ focus-ring, hover, disabled, and active (primary fill) all come
5
+ from button-ui's token chain. Pagination owns only:
6
+ - the nav-row layout (flex + gap)
7
+ - the ellipsis cell (a plain <span>, not interactive)
8
+ - one shape mode (`variant="button"` switches button-ui's
9
+ composed variant to `outline` for 1×1 bordered cells)
3
10
  ═══════════════════════════════════════════════════════════════ */
4
11
 
5
12
  @scope (pagination-ui) {
6
13
  :where(:scope) {
7
14
  /* ── Layout ── */
8
15
  --pagination-gap-default: var(--a-space-1);
9
- --pagination-button-size-default: var(--a-size-sm);
10
- --pagination-button-px-default: var(--a-space-1);
11
- --pagination-radius-default: var(--a-radius-sm);
12
16
 
13
- /* ── Typography ── */
14
- --pagination-font-default: var(--a-ui-size);
15
-
16
- /* ── Colors ── */
17
- --pagination-fg-default: var(--a-fg-subtle);
18
- --pagination-fg-hover-default: var(--a-fg);
19
- --pagination-fg-active-default: var(--a-chrome-light);
20
- --pagination-fg-muted-default: var(--a-fg-muted);
21
- --pagination-bg-hover-default: var(--a-bg-muted);
22
- --pagination-bg-active-default: var(--a-accent);
23
- --pagination-fg-disabled-default: var(--a-ui-text-disabled);
24
-
25
- /* ── Transition ── */
26
- --pagination-duration-default: var(--a-duration-fast);
27
- --pagination-easing-default: var(--a-easing);
28
-
29
- /* ── State ── */
30
- --pagination-focus-ring-default: var(--a-focus-ring);
31
-
32
- /* ── Nav (button variant chrome) ── */
33
- --pagination-nav-bg-default: transparent;
34
- --pagination-nav-border-default: transparent;
35
- --pagination-nav-border-hover-default: transparent;
36
- --pagination-nav-bg-disabled-default: transparent;
37
- --pagination-nav-border-disabled-default: transparent;
17
+ /* ── Ellipsis cell (the only piece pagination styles directly) ── */
18
+ --pagination-ellipsis-fg-default: var(--a-fg-muted);
19
+ --pagination-ellipsis-font-default: var(--a-ui-size);
38
20
  text-align: start; /* §text-align-reset — blocks inheritance from centered ancestors */
39
- }
21
+ }
40
22
 
41
23
  :scope {
42
24
  /* ── Base ── */
@@ -51,115 +33,38 @@
51
33
  gap: var(--pagination-gap, var(--pagination-gap-default));
52
34
  }
53
35
 
54
- /* ── All buttons ── */
55
- [slot="nav"] button {
56
- box-sizing: border-box;
57
- display: inline-flex;
58
- align-items: center;
59
- justify-content: center;
60
- min-width: var(--pagination-button-size, var(--pagination-button-size-default));
61
- height: var(--pagination-button-size, var(--pagination-button-size-default));
62
- padding: 0 var(--pagination-button-px, var(--pagination-button-px-default));
63
- border: none;
64
- background: none;
65
- border-radius: var(--pagination-radius, var(--pagination-radius-default));
66
- font: inherit;
67
- font-size: var(--pagination-font, var(--pagination-font-default));
68
- color: var(--pagination-fg, var(--pagination-fg-default));
69
- cursor: pointer;
70
- user-select: none;
71
- line-height: 1;
72
- transition:
73
- background var(--pagination-duration, var(--pagination-duration-default)) var(--pagination-easing, var(--pagination-easing-default)),
74
- border-color var(--pagination-duration, var(--pagination-duration-default)) var(--pagination-easing, var(--pagination-easing-default)),
75
- color var(--pagination-duration, var(--pagination-duration-default)) var(--pagination-easing, var(--pagination-easing-default)),
76
- box-shadow var(--pagination-duration, var(--pagination-duration-default)) var(--pagination-easing, var(--pagination-easing-default));
77
- }
78
-
79
- [slot="nav"] button:not([disabled]):hover {
80
- background: var(--pagination-bg-hover, var(--pagination-bg-hover-default));
81
- color: var(--pagination-fg-hover, var(--pagination-fg-hover-default));
82
- }
83
-
84
- /* ── Active page ── */
85
- [slot="nav"] button[data-active] {
86
- background: var(--pagination-bg-active, var(--pagination-bg-active-default));
87
- color: var(--pagination-fg-active, var(--pagination-fg-active-default));
88
- }
89
-
90
- [slot="nav"] button[data-active]:hover {
91
- background: var(--pagination-bg-active, var(--pagination-bg-active-default));
92
- color: var(--pagination-fg-active, var(--pagination-fg-active-default));
36
+ /* ── Nested button-ui sizing handoff ──
37
+ button-ui resolves height + min-width from `--a-size` (= 24/30/36 px
38
+ at sm/md/lg per the universal size system). We set --a-icon-size for
39
+ prev/next so the caret reads at typographic optical-size pairing
40
+ with the page-number labels (0.875em ≈ 14px at 16px base). */
41
+ [slot="nav"] button-ui {
42
+ --a-icon-size: 0.875em;
93
43
  }
94
44
 
95
- /* ── Disabled (prev/next at boundaries) ── */
96
- [slot="nav"] button[disabled] {
97
- color: var(--pagination-fg-disabled, var(--pagination-fg-disabled-default));
98
- cursor: not-allowed;
99
- pointer-events: none;
100
- }
101
-
102
- /* ── Ellipsis ── */
45
+ /* ── Ellipsis (plain text span — not interactive) ── */
103
46
  [slot="nav"] [data-ellipsis] {
104
47
  box-sizing: border-box;
105
48
  display: inline-flex;
106
49
  align-items: center;
107
50
  justify-content: center;
108
- min-width: var(--pagination-button-size, var(--pagination-button-size-default));
109
- height: var(--pagination-button-size, var(--pagination-button-size-default));
110
- color: var(--pagination-fg-muted, var(--pagination-fg-muted-default));
111
- font-size: var(--pagination-font, var(--pagination-font-default));
51
+ /* Match the height of the nested button-ui so the row baseline aligns */
52
+ min-width: var(--a-size);
53
+ height: var(--a-size);
54
+ color: var(--pagination-ellipsis-fg, var(--pagination-ellipsis-fg-default));
55
+ font-size: var(--pagination-ellipsis-font, var(--pagination-ellipsis-font-default));
112
56
  pointer-events: none;
113
57
  user-select: none;
114
58
  line-height: 1;
115
59
  }
116
60
 
117
- /* ── Focus visible ── */
118
- [slot="nav"] button:focus-visible {
119
- outline: none;
120
- box-shadow: var(--pagination-focus-ring, var(--pagination-focus-ring-default));
121
- }
122
-
123
- /* ═══════════════════════════════════════════════════════════
124
- VARIANT: button — square 1:1 aspect-ratio page buttons
125
- ═══════════════════════════════════════════════════════════ */
126
-
127
- :scope[variant="button"] {
128
- --pagination-nav-bg-default: var(--a-bg);
129
- --pagination-nav-border-default: var(--a-border-subtle);
130
- --pagination-nav-border-hover-default: var(--a-border);
131
- --pagination-nav-bg-disabled-default: var(--a-bg);
132
- --pagination-nav-border-disabled-default: var(--a-border-subtle);
133
- }
134
-
135
- :scope[variant="button"] [slot="nav"] button {
136
- width: var(--pagination-button-size, var(--pagination-button-size-default));
137
- min-width: var(--pagination-button-size, var(--pagination-button-size-default));
138
- height: var(--pagination-button-size, var(--pagination-button-size-default));
139
- padding: 0;
61
+ /* ── variant="button" square 1:1 cells via outline button-ui ──
62
+ The class JS swaps each non-active button-ui from `variant="ghost"`
63
+ to `variant="outline"` in this mode. CSS only nudges the inactive
64
+ buttons to be square (button-ui's default min-width = height, but
65
+ ghost-mode buttons can collapse below that — outline buttons keep
66
+ aspect-ratio 1 here for the bordered-cell look). */
67
+ :scope[variant="button"] [slot="nav"] button-ui {
140
68
  aspect-ratio: 1;
141
- border: 1px solid var(--pagination-nav-border, var(--pagination-nav-border-default));
142
- border-radius: var(--pagination-radius, var(--pagination-radius-default));
143
- background: var(--pagination-nav-bg, var(--pagination-nav-bg-default));
144
- }
145
-
146
- :scope[variant="button"] [slot="nav"] button:not([disabled]):hover {
147
- border-color: var(--pagination-nav-border-hover, var(--pagination-nav-border-hover-default));
148
- background: var(--pagination-bg-hover, var(--pagination-bg-hover-default));
149
- }
150
-
151
- :scope[variant="button"] [slot="nav"] button[data-active] {
152
- border-color: transparent;
153
- background: var(--pagination-bg-active, var(--pagination-bg-active-default));
154
- color: var(--pagination-fg-active, var(--pagination-fg-active-default));
155
- }
156
-
157
- :scope[variant="button"] [slot="nav"] button[data-active]:hover {
158
- background: var(--pagination-bg-active, var(--pagination-bg-active-default));
159
- }
160
-
161
- :scope[variant="button"] [slot="nav"] button[disabled] {
162
- border-color: var(--pagination-nav-border-disabled, var(--pagination-nav-border-disabled-default));
163
- background: var(--pagination-nav-bg-disabled, var(--pagination-nav-bg-disabled-default));
164
69
  }
165
70
  }
@@ -24,10 +24,16 @@ export class UIPagination extends UIElement {
24
24
  page: number;
25
25
  /** Number of page buttons to show on each side of the current page. */
26
26
  siblings: number;
27
+ /** Universal size — threads through to every nested `<button-ui size=…>`
28
+ so pagination honors the substrate's 24/30/36 px size system
29
+ (with [density] modifier). Default `md` matches `<button-ui>`'s
30
+ default; pass `size="sm"` for a denser numbered row.
31
+ */
32
+ size: 'sm' | 'md' | 'lg';
27
33
  /** Total number of pages. */
28
34
  total: number;
29
- /** Visual variant */
30
- variant: string;
35
+ /** Visual variant — `default` (ghost buttons w/ hover bg) or `button` (1×1 bordered cells; active page filled). */
36
+ variant: 'default' | 'button';
31
37
 
32
38
  addEventListener<K extends keyof HTMLElementEventMap>(
33
39
  type: K,
@@ -0,0 +1,195 @@
1
+ /**
2
+ * pagination-ui — regression guards for the v0.6.36 native-primitive
3
+ * leak fix and the universal [size] system threading.
4
+ *
5
+ * Before the fix, every page / prev / next item was a raw <button>,
6
+ * and pagination hardcoded `--a-size-sm` for its button cells (no
7
+ * [size] prop, no threading to the substrate's 24/30/36 px size
8
+ * system). After the fix, every item is a <button-ui> whose `size`
9
+ * attribute mirrors the host's, and active items render as
10
+ * `variant="primary"` so the filled-accent state comes from
11
+ * button-ui's primary surface matrix.
12
+ */
13
+
14
+ import { describe, it, expect, beforeEach } from 'vitest';
15
+ import { readFileSync } from 'node:fs';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { dirname, resolve } from 'node:path';
18
+ import '../../core/element.js';
19
+ import './pagination.js';
20
+ import '../button/button.js';
21
+
22
+ const HERE = dirname(fileURLToPath(import.meta.url));
23
+ const CLASS_JS = readFileSync(resolve(HERE, 'pagination.class.js'), 'utf8');
24
+
25
+ const tick = () => new Promise((r) => queueMicrotask(r));
26
+
27
+ function mount(html) {
28
+ const wrap = document.createElement('div');
29
+ wrap.innerHTML = html;
30
+ document.body.appendChild(wrap);
31
+ return wrap.firstElementChild;
32
+ }
33
+
34
+ beforeEach(() => { document.body.innerHTML = ''; });
35
+
36
+ // ── Source-grep contract guards ─────────────────────────────────────
37
+
38
+ describe('pagination-ui — no native <button> leak', () => {
39
+ it('class.js creates <button-ui> elements (not raw <button>)', () => {
40
+ expect(CLASS_JS).toMatch(/createElement\(['"]button-ui['"]\)/);
41
+ // Negative — guard against accidental revert to raw <button>.
42
+ expect(CLASS_JS).not.toMatch(/createElement\(['"]button['"]\)/);
43
+ });
44
+
45
+ it('declares caret-left + caret-right in static requiredIcons', () => {
46
+ expect(CLASS_JS).toMatch(/static\s+requiredIcons\s*=\s*\[[^\]]*['"]caret-left['"]/);
47
+ expect(CLASS_JS).toMatch(/static\s+requiredIcons\s*=\s*\[[^\]]*['"]caret-right['"]/);
48
+ });
49
+
50
+ it('declares size prop in static properties (universal [size] thread-through)', () => {
51
+ expect(CLASS_JS).toMatch(/static\s+properties\s*=[\s\S]*?size:\s*\{\s*type:\s*String/);
52
+ });
53
+ });
54
+
55
+ // ── DOM contract guards ─────────────────────────────────────────────
56
+
57
+ describe('pagination-ui — DOM composition', () => {
58
+ it('stamps <button-ui> for every page / prev / next cell', async () => {
59
+ const el = mount('<pagination-ui page="3" total="10"></pagination-ui>');
60
+ await tick();
61
+ const buttons = el.querySelectorAll('button-ui');
62
+ expect(buttons.length).toBeGreaterThan(0);
63
+ // No raw <button> children — confirms the native-leak is closed at runtime.
64
+ expect(el.querySelectorAll(':scope button:not(button-ui)').length).toBe(0);
65
+ });
66
+
67
+ it('threads size attribute through to every nested button-ui', async () => {
68
+ const el = mount('<pagination-ui page="2" total="5" size="lg"></pagination-ui>');
69
+ await tick();
70
+ const buttons = el.querySelectorAll('button-ui');
71
+ expect(buttons.length).toBeGreaterThan(0);
72
+ for (const btn of buttons) {
73
+ expect(btn.getAttribute('size')).toBe('lg');
74
+ }
75
+ });
76
+
77
+ it('defaults to size="md" (aligned with button-ui default)', async () => {
78
+ const el = mount('<pagination-ui page="2" total="5"></pagination-ui>');
79
+ await tick();
80
+ const buttons = el.querySelectorAll('button-ui');
81
+ expect(buttons.length).toBeGreaterThan(0);
82
+ for (const btn of buttons) {
83
+ expect(btn.getAttribute('size')).toBe('md');
84
+ }
85
+ });
86
+
87
+ it('renders the active page as variant="primary" (button-ui token chain)', async () => {
88
+ const el = mount('<pagination-ui page="3" total="10"></pagination-ui>');
89
+ await tick();
90
+ const active = el.querySelector('button-ui[data-active]');
91
+ expect(active).toBeTruthy();
92
+ expect(active.getAttribute('variant')).toBe('primary');
93
+ expect(active.getAttribute('aria-current')).toBe('page');
94
+ });
95
+
96
+ it('renders prev / next as ghost (default variant) or outline (button variant)', async () => {
97
+ const elDefault = mount('<pagination-ui page="3" total="10"></pagination-ui>');
98
+ await tick();
99
+ expect(elDefault.querySelector('button-ui[data-prev]').getAttribute('variant')).toBe('ghost');
100
+ expect(elDefault.querySelector('button-ui[data-next]').getAttribute('variant')).toBe('ghost');
101
+
102
+ const elButton = mount('<pagination-ui page="3" total="10" variant="button"></pagination-ui>');
103
+ await tick();
104
+ expect(elButton.querySelector('button-ui[data-prev]').getAttribute('variant')).toBe('outline');
105
+ expect(elButton.querySelector('button-ui[data-next]').getAttribute('variant')).toBe('outline');
106
+ });
107
+
108
+ it('uses icon="caret-left" / "caret-right" on prev / next (not glyph text)', async () => {
109
+ const el = mount('<pagination-ui page="3" total="10"></pagination-ui>');
110
+ await tick();
111
+ expect(el.querySelector('button-ui[data-prev]').getAttribute('icon')).toBe('caret-left');
112
+ expect(el.querySelector('button-ui[data-next]').getAttribute('icon')).toBe('caret-right');
113
+ });
114
+ });
115
+
116
+ // ── Width invariance: compact-mode cell count is constant across pages ──
117
+ //
118
+ // Compact width W = 2*siblings + 5 (= 7 at siblings=1). When the current
119
+ // page advances by one, the row's visible cell count must NOT change —
120
+ // otherwise the layout wobbles. For total ≤ W, we show every page and
121
+ // skip compact mode entirely (no point compressing if it doesn't save
122
+ // slots — also wobble-free since every page just highlights a different
123
+ // constant-width row).
124
+
125
+ describe('pagination-ui — compact-mode width invariance', () => {
126
+ function pageValues(el) {
127
+ return [...el.querySelectorAll('button-ui[data-page]')].map((b) => Number(b.dataset.value));
128
+ }
129
+ function cellCount(el) {
130
+ return el.querySelectorAll('button-ui[data-page], [data-ellipsis]').length;
131
+ }
132
+ function hasEllipsis(el) {
133
+ return el.querySelectorAll('[data-ellipsis]').length > 0;
134
+ }
135
+
136
+ it('total=5 siblings=1 → never compacts (shows 1-5 every page)', async () => {
137
+ for (const page of [1, 2, 3, 4, 5]) {
138
+ const el = mount(`<pagination-ui page="${page}" total="5" siblings="1"></pagination-ui>`);
139
+ await tick();
140
+ expect(pageValues(el)).toEqual([1, 2, 3, 4, 5]);
141
+ expect(hasEllipsis(el)).toBe(false);
142
+ }
143
+ });
144
+
145
+ it('total=10 siblings=1 → exactly 7 cells on every page (no wobble)', async () => {
146
+ for (const page of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) {
147
+ const el = mount(`<pagination-ui page="${page}" total="10" siblings="1"></pagination-ui>`);
148
+ await tick();
149
+ expect(cellCount(el)).toBe(7);
150
+ }
151
+ });
152
+
153
+ it('total=10 page=3 siblings=1 → near-start: [1,2,3,4,5,…,10]', async () => {
154
+ const el = mount('<pagination-ui page="3" total="10" siblings="1"></pagination-ui>');
155
+ await tick();
156
+ expect(pageValues(el)).toEqual([1, 2, 3, 4, 5, 10]);
157
+ expect(el.querySelectorAll('[data-ellipsis]').length).toBe(1);
158
+ });
159
+
160
+ it('total=10 page=4 siblings=1 → still near-start: [1,2,3,4,5,…,10] (same as page=3)', async () => {
161
+ const el = mount('<pagination-ui page="4" total="10" siblings="1"></pagination-ui>');
162
+ await tick();
163
+ expect(pageValues(el)).toEqual([1, 2, 3, 4, 5, 10]);
164
+ expect(el.querySelectorAll('[data-ellipsis]').length).toBe(1);
165
+ });
166
+
167
+ it('total=10 page=5 siblings=1 → middle: [1,…,4,5,6,…,10]', async () => {
168
+ const el = mount('<pagination-ui page="5" total="10" siblings="1"></pagination-ui>');
169
+ await tick();
170
+ expect(pageValues(el)).toEqual([1, 4, 5, 6, 10]);
171
+ expect(el.querySelectorAll('[data-ellipsis]').length).toBe(2);
172
+ });
173
+
174
+ it('total=10 page=7 siblings=1 → near-end: [1,…,6,7,8,9,10]', async () => {
175
+ const el = mount('<pagination-ui page="7" total="10" siblings="1"></pagination-ui>');
176
+ await tick();
177
+ expect(pageValues(el)).toEqual([1, 6, 7, 8, 9, 10]);
178
+ expect(el.querySelectorAll('[data-ellipsis]').length).toBe(1);
179
+ });
180
+
181
+ it('total=20 page=10 siblings=1 → middle: [1,…,9,10,11,…,20]', async () => {
182
+ const el = mount('<pagination-ui page="10" total="20" siblings="1"></pagination-ui>');
183
+ await tick();
184
+ expect(pageValues(el)).toEqual([1, 9, 10, 11, 20]);
185
+ expect(el.querySelectorAll('[data-ellipsis]').length).toBe(2);
186
+ });
187
+
188
+ it('total=20 siblings=2 → exactly 9 cells on every page', async () => {
189
+ for (const page of [1, 5, 10, 15, 20]) {
190
+ const el = mount(`<pagination-ui page="${page}" total="20" siblings="2"></pagination-ui>`);
191
+ await tick();
192
+ expect(cellCount(el)).toBe(9);
193
+ }
194
+ });
195
+ });
@@ -13,6 +13,11 @@ description: >-
13
13
  range is automatically truncated for high page counts. Use below tables,
14
14
  card grids, or list views; for cursor-based pagination use a custom
15
15
  load-more <button-ui> instead.
16
+ # Per ADR-0027 — primitives that programmatically create other primitives
17
+ # do NOT auto-import them. Consumer (or demo shell) must explicitly import.
18
+ composes:
19
+ - button-ui # every page / prev / next slot is a <button-ui> stamp
20
+ - icon-ui # caret-left / caret-right inside prev/next via button-ui[icon=…]
16
21
  props:
17
22
  page:
18
23
  description: Current active page number.
@@ -27,9 +32,25 @@ props:
27
32
  type: number
28
33
  default: 1
29
34
  variant:
30
- description: Visual variant
35
+ description: Visual variant — `default` (ghost buttons w/ hover bg) or `button` (1×1 bordered cells; active page filled).
31
36
  type: string
32
37
  default: default
38
+ enum:
39
+ - default
40
+ - button
41
+ size:
42
+ description: |
43
+ Universal size — threads through to every nested `<button-ui size=…>`
44
+ so pagination honors the substrate's 24/30/36 px size system
45
+ (with [density] modifier). Default `md` matches `<button-ui>`'s
46
+ default; pass `size="sm"` for a denser numbered row.
47
+ type: string
48
+ default: md
49
+ reflect: true
50
+ enum:
51
+ - sm
52
+ - md
53
+ - lg
33
54
  events:
34
55
  page-change:
35
56
  description: Fired when a page button is clicked. detail contains { page }.
@@ -0,0 +1,152 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://adiaui.dev/a2ui/v0_9/components/PasswordStrength.json",
4
+ "title": "PasswordStrength",
5
+ "description": "Visual strength indicator for password inputs — 4-segment bar (weak /\nfair / good / strong) computed from a heuristic combining length,\ncharacter-class diversity, and repeat-pattern penalty. Pairs with\n`<input-ui type=\"password\">` via JS:\n input.addEventListener('input', e => meter.value = e.target.value)\nRead-only display primitive — no form participation. Emits a\n`score-change` event when the bucket changes so consumers can gate\na submit button on `detail.satisfied` (score ≥ min-score).\n\n**Security note:** [value] is held as a JS property only, NOT\nreflected to a DOM attribute. The element stamps the bar + label\nfrom the property; the password never appears in the rendered HTML.\nDo not set the value via setAttribute (it will be no-op).\n",
6
+ "type": "object",
7
+ "allOf": [
8
+ {
9
+ "$ref": "common_types.json#/$defs/ComponentCommon"
10
+ },
11
+ {
12
+ "$ref": "common_types.json#/$defs/CatalogComponentCommon"
13
+ }
14
+ ],
15
+ "properties": {
16
+ "component": {
17
+ "const": "PasswordStrength"
18
+ },
19
+ "minScore": {
20
+ "description": "Minimum acceptable score (0–3). The `score-change` event's\n`detail.satisfied` boolean reflects whether the current score\nmeets this threshold. Useful for gating a submit button.\n",
21
+ "type": "number",
22
+ "default": 2
23
+ },
24
+ "showLabel": {
25
+ "description": "Display the textual score label (\"Weak\" / \"Fair\" / \"Good\" /\n\"Strong\") below the bar. Defaults to true.\n",
26
+ "type": "boolean",
27
+ "default": true
28
+ },
29
+ "value": {
30
+ "description": "The password string to score. JS-property only — NOT reflected\nto a DOM attribute (so the password never leaks into rendered\nHTML). Wire to an input via `input.addEventListener('input', e =>\nmeter.value = e.target.value)`.\n",
31
+ "$ref": "common_types.json#/$defs/DynamicString"
32
+ }
33
+ },
34
+ "required": [
35
+ "component"
36
+ ],
37
+ "unevaluatedProperties": false,
38
+ "x-adiaui": {
39
+ "anti_patterns": [
40
+ {
41
+ "fix": "<password-strength-ui id=\"m\"></password-strength-ui>\n<script>document.getElementById('m').value = pwd;</script>\n",
42
+ "why": "`value` is JS-property only; the attribute is ignored. The\npassword also shouldn't be authored into HTML at all (server\ntemplate / static page) — that defeats the purpose.\n",
43
+ "wrong": "<password-strength-ui value=\"hunter2\"></password-strength-ui>\n"
44
+ }
45
+ ],
46
+ "category": "display",
47
+ "composes": [],
48
+ "events": {
49
+ "score-change": {
50
+ "description": "Fired when the computed score crosses a bucket boundary (not on every keystroke). Detail carries the new score + label + satisfied flag.",
51
+ "detail": {
52
+ "label": "string",
53
+ "satisfied": "boolean",
54
+ "score": "number"
55
+ }
56
+ }
57
+ },
58
+ "examples": [
59
+ {
60
+ "description": "Standard pairing with input-ui[type=password] via input listener.",
61
+ "a2ui": "[\n {\n \"id\": \"field\",\n \"component\": \"Field\",\n \"label\": \"Password\",\n \"children\": [\"pwd\", \"meter\"]\n },\n {\n \"id\": \"pwd\",\n \"component\": \"Input\",\n \"type\": \"password\",\n \"name\": \"password\"\n },\n {\n \"id\": \"meter\",\n \"component\": \"PasswordStrength\"\n }\n]\n",
62
+ "name": "paired-with-input"
63
+ }
64
+ ],
65
+ "keywords": [
66
+ "password-strength",
67
+ "strength-meter",
68
+ "password",
69
+ "strength",
70
+ "meter",
71
+ "security"
72
+ ],
73
+ "name": "UIPasswordStrength",
74
+ "related": [
75
+ "input",
76
+ "field",
77
+ "progress"
78
+ ],
79
+ "slots": {
80
+ "default": {
81
+ "description": "Optional area for requirements checklist content (e.g. <ul> of \"at least 8 characters\", \"mixed case\", etc.)."
82
+ }
83
+ },
84
+ "states": [
85
+ {
86
+ "description": "Default — no value set, all segments grey.",
87
+ "name": "idle"
88
+ },
89
+ {
90
+ "description": "Value present; segments lit per score 0..3.",
91
+ "attribute": "data-score",
92
+ "name": "scored"
93
+ }
94
+ ],
95
+ "status": "stable",
96
+ "synonyms": {
97
+ "password": [
98
+ "password-strength",
99
+ "strength-meter"
100
+ ],
101
+ "strength": [
102
+ "password-strength",
103
+ "meter"
104
+ ]
105
+ },
106
+ "tag": "password-strength-ui",
107
+ "tokens": {
108
+ "--password-strength-color-fair": {
109
+ "description": "Color for score=1 lit segments.",
110
+ "default": "var(--a-warning-bg)"
111
+ },
112
+ "--password-strength-color-good": {
113
+ "description": "Color for score=2 lit segments.",
114
+ "default": "var(--a-info-bg)"
115
+ },
116
+ "--password-strength-color-strong": {
117
+ "description": "Color for score=3 lit segments.",
118
+ "default": "var(--a-success-bg)"
119
+ },
120
+ "--password-strength-color-weak": {
121
+ "description": "Color for score=0 lit segments.",
122
+ "default": "var(--a-danger-bg)"
123
+ },
124
+ "--password-strength-gap": {
125
+ "description": "Gap between segments.",
126
+ "default": "var(--a-space-1)"
127
+ },
128
+ "--password-strength-label-fg": {
129
+ "description": "Label text color.",
130
+ "default": "var(--a-fg-muted)"
131
+ },
132
+ "--password-strength-label-size": {
133
+ "description": "Label font size.",
134
+ "default": "var(--a-ui-sm)"
135
+ },
136
+ "--password-strength-radius": {
137
+ "description": "Segment border radius.",
138
+ "default": "var(--a-radius-full)"
139
+ },
140
+ "--password-strength-segment-bg": {
141
+ "description": "Unlit segment background (track color).",
142
+ "default": "var(--a-canvas-1-scrim)"
143
+ },
144
+ "--password-strength-segment-height": {
145
+ "description": "Height of each bar segment.",
146
+ "default": "4px"
147
+ }
148
+ },
149
+ "traits": [],
150
+ "version": 1
151
+ }
152
+ }