smoodly 0.0.50 → 0.0.52
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 +14 -13
- package/dist/admin/auth-config.d.ts +6 -5
- package/dist/admin/auth.d.ts +3 -11
- package/dist/admin/auth.js +1 -30
- package/dist/admin/browser/fetch-op.js +3 -2
- package/dist/admin/browser/main.js +6 -2
- package/dist/admin/client-ops.d.ts +5 -4
- package/dist/admin/client-ops.js +14 -11
- package/dist/admin/delivery-mode.d.ts +1 -1
- package/dist/admin/delivery-mode.js +2 -2
- package/dist/admin/drift.d.ts +0 -2
- package/dist/admin/drift.js +1 -1
- package/dist/admin/editor/EditorView.d.ts +0 -3
- package/dist/admin/editor/EditorView.js +11 -16
- package/dist/admin/editor/FloatingToolbar.d.ts +0 -1
- package/dist/admin/editor/FloatingToolbar.js +1 -1
- package/dist/admin/editor/SeoSettings.d.ts +0 -2
- package/dist/admin/editor/SeoSettings.js +2 -2
- package/dist/admin/editor/canvas-address.d.ts +8 -0
- package/dist/admin/editor/canvas-address.js +18 -2
- package/dist/admin/editor/canvas-diagnosis.d.ts +2 -0
- package/dist/admin/editor/canvas-diagnosis.js +3 -0
- package/dist/admin/editor/inline-dom.d.ts +0 -4
- package/dist/admin/editor/inline-dom.js +1 -1
- package/dist/admin/editor/preview-grant.d.ts +6 -0
- package/dist/admin/editor/preview-grant.js +25 -0
- package/dist/admin/editor/useCanvasDiagnosis.js +2 -2
- package/dist/admin/files.generated.js +1 -1
- package/dist/admin/fixed-nodes.d.ts +0 -6
- package/dist/admin/fixed-nodes.js +1 -81
- package/dist/admin/forms/CropEditor.d.ts +4 -2
- package/dist/admin/forms/CropEditor.js +58 -5
- package/dist/admin/forms/ImagePreview.d.ts +3 -1
- package/dist/admin/forms/ImagePreview.js +9 -3
- package/dist/admin/forms/MediaGrid.js +2 -1
- package/dist/admin/forms/MediaPicker.js +9 -6
- package/dist/admin/forms/MediaWidget.js +11 -9
- package/dist/admin/forms/assets-context.d.ts +7 -3
- package/dist/admin/forms/assets-context.js +5 -3
- package/dist/admin/forms/crop-editor.d.ts +24 -2
- package/dist/admin/forms/crop-editor.js +36 -2
- package/dist/admin/forms/date-picker.d.ts +0 -1
- package/dist/admin/forms/date-picker.js +1 -1
- package/dist/admin/forms/tabs.d.ts +0 -1
- package/dist/admin/forms/tabs.js +1 -1
- package/dist/admin/forms/upload.d.ts +18 -11
- package/dist/admin/forms/upload.js +62 -24
- package/dist/admin/global-items.d.ts +0 -3
- package/dist/admin/global-items.js +0 -54
- package/dist/admin/handler.d.ts +22 -28
- package/dist/admin/handler.js +146 -47
- package/dist/admin/health.d.ts +0 -4
- package/dist/admin/index.d.ts +5 -8
- package/dist/admin/index.js +3 -6
- package/dist/admin/ops.d.ts +36 -22
- package/dist/admin/page-data.d.ts +14 -6
- package/dist/admin/page-data.js +1 -1
- package/dist/admin/session-flow.d.ts +43 -0
- package/dist/admin/session-flow.js +94 -0
- package/dist/admin/shell/AdminApp.d.ts +8 -3
- package/dist/admin/shell/AdminApp.js +37 -58
- package/dist/admin/shell/ContentHealthPage.js +1 -3
- package/dist/admin/shell/GlobalFields.d.ts +0 -3
- package/dist/admin/shell/GlobalFields.js +1 -1
- package/dist/admin/shell/IconRail.js +5 -4
- package/dist/admin/shell/MediaScreen.js +9 -6
- package/dist/admin/shell/MembersPage.d.ts +1 -2
- package/dist/admin/shell/MembersPage.js +19 -90
- package/dist/admin/shell/SettingsView.d.ts +1 -2
- package/dist/admin/shell/SettingsView.js +2 -2
- package/dist/admin/shell/SignedOutView.d.ts +5 -0
- package/dist/admin/shell/SignedOutView.js +9 -0
- package/dist/admin/shell/console-links.d.ts +5 -0
- package/dist/admin/shell/console-links.js +11 -0
- package/dist/admin/shell/members-list.d.ts +1 -14
- package/dist/admin/shell/members-list.js +2 -23
- package/dist/admin/shell/pages-tree.d.ts +0 -2
- package/dist/admin/shell/pages-tree.js +1 -1
- package/dist/admin/shell/presence-socket.d.ts +10 -0
- package/dist/admin/shell/presence-socket.js +191 -0
- package/dist/admin/shell/presence.d.ts +6 -18
- package/dist/admin/shell/presence.js +4 -11
- package/dist/admin/shell/session.d.ts +20 -12
- package/dist/admin/shell/session.js +113 -47
- package/dist/admin/ui/LocaleSwitcher.d.ts +0 -4
- package/dist/admin/ui/LocaleSwitcher.js +2 -2
- package/dist/admin/ui/primitives.d.ts +4 -14
- package/dist/admin/ui/primitives.js +5 -8
- package/dist/admin/ui/theme.js +13 -2
- package/dist/admin/validate.js +10 -1
- package/dist/api-client.d.ts +26 -0
- package/dist/api-client.js +107 -0
- package/dist/assemble.d.ts +4 -41
- package/dist/assemble.js +4 -302
- package/dist/asset-store.d.ts +28 -30
- package/dist/asset-store.js +26 -121
- package/dist/astro/components/Head.astro +4 -1
- package/dist/astro/components/Image.astro +12 -17
- package/dist/astro/components/Video.astro +5 -2
- package/dist/astro/head.d.ts +3 -0
- package/dist/astro/head.js +24 -0
- package/dist/astro/index.js +54 -25
- package/dist/astro/locale-paths.d.ts +5 -2
- package/dist/astro/locale-paths.js +10 -2
- package/dist/astro/routes/admin.js +9 -7
- package/dist/astro/runtime.d.ts +2 -1
- package/dist/astro/runtime.js +4 -3
- package/dist/astro/server.d.ts +7 -11
- package/dist/astro/server.js +22 -28
- package/dist/astro/shell.d.ts +2 -2
- package/dist/astro/static-media-node.d.ts +4 -0
- package/dist/astro/static-media-node.js +25 -0
- package/dist/astro/static-media.d.ts +17 -0
- package/dist/astro/static-media.js +55 -0
- package/dist/astro/static.d.ts +5 -10
- package/dist/astro/static.js +13 -29
- package/dist/canvas-marker.d.ts +14 -0
- package/dist/canvas-marker.js +36 -0
- package/dist/cli/api.d.ts +14 -0
- package/dist/cli/api.js +48 -0
- package/dist/cli/app-env.d.ts +2 -0
- package/dist/cli/app-env.js +20 -0
- package/dist/cli/export.d.ts +16 -2
- package/dist/cli/export.js +52 -35
- package/dist/cli/import.d.ts +7 -11
- package/dist/cli/import.js +50 -54
- package/dist/cli/load-config.d.ts +0 -1
- package/dist/cli/load-config.js +1 -1
- package/dist/cli/main.d.ts +4 -12
- package/dist/cli/main.js +26 -72
- package/dist/cli/push.d.ts +6 -0
- package/dist/cli/push.js +31 -0
- package/dist/collections.d.ts +0 -1
- package/dist/collections.js +1 -1
- package/dist/config.d.ts +20 -5
- package/dist/config.js +18 -1
- package/dist/content/content-api.d.ts +22 -23
- package/dist/content/content-api.js +46 -56
- package/dist/content/mime.d.ts +1 -0
- package/dist/content/mime.js +6 -0
- package/dist/deploy-store.d.ts +19 -0
- package/dist/deploy-store.js +1 -0
- package/dist/entry-store.d.ts +3 -89
- package/dist/entry-store.js +2 -550
- package/dist/env.d.ts +27 -20
- package/dist/env.js +38 -50
- package/dist/fields.d.ts +11 -3
- package/dist/fields.js +33 -2
- package/dist/global-id.d.ts +2 -0
- package/dist/global-id.js +4 -2
- package/dist/image/SmoodlyImage.d.ts +9 -5
- package/dist/image/SmoodlyImage.js +29 -15
- package/dist/image/SmoodlyVideo.d.ts +1 -1
- package/dist/image/SmoodlyVideo.js +3 -2
- package/dist/image/index.d.ts +2 -3
- package/dist/image/index.js +0 -2
- package/dist/image/markup.d.ts +53 -0
- package/dist/image/markup.js +111 -0
- package/dist/index.d.ts +12 -22
- package/dist/index.js +7 -20
- package/dist/media-cut.d.ts +44 -0
- package/dist/media-cut.js +70 -0
- package/dist/media.d.ts +57 -3
- package/dist/media.js +125 -15
- package/dist/member-store.d.ts +3 -22
- package/dist/member-store.js +1 -53
- package/dist/next/admin-route.d.ts +1 -1
- package/dist/next/admin-route.js +5 -3
- package/dist/next/meta.js +2 -3
- package/dist/next/page-renderer.d.ts +2 -2
- package/dist/next/page-renderer.js +28 -15
- package/dist/next/queries.d.ts +2 -2
- package/dist/path-index.d.ts +0 -24
- package/dist/path-index.js +0 -98
- package/dist/push.d.ts +30 -0
- package/dist/push.js +52 -0
- package/dist/read-counter.d.ts +2 -0
- package/dist/read-counter.js +6 -0
- package/dist/reads.d.ts +9 -8
- package/dist/reads.js +7 -3
- package/dist/registry-json.d.ts +11 -0
- package/dist/registry-json.js +81 -0
- package/dist/render.d.ts +0 -1
- package/dist/render.js +1 -1
- package/dist/resolve.d.ts +12 -6
- package/dist/resolve.js +11 -8
- package/dist/revalidate.d.ts +8 -1
- package/dist/revalidate.js +8 -1
- package/dist/richtext/index.d.ts +1 -1
- package/dist/section-wrapper.d.ts +1 -1
- package/dist/section-wrapper.js +0 -9
- package/dist/signed-message.d.ts +26 -0
- package/dist/signed-message.js +103 -0
- package/dist/site.d.ts +59 -18
- package/dist/site.js +21 -252
- package/dist/space-transfer.d.ts +5 -2
- package/dist/space-transfer.js +17 -5
- package/dist/store.d.ts +2 -80
- package/dist/store.js +3 -489
- package/dist/toolset.d.ts +2 -1
- package/dist/toolset.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -15
- package/dist/admin/ops-impl.d.ts +0 -51
- package/dist/admin/ops-impl.js +0 -1400
- package/dist/admin/shell/InviteModal.d.ts +0 -6
- package/dist/admin/shell/InviteModal.js +0 -28
- package/dist/admin/shell/LoginView.d.ts +0 -5
- package/dist/admin/shell/LoginView.js +0 -80
- package/dist/admin/shell/SchemaNotice.d.ts +0 -4
- package/dist/admin/shell/SchemaNotice.js +0 -4
- package/dist/admin/shell/SetPasswordView.d.ts +0 -7
- package/dist/admin/shell/SetPasswordView.js +0 -41
- package/dist/admin/shell/presence-realtime.d.ts +0 -3
- package/dist/admin/shell/presence-realtime.js +0 -32
- package/dist/admin/supabase-auth.d.ts +0 -7
- package/dist/admin/supabase-auth.js +0 -30
- package/dist/astro/deploy-deps.d.ts +0 -10
- package/dist/astro/deploy-deps.js +0 -8
- package/dist/astro/image.d.ts +0 -27
- package/dist/astro/image.js +0 -29
- package/dist/bind.d.ts +0 -19
- package/dist/bind.js +0 -34
- package/dist/cli/add-admin.d.ts +0 -11
- package/dist/cli/add-admin.js +0 -25
- package/dist/cli/load-collections.d.ts +0 -2
- package/dist/cli/load-collections.js +0 -27
- package/dist/cli/migrate.d.ts +0 -43
- package/dist/cli/migrate.js +0 -138
- package/dist/cli/rebuild.d.ts +0 -23
- package/dist/cli/rebuild.js +0 -22
- package/dist/cli/space-io.d.ts +0 -41
- package/dist/cli/space-io.js +0 -84
- package/dist/client.d.ts +0 -11
- package/dist/client.js +0 -54
- package/dist/current-space.d.ts +0 -4
- package/dist/current-space.js +0 -9
- package/dist/document-stamp.d.ts +0 -7
- package/dist/document-stamp.js +0 -48
- package/dist/image/loaders.d.ts +0 -15
- package/dist/image/loaders.js +0 -5
- package/dist/image/next-config.d.ts +0 -18
- package/dist/image/next-config.js +0 -32
- package/dist/image/remote-pattern.d.ts +0 -1
- package/dist/image/remote-pattern.js +0 -5
- package/dist/migrations.d.ts +0 -37
- package/dist/migrations.js +0 -73
- package/dist/project.d.ts +0 -88
- package/dist/project.js +0 -231
- package/dist/published.d.ts +0 -142
- package/dist/published.js +0 -164
- package/dist/ref-index.d.ts +0 -19
- package/dist/ref-index.js +0 -0
- package/dist/schema-read.d.ts +0 -23
- package/dist/schema-read.js +0 -50
- package/dist/schema-version.d.ts +0 -19
- package/dist/schema-version.js +0 -34
- package/dist/sql-space.d.ts +0 -44
- package/dist/sql-space.js +0 -289
- package/dist/sql.d.ts +0 -15
- package/dist/sql.js +0 -556
- package/dist/supabase-asset-store.d.ts +0 -21
- package/dist/supabase-asset-store.js +0 -170
- package/dist/supabase-entry-store.d.ts +0 -92
- package/dist/supabase-entry-store.js +0 -824
- package/dist/supabase-member-store.d.ts +0 -21
- package/dist/supabase-member-store.js +0 -96
- package/dist/supabase-path-index.d.ts +0 -30
- package/dist/supabase-path-index.js +0 -138
- package/dist/supabase-published-store.d.ts +0 -56
- package/dist/supabase-published-store.js +0 -314
- package/dist/supabase-store.d.ts +0 -103
- package/dist/supabase-store.js +0 -678
- package/migrations/20260926120000_smoodly_v1.sql +0 -535
- package/migrations/20260926130000_smoodly_v2.sql +0 -26
- package/migrations/20260926140000_smoodly_v3.sql +0 -38
- package/migrations/20260926150000_smoodly_v4.sql +0 -41
- package/migrations/20260929120000_smoodly_v5.sql +0 -56
- package/migrations/20260929135523_smoodly_v6.sql +0 -200
- package/migrations/20260930040956_smoodly_v7.sql +0 -84
- package/migrations/20261007120000_smoodly_v8.sql +0 -154
- package/migrations/20261007200000_smoodly_v9.sql +0 -30
- package/migrations/20261008120000_smoodly_v10.sql +0 -47
- package/migrations/20261009120000_smoodly_v11.sql +0 -49
|
@@ -1,22 +1,30 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type
|
|
3
|
-
export type
|
|
4
|
-
export type Arrival = "invite" | "recovery";
|
|
5
|
-
/** What an auth email link says it is: GoTrue's implicit flow puts `type=` in the fragment; a query form is accepted too. */
|
|
6
|
-
export declare function arrivalOf(hash: string, search: string): Arrival | null;
|
|
1
|
+
import type { SignInConfig } from "../auth-config.ts";
|
|
2
|
+
import { type StoredSession } from "../session-flow.ts";
|
|
3
|
+
export type SessionUser = StoredSession["user"];
|
|
7
4
|
export type SessionState = {
|
|
8
5
|
status: "loading";
|
|
9
6
|
} | {
|
|
10
7
|
status: "signed_out";
|
|
8
|
+
reason: "signed_out" | "refused";
|
|
9
|
+
message?: string;
|
|
11
10
|
} | {
|
|
12
11
|
status: "signed_in";
|
|
13
|
-
|
|
12
|
+
user: SessionUser;
|
|
14
13
|
};
|
|
15
|
-
export declare function
|
|
16
|
-
|
|
14
|
+
export declare function useSpaceSession({ apiUrl, spaceId, signIn }: {
|
|
15
|
+
apiUrl: string;
|
|
16
|
+
spaceId: string;
|
|
17
|
+
signIn: SignInConfig;
|
|
18
|
+
}): {
|
|
17
19
|
state: SessionState;
|
|
20
|
+
/** The current pass, read per call. */
|
|
18
21
|
token: () => string | null;
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
/** Renews now (once at a time); true when a fresh pass is in hand. A refused refresh sends the person to sign in. */
|
|
23
|
+
renew: () => Promise<boolean>;
|
|
24
|
+
/** Ends this site's session; the signed-out page follows. */
|
|
25
|
+
signOut: () => Promise<void>;
|
|
26
|
+
/** Sends the person to sign in. */
|
|
27
|
+
signInAgain: () => Promise<void>;
|
|
28
|
+
/** Hears every new pass (a renewal here or in another tab). */
|
|
29
|
+
onRenew: (listener: (pass: string) => void) => () => void;
|
|
22
30
|
};
|
|
@@ -1,54 +1,120 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
// The editor's session, browser side
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
import { useEffect, useRef, useState } from "react";
|
|
10
|
-
import {
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const type = fromHash ?? new URLSearchParams(search).get("type");
|
|
15
|
-
return type === "invite" || type === "recovery" ? type : null;
|
|
16
|
-
}
|
|
17
|
-
export function useAdminSession(auth) {
|
|
18
|
-
const browser = typeof window !== "undefined";
|
|
19
|
-
// Read BEFORE the client exists: it consumes and strips the fragment as soon as it is constructed.
|
|
20
|
-
const [arrival, setArrival] = useState(() => browser ? arrivalOf(window.location.hash, window.location.search) : null);
|
|
21
|
-
const [client] = useState(() => createClient(auth.url, auth.key, {
|
|
22
|
-
// Own storage key: a self-hoster's own supabase-js client in the same
|
|
23
|
-
// browser must never share the admin's session slot (DESIGN.md §1).
|
|
24
|
-
auth: { autoRefreshToken: browser, persistSession: browser, detectSessionInUrl: browser, storageKey: "smoodly-auth" },
|
|
25
|
-
}));
|
|
2
|
+
// The editor's session for this site's space, browser side (spec 2026-10-16 sign-in and the dashboard §2, §6): no
|
|
3
|
+
// credentials are typed here. On load a one-time code in the fragment is exchanged and stripped; a stored session is
|
|
4
|
+
// used, renewed five minutes before its pass expires; with neither, the person is sent to sign in (smoodly.io, or the
|
|
5
|
+
// local Worker's dev page) and comes back with a code. A refused pass renews once, then sends them to sign in again;
|
|
6
|
+
// Sign out ends this site's session and shows the signed-out page rather than redirecting, which would sign them
|
|
7
|
+
// straight back in while their smoodly.io session lives. Another tab's renewal or sign out is adopted from the
|
|
8
|
+
// `storage` event. The pure steps are admin/session-flow.ts.
|
|
9
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
10
|
+
import { codeFrom, endSession, exchange, readSession, renew, RENEW_BEFORE_MS, signInAddress, STORAGE_KEY } from "../session-flow.js";
|
|
11
|
+
/** A renewal's timer fires up to this much later, so two tabs rarely post the same refresh at once. */
|
|
12
|
+
const JITTER_MS = 30000;
|
|
13
|
+
export function useSpaceSession({ apiUrl, spaceId, signIn }) {
|
|
26
14
|
const [state, setState] = useState({ status: "loading" });
|
|
27
|
-
const
|
|
15
|
+
const [expiresAt, setExpiresAt] = useState(null);
|
|
16
|
+
const held = useRef(null);
|
|
17
|
+
const listeners = useRef(new Set());
|
|
18
|
+
const inflight = useRef(null);
|
|
19
|
+
const stores = () => ({ local: window.localStorage, tab: window.sessionStorage });
|
|
20
|
+
const adopt = useCallback((s) => {
|
|
21
|
+
held.current = s;
|
|
22
|
+
setExpiresAt(s.expiresAt);
|
|
23
|
+
setState({ status: "signed_in", user: s.user });
|
|
24
|
+
for (const l of listeners.current)
|
|
25
|
+
l(s.pass);
|
|
26
|
+
}, []);
|
|
27
|
+
const signInAgain = useCallback(async () => {
|
|
28
|
+
const back = window.location.origin + window.location.pathname + window.location.search;
|
|
29
|
+
window.location.assign(await signInAddress(signIn, spaceId, back, stores()));
|
|
30
|
+
}, [signIn, spaceId]);
|
|
31
|
+
const renewNow = useCallback(() => {
|
|
32
|
+
// A tab with no session (signed out here or in another tab) renews nothing and never redirects by itself.
|
|
33
|
+
if (!held.current)
|
|
34
|
+
return Promise.resolve(false);
|
|
35
|
+
inflight.current ?? (inflight.current = (async () => {
|
|
36
|
+
try {
|
|
37
|
+
const s = await renew(apiUrl, spaceId, stores(), held.current?.refresh ?? null);
|
|
38
|
+
if (s) {
|
|
39
|
+
adopt(s);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
held.current = null;
|
|
43
|
+
await signInAgain();
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Offline or the service down: the session stays; the next call or timer tries again.
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
inflight.current = null;
|
|
52
|
+
}
|
|
53
|
+
})());
|
|
54
|
+
return inflight.current;
|
|
55
|
+
}, [apiUrl, spaceId, adopt, signInAgain]);
|
|
28
56
|
useEffect(() => {
|
|
29
|
-
let
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
57
|
+
let live = true;
|
|
58
|
+
void (async () => {
|
|
59
|
+
const code = codeFrom(window.location.hash);
|
|
60
|
+
if (code) {
|
|
61
|
+
// The code is good once: out of the address bar and the history entry before anything else.
|
|
62
|
+
window.history.replaceState(window.history.state, "", window.location.pathname + window.location.search);
|
|
63
|
+
const got = await exchange(apiUrl, spaceId, code, stores());
|
|
64
|
+
if (!live)
|
|
65
|
+
return;
|
|
66
|
+
if ("refused" in got)
|
|
67
|
+
setState({ status: "signed_out", reason: "refused", message: got.refused });
|
|
68
|
+
else
|
|
69
|
+
adopt(got);
|
|
36
70
|
return;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (!
|
|
71
|
+
}
|
|
72
|
+
const stored = readSession(window.localStorage, spaceId);
|
|
73
|
+
if (!stored) {
|
|
74
|
+
await signInAgain();
|
|
40
75
|
return;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
76
|
+
}
|
|
77
|
+
held.current = stored;
|
|
78
|
+
if (stored.expiresAt - Date.now() > RENEW_BEFORE_MS)
|
|
79
|
+
adopt(stored);
|
|
80
|
+
else
|
|
81
|
+
await renewNow();
|
|
82
|
+
})();
|
|
83
|
+
const onStorage = (e) => {
|
|
84
|
+
if (e.key !== STORAGE_KEY(spaceId))
|
|
85
|
+
return;
|
|
86
|
+
const s = readSession(window.localStorage, spaceId);
|
|
87
|
+
if (s) {
|
|
88
|
+
if (s.refresh !== held.current?.refresh)
|
|
89
|
+
adopt(s);
|
|
90
|
+
}
|
|
91
|
+
else if (held.current) {
|
|
92
|
+
held.current = null;
|
|
93
|
+
setState({ status: "signed_out", reason: "signed_out" });
|
|
94
|
+
}
|
|
51
95
|
};
|
|
52
|
-
|
|
53
|
-
|
|
96
|
+
window.addEventListener("storage", onStorage);
|
|
97
|
+
return () => { live = false; window.removeEventListener("storage", onStorage); };
|
|
98
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
99
|
+
// Renew five minutes before the pass expires, a little later per tab.
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (expiresAt === null)
|
|
102
|
+
return;
|
|
103
|
+
const wait = Math.max(0, expiresAt - RENEW_BEFORE_MS - Date.now()) + Math.floor(Math.random() * JITTER_MS);
|
|
104
|
+
const timer = setTimeout(() => { void renewNow(); }, wait);
|
|
105
|
+
return () => clearTimeout(timer);
|
|
106
|
+
}, [expiresAt, renewNow]);
|
|
107
|
+
return {
|
|
108
|
+
state,
|
|
109
|
+
token: () => held.current?.pass ?? null,
|
|
110
|
+
renew: renewNow,
|
|
111
|
+
signOut: async () => {
|
|
112
|
+
held.current = null;
|
|
113
|
+
setExpiresAt(null);
|
|
114
|
+
await endSession(apiUrl, spaceId, stores());
|
|
115
|
+
setState({ status: "signed_out", reason: "signed_out" });
|
|
116
|
+
},
|
|
117
|
+
signInAgain,
|
|
118
|
+
onRenew: (listener) => { listeners.current.add(listener); return () => { listeners.current.delete(listener); }; },
|
|
119
|
+
};
|
|
54
120
|
}
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import type { AdminRegistry } from "../serialize.ts";
|
|
2
|
-
/** Above this many supported locales the switch is a dropdown. */
|
|
3
|
-
export declare const SEGMENTED_MAX = 3;
|
|
4
|
-
/** "fi" → "Fi": the locale code as the pill reads it. */
|
|
5
|
-
export declare const localeLabel: (locale: string) => string;
|
|
6
2
|
export declare function LocaleSwitcher({ registry, present, parentLocales, value, adding, onSwitch, onAdd }: {
|
|
7
3
|
registry: AdminRegistry;
|
|
8
4
|
/** The locales the record has; `[value]` while nothing is loaded. */
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Segmented } from "./primitives.js";
|
|
4
4
|
/** Above this many supported locales the switch is a dropdown. */
|
|
5
|
-
|
|
5
|
+
const SEGMENTED_MAX = 3;
|
|
6
6
|
/** "fi" → "Fi": the locale code as the pill reads it. */
|
|
7
|
-
|
|
7
|
+
const localeLabel = (locale) => locale.charAt(0).toUpperCase() + locale.slice(1);
|
|
8
8
|
export function LocaleSwitcher({ registry, present, parentLocales, value, adding, onSwitch, onAdd }) {
|
|
9
9
|
const supported = registry.locales.supported;
|
|
10
10
|
const missing = supported.filter((l) => !present.includes(l) && l !== value);
|
|
@@ -105,17 +105,6 @@ export declare function Segmented<T extends string>({ options, value, onChange,
|
|
|
105
105
|
className?: string;
|
|
106
106
|
} | undefined;
|
|
107
107
|
}): import("react").JSX.Element;
|
|
108
|
-
/** A pill row (the f.radio look, spec 2026-09-14 §5): no track, the chosen pill filled in the
|
|
109
|
-
* soft accent. For a small choice inside a table row, where a segmented control reads as tabs. */
|
|
110
|
-
export declare function Pills<T extends string>({ options, value, onChange, disabled, title, label }: {
|
|
111
|
-
options: readonly T[];
|
|
112
|
-
value: T;
|
|
113
|
-
onChange(v: T): void;
|
|
114
|
-
disabled?: boolean;
|
|
115
|
-
/** Why the row is off, on every pill. */
|
|
116
|
-
title?: string;
|
|
117
|
-
label?(o: T): ReactNode;
|
|
118
|
-
}): import("react").JSX.Element;
|
|
119
108
|
export declare function StatusBadge({ status }: {
|
|
120
109
|
status: string;
|
|
121
110
|
}): import("react").JSX.Element;
|
|
@@ -133,9 +122,6 @@ export declare function SectionLabel({ children, action, caps, style }: {
|
|
|
133
122
|
caps?: boolean;
|
|
134
123
|
style?: CSSProperties;
|
|
135
124
|
}): import("react").JSX.Element;
|
|
136
|
-
export declare function Divider({ bleed }: {
|
|
137
|
-
bleed?: number;
|
|
138
|
-
}): import("react").JSX.Element;
|
|
139
125
|
/** The lines under a publish-validation banner: one per field, each focusing its widget
|
|
140
126
|
* when the screen can reveal it (spec 2026-09-14 §2). */
|
|
141
127
|
export declare function FieldErrorList({ fields, onReveal }: {
|
|
@@ -151,6 +137,10 @@ export declare function Banner({ tone, children, onDismiss, revealKey }: {
|
|
|
151
137
|
* list shrinks as fields are fixed, so typing never scrolls. */
|
|
152
138
|
revealKey?: string;
|
|
153
139
|
}): import("react").JSX.Element;
|
|
140
|
+
/** An upload's progress (spec 2026-10-18 media on R2 and Images §9): a bar and the percentage of the bytes sent. */
|
|
141
|
+
export declare function UploadProgress({ fraction }: {
|
|
142
|
+
fraction: number;
|
|
143
|
+
}): import("react").JSX.Element;
|
|
154
144
|
export declare function cx(...parts: (string | false | null | undefined)[]): string;
|
|
155
145
|
/** One line of a form's info block: a muted label, the value right-aligned and clipped. */
|
|
156
146
|
export declare function InfoRow({ label, value, mono }: {
|
|
@@ -105,11 +105,6 @@ export function Segmented({ options, value, onChange, stretch, disabled, label,
|
|
|
105
105
|
return (_jsx("button", { type: "button", className: cx("sm-seg__opt", extra?.className), "aria-pressed": o === value, disabled: disabled || extra?.disabled, title: extra?.title, onClick: () => onChange(o), children: label ? label(o) : o }, o));
|
|
106
106
|
}) }));
|
|
107
107
|
}
|
|
108
|
-
/** A pill row (the f.radio look, spec 2026-09-14 §5): no track, the chosen pill filled in the
|
|
109
|
-
* soft accent. For a small choice inside a table row, where a segmented control reads as tabs. */
|
|
110
|
-
export function Pills({ options, value, onChange, disabled, title, label }) {
|
|
111
|
-
return (_jsx("div", { className: cx("sm-pills", disabled && "sm-pills--disabled"), role: "radiogroup", children: options.map((o) => (_jsx("button", { type: "button", className: "sm-pill", "aria-pressed": o === value, disabled: disabled, title: title, onClick: () => onChange(o), children: label ? label(o) : o }, o))) }));
|
|
112
|
-
}
|
|
113
108
|
/* ── data ────────────────────────────────────────────────────────────── */
|
|
114
109
|
// Keyed by the product labels of ui/status-label.ts (lowercased by StatusBadge).
|
|
115
110
|
const STATUS_TONE = {
|
|
@@ -141,9 +136,6 @@ export function SaveIndicator({ state, pending = false }) {
|
|
|
141
136
|
export function SectionLabel({ children, action, caps, style }) {
|
|
142
137
|
return (_jsxs("div", { className: cx("sm-sectionlabel", caps && "sm-sectionlabel--caps"), style: style, children: [_jsx("span", { className: "sm-label", children: children }), action && _jsx("span", { style: { color: "var(--accent)", fontWeight: 600, fontSize: "var(--text-sm)" }, children: action })] }));
|
|
143
138
|
}
|
|
144
|
-
export function Divider({ bleed = 0 }) {
|
|
145
|
-
return _jsx("div", { className: "sm-divider", style: { margin: `0 -${bleed}px` } });
|
|
146
|
-
}
|
|
147
139
|
/** The lines under a publish-validation banner: one per field, each focusing its widget
|
|
148
140
|
* when the screen can reveal it (spec 2026-09-14 §2). */
|
|
149
141
|
export function FieldErrorList({ fields, onReveal }) {
|
|
@@ -161,6 +153,11 @@ export function Banner({ tone, children, onDismiss, revealKey }) {
|
|
|
161
153
|
}, [revealKey]);
|
|
162
154
|
return (_jsxs("div", { ref: ref, className: `sm-banner sm-banner--${tone}`, role: tone === "error" ? "alert" : undefined, children: [_jsx("div", { style: { flex: 1, minWidth: 0 }, children: children }), onDismiss && (_jsx("button", { type: "button", onClick: onDismiss, "aria-label": "Dismiss", style: { background: "transparent", border: 0, color: "inherit", cursor: "pointer", font: "inherit", padding: 0 }, children: "\u00D7" }))] }));
|
|
163
155
|
}
|
|
156
|
+
/** An upload's progress (spec 2026-10-18 media on R2 and Images §9): a bar and the percentage of the bytes sent. */
|
|
157
|
+
export function UploadProgress({ fraction }) {
|
|
158
|
+
const pct = Math.round(Math.min(1, Math.max(0, fraction)) * 100);
|
|
159
|
+
return (_jsxs("div", { className: "sm-upload", role: "progressbar", "aria-label": "Uploading", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": pct, children: [_jsx("div", { className: "sm-upload__track", children: _jsx("div", { className: "sm-upload__fill", style: { width: `${pct}%` } }) }), _jsxs("span", { className: "sm-upload__pct", children: ["Uploading\u2026 ", pct, "%"] })] }));
|
|
160
|
+
}
|
|
164
161
|
export function cx(...parts) {
|
|
165
162
|
return parts.filter(Boolean).join(" ");
|
|
166
163
|
}
|
package/dist/admin/ui/theme.js
CHANGED
|
@@ -230,6 +230,8 @@ body:has(.sm-admin){margin:0}
|
|
|
230
230
|
lit tile a build in flight, the warning tile a build that has not landed or a site that did not answer. */
|
|
231
231
|
.sm-rail__deploy{margin-top:auto}
|
|
232
232
|
.sm-rail__deploy+.sm-rail__foot{margin-top:0}
|
|
233
|
+
/* A publish on its way to the site (plan 2026-10-14 publish through the queue): a quiet line above the Deploy tile
|
|
234
|
+
and the avatar, the warning colour when a delivery landed late or with an error. */
|
|
233
235
|
.sm-rail__badge{position:absolute;top:-4px;right:-6px;min-width:16px;height:16px;padding:0 4px;border-radius:var(--radius-round);background:var(--accent);color:#fff;font-size:10px;font-weight:600;line-height:16px;text-align:center}
|
|
234
236
|
.sm-rail__tile--lit{background:var(--accent-soft);color:var(--accent);animation:sm-rail-pulse 1.6s ease-in-out infinite}
|
|
235
237
|
.sm-rail__tile--warn{background:var(--warn-soft);color:var(--warn)}
|
|
@@ -681,7 +683,6 @@ body:has(.sm-admin){margin:0}
|
|
|
681
683
|
.sm-counter--over{color:var(--warn)}
|
|
682
684
|
/* The page glyph beside the page title in the empty-selection inspector head. */
|
|
683
685
|
.sm-pageglyph{width:10px;height:13px;border:1.5px solid var(--text-muted);border-radius:var(--radius-xs);flex:none}
|
|
684
|
-
.sm-divider{height:1px;background:var(--line);flex:none}
|
|
685
686
|
.sm-sectionlabel{display:flex;align-items:center;justify-content:space-between;gap:8px}
|
|
686
687
|
/* The overline: a small uppercase tracked heading for a group — the
|
|
687
688
|
inspector's Spacing & layout / Advanced, the picker's Add to <zone> and
|
|
@@ -970,6 +971,11 @@ body:has(.sm-admin){margin:0}
|
|
|
970
971
|
}
|
|
971
972
|
/* The picker's drop bar: tall enough to aim a drag at (2026-09-25). */
|
|
972
973
|
.sm-dropzone--bar{padding:28px 10px;font-size:var(--text-sm)}
|
|
974
|
+
.sm-upload{display:flex;flex-direction:column;align-items:stretch;gap:6px;width:100%;max-width:280px;font-size:var(--text-sm);color:var(--text-muted)}
|
|
975
|
+
.sm-upload__track{height:6px;border-radius:999px;background:var(--surface-inset);overflow:hidden}
|
|
976
|
+
.sm-upload__fill{height:100%;background:var(--accent);border-radius:999px;transition:width 120ms linear}
|
|
977
|
+
.sm-upload__pct{font-variant-numeric:tabular-nums}
|
|
978
|
+
.sm-mediascreen__upload{padding:8px 20px 0}
|
|
973
979
|
.sm-mediagrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px}
|
|
974
980
|
.sm-mediacard{
|
|
975
981
|
display:flex;flex-direction:column;gap:4px;padding:6px;background:transparent;border:1.5px solid transparent;
|
|
@@ -999,7 +1005,7 @@ body:has(.sm-admin){margin:0}
|
|
|
999
1005
|
.sm-media__actions{display:flex;gap:6px}
|
|
1000
1006
|
.sm-media__chip{display:flex;align-items:center;gap:8px;padding:6px 12px;background:var(--surface-inset);border-radius:var(--radius-btn)}
|
|
1001
1007
|
.sm-crop{display:grid;grid-template-columns:1fr 240px;gap:14px;min-height:0}
|
|
1002
|
-
.sm-crop__stage{position:relative;user-select:none;touch-action:none;background:var(--surface-inset);border-radius:var(--radius-md);overflow:hidden}
|
|
1008
|
+
.sm-crop__stage{position:relative;align-self:start;user-select:none;touch-action:none;background:var(--surface-inset);border-radius:var(--radius-md);overflow:hidden}
|
|
1003
1009
|
.sm-crop__stage>img{display:block;width:100%;height:auto;pointer-events:none}
|
|
1004
1010
|
.sm-crop__shade{position:absolute;inset:0;background:rgba(0,0,0,.45);pointer-events:none}
|
|
1005
1011
|
.sm-crop__rect{position:absolute;outline:1.5px solid #fff;box-shadow:0 0 0 1px rgba(0,0,0,.4);cursor:move;background:transparent}
|
|
@@ -1010,6 +1016,11 @@ body:has(.sm-admin){margin:0}
|
|
|
1010
1016
|
.sm-crop__side{display:flex;flex-direction:column;gap:10px}
|
|
1011
1017
|
.sm-crop__preview{position:relative;overflow:hidden;border-radius:var(--radius-md);background:var(--surface-inset)}
|
|
1012
1018
|
.sm-crop__preview>img{position:absolute;max-width:none;display:block}
|
|
1019
|
+
.sm-crop__ghost{position:absolute;outline:1.5px dashed rgba(255,255,255,.9);box-shadow:0 0 0 1px rgba(0,0,0,.35);pointer-events:none}
|
|
1020
|
+
.sm-crop__follows{margin-left:4px;color:var(--accent)}
|
|
1021
|
+
.sm-crop__previews{display:flex;gap:8px;align-items:flex-start}
|
|
1022
|
+
.sm-crop__figure{flex:1;min-width:0;margin:0;display:flex;flex-direction:column;gap:4px}
|
|
1023
|
+
.sm-crop__figure>figcaption{font-size:11px;color:var(--text-muted)}
|
|
1013
1024
|
.sm-mediascreen{display:grid;grid-template-columns:1fr 300px;column-gap:var(--gutter);flex:1;min-height:0}
|
|
1014
1025
|
.sm-mediascreen__grid{padding:var(--table-pad);overflow:auto;background:var(--surface-form);border-radius:var(--radius-card-lg)}
|
|
1015
1026
|
.sm-mediascreen__inspector{
|
package/dist/admin/validate.js
CHANGED
|
@@ -14,6 +14,7 @@ import { rowsProblems } from "../custom/rows.js";
|
|
|
14
14
|
import { isCalendarDay, isInstant } from "./forms/date-picker.js";
|
|
15
15
|
import { fits, misfitMessage } from "./drift.js";
|
|
16
16
|
import { isSegment } from "../paths.js";
|
|
17
|
+
import { isCrop } from "../media.js";
|
|
17
18
|
/** A slug outside the path grammar, at publish (spec 2026-09-28 ASCII slugs §3). */
|
|
18
19
|
export const SLUG_GRAMMAR_MESSAGE = "Use lowercase letters, digits and single hyphens, like my-post.";
|
|
19
20
|
const subfields = (d) => d.fields && typeof d.fields === "object" ? d.fields : {};
|
|
@@ -81,7 +82,15 @@ function validateValue(path, value, d, errors, elements, scope) {
|
|
|
81
82
|
errors.push({ field: path, message: "Not a date and time." });
|
|
82
83
|
return;
|
|
83
84
|
}
|
|
84
|
-
if (d.type === "image"
|
|
85
|
+
if (d.type === "image") {
|
|
86
|
+
// The crop window writes fractions inside the image (spec 2026-10-18 media on R2 and Images §6); the server cuts them.
|
|
87
|
+
const v = value;
|
|
88
|
+
const crops = v.crops === undefined ? [] : v.crops && typeof v.crops === "object" && !Array.isArray(v.crops) ? Object.values(v.crops) : [null];
|
|
89
|
+
if ((v.crop !== undefined && !isCrop(v.crop)) || crops.some((c) => !isCrop(c)))
|
|
90
|
+
errors.push({ field: path, message: "A crop lies outside the image." });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (d.type === "video" || d.type === "file")
|
|
85
94
|
return;
|
|
86
95
|
if (d.type === "list") {
|
|
87
96
|
if (!Array.isArray(value))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ResolvedConfig } from "./config.ts";
|
|
2
|
+
import { type PushBody } from "./push.ts";
|
|
3
|
+
import { type ContentReads, type DeliveryReads } from "./site.ts";
|
|
4
|
+
export type SmoodlyApiOptions = {
|
|
5
|
+
/** Without a trailing slash: `https://api.smoodly.io`, locally `http://localhost:8787`. */
|
|
6
|
+
apiUrl: string;
|
|
7
|
+
spaceId: string;
|
|
8
|
+
/** The secret key on a server, the publishable key where only published content is read. */
|
|
9
|
+
key: string;
|
|
10
|
+
config: ResolvedConfig;
|
|
11
|
+
/** Test seam; default the global fetch. */
|
|
12
|
+
fetch?: typeof fetch;
|
|
13
|
+
/** The schema push before the first call (spec 2026-10-16 the control plane §4): default, under NODE_ENV=development,
|
|
14
|
+
* the config and the site's address as `live`; `false` sends none. A config change reloads the glue module in both
|
|
15
|
+
* dev servers, which makes a new client and so a new push; an unchanged registry costs the service nothing. */
|
|
16
|
+
push?: false | PushBody;
|
|
17
|
+
};
|
|
18
|
+
/** The content API refused a read: its status, its code (`unauthorized`, `not_found`, `validation`, `internal`) and its message. */
|
|
19
|
+
export declare class SmoodlyApiError extends Error {
|
|
20
|
+
readonly status: number;
|
|
21
|
+
readonly code: string;
|
|
22
|
+
constructor(status: number, code: string, message: string);
|
|
23
|
+
}
|
|
24
|
+
/** A site's reads over the content API: the same face as the service's own site, so the read layer and the shells run
|
|
25
|
+
* unchanged. Every call counts as one read for the adapters' counters. */
|
|
26
|
+
export declare function createSmoodlyApi(options: SmoodlyApiOptions): ContentReads & DeliveryReads;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// The site's reads over the content API (spec 2026-10-14 §1): one HTTP call per read, the key in the Authorization
|
|
2
|
+
// header, the envelope `{ ok, data, tags }`. The statics (cacheKey, styles, custom, code pages) are computed from the
|
|
3
|
+
// config exactly as the service's site computes them (site.ts siteStatics), so a cache key never disagrees across
|
|
4
|
+
// the wire. A document's tags ride on the hit (SiteHit.tags), where the read layer takes them.
|
|
5
|
+
import { countRead } from "./read-counter.js";
|
|
6
|
+
import { pushBody, pushSchema } from "./push.js";
|
|
7
|
+
import { siteOptions, siteStatics, } from "./site.js";
|
|
8
|
+
/** The content API refused a read: its status, its code (`unauthorized`, `not_found`, `validation`, `internal`) and its message. */
|
|
9
|
+
export class SmoodlyApiError extends Error {
|
|
10
|
+
constructor(status, code, message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.name = "SmoodlyApiError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** A site's reads over the content API: the same face as the service's own site, so the read layer and the shells run
|
|
18
|
+
* unchanged. Every call counts as one read for the adapters' counters. */
|
|
19
|
+
export function createSmoodlyApi(options) {
|
|
20
|
+
const apiUrl = options.apiUrl.replace(/\/+$/, "");
|
|
21
|
+
const base = `${apiUrl}/v1/spaces/${encodeURIComponent(options.spaceId)}`;
|
|
22
|
+
const doFetch = options.fetch ?? fetch;
|
|
23
|
+
const statics = siteStatics(siteOptions(options.config));
|
|
24
|
+
const localeOf = (o) => o?.locale ?? statics.locales.default;
|
|
25
|
+
const env = typeof process === "undefined" ? {} : process.env;
|
|
26
|
+
const push = options.push ?? (env.NODE_ENV === "development" ? pushBody(options.config, env, "live") : false);
|
|
27
|
+
/** One push per client, awaited by every call until it settles; its failure is logged, never thrown. */
|
|
28
|
+
let pushed = null;
|
|
29
|
+
const pushOnce = (body) => (pushed ?? (pushed = pushSchema({ apiUrl, spaceId: options.spaceId, key: options.key, fetch: doFetch }, body).then(() => undefined, (e) => console.error(`smoodly: the schema push failed — ${e instanceof Error ? e.message : String(e)}; the site runs against the schema the space has.`))));
|
|
30
|
+
async function call(path, params) {
|
|
31
|
+
if (push)
|
|
32
|
+
await pushOnce(push);
|
|
33
|
+
countRead();
|
|
34
|
+
const url = new URL(`${base}${path}`);
|
|
35
|
+
for (const [k, v] of Object.entries(params))
|
|
36
|
+
if (v !== undefined && v !== "")
|
|
37
|
+
url.searchParams.set(k, v);
|
|
38
|
+
let res;
|
|
39
|
+
try {
|
|
40
|
+
res = await doFetch(url.toString(), { headers: { authorization: `Bearer ${options.key}`, accept: "application/json" } });
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
throw new TypeError(`smoodly: can't reach SMOODLY_API_URL (${apiUrl}): ${e instanceof Error ? e.message : String(e)}`);
|
|
44
|
+
}
|
|
45
|
+
const body = (await res.json().catch(() => null));
|
|
46
|
+
if (!body || !body.ok) {
|
|
47
|
+
throw new SmoodlyApiError(res.status, body?.code ?? "internal", body?.message ?? `smoodly: SMOODLY_API_URL (${apiUrl}) answered ${res.status} with no message.`);
|
|
48
|
+
}
|
|
49
|
+
return { data: body.data, tags: body.tags };
|
|
50
|
+
}
|
|
51
|
+
const draftParam = (o) => ({ draft: o?.draft ? "1" : undefined });
|
|
52
|
+
/** The response's tags on a published hit (the read layer takes them from there); a draft carries none. */
|
|
53
|
+
const withTags = (hit, tags, draft) => hit && hit.kind !== "redirect" && !draft ? { ...hit, tags: hit.tags ?? tags } : hit;
|
|
54
|
+
return {
|
|
55
|
+
locales: statics.locales,
|
|
56
|
+
homePageSlug: statics.homePageSlug,
|
|
57
|
+
cacheKey: statics.cacheKey,
|
|
58
|
+
styles: statics.styles,
|
|
59
|
+
custom: statics.custom,
|
|
60
|
+
codePage: statics.codePage,
|
|
61
|
+
async locate(locale, path) {
|
|
62
|
+
return (await call("/locate", { locale, path })).data;
|
|
63
|
+
},
|
|
64
|
+
async targetPaths(target) {
|
|
65
|
+
return (await call("/target-paths", { kind: target.kind, id: target.id })).data;
|
|
66
|
+
},
|
|
67
|
+
async load(target, locale, path, o) {
|
|
68
|
+
const draft = !!o?.draft || o?.version !== undefined;
|
|
69
|
+
const r = await call("/load", { kind: target.kind, id: target.id, locale, path, draft: o?.draft ? "1" : undefined, version: o?.version });
|
|
70
|
+
return withTags(r.data, r.tags, draft);
|
|
71
|
+
},
|
|
72
|
+
async lookup(locale, path) {
|
|
73
|
+
const r = await call("/lookup", { locale, path });
|
|
74
|
+
return withTags(r.data.hit, r.tags, false);
|
|
75
|
+
},
|
|
76
|
+
async lookupWithGlobals(locale, path, items) {
|
|
77
|
+
const r = await call("/lookup", { locale, path, globals: items.map((i) => i.name).join(",") });
|
|
78
|
+
return { hit: withTags(r.data.hit, r.tags, false), globals: new Map(Object.entries(r.data.globals)) };
|
|
79
|
+
},
|
|
80
|
+
async getPagesTree(o) {
|
|
81
|
+
return (await call("/pages", { locale: localeOf(o), ...draftParam(o) })).data;
|
|
82
|
+
},
|
|
83
|
+
async getPage(target, o) {
|
|
84
|
+
return (await call("/page", { locale: localeOf(o), target, ...draftParam(o) })).data;
|
|
85
|
+
},
|
|
86
|
+
async getEntries(collection, o = {}) {
|
|
87
|
+
const { locale: _l, draft: _d, ...query } = o;
|
|
88
|
+
const q = Object.keys(query).length ? JSON.stringify(query) : undefined;
|
|
89
|
+
return (await call(`/entries/${encodeURIComponent(collection)}`, { locale: localeOf(o), query: q, ...draftParam(o) })).data;
|
|
90
|
+
},
|
|
91
|
+
async getEntry(collection, target, o) {
|
|
92
|
+
return (await call(`/entry/${encodeURIComponent(collection)}`, { locale: localeOf(o), target, ...draftParam(o) })).data;
|
|
93
|
+
},
|
|
94
|
+
async getEntriesTree(collection, o) {
|
|
95
|
+
return (await call(`/entries-tree/${encodeURIComponent(collection)}`, { locale: localeOf(o), ...draftParam(o) })).data;
|
|
96
|
+
},
|
|
97
|
+
async getGlobal(item, o) {
|
|
98
|
+
return (await call(`/global/${encodeURIComponent(item.name)}`, { locale: localeOf(o), ...draftParam(o) })).data;
|
|
99
|
+
},
|
|
100
|
+
async addresses(locale) {
|
|
101
|
+
return (await call("/addresses", { locale })).data;
|
|
102
|
+
},
|
|
103
|
+
async newestPublishedAt() {
|
|
104
|
+
return (await call("/status", {})).data.newestPublishedAt;
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
package/dist/assemble.d.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import { type EntryRecord } from "./entry-store.ts";
|
|
2
1
|
import type { CollectionSchema } from "./collections.ts";
|
|
3
|
-
import {
|
|
4
|
-
import { type GlobalSchema } from "./global.ts";
|
|
5
|
-
import type { PageRecord } from "./store.ts";
|
|
2
|
+
import type { PagePathOptions } from "./paths.ts";
|
|
6
3
|
import type { Descriptor } from "./fields.ts";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import type { PublishedPageRow } from "./published.ts";
|
|
10
|
-
import type { EntryQuery, PageTreeItem, SiteEntry, SitePage, SiteOptions } from "./site.ts";
|
|
4
|
+
import type { Anchors } from "./anchors.ts";
|
|
5
|
+
import type { EntryQuery, SiteOptions } from "./site.ts";
|
|
11
6
|
/** What the site computes from its options before any read: the registered shapes, the code-owned addresses and the resolved styles. */
|
|
12
|
-
export declare function siteShape(options: SiteOptions): {
|
|
7
|
+
export declare function siteShape(options: Omit<SiteOptions, "stores">): {
|
|
13
8
|
locales: import("./paths.ts").LocaleSet;
|
|
14
9
|
pageSchemas: import("./site.ts").SitePageSchema[];
|
|
15
10
|
homePageSlug: string;
|
|
@@ -26,35 +21,3 @@ export type SiteShape = ReturnType<typeof siteShape>;
|
|
|
26
21
|
export declare function pageFieldsOf(shape: SiteShape, template: string | null): Record<string, Descriptor>;
|
|
27
22
|
/** The checks `getEntries` makes before any read, in draft mode and outside it: one function both paths call. */
|
|
28
23
|
export declare function checkEntryQuery(schema: CollectionSchema<any>, collection: string, o: EntryQuery): void;
|
|
29
|
-
/** One read row: the public item, and the record it was projected from (a tree and a bare slug need `parentId`). */
|
|
30
|
-
export type ReadRow = {
|
|
31
|
-
record: EntryRecord;
|
|
32
|
-
item: EntryItem;
|
|
33
|
-
};
|
|
34
|
-
export type Assembler = {
|
|
35
|
-
/** A page of the locale, draft or published, with a stored version in draft mode. */
|
|
36
|
-
page(id: string, locale: string, path: string, opts: {
|
|
37
|
-
draft: boolean;
|
|
38
|
-
version?: string;
|
|
39
|
-
}): Promise<SitePage | null>;
|
|
40
|
-
entry(id: string, locale: string, path: string, opts: {
|
|
41
|
-
draft: boolean;
|
|
42
|
-
}): Promise<SiteEntry | null>;
|
|
43
|
-
global<V>(item: GlobalSchema<V>, locale: string, opts: {
|
|
44
|
-
draft: boolean;
|
|
45
|
-
}): Promise<V | null>;
|
|
46
|
-
/** The pages tree over the editing tables. */
|
|
47
|
-
tree(locale: string, opts: {
|
|
48
|
-
draft: boolean;
|
|
49
|
-
}): Promise<PageTreeItem[]>;
|
|
50
|
-
/** Rows with the record and the projected item. */
|
|
51
|
-
entries(collection: string, q: EntryQuery): Promise<ReadRow[]>;
|
|
52
|
-
/** The light tree row of a node as the delivery table stores it (path null when hidden), for a locale. */
|
|
53
|
-
treeRow(record: PageRecord, all: PageRecord[], locale: string): Promise<Omit<PublishedPageRow, "document" | "tags" | "model" | "stale" | "publishedAt">>;
|
|
54
|
-
/** A memoizing copy for one projection call: listPages, getMany, pathsIn, fieldsOf and assets.getMany answer once per argument set.
|
|
55
|
-
* `pages`: the page list the caller already read, so the copy's listPages answers with it and never reads again. */
|
|
56
|
-
batch(primed?: {
|
|
57
|
-
pages?: PageRecord[];
|
|
58
|
-
}): Assembler;
|
|
59
|
-
};
|
|
60
|
-
export declare function createAssembler(options: SiteOptions, shape: SiteShape): Assembler;
|