@astronox/core 0.2.0 → 0.2.2
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 +9 -30
- package/package.json +7 -1
- package/src/admin/contracts.ts +28 -0
- package/src/admin/index.ts +2 -0
- package/src/admin/module.ts +28 -0
- package/src/auth/contracts.ts +53 -0
- package/src/auth/firebase/client.ts +53 -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/index.ts +3 -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/MediaTrigger/MediaTrigger.astro +12 -4
- package/src/components/actions/MediaTrigger/MediaTrigger.css +1 -0
- package/src/components/content/CardBody/CardBody.astro +3 -0
- package/src/components/content/CardBody/CardBody.css +10 -0
- package/src/components/content/MarkdownContent/MarkdownContent.css +0 -1
- package/src/components/content/MarkdownContent/MarkdownNode.astro +1 -0
- package/src/components/content/MarkdownContent/markdownContentRenderer.ts +549 -0
- package/src/components/feedback/PreferenceBootstrap/PreferenceBootstrap.astro +403 -0
- package/src/components/forms/Checkbox/Checkbox.astro +66 -0
- package/src/components/forms/Checkbox/Checkbox.css +126 -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/FieldGroup/FieldGroup.astro +34 -0
- package/src/components/forms/FieldGroup/FieldGroup.css +30 -0
- package/src/components/forms/TextInput/TextInput.astro +7 -1
- package/src/components/forms/Textarea/Textarea.astro +32 -0
- package/src/components/forms/Textarea/Textarea.css +54 -0
- package/src/components/icons/BackIcon.astro +26 -0
- package/src/components/icons/CheckIcon.astro +22 -0
- package/src/components/icons/DeleteIcon.astro +22 -0
- package/src/components/icons/StatusIndicatorIcon.astro +16 -0
- package/src/components/index.ts +12 -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/SubNavigation/SubNavigation.css +0 -5
- package/src/composition/Grid/Grid.astro +9 -0
- package/src/composition/Grid/Grid.css +52 -0
- package/src/composition/GridItem/GridItem.astro +9 -0
- package/src/composition/GridItem/GridItem.css +12 -0
- package/src/composition/SurfaceGroup/SurfaceGroup.css +21 -7
- package/src/composition/SurfaceGroup/stickySurfaceGroupController.ts +3 -0
- package/src/data/firestore/client/contracts.ts +55 -0
- package/src/data/firestore/client/firebase.ts +12 -0
- package/src/data/firestore/client/index.ts +3 -0
- package/src/data/firestore/client/repository.ts +111 -0
- package/src/display/contracts.ts +41 -0
- package/src/display/index.ts +1 -0
- package/src/display/tableProgressive.ts +62 -0
- package/src/patterns/Combobox/Combobox.astro +23 -0
- package/src/patterns/Combobox/comboboxController.ts +72 -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/Table.css +2 -1
- package/src/patterns/Table/TableBody.astro +11 -1
- package/src/patterns/Table/TableBody.css +6 -0
- package/src/patterns/Table/TableCell.css +6 -0
- 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 +83 -33
- package/src/patterns/Table/tableLayout.ts +10 -3
- package/src/patterns/Table/tableProgressiveController.ts +615 -227
- package/src/patterns/Table/tableSortController.ts +25 -7
- package/src/styles/interaction.css +16 -0
- package/src/styles/style.css +4 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
export type PreferenceScalar = boolean | number | string;
|
|
2
|
+
|
|
3
|
+
export type PreferenceSchema =
|
|
4
|
+
| { type: 'boolean' }
|
|
5
|
+
| {
|
|
6
|
+
type: 'integer';
|
|
7
|
+
min?: number;
|
|
8
|
+
max?: number;
|
|
9
|
+
values?: readonly number[];
|
|
10
|
+
}
|
|
11
|
+
| { type: 'number'; min?: number; max?: number }
|
|
12
|
+
| { type: 'string'; values?: readonly string[] }
|
|
13
|
+
| {
|
|
14
|
+
type: 'object';
|
|
15
|
+
properties: Readonly<Record<string, PreferenceSchema>>;
|
|
16
|
+
}
|
|
17
|
+
| { type: 'array'; items: PreferenceSchema };
|
|
18
|
+
|
|
19
|
+
export type PreferenceMigration = {
|
|
20
|
+
fromVersion: number;
|
|
21
|
+
fromKey?: string;
|
|
22
|
+
rename?: Readonly<Record<string, string>>;
|
|
23
|
+
defaults?: Readonly<Record<string, unknown>>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type PreferenceDefinition<T> = {
|
|
27
|
+
id: string;
|
|
28
|
+
key: string;
|
|
29
|
+
version: number;
|
|
30
|
+
defaultValue: T;
|
|
31
|
+
schema: PreferenceSchema;
|
|
32
|
+
migrations?: readonly PreferenceMigration[];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type AnyPreferenceDefinition = PreferenceDefinition<unknown>;
|
|
36
|
+
|
|
37
|
+
export type PreferenceBinding = {
|
|
38
|
+
id: string;
|
|
39
|
+
path?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type PreferenceCondition = PreferenceBinding & {
|
|
43
|
+
equals: PreferenceScalar;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type PreferenceNumberBounds = {
|
|
47
|
+
min: number;
|
|
48
|
+
max: number;
|
|
49
|
+
when?: PreferenceCondition;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type PreferenceRegistry = {
|
|
53
|
+
get<T>(definition: PreferenceDefinition<T>): T;
|
|
54
|
+
set<T>(definition: PreferenceDefinition<T>, value: T): void;
|
|
55
|
+
reset<T>(definition: PreferenceDefinition<T>): void;
|
|
56
|
+
subscribe<T>(
|
|
57
|
+
definition: PreferenceDefinition<T>,
|
|
58
|
+
listener: (value: T) => void,
|
|
59
|
+
): () => void;
|
|
60
|
+
destroy(): void;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type PreferenceRegistryConfig = {
|
|
64
|
+
definitions: readonly AnyPreferenceDefinition[];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type PreferenceSnapshot = {
|
|
68
|
+
values: Record<string, unknown>;
|
|
69
|
+
rawValues: Record<string, string>;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type PreferenceChangeDetail = {
|
|
73
|
+
id: string;
|
|
74
|
+
source: string;
|
|
75
|
+
value: unknown;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const snapshotGlobalKey = '__astronoxPreferenceSnapshot';
|
|
79
|
+
const preferenceChangeEvent = 'astronox:preference-change';
|
|
80
|
+
let registrySequence = 0;
|
|
81
|
+
|
|
82
|
+
export function definePreference<T>(
|
|
83
|
+
definition: PreferenceDefinition<T>,
|
|
84
|
+
): PreferenceDefinition<T> {
|
|
85
|
+
assertDefinition(definition);
|
|
86
|
+
if (!matchesPreferenceSchema(definition.defaultValue, definition.schema)) {
|
|
87
|
+
throw new TypeError(`Invalid default value for preference ${definition.id}`);
|
|
88
|
+
}
|
|
89
|
+
return definition;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function createPreferenceRegistry({
|
|
93
|
+
definitions,
|
|
94
|
+
}: PreferenceRegistryConfig): PreferenceRegistry {
|
|
95
|
+
const source = `registry:${++registrySequence}`;
|
|
96
|
+
const definitionsById = new Map<string, AnyPreferenceDefinition>();
|
|
97
|
+
const definitionsByStorageKey = new Map<string, AnyPreferenceDefinition>();
|
|
98
|
+
for (const definition of definitions) {
|
|
99
|
+
assertDefinition(definition);
|
|
100
|
+
if (definitionsById.has(definition.id)) {
|
|
101
|
+
throw new Error(`Duplicate preference definition: ${definition.id}`);
|
|
102
|
+
}
|
|
103
|
+
const storageKey = getPreferenceStorageKey(definition);
|
|
104
|
+
if (definitionsByStorageKey.has(storageKey)) {
|
|
105
|
+
throw new Error(`Duplicate preference storage key: ${storageKey}`);
|
|
106
|
+
}
|
|
107
|
+
definitionsById.set(definition.id, definition);
|
|
108
|
+
definitionsByStorageKey.set(storageKey, definition);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const snapshot = resolveSnapshot(definitions);
|
|
112
|
+
const listeners = new Map<string, Set<(value: unknown) => void>>();
|
|
113
|
+
|
|
114
|
+
const requireDefinition = <T>(
|
|
115
|
+
definition: PreferenceDefinition<T>,
|
|
116
|
+
): PreferenceDefinition<T> => {
|
|
117
|
+
if (!definitionsById.has(definition.id)) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Preference ${definition.id} does not belong to this registry`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return definition;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const notify = (id: string, value: unknown): void => {
|
|
126
|
+
for (const listener of listeners.get(id) ?? []) listener(value);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const apply = (
|
|
130
|
+
definition: AnyPreferenceDefinition,
|
|
131
|
+
value: unknown,
|
|
132
|
+
options: {
|
|
133
|
+
persist: boolean;
|
|
134
|
+
announce: boolean;
|
|
135
|
+
notifyIfEqual?: boolean;
|
|
136
|
+
},
|
|
137
|
+
): void => {
|
|
138
|
+
if (!matchesPreferenceSchema(value, definition.schema)) return;
|
|
139
|
+
const raw = JSON.stringify(value);
|
|
140
|
+
if (snapshot.rawValues[definition.id] === raw) {
|
|
141
|
+
if (options.notifyIfEqual) {
|
|
142
|
+
notify(definition.id, structuredCloneValue(value));
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const cloned = structuredCloneValue(value);
|
|
148
|
+
snapshot.values[definition.id] = cloned;
|
|
149
|
+
snapshot.rawValues[definition.id] = raw;
|
|
150
|
+
syncGlobalSnapshot(definition.id, cloned, raw);
|
|
151
|
+
if (options.persist) {
|
|
152
|
+
writeStorage(getPreferenceStorageKey(definition), raw);
|
|
153
|
+
}
|
|
154
|
+
notify(definition.id, cloned);
|
|
155
|
+
if (options.announce) {
|
|
156
|
+
document.dispatchEvent(
|
|
157
|
+
new CustomEvent<PreferenceChangeDetail>(preferenceChangeEvent, {
|
|
158
|
+
detail: { id: definition.id, source, value: cloned },
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const handlePreferenceChange = (event: Event): void => {
|
|
165
|
+
const detail = (event as CustomEvent<PreferenceChangeDetail>).detail;
|
|
166
|
+
if (detail?.source === source) return;
|
|
167
|
+
const definition = detail ? definitionsById.get(detail.id) : undefined;
|
|
168
|
+
if (definition) {
|
|
169
|
+
apply(definition, detail.value, {
|
|
170
|
+
persist: false,
|
|
171
|
+
announce: false,
|
|
172
|
+
notifyIfEqual: true,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const handleStorage = (event: StorageEvent): void => {
|
|
178
|
+
const definition = event.key
|
|
179
|
+
? definitionsByStorageKey.get(event.key)
|
|
180
|
+
: undefined;
|
|
181
|
+
if (!definition) return;
|
|
182
|
+
const value = parseStoredValue(event.newValue);
|
|
183
|
+
if (!matchesPreferenceSchema(value, definition.schema)) return;
|
|
184
|
+
apply(definition, value, { persist: false, announce: true });
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
document.addEventListener(preferenceChangeEvent, handlePreferenceChange);
|
|
188
|
+
window.addEventListener('storage', handleStorage);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
get<T>(definition: PreferenceDefinition<T>): T {
|
|
192
|
+
requireDefinition(definition);
|
|
193
|
+
return structuredCloneValue(snapshot.values[definition.id] as T);
|
|
194
|
+
},
|
|
195
|
+
set<T>(definition: PreferenceDefinition<T>, value: T): void {
|
|
196
|
+
requireDefinition(definition);
|
|
197
|
+
if (!matchesPreferenceSchema(value, definition.schema)) {
|
|
198
|
+
throw new TypeError(`Invalid value for preference ${definition.id}`);
|
|
199
|
+
}
|
|
200
|
+
apply(definition, value, { persist: true, announce: true });
|
|
201
|
+
},
|
|
202
|
+
reset<T>(definition: PreferenceDefinition<T>): void {
|
|
203
|
+
requireDefinition(definition);
|
|
204
|
+
apply(definition, definition.defaultValue, {
|
|
205
|
+
persist: true,
|
|
206
|
+
announce: true,
|
|
207
|
+
});
|
|
208
|
+
},
|
|
209
|
+
subscribe<T>(
|
|
210
|
+
definition: PreferenceDefinition<T>,
|
|
211
|
+
listener: (value: T) => void,
|
|
212
|
+
): () => void {
|
|
213
|
+
requireDefinition(definition);
|
|
214
|
+
const definitionListeners = listeners.get(definition.id) ?? new Set();
|
|
215
|
+
const wrapped = (value: unknown): void => listener(value as T);
|
|
216
|
+
definitionListeners.add(wrapped);
|
|
217
|
+
listeners.set(definition.id, definitionListeners);
|
|
218
|
+
return () => {
|
|
219
|
+
definitionListeners.delete(wrapped);
|
|
220
|
+
if (definitionListeners.size === 0) listeners.delete(definition.id);
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
destroy(): void {
|
|
224
|
+
document.removeEventListener(preferenceChangeEvent, handlePreferenceChange);
|
|
225
|
+
window.removeEventListener('storage', handleStorage);
|
|
226
|
+
listeners.clear();
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function getPreferenceStorageKey(
|
|
232
|
+
definition: Pick<AnyPreferenceDefinition, 'key' | 'version'>,
|
|
233
|
+
): string {
|
|
234
|
+
return `${definition.key}:v${definition.version}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function matchesPreferenceSchema(
|
|
238
|
+
value: unknown,
|
|
239
|
+
schema: PreferenceSchema,
|
|
240
|
+
): boolean {
|
|
241
|
+
if (schema.type === 'boolean') return typeof value === 'boolean';
|
|
242
|
+
if (schema.type === 'string') {
|
|
243
|
+
return (
|
|
244
|
+
typeof value === 'string' &&
|
|
245
|
+
(!schema.values || schema.values.includes(value))
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
if (schema.type === 'number' || schema.type === 'integer') {
|
|
249
|
+
if (
|
|
250
|
+
typeof value !== 'number' ||
|
|
251
|
+
!Number.isFinite(value) ||
|
|
252
|
+
(schema.type === 'integer' && !Number.isInteger(value)) ||
|
|
253
|
+
(schema.min !== undefined && value < schema.min) ||
|
|
254
|
+
(schema.max !== undefined && value > schema.max)
|
|
255
|
+
) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
return (
|
|
259
|
+
schema.type !== 'integer' ||
|
|
260
|
+
!schema.values ||
|
|
261
|
+
schema.values.includes(value)
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if (schema.type === 'array') {
|
|
265
|
+
return (
|
|
266
|
+
Array.isArray(value) &&
|
|
267
|
+
value.every((item) => matchesPreferenceSchema(item, schema.items))
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
if (!isRecord(value)) return false;
|
|
271
|
+
|
|
272
|
+
return Object.entries(schema.properties).every(([key, propertySchema]) =>
|
|
273
|
+
matchesPreferenceSchema(value[key], propertySchema),
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function resolveSnapshot(
|
|
278
|
+
definitions: readonly AnyPreferenceDefinition[],
|
|
279
|
+
): PreferenceSnapshot {
|
|
280
|
+
const snapshot = getGlobalSnapshot() ?? { values: {}, rawValues: {} };
|
|
281
|
+
setGlobalSnapshot(snapshot);
|
|
282
|
+
|
|
283
|
+
for (const definition of definitions) {
|
|
284
|
+
const bootstrapped = snapshot.values[definition.id];
|
|
285
|
+
if (matchesPreferenceSchema(bootstrapped, definition.schema)) {
|
|
286
|
+
snapshot.rawValues[definition.id] = JSON.stringify(bootstrapped);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const value = readPreference(definition);
|
|
291
|
+
snapshot.values[definition.id] = value;
|
|
292
|
+
snapshot.rawValues[definition.id] = JSON.stringify(value);
|
|
293
|
+
}
|
|
294
|
+
return snapshot;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function readPreference<T>(definition: PreferenceDefinition<T>): T {
|
|
298
|
+
const current = parseStoredValue(
|
|
299
|
+
readStorage(getPreferenceStorageKey(definition)),
|
|
300
|
+
);
|
|
301
|
+
if (matchesPreferenceSchema(current, definition.schema)) return current as T;
|
|
302
|
+
|
|
303
|
+
for (const migration of [...(definition.migrations ?? [])].toSorted(
|
|
304
|
+
(left, right) => right.fromVersion - left.fromVersion,
|
|
305
|
+
)) {
|
|
306
|
+
const key = `${migration.fromKey ?? definition.key}:v${migration.fromVersion}`;
|
|
307
|
+
const migrated = applyMigration(
|
|
308
|
+
parseStoredValue(readStorage(key)),
|
|
309
|
+
migration,
|
|
310
|
+
);
|
|
311
|
+
if (matchesPreferenceSchema(migrated, definition.schema)) {
|
|
312
|
+
writeStorage(
|
|
313
|
+
getPreferenceStorageKey(definition),
|
|
314
|
+
JSON.stringify(migrated),
|
|
315
|
+
);
|
|
316
|
+
return migrated as T;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const fallback = structuredCloneValue(definition.defaultValue);
|
|
321
|
+
writeStorage(getPreferenceStorageKey(definition), JSON.stringify(fallback));
|
|
322
|
+
return fallback;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function applyMigration(
|
|
326
|
+
value: unknown,
|
|
327
|
+
migration: PreferenceMigration,
|
|
328
|
+
): unknown {
|
|
329
|
+
if (!isRecord(value)) return value;
|
|
330
|
+
const migrated: Record<string, unknown> = { ...value };
|
|
331
|
+
for (const [from, to] of Object.entries(migration.rename ?? {})) {
|
|
332
|
+
if (from in migrated && !(to in migrated)) migrated[to] = migrated[from];
|
|
333
|
+
delete migrated[from];
|
|
334
|
+
}
|
|
335
|
+
for (const [key, fallback] of Object.entries(migration.defaults ?? {})) {
|
|
336
|
+
if (!(key in migrated)) migrated[key] = fallback;
|
|
337
|
+
}
|
|
338
|
+
return migrated;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function getGlobalSnapshot(): PreferenceSnapshot | undefined {
|
|
342
|
+
return (globalThis as Record<string, unknown>)[snapshotGlobalKey] as
|
|
343
|
+
| PreferenceSnapshot
|
|
344
|
+
| undefined;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function setGlobalSnapshot(snapshot: PreferenceSnapshot): void {
|
|
348
|
+
(globalThis as Record<string, unknown>)[snapshotGlobalKey] = snapshot;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function syncGlobalSnapshot(id: string, value: unknown, raw: string): void {
|
|
352
|
+
const globalSnapshot = getGlobalSnapshot();
|
|
353
|
+
if (!globalSnapshot) return;
|
|
354
|
+
globalSnapshot.values[id] = value;
|
|
355
|
+
globalSnapshot.rawValues[id] = raw;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function readStorage(key: string): string | null {
|
|
359
|
+
try {
|
|
360
|
+
return window.localStorage.getItem(key);
|
|
361
|
+
} catch {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function writeStorage(key: string, value: string): void {
|
|
367
|
+
try {
|
|
368
|
+
window.localStorage.setItem(key, value);
|
|
369
|
+
} catch {
|
|
370
|
+
// The in-memory snapshot remains authoritative for this document.
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function parseStoredValue(raw: string | null): unknown {
|
|
375
|
+
if (raw === null) return undefined;
|
|
376
|
+
try {
|
|
377
|
+
return JSON.parse(raw);
|
|
378
|
+
} catch {
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function assertDefinition(definition: AnyPreferenceDefinition): void {
|
|
384
|
+
if (!definition.id.trim() || !definition.key.trim()) {
|
|
385
|
+
throw new Error('Preference ids and keys must be non-empty');
|
|
386
|
+
}
|
|
387
|
+
if (!Number.isInteger(definition.version) || definition.version < 1) {
|
|
388
|
+
throw new RangeError('Preference versions must be positive integers');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function structuredCloneValue<T>(value: T): T {
|
|
393
|
+
return typeof structuredClone === 'function'
|
|
394
|
+
? structuredClone(value)
|
|
395
|
+
: (JSON.parse(JSON.stringify(value)) as T);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
399
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
400
|
+
}
|
|
@@ -57,9 +57,21 @@ const resolvedTone = tone ?? dataTone;
|
|
|
57
57
|
rel={resolvedRel}
|
|
58
58
|
target={target}
|
|
59
59
|
>
|
|
60
|
-
{
|
|
60
|
+
{
|
|
61
|
+
hasLeading ? (
|
|
62
|
+
<span class="nox-button__affix">
|
|
63
|
+
<slot name="leading" />
|
|
64
|
+
</span>
|
|
65
|
+
) : null
|
|
66
|
+
}
|
|
61
67
|
<slot />
|
|
62
|
-
{
|
|
68
|
+
{
|
|
69
|
+
hasTrailing ? (
|
|
70
|
+
<span class="nox-button__affix">
|
|
71
|
+
<slot name="trailing" />
|
|
72
|
+
</span>
|
|
73
|
+
) : null
|
|
74
|
+
}
|
|
63
75
|
</a>
|
|
64
76
|
) : href ? (
|
|
65
77
|
<span
|
|
@@ -74,9 +86,21 @@ const resolvedTone = tone ?? dataTone;
|
|
|
74
86
|
data-button-size={size}
|
|
75
87
|
data-tone={resolvedTone}
|
|
76
88
|
>
|
|
77
|
-
{
|
|
89
|
+
{
|
|
90
|
+
hasLeading ? (
|
|
91
|
+
<span class="nox-button__affix">
|
|
92
|
+
<slot name="leading" />
|
|
93
|
+
</span>
|
|
94
|
+
) : null
|
|
95
|
+
}
|
|
78
96
|
<slot />
|
|
79
|
-
{
|
|
97
|
+
{
|
|
98
|
+
hasTrailing ? (
|
|
99
|
+
<span class="nox-button__affix">
|
|
100
|
+
<slot name="trailing" />
|
|
101
|
+
</span>
|
|
102
|
+
) : null
|
|
103
|
+
}
|
|
80
104
|
</span>
|
|
81
105
|
) : (
|
|
82
106
|
<button
|
|
@@ -92,9 +116,21 @@ const resolvedTone = tone ?? dataTone;
|
|
|
92
116
|
disabled={disabled}
|
|
93
117
|
type={type}
|
|
94
118
|
>
|
|
95
|
-
{
|
|
119
|
+
{
|
|
120
|
+
hasLeading ? (
|
|
121
|
+
<span class="nox-button__affix">
|
|
122
|
+
<slot name="leading" />
|
|
123
|
+
</span>
|
|
124
|
+
) : null
|
|
125
|
+
}
|
|
96
126
|
<slot />
|
|
97
|
-
{
|
|
127
|
+
{
|
|
128
|
+
hasTrailing ? (
|
|
129
|
+
<span class="nox-button__affix">
|
|
130
|
+
<slot name="trailing" />
|
|
131
|
+
</span>
|
|
132
|
+
) : null
|
|
133
|
+
}
|
|
98
134
|
</button>
|
|
99
135
|
)
|
|
100
136
|
}
|
|
@@ -80,6 +80,12 @@
|
|
|
80
80
|
block-size: 2rem;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
.nox-button[data-button-icon-only] > [data-tone] {
|
|
84
|
+
display: grid;
|
|
85
|
+
place-items: center;
|
|
86
|
+
color: var(--nox-tone-color);
|
|
87
|
+
}
|
|
88
|
+
|
|
83
89
|
.nox-button[data-button-icon-only][data-button-size='sm'] svg {
|
|
84
90
|
inline-size: 1.5rem;
|
|
85
91
|
block-size: 1.5rem;
|
|
@@ -90,6 +96,19 @@
|
|
|
90
96
|
block-size: 2.25rem;
|
|
91
97
|
}
|
|
92
98
|
|
|
99
|
+
.nox-button__affix {
|
|
100
|
+
display: grid;
|
|
101
|
+
inline-size: 1.25rem;
|
|
102
|
+
block-size: 1.25rem;
|
|
103
|
+
place-items: center;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.nox-button__affix svg {
|
|
107
|
+
display: block;
|
|
108
|
+
inline-size: 100%;
|
|
109
|
+
block-size: 100%;
|
|
110
|
+
}
|
|
111
|
+
|
|
93
112
|
.nox-button[data-button-borderless] {
|
|
94
113
|
border: 0;
|
|
95
114
|
border-radius: 0;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
3
|
+
import type { Tone } from '../../../theme/contracts';
|
|
4
|
+
import Button from '../Button/Button.astro';
|
|
5
|
+
|
|
6
|
+
type Props = HTMLAttributes<'button'> & {
|
|
7
|
+
label: string;
|
|
8
|
+
pendingLabel?: string;
|
|
9
|
+
tone?: Tone;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
label,
|
|
14
|
+
pendingLabel = label,
|
|
15
|
+
tone,
|
|
16
|
+
...attrs
|
|
17
|
+
}: Props = Astro.props;
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
<Button
|
|
21
|
+
{...attrs}
|
|
22
|
+
type="button"
|
|
23
|
+
prominent
|
|
24
|
+
tone={tone}
|
|
25
|
+
data-auth-provider-action
|
|
26
|
+
data-auth-provider-label={label}
|
|
27
|
+
data-auth-provider-pending-label={pendingLabel}
|
|
28
|
+
>
|
|
29
|
+
{
|
|
30
|
+
Astro.slots.has('leading') ? (
|
|
31
|
+
<span slot="leading"><slot name="leading" /></span>
|
|
32
|
+
) : null
|
|
33
|
+
}
|
|
34
|
+
<span data-auth-provider-action-label>{label}</span>
|
|
35
|
+
</Button>
|
|
@@ -4,29 +4,37 @@ import type { DisplayMedia } from '../../../display/contracts';
|
|
|
4
4
|
import type { Tone } from '../../../theme/contracts';
|
|
5
5
|
import Media from '../../../primitives/media/Media/Media.astro';
|
|
6
6
|
|
|
7
|
-
type
|
|
7
|
+
type SharedProps = {
|
|
8
8
|
accentTone?: Tone;
|
|
9
9
|
label: string;
|
|
10
10
|
media: DisplayMedia;
|
|
11
11
|
tone?: Tone;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
type Props = SharedProps &
|
|
15
|
+
Omit<HTMLAttributes<'a'>, 'href' | 'type'> & {
|
|
16
|
+
href?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
14
19
|
const {
|
|
15
20
|
accentTone,
|
|
16
21
|
class: className,
|
|
17
22
|
'data-tone': dataTone,
|
|
23
|
+
href,
|
|
18
24
|
label,
|
|
19
25
|
media,
|
|
20
26
|
tone,
|
|
21
27
|
...attrs
|
|
22
28
|
}: Props = Astro.props;
|
|
23
29
|
const resolvedTone = tone ?? dataTone;
|
|
30
|
+
const Root = href ? 'a' : 'button';
|
|
24
31
|
---
|
|
25
32
|
|
|
26
|
-
<
|
|
33
|
+
<Root
|
|
27
34
|
{...attrs}
|
|
28
35
|
class:list={['media-trigger', className]}
|
|
29
|
-
|
|
36
|
+
href={href}
|
|
37
|
+
type={href ? undefined : 'button'}
|
|
30
38
|
aria-label={label}
|
|
31
39
|
data-tone={resolvedTone}
|
|
32
40
|
data-widget="media-trigger"
|
|
@@ -52,4 +60,4 @@ const resolvedTone = tone ?? dataTone;
|
|
|
52
60
|
</span>
|
|
53
61
|
) : null
|
|
54
62
|
}
|
|
55
|
-
</
|
|
63
|
+
</Root>
|
|
@@ -5,6 +5,7 @@ type Props = HTMLAttributes<'div'> & {
|
|
|
5
5
|
id?: string;
|
|
6
6
|
inset?: 'default' | 'none';
|
|
7
7
|
overlayBoundary?: boolean;
|
|
8
|
+
scrollable?: boolean;
|
|
8
9
|
};
|
|
9
10
|
|
|
10
11
|
const {
|
|
@@ -12,6 +13,7 @@ const {
|
|
|
12
13
|
id,
|
|
13
14
|
inset = 'default',
|
|
14
15
|
overlayBoundary = false,
|
|
16
|
+
scrollable = false,
|
|
15
17
|
...attrs
|
|
16
18
|
}: Props = Astro.props;
|
|
17
19
|
---
|
|
@@ -24,6 +26,7 @@ const {
|
|
|
24
26
|
data-surface
|
|
25
27
|
data-inset={inset}
|
|
26
28
|
data-overlay-boundary={overlayBoundary ? true : undefined}
|
|
29
|
+
data-scrollable={scrollable ? true : undefined}
|
|
27
30
|
>
|
|
28
31
|
<div class="card-body__content">
|
|
29
32
|
<slot />
|
|
@@ -41,6 +41,16 @@
|
|
|
41
41
|
overflow: clip;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
.card-body[data-scrollable] {
|
|
45
|
+
overflow: auto;
|
|
46
|
+
overscroll-behavior: contain;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.card-body[data-scrollable] .card-body__content {
|
|
50
|
+
min-block-size: 100%;
|
|
51
|
+
align-self: flex-start;
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
.card-body p {
|
|
45
55
|
margin: 0;
|
|
46
56
|
}
|
|
@@ -112,6 +112,7 @@ const listItems =
|
|
|
112
112
|
<div class="markdown-content__change">
|
|
113
113
|
<span class="markdown-content__change-label">
|
|
114
114
|
{node.label.map((child) => <MarkdownInline node={child} />)}
|
|
115
|
+
<span>:</span>
|
|
115
116
|
</span>
|
|
116
117
|
{node.presentation === 'statement' ? (
|
|
117
118
|
<span class="markdown-content__change-values">
|