@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
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# AstroNox Core
|
|
2
2
|
|
|
3
|
-
AstroNox Core is
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
AstroNox Core is a reusable foundation for Astro applications. It provides
|
|
4
|
+
components, composition primitives, interaction patterns, display contracts,
|
|
5
|
+
internationalization and SEO infrastructure, and a complete shared stylesheet.
|
|
6
6
|
|
|
7
7
|
Product data, product routes, branding, integrations, and business rules stay
|
|
8
8
|
in the consuming site.
|
|
@@ -33,11 +33,13 @@ import { Stack, SurfaceGroup } from '@astronox/core/composition';
|
|
|
33
33
|
</Stack>
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Load the shared visual system once from the consuming application's
|
|
36
|
+
Load the shared visual system once from the consuming application's root
|
|
37
|
+
layout:
|
|
37
38
|
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
```astro
|
|
40
|
+
---
|
|
41
|
+
import '@astronox/core/styles.css';
|
|
42
|
+
---
|
|
41
43
|
```
|
|
42
44
|
|
|
43
45
|
Use the remaining entry points for layouts, patterns, primitives, display
|
|
@@ -50,26 +52,3 @@ and responsive media presentation are available through the `i18n`, `routing`,
|
|
|
50
52
|
definitions, translations, route names, product metadata, and assets. Generic
|
|
51
53
|
component labels can extend `defaultCoreMessages` through
|
|
52
54
|
`resolveCoreMessages()` while retaining the English fallback.
|
|
53
|
-
|
|
54
|
-
## Local Development
|
|
55
|
-
|
|
56
|
-
`astronox-core` is the only source of truth. A consuming project may link this
|
|
57
|
-
package locally while developing a shared change, but committed dependencies
|
|
58
|
-
must always reference a released package version.
|
|
59
|
-
|
|
60
|
-
The expected workflow is:
|
|
61
|
-
|
|
62
|
-
1. Keep an exact published version in the consumer manifest and lockfile.
|
|
63
|
-
2. Link the sibling package locally without touching either file with
|
|
64
|
-
`npm install --no-save --package-lock=false ../astronox-core`.
|
|
65
|
-
3. Implement and validate the generic change through the linked consumer.
|
|
66
|
-
4. Publish a prerelease or stable package version.
|
|
67
|
-
5. Update the exact consumer dependency before deployment.
|
|
68
|
-
|
|
69
|
-
Do not edit installed package files or copy `src` folders between projects.
|
|
70
|
-
|
|
71
|
-
## Releases
|
|
72
|
-
|
|
73
|
-
AstroNox Core follows semantic versioning. Public entry points are compatibility
|
|
74
|
-
boundaries; internal files may change without notice. The initial `0.x` series
|
|
75
|
-
is reserved for stabilizing the API with the first production consumers.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astronox/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Reusable Astro components, patterns, styles, and infrastructure for AstroNox sites.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -30,10 +30,15 @@
|
|
|
30
30
|
"exports": {
|
|
31
31
|
".": "./src/index.ts",
|
|
32
32
|
"./client": "./src/client/index.ts",
|
|
33
|
+
"./admin": "./src/admin/index.ts",
|
|
34
|
+
"./auth": "./src/auth/index.ts",
|
|
35
|
+
"./auth/firebase": "./src/auth/firebase/index.ts",
|
|
36
|
+
"./auth/firebase/client": "./src/auth/firebase/client.ts",
|
|
33
37
|
"./components": "./src/components/index.ts",
|
|
34
38
|
"./composition": "./src/composition/index.ts",
|
|
35
39
|
"./config": "./src/config/index.ts",
|
|
36
40
|
"./data": "./src/data/index.ts",
|
|
41
|
+
"./data/firestore/client": "./src/data/firestore/client/index.ts",
|
|
37
42
|
"./display": "./src/display/index.ts",
|
|
38
43
|
"./i18n": "./src/i18n/index.ts",
|
|
39
44
|
"./layouts": "./src/layouts/index.ts",
|
|
@@ -62,6 +67,7 @@
|
|
|
62
67
|
"astro": "^7.0.0"
|
|
63
68
|
},
|
|
64
69
|
"dependencies": {
|
|
70
|
+
"firebase": "^12.0.0",
|
|
65
71
|
"zod": "4.4.3"
|
|
66
72
|
},
|
|
67
73
|
"devDependencies": {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AuthActor, AuthSession } from '../auth';
|
|
2
|
+
|
|
3
|
+
export type AdminPage<TItem, TCursor = string> = {
|
|
4
|
+
items: readonly TItem[];
|
|
5
|
+
nextCursor?: TCursor;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export interface AdminResource<
|
|
9
|
+
TItem,
|
|
10
|
+
TCreateInput,
|
|
11
|
+
TUpdateInput = TCreateInput,
|
|
12
|
+
TIndexInput = void,
|
|
13
|
+
> {
|
|
14
|
+
getIndex(input: TIndexInput): Promise<AdminPage<TItem>>;
|
|
15
|
+
getDetail(input: { id: string }): Promise<TItem | null>;
|
|
16
|
+
create(input: TCreateInput, actor: AuthActor): Promise<TItem>;
|
|
17
|
+
update(
|
|
18
|
+
input: { id: string; value: TUpdateInput },
|
|
19
|
+
actor: AuthActor,
|
|
20
|
+
): Promise<TItem>;
|
|
21
|
+
delete(input: { id: string }, actor: AuthActor): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type AdminAccess<TResources extends Record<string, unknown>> = {
|
|
25
|
+
actor: AuthActor;
|
|
26
|
+
resources: TResources;
|
|
27
|
+
session: AuthSession;
|
|
28
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AuthModule } from '../auth';
|
|
2
|
+
import type { AdminAccess } from './contracts';
|
|
3
|
+
|
|
4
|
+
export function createAdminModule<
|
|
5
|
+
TCredential,
|
|
6
|
+
TCapability extends string,
|
|
7
|
+
TResources extends Record<string, unknown>,
|
|
8
|
+
>({
|
|
9
|
+
auth,
|
|
10
|
+
capability,
|
|
11
|
+
resources,
|
|
12
|
+
}: {
|
|
13
|
+
auth: AuthModule<TCredential, TCapability>;
|
|
14
|
+
capability: TCapability;
|
|
15
|
+
resources: TResources;
|
|
16
|
+
}) {
|
|
17
|
+
return {
|
|
18
|
+
async getActor(token?: string | null) {
|
|
19
|
+
return (await auth.getSession(token))?.actor ?? null;
|
|
20
|
+
},
|
|
21
|
+
async requireAccess(
|
|
22
|
+
token?: string | null,
|
|
23
|
+
): Promise<AdminAccess<TResources>> {
|
|
24
|
+
const session = await auth.requireCapability(token, capability);
|
|
25
|
+
return { actor: session.actor, resources, session };
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type AuthClaims = Readonly<Record<string, unknown>>;
|
|
2
|
+
|
|
3
|
+
export type AuthActor = {
|
|
4
|
+
id: string;
|
|
5
|
+
claims: AuthClaims;
|
|
6
|
+
displayName?: string;
|
|
7
|
+
email?: string;
|
|
8
|
+
imageUrl?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type AuthSession = {
|
|
12
|
+
actor: AuthActor;
|
|
13
|
+
expiresAt: Date;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type AuthSessionGrant = AuthSession & {
|
|
17
|
+
token: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export interface IdentityProvider<TCredential = string> {
|
|
21
|
+
authenticate(credential: TCredential): Promise<AuthActor>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SessionProvider<TCredential = string> {
|
|
25
|
+
create(
|
|
26
|
+
credential: TCredential,
|
|
27
|
+
actor: AuthActor,
|
|
28
|
+
): Promise<AuthSessionGrant>;
|
|
29
|
+
get(token: string): Promise<AuthSession | null>;
|
|
30
|
+
revoke(token: string): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AuthorizationPolicy<TCapability extends string = string> {
|
|
34
|
+
allows(actor: AuthActor, capability: TCapability): boolean | Promise<boolean>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class AuthenticationError extends Error {
|
|
38
|
+
readonly code = 'AUTHENTICATION_REQUIRED';
|
|
39
|
+
|
|
40
|
+
constructor(message = 'Authentication is required') {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = 'AuthenticationError';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class AuthorizationError extends Error {
|
|
47
|
+
readonly code = 'AUTHORIZATION_DENIED';
|
|
48
|
+
|
|
49
|
+
constructor(message = 'Access is denied') {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = 'AuthorizationError';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { getApps, initializeApp } from 'firebase/app';
|
|
2
|
+
import {
|
|
3
|
+
GoogleAuthProvider,
|
|
4
|
+
getAuth,
|
|
5
|
+
onAuthStateChanged,
|
|
6
|
+
signInWithPopup,
|
|
7
|
+
signOut,
|
|
8
|
+
type User,
|
|
9
|
+
} from 'firebase/auth';
|
|
10
|
+
import type { AuthActor } from '../contracts';
|
|
11
|
+
import type { FirebaseClientConfig } from './contracts';
|
|
12
|
+
|
|
13
|
+
export type FirebaseGoogleAuthClient = {
|
|
14
|
+
observe(listener: (actor: AuthActor | null) => void): () => void;
|
|
15
|
+
signIn(): Promise<AuthActor>;
|
|
16
|
+
signOut(): Promise<void>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function toAuthActor(user: User): AuthActor {
|
|
20
|
+
return {
|
|
21
|
+
id: user.uid,
|
|
22
|
+
claims: {},
|
|
23
|
+
displayName: user.displayName ?? undefined,
|
|
24
|
+
email: user.email ?? undefined,
|
|
25
|
+
imageUrl: user.photoURL ?? undefined,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createFirebaseGoogleAuthClient(
|
|
30
|
+
config: FirebaseClientConfig,
|
|
31
|
+
): FirebaseGoogleAuthClient {
|
|
32
|
+
const app =
|
|
33
|
+
getApps().find((candidate) => candidate.name === config.projectId) ??
|
|
34
|
+
initializeApp(config, config.projectId);
|
|
35
|
+
const auth = getAuth(app);
|
|
36
|
+
const provider = new GoogleAuthProvider();
|
|
37
|
+
provider.setCustomParameters({ prompt: 'select_account' });
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
observe(listener) {
|
|
41
|
+
return onAuthStateChanged(auth, (user) => {
|
|
42
|
+
listener(user ? toAuthActor(user) : null);
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
async signIn() {
|
|
46
|
+
const result = await signInWithPopup(auth, provider);
|
|
47
|
+
return toAuthActor(result.user);
|
|
48
|
+
},
|
|
49
|
+
async signOut() {
|
|
50
|
+
await signOut(auth);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './contracts';
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthenticationError,
|
|
3
|
+
AuthorizationError,
|
|
4
|
+
type AuthActor,
|
|
5
|
+
type AuthSession,
|
|
6
|
+
type AuthSessionGrant,
|
|
7
|
+
type AuthorizationPolicy,
|
|
8
|
+
type IdentityProvider,
|
|
9
|
+
type SessionProvider,
|
|
10
|
+
} from './contracts';
|
|
11
|
+
|
|
12
|
+
export type AuthModule<TCredential, TCapability extends string> = {
|
|
13
|
+
createSession(credential: TCredential): Promise<AuthSessionGrant>;
|
|
14
|
+
createAuthorizedSession(
|
|
15
|
+
credential: TCredential,
|
|
16
|
+
capability: TCapability,
|
|
17
|
+
): Promise<AuthSessionGrant>;
|
|
18
|
+
getSession(token?: string | null): Promise<AuthSession | null>;
|
|
19
|
+
requireCapability(
|
|
20
|
+
token: string | null | undefined,
|
|
21
|
+
capability: TCapability,
|
|
22
|
+
): Promise<AuthSession>;
|
|
23
|
+
revokeSession(token?: string | null): Promise<void>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function createAuthModule<TCredential, TCapability extends string>({
|
|
27
|
+
authorizationPolicy,
|
|
28
|
+
identityProvider,
|
|
29
|
+
sessionProvider,
|
|
30
|
+
}: {
|
|
31
|
+
authorizationPolicy: AuthorizationPolicy<TCapability>;
|
|
32
|
+
identityProvider: IdentityProvider<TCredential>;
|
|
33
|
+
sessionProvider: SessionProvider<TCredential>;
|
|
34
|
+
}): AuthModule<TCredential, TCapability> {
|
|
35
|
+
async function authenticate(credential: TCredential): Promise<AuthActor> {
|
|
36
|
+
return identityProvider.authenticate(credential);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function authorize(
|
|
40
|
+
actor: AuthActor,
|
|
41
|
+
capability: TCapability,
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
if (!(await authorizationPolicy.allows(actor, capability))) {
|
|
44
|
+
throw new AuthorizationError();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
async createSession(credential) {
|
|
50
|
+
const actor = await authenticate(credential);
|
|
51
|
+
return sessionProvider.create(credential, actor);
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
async createAuthorizedSession(credential, capability) {
|
|
55
|
+
const actor = await authenticate(credential);
|
|
56
|
+
await authorize(actor, capability);
|
|
57
|
+
return sessionProvider.create(credential, actor);
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async getSession(token) {
|
|
61
|
+
if (!token) return null;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
return await sessionProvider.get(token);
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
async requireCapability(token, capability) {
|
|
71
|
+
if (!token) throw new AuthenticationError();
|
|
72
|
+
|
|
73
|
+
const session = await sessionProvider.get(token);
|
|
74
|
+
if (!session) throw new AuthenticationError();
|
|
75
|
+
await authorize(session.actor, capability);
|
|
76
|
+
return session;
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
async revokeSession(token) {
|
|
80
|
+
if (!token) return;
|
|
81
|
+
await sessionProvider.revoke(token);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function requireClaim<TCapability extends string>(
|
|
87
|
+
claim: string,
|
|
88
|
+
expected: unknown,
|
|
89
|
+
capability?: TCapability,
|
|
90
|
+
): AuthorizationPolicy<TCapability> {
|
|
91
|
+
return {
|
|
92
|
+
allows(actor: AuthActor, requestedCapability: TCapability) {
|
|
93
|
+
if (capability !== undefined && requestedCapability !== capability) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return actor.claims[claim] === expected;
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
package/src/client/index.ts
CHANGED