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
package/dist/env.js
CHANGED
|
@@ -1,50 +1,49 @@
|
|
|
1
|
-
// The one config shape (spec 2026-09-04 §4/§6).
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const SITE_NEEDS = ["
|
|
7
|
-
|
|
1
|
+
// The one config shape (spec 2026-09-04 §4/§6; spec 2026-10-14 §1; spec 2026-10-16 the control plane §5). A site and
|
|
2
|
+
// its admin read over Smoodly's API with three names — the API's address (https://api.smoodly.io unless set), the
|
|
3
|
+
// space, the space's secret key (sk_) — and may hold the space's publishable key (pk_); the admin learns where editors
|
|
4
|
+
// sign in from the service (GET /v1/auth). Media addresses come with the content, so the site names no media host
|
|
5
|
+
// (spec 2026-10-18 media on R2 and Images §7).
|
|
6
|
+
const SITE_NEEDS = ["SMOODLY_SPACE_ID", "SMOODLY_SECRET_KEY"];
|
|
7
|
+
/** Smoodly's hosted API; SMOODLY_API_URL is set only for another address (the repo's loop, a staging service). */
|
|
8
|
+
export const DEFAULT_API_URL = "https://api.smoodly.io";
|
|
9
|
+
/** The admin learns where to sign in from the service, so it needs what the site needs. */
|
|
10
|
+
const ADMIN_NEEDS = SITE_NEEDS;
|
|
11
|
+
const NEEDS = { site: SITE_NEEDS, admin: ADMIN_NEEDS };
|
|
12
|
+
const isHttp = (v) => /^https?:\/\/[^/\s]+/.test(v);
|
|
8
13
|
const DEPLOYING = "https://smoodly.io/docs/getting-started/deploying";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return null;
|
|
14
|
-
try {
|
|
15
|
-
const role = JSON.parse(atob(v.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"))).role;
|
|
16
|
-
return typeof role === "string" ? role : null;
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
/** The live site reads with the URL and the secret key; the admin and presence also need the
|
|
23
|
-
* publishable key, so their callers pass "admin" (spec 2026-09-30 tester findings §4). A key
|
|
24
|
-
* in the wrong variable or of no known form is named here, not by a failed read later. */
|
|
14
|
+
/** A Smoodly key: its kind, the space it belongs to, the random part and the stamp (spec 2026-10-16 the control plane §1). */
|
|
15
|
+
const SMOODLY_KEY = (kind) => new RegExp(`^${kind}_sp_(eu_)?[a-z2-7]{20}_[a-z2-7]{32}_[a-z2-7]{16}$`);
|
|
16
|
+
/** The live site and its admin read over the API with its address, the space and the secret key (spec 2026-10-16 the
|
|
17
|
+
* control plane §5). A key in the wrong variable or of no known form is named here, not by a failed read later. */
|
|
25
18
|
export function resolveSmoodlyEnv(env, need = "site") {
|
|
26
|
-
const missing =
|
|
19
|
+
const missing = NEEDS[need].filter((name) => !env[name]);
|
|
27
20
|
if (missing.length > 0)
|
|
28
21
|
throw new Error(`smoodly: set ${missing.join(", ")} in .env (Astro) or .env.local (Next.js), or in your host's environment variables (Deploying: ${DEPLOYING}).`);
|
|
29
22
|
const secretKey = env.SMOODLY_SECRET_KEY;
|
|
30
23
|
const publishableKey = env.SMOODLY_PUBLISHABLE_KEY ?? "";
|
|
31
|
-
if (
|
|
32
|
-
throw new Error("smoodly: SMOODLY_SECRET_KEY holds
|
|
24
|
+
if (SMOODLY_KEY("pk").test(secretKey)) {
|
|
25
|
+
throw new Error("smoodly: SMOODLY_SECRET_KEY holds the publishable key (pk_…) — copy the secret key (sk_…) from the space page.");
|
|
33
26
|
}
|
|
34
|
-
if (!
|
|
35
|
-
throw new Error("smoodly: SMOODLY_SECRET_KEY is
|
|
27
|
+
if (!SMOODLY_KEY("sk").test(secretKey)) {
|
|
28
|
+
throw new Error("smoodly: SMOODLY_SECRET_KEY is not a Smoodly secret key (sk_…) — copy it from the space page.");
|
|
36
29
|
}
|
|
37
|
-
if (
|
|
38
|
-
throw new Error("smoodly: SMOODLY_PUBLISHABLE_KEY holds
|
|
30
|
+
if (SMOODLY_KEY("sk").test(publishableKey)) {
|
|
31
|
+
throw new Error("smoodly: SMOODLY_PUBLISHABLE_KEY holds the secret key (sk_…) — it reaches the browser; copy the publishable key (pk_…) from the space page.");
|
|
39
32
|
}
|
|
40
|
-
if (publishableKey && !
|
|
41
|
-
throw new Error("smoodly: SMOODLY_PUBLISHABLE_KEY is
|
|
33
|
+
if (publishableKey && !SMOODLY_KEY("pk").test(publishableKey)) {
|
|
34
|
+
throw new Error("smoodly: SMOODLY_PUBLISHABLE_KEY is not a Smoodly publishable key (pk_…) — copy it from the space page.");
|
|
42
35
|
}
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
45
|
-
throw new Error(`smoodly:
|
|
36
|
+
const apiUrl = (env.SMOODLY_API_URL || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
37
|
+
if (!isHttp(apiUrl)) {
|
|
38
|
+
throw new Error(`smoodly: SMOODLY_API_URL "${apiUrl}" is not an http(s) address — the address of Smoodly's API (locally the service on http://localhost:8787; see Deploying: ${DEPLOYING}).`);
|
|
46
39
|
}
|
|
47
|
-
|
|
40
|
+
const spaceId = env.SMOODLY_SPACE_ID;
|
|
41
|
+
return { secretKey, publishableKey, apiUrl, spaceId };
|
|
42
|
+
}
|
|
43
|
+
/** What the API client is built from (createSmoodlyApi): the site's env, the secret key as the key. */
|
|
44
|
+
export function apiEnv(env = process.env) {
|
|
45
|
+
const e = resolveSmoodlyEnv(env, "site");
|
|
46
|
+
return { apiUrl: e.apiUrl, spaceId: e.spaceId, key: e.secretKey };
|
|
48
47
|
}
|
|
49
48
|
let siteUrlWarned = false;
|
|
50
49
|
/** The site's own address for the root layout's `metadataBase`, so canonical, hreflang and social
|
|
@@ -68,18 +67,7 @@ export function siteUrl(env = process.env) {
|
|
|
68
67
|
/** `resolveSmoodlyEnv(process.env)` — call lazily, never at module load,
|
|
69
68
|
* so `next build` without env still succeeds. */
|
|
70
69
|
export const smoodlyEnv = (need = "site") => resolveSmoodlyEnv(process.env, need);
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
/** The presence config, with the space the database says the secret key acts in —
|
|
75
|
-
* asked, never read out of the key, so it cannot disagree with where rows land. */
|
|
76
|
-
export function presenceConfig(env, db) {
|
|
77
|
-
const cacheKey = `${env.url}\n${env.secretKey}`;
|
|
78
|
-
let space = spaces.get(cacheKey);
|
|
79
|
-
if (!space) {
|
|
80
|
-
space = readCurrentSpace(db ?? createSmoodlyClient(env));
|
|
81
|
-
spaces.set(cacheKey, space);
|
|
82
|
-
space.catch(() => spaces.delete(cacheKey));
|
|
83
|
-
}
|
|
84
|
-
return space.then((spaceId) => ({ url: env.url, key: env.publishableKey, spaceId }));
|
|
70
|
+
/** The presence config from the env: no lookup, the space is the site's own. */
|
|
71
|
+
export function presenceConfig(env) {
|
|
72
|
+
return { apiUrl: env.apiUrl, spaceId: env.spaceId };
|
|
85
73
|
}
|
package/dist/fields.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Toolset } from "./toolset.ts";
|
|
2
2
|
import { type ResolvedFile, type ResolvedImage, type ResolvedVideo } from "./media.ts";
|
|
3
|
+
import { type ImageQuality } from "./media-cut.ts";
|
|
3
4
|
import { type Condition, when } from "./conditions.ts";
|
|
4
5
|
export type Descriptor = {
|
|
5
6
|
type: string;
|
|
@@ -88,10 +89,17 @@ export declare const f: {
|
|
|
88
89
|
datetime: () => FieldBuilder<string, {}>;
|
|
89
90
|
number: () => FieldBuilder<number, {}>;
|
|
90
91
|
/** `aspect`: one `"w:h"` or several the crop tool may lock to — a per-use
|
|
91
|
-
* constraint, so the component knows its box before the image loads.
|
|
92
|
-
|
|
92
|
+
* constraint, so the component knows its box before the image loads.
|
|
93
|
+
* `crops`: named crops for art direction, a ratio each (spec 2026-10-18 media on R2 and Images §6), framed by the
|
|
94
|
+
* editor in one tab each and placed by the view's `media`; the first is the field's main crop. Not with `aspect`.
|
|
95
|
+
* `quality` and `maxWidth`: this kind of image's cut quality and srcset cap, over the project's and under a view's
|
|
96
|
+
* (spec 2026-10-18 image quality and max width §1). */
|
|
97
|
+
image: <const C extends Record<string, string> = Record<string, string>>(opts?: {
|
|
93
98
|
aspect?: string | string[];
|
|
94
|
-
|
|
99
|
+
crops?: C;
|
|
100
|
+
quality?: ImageQuality;
|
|
101
|
+
maxWidth?: number;
|
|
102
|
+
}) => FieldBuilder<ResolvedImage<Extract<keyof C, string>>, {}>;
|
|
95
103
|
video: () => FieldBuilder<ResolvedVideo, {}>;
|
|
96
104
|
file: () => FieldBuilder<ResolvedFile, {}>;
|
|
97
105
|
richtext: (opts?: {
|
package/dist/fields.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// keyed by descriptor type. Chaining is immutable.
|
|
4
4
|
import { DEFAULT_TOOLSET } from "./toolset.js";
|
|
5
5
|
import { parseAspect } from "./media.js";
|
|
6
|
+
import { isImageQuality, isMaxWidth, qualityError } from "./media-cut.js";
|
|
6
7
|
import { when } from "./conditions.js";
|
|
7
8
|
/** Normalizes a schema's option list to objects; refuses an empty list,
|
|
8
9
|
* since a choice with nothing to choose is a schema bug (spec 2026-09-10
|
|
@@ -86,9 +87,19 @@ export const f = {
|
|
|
86
87
|
datetime: () => builder({ type: "datetime" }),
|
|
87
88
|
number: () => builder({ type: "number" }),
|
|
88
89
|
/** `aspect`: one `"w:h"` or several the crop tool may lock to — a per-use
|
|
89
|
-
* constraint, so the component knows its box before the image loads.
|
|
90
|
+
* constraint, so the component knows its box before the image loads.
|
|
91
|
+
* `crops`: named crops for art direction, a ratio each (spec 2026-10-18 media on R2 and Images §6), framed by the
|
|
92
|
+
* editor in one tab each and placed by the view's `media`; the first is the field's main crop. Not with `aspect`.
|
|
93
|
+
* `quality` and `maxWidth`: this kind of image's cut quality and srcset cap, over the project's and under a view's
|
|
94
|
+
* (spec 2026-10-18 image quality and max width §1). */
|
|
90
95
|
image: (opts) => {
|
|
91
96
|
const aspect = opts?.aspect;
|
|
97
|
+
const crops = opts?.crops;
|
|
98
|
+
const quality = opts?.quality, maxWidth = opts?.maxWidth;
|
|
99
|
+
if (quality !== undefined && !isImageQuality(quality))
|
|
100
|
+
throw new Error(qualityError(quality, "f.image quality"));
|
|
101
|
+
if (maxWidth !== undefined && !isMaxWidth(maxWidth))
|
|
102
|
+
throw new Error("smoodly: f.image maxWidth must be a positive whole number of pixels.");
|
|
92
103
|
if (aspect !== undefined) {
|
|
93
104
|
const list = Array.isArray(aspect) ? aspect : [aspect];
|
|
94
105
|
if (list.length === 0)
|
|
@@ -96,7 +107,27 @@ export const f = {
|
|
|
96
107
|
for (const a of list)
|
|
97
108
|
parseAspect(a);
|
|
98
109
|
}
|
|
99
|
-
|
|
110
|
+
if (crops !== undefined) {
|
|
111
|
+
if (aspect !== undefined)
|
|
112
|
+
throw new Error("smoodly: f.image takes `crops` or `aspect`, not both — each named crop has its own ratio.");
|
|
113
|
+
const names = Object.keys(crops);
|
|
114
|
+
if (names.length === 0)
|
|
115
|
+
throw new Error('smoodly: f.image `crops` names no crop — name one or more, like { desktop: "16:9" }.');
|
|
116
|
+
for (const name of names) {
|
|
117
|
+
if (!/^[a-z][a-zA-Z0-9]*$/.test(name))
|
|
118
|
+
throw new Error(`smoodly: f.image crop "${name}" is not a name — start with a lowercase letter, then letters and digits, like "mobile".`);
|
|
119
|
+
try {
|
|
120
|
+
parseAspect(crops[name]);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
throw new Error(`smoodly: f.image crop "${name}": "${crops[name]}" is not an aspect — use "w:h", like "16:9".`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return builder({
|
|
128
|
+
type: "image", ...(aspect !== undefined ? { aspect } : {}), ...(crops !== undefined ? { crops: { ...crops } } : {}),
|
|
129
|
+
...(quality !== undefined ? { quality } : {}), ...(maxWidth !== undefined ? { maxWidth } : {}),
|
|
130
|
+
});
|
|
100
131
|
},
|
|
101
132
|
video: () => builder({ type: "video" }),
|
|
102
133
|
file: () => builder({ type: "file" }),
|
package/dist/global-id.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** The namespace a global item's id is derived in: fixed, so an export and an import of the same registry agree on the row. */
|
|
2
|
+
export declare const DEFAULT_SPACE_ID = "00000000-0000-0000-0000-000000000000";
|
|
1
3
|
/** RFC 4122 §4.3 uuid v5: sha1(namespace bytes ‖ name bytes), version 5,
|
|
2
4
|
* variant 10xx, first 16 bytes of the digest. */
|
|
3
5
|
export declare function globalEntryId(space: string, name: string): Promise<string>;
|
package/dist/global-id.js
CHANGED
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
// share one table: a name-only id would make space B's footer collide
|
|
8
8
|
// with space A's.
|
|
9
9
|
//
|
|
10
|
-
// No imports — Web Crypto (`
|
|
10
|
+
// No imports — Web Crypto (`crypto.subtle`) exists in Node
|
|
11
11
|
// >= 20 and in every browser, so this module carries no node builtin
|
|
12
12
|
// into the package root's graph. It matters because every scaffolded
|
|
13
13
|
// component does `import { smoodly } from "smoodly/react"`, which re-exports
|
|
14
14
|
// the root, and a "use client" section's browser build would fail on `node:crypto`.
|
|
15
|
+
/** The namespace a global item's id is derived in: fixed, so an export and an import of the same registry agree on the row. */
|
|
16
|
+
export const DEFAULT_SPACE_ID = "00000000-0000-0000-0000-000000000000";
|
|
15
17
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
16
18
|
/** The namespace's 16 raw bytes — RFC 4122 hashes the bytes, not the text. */
|
|
17
19
|
function uuidBytes(uuid) {
|
|
@@ -27,7 +29,7 @@ function uuidBytes(uuid) {
|
|
|
27
29
|
* variant 10xx, first 16 bytes of the digest. */
|
|
28
30
|
export async function globalEntryId(space, name) {
|
|
29
31
|
const input = new Uint8Array([...uuidBytes(space), ...new TextEncoder().encode(name)]);
|
|
30
|
-
const digest = new Uint8Array(await
|
|
32
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-1", input));
|
|
31
33
|
const bytes = digest.subarray(0, 16);
|
|
32
34
|
bytes[6] = (bytes[6] & 0x0f) | 0x50; // version 5 in the high nibble of octet 6
|
|
33
35
|
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 10xx in the top bits of octet 8
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { CSSProperties, ReactElement } from "react";
|
|
2
2
|
import type { ResolvedImage } from "../media.ts";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ImageQuality } from "../media-cut.ts";
|
|
4
4
|
export type SmoodlyImageProps = {
|
|
5
5
|
image: ResolvedImage;
|
|
6
|
-
/** Overrides the value's aspect for this placement (`"21:9"`). */
|
|
6
|
+
/** Overrides the value's aspect for this placement (`"21:9"`): the cut is framed around the hotspot. */
|
|
7
7
|
aspect?: string;
|
|
8
|
+
/** Where each named crop applies, in order: `{ mobile: "(max-width: 767px)" }` (spec 2026-10-18 media §6). */
|
|
9
|
+
media?: Record<string, string>;
|
|
8
10
|
sizes?: string;
|
|
9
11
|
priority?: boolean;
|
|
10
12
|
loading?: "eager" | "lazy";
|
|
@@ -12,10 +14,12 @@ export type SmoodlyImageProps = {
|
|
|
12
14
|
style?: CSSProperties;
|
|
13
15
|
/** Overrides the resolved alt (rarely right — the editor owns it). */
|
|
14
16
|
alt?: string;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
/** This placement's cut quality (50, 60, 75, 85, 95 or 100) and srcset cap, over the field's and the project's (spec
|
|
18
|
+
* 2026-10-18 image quality and max width §1). */
|
|
19
|
+
quality?: ImageQuality;
|
|
20
|
+
maxWidth?: number;
|
|
17
21
|
} & {
|
|
18
22
|
/** Data attributes land on the wrapper box — a section's field mark (spec 2026-09-10 §1). */
|
|
19
23
|
[attr: `data-${string}`]: string | undefined;
|
|
20
24
|
};
|
|
21
|
-
export declare function SmoodlyImage({ image, aspect, sizes, priority, loading, className, style, alt,
|
|
25
|
+
export declare function SmoodlyImage({ image, aspect, media, sizes, priority, loading, className, style, alt, quality, maxWidth, ...data }: SmoodlyImageProps): ReactElement;
|
|
@@ -1,16 +1,30 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { preconnect } from "react-dom";
|
|
3
|
+
import { imageMarkup } from "./markup.js";
|
|
4
|
+
/** `a:b;c-d:e` as React's style object, in the same order, so React writes the same string back. */
|
|
5
|
+
function styleObject(css) {
|
|
6
|
+
const out = {};
|
|
7
|
+
for (const decl of css.split(";")) {
|
|
8
|
+
const at = decl.indexOf(":");
|
|
9
|
+
if (at > 0)
|
|
10
|
+
out[decl.slice(0, at).replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = decl.slice(at + 1);
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
function originOf(url) {
|
|
15
|
+
try {
|
|
16
|
+
return new URL(url).origin;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function SmoodlyImage({ image, aspect, media, sizes, priority, loading, className, style, alt, quality, maxWidth, ...data }) {
|
|
23
|
+
const m = imageMarkup(image, { aspect, media, sizes, priority, loading, alt, quality, maxWidth });
|
|
24
|
+
const origin = originOf(image.cut ?? image.url);
|
|
25
|
+
if (origin)
|
|
26
|
+
preconnect(origin);
|
|
27
|
+
const i = m.img;
|
|
28
|
+
const img = (_jsx("img", { src: i.src, srcSet: i.srcset, sizes: i.sizes, alt: i.alt, width: i.width, height: i.height, loading: i.loading, fetchPriority: i.fetchpriority, decoding: i.decoding, style: styleObject(i.style) }));
|
|
29
|
+
return (_jsx("span", { className: className, ...data, style: { ...styleObject(m.box), ...style }, children: m.sources.length > 0 ? (_jsxs("picture", { children: [m.sources.map((s) => _jsx("source", { media: s.media, srcSet: s.srcset, sizes: s.sizes, width: s.width, height: s.height, type: s.type }, `${s.media}:${s.type}`)), img] })) : img }));
|
|
16
30
|
}
|
|
@@ -3,5 +3,5 @@ import type { ResolvedVideo } from "../media.ts";
|
|
|
3
3
|
export type SmoodlyVideoProps = {
|
|
4
4
|
video: ResolvedVideo;
|
|
5
5
|
} & Omit<VideoHTMLAttributes<HTMLVideoElement>, "src" | "poster" | "width" | "height">;
|
|
6
|
-
/** A <video>
|
|
6
|
+
/** A <video> from the media host with its poster (a cut where there is one) and size. No transcoding (spec §9). */
|
|
7
7
|
export declare function SmoodlyVideo({ video, ...rest }: SmoodlyVideoProps): ReactElement;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
|
|
2
|
+
import { posterUrl } from "./markup.js";
|
|
3
|
+
/** A <video> from the media host with its poster (a cut where there is one) and size. No transcoding (spec §9). */
|
|
3
4
|
export function SmoodlyVideo({ video, ...rest }) {
|
|
4
|
-
return (_jsx("video", { src: video.url, poster: video
|
|
5
|
+
return (_jsx("video", { src: video.url, poster: posterUrl(video), width: video.width, height: video.height, playsInline: true, ...rest }));
|
|
5
6
|
}
|
package/dist/image/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { SmoodlyImage, type SmoodlyImageProps } from "./SmoodlyImage.tsx";
|
|
2
2
|
export { SmoodlyVideo, type SmoodlyVideoProps } from "./SmoodlyVideo.tsx";
|
|
3
|
-
export { imageUrl, nextLoader, type ImageLoader, type ImageUrlOptions } from "./loaders.ts";
|
|
4
3
|
export { frameFor, boxAspectOf, type Frame } from "./crop-math.ts";
|
|
5
|
-
export {
|
|
6
|
-
export type {
|
|
4
|
+
export type { ResolvedImage, ResolvedCrop, ResolvedVideo, ResolvedFile, Crop, Hotspot } from "../media.ts";
|
|
5
|
+
export type { ImageQuality } from "../media-cut.ts";
|
package/dist/image/index.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
export { SmoodlyImage } from "./SmoodlyImage.js";
|
|
2
2
|
export { SmoodlyVideo } from "./SmoodlyVideo.js";
|
|
3
|
-
export { imageUrl, nextLoader } from "./loaders.js";
|
|
4
3
|
export { frameFor, boxAspectOf } from "./crop-math.js";
|
|
5
|
-
export { smoodlyImages, isPrivateHost } from "./next-config.js";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ResolvedImage, type ResolvedVideo } from "../media.ts";
|
|
2
|
+
import { type ImageQuality } from "../media-cut.ts";
|
|
3
|
+
export type ImageMarkupOptions = {
|
|
4
|
+
/** Overrides the value's ratio for this placement: the cut is framed in CSS around the hotspot. */
|
|
5
|
+
aspect?: string;
|
|
6
|
+
sizes?: string;
|
|
7
|
+
/** Where each named crop applies: `{ mobile: "(max-width: 767px)" }`, in order; the first match wins. */
|
|
8
|
+
media?: Record<string, string>;
|
|
9
|
+
priority?: boolean;
|
|
10
|
+
loading?: "eager" | "lazy";
|
|
11
|
+
/** Overrides the resolved alt (rarely right — the editor owns it). */
|
|
12
|
+
alt?: string;
|
|
13
|
+
/** This placement's cut quality and srcset cap, over the image's own (its field's or project's) and the default 75
|
|
14
|
+
* (spec 2026-10-18 image quality and max width §1). */
|
|
15
|
+
quality?: ImageQuality;
|
|
16
|
+
maxWidth?: number;
|
|
17
|
+
};
|
|
18
|
+
/** A <source>: `media` for a placed crop, `type` for a format a static build offers (astro/static-media.ts). */
|
|
19
|
+
export type ImageSource = {
|
|
20
|
+
media?: string;
|
|
21
|
+
srcset: string;
|
|
22
|
+
sizes?: string;
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
type?: string;
|
|
26
|
+
};
|
|
27
|
+
export type ImageTag = {
|
|
28
|
+
src: string;
|
|
29
|
+
srcset?: string;
|
|
30
|
+
sizes?: string;
|
|
31
|
+
alt: string;
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
loading: "eager" | "lazy";
|
|
35
|
+
fetchpriority?: "high";
|
|
36
|
+
decoding: "async";
|
|
37
|
+
style: string;
|
|
38
|
+
};
|
|
39
|
+
export type ImageMarkup = {
|
|
40
|
+
box: string;
|
|
41
|
+
sources: ImageSource[];
|
|
42
|
+
img: ImageTag;
|
|
43
|
+
};
|
|
44
|
+
export declare function imageMarkup(image: ResolvedImage, options: ImageMarkupOptions): ImageMarkup;
|
|
45
|
+
/** A video's poster: its cut at the src width and its own quality where there is one, else the original. */
|
|
46
|
+
export declare function posterUrl(video: ResolvedVideo): string | undefined;
|
|
47
|
+
/** The markup as HTML, attribute for attribute what React writes for <SmoodlyImage> (Astro's <Image> sets it as is),
|
|
48
|
+
* React's spellings included (`srcSet`: attribute names are case-insensitive in HTML). */
|
|
49
|
+
export declare function imageHtml(m: ImageMarkup, box?: {
|
|
50
|
+
class?: string;
|
|
51
|
+
style?: string;
|
|
52
|
+
data?: Record<string, string | undefined>;
|
|
53
|
+
}): string;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// One image markup for both frameworks (spec 2026-10-18 media on R2 and Images §7; plan (c) decision 5): what
|
|
2
|
+
// <SmoodlyImage> (React) and <Image> (Astro) render, decided here so both are only markup and a test holds them equal.
|
|
3
|
+
// Plain HTML, no optimizer and no Smoodly JavaScript: a box, an <img> whose srcset lists the cut's widths, or a
|
|
4
|
+
// <picture> with one <source> per crop the view places. Without a cut (locally, an SVG) the original is framed in CSS
|
|
5
|
+
// by crop and hotspot, today's path (crop-math.ts).
|
|
6
|
+
import { CENTER_HOTSPOT, clamp01, parseAspect } from "../media.js";
|
|
7
|
+
import { cutUrl, cutWidths, DEFAULT_QUALITY, isImageQuality, parseCutSegment, qualityError } from "../media-cut.js";
|
|
8
|
+
import { escapeHtml } from "../richtext-href.js";
|
|
9
|
+
import { boxAspectOf, frameFor } from "./crop-math.js";
|
|
10
|
+
/** The `src` of a cut: the widest kept width up to 1280, a browser with no srcset support's fair middle. */
|
|
11
|
+
const SRC_WIDTH = 1280;
|
|
12
|
+
const pct = (n) => `${Math.round(n * 1000) / 1000}%`;
|
|
13
|
+
const frameStyle = (f) => `position:absolute;left:${pct(f.left)};top:${pct(f.top)};width:${pct(f.width)};height:${pct(f.height)};max-width:none`;
|
|
14
|
+
const FULL = frameStyle({ left: 0, top: 0, width: 100, height: 100 });
|
|
15
|
+
const boxStyle = (aspect) => `display:block;position:relative;overflow:hidden;aspect-ratio:${aspect}`;
|
|
16
|
+
/** The rectangle a cut's address names (`…/<left>.<top>.<width>.<height>.<sig>`), or null for none. */
|
|
17
|
+
function rectOfCut(cut) {
|
|
18
|
+
return cut ? parseCutSegment(cut.slice(cut.lastIndexOf("/") + 1))?.rect ?? null : null;
|
|
19
|
+
}
|
|
20
|
+
function deliveryOf(image, options) {
|
|
21
|
+
const quality = options.quality ?? image.quality ?? DEFAULT_QUALITY;
|
|
22
|
+
if (!isImageQuality(quality))
|
|
23
|
+
throw new Error(qualityError(quality));
|
|
24
|
+
const maxWidth = options.maxWidth ?? image.maxWidth;
|
|
25
|
+
return { quality, ...(maxWidth !== undefined ? { maxWidth } : {}) };
|
|
26
|
+
}
|
|
27
|
+
function srcsetOf(cut, width, d) {
|
|
28
|
+
const widths = cutWidths(width, d.maxWidth);
|
|
29
|
+
const fit = widths.filter((w) => w <= SRC_WIDTH);
|
|
30
|
+
return {
|
|
31
|
+
src: cutUrl(cut, fit.length > 0 ? fit[fit.length - 1] : widths[0], d.quality),
|
|
32
|
+
srcset: widths.map((w) => `${cutUrl(cut, w, d.quality)} ${w}w`).join(", "),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** A cut shown in a box of another ratio (the view's `aspect`): framed around the hotspot, as the original was. */
|
|
36
|
+
function frameOverCut(image, rect, box) {
|
|
37
|
+
const spot = image.hotspot ?? CENTER_HOTSPOT;
|
|
38
|
+
const hotspot = { x: clamp01((spot.x * image.width - rect.left) / rect.width), y: clamp01((spot.y * image.height - rect.top) / rect.height) };
|
|
39
|
+
return frameStyle(frameFor({ width: rect.width, height: rect.height, hotspot }, box));
|
|
40
|
+
}
|
|
41
|
+
export function imageMarkup(image, options) {
|
|
42
|
+
const loading = options.priority ? "eager" : options.loading ?? "lazy";
|
|
43
|
+
const tag = { alt: options.alt ?? image.alt, loading, ...(options.priority ? { fetchpriority: "high" } : {}), decoding: "async" };
|
|
44
|
+
const sizes = options.sizes ? { sizes: options.sizes } : {};
|
|
45
|
+
const rect = image.mime === "image/svg+xml" ? null : rectOfCut(image.cut);
|
|
46
|
+
const spot = image.hotspot ?? CENTER_HOTSPOT;
|
|
47
|
+
const delivery = deliveryOf(image, options);
|
|
48
|
+
// Art direction: a <source> per placed crop; each has its own shape, so the box takes it from the chosen source's
|
|
49
|
+
// width and height, and the image covers it (around the hotspot where there is no cut, locally).
|
|
50
|
+
const sources = [];
|
|
51
|
+
for (const [name, media] of Object.entries(options.media ?? {})) {
|
|
52
|
+
const crop = image.crops?.[name];
|
|
53
|
+
if (!crop)
|
|
54
|
+
continue;
|
|
55
|
+
const own = rectOfCut(crop.cut);
|
|
56
|
+
sources.push({ media, srcset: own && crop.cut ? srcsetOf(crop.cut, own.width, delivery).srcset : image.url, ...sizes, width: crop.width, height: crop.height });
|
|
57
|
+
}
|
|
58
|
+
if (sources.length > 0) {
|
|
59
|
+
const main = image.crops ? Object.values(image.crops)[0] : undefined;
|
|
60
|
+
const size = rect ? { width: rect.width, height: rect.height } : main ? { width: main.width, height: main.height } : { width: image.width, height: image.height };
|
|
61
|
+
return {
|
|
62
|
+
box: "display:block",
|
|
63
|
+
sources,
|
|
64
|
+
img: {
|
|
65
|
+
...(rect && image.cut ? { ...srcsetOf(image.cut, rect.width, delivery), ...sizes } : { src: image.url }),
|
|
66
|
+
...tag, ...size,
|
|
67
|
+
style: `display:block;width:100%;height:auto;object-fit:cover;object-position:${pct(spot.x * 100)} ${pct(spot.y * 100)}`,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (rect && image.cut) {
|
|
72
|
+
const own = rect.width / rect.height;
|
|
73
|
+
const box = options.aspect ? parseAspect(options.aspect) : own;
|
|
74
|
+
return {
|
|
75
|
+
box: boxStyle(box),
|
|
76
|
+
sources,
|
|
77
|
+
img: {
|
|
78
|
+
...srcsetOf(image.cut, rect.width, delivery), ...sizes, ...tag, width: rect.width, height: rect.height,
|
|
79
|
+
style: Math.abs(box / own - 1) < 0.005 ? FULL : frameOverCut(image, rect, box),
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// No cut: the original, framed in CSS by crop and hotspot (an SVG fills its box as it is).
|
|
84
|
+
const box = boxAspectOf(image, options.aspect);
|
|
85
|
+
return {
|
|
86
|
+
box: boxStyle(box),
|
|
87
|
+
sources,
|
|
88
|
+
img: { src: image.url, ...tag, width: image.width, height: image.height, style: frameStyle(frameFor(image, box)) },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** A video's poster: its cut at the src width and its own quality where there is one, else the original. */
|
|
92
|
+
export function posterUrl(video) {
|
|
93
|
+
const poster = video.poster;
|
|
94
|
+
if (!poster)
|
|
95
|
+
return undefined;
|
|
96
|
+
const rect = rectOfCut(poster.cut);
|
|
97
|
+
return rect && poster.cut ? srcsetOf(poster.cut, rect.width, deliveryOf(poster, {})).src : poster.url;
|
|
98
|
+
}
|
|
99
|
+
const attr = (name, value) => (value === undefined ? "" : ` ${name}="${escapeHtml(String(value))}"`);
|
|
100
|
+
/** The markup as HTML, attribute for attribute what React writes for <SmoodlyImage> (Astro's <Image> sets it as is),
|
|
101
|
+
* React's spellings included (`srcSet`: attribute names are case-insensitive in HTML). */
|
|
102
|
+
export function imageHtml(m, box = {}) {
|
|
103
|
+
const style = box.style ? `${m.box};${box.style}` : m.box;
|
|
104
|
+
const data = Object.entries(box.data ?? {}).map(([k, v]) => attr(k, v)).join("");
|
|
105
|
+
const i = m.img;
|
|
106
|
+
const img = `<img${attr("src", i.src)}${attr("srcSet", i.srcset)}${attr("sizes", i.sizes)}${attr("alt", i.alt)}${attr("width", i.width)}${attr("height", i.height)}`
|
|
107
|
+
+ `${attr("loading", i.loading)}${attr("fetchPriority", i.fetchpriority)}${attr("decoding", i.decoding)}${attr("style", i.style)}/>`;
|
|
108
|
+
const sources = m.sources.map((s) => `<source${attr("media", s.media)}${attr("srcSet", s.srcset)}${attr("sizes", s.sizes)}${attr("width", s.width)}${attr("height", s.height)}${attr("type", s.type)}/>`).join("");
|
|
109
|
+
const inner = m.sources.length > 0 ? `<picture>${sources}${img}</picture>` : img;
|
|
110
|
+
return `<span${attr("class", box.class)}${data}${attr("style", style)}>${inner}</span>`;
|
|
111
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { collection } from "./collections.ts";
|
|
|
2
2
|
import { toolset } from "./toolset.ts";
|
|
3
3
|
import { globalItem } from "./global.ts";
|
|
4
4
|
import { marks } from "./marks.ts";
|
|
5
|
-
export { createSupabaseStores, siteOptions } from "./bind.ts";
|
|
6
5
|
export { schemaOf } from "./registration.ts";
|
|
7
6
|
export type { SchemaOr } from "./registration.ts";
|
|
8
7
|
export { EMPTY_PAGE_CONTEXT } from "./page-context.ts";
|
|
@@ -25,36 +24,24 @@ export { ASSET_COLLECTION, DEFAULT_MEDIA, MEDIA_TYPES, FULL_CROP, CENTER_HOTSPOT
|
|
|
25
24
|
export type { Crop, Hotspot, AssetKind, ImageValue, VideoValue, FileValue, AssetMeta, AssetRecord, ResolvedImage, ResolvedVideo, ResolvedFile, } from "./media.ts";
|
|
26
25
|
export { z } from "./zones.ts";
|
|
27
26
|
export { defineConfig } from "./config.ts";
|
|
28
|
-
export { collectionSQL, coreTablesSQL } from "./sql.ts";
|
|
29
|
-
export { SCHEMA_VERSION, SCHEMA_VERSION_KEY, schemaVersionSQL, coreFileVersion, schemaBehind, schemaBehindMessage } from "./schema-version.ts";
|
|
30
27
|
export { draftToken, sameToken, isDraftToken, assertFresh, StaleDraftError } from "./draft-token.ts";
|
|
31
28
|
export type { DraftToken, DraftPointerRow } from "./draft-token.ts";
|
|
32
|
-
export {
|
|
33
|
-
export { MemoryAssetStore, assetErrors, kindOf, extensionFor, objectPath, withDownload, PAGE_SIZE as ASSET_PAGE_SIZE } from "./asset-store.ts";
|
|
29
|
+
export { assetErrors, kindOf, extensionFor, objectPath, PAGE_SIZE as ASSET_PAGE_SIZE } from "./asset-store.ts";
|
|
34
30
|
export type { AssetStore, UploadTicket, FinishInput, AssetQuery, AssetPage, BeginUploadInput, CreateAssetInput } from "./asset-store.ts";
|
|
35
31
|
export { imageDimensions } from "./image-dimensions.ts";
|
|
36
|
-
export {
|
|
37
|
-
export {
|
|
38
|
-
export { MemoryMemberStore, sortMembers, memberErrors } from "./member-store.ts";
|
|
32
|
+
export type { ImageQuality } from "./media-cut.ts";
|
|
33
|
+
export { sortMembers, memberErrors } from "./member-store.ts";
|
|
39
34
|
export type { MemberStore, MemberRecord, InviteInput, InviteResult } from "./member-store.ts";
|
|
40
|
-
export { SupabaseMemberStore } from "./supabase-member-store.ts";
|
|
41
35
|
export { MEMBER_ROLES, can, normalizeRole, isMemberRole } from "./permissions.ts";
|
|
42
36
|
export type { MemberRole, Capability } from "./permissions.ts";
|
|
43
37
|
export { collectEntryRefs, collectTreeRefs } from "./refs.ts";
|
|
44
38
|
export type { RefEdge, SectionLike } from "./refs.ts";
|
|
45
39
|
export { richtextBlocks, elementSchemas, IMAGE_NODE, ELEMENT_NODE } from "./richtext-walk.ts";
|
|
46
40
|
export type { BlockNode, ElementSchemas, ImageNodeAttrs, ElementNodeAttrs } from "./richtext-walk.ts";
|
|
47
|
-
export { MemoryRefIndex } from "./ref-index.ts";
|
|
48
|
-
export { MemoryPathIndex } from "./path-index.ts";
|
|
49
41
|
export type { PathIndex } from "./path-index.ts";
|
|
50
|
-
export { MemoryEntryStore } from "./entry-store.ts";
|
|
51
|
-
export { SupabaseEntryStore } from "./supabase-entry-store.ts";
|
|
52
|
-
export { SupabasePathIndex } from "./supabase-path-index.ts";
|
|
53
42
|
export type { EntryStore, EntryRecord, EntryLocale, EntryVersion, EntryScan, EntryUsage, ListOptions, CreateEntryOptions, AddEntryLocaleOptions, EntryStoreOptions, Where, WhereValue, SnapshotQuery, SnapshotRow, } from "./entry-store.ts";
|
|
54
43
|
export { entryErrors, compareEntries, DEFAULT_ENTRY_LOCALES, localizedKeys, splitFields, entryLocaleRow, entryFieldsIn, entryRefEdges, entryTitleIn, matchesWhere, } from "./entry-store.ts";
|
|
55
|
-
export {
|
|
56
|
-
export { SupabasePublishedStore } from "./supabase-published-store.ts";
|
|
57
|
-
export type { PublishedStore, PublishedPageRow, PublishedEntryRow, PublishedRoute, PublishedTreeRow, PublishedQuery, PublishedKey } from "./published.ts";
|
|
44
|
+
export type { DeployStore } from "./deploy-store.ts";
|
|
58
45
|
export { affectedTargets, pageTag, entryTag, globalTag, pagesTreeTag, collectionTag } from "./revalidate.ts";
|
|
59
46
|
export type { AffectedTargets } from "./revalidate.ts";
|
|
60
47
|
export { resolveTree, resolveFields, resolveMedia } from "./resolve.ts";
|
|
@@ -83,7 +70,7 @@ export { SMD_KEY } from "./smd-key.ts";
|
|
|
83
70
|
export type { SmdInternal } from "./smd-key.ts";
|
|
84
71
|
export { CSS_MAX, cssProblem, safeCss, scopedCss } from "./css.ts";
|
|
85
72
|
export { globalEntryId } from "./global-id.ts";
|
|
86
|
-
export { DEFAULT_SPACE_ID } from "./
|
|
73
|
+
export { DEFAULT_SPACE_ID } from "./global-id.ts";
|
|
87
74
|
export type { GlobalSchema, GlobalLink } from "./global.ts";
|
|
88
75
|
/** The registration namespace: schemas and their parts. The React pairing (`smoodly.section`, `<smoodly.Section>`) is smoodly/react's. */
|
|
89
76
|
export declare const smoodly: {
|
|
@@ -127,8 +114,11 @@ export declare const smoodly: {
|
|
|
127
114
|
};
|
|
128
115
|
export { NODE_ATTR, ZONE_ATTR, EMPTY_ATTR, BLANK_ATTR, FIELD_ATTR, ITEM_ATTR, EDITOR_ATTR, resolveMarkPath } from "./marks.ts";
|
|
129
116
|
export type { MarkNode, MarkPath, Marks, Scope } from "./marks.ts";
|
|
130
|
-
export { resolveSmoodlyEnv, smoodlyEnv, presenceConfig, siteUrl } from "./env.ts";
|
|
117
|
+
export { apiEnv, resolveSmoodlyEnv, smoodlyEnv, presenceConfig, siteUrl } from "./env.ts";
|
|
118
|
+
export type { SmoodlyEnvNeed } from "./env.ts";
|
|
119
|
+
export { createSmoodlyApi, SmoodlyApiError } from "./api-client.ts";
|
|
120
|
+
export type { SmoodlyApiOptions } from "./api-client.ts";
|
|
131
121
|
export type { SmoodlyEnv, PresenceConfig } from "./env.ts";
|
|
132
|
-
export {
|
|
133
|
-
export {
|
|
134
|
-
export type { SiteOptions, SitePageSchema,
|
|
122
|
+
export { MESSAGE_MAX_AGE_MS, signMessage, verifyMessage } from "./signed-message.ts";
|
|
123
|
+
export { pageContextOf, siteStatics } from "./site.ts";
|
|
124
|
+
export type { SiteOptions, SitePageSchema, ContentReads, DeliveryReads, LoadOptions, PageTreeItem, EntryTreeItem, EntryQuery, } from "./site.ts";
|