@astronox/core 0.2.1 → 0.2.3
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/package.json +7 -1
- package/src/admin/cache.ts +217 -0
- package/src/admin/contracts.ts +28 -0
- package/src/admin/index.ts +3 -0
- package/src/admin/module.ts +28 -0
- package/src/auth/contracts.ts +53 -0
- package/src/auth/firebase/client.ts +83 -0
- package/src/auth/firebase/contracts.ts +8 -0
- package/src/auth/firebase/index.ts +1 -0
- package/src/auth/index.ts +2 -0
- package/src/auth/module.ts +99 -0
- package/src/client/formSubmission.ts +177 -0
- package/src/client/index.ts +4 -0
- package/src/client/preferences.ts +400 -0
- package/src/components/actions/Button/Button.astro +42 -6
- package/src/components/actions/Button/Button.css +19 -0
- package/src/components/actions/IdentityProviderAction/IdentityProviderAction.astro +35 -0
- package/src/components/actions/ScrollToTop/ScrollToTop.css +11 -0
- package/src/components/content/CardBody/CardBody.astro +3 -0
- package/src/components/content/CardBody/CardBody.css +16 -2
- package/src/components/content/CardFooter/CardFooter.astro +9 -0
- package/src/components/content/CardFooter/CardFooter.css +45 -3
- package/src/components/content/CardLink/CardLink.astro +12 -3
- package/src/components/content/CardLink/CardLink.css +32 -0
- package/src/components/content/MarkdownContent/MarkdownContent.css +14 -4
- package/src/components/content/MarkdownContent/MarkdownInline.astro +1 -1
- package/src/components/content/MarkdownContent/contracts.ts +1 -1
- package/src/components/content/MarkdownContent/markdownContentRenderer.ts +552 -0
- package/src/components/data-display/TextPair/TextPair.css +8 -2
- package/src/components/feedback/PreferenceBootstrap/PreferenceBootstrap.astro +403 -0
- package/src/components/forms/Checkbox/Checkbox.astro +6 -0
- package/src/components/forms/ChoiceGroup/ChoiceGroup.astro +57 -0
- package/src/components/forms/ChoiceGroup/ChoiceGroup.css +108 -0
- package/src/components/forms/ChoiceGroup/contracts.ts +9 -0
- package/src/components/forms/DateTimeInput/DateTimeInput.astro +14 -0
- package/src/components/forms/PreparedFormBootstrap/PreparedFormBootstrap.astro +203 -0
- package/src/components/forms/PreparedFormBootstrap/contracts.ts +15 -0
- package/src/components/forms/TextInput/TextInput.astro +7 -1
- package/src/components/forms/Textarea/Textarea.astro +35 -0
- package/src/components/forms/Textarea/Textarea.css +62 -0
- package/src/components/icons/BackIcon.astro +26 -0
- package/src/components/icons/DeleteIcon.astro +22 -0
- package/src/components/icons/StatusIndicatorIcon.astro +16 -0
- package/src/components/index.ts +18 -0
- package/src/components/media/CanvasFrame/CanvasFrame.astro +1 -17
- package/src/components/media/EmbedFrame/EmbedFrame.astro +1 -19
- package/src/components/media/EmbedFrame/EmbedFrame.css +0 -2
- package/src/components/navigation/Breadcrumbs/Breadcrumbs.astro +5 -1
- package/src/components/navigation/Breadcrumbs/Breadcrumbs.css +12 -17
- package/src/components/navigation/Footer/Footer.astro +102 -0
- package/src/components/navigation/Footer/Footer.css +155 -0
- package/src/components/navigation/Footer/contracts.ts +17 -0
- package/src/components/navigation/SubNavigation/SubNavigation.css +0 -5
- package/src/composition/Grid/Grid.css +16 -0
- package/src/composition/SurfaceGroup/SurfaceGroup.css +21 -7
- package/src/composition/SurfaceGroup/stickySurfaceGroupController.ts +3 -0
- package/src/data/firestore/client/contracts.ts +78 -0
- package/src/data/firestore/client/firebase.ts +56 -0
- package/src/data/firestore/client/index.ts +3 -0
- package/src/data/firestore/client/repository.ts +111 -0
- package/src/display/contracts.ts +44 -1
- package/src/display/index.ts +1 -0
- package/src/display/tableProgressive.ts +62 -0
- package/src/layouts/BaseLayout.astro +8 -1
- package/src/patterns/Combobox/Combobox.astro +23 -0
- package/src/patterns/Combobox/comboboxController.ts +69 -0
- package/src/patterns/Fullscreen/FullscreenControl.astro +4 -14
- package/src/patterns/Fullscreen/FullscreenControl.css +0 -26
- package/src/patterns/Modal/Modal.astro +1 -1
- package/src/patterns/Modal/Modal.css +38 -1
- package/src/patterns/Table/Table.astro +122 -7
- package/src/patterns/Table/TableBody.astro +8 -0
- package/src/patterns/Table/TableBody.css +3 -3
- package/src/patterns/Table/TableCell.css +11 -1
- package/src/patterns/Table/TableCellValue.astro +31 -0
- package/src/patterns/Table/TableFooter.astro +3 -1
- package/src/patterns/Table/TableHeader.astro +1 -0
- package/src/patterns/Table/TablePagination.astro +63 -47
- package/src/patterns/Table/TableSkeleton.astro +18 -22
- package/src/patterns/Table/TableSkeleton.css +30 -5
- package/src/patterns/Table/tableClientController.ts +253 -0
- package/src/patterns/Table/tableFilterController.ts +23 -31
- package/src/patterns/Table/tableProgressiveController.ts +615 -227
- package/src/patterns/Table/tableSortController.ts +25 -7
- package/src/styles/base.css +24 -7
- package/src/styles/interaction.css +16 -0
- package/src/styles/style.css +3 -0
- package/src/styles/tokens.css +5 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addDoc,
|
|
3
|
+
collection,
|
|
4
|
+
deleteDoc,
|
|
5
|
+
doc,
|
|
6
|
+
documentId,
|
|
7
|
+
getDoc,
|
|
8
|
+
getDocs,
|
|
9
|
+
limit as limitQuery,
|
|
10
|
+
orderBy as orderByQuery,
|
|
11
|
+
query,
|
|
12
|
+
startAfter,
|
|
13
|
+
updateDoc,
|
|
14
|
+
type QueryConstraint,
|
|
15
|
+
} from 'firebase/firestore';
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
ClientFirestoreRepository,
|
|
19
|
+
ClientFirestoreRepositoryConfig,
|
|
20
|
+
} from './contracts';
|
|
21
|
+
|
|
22
|
+
const defaultPageSize = 25;
|
|
23
|
+
const maximumPageSize = 100;
|
|
24
|
+
|
|
25
|
+
export function createClientFirestoreRepository<
|
|
26
|
+
TDocument,
|
|
27
|
+
TItem,
|
|
28
|
+
TCreateInput,
|
|
29
|
+
TUpdateInput = TCreateInput,
|
|
30
|
+
>({
|
|
31
|
+
collection: collectionName,
|
|
32
|
+
cursor: cursorConfig,
|
|
33
|
+
database,
|
|
34
|
+
mapper,
|
|
35
|
+
orderBy = [],
|
|
36
|
+
schema,
|
|
37
|
+
}: ClientFirestoreRepositoryConfig<
|
|
38
|
+
TDocument,
|
|
39
|
+
TItem,
|
|
40
|
+
TCreateInput,
|
|
41
|
+
TUpdateInput
|
|
42
|
+
>): ClientFirestoreRepository<TItem, TCreateInput, TUpdateInput> {
|
|
43
|
+
const documents = collection(database, collectionName);
|
|
44
|
+
const read = (id: string, value: unknown): TItem =>
|
|
45
|
+
mapper.fromDocument(id, schema.parse(value));
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
async getIndex({ cursor, limit = defaultPageSize } = {}) {
|
|
49
|
+
const pageSize = Math.min(
|
|
50
|
+
maximumPageSize,
|
|
51
|
+
Math.max(1, Math.floor(limit)),
|
|
52
|
+
);
|
|
53
|
+
const constraints: QueryConstraint[] = orderBy.map((order) =>
|
|
54
|
+
orderByQuery(order.field, order.direction ?? 'asc'),
|
|
55
|
+
);
|
|
56
|
+
constraints.push(orderByQuery(documentId(), 'asc'));
|
|
57
|
+
|
|
58
|
+
if (cursor && cursorConfig) {
|
|
59
|
+
constraints.push(startAfter(...cursorConfig.toQueryValues(cursor)));
|
|
60
|
+
} else if (cursor) {
|
|
61
|
+
const cursorDocument = await getDoc(doc(documents, cursor));
|
|
62
|
+
if (cursorDocument.exists()) {
|
|
63
|
+
constraints.push(startAfter(cursorDocument));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
constraints.push(limitQuery(pageSize));
|
|
67
|
+
|
|
68
|
+
const snapshot = await getDocs(query(documents, ...constraints));
|
|
69
|
+
const results = snapshot.docs.map((document) => ({
|
|
70
|
+
document: schema.parse(document.data()),
|
|
71
|
+
id: document.id,
|
|
72
|
+
}));
|
|
73
|
+
const items = results.map(({ document, id }) =>
|
|
74
|
+
mapper.fromDocument(id, document),
|
|
75
|
+
);
|
|
76
|
+
const lastResult = results.at(-1);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
items,
|
|
80
|
+
nextCursor:
|
|
81
|
+
snapshot.size === pageSize && lastResult
|
|
82
|
+
? cursorConfig
|
|
83
|
+
? cursorConfig.serialize(lastResult)
|
|
84
|
+
: lastResult.id
|
|
85
|
+
: undefined,
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
async getDetail({ id }) {
|
|
90
|
+
const snapshot = await getDoc(doc(documents, id));
|
|
91
|
+
return snapshot.exists() ? read(snapshot.id, snapshot.data()) : null;
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
async create(input) {
|
|
95
|
+
const reference = await addDoc(documents, mapper.toCreate(input));
|
|
96
|
+
const snapshot = await getDoc(reference);
|
|
97
|
+
return read(snapshot.id, snapshot.data());
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
async update({ id, value }) {
|
|
101
|
+
const reference = doc(documents, id);
|
|
102
|
+
await updateDoc(reference, mapper.toUpdate(value));
|
|
103
|
+
const snapshot = await getDoc(reference);
|
|
104
|
+
return read(snapshot.id, snapshot.data());
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
async delete({ id }) {
|
|
108
|
+
await deleteDoc(doc(documents, id));
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
package/src/display/contracts.ts
CHANGED
|
@@ -104,10 +104,13 @@ type DisplayCardLinkContent = {
|
|
|
104
104
|
compactContent?: 'text' | 'media';
|
|
105
105
|
compactText?: DisplayTextValue;
|
|
106
106
|
compactVisual?: 'show' | 'hide';
|
|
107
|
+
density?: 'default' | 'relaxed';
|
|
107
108
|
iconTone?: Tone;
|
|
108
|
-
label
|
|
109
|
+
label?: string;
|
|
109
110
|
title: string;
|
|
110
111
|
meta?: DisplayTextValue;
|
|
112
|
+
textLayout?: TextPairLayout;
|
|
113
|
+
textTreatment?: 'default' | 'heading';
|
|
111
114
|
media?: DisplayMedia;
|
|
112
115
|
mediaPosition?: 'start' | 'end';
|
|
113
116
|
mediaSize?: 'default' | 'compact' | 'small';
|
|
@@ -190,11 +193,33 @@ export type TableColumn = {
|
|
|
190
193
|
sizing?: 'content' | 'fill';
|
|
191
194
|
wrap?: boolean;
|
|
192
195
|
sort?: {
|
|
196
|
+
firstDirection?: 'ascending' | 'descending';
|
|
193
197
|
type: 'number' | 'text';
|
|
194
198
|
tieBreaker?: string;
|
|
195
199
|
};
|
|
196
200
|
};
|
|
197
201
|
|
|
202
|
+
type TableActionBase = {
|
|
203
|
+
borderless?: boolean;
|
|
204
|
+
hoverTone?: Tone;
|
|
205
|
+
icon: AstroComponent;
|
|
206
|
+
iconTone?: Tone;
|
|
207
|
+
label: string;
|
|
208
|
+
tone?: Tone;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export type TableAction = TableActionBase &
|
|
212
|
+
(
|
|
213
|
+
| {
|
|
214
|
+
href: string;
|
|
215
|
+
modalTargetId?: never;
|
|
216
|
+
}
|
|
217
|
+
| {
|
|
218
|
+
href?: never;
|
|
219
|
+
modalTargetId: string;
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
|
|
198
223
|
export type TableCell = {
|
|
199
224
|
tooltip?: string;
|
|
200
225
|
} & (
|
|
@@ -238,6 +263,10 @@ export type TableCell = {
|
|
|
238
263
|
kind: 'tags';
|
|
239
264
|
tags: readonly DisplayTag[];
|
|
240
265
|
}
|
|
266
|
+
| {
|
|
267
|
+
kind: 'actions';
|
|
268
|
+
actions: readonly TableAction[];
|
|
269
|
+
}
|
|
241
270
|
);
|
|
242
271
|
|
|
243
272
|
export type TableRow = {
|
|
@@ -256,6 +285,20 @@ export type TablePaginationModel = {
|
|
|
256
285
|
nextHref?: string;
|
|
257
286
|
};
|
|
258
287
|
|
|
288
|
+
export type TableProgressiveEntry = {
|
|
289
|
+
key: string;
|
|
290
|
+
source?: string;
|
|
291
|
+
filters?: TableRow['filters'];
|
|
292
|
+
sortValues?: TableRow['sortValues'];
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
export type TableProgressiveModel = {
|
|
296
|
+
entries: readonly TableProgressiveEntry[];
|
|
297
|
+
sources?: Readonly<Record<string, string>>;
|
|
298
|
+
batchSize?: number;
|
|
299
|
+
initialOffset?: number;
|
|
300
|
+
};
|
|
301
|
+
|
|
259
302
|
type NavigationItemBase = {
|
|
260
303
|
active?: boolean;
|
|
261
304
|
id?: string;
|
package/src/display/index.ts
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TablePaginationModel,
|
|
3
|
+
TableProgressiveModel,
|
|
4
|
+
TableRow,
|
|
5
|
+
} from './contracts';
|
|
6
|
+
|
|
7
|
+
export const tableProgressiveBatchSize = 25;
|
|
8
|
+
|
|
9
|
+
export type PagedTablePresentation = {
|
|
10
|
+
pagination: TablePaginationModel;
|
|
11
|
+
progressive: TableProgressiveModel;
|
|
12
|
+
rows: readonly TableRow[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function createPagedTablePresentation(
|
|
16
|
+
allRows: readonly TableRow[],
|
|
17
|
+
page: number,
|
|
18
|
+
getPageHref: (page: number) => string,
|
|
19
|
+
batchSize: number = tableProgressiveBatchSize,
|
|
20
|
+
): PagedTablePresentation {
|
|
21
|
+
const resolvedBatchSize = Math.max(1, Math.floor(batchSize));
|
|
22
|
+
const totalPages = Math.max(1, Math.ceil(allRows.length / resolvedBatchSize));
|
|
23
|
+
if (!Number.isInteger(page) || page < 1 || page > totalPages) {
|
|
24
|
+
throw new RangeError(`Invalid table page: ${page}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const normalizedRows = allRows.map((row, index): TableRow => ({
|
|
28
|
+
...row,
|
|
29
|
+
key: row.key ?? `table-row-${index + 1}`,
|
|
30
|
+
}));
|
|
31
|
+
const keys = new Set(normalizedRows.map((row) => row.key));
|
|
32
|
+
if (keys.size !== normalizedRows.length) {
|
|
33
|
+
throw new Error('Progressive table rows require unique keys');
|
|
34
|
+
}
|
|
35
|
+
const sources = Object.fromEntries(
|
|
36
|
+
Array.from({ length: totalPages }, (_, index) => {
|
|
37
|
+
const sourcePage = index + 1;
|
|
38
|
+
return [String(sourcePage), getPageHref(sourcePage)];
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
const start = (page - 1) * resolvedBatchSize;
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
pagination: {
|
|
45
|
+
page,
|
|
46
|
+
totalPages,
|
|
47
|
+
nextHref: page < totalPages ? getPageHref(page + 1) : undefined,
|
|
48
|
+
},
|
|
49
|
+
progressive: {
|
|
50
|
+
batchSize: resolvedBatchSize,
|
|
51
|
+
entries: normalizedRows.map((row, index) => ({
|
|
52
|
+
key: row.key!,
|
|
53
|
+
source: String(Math.floor(index / resolvedBatchSize) + 1),
|
|
54
|
+
filters: row.filters,
|
|
55
|
+
sortValues: row.sortValues,
|
|
56
|
+
})),
|
|
57
|
+
initialOffset: start,
|
|
58
|
+
sources,
|
|
59
|
+
},
|
|
60
|
+
rows: normalizedRows.slice(start, start + resolvedBatchSize),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -16,6 +16,7 @@ type Props = HTMLAttributes<'html'> & {
|
|
|
16
16
|
currentPath?: string;
|
|
17
17
|
breadcrumbs?: readonly BreadcrumbItem[];
|
|
18
18
|
breadcrumbLabel: string;
|
|
19
|
+
breadcrumbTone?: Tone;
|
|
19
20
|
scrollToTopLabel?: string;
|
|
20
21
|
structuredData?: readonly JsonLdNode[];
|
|
21
22
|
tone?: Tone;
|
|
@@ -23,6 +24,7 @@ type Props = HTMLAttributes<'html'> & {
|
|
|
23
24
|
|
|
24
25
|
const {
|
|
25
26
|
breadcrumbLabel,
|
|
27
|
+
breadcrumbTone,
|
|
26
28
|
breadcrumbs,
|
|
27
29
|
currentPath = Astro.url.pathname,
|
|
28
30
|
'data-tone': dataTone,
|
|
@@ -59,7 +61,11 @@ const resolvedTone = tone ?? dataTone;
|
|
|
59
61
|
<div class="site-main__content">
|
|
60
62
|
{
|
|
61
63
|
breadcrumbs?.length ? (
|
|
62
|
-
<Breadcrumbs
|
|
64
|
+
<Breadcrumbs
|
|
65
|
+
items={breadcrumbs}
|
|
66
|
+
label={breadcrumbLabel}
|
|
67
|
+
tone={breadcrumbTone}
|
|
68
|
+
/>
|
|
63
69
|
) : null
|
|
64
70
|
}
|
|
65
71
|
|
|
@@ -68,6 +74,7 @@ const resolvedTone = tone ?? dataTone;
|
|
|
68
74
|
</PageSurface>
|
|
69
75
|
</div>
|
|
70
76
|
</main>
|
|
77
|
+
<slot name="footer" />
|
|
71
78
|
</div>
|
|
72
79
|
{scrollToTopLabel ? <ScrollToTop label={scrollToTopLabel} /> : null}
|
|
73
80
|
</body>
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { HTMLAttributes } from 'astro/types';
|
|
3
|
+
import type {
|
|
4
|
+
PreferenceBinding,
|
|
5
|
+
PreferenceNumberBounds,
|
|
6
|
+
} from '../../client/preferences';
|
|
3
7
|
import type { Tone } from '../../theme/contracts';
|
|
4
8
|
import type {
|
|
5
9
|
ComboboxOption,
|
|
@@ -15,6 +19,7 @@ type SharedProps = Omit<HTMLAttributes<'div'>, 'id'> & {
|
|
|
15
19
|
id: string;
|
|
16
20
|
label: string;
|
|
17
21
|
disabled?: boolean;
|
|
22
|
+
preference?: PreferenceBinding;
|
|
18
23
|
size?: ComboboxSize;
|
|
19
24
|
tone?: Tone;
|
|
20
25
|
width?: 'auto' | 'fill';
|
|
@@ -41,6 +46,7 @@ type NumberProps = SharedProps & {
|
|
|
41
46
|
value: number;
|
|
42
47
|
decrementLabel: string;
|
|
43
48
|
incrementLabel: string;
|
|
49
|
+
preferenceBounds?: PreferenceNumberBounds;
|
|
44
50
|
};
|
|
45
51
|
|
|
46
52
|
type Props = OptionsProps | NumberProps;
|
|
@@ -130,6 +136,8 @@ if (mode === 'number') {
|
|
|
130
136
|
'max',
|
|
131
137
|
'min',
|
|
132
138
|
'mode',
|
|
139
|
+
'preference',
|
|
140
|
+
'preferenceBounds',
|
|
133
141
|
'size',
|
|
134
142
|
'step',
|
|
135
143
|
'tone',
|
|
@@ -185,6 +193,7 @@ if (mode === 'number') {
|
|
|
185
193
|
'mode',
|
|
186
194
|
'options',
|
|
187
195
|
'placeholder',
|
|
196
|
+
'preference',
|
|
188
197
|
'removeLabel',
|
|
189
198
|
'searchPlaceholder',
|
|
190
199
|
'selectionMode',
|
|
@@ -233,6 +242,20 @@ const searchId = `${props.id}-search`;
|
|
|
233
242
|
data-combobox-number-step={step}
|
|
234
243
|
data-disabled={disabled ? true : undefined}
|
|
235
244
|
data-tone={resolvedTone}
|
|
245
|
+
data-preference-id={props.preference?.id}
|
|
246
|
+
data-preference-path={props.preference?.path}
|
|
247
|
+
data-preference-control={
|
|
248
|
+
props.preference
|
|
249
|
+
? mode === 'number'
|
|
250
|
+
? 'combobox-number'
|
|
251
|
+
: 'combobox-options'
|
|
252
|
+
: undefined
|
|
253
|
+
}
|
|
254
|
+
data-preference-number-bounds={
|
|
255
|
+
mode === 'number' && (props as NumberProps).preferenceBounds
|
|
256
|
+
? JSON.stringify((props as NumberProps).preferenceBounds)
|
|
257
|
+
: undefined
|
|
258
|
+
}
|
|
236
259
|
>
|
|
237
260
|
<span class="combobox__sizer" aria-hidden="true">
|
|
238
261
|
{
|
|
@@ -17,6 +17,13 @@ export type ComboboxNumberBounds = {
|
|
|
17
17
|
max: number;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
export type DynamicComboboxOption = {
|
|
21
|
+
description?: string;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
label: string;
|
|
24
|
+
value: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
20
27
|
type NumberBounds = {
|
|
21
28
|
min: number;
|
|
22
29
|
max: number;
|
|
@@ -159,6 +166,68 @@ export function clearCombobox(
|
|
|
159
166
|
root.dispatchEvent(new Event('combobox:clear'));
|
|
160
167
|
}
|
|
161
168
|
|
|
169
|
+
export function setComboboxOptions(
|
|
170
|
+
root: HTMLElement,
|
|
171
|
+
options: readonly DynamicComboboxOption[],
|
|
172
|
+
selectedValues: readonly string[] = [],
|
|
173
|
+
): void {
|
|
174
|
+
if (root.dataset.comboboxMode === 'number') return;
|
|
175
|
+
const optionsRoot = root.querySelector<HTMLElement>('[data-combobox-options]');
|
|
176
|
+
const empty = root.querySelector<HTMLElement>('[data-combobox-empty]');
|
|
177
|
+
if (!optionsRoot || !empty) return;
|
|
178
|
+
|
|
179
|
+
optionsRoot.querySelectorAll('[data-combobox-option]').forEach((node) => node.remove());
|
|
180
|
+
root.querySelectorAll('template[data-combobox-tag-template]').forEach((node) => node.remove());
|
|
181
|
+
|
|
182
|
+
options.forEach((option, index) => {
|
|
183
|
+
const button = document.createElement('button');
|
|
184
|
+
button.className = 'combobox__option';
|
|
185
|
+
button.id = `${root.id}-option-${index}`;
|
|
186
|
+
button.type = 'button';
|
|
187
|
+
button.role = 'option';
|
|
188
|
+
button.tabIndex = -1;
|
|
189
|
+
button.disabled = option.disabled === true;
|
|
190
|
+
button.setAttribute('aria-selected', 'false');
|
|
191
|
+
if (option.disabled) button.setAttribute('aria-disabled', 'true');
|
|
192
|
+
button.dataset.comboboxOption = option.value;
|
|
193
|
+
button.dataset.comboboxSearchText = [option.label, option.description ?? ''].join(' ');
|
|
194
|
+
button.dataset.interactionAccent = 'start';
|
|
195
|
+
|
|
196
|
+
const content = document.createElement('span');
|
|
197
|
+
content.className = 'combobox__option-content';
|
|
198
|
+
const label = document.createElement('span');
|
|
199
|
+
label.className = 'combobox__option-label';
|
|
200
|
+
label.textContent = option.label;
|
|
201
|
+
content.append(label);
|
|
202
|
+
if (option.description) {
|
|
203
|
+
const description = document.createElement('span');
|
|
204
|
+
description.className = 'combobox__option-description';
|
|
205
|
+
description.textContent = option.description;
|
|
206
|
+
content.append(description);
|
|
207
|
+
}
|
|
208
|
+
button.append(content);
|
|
209
|
+
optionsRoot.insertBefore(button, empty);
|
|
210
|
+
|
|
211
|
+
const template = document.createElement('template');
|
|
212
|
+
template.dataset.comboboxTagTemplate = option.value;
|
|
213
|
+
const tag = document.createElement('span');
|
|
214
|
+
tag.className = 'tag';
|
|
215
|
+
tag.dataset.widget = 'tag';
|
|
216
|
+
tag.dataset.size = 'sm';
|
|
217
|
+
tag.dataset.comboboxTag = option.value;
|
|
218
|
+
const tagLabel = document.createElement('span');
|
|
219
|
+
tagLabel.className = 'tag__label';
|
|
220
|
+
tagLabel.dataset.tagLabel = '';
|
|
221
|
+
tagLabel.textContent = option.label;
|
|
222
|
+
tag.append(tagLabel);
|
|
223
|
+
template.content.append(tag);
|
|
224
|
+
root.append(template);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
empty.hidden = options.length > 0;
|
|
228
|
+
setComboboxValues(root, selectedValues);
|
|
229
|
+
}
|
|
230
|
+
|
|
162
231
|
export function setComboboxNumberValue(
|
|
163
232
|
root: HTMLElement,
|
|
164
233
|
requestedValue: number,
|
|
@@ -6,8 +6,6 @@ import EnterFullscreenIcon from '../../components/icons/EnterFullscreenIcon.astr
|
|
|
6
6
|
import ExitFullscreenIcon from '../../components/icons/ExitFullscreenIcon.astro';
|
|
7
7
|
import ExternalLinkIcon from '../../components/icons/ExternalLinkIcon.astro';
|
|
8
8
|
|
|
9
|
-
type FullscreenControlPlacement = 'inline' | 'overlay';
|
|
10
|
-
type FullscreenOverlayOffset = 'default' | 'native-controls';
|
|
11
9
|
type FullscreenUnsupportedFallback = {
|
|
12
10
|
href: string;
|
|
13
11
|
label: string;
|
|
@@ -17,8 +15,6 @@ type FullscreenUnsupportedFallback = {
|
|
|
17
15
|
type Props = Omit<HTMLAttributes<'button'>, 'aria-controls' | 'type'> & {
|
|
18
16
|
enterLabel: string;
|
|
19
17
|
exitLabel: string;
|
|
20
|
-
overlayOffset?: FullscreenOverlayOffset;
|
|
21
|
-
placement?: FullscreenControlPlacement;
|
|
22
18
|
targetId: string;
|
|
23
19
|
tone?: Tone;
|
|
24
20
|
unsupportedFallback?: FullscreenUnsupportedFallback;
|
|
@@ -28,8 +24,6 @@ const {
|
|
|
28
24
|
class: className,
|
|
29
25
|
enterLabel,
|
|
30
26
|
exitLabel,
|
|
31
|
-
overlayOffset = 'default',
|
|
32
|
-
placement = 'inline',
|
|
33
27
|
targetId,
|
|
34
28
|
tone,
|
|
35
29
|
unsupportedFallback,
|
|
@@ -44,10 +38,9 @@ const resolvedClass = ['fullscreen-control', className]
|
|
|
44
38
|
{...attrs}
|
|
45
39
|
class={resolvedClass}
|
|
46
40
|
type="button"
|
|
47
|
-
borderless
|
|
48
|
-
full
|
|
41
|
+
borderless
|
|
42
|
+
full
|
|
49
43
|
iconOnly
|
|
50
|
-
size={placement === 'overlay' ? 'md' : undefined}
|
|
51
44
|
tone={tone}
|
|
52
45
|
aria-controls={targetId}
|
|
53
46
|
aria-label={enterLabel}
|
|
@@ -56,8 +49,6 @@ const resolvedClass = ['fullscreen-control', className]
|
|
|
56
49
|
data-fullscreen-control
|
|
57
50
|
data-fullscreen-enter-label={enterLabel}
|
|
58
51
|
data-fullscreen-exit-label={exitLabel}
|
|
59
|
-
data-fullscreen-overlay-offset={overlayOffset}
|
|
60
|
-
data-fullscreen-placement={placement}
|
|
61
52
|
data-fullscreen-target={targetId}
|
|
62
53
|
>
|
|
63
54
|
<span class="fullscreen-control__icon" data-fullscreen-enter-icon>
|
|
@@ -73,10 +64,9 @@ const resolvedClass = ['fullscreen-control', className]
|
|
|
73
64
|
<Button
|
|
74
65
|
href={unsupportedFallback.href}
|
|
75
66
|
target={unsupportedFallback.target ?? '_blank'}
|
|
76
|
-
borderless
|
|
77
|
-
full
|
|
67
|
+
borderless
|
|
68
|
+
full
|
|
78
69
|
iconOnly
|
|
79
|
-
size={placement === 'overlay' ? 'md' : undefined}
|
|
80
70
|
tone={tone}
|
|
81
71
|
aria-label={unsupportedFallback.label}
|
|
82
72
|
title={unsupportedFallback.label}
|
|
@@ -15,32 +15,6 @@
|
|
|
15
15
|
display: grid;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
.fullscreen-control[data-fullscreen-placement='overlay'].nox-button {
|
|
19
|
-
position: absolute;
|
|
20
|
-
z-index: 2;
|
|
21
|
-
display: none;
|
|
22
|
-
inset-inline-end: var(--nox-space-3);
|
|
23
|
-
inset-block-end: var(--nox-space-3);
|
|
24
|
-
inline-size: var(--nox-section-responsive-block-size);
|
|
25
|
-
block-size: var(--nox-section-responsive-block-size);
|
|
26
|
-
min-block-size: 0;
|
|
27
|
-
border-radius: var(--nox-radius-md);
|
|
28
|
-
box-shadow: var(--nox-shadow-surface);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
[data-fullscreen-region]:fullscreen
|
|
32
|
-
.fullscreen-control[data-fullscreen-placement='overlay'].nox-button {
|
|
33
|
-
display: inline-flex;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
[data-fullscreen-region]:fullscreen
|
|
37
|
-
.fullscreen-control[data-fullscreen-placement='overlay'][data-fullscreen-overlay-offset='native-controls'].nox-button {
|
|
38
|
-
inset-block-end: calc(
|
|
39
|
-
var(--nox-fullscreen-native-controls-block-size, 2.5rem) +
|
|
40
|
-
var(--nox-space-3)
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
18
|
[data-fullscreen-region]:fullscreen {
|
|
45
19
|
inline-size: 100vw;
|
|
46
20
|
block-size: 100vh;
|
|
@@ -4,7 +4,7 @@ import type { Tone } from '../../theme/contracts';
|
|
|
4
4
|
import ModalClose from './ModalClose.astro';
|
|
5
5
|
|
|
6
6
|
type ModalCloseControl = 'default' | 'delegated';
|
|
7
|
-
type ModalSizing = 'content' | 'viewport';
|
|
7
|
+
type ModalSizing = 'adaptive' | 'confirmation' | 'content' | 'viewport';
|
|
8
8
|
|
|
9
9
|
type Props = HTMLAttributes<'dialog'> & {
|
|
10
10
|
id: string;
|
|
@@ -48,6 +48,33 @@ html:has(.modal[open]) [data-site-viewport] {
|
|
|
48
48
|
overflow: auto;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
.modal[data-sizing='confirmation'] .modal__content {
|
|
52
|
+
inline-size: min(42rem, 100%);
|
|
53
|
+
block-size: auto;
|
|
54
|
+
max-block-size: 100%;
|
|
55
|
+
align-self: center;
|
|
56
|
+
overflow: auto;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.modal[data-sizing='adaptive'] .modal__content {
|
|
60
|
+
display: flex;
|
|
61
|
+
block-size: 100%;
|
|
62
|
+
min-block-size: 0;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
justify-content: center;
|
|
65
|
+
overflow: hidden;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.modal[data-sizing='adaptive']
|
|
69
|
+
.modal__content
|
|
70
|
+
> [data-composition='surface-group'] {
|
|
71
|
+
inline-size: 100%;
|
|
72
|
+
min-block-size: 0;
|
|
73
|
+
max-block-size: 100%;
|
|
74
|
+
flex: 0 1 auto;
|
|
75
|
+
overflow: hidden;
|
|
76
|
+
}
|
|
77
|
+
|
|
51
78
|
.modal__default-close {
|
|
52
79
|
position: absolute;
|
|
53
80
|
z-index: 1;
|
|
@@ -81,7 +108,17 @@ html:has(.modal[open]) [data-site-viewport] {
|
|
|
81
108
|
padding-inline: 0;
|
|
82
109
|
}
|
|
83
110
|
|
|
84
|
-
.
|
|
111
|
+
.modal:not(
|
|
112
|
+
[data-sizing='adaptive'],
|
|
113
|
+
[data-sizing='confirmation']
|
|
114
|
+
)
|
|
115
|
+
.modal__content
|
|
116
|
+
[data-surface] {
|
|
85
117
|
border-radius: 0;
|
|
86
118
|
}
|
|
119
|
+
|
|
120
|
+
.modal:is([data-sizing='adaptive'], [data-sizing='confirmation'])
|
|
121
|
+
.modal__content {
|
|
122
|
+
padding: var(--nox-space-2);
|
|
123
|
+
}
|
|
87
124
|
}
|