@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.
- package/README.md +16 -1
- package/package.json +18 -4
- package/src/admin/authController.ts +166 -0
- package/src/admin/cache.ts +44 -17
- package/src/admin/index.ts +3 -0
- package/src/admin/preparedDestination.ts +114 -0
- package/src/admin/resourceClient.ts +107 -0
- package/src/ads/adsense/index.ts +1 -0
- package/src/ads/adsense.ts +112 -0
- package/src/ads/contracts.ts +11 -0
- package/src/ads/index.ts +2 -0
- package/src/ads/slot.ts +16 -0
- package/src/auth/contracts.ts +1 -4
- package/src/auth/firebase/client.ts +19 -6
- package/src/auth/firebase/index.ts +1 -0
- package/src/client/formSubmission.ts +6 -1
- package/src/client/preferences.ts +8 -4
- package/src/components/actions/Button/Button.astro +30 -42
- package/src/components/actions/CardAction/CardAction.css +5 -10
- package/src/components/actions/IdentityProviderAction/IdentityProviderAction.astro +6 -8
- package/src/components/actions/MediaTrigger/MediaTrigger.astro +1 -1
- package/src/components/actions/ScrollToTop/ScrollToTop.astro +2 -1
- package/src/components/actions/ScrollToTop/ScrollToTop.css +2 -6
- package/src/components/actions/ThemeToggle/ThemeToggle.astro +3 -3
- package/src/components/actions/ThemeToggle/themeToggleController.ts +2 -3
- package/src/components/content/CardFooter/CardFooter.css +2 -4
- package/src/components/content/CardHeader/CardHeader.astro +1 -4
- package/src/components/content/CardLink/CardLink.astro +3 -15
- package/src/components/content/CardLink/CardLink.css +4 -15
- package/src/components/content/CodeBlock/CodeBlock.astro +7 -6
- package/src/components/content/CodeBlock/CodeBlock.css +2 -5
- package/src/components/content/CodeBlock/codeBlockController.ts +1 -2
- package/src/components/content/MarkdownContent/MarkdownContent.astro +1 -3
- package/src/components/content/MarkdownContent/MarkdownContent.css +7 -2
- package/src/components/content/MarkdownContent/MarkdownInline.astro +11 -3
- package/src/components/content/MarkdownContent/MarkdownNode.astro +28 -33
- package/src/components/content/MarkdownContent/MarkdownSymbol.astro +30 -19
- package/src/components/content/MarkdownContent/contracts.ts +15 -15
- package/src/components/content/MarkdownContent/markdownContentRenderer.ts +27 -83
- package/src/components/feedback/PreferenceBootstrap/PreferenceBootstrap.astro +19 -22
- package/src/components/feedback/Spinner/Spinner.astro +1 -2
- package/src/components/feedback/Tooltip/tooltipController.ts +1 -4
- package/src/components/forms/ChoiceGroup/ChoiceGroup.css +1 -4
- package/src/components/forms/DateTimeInput/DateTimeInput.astro +2 -1
- package/src/components/forms/PreparedFormBootstrap/PreparedFormBootstrap.astro +10 -8
- package/src/components/forms/PreparedFormBootstrap/parsePreparedFormState.ts +48 -0
- package/src/components/forms/Textarea/Textarea.astro +2 -2
- package/src/components/icons/BurnIcon.astro +2 -1
- package/src/components/icons/FieldIcon.astro +10 -4
- package/src/components/icons/GrassIcon.astro +3 -2
- package/src/components/icons/SlowIcon.astro +1 -2
- package/src/components/icons/StunIcon.astro +2 -1
- package/src/components/icons/WaterIcon.astro +3 -2
- package/src/components/index.ts +2 -0
- package/src/components/media/CanvasFrame/CanvasFrame.astro +1 -2
- package/src/components/media/CanvasFrame/canvasFrameController.ts +1 -4
- package/src/components/media/EmbedFrame/EmbedFrame.astro +5 -12
- package/src/components/media/EmbedFrame/embedFrameController.ts +4 -4
- package/src/components/monetization/AdSlot/AdSlot.astro +33 -0
- package/src/components/monetization/AdSlot/AdSlot.css +53 -0
- package/src/components/navigation/DesktopNavigation.css +1 -2
- package/src/components/navigation/Footer/Footer.css +3 -9
- package/src/components/navigation/MobileNavigation.css +3 -12
- package/src/components/navigation/navigationTypes.ts +1 -4
- package/src/components/seo/SeoHead.astro +1 -6
- package/src/composition/SurfaceGroup/SurfaceGroup.css +4 -14
- package/src/config/environment.ts +52 -0
- package/src/config/index.ts +3 -0
- package/src/config/integrations.ts +9 -0
- package/src/config/siteConfig.ts +5 -0
- package/src/config/storage.ts +16 -0
- package/src/content/admin.ts +354 -0
- package/src/content/cache.ts +177 -0
- package/src/content/catalog.ts +72 -0
- package/src/content/contracts.ts +77 -0
- package/src/content/drafts.ts +49 -0
- package/src/content/index.ts +7 -0
- package/src/content/markdown.ts +146 -0
- package/src/content/snapshot.ts +139 -0
- package/src/data/firestore/client/contracts.ts +10 -2
- package/src/data/index.ts +1 -0
- package/src/data/memoizedLoader.ts +13 -0
- package/src/display/contracts.ts +1 -5
- package/src/i18n/config.ts +2 -8
- package/src/i18n/coreMessages.ts +2 -9
- package/src/i18n/preference.ts +2 -6
- package/src/patterns/Combobox/Combobox.astro +9 -12
- package/src/patterns/Combobox/comboboxController.ts +14 -8
- package/src/patterns/Fullscreen/FullscreenControl.astro +5 -1
- package/src/patterns/Fullscreen/fullscreenController.ts +1 -3
- package/src/patterns/MapViewport/geometry.ts +1 -4
- package/src/patterns/Modal/Modal.css +1 -4
- package/src/patterns/Modal/ModalOpen.astro +5 -1
- package/src/patterns/Table/Table.astro +10 -10
- package/src/patterns/Table/TableBody.astro +1 -4
- package/src/patterns/Table/TableBody.css +2 -4
- package/src/patterns/Table/TablePagination.astro +23 -30
- package/src/patterns/Table/TableSkeleton.astro +1 -5
- package/src/patterns/Table/tableClientController.ts +2 -2
- package/src/patterns/Table/tableFilterController.ts +1 -3
- package/src/patterns/Table/tableHeaderOverflowController.ts +1 -4
- package/src/patterns/Table/tableProgressiveController.ts +62 -60
- package/src/patterns/Table/tableScrollController.ts +3 -2
- package/src/primitives/text/TextBlock/TextBlock.astro +1 -3
- package/src/publication/contracts.ts +11 -0
- package/src/publication/http.ts +31 -0
- package/src/publication/index.ts +2 -0
- package/src/routing/localized.ts +9 -12
- package/src/seo/localizedMeta.ts +2 -6
- package/src/styles/interaction.css +7 -7
- package/src/styles/style.css +11 -20
- 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,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:
|
|
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<
|
|
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
|
@@ -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
|
+
}
|
package/src/display/contracts.ts
CHANGED
|
@@ -175,11 +175,7 @@ export type StageFlowStep = {
|
|
|
175
175
|
};
|
|
176
176
|
|
|
177
177
|
export type TableColumnRole =
|
|
178
|
-
| '
|
|
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 = {
|
package/src/i18n/config.ts
CHANGED
|
@@ -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;
|
package/src/i18n/coreMessages.ts
CHANGED
|
@@ -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
|
}
|
package/src/i18n/preference.ts
CHANGED
|
@@ -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
|
-
|
|
249
|
-
?
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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>(
|
|
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
|
|
180
|
-
|
|
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 = [
|
|
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<
|
|
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;
|
|
@@ -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<
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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;
|