@adia-ai/web-components 0.6.35 → 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.
- package/CHANGELOG.md +56 -0
- package/components/badge/badge.a2ui.json +10 -0
- package/components/badge/badge.css +70 -0
- package/components/badge/badge.yaml +20 -0
- package/components/blockquote/blockquote.a2ui.json +121 -0
- package/components/blockquote/blockquote.class.js +68 -0
- package/components/blockquote/blockquote.css +46 -0
- package/components/blockquote/blockquote.d.ts +31 -0
- package/components/blockquote/blockquote.js +17 -0
- package/components/blockquote/blockquote.yaml +124 -0
- package/components/button/button.css +11 -3
- package/components/calendar-picker/calendar-picker.a2ui.json +15 -0
- package/components/calendar-picker/calendar-picker.class.js +7 -1
- package/components/calendar-picker/calendar-picker.yaml +14 -0
- package/components/color-input/color-input.a2ui.json +2 -2
- package/components/color-input/color-input.class.js +9 -2
- package/components/color-input/color-input.yaml +2 -2
- package/components/combobox/combobox.class.js +4 -0
- package/components/combobox/combobox.css +12 -0
- package/components/context-menu/context-menu.a2ui.json +159 -0
- package/components/context-menu/context-menu.class.js +275 -0
- package/components/context-menu/context-menu.css +56 -0
- package/components/context-menu/context-menu.d.ts +70 -0
- package/components/context-menu/context-menu.js +17 -0
- package/components/context-menu/context-menu.yaml +136 -0
- package/components/date-range-picker/date-range-picker.a2ui.json +15 -0
- package/components/date-range-picker/date-range-picker.class.js +3 -1
- package/components/date-range-picker/date-range-picker.css +4 -1
- package/components/date-range-picker/date-range-picker.yaml +14 -0
- package/components/datetime-picker/datetime-picker.a2ui.json +15 -0
- package/components/datetime-picker/datetime-picker.class.js +3 -1
- package/components/datetime-picker/datetime-picker.css +7 -1
- package/components/datetime-picker/datetime-picker.d.ts +2 -0
- package/components/datetime-picker/datetime-picker.yaml +14 -0
- package/components/empty-state/empty-state.class.js +2 -0
- package/components/feed/feed.class.js +13 -5
- package/components/feed/feed.css +14 -0
- package/components/index.js +9 -0
- package/components/input/input.css +15 -1
- package/components/input/input.test.js +40 -0
- package/components/integration-card/integration-card.class.js +9 -0
- package/components/integration-card/integration-card.test.js +4 -3
- package/components/nav-group/nav-group.css +7 -1
- package/components/number-format/number-format.a2ui.json +180 -0
- package/components/number-format/number-format.class.js +96 -0
- package/components/number-format/number-format.css +18 -0
- package/components/number-format/number-format.d.ts +68 -0
- package/components/number-format/number-format.js +17 -0
- package/components/number-format/number-format.yaml +204 -0
- package/components/pagination/pagination.a2ui.json +19 -2
- package/components/pagination/pagination.class.js +90 -37
- package/components/pagination/pagination.css +32 -127
- package/components/pagination/pagination.d.ts +8 -2
- package/components/pagination/pagination.test.js +195 -0
- package/components/pagination/pagination.yaml +22 -1
- package/components/password-strength/password-strength.a2ui.json +152 -0
- package/components/password-strength/password-strength.class.js +157 -0
- package/components/password-strength/password-strength.css +80 -0
- package/components/password-strength/password-strength.d.ts +59 -0
- package/components/password-strength/password-strength.js +17 -0
- package/components/password-strength/password-strength.yaml +153 -0
- package/components/popover/popover.css +43 -23
- package/components/popover/popover.yaml +8 -4
- package/components/qr-code/QR-TEST.svg +4 -0
- package/components/qr-code/qr-code.a2ui.json +154 -0
- package/components/qr-code/qr-code.class.js +129 -0
- package/components/qr-code/qr-code.css +41 -0
- package/components/qr-code/qr-code.d.ts +83 -0
- package/components/qr-code/qr-code.js +17 -0
- package/components/qr-code/qr-code.yaml +203 -0
- package/components/qr-code/qr-encoder.js +633 -0
- package/components/relative-time/relative-time.a2ui.json +120 -0
- package/components/relative-time/relative-time.class.js +136 -0
- package/components/relative-time/relative-time.css +22 -0
- package/components/relative-time/relative-time.d.ts +51 -0
- package/components/relative-time/relative-time.js +17 -0
- package/components/relative-time/relative-time.yaml +133 -0
- package/components/search/search.class.js +2 -0
- package/components/segmented/segmented.class.js +5 -1
- package/components/select/select.class.js +4 -0
- package/components/skip-nav/skip-nav.a2ui.json +92 -0
- package/components/skip-nav/skip-nav.class.js +45 -0
- package/components/skip-nav/skip-nav.css +54 -0
- package/components/skip-nav/skip-nav.d.ts +27 -0
- package/components/skip-nav/skip-nav.js +12 -0
- package/components/skip-nav/skip-nav.yaml +68 -0
- package/components/slider/slider.a2ui.json +16 -1
- package/components/slider/slider.class.js +264 -122
- package/components/slider/slider.css +82 -2
- package/components/slider/slider.d.ts +19 -3
- package/components/slider/slider.test.js +55 -0
- package/components/slider/slider.yaml +28 -6
- package/components/table/table.class.js +29 -6
- package/components/table/table.css +31 -4
- package/components/table-toolbar/table-toolbar.class.js +4 -1
- package/components/tag/tag.a2ui.json +10 -0
- package/components/tag/tag.class.js +8 -1
- package/components/tag/tag.css +108 -20
- package/components/tag/tag.d.ts +14 -0
- package/components/tag/tag.test.js +99 -1
- package/components/tag/tag.yaml +20 -0
- package/components/tags-input/tags-input.class.js +10 -3
- package/components/tags-input/tags-input.css +12 -3
- package/components/textarea/textarea.css +10 -1
- package/components/toast/toast.class.js +12 -4
- package/components/toc/toc.a2ui.json +159 -0
- package/components/toc/toc.class.js +222 -0
- package/components/toc/toc.css +92 -0
- package/components/toc/toc.d.ts +61 -0
- package/components/toc/toc.js +17 -0
- package/components/toc/toc.yaml +180 -0
- package/components/toolbar/toolbar.class.js +3 -0
- package/components/visually-hidden/visually-hidden.a2ui.json +71 -0
- package/components/visually-hidden/visually-hidden.class.js +14 -0
- package/components/visually-hidden/visually-hidden.css +25 -0
- package/components/visually-hidden/visually-hidden.d.ts +26 -0
- package/components/visually-hidden/visually-hidden.js +12 -0
- package/components/visually-hidden/visually-hidden.yaml +54 -0
- package/core/anchor.js +19 -3
- package/core/provider.js +19 -2
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +101 -89
- package/package.json +1 -1
- package/styles/colors/semantics.css +11 -2
- package/styles/components.css +9 -0
- package/styles/resets.css +10 -0
|
@@ -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
|
-
|
|
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('
|
|
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
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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('
|
|
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.
|
|
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.
|
|
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
|
-
#
|
|
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
|
|
|
@@ -1,42 +1,24 @@
|
|
|
1
1
|
/* ═══════════════════════════════════════════════════════════════
|
|
2
|
-
PAGINATION-
|
|
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
|
-
/* ──
|
|
14
|
-
--pagination-
|
|
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
|
-
/* ──
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
/* ──
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
/* ──
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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:
|
|
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 }.
|