@astronox/core 0.2.4 → 0.3.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.
Files changed (112) hide show
  1. package/README.md +16 -1
  2. package/package.json +18 -4
  3. package/src/admin/authController.ts +166 -0
  4. package/src/admin/cache.ts +44 -17
  5. package/src/admin/index.ts +3 -0
  6. package/src/admin/preparedDestination.ts +114 -0
  7. package/src/admin/resourceClient.ts +107 -0
  8. package/src/ads/adsense/index.ts +1 -0
  9. package/src/ads/adsense.ts +112 -0
  10. package/src/ads/contracts.ts +11 -0
  11. package/src/ads/index.ts +2 -0
  12. package/src/ads/slot.ts +16 -0
  13. package/src/auth/contracts.ts +1 -4
  14. package/src/auth/firebase/client.ts +19 -6
  15. package/src/auth/firebase/index.ts +1 -0
  16. package/src/client/formSubmission.ts +6 -1
  17. package/src/client/preferences.ts +8 -4
  18. package/src/components/actions/Button/Button.astro +30 -42
  19. package/src/components/actions/CardAction/CardAction.css +5 -10
  20. package/src/components/actions/IdentityProviderAction/IdentityProviderAction.astro +6 -8
  21. package/src/components/actions/MediaTrigger/MediaTrigger.astro +1 -1
  22. package/src/components/actions/ScrollToTop/ScrollToTop.astro +2 -1
  23. package/src/components/actions/ScrollToTop/ScrollToTop.css +2 -6
  24. package/src/components/actions/ThemeToggle/ThemeToggle.astro +3 -3
  25. package/src/components/actions/ThemeToggle/themeToggleController.ts +2 -3
  26. package/src/components/content/CardFooter/CardFooter.css +2 -4
  27. package/src/components/content/CardHeader/CardHeader.astro +1 -4
  28. package/src/components/content/CardLink/CardLink.astro +3 -15
  29. package/src/components/content/CardLink/CardLink.css +4 -15
  30. package/src/components/content/CodeBlock/CodeBlock.astro +7 -6
  31. package/src/components/content/CodeBlock/CodeBlock.css +2 -5
  32. package/src/components/content/CodeBlock/codeBlockController.ts +1 -2
  33. package/src/components/content/MarkdownContent/MarkdownContent.astro +1 -3
  34. package/src/components/content/MarkdownContent/MarkdownContent.css +7 -2
  35. package/src/components/content/MarkdownContent/MarkdownInline.astro +11 -3
  36. package/src/components/content/MarkdownContent/MarkdownNode.astro +28 -33
  37. package/src/components/content/MarkdownContent/MarkdownSymbol.astro +30 -19
  38. package/src/components/content/MarkdownContent/contracts.ts +15 -15
  39. package/src/components/content/MarkdownContent/markdownContentRenderer.ts +27 -83
  40. package/src/components/feedback/PreferenceBootstrap/PreferenceBootstrap.astro +19 -22
  41. package/src/components/feedback/Spinner/Spinner.astro +1 -2
  42. package/src/components/feedback/Tooltip/tooltipController.ts +1 -4
  43. package/src/components/forms/ChoiceGroup/ChoiceGroup.css +1 -4
  44. package/src/components/forms/DateTimeInput/DateTimeInput.astro +2 -1
  45. package/src/components/forms/PreparedFormBootstrap/PreparedFormBootstrap.astro +10 -8
  46. package/src/components/forms/PreparedFormBootstrap/parsePreparedFormState.ts +48 -0
  47. package/src/components/forms/Textarea/Textarea.astro +2 -2
  48. package/src/components/icons/BurnIcon.astro +2 -1
  49. package/src/components/icons/FieldIcon.astro +10 -4
  50. package/src/components/icons/GrassIcon.astro +3 -2
  51. package/src/components/icons/SlowIcon.astro +1 -2
  52. package/src/components/icons/StunIcon.astro +2 -1
  53. package/src/components/icons/WaterIcon.astro +3 -2
  54. package/src/components/index.ts +2 -0
  55. package/src/components/media/CanvasFrame/CanvasFrame.astro +1 -2
  56. package/src/components/media/CanvasFrame/canvasFrameController.ts +1 -4
  57. package/src/components/media/EmbedFrame/EmbedFrame.astro +5 -12
  58. package/src/components/media/EmbedFrame/embedFrameController.ts +4 -4
  59. package/src/components/monetization/AdSlot/AdSlot.astro +33 -0
  60. package/src/components/monetization/AdSlot/AdSlot.css +53 -0
  61. package/src/components/navigation/DesktopNavigation.css +1 -2
  62. package/src/components/navigation/Footer/Footer.css +3 -9
  63. package/src/components/navigation/MobileNavigation.css +3 -12
  64. package/src/components/navigation/navigationTypes.ts +1 -4
  65. package/src/components/seo/SeoHead.astro +1 -6
  66. package/src/composition/SurfaceGroup/SurfaceGroup.css +4 -14
  67. package/src/config/environment.ts +52 -0
  68. package/src/config/index.ts +3 -0
  69. package/src/config/integrations.ts +9 -0
  70. package/src/config/siteConfig.ts +5 -0
  71. package/src/config/storage.ts +16 -0
  72. package/src/content/admin.ts +354 -0
  73. package/src/content/cache.ts +177 -0
  74. package/src/content/catalog.ts +72 -0
  75. package/src/content/contracts.ts +77 -0
  76. package/src/content/drafts.ts +49 -0
  77. package/src/content/index.ts +7 -0
  78. package/src/content/markdown.ts +146 -0
  79. package/src/content/snapshot.ts +139 -0
  80. package/src/data/firestore/client/contracts.ts +10 -2
  81. package/src/data/index.ts +1 -0
  82. package/src/data/memoizedLoader.ts +13 -0
  83. package/src/display/contracts.ts +1 -5
  84. package/src/i18n/config.ts +2 -8
  85. package/src/i18n/coreMessages.ts +2 -9
  86. package/src/i18n/preference.ts +2 -6
  87. package/src/patterns/Combobox/Combobox.astro +9 -12
  88. package/src/patterns/Combobox/comboboxController.ts +14 -8
  89. package/src/patterns/Fullscreen/FullscreenControl.astro +5 -1
  90. package/src/patterns/Fullscreen/fullscreenController.ts +1 -3
  91. package/src/patterns/MapViewport/geometry.ts +1 -4
  92. package/src/patterns/Modal/Modal.css +1 -4
  93. package/src/patterns/Modal/ModalOpen.astro +5 -1
  94. package/src/patterns/Table/Table.astro +10 -10
  95. package/src/patterns/Table/TableBody.astro +1 -4
  96. package/src/patterns/Table/TableBody.css +2 -4
  97. package/src/patterns/Table/TablePagination.astro +23 -30
  98. package/src/patterns/Table/TableSkeleton.astro +1 -5
  99. package/src/patterns/Table/tableClientController.ts +2 -2
  100. package/src/patterns/Table/tableFilterController.ts +1 -3
  101. package/src/patterns/Table/tableHeaderOverflowController.ts +1 -4
  102. package/src/patterns/Table/tableProgressiveController.ts +62 -60
  103. package/src/patterns/Table/tableScrollController.ts +3 -2
  104. package/src/primitives/text/TextBlock/TextBlock.astro +1 -3
  105. package/src/publication/contracts.ts +11 -0
  106. package/src/publication/http.ts +31 -0
  107. package/src/publication/index.ts +2 -0
  108. package/src/routing/localized.ts +9 -12
  109. package/src/seo/localizedMeta.ts +2 -6
  110. package/src/styles/interaction.css +7 -7
  111. package/src/styles/style.css +11 -20
  112. package/src/theme/contracts.ts +1 -6
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod';
2
+ import type { ContentDocumentDraft, ContentPlacementDraft } from './contracts';
3
+
4
+ export const contentKeySchema = z
5
+ .string()
6
+ .trim()
7
+ .min(1)
8
+ .max(80)
9
+ .transform((value) =>
10
+ value
11
+ .normalize('NFKD')
12
+ .replace(/[\u0300-\u036f]/g, '')
13
+ .toLowerCase()
14
+ .replace(/[^a-z0-9]+/g, '-')
15
+ .replace(/^-+|-+$/g, ''),
16
+ )
17
+ .pipe(z.string().min(1));
18
+
19
+ const placementDraftSchema = z.object({
20
+ key: contentKeySchema,
21
+ title: z.string().trim().min(1).max(120),
22
+ });
23
+
24
+ const documentDraftSchema = z.object({
25
+ placementId: z.string().trim().min(1),
26
+ content: z.string().trim().min(1).max(200_000),
27
+ order: z.coerce.number().int().positive(),
28
+ slug: contentKeySchema,
29
+ summary: z
30
+ .string()
31
+ .trim()
32
+ .max(500)
33
+ .optional()
34
+ .transform((value) => value || undefined),
35
+ title: z.string().trim().min(1).max(160),
36
+ visibility: z.enum(['public', 'private']),
37
+ });
38
+
39
+ export function parseContentPlacementDraft(
40
+ input: unknown,
41
+ ): ContentPlacementDraft {
42
+ return placementDraftSchema.parse(input);
43
+ }
44
+
45
+ export function parseContentDocumentDraft(
46
+ input: unknown,
47
+ ): ContentDocumentDraft {
48
+ return documentDraftSchema.parse(input);
49
+ }
@@ -0,0 +1,7 @@
1
+ export * from './admin';
2
+ export * from './cache';
3
+ export * from './catalog';
4
+ export * from './contracts';
5
+ export * from './drafts';
6
+ export * from './markdown';
7
+ export * from './snapshot';
@@ -0,0 +1,146 @@
1
+ import { fromMarkdown } from 'mdast-util-from-markdown';
2
+ import type {
3
+ MarkdownBlock,
4
+ MarkdownDocument,
5
+ MarkdownHeadingLevel,
6
+ MarkdownInline,
7
+ } from '../components/content/MarkdownContent/contracts';
8
+
9
+ type AstNode = {
10
+ children?: AstNode[];
11
+ depth?: number;
12
+ lang?: string;
13
+ type?: string;
14
+ url?: string;
15
+ value?: string;
16
+ };
17
+
18
+ export function parseMarkdownDocument(source: string): MarkdownDocument {
19
+ const root = fromMarkdown(source) as AstNode;
20
+ const headingIds = new Map<string, number>();
21
+ return {
22
+ blocks: (root.children ?? []).flatMap((node) => toBlocks(node, headingIds)),
23
+ };
24
+ }
25
+
26
+ function toBlocks(
27
+ node: AstNode,
28
+ headingIds: Map<string, number>,
29
+ ): MarkdownBlock[] {
30
+ switch (node.type) {
31
+ case 'paragraph':
32
+ return [{ type: 'paragraph', children: toInlineChildren(node) }];
33
+ case 'heading': {
34
+ const children = toInlineChildren(node);
35
+ const title = inlineText(children);
36
+ const level = Math.min(
37
+ 6,
38
+ Math.max(2, node.depth ?? 2),
39
+ ) as MarkdownHeadingLevel;
40
+ return [
41
+ {
42
+ type: 'heading',
43
+ children,
44
+ id: uniqueSlug(title, headingIds),
45
+ level,
46
+ },
47
+ ];
48
+ }
49
+ case 'list':
50
+ return [
51
+ {
52
+ type: 'list',
53
+ items: (node.children ?? [])
54
+ .filter((item) => item.type === 'listItem')
55
+ .map((item) => ({
56
+ children: (item.children ?? []).flatMap((child) =>
57
+ toBlocks(child, headingIds),
58
+ ),
59
+ })),
60
+ },
61
+ ];
62
+ case 'blockquote':
63
+ return [
64
+ {
65
+ type: 'quote',
66
+ children: (node.children ?? []).flatMap((child) =>
67
+ toBlocks(child, headingIds),
68
+ ),
69
+ },
70
+ ];
71
+ case 'code':
72
+ return [
73
+ {
74
+ type: 'code',
75
+ language: node.lang,
76
+ value: node.value ?? '',
77
+ },
78
+ ];
79
+ case 'thematicBreak':
80
+ return [{ type: 'separator' }];
81
+ default:
82
+ return [];
83
+ }
84
+ }
85
+
86
+ function toInlineChildren(node: AstNode): MarkdownInline[] {
87
+ return (node.children ?? []).flatMap(toInline);
88
+ }
89
+
90
+ function toInline(node: AstNode): MarkdownInline[] {
91
+ switch (node.type) {
92
+ case 'text':
93
+ return [{ type: 'text', value: node.value ?? '' }];
94
+ case 'strong':
95
+ return [{ type: 'strong', children: toInlineChildren(node) }];
96
+ case 'emphasis':
97
+ return [{ type: 'emphasis', children: toInlineChildren(node) }];
98
+ case 'inlineCode':
99
+ return [{ type: 'code', value: node.value ?? '' }];
100
+ case 'break':
101
+ return [{ type: 'break' }];
102
+ case 'link': {
103
+ const href = node.url ?? '';
104
+ return [
105
+ {
106
+ type: 'link',
107
+ children: toInlineChildren(node),
108
+ external: /^(?:https?:)?\/\//i.test(href),
109
+ href,
110
+ },
111
+ ];
112
+ }
113
+ default:
114
+ return [];
115
+ }
116
+ }
117
+
118
+ function inlineText(nodes: readonly MarkdownInline[]): string {
119
+ return nodes
120
+ .map((node) => {
121
+ if (node.type === 'text' || node.type === 'code') return node.value;
122
+ if (
123
+ node.type === 'strong' ||
124
+ node.type === 'emphasis' ||
125
+ node.type === 'link'
126
+ ) {
127
+ return inlineText(node.children);
128
+ }
129
+ return '';
130
+ })
131
+ .join('')
132
+ .trim();
133
+ }
134
+
135
+ function uniqueSlug(value: string, counts: Map<string, number>): string {
136
+ const base =
137
+ value
138
+ .normalize('NFKD')
139
+ .replace(/[\u0300-\u036f]/g, '')
140
+ .toLowerCase()
141
+ .replace(/[^a-z0-9]+/g, '-')
142
+ .replace(/^-+|-+$/g, '') || 'section';
143
+ const count = counts.get(base) ?? 0;
144
+ counts.set(base, count + 1);
145
+ return count === 0 ? base : `${base}-${count + 1}`;
146
+ }
@@ -0,0 +1,139 @@
1
+ import type {
2
+ ContentDocument,
3
+ ContentPlacement,
4
+ ContentSnapshot,
5
+ } from './contracts';
6
+
7
+ type RecordValue = Record<string, unknown>;
8
+
9
+ export type ContentSnapshotFields = {
10
+ documents: string;
11
+ documentPlacementId: string;
12
+ label: string;
13
+ placements: string;
14
+ };
15
+
16
+ export function parseContentSnapshot(
17
+ value: unknown,
18
+ fields: ContentSnapshotFields,
19
+ ): ContentSnapshot {
20
+ const source = requireRecord(value, fields.label);
21
+ const rawPlacements = requireArray(source[fields.placements], fields.label);
22
+ const rawDocuments = requireArray(source[fields.documents], fields.label);
23
+ const placements = rawPlacements.map((entry) =>
24
+ parsePlacement(entry, fields.label),
25
+ );
26
+ const documents = rawDocuments.map((entry) =>
27
+ parseDocument(entry, fields.documentPlacementId, fields.label),
28
+ );
29
+ const placementIds = new Set(placements.map((placement) => placement.id));
30
+
31
+ for (const document of documents) {
32
+ if (!placementIds.has(document.placementId)) {
33
+ throw new Error(
34
+ `Invalid ${fields.label} document placement: ${document.id}`,
35
+ );
36
+ }
37
+ }
38
+
39
+ return {
40
+ placements,
41
+ documents,
42
+ updatedAt:
43
+ source.updatedAt === undefined || source.updatedAt === null
44
+ ? null
45
+ : parseSnapshotDate(source.updatedAt, fields.label),
46
+ };
47
+ }
48
+
49
+ export function projectPublicContentSnapshot<
50
+ TPlacement extends ContentPlacement,
51
+ TDocument extends ContentDocument,
52
+ >(
53
+ snapshot: ContentSnapshot<TPlacement, TDocument>,
54
+ ): ContentSnapshot<TPlacement, TDocument> {
55
+ return {
56
+ ...snapshot,
57
+ documents: snapshot.documents.filter(
58
+ (document) => document.visibility === 'public',
59
+ ),
60
+ };
61
+ }
62
+
63
+ export function parseSnapshotDate(value: unknown, label = 'snapshot'): Date {
64
+ const timestamp = isRecord(value)
65
+ ? typeof value._seconds === 'number'
66
+ ? value._seconds * 1_000
67
+ : typeof value.seconds === 'number'
68
+ ? value.seconds * 1_000
69
+ : Number.NaN
70
+ : value;
71
+ const date = new Date(timestamp as string | number | Date);
72
+ if (Number.isNaN(date.getTime())) throw new Error(`Invalid ${label} date`);
73
+ return date;
74
+ }
75
+
76
+ function parsePlacement(value: unknown, label: string): ContentPlacement {
77
+ const source = requireRecord(value, label);
78
+ return {
79
+ id: requireString(source.id, label),
80
+ key: requireString(source.key, label),
81
+ title: requireString(source.title, label),
82
+ };
83
+ }
84
+
85
+ function parseDocument(
86
+ value: unknown,
87
+ placementIdField: string,
88
+ label: string,
89
+ ): ContentDocument {
90
+ const source = requireRecord(value, label);
91
+ const order =
92
+ typeof source.order === 'number' ? source.order : Number(source.order);
93
+ if (!Number.isInteger(order) || order < 1) {
94
+ throw new Error(`Invalid ${label} document order`);
95
+ }
96
+ if (source.private !== undefined && typeof source.private !== 'boolean') {
97
+ throw new Error(`Invalid ${label} document visibility`);
98
+ }
99
+ const summary = readOptionalString(source.summary, label);
100
+
101
+ return {
102
+ content: requireString(source.content, label),
103
+ id: requireString(source.id, label),
104
+ order,
105
+ placementId: requireString(source[placementIdField], label),
106
+ slug: requireString(source.slug, label),
107
+ ...(summary ? { summary } : {}),
108
+ title: requireString(source.title, label),
109
+ updatedAt: parseSnapshotDate(source.updatedAt, label),
110
+ visibility: source.private === true ? 'private' : 'public',
111
+ };
112
+ }
113
+
114
+ function requireArray(value: unknown, label: string): unknown[] {
115
+ if (!Array.isArray(value)) throw new Error(`Invalid ${label} snapshot`);
116
+ return value;
117
+ }
118
+
119
+ function requireRecord(value: unknown, label: string): RecordValue {
120
+ if (!isRecord(value)) throw new Error(`Invalid ${label} snapshot`);
121
+ return value;
122
+ }
123
+
124
+ function requireString(value: unknown, label: string): string {
125
+ const resolved = typeof value === 'string' ? value.trim() : '';
126
+ if (!resolved) throw new Error(`Invalid ${label} snapshot string`);
127
+ return resolved;
128
+ }
129
+
130
+ function readOptionalString(value: unknown, label: string): string | undefined {
131
+ if (value === undefined || value === null || value === '') return undefined;
132
+ if (typeof value !== 'string')
133
+ throw new Error(`Invalid ${label} optional string`);
134
+ return value.trim() || undefined;
135
+ }
136
+
137
+ function isRecord(value: unknown): value is RecordValue {
138
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
139
+ }
@@ -1,5 +1,7 @@
1
1
  import type { Firestore } from 'firebase/firestore';
