@pienter/ui 0.2.0 → 0.5.0
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 +259 -0
- package/CONVENTIONS.md +297 -385
- package/README.md +3 -18
- package/components/feedback/toast/toast.css +1 -1
- package/components/form/combobox/Combobox.vue +21 -38
- package/components/form/combobox/combobox.css +1 -1
- package/components/form/form/Form.vue +1 -2
- package/components/form/label/label.css +1 -1
- package/components/form/number-field/NumberField.vue +1 -1
- package/components/form/number-field/number-field.css +1 -1
- package/components/form/radio-group/RadioGroup.vue +2 -5
- package/components/form/slider/slider.css +2 -3
- package/components/form/tags-input/tags-input.css +1 -2
- package/components/form/textarea/textarea.css +1 -1
- package/components/layout/accordion/Accordion.vue +6 -27
- package/components/layout/collapsible/Collapsible.vue +9 -19
- package/components/layout/table/table.css +3 -3
- package/components/navigation/pagination/Pagination.vue +1 -1
- package/components/navigation/pagination/PaginationFooter.vue +23 -0
- package/components/navigation/pagination/pagination.css +128 -29
- package/components/navigation/sidebar/Sidebar.vue +326 -84
- package/components/navigation/sidebar/SidebarMenuItem.vue +199 -0
- package/components/navigation/sidebar/context.ts +21 -0
- package/components/navigation/sidebar/sidebar.css +346 -78
- package/components/navigation/sidebar/types.ts +13 -1
- package/components/overlay/alert-dialog/AlertDialog.vue +10 -31
- package/components/overlay/command/Command.vue +10 -38
- package/components/overlay/command/command.css +1 -1
- package/components/overlay/dropdown-menu/DropdownMenu.vue +37 -62
- package/components/overlay/modal/Modal.vue +7 -28
- package/components/overlay/popover/Popover.vue +9 -31
- package/components/overlay/sheet/Sheet.vue +7 -28
- package/components/overlay/tooltip/Tooltip.vue +14 -19
- package/{utils/controllers/dialog.ts → composables/useDialog.ts} +90 -78
- package/composables/useDisclosure.ts +113 -0
- package/composables/useEventListener.ts +16 -0
- package/{utils/controllers/menu.ts → composables/useMenu.ts} +66 -102
- package/{utils/controllers/popover.ts → composables/usePopover.ts} +107 -120
- package/package.json +6 -38
- package/styles/0-settings/colors.css +8 -1
- package/styles/0-settings/layout.css +18 -0
- package/styles/0-settings/motion.css +2 -2
- package/styles/0-settings/spacing.css +3 -1
- package/utils/navigation/sidebar.ts +97 -0
- package/utils/validation/form.ts +6 -9
- package/components/action/button/Button.astro +0 -95
- package/components/action/button/IconButton.astro +0 -86
- package/components/display/avatar/Avatar.astro +0 -17
- package/components/display/avatar/AvatarStack.astro +0 -9
- package/components/display/badge/Badge.astro +0 -15
- package/components/display/empty/Empty.astro +0 -9
- package/components/display/icon/Icon.astro +0 -52
- package/components/feedback/alert/Alert.astro +0 -52
- package/components/feedback/progress/Progress.astro +0 -68
- package/components/feedback/skeleton/Skeleton.astro +0 -32
- package/components/feedback/spinner/Spinner.astro +0 -25
- package/components/feedback/toast/Toast.astro +0 -50
- package/components/form/checkbox/Checkbox.astro +0 -79
- package/components/form/date-input/DateInput.astro +0 -105
- package/components/form/form/Form.astro +0 -106
- package/components/form/input-otp/InputOTP.astro +0 -147
- package/components/form/label/Label.astro +0 -13
- package/components/form/number-field/NumberField.astro +0 -142
- package/components/form/radio-group/RadioGroup.astro +0 -105
- package/components/form/select/Select.astro +0 -105
- package/components/form/slider/Slider.astro +0 -205
- package/components/form/switch/Switch.astro +0 -75
- package/components/form/tags-input/TagsInput.astro +0 -153
- package/components/form/text-input/TextInput.astro +0 -84
- package/components/form/textarea/Textarea.astro +0 -86
- package/components/layout/card/Card.astro +0 -13
- package/components/layout/separator/Separator.astro +0 -31
- package/components/layout/table/Table.astro +0 -116
- package/components/navigation/breadcrumb/Breadcrumb.astro +0 -36
- package/components/navigation/navbar/Navbar.astro +0 -62
- package/components/navigation/sidebar/Sidebar.astro +0 -132
- package/components/navigation/sidebar/SidebarItemRender.astro +0 -83
- package/components/navigation/sidebar/SidebarItemRender.vue +0 -98
- package/components/overlay/alert-dialog/AlertDialog.astro +0 -112
- package/components/overlay/modal/Modal.astro +0 -66
- package/utils/controllers/disclosure.ts +0 -117
- package/utils/controllers/form.ts +0 -524
- package/utils/controllers/index.ts +0 -39
- package/utils/controllers/number-field.ts +0 -103
- package/utils/controllers/otp.ts +0 -252
- package/utils/controllers/sidebar.ts +0 -610
- package/utils/controllers/slider.ts +0 -336
- package/utils/controllers/tags-input.ts +0 -255
- package/utils/controllers/toast.ts +0 -426
package/README.md
CHANGED
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
# @pienter/ui
|
|
2
2
|
|
|
3
|
-
Shared Pienter UI package with
|
|
3
|
+
Shared Pienter UI package with Vue components, CSS, icons, and browser utilities.
|
|
4
4
|
|
|
5
|
-
This package publishes raw source files. Consuming projects compile the Vue,
|
|
5
|
+
This package publishes raw source files. Consuming projects compile the Vue, TypeScript, and CSS files with their own toolchain.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
npm install @pienter/ui
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Install the framework peer you use:
|
|
14
|
-
|
|
15
|
-
```sh
|
|
16
|
-
npm install vue
|
|
17
|
-
npm install astro
|
|
10
|
+
npm install @pienter/ui vue
|
|
18
11
|
```
|
|
19
12
|
|
|
20
13
|
## Usage
|
|
@@ -24,14 +17,6 @@ import Button from '@pienter/ui/components/Button.vue';
|
|
|
24
17
|
import '@pienter/ui/styles';
|
|
25
18
|
```
|
|
26
19
|
|
|
27
|
-
Astro components are available beside Vue components:
|
|
28
|
-
|
|
29
|
-
```astro
|
|
30
|
-
---
|
|
31
|
-
import Button from '@pienter/ui/components/Button.astro';
|
|
32
|
-
---
|
|
33
|
-
```
|
|
34
|
-
|
|
35
20
|
Components use explicit flat public entrypoints. Intentional shared types are
|
|
36
21
|
available through module type entrypoints:
|
|
37
22
|
|
|
@@ -85,12 +85,8 @@
|
|
|
85
85
|
</template>
|
|
86
86
|
|
|
87
87
|
<script setup lang="ts">
|
|
88
|
-
import { computed, ref, watch
|
|
89
|
-
import {
|
|
90
|
-
mountPopover,
|
|
91
|
-
type PopoverControl,
|
|
92
|
-
type Placement,
|
|
93
|
-
} from '../../../utils/controllers/popover.js';
|
|
88
|
+
import { computed, ref, watch } from 'vue';
|
|
89
|
+
import { usePopover, type Placement } from '../../../composables/usePopover.js';
|
|
94
90
|
import { generateId } from '../../../utils/a11y/id.js';
|
|
95
91
|
|
|
96
92
|
defineOptions({ inheritAttrs: false });
|
|
@@ -218,7 +214,25 @@ const activeIndex = ref<number | null>(null);
|
|
|
218
214
|
// label of the currently-selected option.
|
|
219
215
|
const isTyping = ref(false);
|
|
220
216
|
|
|
221
|
-
|
|
217
|
+
// Anchor the listbox to the input itself so positioning tracks the
|
|
218
|
+
// input's box (the input is the visual reference, not the wrapper).
|
|
219
|
+
// `modal: false` → `popover="auto"` so the browser handles
|
|
220
|
+
// outside-click / focus-out / Escape light-dismiss. The listbox
|
|
221
|
+
// closes naturally when the user tabs or clicks away.
|
|
222
|
+
const control = usePopover(listboxRef, {
|
|
223
|
+
anchor: inputRef,
|
|
224
|
+
placement: () => props.placement,
|
|
225
|
+
offset: () => props.offset,
|
|
226
|
+
onOpen: () => {
|
|
227
|
+
isOpen.value = true;
|
|
228
|
+
emit('open-change', true);
|
|
229
|
+
},
|
|
230
|
+
onClose: () => {
|
|
231
|
+
isOpen.value = false;
|
|
232
|
+
activeIndex.value = null;
|
|
233
|
+
emit('open-change', false);
|
|
234
|
+
},
|
|
235
|
+
});
|
|
222
236
|
|
|
223
237
|
const hasErrors = computed(() => props.errors.length > 0);
|
|
224
238
|
|
|
@@ -271,12 +285,10 @@ const activeOptionDomId = computed(() => {
|
|
|
271
285
|
|
|
272
286
|
function open(): void {
|
|
273
287
|
if (props.disabled) return;
|
|
274
|
-
if (!control) return;
|
|
275
288
|
control.show();
|
|
276
289
|
}
|
|
277
290
|
|
|
278
291
|
function close(): void {
|
|
279
|
-
if (!control) return;
|
|
280
292
|
control.hide();
|
|
281
293
|
}
|
|
282
294
|
|
|
@@ -463,30 +475,6 @@ function onKeydown(event: KeyboardEvent): void {
|
|
|
463
475
|
}
|
|
464
476
|
}
|
|
465
477
|
|
|
466
|
-
onMounted(() => {
|
|
467
|
-
if (!listboxRef.value || !inputRef.value) return;
|
|
468
|
-
control = mountPopover(listboxRef.value, {
|
|
469
|
-
// Anchor the listbox to the input itself so positioning
|
|
470
|
-
// tracks the input's box (the input is the visual reference,
|
|
471
|
-
// not the wrapper).
|
|
472
|
-
anchor: inputRef.value,
|
|
473
|
-
placement: props.placement,
|
|
474
|
-
offset: props.offset,
|
|
475
|
-
// `modal: false` → `popover="auto"` so the browser handles
|
|
476
|
-
// outside-click / focus-out / Escape light-dismiss. The
|
|
477
|
-
// listbox closes naturally when the user tabs or clicks away.
|
|
478
|
-
onOpen: () => {
|
|
479
|
-
isOpen.value = true;
|
|
480
|
-
emit('open-change', true);
|
|
481
|
-
},
|
|
482
|
-
onClose: () => {
|
|
483
|
-
isOpen.value = false;
|
|
484
|
-
activeIndex.value = null;
|
|
485
|
-
emit('open-change', false);
|
|
486
|
-
},
|
|
487
|
-
});
|
|
488
|
-
});
|
|
489
|
-
|
|
490
478
|
// Watch model changes from the consumer and reset typing state so
|
|
491
479
|
// the input mirrors the new selection's label cleanly.
|
|
492
480
|
watch(
|
|
@@ -496,11 +484,6 @@ watch(
|
|
|
496
484
|
query.value = '';
|
|
497
485
|
},
|
|
498
486
|
);
|
|
499
|
-
|
|
500
|
-
onBeforeUnmount(() => {
|
|
501
|
-
control?.teardown();
|
|
502
|
-
control = null;
|
|
503
|
-
});
|
|
504
487
|
</script>
|
|
505
488
|
|
|
506
489
|
<style>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* disabled state match TextInput / Textarea exactly.
|
|
9
9
|
*
|
|
10
10
|
* The listbox is a separate element (a `<ul popover="auto">`) the
|
|
11
|
-
*
|
|
11
|
+
* composable positions via `usePopover`. CSS resets the
|
|
12
12
|
* user-agent popover defaults (`margin: 0; border: 0; inset: auto`)
|
|
13
13
|
* and supplies the surface look — the Popover API places the
|
|
14
14
|
* element in the top layer; inline `left`/`top` from the controller
|
|
@@ -131,8 +131,7 @@ function handleSubmit(): void {
|
|
|
131
131
|
|
|
132
132
|
// Validation passed. Clear any lingering client-side errors from a
|
|
133
133
|
// prior failed submit so child form-primitives don't keep displaying
|
|
134
|
-
// them while the consumer's async submit runs
|
|
135
|
-
// controller's `clearAllErrors(el)` between client-pass and onSubmit).
|
|
134
|
+
// them while the consumer's async submit runs.
|
|
136
135
|
clientFieldErrors.value = {};
|
|
137
136
|
|
|
138
137
|
// Surface FormData to the consumer's submit handler; consumer manages
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* use cases where the form-primitive components' built-in label is not
|
|
6
6
|
* suitable (e.g., labels associated with custom groups or with multiple
|
|
7
7
|
* controls). Importing this file re-imports the scaffold so a
|
|
8
|
-
* standalone Label
|
|
8
|
+
* standalone Label on a page that does not also use
|
|
9
9
|
* TextInput / Textarea / Select still gets the typography rules.
|
|
10
10
|
*/
|
|
11
11
|
@import '../text-input/text-input.css';
|
|
@@ -143,7 +143,7 @@ function handleStep(direction: 'decrement' | 'increment'): void {
|
|
|
143
143
|
input.stepDown();
|
|
144
144
|
}
|
|
145
145
|
// Dispatch native input/change events so consumer-attached DOM
|
|
146
|
-
// listeners observe the same
|
|
146
|
+
// listeners observe stepper clicks the same as typed edits.
|
|
147
147
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
148
148
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
149
149
|
emit('update:modelValue', input.valueAsNumber);
|
|
@@ -8,11 +8,8 @@
|
|
|
8
8
|
:aria-describedby="describedBy"
|
|
9
9
|
:aria-invalid="hasErrors ? 'true' : undefined"
|
|
10
10
|
>
|
|
11
|
-
<!-- aria-invalid lives on the <fieldset
|
|
12
|
-
|
|
13
|
-
is intentional — see ./AUDIT.md "ARIA
|
|
14
|
-
expectations" for the rationale. Do NOT remove either binding without
|
|
15
|
-
coordinating with the form controller. -->
|
|
11
|
+
<!-- aria-invalid lives on the <fieldset>: a group-level signal — see
|
|
12
|
+
./AUDIT.md "ARIA expectations" for the rationale. -->
|
|
16
13
|
<legend class="pui-field__label">{{ label }}</legend>
|
|
17
14
|
<p v-if="hint" :id="hintId" class="pui-field__hint">{{ hint }}</p>
|
|
18
15
|
<div class="pui-radio-group" :data-orientation="orientation">
|
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
* horizontal track that hosts a faint background rail, a coloured
|
|
6
6
|
* fill segment representing the current value (or range), and one
|
|
7
7
|
* or two `<button role="slider">` thumbs. Each thumb is absolutely
|
|
8
|
-
* positioned via `inset-inline-start: <pct
|
|
9
|
-
*
|
|
10
|
-
* percentage so the visual is identical across both tiers.
|
|
8
|
+
* positioned via `inset-inline-start: <pct>%`, written by Vue's
|
|
9
|
+
* reactive computed.
|
|
11
10
|
*
|
|
12
11
|
* Layout rationale: rejected the native `<input type="range">`
|
|
13
12
|
* baseline for two reasons — (a) cross-browser thumb / track
|
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
* row that hosts existing tag pills followed by a text input where
|
|
7
7
|
* the user types the next tag. The whole row reads as one
|
|
8
8
|
* integrated control: clicking anywhere lands focus on the entry
|
|
9
|
-
* input (
|
|
10
|
-
* `mountTagsInput`).
|
|
9
|
+
* input (a click handler in `TagsInput.vue`).
|
|
11
10
|
*
|
|
12
11
|
* Layout rationale: rejected the "fixed-height pills + horizontally-
|
|
13
12
|
* scrolling row" alternative because vertical wrapping reads better
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
/* Auto-resize variant — opt-in via the `autoResize` prop, which sets
|
|
15
15
|
* `style="field-sizing: content"` inline on the <textarea>. The
|
|
16
|
-
* inline style is the only documented
|
|
16
|
+
* inline style is the only documented path, so the
|
|
17
17
|
* selector matches it directly. Browsers without `field-sizing`
|
|
18
18
|
* support fall back gracefully to the default fixed-rows/min-height
|
|
19
19
|
* box. */
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
</template>
|
|
6
6
|
|
|
7
7
|
<script setup lang="ts">
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { ref } from 'vue';
|
|
9
|
+
import { useDisclosure } from '../../../composables/useDisclosure.js';
|
|
10
10
|
|
|
11
11
|
const props = withDefaults(
|
|
12
12
|
defineProps<{
|
|
@@ -25,32 +25,11 @@ const props = withDefaults(
|
|
|
25
25
|
);
|
|
26
26
|
|
|
27
27
|
const rootRef = ref<HTMLDivElement | null>(null);
|
|
28
|
-
let teardown: (() => void) | null = null;
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
onMounted(mount);
|
|
38
|
-
|
|
39
|
-
// `singleOpen` is read once at mount time. If a consumer toggles it
|
|
40
|
-
// at runtime we re-mount so the new policy takes effect immediately.
|
|
41
|
-
// This is rare (the prop is typically static per surface) but cheap.
|
|
42
|
-
watch(
|
|
43
|
-
() => props.singleOpen,
|
|
44
|
-
() => {
|
|
45
|
-
teardown?.();
|
|
46
|
-
teardown = null;
|
|
47
|
-
mount();
|
|
48
|
-
},
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
onBeforeUnmount(() => {
|
|
52
|
-
teardown?.();
|
|
53
|
-
teardown = null;
|
|
29
|
+
// `singleOpen` is read per toggle, so a runtime change takes effect on
|
|
30
|
+
// the next interaction without re-wiring.
|
|
31
|
+
useDisclosure(rootRef, {
|
|
32
|
+
singleOpen: () => props.singleOpen,
|
|
54
33
|
});
|
|
55
34
|
</script>
|
|
56
35
|
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
</template>
|
|
18
18
|
|
|
19
19
|
<script setup lang="ts">
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import { ref } from 'vue';
|
|
21
|
+
import { useDisclosure } from '../../../composables/useDisclosure.js';
|
|
22
22
|
import Icon from '../../display/icon/Icon.vue';
|
|
23
23
|
|
|
24
24
|
withDefaults(
|
|
@@ -51,24 +51,14 @@ const emit = defineEmits<{
|
|
|
51
51
|
}>();
|
|
52
52
|
|
|
53
53
|
const rootRef = ref<HTMLDetailsElement | null>(null);
|
|
54
|
-
let teardown: (() => void) | null = null;
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
onToggle: (_target, open) => {
|
|
64
|
-
emit('update:open', open);
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
onBeforeUnmount(() => {
|
|
70
|
-
teardown?.();
|
|
71
|
-
teardown = null;
|
|
55
|
+
// Collapsible is single-disclosure: no `singleOpen` coordination
|
|
56
|
+
// needed. The composable's only role here is a normalized callback —
|
|
57
|
+
// the native `<details>` element does the open/close work itself.
|
|
58
|
+
useDisclosure(rootRef, {
|
|
59
|
+
onToggle: (_target, open) => {
|
|
60
|
+
emit('update:open', open);
|
|
61
|
+
},
|
|
72
62
|
});
|
|
73
63
|
</script>
|
|
74
64
|
|
|
@@ -93,8 +93,8 @@
|
|
|
93
93
|
|
|
94
94
|
/* ===== sort affordance ===== */
|
|
95
95
|
|
|
96
|
-
/*
|
|
97
|
-
|
|
96
|
+
/* Rendered as a <button>; the resets below also cover <a> so a
|
|
97
|
+
consumer-supplied link header behaves identically. */
|
|
98
98
|
.pui-table__sort {
|
|
99
99
|
display: inline-flex;
|
|
100
100
|
align-items: center;
|
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
.pui-table__loading {
|
|
168
168
|
position: absolute;
|
|
169
169
|
inset: 0;
|
|
170
|
-
z-index: 1;
|
|
170
|
+
z-index: var(--layer-1);
|
|
171
171
|
|
|
172
172
|
/* the tint gives the blur a surface to sit in; without it the frosting
|
|
173
173
|
reads as a smudge rather than a pane */
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="pui-pagination-footer">
|
|
3
|
+
<div v-if="$slots.summary" class="pui-pagination-footer__summary">
|
|
4
|
+
<slot name="summary" />
|
|
5
|
+
</div>
|
|
6
|
+
<div class="pui-pagination-footer__control">
|
|
7
|
+
<slot />
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup lang="ts">
|
|
13
|
+
// Pure markup with slots — no props, no state, no ARIA of its own. The
|
|
14
|
+
// landmark lives on the `<nav>` that `Pagination` renders inside the default
|
|
15
|
+
// slot; a second landmark here would be noise.
|
|
16
|
+
//
|
|
17
|
+
// A plain `<div>`, not `<footer>`: dropped at body level a `<footer>` becomes
|
|
18
|
+
// the `contentinfo` landmark, which this is not.
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<style>
|
|
22
|
+
@import './pagination.css';
|
|
23
|
+
</style>
|
|
@@ -1,53 +1,152 @@
|
|
|
1
|
+
/* `.pui-pagination` is the bare control, `.pui-pagination-footer` the bar that
|
|
2
|
+
seats it opposite a result summary — one visual family, one file. Rationale
|
|
3
|
+
for the less obvious rules lives in AUDIT.md. */
|
|
1
4
|
@layer components {
|
|
5
|
+
/* ===== bare control ===== */
|
|
6
|
+
|
|
2
7
|
.pui-pagination {
|
|
8
|
+
/* The STRIP owns the control height, not the cells: `box-sizing` is
|
|
9
|
+
border-box, so this matches a `sm` Button's 30px outer box instead of
|
|
10
|
+
standing 2px taller once the strip's own border is added. Cells then
|
|
11
|
+
stretch to fill whatever is left inside it.
|
|
12
|
+
No `overflow: hidden` to clip the cell corners — it clips the focus
|
|
13
|
+
ring too, so the end cells carry the radius instead. */
|
|
3
14
|
display: inline-flex;
|
|
4
|
-
align-items:
|
|
5
|
-
|
|
6
|
-
list-style: none;
|
|
7
|
-
padding: 0;
|
|
15
|
+
align-items: stretch;
|
|
16
|
+
height: var(--control-height-sm);
|
|
8
17
|
margin: 0;
|
|
18
|
+
padding: 0;
|
|
19
|
+
list-style: none;
|
|
20
|
+
border: var(--stroke-sm) solid var(--border-clr-base);
|
|
21
|
+
border-radius: var(--radius-sm);
|
|
22
|
+
background: var(--bg-clr-surface);
|
|
23
|
+
|
|
24
|
+
& > li {
|
|
25
|
+
display: flex;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
& > li + li :is(.pui-pagination__item, .pui-pagination__ellipsis) {
|
|
29
|
+
border-inline-start: var(--stroke-sm) solid var(--border-clr-base);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* First and last are always the prev / next buttons. */
|
|
33
|
+
& > li:first-child .pui-pagination__item {
|
|
34
|
+
border-start-start-radius: var(--radius-sm);
|
|
35
|
+
border-end-start-radius: var(--radius-sm);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
& > li:last-child .pui-pagination__item {
|
|
39
|
+
border-start-end-radius: var(--radius-sm);
|
|
40
|
+
border-end-end-radius: var(--radius-sm);
|
|
41
|
+
}
|
|
9
42
|
}
|
|
10
43
|
|
|
11
44
|
.pui-pagination__item {
|
|
45
|
+
/* box → typography → colour → transition */
|
|
46
|
+
display: inline-grid;
|
|
47
|
+
place-items: center;
|
|
48
|
+
min-width: var(--control-height-sm);
|
|
49
|
+
height: 100%;
|
|
50
|
+
padding-inline: var(--space-2xs);
|
|
12
51
|
appearance: none;
|
|
13
|
-
border:
|
|
14
|
-
background:
|
|
52
|
+
border: 0;
|
|
53
|
+
background: transparent;
|
|
15
54
|
font: inherit;
|
|
16
55
|
font-size: var(--step--1);
|
|
17
|
-
font-weight: var(--fw-
|
|
18
|
-
|
|
19
|
-
height: 2.2rem;
|
|
20
|
-
display: inline-grid;
|
|
21
|
-
place-items: center;
|
|
22
|
-
cursor: pointer;
|
|
23
|
-
border-radius: var(--radius-sm);
|
|
24
|
-
text-decoration: none;
|
|
56
|
+
font-weight: var(--fw-control);
|
|
57
|
+
font-variant-numeric: tabular-nums;
|
|
25
58
|
color: var(--text-clr-base);
|
|
59
|
+
text-decoration: none;
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
transition: background-color var(--duration-base) var(--ease-base);
|
|
62
|
+
|
|
63
|
+
&:hover:not(:disabled, [aria-disabled='true']) {
|
|
64
|
+
background: var(--bg-clr-surface-2);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
&:focus-visible {
|
|
68
|
+
/* Inset and lifted, or the neighbouring cell overdraws the ring. */
|
|
69
|
+
position: relative;
|
|
70
|
+
z-index: 1;
|
|
71
|
+
outline: var(--outline-width) solid var(--outline-clr-base);
|
|
72
|
+
outline-offset: calc(var(--outline-offset) * -1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Never `--text-clr-inverse` here: it is near-white in both themes and
|
|
76
|
+
only pairs with a fill or an inverted bar. See AUDIT.md. */
|
|
77
|
+
&[aria-current='page'] {
|
|
78
|
+
background: var(--bg-clr-selected);
|
|
79
|
+
color: var(--text-clr-base);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
&:disabled,
|
|
83
|
+
&[aria-disabled='true'] {
|
|
84
|
+
opacity: var(--opacity-disabled);
|
|
85
|
+
cursor: not-allowed;
|
|
86
|
+
}
|
|
26
87
|
}
|
|
27
88
|
|
|
28
|
-
.pui-
|
|
29
|
-
|
|
89
|
+
.pui-pagination__ellipsis {
|
|
90
|
+
display: inline-grid;
|
|
91
|
+
place-items: center;
|
|
92
|
+
min-width: var(--control-height-sm);
|
|
93
|
+
height: 100%;
|
|
94
|
+
font-size: var(--step--1);
|
|
95
|
+
color: var(--text-clr-muted);
|
|
30
96
|
}
|
|
31
97
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
98
|
+
/* ===== narrow reduction ===== */
|
|
99
|
+
|
|
100
|
+
/* The `<nav>` is the query container, not the `<ul>` — the `<ul>` is
|
|
101
|
+
inline-flex, so `inline-size` containment would collapse it. */
|
|
102
|
+
.pui-pagination-nav {
|
|
103
|
+
container-type: inline-size;
|
|
35
104
|
}
|
|
36
105
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
106
|
+
/* Keep the arrows and the current page, drop the rest of the run. */
|
|
107
|
+
@container (max-width: 18rem) {
|
|
108
|
+
.pui-pagination
|
|
109
|
+
> li:not(:first-child, :last-child, :has(> [aria-current='page'])) {
|
|
110
|
+
display: none;
|
|
111
|
+
}
|
|
41
112
|
}
|
|
42
113
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
114
|
+
/* ===== footer bar ===== */
|
|
115
|
+
|
|
116
|
+
.pui-pagination-footer {
|
|
117
|
+
container-type: inline-size;
|
|
118
|
+
display: flex;
|
|
119
|
+
flex-wrap: wrap;
|
|
120
|
+
align-items: center;
|
|
121
|
+
gap: var(--space-xs) var(--space-s);
|
|
122
|
+
padding: var(--space-xs) var(--space-s);
|
|
123
|
+
border-block-start: var(--stroke-sm) solid var(--border-clr-base);
|
|
124
|
+
background: var(--bg-clr-surface);
|
|
47
125
|
}
|
|
48
126
|
|
|
49
|
-
.pui-
|
|
127
|
+
.pui-pagination-footer__summary {
|
|
128
|
+
font-size: var(--step--1);
|
|
50
129
|
color: var(--text-clr-muted);
|
|
51
|
-
|
|
130
|
+
font-variant-numeric: tabular-nums;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.pui-pagination-footer__control {
|
|
134
|
+
/* Block, so the `<nav>` inside inherits a width to query; `flex: 1`
|
|
135
|
+
holds it at the trailing edge with or without a summary. */
|
|
136
|
+
flex: 1;
|
|
137
|
+
text-align: end;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@container (max-width: 30rem) {
|
|
141
|
+
.pui-pagination-footer__summary,
|
|
142
|
+
.pui-pagination-footer__control {
|
|
143
|
+
flex: 1 0 100%;
|
|
144
|
+
text-align: center;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* Stacked, the summary reads as a caption under the control. */
|
|
148
|
+
.pui-pagination-footer__summary {
|
|
149
|
+
order: 1;
|
|
150
|
+
}
|
|
52
151
|
}
|
|
53
152
|
}
|