smoodly 0.0.9 → 0.0.11
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 +167 -18
- package/dist/admin/client-ops.js +1 -0
- package/dist/admin/editor/CanvasOverlay.d.ts +21 -0
- package/dist/admin/editor/CanvasOverlay.js +39 -0
- package/dist/admin/editor/EditorView.js +56 -237
- package/dist/admin/editor/InsertPicker.d.ts +18 -0
- package/dist/admin/editor/InsertPicker.js +18 -0
- package/dist/admin/editor/NoticePane.d.ts +4 -0
- package/dist/admin/editor/NoticePane.js +4 -0
- package/dist/admin/editor/Outline.d.ts +18 -0
- package/dist/admin/editor/Outline.js +12 -0
- package/dist/admin/editor/locale-url.d.ts +12 -0
- package/dist/admin/editor/locale-url.js +32 -0
- package/dist/admin/editor/overlay-geometry.d.ts +14 -0
- package/dist/admin/editor/overlay-geometry.js +27 -0
- package/dist/admin/editor/useCanvasBridge.d.ts +15 -0
- package/dist/admin/editor/useCanvasBridge.js +53 -0
- package/dist/admin/editor/usePageLocale.d.ts +23 -0
- package/dist/admin/editor/usePageLocale.js +88 -0
- package/dist/admin/forms/CropEditor.d.ts +10 -0
- package/dist/admin/forms/CropEditor.js +61 -0
- package/dist/admin/forms/FieldWidget.js +8 -26
- package/dist/admin/forms/MediaGrid.d.ts +11 -0
- package/dist/admin/forms/MediaGrid.js +16 -0
- package/dist/admin/forms/MediaPicker.d.ts +8 -0
- package/dist/admin/forms/MediaPicker.js +44 -0
- package/dist/admin/forms/MediaWidget.d.ts +9 -0
- package/dist/admin/forms/MediaWidget.js +83 -0
- package/dist/admin/forms/RichtextToolbar.d.ts +20 -0
- package/dist/admin/forms/RichtextToolbar.js +37 -0
- package/dist/admin/forms/RichtextWidget.d.ts +5 -0
- package/dist/admin/forms/RichtextWidget.js +123 -0
- package/dist/admin/forms/assets-context.d.ts +19 -0
- package/dist/admin/forms/assets-context.js +35 -0
- package/dist/admin/forms/crop-editor.d.ts +13 -0
- package/dist/admin/forms/crop-editor.js +72 -0
- package/dist/admin/forms/measure.d.ts +2 -0
- package/dist/admin/forms/measure.js +39 -0
- package/dist/admin/forms/richtext-extensions.d.ts +5 -0
- package/dist/admin/forms/richtext-extensions.js +74 -0
- package/dist/admin/forms/richtext-paste.d.ts +22 -0
- package/dist/admin/forms/richtext-paste.js +59 -0
- package/dist/admin/forms/upload.d.ts +17 -0
- package/dist/admin/forms/upload.js +33 -0
- package/dist/admin/index.d.ts +1 -2
- package/dist/admin/index.js +0 -1
- package/dist/admin/next/create-admin.d.ts +3 -7
- package/dist/admin/next/create-admin.js +14 -5
- package/dist/admin/next/op-handler.d.ts +3 -0
- package/dist/admin/next/op-handler.js +1 -0
- package/dist/admin/ops-impl.d.ts +3 -0
- package/dist/admin/ops-impl.js +83 -2
- package/dist/admin/ops.d.ts +27 -0
- package/dist/admin/richtext-doc.d.ts +7 -0
- package/dist/admin/richtext-doc.js +18 -0
- package/dist/admin/serialize.d.ts +5 -0
- package/dist/admin/serialize.js +1 -0
- package/dist/admin/shell/AdminApp.d.ts +1 -2
- package/dist/admin/shell/AdminApp.js +18 -19
- package/dist/admin/shell/EntriesList.js +3 -2
- package/dist/admin/shell/EntryForm.js +8 -24
- package/dist/admin/shell/IconRail.d.ts +7 -0
- package/dist/admin/shell/IconRail.js +73 -0
- package/dist/admin/shell/ListView.js +3 -1
- package/dist/admin/shell/MediaScreen.d.ts +8 -0
- package/dist/admin/shell/MediaScreen.js +86 -0
- package/dist/admin/shell/PagesList.js +3 -2
- package/dist/admin/shell/RouteLink.d.ts +4 -0
- package/dist/admin/shell/RouteLink.js +12 -0
- package/dist/admin/shell/SharedForm.js +3 -21
- package/dist/admin/shell/media-list.d.ts +8 -0
- package/dist/admin/shell/media-list.js +14 -0
- package/dist/admin/shell/router.d.ts +37 -0
- package/dist/admin/shell/router.js +84 -0
- package/dist/admin/shell/session.js +3 -1
- package/dist/admin/ui/Modal.d.ts +8 -0
- package/dist/admin/ui/Modal.js +15 -0
- package/dist/admin/ui/primitives.d.ts +3 -2
- package/dist/admin/ui/primitives.js +8 -6
- package/dist/admin/ui/theme.d.ts +1 -1
- package/dist/admin/ui/theme.js +133 -24
- package/dist/admin/validate.js +16 -0
- package/dist/asset-store.d.ts +86 -0
- package/dist/asset-store.js +141 -0
- package/dist/collections.d.ts +0 -14
- package/dist/collections.js +2 -10
- package/dist/config.d.ts +12 -0
- package/dist/config.js +17 -5
- package/dist/fields.d.ts +10 -11
- package/dist/fields.js +16 -2
- package/dist/image/SmoodlyImage.d.ts +18 -0
- package/dist/image/SmoodlyImage.js +16 -0
- package/dist/image/SmoodlyVideo.d.ts +7 -0
- package/dist/image/SmoodlyVideo.js +5 -0
- package/dist/image/crop-math.d.ts +14 -0
- package/dist/image/crop-math.js +38 -0
- package/dist/image/index.d.ts +6 -0
- package/dist/image/index.js +5 -0
- package/dist/image/loaders.d.ts +15 -0
- package/dist/image/loaders.js +5 -0
- package/dist/image/next-config.d.ts +15 -0
- package/dist/image/next-config.js +27 -0
- package/dist/image-dimensions.d.ts +4 -0
- package/dist/image-dimensions.js +25 -0
- package/dist/index.d.ts +12 -4
- package/dist/index.js +8 -2
- package/dist/media.d.ts +108 -0
- package/dist/media.js +129 -0
- package/dist/refs.d.ts +2 -0
- package/dist/refs.js +25 -4
- package/dist/resolve.d.ts +19 -4
- package/dist/resolve.js +55 -2
- package/dist/site.d.ts +2 -0
- package/dist/site.js +21 -3
- package/dist/sql-space.d.ts +8 -1
- package/dist/sql-space.js +28 -0
- package/dist/sql.js +30 -0
- package/dist/store.js +3 -0
- package/dist/supabase-asset-store.d.ts +21 -0
- package/dist/supabase-asset-store.js +170 -0
- package/dist/toolset.d.ts +30 -0
- package/dist/toolset.js +49 -0
- package/package.json +15 -1
- package/dist/admin/richtext.d.ts +0 -16
- package/dist/admin/richtext.js +0 -98
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// <SmoodlyImage>: a box at the aspect (declared, chosen or natural), the
|
|
3
|
+
// image inside positioned by crop and hotspot, dimensions set so nothing
|
|
4
|
+
// shifts. Wraps next/image; the only file besides SmoodlyVideo that a
|
|
5
|
+
// site imports from the media layer. Server-safe — no "use client".
|
|
6
|
+
import Image from "next/image";
|
|
7
|
+
import { boxAspectOf, frameFor } from "./crop-math.js";
|
|
8
|
+
export function SmoodlyImage({ image, aspect, sizes, priority, loading, className, style, alt, loader, unoptimized }) {
|
|
9
|
+
const box = boxAspectOf(image, aspect);
|
|
10
|
+
const frame = loader?.handlesCrop ? { left: 0, top: 0, width: 100, height: 100 } : frameFor(image, box);
|
|
11
|
+
const svg = image.mime === "image/svg+xml";
|
|
12
|
+
return (_jsx("span", { className: className, style: { display: "block", position: "relative", overflow: "hidden", aspectRatio: String(box), ...style }, children: _jsx(Image, { src: image.url, alt: alt ?? image.alt, width: image.width, height: image.height, sizes: sizes, priority: priority, loading: loading, unoptimized: unoptimized ?? svg, ...(loader ? { loader: ({ width, quality }) => loader.url(image, { width, quality }) } : {}), style: {
|
|
13
|
+
position: "absolute", left: `${frame.left}%`, top: `${frame.top}%`,
|
|
14
|
+
width: `${frame.width}%`, height: `${frame.height}%`, maxWidth: "none",
|
|
15
|
+
} }) }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ReactElement, VideoHTMLAttributes } from "react";
|
|
2
|
+
import type { ResolvedVideo } from "../media.ts";
|
|
3
|
+
export type SmoodlyVideoProps = {
|
|
4
|
+
video: ResolvedVideo;
|
|
5
|
+
} & Omit<VideoHTMLAttributes<HTMLVideoElement>, "src" | "poster" | "width" | "height">;
|
|
6
|
+
/** A <video> over the bucket's CDN with its poster and size. No transcoding (spec §9). */
|
|
7
|
+
export declare function SmoodlyVideo({ video, ...rest }: SmoodlyVideoProps): ReactElement;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/** A <video> over the bucket's CDN with its poster and size. No transcoding (spec §9). */
|
|
3
|
+
export function SmoodlyVideo({ video, ...rest }) {
|
|
4
|
+
return (_jsx("video", { src: video.url, poster: video.poster?.url, width: video.width, height: video.height, playsInline: true, ...rest }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Crop, type Hotspot, type ResolvedImage } from "../media.ts";
|
|
2
|
+
export type Frame = {
|
|
3
|
+
left: number;
|
|
4
|
+
top: number;
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function boxAspectOf(image: ResolvedImage, override?: string): number;
|
|
9
|
+
export declare function frameFor(image: {
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
crop?: Crop;
|
|
13
|
+
hotspot?: Hotspot;
|
|
14
|
+
}, boxAspect: number): Frame;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// The render-side frame: where the image sits inside its box so the crop
|
|
2
|
+
// covers it and the hotspot stays as central as the crop allows. Box
|
|
3
|
+
// units: height 1, width = the box's aspect. Percentages out, so the
|
|
4
|
+
// component never needs the box's pixel size (spec 2026-09-08 media §9).
|
|
5
|
+
import { CENTER_HOTSPOT, FULL_CROP, clamp01, cropAspect, parseAspect } from "../media.js";
|
|
6
|
+
export function boxAspectOf(image, override) {
|
|
7
|
+
if (override)
|
|
8
|
+
return parseAspect(override);
|
|
9
|
+
if (image.aspect)
|
|
10
|
+
return parseAspect(image.aspect);
|
|
11
|
+
const imageAspect = image.width / image.height;
|
|
12
|
+
return image.crop ? cropAspect(image.crop, imageAspect) : imageAspect;
|
|
13
|
+
}
|
|
14
|
+
export function frameFor(image, boxAspect) {
|
|
15
|
+
const crop = image.crop ?? FULL_CROP;
|
|
16
|
+
const hotspot = image.hotspot ?? CENTER_HOTSPOT;
|
|
17
|
+
const imageAspect = image.width / image.height;
|
|
18
|
+
const ca = cropAspect(crop, imageAspect);
|
|
19
|
+
// Scale the image so the crop covers the box: fit the crop's height when
|
|
20
|
+
// the crop is wider than the box, its width when it is taller.
|
|
21
|
+
let w, h;
|
|
22
|
+
if (ca >= boxAspect) {
|
|
23
|
+
h = 1 / crop.height;
|
|
24
|
+
w = h * imageAspect;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
w = boxAspect / crop.width;
|
|
28
|
+
h = w / imageAspect;
|
|
29
|
+
}
|
|
30
|
+
const cropW = crop.width * w, cropH = crop.height * h;
|
|
31
|
+
const hx = clamp01((hotspot.x - crop.left) / crop.width);
|
|
32
|
+
const hy = clamp01((hotspot.y - crop.top) / crop.height);
|
|
33
|
+
const winLeft = Math.min(Math.max(hx * cropW - boxAspect / 2, 0), Math.max(cropW - boxAspect, 0));
|
|
34
|
+
const winTop = Math.min(Math.max(hy * cropH - 0.5, 0), Math.max(cropH - 1, 0));
|
|
35
|
+
const left = -(crop.left * w) - winLeft;
|
|
36
|
+
const top = -(crop.top * h) - winTop;
|
|
37
|
+
return { left: (left / boxAspect) * 100, top: top * 100, width: (w / boxAspect) * 100, height: h * 100 };
|
|
38
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { SmoodlyImage, type SmoodlyImageProps } from "./SmoodlyImage.tsx";
|
|
2
|
+
export { SmoodlyVideo, type SmoodlyVideoProps } from "./SmoodlyVideo.tsx";
|
|
3
|
+
export { imageUrl, nextLoader, type ImageLoader, type ImageUrlOptions } from "./loaders.ts";
|
|
4
|
+
export { frameFor, boxAspectOf, type Frame } from "./crop-math.ts";
|
|
5
|
+
export { smoodlyImages, isPrivateHost, type RemotePattern, type SmoodlyImagesConfig } from "./next-config.ts";
|
|
6
|
+
export type { ResolvedImage, ResolvedVideo, ResolvedFile, Crop, Hotspot } from "../media.ts";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { SmoodlyImage } from "./SmoodlyImage.js";
|
|
2
|
+
export { SmoodlyVideo } from "./SmoodlyVideo.js";
|
|
3
|
+
export { imageUrl, nextLoader } from "./loaders.js";
|
|
4
|
+
export { frameFor, boxAspectOf } from "./crop-math.js";
|
|
5
|
+
export { smoodlyImages, isPrivateHost } from "./next-config.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ResolvedImage } from "../media.ts";
|
|
2
|
+
export type ImageUrlOptions = {
|
|
3
|
+
width?: number;
|
|
4
|
+
quality?: number;
|
|
5
|
+
format?: string;
|
|
6
|
+
};
|
|
7
|
+
export type ImageLoader = {
|
|
8
|
+
name: string;
|
|
9
|
+
/** True when the URL already applies crop and hotspot. */
|
|
10
|
+
handlesCrop: boolean;
|
|
11
|
+
url(image: ResolvedImage, opts: ImageUrlOptions): string;
|
|
12
|
+
};
|
|
13
|
+
export declare const nextLoader: ImageLoader;
|
|
14
|
+
/** The escape hatch: a URL for a width, through the loader. */
|
|
15
|
+
export declare function imageUrl(image: ResolvedImage, opts?: ImageUrlOptions, loader?: ImageLoader): string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type RemotePattern = {
|
|
2
|
+
protocol: "http" | "https";
|
|
3
|
+
hostname: string;
|
|
4
|
+
port?: string;
|
|
5
|
+
pathname: string;
|
|
6
|
+
};
|
|
7
|
+
export type SmoodlyImagesConfig = {
|
|
8
|
+
remotePatterns: RemotePattern[];
|
|
9
|
+
dangerouslyAllowLocalIP?: true;
|
|
10
|
+
};
|
|
11
|
+
/** `localhost`, loopback and the RFC 1918 ranges — the hosts a local
|
|
12
|
+
* Supabase stack answers on. A public hostname that happens to resolve
|
|
13
|
+
* privately (a Docker alias) is not detected; set the flag by hand then. */
|
|
14
|
+
export declare function isPrivateHost(hostname: string): boolean;
|
|
15
|
+
export declare function smoodlyImages(url?: string | undefined): SmoodlyImagesConfig;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** `localhost`, loopback and the RFC 1918 ranges — the hosts a local
|
|
2
|
+
* Supabase stack answers on. A public hostname that happens to resolve
|
|
3
|
+
* privately (a Docker alias) is not detected; set the flag by hand then. */
|
|
4
|
+
export function isPrivateHost(hostname) {
|
|
5
|
+
const h = hostname.replace(/^\[|\]$/g, "");
|
|
6
|
+
if (h === "localhost" || h.endsWith(".localhost") || h === "::1")
|
|
7
|
+
return true;
|
|
8
|
+
const m = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(h);
|
|
9
|
+
if (!m)
|
|
10
|
+
return false;
|
|
11
|
+
const [a, b] = [Number(m[1]), Number(m[2])];
|
|
12
|
+
return a === 127 || a === 10 || (a === 192 && b === 168) || (a === 172 && b >= 16 && b <= 31);
|
|
13
|
+
}
|
|
14
|
+
export function smoodlyImages(url = process.env.SMOODLY_URL) {
|
|
15
|
+
if (!url) {
|
|
16
|
+
console.warn("smoodly: SMOODLY_URL is not set — next/image will refuse Supabase media URLs until it is.");
|
|
17
|
+
return { remotePatterns: [] };
|
|
18
|
+
}
|
|
19
|
+
const u = new URL(url);
|
|
20
|
+
const pattern = {
|
|
21
|
+
protocol: u.protocol === "http:" ? "http" : "https",
|
|
22
|
+
hostname: u.hostname,
|
|
23
|
+
...(u.port ? { port: u.port } : {}),
|
|
24
|
+
pathname: "/storage/v1/object/public/media/**",
|
|
25
|
+
};
|
|
26
|
+
return { remotePatterns: [pattern], ...(isPrivateHost(u.hostname) ? { dangerouslyAllowLocalIP: true } : {}) };
|
|
27
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Header-only image dimensions for the two formats the starter ships
|
|
2
|
+
// (JPEG, PNG). The seed and AssetStore.create use it server-side, where
|
|
3
|
+
// there is no <img> to measure with and no sharp to depend on. Null for
|
|
4
|
+
// anything else — the row then carries no dimensions.
|
|
5
|
+
export function imageDimensions(bytes) {
|
|
6
|
+
if (bytes.length >= 24 && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) {
|
|
7
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
8
|
+
return { width: view.getUint32(16), height: view.getUint32(20) };
|
|
9
|
+
}
|
|
10
|
+
if (bytes.length >= 4 && bytes[0] === 0xff && bytes[1] === 0xd8) {
|
|
11
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
12
|
+
let i = 2;
|
|
13
|
+
while (i + 9 < bytes.length) {
|
|
14
|
+
if (bytes[i] !== 0xff)
|
|
15
|
+
return null;
|
|
16
|
+
const marker = bytes[i + 1];
|
|
17
|
+
// SOF0..SOF15 except DHT (C4), JPG (C8) and DAC (CC) carry the frame size
|
|
18
|
+
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
19
|
+
return { height: view.getUint16(i + 5), width: view.getUint16(i + 7) };
|
|
20
|
+
}
|
|
21
|
+
i += 2 + view.getUint16(i + 2);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { section, element } from "./pairing.tsx";
|
|
2
|
-
import { collection
|
|
2
|
+
import { collection } from "./collections.ts";
|
|
3
|
+
import { toolset } from "./toolset.ts";
|
|
3
4
|
import { Section } from "./section-wrapper.tsx";
|
|
4
5
|
import { page } from "./render.tsx";
|
|
5
6
|
import { entryPage } from "./entry-page.tsx";
|
|
@@ -11,11 +12,17 @@ export type { PageSchema } from "./page.ts";
|
|
|
11
12
|
export { fixedSlug } from "./page.ts";
|
|
12
13
|
export type { EntryPageSchema, PairedEntryPage, EntryPageProps } from "./entry-page.tsx";
|
|
13
14
|
export { f } from "./fields.ts";
|
|
15
|
+
export { ASSET_COLLECTION, DEFAULT_MEDIA, MEDIA_TYPES, FULL_CROP, CENTER_HOTSPOT, parseAspect, cropAspect, clamp01, isMediaValue, collectAssetIds, applyAssets, } from "./media.ts";
|
|
16
|
+
export type { Crop, Hotspot, AssetKind, ImageValue, VideoValue, FileValue, AssetMeta, AssetRecord, ResolvedImage, ResolvedVideo, ResolvedFile, } from "./media.ts";
|
|
14
17
|
export { z } from "./zones.ts";
|
|
15
18
|
export { defineConfig } from "./config.ts";
|
|
16
19
|
export { collectionSQL, coreTablesSQL } from "./sql.ts";
|
|
17
20
|
export { MemoryPageStore } from "./store.ts";
|
|
21
|
+
export { MemoryAssetStore, assetErrors, kindOf, extensionFor, objectPath, withDownload, PAGE_SIZE as ASSET_PAGE_SIZE } from "./asset-store.ts";
|
|
22
|
+
export type { AssetStore, UploadTicket, FinishInput, AssetQuery, AssetPage, BeginUploadInput, CreateAssetInput } from "./asset-store.ts";
|
|
23
|
+
export { imageDimensions } from "./image-dimensions.ts";
|
|
18
24
|
export { SupabasePageStore } from "./supabase-store.ts";
|
|
25
|
+
export { SupabaseAssetStore } from "./supabase-asset-store.ts";
|
|
19
26
|
export { collectEntryRefs, collectTreeRefs } from "./refs.ts";
|
|
20
27
|
export type { RefEdge, SectionLike } from "./refs.ts";
|
|
21
28
|
export { MemoryRefIndex } from "./ref-index.ts";
|
|
@@ -28,8 +35,8 @@ export type { EntryStore, EntryRecord, EntryLocale, EntryVersion, EntryUsage, Li
|
|
|
28
35
|
export { entryErrors, compareEntries, DEFAULT_ENTRY_LOCALES, localizedKeys, splitFields, entryLocaleRow, entryFieldsIn, entryRefEdges, entryTitleIn, } from "./entry-store.ts";
|
|
29
36
|
export { affectedTargets, pageTag, entryTag, sharedTag } from "./revalidate.ts";
|
|
30
37
|
export type { AffectedTargets } from "./revalidate.ts";
|
|
31
|
-
export { resolveTree, resolveFields } from "./resolve.ts";
|
|
32
|
-
export type { EntryFetcher, ResolveFieldsOptions } from "./resolve.ts";
|
|
38
|
+
export { resolveTree, resolveFields, resolveMedia } from "./resolve.ts";
|
|
39
|
+
export type { EntryFetcher, ResolveFieldsOptions, AssetFetcher } from "./resolve.ts";
|
|
33
40
|
export { SEGMENT_RE, isSegment, assertSegment, joinPath, splitPath, segmentFor, isHomeNode, pagePathIn, chainOf, assertDepth, pagePath, pagePathRows, entryPathRows, collectionSegment, buildPageTree, perLocaleSegments, assertSupportedLocale, unsupportedLocale, } from "./paths.ts";
|
|
34
41
|
export type { LocaleSet, PathTarget, PathRow, PageLike, EntryLike, PageTreeNode, PagePathOptions } from "./paths.ts";
|
|
35
42
|
export { fixedPathFor } from "./localize.ts";
|
|
@@ -39,7 +46,8 @@ export type { SmoodlyConfig, ResolvedConfig } from "./config.ts";
|
|
|
39
46
|
export type { FieldBuilder, Descriptor, ValueOf } from "./fields.ts";
|
|
40
47
|
export type { SectionSchema, ElementSchema, BuilderMap } from "./schema.ts";
|
|
41
48
|
export type { Props, PairedComponent } from "./pairing.tsx";
|
|
42
|
-
export type { CollectionSchema,
|
|
49
|
+
export type { CollectionSchema, EntryOrder } from "./collections.ts";
|
|
50
|
+
export { DEFAULT_TOOLSET, type Toolset, type ToolsetMark, type ToolsetNode } from "./toolset.ts";
|
|
43
51
|
export { collectionOrder, collectionDepth, DEFAULT_ORDER } from "./collections.ts";
|
|
44
52
|
export type { ZonePolicy } from "./zones.ts";
|
|
45
53
|
export { SHARED_NODE_TYPE, STYLES_KEY, sharedLink } from "./shared.ts";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { schema } from "./schema.js";
|
|
2
2
|
import { section, element } from "./pairing.js";
|
|
3
|
-
import { collection
|
|
3
|
+
import { collection } from "./collections.js";
|
|
4
|
+
import { toolset } from "./toolset.js";
|
|
4
5
|
import { Section } from "./section-wrapper.js";
|
|
5
6
|
import { page } from "./render.js";
|
|
6
7
|
import { entryPage } from "./entry-page.js";
|
|
@@ -9,11 +10,15 @@ export { Zone, renderPage, EMPTY_PAGE_CONTEXT } from "./render.js";
|
|
|
9
10
|
export { RichText } from "./richtext-render.js";
|
|
10
11
|
export { fixedSlug } from "./page.js";
|
|
11
12
|
export { f } from "./fields.js";
|
|
13
|
+
export { ASSET_COLLECTION, DEFAULT_MEDIA, MEDIA_TYPES, FULL_CROP, CENTER_HOTSPOT, parseAspect, cropAspect, clamp01, isMediaValue, collectAssetIds, applyAssets, } from "./media.js";
|
|
12
14
|
export { z } from "./zones.js";
|
|
13
15
|
export { defineConfig } from "./config.js";
|
|
14
16
|
export { collectionSQL, coreTablesSQL } from "./sql.js";
|
|
15
17
|
export { MemoryPageStore } from "./store.js";
|
|
18
|
+
export { MemoryAssetStore, assetErrors, kindOf, extensionFor, objectPath, withDownload, PAGE_SIZE as ASSET_PAGE_SIZE } from "./asset-store.js";
|
|
19
|
+
export { imageDimensions } from "./image-dimensions.js";
|
|
16
20
|
export { SupabasePageStore } from "./supabase-store.js";
|
|
21
|
+
export { SupabaseAssetStore } from "./supabase-asset-store.js";
|
|
17
22
|
export { collectEntryRefs, collectTreeRefs } from "./refs.js";
|
|
18
23
|
export { MemoryRefIndex } from "./ref-index.js";
|
|
19
24
|
export { MemoryPathIndex } from "./path-index.js";
|
|
@@ -22,10 +27,11 @@ export { SupabaseEntryStore } from "./supabase-entry-store.js";
|
|
|
22
27
|
export { SupabasePathIndex } from "./supabase-path-index.js";
|
|
23
28
|
export { entryErrors, compareEntries, DEFAULT_ENTRY_LOCALES, localizedKeys, splitFields, entryLocaleRow, entryFieldsIn, entryRefEdges, entryTitleIn, } from "./entry-store.js";
|
|
24
29
|
export { affectedTargets, pageTag, entryTag, sharedTag } from "./revalidate.js";
|
|
25
|
-
export { resolveTree, resolveFields } from "./resolve.js";
|
|
30
|
+
export { resolveTree, resolveFields, resolveMedia } from "./resolve.js";
|
|
26
31
|
export { SEGMENT_RE, isSegment, assertSegment, joinPath, splitPath, segmentFor, isHomeNode, pagePathIn, chainOf, assertDepth, pagePath, pagePathRows, entryPathRows, collectionSegment, buildPageTree, perLocaleSegments, assertSupportedLocale, unsupportedLocale, } from "./paths.js";
|
|
27
32
|
export { fixedPathFor } from "./localize.js";
|
|
28
33
|
export { pathOptions, pageErrors, localeRow, pageRefEdges, DEFAULT_LOCALES } from "./store.js";
|
|
34
|
+
export { DEFAULT_TOOLSET } from "./toolset.js";
|
|
29
35
|
export { collectionOrder, collectionDepth, DEFAULT_ORDER } from "./collections.js";
|
|
30
36
|
export { SHARED_NODE_TYPE, STYLES_KEY, sharedLink } from "./shared.js";
|
|
31
37
|
export { sharedEntryId } from "./shared-id.js";
|
package/dist/media.d.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Descriptor } from "./fields.ts";
|
|
2
|
+
export type Crop = {
|
|
3
|
+
left: number;
|
|
4
|
+
top: number;
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
};
|
|
8
|
+
export type Hotspot = {
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
};
|
|
12
|
+
export type AssetKind = "image" | "video" | "file";
|
|
13
|
+
export type ImageValue = {
|
|
14
|
+
asset: string;
|
|
15
|
+
alt?: string;
|
|
16
|
+
crop?: Crop;
|
|
17
|
+
hotspot?: Hotspot;
|
|
18
|
+
aspect?: string;
|
|
19
|
+
};
|
|
20
|
+
export type VideoValue = {
|
|
21
|
+
asset: string;
|
|
22
|
+
title?: string;
|
|
23
|
+
};
|
|
24
|
+
export type FileValue = {
|
|
25
|
+
asset: string;
|
|
26
|
+
title?: string;
|
|
27
|
+
};
|
|
28
|
+
export type AssetMeta = {
|
|
29
|
+
title?: string | null;
|
|
30
|
+
alt?: string | null;
|
|
31
|
+
caption?: string | null;
|
|
32
|
+
credit?: string | null;
|
|
33
|
+
};
|
|
34
|
+
export type AssetRecord = {
|
|
35
|
+
id: string;
|
|
36
|
+
kind: AssetKind;
|
|
37
|
+
status: "pending" | "ready";
|
|
38
|
+
/** The object path in the bucket: `<space_id>/<id>.<ext>`. */
|
|
39
|
+
path: string;
|
|
40
|
+
mime: string;
|
|
41
|
+
size: number;
|
|
42
|
+
width: number | null;
|
|
43
|
+
height: number | null;
|
|
44
|
+
duration: number | null;
|
|
45
|
+
posterId: string | null;
|
|
46
|
+
filename: string;
|
|
47
|
+
title: string | null;
|
|
48
|
+
alt: string | null;
|
|
49
|
+
caption: string | null;
|
|
50
|
+
credit: string | null;
|
|
51
|
+
/** Derived by the store from the bucket's public URL, never stored. */
|
|
52
|
+
url: string;
|
|
53
|
+
createdAt: number;
|
|
54
|
+
updatedAt: number;
|
|
55
|
+
};
|
|
56
|
+
export type ResolvedImage = {
|
|
57
|
+
url: string;
|
|
58
|
+
width: number;
|
|
59
|
+
height: number;
|
|
60
|
+
mime: string;
|
|
61
|
+
alt: string;
|
|
62
|
+
crop?: Crop;
|
|
63
|
+
hotspot?: Hotspot;
|
|
64
|
+
aspect?: string;
|
|
65
|
+
};
|
|
66
|
+
export type ResolvedVideo = {
|
|
67
|
+
url: string;
|
|
68
|
+
width?: number;
|
|
69
|
+
height?: number;
|
|
70
|
+
duration?: number;
|
|
71
|
+
mime: string;
|
|
72
|
+
poster?: ResolvedImage;
|
|
73
|
+
title?: string;
|
|
74
|
+
};
|
|
75
|
+
export type ResolvedFile = {
|
|
76
|
+
url: string;
|
|
77
|
+
mime: string;
|
|
78
|
+
size: number;
|
|
79
|
+
filename: string;
|
|
80
|
+
title?: string;
|
|
81
|
+
};
|
|
82
|
+
/** The refs target collection of every asset edge — a reserved registered name. */
|
|
83
|
+
export declare const ASSET_COLLECTION = "asset";
|
|
84
|
+
export declare const MEDIA_TYPES: ReadonlySet<string>;
|
|
85
|
+
/** Cache lifetime of a media object, in seconds — a year. Objects are
|
|
86
|
+
* immutable (no upsert; Replace picks a new id), so the CDN, the browser
|
|
87
|
+
* and next/image may keep one for as long as they like. */
|
|
88
|
+
export declare const MEDIA_CACHE_SECONDS = 31536000;
|
|
89
|
+
export declare const DEFAULT_MEDIA: {
|
|
90
|
+
readonly maxSize: number;
|
|
91
|
+
readonly accept: readonly ["image/jpeg", "image/png", "image/webp", "image/avif", "image/gif", "image/svg+xml", "video/mp4", "video/webm", "application/pdf"];
|
|
92
|
+
};
|
|
93
|
+
export declare const FULL_CROP: Crop;
|
|
94
|
+
export declare const CENTER_HOTSPOT: Hotspot;
|
|
95
|
+
export declare const clamp01: (n: number) => number;
|
|
96
|
+
/** `"16:9"` → 16/9. Throws on anything else, so a typo fails at registration. */
|
|
97
|
+
export declare function parseAspect(s: string): number;
|
|
98
|
+
/** The crop's own pixel ratio, given the image's. */
|
|
99
|
+
export declare const cropAspect: (crop: Crop, imageAspect: number) => number;
|
|
100
|
+
export declare const isMediaValue: (v: unknown) => v is {
|
|
101
|
+
asset: string;
|
|
102
|
+
};
|
|
103
|
+
/** Every asset id a value set references, walking lists and objects —
|
|
104
|
+
* unlike refs, media may sit inside a container (spec §8). */
|
|
105
|
+
export declare function collectAssetIds(values: Record<string, unknown> | undefined, descriptors: Record<string, Descriptor>, out: Set<string>): void;
|
|
106
|
+
/** Replace every media value in place with its resolved object — or delete
|
|
107
|
+
* it when the asset is missing or pending, so `image?.url` guards hold. */
|
|
108
|
+
export declare function applyAssets(values: Record<string, unknown> | undefined, descriptors: Record<string, Descriptor>, assets: Map<string, AssetRecord>): void;
|
package/dist/media.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/** The refs target collection of every asset edge — a reserved registered name. */
|
|
2
|
+
export const ASSET_COLLECTION = "asset";
|
|
3
|
+
export const MEDIA_TYPES = new Set(["image", "video", "file"]);
|
|
4
|
+
/** Cache lifetime of a media object, in seconds — a year. Objects are
|
|
5
|
+
* immutable (no upsert; Replace picks a new id), so the CDN, the browser
|
|
6
|
+
* and next/image may keep one for as long as they like. */
|
|
7
|
+
export const MEDIA_CACHE_SECONDS = 31536000;
|
|
8
|
+
export const DEFAULT_MEDIA = {
|
|
9
|
+
maxSize: 25 * 1024 * 1024,
|
|
10
|
+
accept: [
|
|
11
|
+
"image/jpeg", "image/png", "image/webp", "image/avif", "image/gif", "image/svg+xml",
|
|
12
|
+
"video/mp4", "video/webm", "application/pdf",
|
|
13
|
+
],
|
|
14
|
+
};
|
|
15
|
+
export const FULL_CROP = { left: 0, top: 0, width: 1, height: 1 };
|
|
16
|
+
export const CENTER_HOTSPOT = { x: 0.5, y: 0.5 };
|
|
17
|
+
export const clamp01 = (n) => Math.min(1, Math.max(0, n));
|
|
18
|
+
const ASPECT_RE = /^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/;
|
|
19
|
+
/** `"16:9"` → 16/9. Throws on anything else, so a typo fails at registration. */
|
|
20
|
+
export function parseAspect(s) {
|
|
21
|
+
const m = ASPECT_RE.exec(s);
|
|
22
|
+
const w = m ? Number(m[1]) : 0;
|
|
23
|
+
const h = m ? Number(m[2]) : 0;
|
|
24
|
+
if (!m || w <= 0 || h <= 0)
|
|
25
|
+
throw new Error(`smoodly: "${s}" is not an aspect — use "w:h", like "16:9".`);
|
|
26
|
+
return w / h;
|
|
27
|
+
}
|
|
28
|
+
/** The crop's own pixel ratio, given the image's. */
|
|
29
|
+
export const cropAspect = (crop, imageAspect) => (crop.width * imageAspect) / crop.height;
|
|
30
|
+
export const isMediaValue = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && typeof v.asset === "string";
|
|
31
|
+
/** Every asset id a value set references, walking lists and objects —
|
|
32
|
+
* unlike refs, media may sit inside a container (spec §8). */
|
|
33
|
+
export function collectAssetIds(values, descriptors, out) {
|
|
34
|
+
if (!values)
|
|
35
|
+
return;
|
|
36
|
+
for (const [key, d] of Object.entries(descriptors)) {
|
|
37
|
+
const value = values[key];
|
|
38
|
+
if (value == null)
|
|
39
|
+
continue;
|
|
40
|
+
if (MEDIA_TYPES.has(d.type)) {
|
|
41
|
+
if (isMediaValue(value))
|
|
42
|
+
out.add(value.asset);
|
|
43
|
+
}
|
|
44
|
+
else if (d.type === "object" && typeof value === "object" && !Array.isArray(value)) {
|
|
45
|
+
collectAssetIds(value, d.fields, out);
|
|
46
|
+
}
|
|
47
|
+
else if (d.type === "list" && Array.isArray(value)) {
|
|
48
|
+
const item = d.item;
|
|
49
|
+
for (const v of value) {
|
|
50
|
+
if (MEDIA_TYPES.has(item.type)) {
|
|
51
|
+
if (isMediaValue(v))
|
|
52
|
+
out.add(v.asset);
|
|
53
|
+
}
|
|
54
|
+
else if (item.type === "object" && v && typeof v === "object") {
|
|
55
|
+
collectAssetIds(v, item.fields, out);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function resolvedImage(asset, value, d) {
|
|
62
|
+
if (asset.width === null || asset.height === null)
|
|
63
|
+
return undefined;
|
|
64
|
+
const declared = d.aspect;
|
|
65
|
+
const aspect = value.aspect ?? (typeof declared === "string" ? declared : undefined);
|
|
66
|
+
return {
|
|
67
|
+
url: asset.url, width: asset.width, height: asset.height, mime: asset.mime,
|
|
68
|
+
alt: value.alt ?? asset.alt ?? "",
|
|
69
|
+
...(value.crop ? { crop: value.crop } : {}),
|
|
70
|
+
...(value.hotspot ? { hotspot: value.hotspot } : {}),
|
|
71
|
+
...(aspect ? { aspect } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function resolveOne(value, d, assets) {
|
|
75
|
+
if (!isMediaValue(value))
|
|
76
|
+
return undefined;
|
|
77
|
+
const asset = assets.get(value.asset);
|
|
78
|
+
if (!asset || asset.status !== "ready")
|
|
79
|
+
return undefined;
|
|
80
|
+
if (d.type === "image")
|
|
81
|
+
return resolvedImage(asset, value, d);
|
|
82
|
+
if (d.type === "video") {
|
|
83
|
+
const v = value;
|
|
84
|
+
const poster = asset.posterId ? assets.get(asset.posterId) : undefined;
|
|
85
|
+
const posterResolved = poster ? resolvedImage(poster, { asset: poster.id }, { type: "image" }) : undefined;
|
|
86
|
+
return {
|
|
87
|
+
url: asset.url, mime: asset.mime,
|
|
88
|
+
...(asset.width !== null ? { width: asset.width } : {}),
|
|
89
|
+
...(asset.height !== null ? { height: asset.height } : {}),
|
|
90
|
+
...(asset.duration !== null ? { duration: asset.duration } : {}),
|
|
91
|
+
...(v.title ? { title: v.title } : {}),
|
|
92
|
+
...(posterResolved ? { poster: posterResolved } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const f = value;
|
|
96
|
+
return { url: asset.url, mime: asset.mime, size: asset.size, filename: asset.filename, ...(f.title ? { title: f.title } : {}) };
|
|
97
|
+
}
|
|
98
|
+
/** Replace every media value in place with its resolved object — or delete
|
|
99
|
+
* it when the asset is missing or pending, so `image?.url` guards hold. */
|
|
100
|
+
export function applyAssets(values, descriptors, assets) {
|
|
101
|
+
if (!values)
|
|
102
|
+
return;
|
|
103
|
+
for (const [key, d] of Object.entries(descriptors)) {
|
|
104
|
+
const value = values[key];
|
|
105
|
+
if (value == null)
|
|
106
|
+
continue;
|
|
107
|
+
if (MEDIA_TYPES.has(d.type)) {
|
|
108
|
+
const resolved = resolveOne(value, d, assets);
|
|
109
|
+
if (resolved === undefined)
|
|
110
|
+
delete values[key];
|
|
111
|
+
else
|
|
112
|
+
values[key] = resolved;
|
|
113
|
+
}
|
|
114
|
+
else if (d.type === "object" && typeof value === "object" && !Array.isArray(value)) {
|
|
115
|
+
applyAssets(value, d.fields, assets);
|
|
116
|
+
}
|
|
117
|
+
else if (d.type === "list" && Array.isArray(value)) {
|
|
118
|
+
const item = d.item;
|
|
119
|
+
if (MEDIA_TYPES.has(item.type)) {
|
|
120
|
+
values[key] = value.map((v) => resolveOne(v, item, assets)).filter((v) => v !== undefined);
|
|
121
|
+
}
|
|
122
|
+
else if (item.type === "object") {
|
|
123
|
+
for (const v of value)
|
|
124
|
+
if (v && typeof v === "object")
|
|
125
|
+
applyAssets(v, item.fields, assets);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
package/dist/refs.d.ts
CHANGED
|
@@ -11,10 +11,12 @@ export declare function collectEntryRefs(fields: Record<string, unknown>, schema
|
|
|
11
11
|
export type SectionLike = {
|
|
12
12
|
name: string;
|
|
13
13
|
fields: Record<string, Descriptor>;
|
|
14
|
+
styles?: Record<string, Descriptor>;
|
|
14
15
|
} | {
|
|
15
16
|
schema: {
|
|
16
17
|
name: string;
|
|
17
18
|
fields: Record<string, Descriptor>;
|
|
19
|
+
styles?: Record<string, Descriptor>;
|
|
18
20
|
};
|
|
19
21
|
};
|
|
20
22
|
export declare function collectTreeRefs(tree: PageTree, sections: SectionLike[]): RefEdge[];
|
package/dist/refs.js
CHANGED
|
@@ -5,12 +5,32 @@
|
|
|
5
5
|
// every save; `refs` answers only the REVERSE question (who depends on
|
|
6
6
|
// this?) — forward resolution stays resolveTree's job.
|
|
7
7
|
import { sharedLink } from "./shared.js";
|
|
8
|
+
import { ASSET_COLLECTION, isMediaValue, MEDIA_TYPES } from "./media.js";
|
|
8
9
|
function targetOf(d) {
|
|
9
10
|
const t = d.target?.();
|
|
10
11
|
if (!t?.name)
|
|
11
12
|
throw new Error("smoodly: ref descriptor has no resolvable target.");
|
|
12
13
|
return t.name;
|
|
13
14
|
}
|
|
15
|
+
/** Media edges walk into containers (spec 2026-09-08 media §8); `field`
|
|
16
|
+
* stays the top-level key so the edge is addressable like a ref. */
|
|
17
|
+
function collectMedia(field, value, d, out) {
|
|
18
|
+
if (value == null)
|
|
19
|
+
return;
|
|
20
|
+
if (MEDIA_TYPES.has(d.type)) {
|
|
21
|
+
if (isMediaValue(value))
|
|
22
|
+
out.push({ field, targetCollection: ASSET_COLLECTION, targetId: value.asset });
|
|
23
|
+
}
|
|
24
|
+
else if (d.type === "object" && typeof value === "object" && !Array.isArray(value)) {
|
|
25
|
+
for (const [k, sub] of Object.entries(d.fields)) {
|
|
26
|
+
collectMedia(field, value[k], sub, out);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else if (d.type === "list" && Array.isArray(value)) {
|
|
30
|
+
for (const v of value)
|
|
31
|
+
collectMedia(field, v, d.item, out);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
14
34
|
function collectFrom(values, descriptors, out) {
|
|
15
35
|
for (const [field, d] of Object.entries(descriptors)) {
|
|
16
36
|
const value = values[field];
|
|
@@ -25,10 +45,9 @@ function collectFrom(values, descriptors, out) {
|
|
|
25
45
|
out.push({ field, targetCollection: targetOf(d), targetId: id });
|
|
26
46
|
}
|
|
27
47
|
}
|
|
28
|
-
else
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
continue;
|
|
48
|
+
else {
|
|
49
|
+
// Refs are top-level only (f.list/f.object refuse them); media is not.
|
|
50
|
+
collectMedia(field, value, d, out);
|
|
32
51
|
}
|
|
33
52
|
}
|
|
34
53
|
}
|
|
@@ -56,6 +75,8 @@ export function collectTreeRefs(tree, sections) {
|
|
|
56
75
|
if (!schema)
|
|
57
76
|
continue; // fail-soft, same as renderPage
|
|
58
77
|
collectFrom(node.fields, schema.fields, out);
|
|
78
|
+
if (schema.styles && node.styles)
|
|
79
|
+
collectFrom(node.styles, schema.styles, out);
|
|
59
80
|
}
|
|
60
81
|
}
|
|
61
82
|
return out;
|
package/dist/resolve.d.ts
CHANGED
|
@@ -2,29 +2,44 @@ import type { Descriptor } from "./fields.ts";
|
|
|
2
2
|
import type { CollectionSchema } from "./collections.ts";
|
|
3
3
|
import type { PageTree } from "./render.tsx";
|
|
4
4
|
import { type SharedSchema } from "./shared.ts";
|
|
5
|
+
import { type AssetRecord } from "./media.ts";
|
|
5
6
|
export type EntryFetcher = (collection: string, ids: string[]) => Promise<Record<string, Record<string, unknown>>>;
|
|
7
|
+
export type AssetFetcher = (ids: string[]) => Promise<Record<string, AssetRecord>>;
|
|
6
8
|
type SchemaLike = {
|
|
7
9
|
name: string;
|
|
8
10
|
fields: Record<string, Descriptor>;
|
|
11
|
+
styles?: Record<string, Descriptor>;
|
|
12
|
+
};
|
|
13
|
+
/** One value set whose media resolves in place: a node's fields or styles,
|
|
14
|
+
* an entry's fields, or an entry hydrated through a ref. */
|
|
15
|
+
export type MediaSet = {
|
|
16
|
+
values: Record<string, unknown> | undefined;
|
|
17
|
+
descriptors: Record<string, Descriptor>;
|
|
9
18
|
};
|
|
10
19
|
type ResolveOptions = {
|
|
11
|
-
sections: ({
|
|
12
|
-
name: string;
|
|
13
|
-
fields: Record<string, Descriptor>;
|
|
14
|
-
} | {
|
|
20
|
+
sections: (SchemaLike | {
|
|
15
21
|
schema: SchemaLike;
|
|
16
22
|
})[];
|
|
17
23
|
collections: CollectionSchema<any>[];
|
|
18
24
|
fetch: EntryFetcher;
|
|
25
|
+
/** Media (spec 2026-09-08 §9): one batched asset fetch per page, values become resolved objects. */
|
|
26
|
+
assets?: AssetFetcher;
|
|
19
27
|
/** The registered shared items a placement node may name (spec 2026-09-07 §4). */
|
|
20
28
|
shared?: SharedSchema[];
|
|
21
29
|
/** Optional safety valve on chain length; unlimited by default. */
|
|
22
30
|
maxDepth?: number;
|
|
23
31
|
};
|
|
24
32
|
export declare function resolveTree(tree: PageTree, options: ResolveOptions): Promise<PageTree>;
|
|
33
|
+
/** One value set's media, resolved in place. Exported for the site layer's shared-item styles. */
|
|
34
|
+
export declare function resolveMedia(values: Record<string, unknown>, descriptors: Record<string, Descriptor>, fetch: AssetFetcher): Promise<void>;
|
|
35
|
+
/** Many value sets, one asset round (plus one for posters): the tree's
|
|
36
|
+
* nodes and every entry hydrated through a ref, or a list page's items.
|
|
37
|
+
* Exported for the site layer's content API. */
|
|
38
|
+
export declare function resolveMediaSets(sets: MediaSet[], fetch: AssetFetcher): Promise<void>;
|
|
25
39
|
export type ResolveFieldsOptions = {
|
|
26
40
|
collections: CollectionSchema<any>[];
|
|
27
41
|
fetch: EntryFetcher;
|
|
42
|
+
assets?: AssetFetcher;
|
|
28
43
|
maxDepth?: number;
|
|
29
44
|
};
|
|
30
45
|
/** One entry's fields, refs hydrated — the entry page's counterpart of
|