2
2
 
3
+ export type ClientFirestore = Firestore;
4
+
3
5
  export type ClientFirestorePage<TItem> = {
4
6
  items: readonly TItem[];
5
7
  nextCursor?: string;
@@ -16,11 +18,12 @@ export type ClientFirestoreCursor<TDocument> = {
16
18
  };
17
19
 
18
20
  export type ClientFirestoreDocumentMapper<
21
+ TDocument,
19
22
  TItem,
20
23
  TCreateInput,
21
24
  TUpdateInput,
22
25
  > = {
23
- fromDocument(id: string, value: unknown): TItem;
26
+ fromDocument(id: string, value: TDocument): TItem;
24
27
  toCreate(input: TCreateInput): Record<string, unknown>;
25
28
  toUpdate(input: TUpdateInput): Record<string, unknown>;
26
29
  };
@@ -49,7 +52,12 @@ export type ClientFirestoreRepositoryConfig<
49
52
  collection: string;
50
53
  cursor?: ClientFirestoreCursor<TDocument>;
51
54
  database: Firestore;
52
- mapper: ClientFirestoreDocumentMapper<TItem, TCreateInput, TUpdateInput>;
55
+ mapper: ClientFirestoreDocumentMapper<
56
+ TDocument,
57
+ TItem,
58
+ TCreateInput,
59
+ TUpdateInput
60
+ >;
53
61
  orderBy?: readonly ClientFirestoreOrder[];
54
62
  schema: ClientFirestoreSchema<TDocument>;
55
63
  };
