@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.
Files changed (88) hide show
  1. package/package.json +7 -1
  2. package/src/admin/cache.ts +217 -0
  3. package/src/admin/contracts.ts +28 -0
  4. package/src/admin/index.ts +3 -0
  5. package/src/admin/module.ts +28 -0
  6. package/src/auth/contracts.ts +53 -0
  7. package/src/auth/firebase/client.ts +83 -0
  8. package/src/auth/firebase/contracts.ts +8 -0
  9. package/src/auth/firebase/index.ts +1 -0
  10. package/src/auth/index.ts +2 -0
  11. package/src/auth/module.ts +99 -0
  12. package/src/client/formSubmission.ts +177 -0
  13. package/src/client/index.ts +4 -0
  14. package/src/client/preferences.ts +400 -0
  15. package/src/components/actions/Button/Button.astro +42 -6
  16. package/src/components/actions/Button/Button.css +19 -0
  17. package/src/components/actions/IdentityProviderAction/IdentityProviderAction.astro +35 -0
  18. package/src/components/actions/ScrollToTop/ScrollToTop.css +11 -0
  19. package/src/components/content/CardBody/CardBody.astro +3 -0
  20. package/src/components/content/CardBody/CardBody.css +16 -2
  21. package/src/components/content/CardFooter/CardFooter.astro +9 -0
  22. package/src/components/content/CardFooter/CardFooter.css +45 -3
  23. package/src/components/content/CardLink/CardLink.astro +12 -3
  24. package/src/components/content/CardLink/CardLink.css +32 -0
  25. package/src/components/content/MarkdownContent/MarkdownContent.css +14 -4
  26. package/src/components/content/MarkdownContent/MarkdownInline.astro +1 -1
  27. package/src/components/content/MarkdownContent/contracts.ts +1 -1
  28. package/src/components/content/MarkdownContent/markdownContentRenderer.ts +552 -0
  29. package/src/components/data-display/TextPair/TextPair.css +8 -2
  30. package/src/components/feedback/PreferenceBootstrap/PreferenceBootstrap.astro +403 -0
  31. package/src/components/forms/Checkbox/Checkbox.astro +6 -0
  32. package/src/components/forms/ChoiceGroup/ChoiceGroup.astro +57 -0
  33. package/src/components/forms/ChoiceGroup/ChoiceGroup.css +108 -0
  34. package/src/components/forms/ChoiceGroup/contracts.ts +9 -0
  35. package/src/components/forms/DateTimeInput/DateTimeInput.astro +14 -0
  36. package/src/components/forms/PreparedFormBootstrap/PreparedFormBootstrap.astro +203 -0
  37. package/src/components/forms/PreparedFormBootstrap/contracts.ts +15 -0
  38. package/src/components/forms/TextInput/TextInput.astro +7 -1
  39. package/src/components/forms/Textarea/Textarea.astro +35 -0
  40. package/src/components/forms/Textarea/Textarea.css +62 -0
  41. package/src/components/icons/BackIcon.astro +26 -0
  42. package/src/components/icons/DeleteIcon.astro +22 -0
  43. package/src/components/icons/StatusIndicatorIcon.astro +16 -0
  44. package/src/components/index.ts +18 -0
  45. package/src/components/media/CanvasFrame/CanvasFrame.astro +1 -17
  46. package/src/components/media/EmbedFrame/EmbedFrame.astro +1 -19
  47. package/src/components/media/EmbedFrame/EmbedFrame.css +0 -2
  48. package/src/components/navigation/Breadcrumbs/Breadcrumbs.astro +5 -1
  49. package/src/components/navigation/Breadcrumbs/Breadcrumbs.css +12 -17
  50. package/src/components/navigation/Footer/Footer.astro +102 -0
  51. package/src/components/navigation/Footer/Footer.css +155 -0
  52. package/src/components/navigation/Footer/contracts.ts +17 -0
  53. package/src/components/navigation/SubNavigation/SubNavigation.css +0 -5
  54. package/src/composition/Grid/Grid.css +16 -0
  55. package/src/composition/SurfaceGroup/SurfaceGroup.css +21 -7
  56. package/src/composition/SurfaceGroup/stickySurfaceGroupController.ts +3 -0
  57. package/src/data/firestore/client/contracts.ts +78 -0
  58. package/src/data/firestore/client/firebase.ts +56 -0
  59. package/src/data/firestore/client/index.ts +3 -0
  60. package/src/data/firestore/client/repository.ts +111 -0
  61. package/src/display/contracts.ts +44 -1
  62. package/src/display/index.ts +1 -0
  63. package/src/display/tableProgressive.ts +62 -0
  64. package/src/layouts/BaseLayout.astro +8 -1
  65. package/src/patterns/Combobox/Combobox.astro +23 -0
  66. package/src/patterns/Combobox/comboboxController.ts +69 -0
  67. package/src/patterns/Fullscreen/FullscreenControl.astro +4 -14
  68. package/src/patterns/Fullscreen/FullscreenControl.css +0 -26
  69. package/src/patterns/Modal/Modal.astro +1 -1
  70. package/src/patterns/Modal/Modal.css +38 -1
  71. package/src/patterns/Table/Table.astro +122 -7
  72. package/src/patterns/Table/TableBody.astro +8 -0
  73. package/src/patterns/Table/TableBody.css +3 -3
  74. package/src/patterns/Table/TableCell.css +11 -1
  75. package/src/patterns/Table/TableCellValue.astro +31 -0
  76. package/src/patterns/Table/TableFooter.astro +3 -1
  77. package/src/patterns/Table/TableHeader.astro +1 -0
  78. package/src/patterns/Table/TablePagination.astro +63 -47
  79. package/src/patterns/Table/TableSkeleton.astro +18 -22
  80. package/src/patterns/Table/TableSkeleton.css +30 -5
  81. package/src/patterns/Table/tableClientController.ts +253 -0
  82. package/src/patterns/Table/tableFilterController.ts +23 -31
  83. package/src/patterns/Table/tableProgressiveController.ts +615 -227
  84. package/src/patterns/Table/tableSortController.ts +25 -7
  85. package/src/styles/base.css +24 -7
  86. package/src/styles/interaction.css +16 -0
  87. package/src/styles/style.css +3 -0
  88. 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
+ }
@@ -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: string;
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;
@@ -1,2 +1,3 @@
1
1
  export * from './contracts';
2
2
  export * from './responsiveMedia';
3
+ export * from './tableProgressive';
@@ -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 items={breadcrumbs} label={breadcrumbLabel} />
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={placement === 'inline'}
48
- full={placement === 'inline'}
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={placement === 'inline'}
77
- full={placement === 'inline'}
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
- .modal__content [data-surface] {
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
  }