@pienter/ui 0.3.0 → 0.7.1
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 +258 -0
- package/CONVENTIONS.md +342 -385
- package/README.md +33 -18
- package/components/display/record-details/RecordDetails.vue +61 -0
- package/components/display/record-details/record-details.css +37 -0
- package/components/display/record-details/types.ts +8 -0
- package/components/feedback/toast/toast.css +1 -1
- package/components/form/block-editor/BlockEditor.vue +454 -0
- package/components/form/block-editor/block-editor.css +149 -0
- package/components/form/block-editor/types.ts +15 -0
- package/components/form/combobox/Combobox.vue +42 -72
- 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 -2
- package/components/form/number-field/number-field.css +1 -1
- package/components/form/radio-group/RadioGroup.vue +2 -5
- package/components/form/record-form/RecordFields.vue +128 -0
- package/components/form/record-form/RecordForm.vue +116 -0
- package/components/form/record-form/fields.ts +20 -0
- package/components/form/record-form/record-form.css +15 -0
- package/components/form/record-form/types.ts +28 -0
- package/components/form/slider/slider.css +2 -3
- package/components/form/tags-input/tags-input.css +1 -2
- package/components/form/text-input/text-input.css +2 -0
- 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/index/Index.vue +353 -0
- package/components/layout/index/index.css +114 -0
- package/components/layout/index/useIndex.ts +390 -0
- package/components/layout/table/table.css +5 -4
- package/components/navigation/breadcrumb/Breadcrumb.vue +24 -5
- package/components/navigation/breadcrumb/breadcrumb.css +15 -0
- package/components/navigation/sidebar/Sidebar.vue +329 -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/navigation/tabs/Tabs.vue +6 -0
- 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/composables/useMenu.ts +212 -0
- package/{utils/controllers/popover.ts → composables/usePopover.ts} +107 -120
- package/package.json +16 -40
- package/styles/0-settings/colors.css +10 -0
- 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/a11y/focus.ts +9 -3
- package/utils/cms/index.ts +283 -0
- package/utils/cms/schema.json +126 -0
- 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/pagination/PaginationFooter.astro +0 -24
- 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/menu.ts +0 -255
- 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
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { computed, inject, onScopeDispose, ref, shallowRef, watch } from 'vue';
|
|
2
|
+
import {
|
|
3
|
+
routeLocationKey,
|
|
4
|
+
routerKey,
|
|
5
|
+
isNavigationFailure,
|
|
6
|
+
NavigationFailureType,
|
|
7
|
+
type LocationQuery,
|
|
8
|
+
} from 'vue-router';
|
|
9
|
+
import {
|
|
10
|
+
cloneRecord,
|
|
11
|
+
decodeListQuery,
|
|
12
|
+
encodeListQuery,
|
|
13
|
+
normalizeListQuery,
|
|
14
|
+
type ApiError,
|
|
15
|
+
type ListLoader,
|
|
16
|
+
type ListQuery,
|
|
17
|
+
type QueryOptions,
|
|
18
|
+
} from '../../../utils/cms/index.js';
|
|
19
|
+
|
|
20
|
+
interface Options<T> {
|
|
21
|
+
load: ListLoader<T>;
|
|
22
|
+
queryOptions: QueryOptions;
|
|
23
|
+
syncQuery: boolean;
|
|
24
|
+
searchDebounce: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isListParameter(key: string): boolean {
|
|
28
|
+
return (
|
|
29
|
+
key === 'filter' ||
|
|
30
|
+
key.startsWith('filter[') ||
|
|
31
|
+
['page', 'page_size', 'search', 'sort'].some(
|
|
32
|
+
(name) => key === name || key.startsWith(`${name}[`),
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function routeParams(values: LocationQuery): URLSearchParams {
|
|
38
|
+
const params = new URLSearchParams();
|
|
39
|
+
for (const [key, valuesForKey] of Object.entries(values)) {
|
|
40
|
+
if (!isListParameter(key)) continue;
|
|
41
|
+
for (const value of Array.isArray(valuesForKey)
|
|
42
|
+
? valuesForKey
|
|
43
|
+
: [valuesForKey])
|
|
44
|
+
params.append(key, value ?? '');
|
|
45
|
+
}
|
|
46
|
+
params.sort();
|
|
47
|
+
return params;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function apiError(cause: unknown): ApiError {
|
|
51
|
+
const candidate =
|
|
52
|
+
cause && typeof cause === 'object' && 'error' in cause
|
|
53
|
+
? cause.error
|
|
54
|
+
: cause;
|
|
55
|
+
if (
|
|
56
|
+
candidate &&
|
|
57
|
+
typeof candidate === 'object' &&
|
|
58
|
+
'code' in candidate &&
|
|
59
|
+
typeof candidate.code === 'string' &&
|
|
60
|
+
'message' in candidate &&
|
|
61
|
+
typeof candidate.message === 'string'
|
|
62
|
+
)
|
|
63
|
+
return candidate as ApiError;
|
|
64
|
+
return {
|
|
65
|
+
code: 'load_failed',
|
|
66
|
+
message:
|
|
67
|
+
cause instanceof Error
|
|
68
|
+
? cause.message
|
|
69
|
+
: 'The list could not be loaded.',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface Work {
|
|
74
|
+
promise: Promise<void>;
|
|
75
|
+
finish: () => void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function useIndex<T>(props: Options<T>) {
|
|
79
|
+
const route = inject(routeLocationKey, undefined);
|
|
80
|
+
const router = inject(routerKey, undefined);
|
|
81
|
+
const defaults = () =>
|
|
82
|
+
normalizeListQuery(
|
|
83
|
+
{
|
|
84
|
+
page: 1,
|
|
85
|
+
page_size: props.queryOptions.defaultPageSize ?? 20,
|
|
86
|
+
search: '',
|
|
87
|
+
sort: null,
|
|
88
|
+
filters: {},
|
|
89
|
+
},
|
|
90
|
+
props.queryOptions,
|
|
91
|
+
);
|
|
92
|
+
const query = shallowRef<ListQuery>(defaults());
|
|
93
|
+
const search = ref(query.value.search);
|
|
94
|
+
const rows = shallowRef<T[]>([]);
|
|
95
|
+
const meta = shallowRef({
|
|
96
|
+
page: query.value.page,
|
|
97
|
+
page_size: query.value.page_size,
|
|
98
|
+
total: 0,
|
|
99
|
+
});
|
|
100
|
+
const loading = ref(false);
|
|
101
|
+
const error = shallowRef<ApiError | null>(null);
|
|
102
|
+
const errors = computed(() => (error.value ? [error.value.message] : []));
|
|
103
|
+
const invalidQuery = ref(false);
|
|
104
|
+
let request = 0;
|
|
105
|
+
let navigation = 0;
|
|
106
|
+
let controller: AbortController | undefined;
|
|
107
|
+
let searchTimer: ReturnType<typeof setTimeout> | undefined;
|
|
108
|
+
let disposed = false;
|
|
109
|
+
let activeWork: Work | undefined;
|
|
110
|
+
let pendingUrl: { path: string; query: string; work: Work } | undefined;
|
|
111
|
+
|
|
112
|
+
function startWork(): Work {
|
|
113
|
+
activeWork?.finish();
|
|
114
|
+
cancelRequest();
|
|
115
|
+
let finish!: () => void;
|
|
116
|
+
const promise = new Promise<void>((resolve) => {
|
|
117
|
+
finish = resolve;
|
|
118
|
+
});
|
|
119
|
+
activeWork = { promise, finish };
|
|
120
|
+
return activeWork;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function fail(cause: unknown) {
|
|
124
|
+
error.value = apiError(cause);
|
|
125
|
+
invalidQuery.value = error.value.code === 'invalid_query';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function cancelSearch() {
|
|
129
|
+
clearTimeout(searchTimer);
|
|
130
|
+
searchTimer = undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function cancelRequest() {
|
|
134
|
+
request++;
|
|
135
|
+
controller?.abort();
|
|
136
|
+
loading.value = false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function fetchRows(work = startWork()): Promise<void> {
|
|
140
|
+
cancelRequest();
|
|
141
|
+
if (disposed || invalidQuery.value) {
|
|
142
|
+
work.finish();
|
|
143
|
+
return work.promise;
|
|
144
|
+
}
|
|
145
|
+
const current = request;
|
|
146
|
+
controller = new AbortController();
|
|
147
|
+
const { signal } = controller;
|
|
148
|
+
loading.value = true;
|
|
149
|
+
error.value = null;
|
|
150
|
+
void (async () => {
|
|
151
|
+
try {
|
|
152
|
+
const response = await props.load(cloneRecord(query.value), {
|
|
153
|
+
signal,
|
|
154
|
+
});
|
|
155
|
+
if (current !== request) return;
|
|
156
|
+
rows.value = response.data;
|
|
157
|
+
meta.value = response.meta;
|
|
158
|
+
} catch (cause) {
|
|
159
|
+
if (current === request) fail(cause);
|
|
160
|
+
} finally {
|
|
161
|
+
if (current === request) loading.value = false;
|
|
162
|
+
work.finish();
|
|
163
|
+
}
|
|
164
|
+
})();
|
|
165
|
+
return work.promise;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function accept(next: ListQuery, work: Work) {
|
|
169
|
+
query.value = next;
|
|
170
|
+
search.value = next.search;
|
|
171
|
+
invalidQuery.value = false;
|
|
172
|
+
return fetchRows(work);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const urlQuery = computed(() =>
|
|
176
|
+
props.syncQuery && route ? routeParams(route.query).toString() : '',
|
|
177
|
+
);
|
|
178
|
+
watch(
|
|
179
|
+
[
|
|
180
|
+
urlQuery,
|
|
181
|
+
() => (props.syncQuery ? route?.path : ''),
|
|
182
|
+
() => props.syncQuery,
|
|
183
|
+
() => props.queryOptions,
|
|
184
|
+
() => props.load,
|
|
185
|
+
],
|
|
186
|
+
() => {
|
|
187
|
+
const matchesPending =
|
|
188
|
+
pendingUrl?.path === route?.path &&
|
|
189
|
+
pendingUrl?.query === urlQuery.value;
|
|
190
|
+
const preserveSearch = searchTimer !== undefined && matchesPending;
|
|
191
|
+
const work = preserveSearch
|
|
192
|
+
? activeWork!
|
|
193
|
+
: matchesPending
|
|
194
|
+
? pendingUrl!.work
|
|
195
|
+
: startWork();
|
|
196
|
+
pendingUrl = undefined;
|
|
197
|
+
navigation++;
|
|
198
|
+
if (!preserveSearch) cancelSearch();
|
|
199
|
+
cancelRequest();
|
|
200
|
+
if (props.syncQuery && (!route || !router))
|
|
201
|
+
throw new Error(
|
|
202
|
+
'Index sync-query requires an installed Vue Router.',
|
|
203
|
+
);
|
|
204
|
+
try {
|
|
205
|
+
const next = props.syncQuery
|
|
206
|
+
? decodeListQuery(
|
|
207
|
+
new URLSearchParams(urlQuery.value),
|
|
208
|
+
props.queryOptions,
|
|
209
|
+
)
|
|
210
|
+
: normalizeListQuery(query.value, props.queryOptions);
|
|
211
|
+
if (preserveSearch) {
|
|
212
|
+
query.value = next;
|
|
213
|
+
invalidQuery.value = false;
|
|
214
|
+
loading.value = true;
|
|
215
|
+
} else void accept(next, work);
|
|
216
|
+
} catch (cause) {
|
|
217
|
+
fail(cause);
|
|
218
|
+
work.finish();
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
{ immediate: true, deep: true },
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
function commit(next: ListQuery, replace = false): Promise<void> {
|
|
225
|
+
if (disposed) return Promise.resolve();
|
|
226
|
+
cancelSearch();
|
|
227
|
+
const work = startWork();
|
|
228
|
+
let normalized: ListQuery;
|
|
229
|
+
try {
|
|
230
|
+
normalized = normalizeListQuery(next, props.queryOptions);
|
|
231
|
+
} catch (cause) {
|
|
232
|
+
fail(cause);
|
|
233
|
+
work.finish();
|
|
234
|
+
return work.promise;
|
|
235
|
+
}
|
|
236
|
+
if (!props.syncQuery || !route || !router)
|
|
237
|
+
return accept(normalized, work);
|
|
238
|
+
const values = { ...route.query };
|
|
239
|
+
for (const key of Object.keys(values))
|
|
240
|
+
if (isListParameter(key)) delete values[key];
|
|
241
|
+
for (const [key, value] of encodeListQuery(normalized))
|
|
242
|
+
values[key] = value;
|
|
243
|
+
query.value = normalized;
|
|
244
|
+
search.value = normalized.search;
|
|
245
|
+
const attempt = ++navigation;
|
|
246
|
+
const path = route.path;
|
|
247
|
+
pendingUrl = { path, query: routeParams(values).toString(), work };
|
|
248
|
+
loading.value = true;
|
|
249
|
+
void (async () => {
|
|
250
|
+
try {
|
|
251
|
+
const failure = await router[replace ? 'replace' : 'push']({
|
|
252
|
+
query: values,
|
|
253
|
+
hash: route.hash,
|
|
254
|
+
});
|
|
255
|
+
if (disposed || attempt !== navigation || route.path !== path)
|
|
256
|
+
return;
|
|
257
|
+
if (
|
|
258
|
+
isNavigationFailure(
|
|
259
|
+
failure,
|
|
260
|
+
NavigationFailureType.duplicated,
|
|
261
|
+
)
|
|
262
|
+
) {
|
|
263
|
+
pendingUrl = undefined;
|
|
264
|
+
if (searchTimer !== undefined) {
|
|
265
|
+
query.value = normalized;
|
|
266
|
+
invalidQuery.value = false;
|
|
267
|
+
} else void accept(normalized, work);
|
|
268
|
+
} else if (failure)
|
|
269
|
+
failNavigation(
|
|
270
|
+
new Error('The list URL could not be updated.'),
|
|
271
|
+
work,
|
|
272
|
+
);
|
|
273
|
+
} catch (cause) {
|
|
274
|
+
if (disposed || attempt !== navigation || route.path !== path)
|
|
275
|
+
return;
|
|
276
|
+
failNavigation(cause, work);
|
|
277
|
+
}
|
|
278
|
+
})();
|
|
279
|
+
return work.promise;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function failNavigation(cause: unknown, work: Work) {
|
|
283
|
+
pendingUrl = undefined;
|
|
284
|
+
const preserveSearch = searchTimer !== undefined;
|
|
285
|
+
cancelRequest();
|
|
286
|
+
try {
|
|
287
|
+
query.value = decodeListQuery(
|
|
288
|
+
routeParams(route!.query),
|
|
289
|
+
props.queryOptions,
|
|
290
|
+
);
|
|
291
|
+
if (!preserveSearch) search.value = query.value.search;
|
|
292
|
+
invalidQuery.value = false;
|
|
293
|
+
} catch {
|
|
294
|
+
invalidQuery.value = true;
|
|
295
|
+
}
|
|
296
|
+
error.value = apiError(cause);
|
|
297
|
+
loading.value = preserveSearch;
|
|
298
|
+
work.finish();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function updateCriteria(patch: Partial<Omit<ListQuery, 'page'>>) {
|
|
302
|
+
void commit({
|
|
303
|
+
...query.value,
|
|
304
|
+
search: search.value,
|
|
305
|
+
...patch,
|
|
306
|
+
page: 1,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function setSearch(value: string) {
|
|
311
|
+
if (disposed) return;
|
|
312
|
+
cancelSearch();
|
|
313
|
+
startWork();
|
|
314
|
+
search.value = value;
|
|
315
|
+
loading.value = true;
|
|
316
|
+
searchTimer = setTimeout(
|
|
317
|
+
() => {
|
|
318
|
+
void commit({ ...query.value, search: value, page: 1 }, true);
|
|
319
|
+
},
|
|
320
|
+
Math.max(0, props.searchDebounce),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function reload(): Promise<void> {
|
|
325
|
+
cancelSearch();
|
|
326
|
+
if (pendingUrl || search.value !== query.value.search)
|
|
327
|
+
return commit(
|
|
328
|
+
{
|
|
329
|
+
...query.value,
|
|
330
|
+
search: search.value,
|
|
331
|
+
page:
|
|
332
|
+
search.value !== query.value.search
|
|
333
|
+
? 1
|
|
334
|
+
: query.value.page,
|
|
335
|
+
},
|
|
336
|
+
true,
|
|
337
|
+
);
|
|
338
|
+
return fetchRows();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function reset(): Promise<void> {
|
|
342
|
+
return commit(defaults(), true);
|
|
343
|
+
}
|
|
344
|
+
function setPage(page: number) {
|
|
345
|
+
void commit({
|
|
346
|
+
...query.value,
|
|
347
|
+
search: search.value,
|
|
348
|
+
page: search.value !== query.value.search ? 1 : page,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
function setFilters(filters: ListQuery['filters']) {
|
|
352
|
+
updateCriteria({ filters: { ...filters } });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function setFilter(name: string, value: string | undefined) {
|
|
356
|
+
if (value !== undefined) {
|
|
357
|
+
setFilters({ ...query.value.filters, [name]: value });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const filters = { ...query.value.filters };
|
|
361
|
+
delete filters[name];
|
|
362
|
+
setFilters(filters);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
onScopeDispose(() => {
|
|
366
|
+
disposed = true;
|
|
367
|
+
navigation++;
|
|
368
|
+
cancelSearch();
|
|
369
|
+
cancelRequest();
|
|
370
|
+
activeWork?.finish();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
query,
|
|
375
|
+
search,
|
|
376
|
+
rows,
|
|
377
|
+
meta,
|
|
378
|
+
loading,
|
|
379
|
+
error,
|
|
380
|
+
errors,
|
|
381
|
+
invalidQuery,
|
|
382
|
+
updateCriteria,
|
|
383
|
+
setSearch,
|
|
384
|
+
setPage,
|
|
385
|
+
setFilters,
|
|
386
|
+
setFilter,
|
|
387
|
+
reload,
|
|
388
|
+
reset,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
@@ -66,7 +66,8 @@
|
|
|
66
66
|
align-items: center;
|
|
67
67
|
gap: var(--space-2xs);
|
|
68
68
|
min-width: 0;
|
|
69
|
-
padding: var(--
|
|
69
|
+
padding-block: var(--pui-table-cell-padding-block, var(--space-xs));
|
|
70
|
+
padding-inline: var(--space-s);
|
|
70
71
|
text-align: left;
|
|
71
72
|
|
|
72
73
|
&[data-head] {
|
|
@@ -93,8 +94,8 @@
|
|
|
93
94
|
|
|
94
95
|
/* ===== sort affordance ===== */
|
|
95
96
|
|
|
96
|
-
/*
|
|
97
|
-
|
|
97
|
+
/* Rendered as a <button>; the resets below also cover <a> so a
|
|
98
|
+
consumer-supplied link header behaves identically. */
|
|
98
99
|
.pui-table__sort {
|
|
99
100
|
display: inline-flex;
|
|
100
101
|
align-items: center;
|
|
@@ -167,7 +168,7 @@
|
|
|
167
168
|
.pui-table__loading {
|
|
168
169
|
position: absolute;
|
|
169
170
|
inset: 0;
|
|
170
|
-
z-index: 1;
|
|
171
|
+
z-index: var(--layer-1);
|
|
171
172
|
|
|
172
173
|
/* the tint gives the blur a surface to sit in; without it the frosting
|
|
173
174
|
reads as a smudge rather than a pane */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<nav aria-label="Breadcrumb">
|
|
3
|
-
<ol class="pui-
|
|
2
|
+
<nav aria-label="Breadcrumb" class="pui-breadcrumb">
|
|
3
|
+
<ol class="pui-breadcrumb__trail">
|
|
4
4
|
<template v-for="(item, index) in items" :key="index">
|
|
5
5
|
<li
|
|
6
6
|
v-if="index > 0"
|
|
@@ -13,15 +13,25 @@
|
|
|
13
13
|
v-if="index < items.length - 1"
|
|
14
14
|
class="pui-breadcrumb__item"
|
|
15
15
|
>
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
<slot
|
|
17
|
+
name="link"
|
|
18
|
+
:item="item"
|
|
19
|
+
:index="index"
|
|
20
|
+
link-class="pui-breadcrumb__link"
|
|
21
|
+
>
|
|
22
|
+
<a :href="item.href" class="pui-breadcrumb__link">{{
|
|
23
|
+
item.label
|
|
24
|
+
}}</a>
|
|
25
|
+
</slot>
|
|
19
26
|
</li>
|
|
20
27
|
<li v-else class="pui-breadcrumb__item" aria-current="page">
|
|
21
28
|
{{ item.label }}
|
|
22
29
|
</li>
|
|
23
30
|
</template>
|
|
24
31
|
</ol>
|
|
32
|
+
<div v-if="$slots.actions" class="pui-breadcrumb__actions">
|
|
33
|
+
<slot name="actions" />
|
|
34
|
+
</div>
|
|
25
35
|
</nav>
|
|
26
36
|
</template>
|
|
27
37
|
|
|
@@ -29,6 +39,15 @@
|
|
|
29
39
|
defineProps<{
|
|
30
40
|
items: { label: string; href?: string }[];
|
|
31
41
|
}>();
|
|
42
|
+
|
|
43
|
+
defineSlots<{
|
|
44
|
+
link?: (scope: {
|
|
45
|
+
item: { label: string; href?: string };
|
|
46
|
+
index: number;
|
|
47
|
+
linkClass: string;
|
|
48
|
+
}) => unknown;
|
|
49
|
+
actions?: () => unknown;
|
|
50
|
+
}>();
|
|
32
51
|
</script>
|
|
33
52
|
|
|
34
53
|
<style>
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
@layer components {
|
|
2
2
|
.pui-breadcrumb {
|
|
3
|
+
display: flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
justify-content: space-between;
|
|
6
|
+
gap: var(--space-sm);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.pui-breadcrumb__trail {
|
|
3
10
|
display: flex;
|
|
4
11
|
align-items: center;
|
|
5
12
|
gap: var(--space-2xs);
|
|
@@ -9,6 +16,14 @@
|
|
|
9
16
|
padding: 0;
|
|
10
17
|
margin: 0;
|
|
11
18
|
flex-wrap: wrap;
|
|
19
|
+
min-inline-size: 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.pui-breadcrumb__actions {
|
|
23
|
+
display: inline-flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
gap: var(--space-2xs);
|
|
26
|
+
flex: none;
|
|
12
27
|
}
|
|
13
28
|
|
|
14
29
|
.pui-breadcrumb__item {
|