package/src/data/index.ts CHANGED
@@ -1 +1,2 @@
1
1
  export * from './collectionValidation';
2
+ export * from './memoizedLoader';
@@ -0,0 +1,13 @@
1
+ export function createMemoizedLoader<T>(
2
+ load: () => Promise<T>,
3
+ ): () => Promise<T> {
4
+ let pending: Promise<T> | null = null;
5
+
6
+ return () => {
7
+ pending ??= load().catch((reason) => {
8
+ pending = null;
9
+ throw reason;
10
+ });
11
+ return pending;
12
+ };
13
+ }
@@ -175,11 +175,7 @@ export type StageFlowStep = {
175
175
  };
176
176
 
177
177
  export type TableColumnRole =
178
- | 'ordinal'
179
- | 'media'
180
- | 'primary'
181
- | 'value'
182
- | 'collection';
178
+ 'ordinal' | 'media' | 'primary' | 'value' | 'collection';
183
179
  export type TableColumnAlignment = 'start' | 'center' | 'end';
184
180
 
185
181
  export type TableColumn = {
@@ -26,10 +26,7 @@ export type LocaleConfig<
26
26
  };
27
27
 
28
28
  export function createLocaleConfig<
29
- const TDefinitions extends readonly [
30
- LocaleDefinition,
31
- ...LocaleDefinition[],
32
- ],
29
+ const TDefinitions extends readonly [LocaleDefinition, ...LocaleDefinition[]],
33
30
  const TDefaultLocale extends TDefinitions[number]['id'],
