@pienter/ui 0.2.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 +64 -0
- package/CONVENTIONS.md +1499 -0
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/components/action/button/Button.astro +95 -0
- package/components/action/button/Button.vue +92 -0
- package/components/action/button/IconButton.astro +86 -0
- package/components/action/button/IconButton.vue +85 -0
- package/components/action/button/button.css +194 -0
- package/components/action/toggle/Toggle.vue +38 -0
- package/components/action/toggle/toggle.css +77 -0
- package/components/action/toggle-group/ToggleGroup.vue +67 -0
- package/components/action/toggle-group/toggle-group.css +78 -0
- package/components/display/avatar/Avatar.astro +17 -0
- package/components/display/avatar/Avatar.vue +30 -0
- package/components/display/avatar/AvatarStack.astro +9 -0
- package/components/display/avatar/AvatarStack.vue +11 -0
- package/components/display/avatar/avatar.css +58 -0
- package/components/display/badge/Badge.astro +15 -0
- package/components/display/badge/Badge.vue +23 -0
- package/components/display/badge/badge.css +56 -0
- package/components/display/empty/Empty.astro +9 -0
- package/components/display/empty/Empty.vue +11 -0
- package/components/display/empty/empty.css +39 -0
- package/components/display/icon/Icon.astro +52 -0
- package/components/display/icon/Icon.vue +57 -0
- package/components/display/icon/icon.css +47 -0
- package/components/feedback/alert/Alert.astro +52 -0
- package/components/feedback/alert/Alert.vue +60 -0
- package/components/feedback/alert/alert.css +78 -0
- package/components/feedback/progress/Progress.astro +68 -0
- package/components/feedback/progress/Progress.vue +82 -0
- package/components/feedback/progress/progress.css +68 -0
- package/components/feedback/skeleton/Skeleton.astro +32 -0
- package/components/feedback/skeleton/Skeleton.vue +39 -0
- package/components/feedback/skeleton/skeleton.css +56 -0
- package/components/feedback/spinner/Spinner.astro +25 -0
- package/components/feedback/spinner/Spinner.vue +36 -0
- package/components/feedback/spinner/spinner.css +91 -0
- package/components/feedback/toast/Toast.astro +50 -0
- package/components/feedback/toast/Toast.vue +74 -0
- package/components/feedback/toast/toast.css +128 -0
- package/components/form/checkbox/Checkbox.astro +79 -0
- package/components/form/checkbox/Checkbox.vue +95 -0
- package/components/form/checkbox/checkbox.css +59 -0
- package/components/form/combobox/Combobox.vue +508 -0
- package/components/form/combobox/combobox.css +110 -0
- package/components/form/date-input/DateInput.astro +105 -0
- package/components/form/date-input/DateInput.vue +121 -0
- package/components/form/date-input/date-input.css +19 -0
- package/components/form/form/Form.astro +106 -0
- package/components/form/form/Form.vue +181 -0
- package/components/form/form/form.css +46 -0
- package/components/form/input-otp/InputOTP.astro +147 -0
- package/components/form/input-otp/InputOTP.vue +209 -0
- package/components/form/input-otp/input-otp.css +52 -0
- package/components/form/label/Label.astro +13 -0
- package/components/form/label/Label.vue +20 -0
- package/components/form/label/label.css +11 -0
- package/components/form/number-field/NumberField.astro +142 -0
- package/components/form/number-field/NumberField.vue +155 -0
- package/components/form/number-field/number-field.css +115 -0
- package/components/form/radio-group/RadioGroup.astro +105 -0
- package/components/form/radio-group/RadioGroup.vue +110 -0
- package/components/form/radio-group/radio-group.css +114 -0
- package/components/form/radio-group/types.ts +14 -0
- package/components/form/select/Segmented.vue +36 -0
- package/components/form/select/Select.astro +105 -0
- package/components/form/select/Select.vue +109 -0
- package/components/form/select/select.css +96 -0
- package/components/form/slider/Slider.astro +205 -0
- package/components/form/slider/Slider.vue +321 -0
- package/components/form/slider/slider.css +115 -0
- package/components/form/switch/Switch.astro +75 -0
- package/components/form/switch/Switch.vue +89 -0
- package/components/form/switch/switch.css +64 -0
- package/components/form/tags-input/TagsInput.astro +153 -0
- package/components/form/tags-input/TagsInput.vue +207 -0
- package/components/form/tags-input/tags-input.css +128 -0
- package/components/form/text-input/TextInput.astro +84 -0
- package/components/form/text-input/TextInput.vue +99 -0
- package/components/form/text-input/text-input.css +165 -0
- package/components/form/textarea/Textarea.astro +86 -0
- package/components/form/textarea/Textarea.vue +102 -0
- package/components/form/textarea/textarea.css +25 -0
- package/components/layout/accordion/Accordion.vue +59 -0
- package/components/layout/accordion/accordion.css +87 -0
- package/components/layout/card/Card.astro +13 -0
- package/components/layout/card/Card.vue +20 -0
- package/components/layout/card/card.css +55 -0
- package/components/layout/collapsible/Collapsible.vue +77 -0
- package/components/layout/collapsible/collapsible.css +76 -0
- package/components/layout/separator/Separator.astro +31 -0
- package/components/layout/separator/Separator.vue +33 -0
- package/components/layout/separator/separator.css +27 -0
- package/components/layout/table/DataTable.vue +127 -0
- package/components/layout/table/Table.astro +116 -0
- package/components/layout/table/Table.vue +146 -0
- package/components/layout/table/TableRow.vue +59 -0
- package/components/layout/table/table.css +201 -0
- package/components/layout/table/types.ts +35 -0
- package/components/layout/table/useTable.ts +7 -0
- package/components/navigation/breadcrumb/Breadcrumb.astro +36 -0
- package/components/navigation/breadcrumb/Breadcrumb.vue +36 -0
- package/components/navigation/breadcrumb/breadcrumb.css +37 -0
- package/components/navigation/navbar/Navbar.astro +62 -0
- package/components/navigation/navbar/Navbar.vue +50 -0
- package/components/navigation/navbar/navbar.css +77 -0
- package/components/navigation/pagination/Pagination.vue +107 -0
- package/components/navigation/pagination/pagination.css +53 -0
- package/components/navigation/sidebar/Sidebar.astro +132 -0
- package/components/navigation/sidebar/Sidebar.vue +174 -0
- package/components/navigation/sidebar/SidebarItemRender.astro +83 -0
- package/components/navigation/sidebar/SidebarItemRender.vue +98 -0
- package/components/navigation/sidebar/sidebar.css +303 -0
- package/components/navigation/sidebar/types.ts +72 -0
- package/components/navigation/tabs/Tabs.vue +84 -0
- package/components/navigation/tabs/tabs.css +39 -0
- package/components/overlay/alert-dialog/AlertDialog.astro +112 -0
- package/components/overlay/alert-dialog/AlertDialog.vue +117 -0
- package/components/overlay/alert-dialog/alert-dialog.css +57 -0
- package/components/overlay/command/Command.vue +356 -0
- package/components/overlay/command/command.css +179 -0
- package/components/overlay/dropdown-menu/DropdownMenu.vue +143 -0
- package/components/overlay/dropdown-menu/dropdown-menu.css +120 -0
- package/components/overlay/modal/Modal.astro +66 -0
- package/components/overlay/modal/Modal.vue +85 -0
- package/components/overlay/modal/modal.css +60 -0
- package/components/overlay/popover/Popover.vue +113 -0
- package/components/overlay/popover/popover.css +53 -0
- package/components/overlay/sheet/Sheet.vue +88 -0
- package/components/overlay/sheet/sheet.css +108 -0
- package/components/overlay/tooltip/Tooltip.vue +210 -0
- package/components/overlay/tooltip/tooltip.css +50 -0
- package/composables/useUrlSort.ts +48 -0
- package/icons/alert-triangle.ts +1 -0
- package/icons/arrow-down.ts +1 -0
- package/icons/arrow-up-down.ts +5 -0
- package/icons/arrow-up.ts +1 -0
- package/icons/bell.ts +1 -0
- package/icons/check.ts +1 -0
- package/icons/chevron-down.ts +1 -0
- package/icons/chevron-left.ts +1 -0
- package/icons/chevron-right.ts +1 -0
- package/icons/chevron-up-down.ts +5 -0
- package/icons/chevron-up.ts +1 -0
- package/icons/circle-alert.ts +1 -0
- package/icons/circle-check.ts +1 -0
- package/icons/clipboard.ts +1 -0
- package/icons/download.ts +1 -0
- package/icons/edit.ts +1 -0
- package/icons/external-link.ts +1 -0
- package/icons/eye.ts +1 -0
- package/icons/file.ts +1 -0
- package/icons/filter.ts +1 -0
- package/icons/folder.ts +1 -0
- package/icons/image.ts +1 -0
- package/icons/inbox.ts +1 -0
- package/icons/index.ts +91 -0
- package/icons/info.ts +1 -0
- package/icons/layers.ts +1 -0
- package/icons/link-2.ts +1 -0
- package/icons/list.ts +1 -0
- package/icons/loader.ts +3 -0
- package/icons/menu.ts +1 -0
- package/icons/more-horizontal.ts +1 -0
- package/icons/more-vertical.ts +1 -0
- package/icons/plus-circle.ts +1 -0
- package/icons/plus.ts +1 -0
- package/icons/save.ts +1 -0
- package/icons/search.ts +1 -0
- package/icons/send.ts +1 -0
- package/icons/settings.ts +1 -0
- package/icons/tool.ts +1 -0
- package/icons/trash-2.ts +1 -0
- package/icons/trash.ts +1 -0
- package/icons/upload-cloud.ts +1 -0
- package/icons/upload.ts +1 -0
- package/icons/x.ts +1 -0
- package/package.json +150 -0
- package/styles/0-settings/colors.css +241 -0
- package/styles/0-settings/index.css +5 -0
- package/styles/0-settings/layout.css +52 -0
- package/styles/0-settings/motion.css +11 -0
- package/styles/0-settings/spacing.css +15 -0
- package/styles/0-settings/typography.css +37 -0
- package/styles/0-utils/index.css +1 -0
- package/styles/1-reset/index.css +1 -0
- package/styles/1-reset/reset.css +26 -0
- package/styles/2-base/base.css +42 -0
- package/styles/2-base/forms.css +23 -0
- package/styles/2-base/index.css +2 -0
- package/styles/3-layout/container.css +57 -0
- package/styles/3-layout/index.css +2 -0
- package/styles/3-layout/section.css +17 -0
- package/styles/5-utilities/accessibility.css +13 -0
- package/styles/5-utilities/index.css +2 -0
- package/styles/5-utilities/text.css +5 -0
- package/styles/main.css +8 -0
- package/styles/styles.d.ts +6 -0
- package/utils/a11y/focus.ts +68 -0
- package/utils/a11y/id.ts +10 -0
- package/utils/a11y/index.ts +9 -0
- package/utils/a11y/keyboard.ts +32 -0
- package/utils/a11y/live-region.ts +36 -0
- package/utils/controllers/dialog.ts +205 -0
- package/utils/controllers/disclosure.ts +117 -0
- package/utils/controllers/form.ts +524 -0
- package/utils/controllers/index.ts +39 -0
- package/utils/controllers/menu.ts +255 -0
- package/utils/controllers/number-field.ts +103 -0
- package/utils/controllers/otp.ts +252 -0
- package/utils/controllers/popover.ts +434 -0
- package/utils/controllers/sidebar.ts +610 -0
- package/utils/controllers/slider.ts +336 -0
- package/utils/controllers/tags-input.ts +255 -0
- package/utils/controllers/toast.ts +426 -0
- package/utils/dom/index.ts +1 -0
- package/utils/dom/scroll-lock.ts +48 -0
- package/utils/index.ts +3 -0
- package/utils/sort/index.ts +3 -0
- package/utils/sort/serialize.ts +19 -0
- package/utils/sort/state.ts +11 -0
- package/utils/sort/types.ts +15 -0
- package/utils/validation/form.ts +93 -0
- package/utils/validation/index.ts +13 -0
- package/utils/validation/rules.ts +31 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.pui-table-wrap {
|
|
3
|
+
overflow: auto;
|
|
4
|
+
|
|
5
|
+
/* the scroll port is the reference for the loading overlay's width, so its
|
|
6
|
+
spinner can stay centred in what you can actually see */
|
|
7
|
+
container-type: inline-size;
|
|
8
|
+
background: var(--bg-clr-surface);
|
|
9
|
+
border: var(--stroke-sm) solid var(--border-clr-base);
|
|
10
|
+
border-radius: var(--radius-sm); /* matches the button radius */
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* The table owns the column tracks once via --pui-table-cols; head, body and row
|
|
14
|
+
are subgrids spanning 1/-1, so one track value moves the header and every row. */
|
|
15
|
+
.pui-table {
|
|
16
|
+
display: grid;
|
|
17
|
+
grid-template-columns: var(--pui-table-cols, auto);
|
|
18
|
+
width: 100%;
|
|
19
|
+
min-width: 34rem; /* narrow-screen horizontal-scroll fallback */
|
|
20
|
+
font-size: var(--step--1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.pui-table__head,
|
|
24
|
+
.pui-table__body,
|
|
25
|
+
.pui-table__row {
|
|
26
|
+
display: grid;
|
|
27
|
+
grid-template-columns: subgrid;
|
|
28
|
+
grid-column: 1 / -1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* Containing block for the loading overlay, which is a child of the body so it
|
|
32
|
+
covers the rows and only the rows. */
|
|
33
|
+
.pui-table__body {
|
|
34
|
+
position: relative;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* ===== rows ===== */
|
|
38
|
+
|
|
39
|
+
.pui-table__row {
|
|
40
|
+
/* Top borders give the row separators; the header row zeroes its own, so the
|
|
41
|
+
first body row's top border is the header divider and the last row stays clean. */
|
|
42
|
+
border-block-start: var(--stroke-sm) solid var(--border-clr-base);
|
|
43
|
+
transition: background-color var(--duration-base) var(--ease-base);
|
|
44
|
+
|
|
45
|
+
&:hover {
|
|
46
|
+
background: var(--bg-clr-surface-2);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
&[data-head] {
|
|
50
|
+
border-block-start: 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
&[data-selected='true'] {
|
|
54
|
+
background: var(--bg-clr-brand-soft);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
&[data-clickable='true'] {
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ===== cells ===== */
|
|
63
|
+
|
|
64
|
+
.pui-table__cell {
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
gap: var(--space-2xs);
|
|
68
|
+
min-width: 0;
|
|
69
|
+
padding: var(--space-xs) var(--space-s);
|
|
70
|
+
text-align: left;
|
|
71
|
+
|
|
72
|
+
&[data-head] {
|
|
73
|
+
background: var(--bg-clr-surface-2);
|
|
74
|
+
color: var(--text-clr-base);
|
|
75
|
+
font-weight: var(--fw-control);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
&[data-select] {
|
|
79
|
+
flex: 0 0 auto;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
&[data-align='end'] {
|
|
83
|
+
justify-content: flex-end;
|
|
84
|
+
text-align: right;
|
|
85
|
+
font-variant-numeric: tabular-nums; /* numeric cells read cleaner */
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
&[data-align='center'] {
|
|
89
|
+
justify-content: center;
|
|
90
|
+
text-align: center;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* ===== sort affordance ===== */
|
|
95
|
+
|
|
96
|
+
/* One rule for both tiers — a <button> in Vue, an <a> in Astro — so the resets below
|
|
97
|
+
deliberately cover both elements. */
|
|
98
|
+
.pui-table__sort {
|
|
99
|
+
display: inline-flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
gap: var(--space-3xs);
|
|
102
|
+
padding: 0;
|
|
103
|
+
border: 0;
|
|
104
|
+
background: transparent;
|
|
105
|
+
color: inherit;
|
|
106
|
+
font: inherit;
|
|
107
|
+
font-weight: inherit;
|
|
108
|
+
text-decoration: none; /* the <a> tier */
|
|
109
|
+
cursor: pointer;
|
|
110
|
+
|
|
111
|
+
&:focus-visible {
|
|
112
|
+
outline: var(--outline-width) solid var(--border-clr-strong);
|
|
113
|
+
outline-offset: var(--outline-offset);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* The glyph carries the direction (Table swaps chevron-up-down / -up / -down);
|
|
118
|
+
colour only separates the unsorted affordance from the sorted column. */
|
|
119
|
+
.pui-table__sort-arrow {
|
|
120
|
+
display: inline-flex;
|
|
121
|
+
|
|
122
|
+
/* the dim rung reads as an affordance; sorted inherits the header's own colour */
|
|
123
|
+
&[data-sort='none'] {
|
|
124
|
+
--pui-icon-clr: var(--icon-clr-dim);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* ===== selection ===== */
|
|
129
|
+
|
|
130
|
+
/* bare select-row checkbox — intentionally NOT the field Checkbox, which
|
|
131
|
+
always renders a <label> and is too heavy for a selection gutter */
|
|
132
|
+
.pui-table__check {
|
|
133
|
+
width: var(--icon-size-md);
|
|
134
|
+
height: var(--icon-size-md);
|
|
135
|
+
accent-color: var(--bg-clr-brand);
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
|
|
138
|
+
&:focus-visible {
|
|
139
|
+
outline: var(--outline-width) solid var(--outline-clr-base);
|
|
140
|
+
outline-offset: var(--outline-offset);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* ===== loading ===== */
|
|
145
|
+
|
|
146
|
+
/* Restates background-color: this outranks .pui-table__row and transition is a
|
|
147
|
+
shorthand, so opacity alone would drop the hover fade. */
|
|
148
|
+
.pui-table__body > .pui-table__row {
|
|
149
|
+
transition:
|
|
150
|
+
background-color var(--duration-base) var(--ease-base),
|
|
151
|
+
opacity var(--duration-base) var(--ease-base);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* Rows only — the overlay is a body child and must not dim with them. */
|
|
155
|
+
.pui-table[aria-busy='true'] .pui-table__body > .pui-table__row {
|
|
156
|
+
opacity: 0.65;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* The overlay is absolute and adds no height, so a row-less body would collapse to
|
|
160
|
+
nothing; busy-and-childless only, so a populated table never shifts — see AUDIT.md. */
|
|
161
|
+
.pui-table[aria-busy='true'] .pui-table__body:not(:has(> .pui-table__row)) {
|
|
162
|
+
min-height: var(--pui-table-loading-min-h, 9rem);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* A <tr> covering the body, so the spinner centres on the rows and the header
|
|
166
|
+
is untouched — no measuring, and it stays correct if the header resizes. */
|
|
167
|
+
.pui-table__loading {
|
|
168
|
+
position: absolute;
|
|
169
|
+
inset: 0;
|
|
170
|
+
z-index: 1;
|
|
171
|
+
|
|
172
|
+
/* the tint gives the blur a surface to sit in; without it the frosting
|
|
173
|
+
reads as a smudge rather than a pane */
|
|
174
|
+
background: color-mix(in srgb, var(--bg-clr-surface) 45%, transparent);
|
|
175
|
+
backdrop-filter: blur(3px);
|
|
176
|
+
-webkit-backdrop-filter: blur(3px);
|
|
177
|
+
|
|
178
|
+
/* the text ramp, not the brand — over the blur a brand hue reads as decoration */
|
|
179
|
+
--pui-icon-clr: var(--text-clr-base);
|
|
180
|
+
|
|
181
|
+
/* Centring lives on the cell: the body spans the full scrollable width, so sticky
|
|
182
|
+
+ 100cqi is what keeps the spinner inside the scroll port — see AUDIT.md. */
|
|
183
|
+
& > td {
|
|
184
|
+
position: sticky;
|
|
185
|
+
left: 0;
|
|
186
|
+
display: grid;
|
|
187
|
+
place-items: center;
|
|
188
|
+
width: 100cqi;
|
|
189
|
+
height: 100%;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* ===== empty state ===== */
|
|
194
|
+
|
|
195
|
+
.pui-table__empty {
|
|
196
|
+
grid-column: 1 / -1;
|
|
197
|
+
padding: var(--space-l);
|
|
198
|
+
color: var(--text-clr-muted);
|
|
199
|
+
text-align: center;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ComputedRef, InjectionKey } from 'vue';
|
|
2
|
+
|
|
3
|
+
/** Cell + header alignment. `'end'` is the numeric case. */
|
|
4
|
+
export type Align = 'start' | 'end' | 'center';
|
|
5
|
+
|
|
6
|
+
/** A table column. Layout only — content is authored in slots (ADR 0004). */
|
|
7
|
+
export interface Column {
|
|
8
|
+
/** Unique id. Also the slot name (`#<key>` on `TableRow`, `#cell:<key>` on `DataTable`) and the `DataTable` field accessor `row[key]`. */
|
|
9
|
+
key: string;
|
|
10
|
+
/** Header text. */
|
|
11
|
+
label: string;
|
|
12
|
+
/** A CSS grid track fed into the table's `grid-template-columns`: `'auto'`, `'1fr'`, `'minmax(180px, 1fr)'`. Defaults to `'auto'`. */
|
|
13
|
+
width?: string;
|
|
14
|
+
/** Cell + header alignment. */
|
|
15
|
+
align?: Align;
|
|
16
|
+
/** Renders a focusable sort affordance in the header; emits `sort` with this `key`. Never mutates data. */
|
|
17
|
+
sortable?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* Defined in `utils/sort` (sorting is a list concept), re-exported so a consumer
|
|
21
|
+
can take `Column` and `SortState` from one import. */
|
|
22
|
+
export type {
|
|
23
|
+
Sort,
|
|
24
|
+
SortDirection,
|
|
25
|
+
SortState,
|
|
26
|
+
} from '../../../utils/sort/index.js';
|
|
27
|
+
|
|
28
|
+
/** Provided by `Table` so `TableRow` reads column layout instead of taking `columns` again. */
|
|
29
|
+
export interface TableColumnsContext {
|
|
30
|
+
columns: ComputedRef<Column[]>;
|
|
31
|
+
selectable: ComputedRef<boolean>;
|
|
32
|
+
clickable: ComputedRef<boolean>;
|
|
33
|
+
}
|
|
34
|
+
export const TABLE_COLUMNS: InjectionKey<TableColumnsContext> =
|
|
35
|
+
Symbol('pui-table-columns');
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Column } from './types.js';
|
|
2
|
+
|
|
3
|
+
/** Build the `grid-template-columns` value from column widths, prepending a track for the selection gutter. */
|
|
4
|
+
export function gridColumns(columns: Column[], selectable: boolean): string {
|
|
5
|
+
const tracks = columns.map((c) => c.width ?? 'auto');
|
|
6
|
+
return (selectable ? 'auto ' : '') + tracks.join(' ');
|
|
7
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
import './breadcrumb.css';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
items: { label: string; href?: string }[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { items, ...rest } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<nav aria-label="Breadcrumb" {...rest}>
|
|
12
|
+
<ol class="pui-breadcrumb">
|
|
13
|
+
{
|
|
14
|
+
items.map((item, index) => (
|
|
15
|
+
<>
|
|
16
|
+
{index > 0 && (
|
|
17
|
+
<li class="pui-breadcrumb__sep" aria-hidden="true">
|
|
18
|
+
/
|
|
19
|
+
</li>
|
|
20
|
+
)}
|
|
21
|
+
{index < items.length - 1 ? (
|
|
22
|
+
<li class="pui-breadcrumb__item">
|
|
23
|
+
<a href={item.href} class="pui-breadcrumb__link">
|
|
24
|
+
{item.label}
|
|
25
|
+
</a>
|
|
26
|
+
</li>
|
|
27
|
+
) : (
|
|
28
|
+
<li class="pui-breadcrumb__item" aria-current="page">
|
|
29
|
+
{item.label}
|
|
30
|
+
</li>
|
|
31
|
+
)}
|
|
32
|
+
</>
|
|
33
|
+
))
|
|
34
|
+
}
|
|
35
|
+
</ol>
|
|
36
|
+
</nav>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<nav aria-label="Breadcrumb">
|
|
3
|
+
<ol class="pui-breadcrumb">
|
|
4
|
+
<template v-for="(item, index) in items" :key="index">
|
|
5
|
+
<li
|
|
6
|
+
v-if="index > 0"
|
|
7
|
+
class="pui-breadcrumb__sep"
|
|
8
|
+
aria-hidden="true"
|
|
9
|
+
>
|
|
10
|
+
/
|
|
11
|
+
</li>
|
|
12
|
+
<li
|
|
13
|
+
v-if="index < items.length - 1"
|
|
14
|
+
class="pui-breadcrumb__item"
|
|
15
|
+
>
|
|
16
|
+
<a :href="item.href" class="pui-breadcrumb__link">{{
|
|
17
|
+
item.label
|
|
18
|
+
}}</a>
|
|
19
|
+
</li>
|
|
20
|
+
<li v-else class="pui-breadcrumb__item" aria-current="page">
|
|
21
|
+
{{ item.label }}
|
|
22
|
+
</li>
|
|
23
|
+
</template>
|
|
24
|
+
</ol>
|
|
25
|
+
</nav>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<script setup lang="ts">
|
|
29
|
+
defineProps<{
|
|
30
|
+
items: { label: string; href?: string }[];
|
|
31
|
+
}>();
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<style>
|
|
35
|
+
@import './breadcrumb.css';
|
|
36
|
+
</style>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.pui-breadcrumb {
|
|
3
|
+
display: flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
gap: var(--space-2xs);
|
|
6
|
+
font-size: var(--step--1);
|
|
7
|
+
color: var(--text-clr-muted);
|
|
8
|
+
list-style: none;
|
|
9
|
+
padding: 0;
|
|
10
|
+
margin: 0;
|
|
11
|
+
flex-wrap: wrap;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.pui-breadcrumb__item {
|
|
15
|
+
display: inline-flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
gap: var(--space-2xs);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.pui-breadcrumb__link {
|
|
21
|
+
color: var(--text-clr-muted);
|
|
22
|
+
text-decoration: none;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.pui-breadcrumb__link:hover {
|
|
26
|
+
color: var(--text-clr-base);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.pui-breadcrumb__sep {
|
|
30
|
+
color: var(--border-clr-base);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.pui-breadcrumb__item[aria-current='page'] {
|
|
34
|
+
color: var(--text-clr-base);
|
|
35
|
+
font-weight: var(--fw-semibold);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
import './navbar.css';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
brandMark?: string;
|
|
6
|
+
brandName?: string;
|
|
7
|
+
brandHref?: string;
|
|
8
|
+
links?: { label: string; href: string; current?: boolean }[];
|
|
9
|
+
ariaLabel?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
brandMark,
|
|
14
|
+
brandName,
|
|
15
|
+
brandHref = '/',
|
|
16
|
+
links,
|
|
17
|
+
ariaLabel = 'Main navigation',
|
|
18
|
+
...rest
|
|
19
|
+
} = Astro.props;
|
|
20
|
+
|
|
21
|
+
const hasBrandSlot = Astro.slots.has('brand');
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<header
|
|
25
|
+
class="pui-navbar container"
|
|
26
|
+
data-container-size="xl"
|
|
27
|
+
aria-label={ariaLabel}
|
|
28
|
+
{...rest}
|
|
29
|
+
>
|
|
30
|
+
<nav class="pui-navbar__content container-content">
|
|
31
|
+
{
|
|
32
|
+
hasBrandSlot ? (
|
|
33
|
+
<slot name="brand" />
|
|
34
|
+
) : (
|
|
35
|
+
<a href={brandHref} class="pui-navbar__brand">
|
|
36
|
+
{brandMark && <span class="pui-navbar__mark">{brandMark}</span>}
|
|
37
|
+
{brandName && <span class="pui-navbar__wordmark">{brandName}</span>}
|
|
38
|
+
</a>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
{
|
|
42
|
+
links?.length && (
|
|
43
|
+
<ul class="pui-navbar__list">
|
|
44
|
+
{links.map((link) => (
|
|
45
|
+
<li>
|
|
46
|
+
<a
|
|
47
|
+
class="pui-navbar__link"
|
|
48
|
+
href={link.href}
|
|
49
|
+
aria-current={link.current ? 'page' : undefined}
|
|
50
|
+
>
|
|
51
|
+
{link.label}
|
|
52
|
+
</a>
|
|
53
|
+
</li>
|
|
54
|
+
))}
|
|
55
|
+
</ul>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
<div class="pui-navbar__actions">
|
|
59
|
+
<slot name="actions" />
|
|
60
|
+
</div>
|
|
61
|
+
</nav>
|
|
62
|
+
</header>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<nav class="pui-navbar" :aria-label="ariaLabel">
|
|
3
|
+
<slot name="brand">
|
|
4
|
+
<a :href="brandHref" class="pui-navbar__brand">
|
|
5
|
+
<span v-if="brandMark" class="pui-navbar__mark">{{
|
|
6
|
+
brandMark
|
|
7
|
+
}}</span>
|
|
8
|
+
<span v-if="brandName" class="pui-navbar__wordmark">{{
|
|
9
|
+
brandName
|
|
10
|
+
}}</span>
|
|
11
|
+
</a>
|
|
12
|
+
</slot>
|
|
13
|
+
<ul v-if="links?.length" class="pui-navbar__list">
|
|
14
|
+
<li v-for="link in links" :key="link.href">
|
|
15
|
+
<a
|
|
16
|
+
class="pui-navbar__link"
|
|
17
|
+
:href="link.href"
|
|
18
|
+
:aria-current="link.current ? 'page' : undefined"
|
|
19
|
+
>{{ link.label }}</a
|
|
20
|
+
>
|
|
21
|
+
</li>
|
|
22
|
+
</ul>
|
|
23
|
+
<div class="pui-navbar__actions">
|
|
24
|
+
<slot name="actions" />
|
|
25
|
+
</div>
|
|
26
|
+
</nav>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<script setup lang="ts">
|
|
30
|
+
withDefaults(
|
|
31
|
+
defineProps<{
|
|
32
|
+
brandMark?: string;
|
|
33
|
+
brandName?: string;
|
|
34
|
+
brandHref?: string;
|
|
35
|
+
links?: { label: string; href: string; current?: boolean }[];
|
|
36
|
+
ariaLabel?: string;
|
|
37
|
+
}>(),
|
|
38
|
+
{
|
|
39
|
+
brandMark: undefined,
|
|
40
|
+
brandName: undefined,
|
|
41
|
+
brandHref: '/',
|
|
42
|
+
links: undefined,
|
|
43
|
+
ariaLabel: 'Main navigation',
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<style>
|
|
49
|
+
@import './navbar.css';
|
|
50
|
+
</style>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.pui-navbar {
|
|
3
|
+
background: hsl(from var(--bg-clr-base) h s l / 0.92);
|
|
4
|
+
backdrop-filter: blur(8px);
|
|
5
|
+
border-block-end: var(--stroke-sm) solid var(--border-clr-base);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.pui-navbar__content {
|
|
9
|
+
display: flex;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.pui-navbar__brand {
|
|
13
|
+
display: inline-flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
gap: var(--space-2xs);
|
|
16
|
+
color: var(--text-clr-base);
|
|
17
|
+
text-decoration: none;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.pui-navbar__mark {
|
|
21
|
+
width: 1.75rem;
|
|
22
|
+
height: 1.75rem;
|
|
23
|
+
background: var(--bg-clr-brand);
|
|
24
|
+
color: var(--bg-clr-brand-ink);
|
|
25
|
+
border: var(--stroke-md) solid var(--border-clr-strong);
|
|
26
|
+
border-radius: var(--radius-xs);
|
|
27
|
+
font-family: var(--ff-display);
|
|
28
|
+
font-weight: var(--fw-display);
|
|
29
|
+
display: grid;
|
|
30
|
+
place-items: center;
|
|
31
|
+
font-size: var(--step--1);
|
|
32
|
+
line-height: 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.pui-navbar__wordmark {
|
|
36
|
+
font-family: var(--ff-display);
|
|
37
|
+
font-weight: var(--fw-display);
|
|
38
|
+
font-size: var(--step-1);
|
|
39
|
+
letter-spacing: var(--ls-display);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.pui-navbar__list {
|
|
43
|
+
display: flex;
|
|
44
|
+
gap: var(--space-s);
|
|
45
|
+
list-style: none;
|
|
46
|
+
margin: 0;
|
|
47
|
+
padding: 0;
|
|
48
|
+
margin-inline-start: var(--space-xs);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.pui-navbar__link {
|
|
52
|
+
font-size: var(--step--1);
|
|
53
|
+
font-weight: var(--fw-semibold);
|
|
54
|
+
color: var(--text-clr-base);
|
|
55
|
+
text-decoration: none;
|
|
56
|
+
padding-block: var(--space-3xs);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.pui-navbar__link[aria-current='page'] {
|
|
60
|
+
border-block-end: 2px solid var(--border-clr-brand);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.pui-navbar__link:hover {
|
|
64
|
+
color: var(--text-clr-brand);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.pui-navbar__link:focus-visible {
|
|
68
|
+
outline: 3px solid var(--outline-clr-base);
|
|
69
|
+
outline-offset: 2px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.pui-navbar__actions {
|
|
73
|
+
margin-inline-start: auto;
|
|
74
|
+
display: flex;
|
|
75
|
+
gap: var(--space-2xs);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<nav aria-label="Pagination">
|
|
3
|
+
<ul class="pui-pagination">
|
|
4
|
+
<li>
|
|
5
|
+
<button
|
|
6
|
+
class="pui-pagination__item"
|
|
7
|
+
:disabled="currentPage <= 1"
|
|
8
|
+
aria-label="Previous page"
|
|
9
|
+
@click="emit('update:currentPage', currentPage - 1)"
|
|
10
|
+
>
|
|
11
|
+
<Icon name="chevron-left" size="sm" />
|
|
12
|
+
</button>
|
|
13
|
+
</li>
|
|
14
|
+
|
|
15
|
+
<template v-for="page in visiblePages" :key="page">
|
|
16
|
+
<li v-if="page === '...'">
|
|
17
|
+
<span class="pui-pagination__ellipsis">…</span>
|
|
18
|
+
</li>
|
|
19
|
+
<li v-else>
|
|
20
|
+
<button
|
|
21
|
+
class="pui-pagination__item"
|
|
22
|
+
:aria-current="
|
|
23
|
+
page === currentPage ? 'page' : undefined
|
|
24
|
+
"
|
|
25
|
+
@click="emit('update:currentPage', page as number)"
|
|
26
|
+
>
|
|
27
|
+
{{ page }}
|
|
28
|
+
</button>
|
|
29
|
+
</li>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<li>
|
|
33
|
+
<button
|
|
34
|
+
class="pui-pagination__item"
|
|
35
|
+
:disabled="currentPage >= totalPages"
|
|
36
|
+
aria-label="Next page"
|
|
37
|
+
@click="emit('update:currentPage', currentPage + 1)"
|
|
38
|
+
>
|
|
39
|
+
<Icon name="chevron-right" size="sm" />
|
|
40
|
+
</button>
|
|
41
|
+
</li>
|
|
42
|
+
</ul>
|
|
43
|
+
</nav>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<script setup lang="ts">
|
|
47
|
+
import { computed } from 'vue';
|
|
48
|
+
import Icon from '../../display/icon/Icon.vue';
|
|
49
|
+
|
|
50
|
+
const props = withDefaults(
|
|
51
|
+
defineProps<{
|
|
52
|
+
currentPage: number;
|
|
53
|
+
totalPages: number;
|
|
54
|
+
maxVisible?: number;
|
|
55
|
+
}>(),
|
|
56
|
+
{
|
|
57
|
+
maxVisible: 5,
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const emit = defineEmits<{
|
|
62
|
+
'update:currentPage': [page: number];
|
|
63
|
+
}>();
|
|
64
|
+
|
|
65
|
+
const visiblePages = computed(() => {
|
|
66
|
+
const total = props.totalPages;
|
|
67
|
+
const current = props.currentPage;
|
|
68
|
+
const max = props.maxVisible;
|
|
69
|
+
|
|
70
|
+
if (total <= max) {
|
|
71
|
+
return Array.from({ length: total }, (_, i) => i + 1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const pages: (number | '...')[] = [];
|
|
75
|
+
|
|
76
|
+
// Always show first page
|
|
77
|
+
pages.push(1);
|
|
78
|
+
|
|
79
|
+
// Calculate the range around the current page
|
|
80
|
+
const rangeStart = Math.max(2, current - 1);
|
|
81
|
+
const rangeEnd = Math.min(total - 1, current + 1);
|
|
82
|
+
|
|
83
|
+
// Add leading ellipsis if there is a gap after page 1
|
|
84
|
+
if (rangeStart > 2) {
|
|
85
|
+
pages.push('...');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Add pages in the range
|
|
89
|
+
for (let i = rangeStart; i <= rangeEnd; i++) {
|
|
90
|
+
pages.push(i);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Add trailing ellipsis if there is a gap before last page
|
|
94
|
+
if (rangeEnd < total - 1) {
|
|
95
|
+
pages.push('...');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Always show last page
|
|
99
|
+
pages.push(total);
|
|
100
|
+
|
|
101
|
+
return pages;
|
|
102
|
+
});
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<style>
|
|
106
|
+
@import './pagination.css';
|
|
107
|
+
</style>
|