34
31
  >({
35
32
  defaultLocale,
@@ -100,10 +97,7 @@ export function createLocaleConfig<
100
97
  },
101
98
  requireLocalizedLocale(value: string): Locale {
102
99
  const locale = localeByUrlCode.get(value.toLowerCase())?.id;
103
- if (
104
- !locale ||
105
- (!prefixDefaultLocale && locale === defaultLocale)
106
- ) {
100
+ if (!locale || (!prefixDefaultLocale && locale === defaultLocale)) {
107
101
  throw new Error(`Unsupported localized route locale: ${value}`);
108
102
  }
109
103
  return locale;
@@ -1,7 +1,4 @@
1
- import {
2
- resolveLocalizedCatalog,
3
- type MessageShape,
4
- } from './catalog';
1
+ import { resolveLocalizedCatalog, type MessageShape } from './catalog';
5
2
 
6
3
  export const defaultCoreMessages = {
7
4
  actions: {
@@ -29,9 +26,5 @@ export function resolveCoreMessages(
29
26
  translated: unknown = undefined,
30
27
  locale = 'en',
31
28
  ): CoreMessages {
32
- return resolveLocalizedCatalog(
33
- defaultCoreMessages,
34
- translated,
35
- locale,
36
- );
29
+ return resolveLocalizedCatalog(defaultCoreMessages, translated, locale);
37
30
  }
@@ -11,9 +11,7 @@ export function createLocalePreferenceController<TLocale extends string>({
11
11
  resolvePath,
12
12
  storageKey,
13
13
  }: LocalePreferenceControllerOptions<TLocale>) {
14
- return function initLocalePreference(
15
- root: Document = document,
16
- ): () => void {
14
+ return function initLocalePreference(root: Document = document): () => void {
17
15
  const element = root.documentElement;
18
16
  if (element.hasAttribute(initializedAttribute)) return () => undefined;
19
17
  element.setAttribute(initializedAttribute, '');
@@ -24,9 +22,7 @@ export function createLocalePreferenceController<TLocale extends string>({
24
22
 
25
23
  const link = target.closest<HTMLAnchorElement>('a[hreflang]');
26
24
  const languageTag = link?.getAttribute('hreflang');
27
- const locale = languageTag
28
- ? resolveLocale(languageTag)
29
- : undefined;
25
+ const locale = languageTag ? resolveLocale(languageTag) : undefined;
30
26
  if (!locale) return;
31
27
 
32
28
  event.preventDefault();
@@ -244,18 +244,15 @@ const searchId = `${props.id}-search`;
244
244
  data-tone={resolvedTone}
245
245
  data-preference-id={props.preference?.id}
246
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
- }
247
+ data-preference-control={props.preference
248
+ ? mode === 'number'
249
+ ? 'combobox-number'
250
+ : 'combobox-options'
251
+ : undefined}
252
+ data-preference-number-bounds={mode === 'number' &&
253
+ (props as NumberProps).preferenceBounds
254
+ ? JSON.stringify((props as NumberProps).preferenceBounds)
255
+ : undefined}
259
256
  >
260
257
  <span class="combobox__sizer" aria-hidden="true">
261
258
  {
@@ -105,10 +105,7 @@ function normalizeNumber(value: number, bounds: NumberBounds): number {
105
105
  return Math.min(bounds.max, Math.max(bounds.min, stepped));
106
106
  }
107
107
 
108
- function syncNumberOptionBounds(
109
- root: HTMLElement,
110
- bounds: NumberBounds,
111
- ): void {
108
+ function syncNumberOptionBounds(root: HTMLElement, bounds: NumberBounds): void {
112
109
  for (const option of getOptions(root)) {
113
110
  const value = Number(option.dataset.comboboxOption);
114
111
  option.hidden =
@@ -172,12 +169,18 @@ export function setComboboxOptions(
172
169
  selectedValues: readonly string[] = [],
173
170
  ): void {
174
171
  if (root.dataset.comboboxMode === 'number') return;
175
- const optionsRoot = root.querySelector<HTMLElement>('[data-combobox-options]');
172
+ const optionsRoot = root.querySelector<HTMLElement>(
173
+ '[data-combobox-options]',
174
+ );
176
175
  const empty = root.querySelector<HTMLElement>('[data-combobox-empty]');
177
176
  if (!optionsRoot || !empty) return;
178
177
 
179
- optionsRoot.querySelectorAll('[data-combobox-option]').forEach((node) => node.remove());
180
- root.querySelectorAll('template[data-combobox-tag-template]').forEach((node) => node.remove());
178
+ optionsRoot
179
+ .querySelectorAll('[data-combobox-option]')
180
+ .forEach((node) => node.remove());
181
+ root
182
+ .querySelectorAll('template[data-combobox-tag-template]')
183
+ .forEach((node) => node.remove());
181
184
 
182
185
  options.forEach((option, index) => {
183
186
  const button = document.createElement('button');
@@ -190,7 +193,10 @@ export function setComboboxOptions(
190
193
  button.setAttribute('aria-selected', 'false');
191
194
  if (option.disabled) button.setAttribute('aria-disabled', 'true');
192
195
  button.dataset.comboboxOption = option.value;
193
- button.dataset.comboboxSearchText = [option.label, option.description ?? ''].join(' ');
196
+ button.dataset.comboboxSearchText = [
197
+ option.label,
198
+ option.description ?? '',
199
+ ].join(' ');
194
200
  button.dataset.interactionAccent = 'start';
195
201
 
196
202
  const content = document.createElement('span');
@@ -12,7 +12,11 @@ type FullscreenUnsupportedFallback = {
12
12
  target?: '_blank' | '_self';
13
13
  };
14
14
 
15
- type Props = Omit<HTMLAttributes<'button'>, 'aria-controls' | 'type'> & {
15
+ type Props = Omit<
16
+ HTMLAttributes<'button'>,
17
+ 'aria-controls' | 'disabled' | 'type'
18
+ > & {
19
+ disabled?: boolean;
16
20
  enterLabel: string;
17
21
  exitLabel: string;
18
22
  targetId: string;
@@ -1,9 +1,7 @@
1
1
  const initializedAttribute = 'data-fullscreen-initialized';
2
2
  const noop = (): void => undefined;
3
3
 
4
- export function initFullscreenControl(
5
- control: HTMLButtonElement,
6
- ): () => void {
4
+ export function initFullscreenControl(control: HTMLButtonElement): () => void {
7
5
  if (control.getAttribute(initializedAttribute) === 'true') return noop;
8
6
 
9
7
  const targetId = control.dataset.fullscreenTarget;
@@ -1,7 +1,4 @@
1
- import type {
2
- MapViewportModel,
3
- MapViewportSizing,
4
- } from './contracts';
1
+ import type { MapViewportModel, MapViewportSizing } from './contracts';
5
2
 
6
3
  export type MapViewportSceneGeometry = {
7
4
  aspectRatio: string;
@@ -108,10 +108,7 @@ html:has(.modal[open]) [data-site-viewport] {
108
108
  padding-inline: 0;
109
109
  }
110
110
 
111
- .modal:not(
112
- [data-sizing='adaptive'],
113
- [data-sizing='confirmation']
114
- )
111
+ .modal:not([data-sizing='adaptive'], [data-sizing='confirmation'])
115
112
  .modal__content
116
113
  [data-surface] {
117
114
  border-radius: 0;
@@ -3,7 +3,11 @@ import type { HTMLAttributes } from 'astro/types';
3
3
  import type { Tone } from '../../theme/contracts';
4
4
  import Button from '../../components/actions/Button/Button.astro';
5
5
 
6
- type Props = Omit<HTMLAttributes<'button'>, 'aria-controls' | 'type'> & {
6
+ type Props = Omit<
7
+ HTMLAttributes<'button'>,
8
+ 'aria-controls' | 'disabled' | 'type'
9
+ > & {
10
+ disabled?: boolean;
7
11
  label: string;
8
12
  targetId: string;
9
13
  tone?: Tone;
@@ -114,7 +114,7 @@ const resolvedRows = rows.map((row, index): TableRow => ({
114
114
  ...row,
115
115
  key: row.key ?? `${paginationKey ?? 'table'}-${index + 1}`,
116
116
  }));
117
- const inferredProgressive =
117
+ const inferredProgressive: TableProgressiveModel | undefined =
118
118
  progressive === undefined && resolvedRows.length > batchSize
119
119
  ? {
120
120
  batchSize,
@@ -128,7 +128,7 @@ const inferredProgressive =
128
128
  const progressiveModel =
129
129
  progressive === false
130
130
  ? undefined
131
- : configuredProgressive ?? inferredProgressive;
131
+ : (configuredProgressive ?? inferredProgressive);
132
132
  const hasProgressiveRows = Boolean(
133
133
  progressiveModel && progressiveModel.entries.length > batchSize,
134
134
  );
@@ -171,7 +171,9 @@ if (clientProgressive && !paginationMessages) {
171
171
  throw new Error('Client-progressive tables require localized messages');
172
172
  }
173
173
  if (progressiveModel?.sources && !paginationKey) {
174
- throw new Error('External progressive tables require a stable pagination key');
174
+ throw new Error(
175
+ 'External progressive tables require a stable pagination key',
176
+ );
175
177
  }
176
178
  if (rowKeys.size !== resolvedRows.length) {
177
179
  throw new Error('Table rows require unique keys');
@@ -193,9 +195,9 @@ if (
193
195
  data-table-client-progressive={clientProgressive ? true : undefined}
194
196
  data-table-density={density}
195
197
  data-table-inline-overflow={inlineOverflow}
196
- data-table-index-minimum-rem={
197
- minimumInlineSize ? Number.parseFloat(minimumInlineSize) : undefined
198
- }
198
+ data-table-index-minimum-rem={minimumInlineSize
199
+ ? Number.parseFloat(minimumInlineSize)
200
+ : undefined}
199
201
  data-table-layout={layout}
200
202
  data-table-loading-state={initialLoadingState}
201
203
  data-table-preparation={preparation}
@@ -209,10 +211,8 @@ if (
209
211
  preparesRows ? (
210
212
  <script is:inline>
211
213
  document.documentElement.setAttribute('data-nox-table-enhanced', '');
212
- document.currentScript?.parentElement?.setAttribute(
213
- 'aria-busy',
214
- 'true',
215
- );
214
+ document.currentScript?.parentElement?.setAttribute( 'aria-busy',
215
+ 'true', );
216
216
  </script>
217
217
  ) : null
218
218
  }
@@ -6,10 +6,7 @@ import TableSortIcon from '../../components/icons/TableSortIcon.astro';
6
6
  import TextBlock from '../../primitives/text/TextBlock/TextBlock.astro';
7
7
  import TableCellContent from './TableCell.astro';
8
8
  import TableSkeleton from './TableSkeleton.astro';
9
- import {
10
- getTableColumnAlignment,
11
- type TableLayout,
12
- } from './tableLayout';
9
+ import { getTableColumnAlignment, type TableLayout } from './tableLayout';
13
10
 
14
11
  type Props = HTMLAttributes<'div'> & {
15
12
  caption: string;