smoodly 0.0.13 → 0.0.15

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.
Files changed (58) hide show
  1. package/README.md +87 -15
  2. package/dist/admin/editor/EditorView.js +12 -10
  3. package/dist/admin/editor/PageInspector.d.ts +21 -0
  4. package/dist/admin/editor/PageInspector.js +24 -0
  5. package/dist/admin/editor/PageSettings.d.ts +1 -3
  6. package/dist/admin/editor/PageSettings.js +7 -6
  7. package/dist/admin/editor/SeoSettings.d.ts +7 -0
  8. package/dist/admin/editor/SeoSettings.js +15 -0
  9. package/dist/admin/forms/FieldWidget.d.ts +1 -1
  10. package/dist/admin/forms/FieldWidget.js +24 -4
  11. package/dist/admin/ops-impl.js +72 -11
  12. package/dist/admin/serialize.d.ts +4 -0
  13. package/dist/admin/serialize.js +2 -0
  14. package/dist/admin/tree-ops.d.ts +2 -0
  15. package/dist/admin/tree-ops.js +5 -1
  16. package/dist/admin/ui/primitives.d.ts +9 -0
  17. package/dist/admin/ui/primitives.js +6 -0
  18. package/dist/admin/ui/resize.d.ts +1 -1
  19. package/dist/admin/ui/resize.js +1 -1
  20. package/dist/admin/ui/theme.js +16 -1
  21. package/dist/admin/validate.js +24 -3
  22. package/dist/collections.d.ts +6 -5
  23. package/dist/entry-store.d.ts +18 -1
  24. package/dist/entry-store.js +27 -2
  25. package/dist/field-defaults.d.ts +8 -0
  26. package/dist/field-defaults.js +18 -0
  27. package/dist/fields.d.ts +26 -1
  28. package/dist/fields.js +31 -1
  29. package/dist/index.d.ts +8 -6
  30. package/dist/index.js +5 -3
  31. package/dist/next/index.d.ts +2 -0
  32. package/dist/next/index.js +1 -0
  33. package/dist/next/meta.d.ts +7 -3
  34. package/dist/next/meta.js +18 -9
  35. package/dist/next/page-renderer.js +1 -1
  36. package/dist/next/queries.d.ts +36 -0
  37. package/dist/next/queries.js +87 -0
  38. package/dist/page-tree.d.ts +3 -0
  39. package/dist/page-tree.js +6 -0
  40. package/dist/page.d.ts +11 -3
  41. package/dist/page.js +26 -0
  42. package/dist/pairing.d.ts +2 -1
  43. package/dist/pairing.js +2 -9
  44. package/dist/render.d.ts +14 -7
  45. package/dist/render.js +2 -2
  46. package/dist/resolve.d.ts +4 -0
  47. package/dist/resolve.js +8 -0
  48. package/dist/revalidate.d.ts +6 -0
  49. package/dist/revalidate.js +6 -0
  50. package/dist/shared.d.ts +7 -7
  51. package/dist/site.d.ts +23 -1
  52. package/dist/site.js +83 -19
  53. package/dist/store.d.ts +4 -0
  54. package/dist/store.js +9 -0
  55. package/dist/supabase-entry-store.js +32 -10
  56. package/dist/supabase-store.d.ts +1 -0
  57. package/dist/supabase-store.js +13 -0
  58. package/package.json +1 -1
@@ -0,0 +1,87 @@
1
+ // The in-app read API (spec 2026-09-11): five typed queries over the
2
+ // framework-free site layer, each draft-aware inside the editor preview
3
+ // and cached under a tag outside it — the pattern getSmoodlyShared set.
4
+ // Lives in next/ because draftMode and unstable_cache are Next's; an
5
+ // Astro glue wraps the same site reads over its own cache seam.
6
+ import { unstable_cache } from "next/cache";
7
+ import { draftMode } from "next/headers";
8
+ import { connection } from "next/server";
9
+ import { collectionTag, entryTag, pageTag, pagesTreeTag, sharedTag } from "../revalidate.js";
10
+ /** JSON with object keys sorted at every level and `undefined` dropped, so two spellings of one query share a unit. */
11
+ function stable(value) {
12
+ if (Array.isArray(value))
13
+ return value.map(stable);
14
+ if (value !== null && typeof value === "object") {
15
+ return Object.fromEntries(Object.entries(value)
16
+ .filter(([, v]) => v !== undefined)
17
+ .sort(([a], [b]) => (a < b ? -1 : 1))
18
+ .map(([k, v]) => [k, stable(v)]));
19
+ }
20
+ return value;
21
+ }
22
+ /** A stable key for an entries query: only what was given, normalized. */
23
+ function entriesKey(o) {
24
+ const { locale: _locale, ...rest } = o;
25
+ return JSON.stringify(stable(rest));
26
+ }
27
+ export function createSmoodlyQueries(options) {
28
+ const { site } = options;
29
+ const sharedByName = new Map((options.shared ?? []).map((s) => [s.name, s]));
30
+ const localeOf = (o) => o?.locale ?? site.locales.default;
31
+ /** Every query gates on the request (never a build prerender), then reads live in draft mode or cached under `tags`. */
32
+ async function run(key, tags, read) {
33
+ await connection();
34
+ const { isEnabled: draft } = await draftMode();
35
+ if (draft)
36
+ return read(true);
37
+ return unstable_cache(() => read(false), ["smoodly-q", ...key], { tags })();
38
+ }
39
+ /** The tag needs the id: a path is located first, uncached, as the renderer does. */
40
+ async function idOf(kind, locale, pathOrId) {
41
+ if (!pathOrId.startsWith("/"))
42
+ return pathOrId;
43
+ await connection();
44
+ const target = await site.locate(locale, pathOrId);
45
+ return target?.kind === kind ? target.id : null;
46
+ }
47
+ return {
48
+ getPageTree: (o) => {
49
+ const locale = localeOf(o);
50
+ return run(["pageTree", locale], [pagesTreeTag()], (draft) => site.getPagesTree({ locale, draft }));
51
+ },
52
+ async getPage(pathOrId, o) {
53
+ const locale = localeOf(o);
54
+ const id = await idOf("page", locale, pathOrId);
55
+ if (!id)
56
+ return null;
57
+ return run(["page", locale, pathOrId], [pageTag(id)], (draft) => site.getPage(pathOrId, { locale, draft }));
58
+ },
59
+ getEntries: (collection, o = {}) => {
60
+ const locale = localeOf(o);
61
+ const { locale: _locale, ...query } = o;
62
+ return run(["entries", collection, locale, entriesKey(o)], [collectionTag(collection)], (draft) => site.getEntries(collection, { ...query, locale, draft }));
63
+ },
64
+ async getEntry(collection, idOrPath, o) {
65
+ const locale = localeOf(o);
66
+ const id = await idOf("entry", locale, idOrPath);
67
+ if (!id)
68
+ return null;
69
+ return run(["entry", collection, locale, idOrPath], [entryTag(id)], async (draft) => {
70
+ const hit = await site.getEntry(collection, idOrPath, { locale, draft });
71
+ // The site layer also answers a bare root slug, but the unit would sit under
72
+ // a tag no op ever fires — so a slug is refused here rather than cached stale.
73
+ if (hit && !idOrPath.startsWith("/") && hit.entry.id !== idOrPath) {
74
+ throw new Error(`smoodly: getEntry("${collection}", "${idOrPath}") takes an id or a path — for a slug, pass the entry's path.`);
75
+ }
76
+ return hit;
77
+ });
78
+ },
79
+ async getShared(name, o) {
80
+ const locale = localeOf(o);
81
+ const item = sharedByName.get(name);
82
+ if (!item)
83
+ throw new Error(`smoodly: no shared item named "${name}".`);
84
+ return run(["shared", name, locale], [sharedTag(name)], (draft) => site.getShared(item, { locale, draft }));
85
+ },
86
+ };
87
+ }
@@ -0,0 +1,3 @@
1
+ export declare function filterPageTree<T extends {
2
+ children: T[];
3
+ }>(items: T[], keep: (item: T) => boolean): T[];
@@ -0,0 +1,6 @@
1
+ // The one rule for pruning a navigation (spec 2026-09-11 §4): keep an
2
+ // item when `keep` says so and recurse; a dropped parent drops its
3
+ // subtree, since a nav cannot show orphans. Pure; the input is untouched.
4
+ export function filterPageTree(items, keep) {
5
+ return items.filter(keep).map((item) => ({ ...item, children: filterPageTree(item.children, keep) }));
6
+ }
package/dist/page.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import type { ZonePolicy } from "./zones.ts";
2
- export type PageSchema<Zones extends Record<string, unknown> = Record<string, unknown>> = {
2
+ import { type Descriptor } from "./fields.ts";
3
+ import type { BuilderMap } from "./schema.ts";
4
+ export type PageSchema<Zones extends Record<string, unknown> = Record<string, unknown>, F extends BuilderMap = BuilderMap> = {
3
5
  kind: "page";
4
6
  name: string;
5
7
  title: string;
@@ -12,16 +14,22 @@ export type PageSchema<Zones extends Record<string, unknown> = Record<string, un
12
14
  slug?: string | Record<string, string>;
13
15
  zones: Record<keyof Zones & string, ZonePolicy>;
14
16
  meta: Record<string, unknown>;
17
+ /** The template's own page-level fields (spec 2026-09-10 page fields §1):
18
+ * stored in the tree per locale, typed on the view's `page.fields`. */
19
+ fields: Record<string, Descriptor>;
15
20
  /** phantom: zone names for Props inference */
16
21
  readonly __zones?: Zones;
22
+ /** phantom: field builders for Props inference */
23
+ readonly __fields?: F;
17
24
  };
18
- export declare function pageSchema<Zones extends Record<string, object>>(input: {
25
+ export declare function pageSchema<Zones extends Record<string, object>, F extends BuilderMap = {}>(input: {
19
26
  name: string;
20
27
  title: string;
21
28
  slug?: string | Record<string, string>;
22
29
  zones: Zones;
23
30
  meta?: Record<string, unknown>;
24
- }): PageSchema<Zones>;
31
+ fields?: F;
32
+ }): PageSchema<Zones, F>;
25
33
  /** The default-locale segment of a fixed slug — what the admin lists and
26
34
  * what today's root-level lookups use. */
27
35
  export declare function fixedSlug(schema: {
package/dist/page.js CHANGED
@@ -1,3 +1,25 @@
1
+ import { describePath, findMatch } from "./fields.js";
2
+ /** What a page field may be: a settings form, never a ref, a list or a document (spec 2026-09-10 page fields §1). */
3
+ const PAGE_FIELD_TYPES = new Set(["text", "link", "date", "number", "boolean", "select", "radio", "checkboxes", "image", "video", "file", "object"]);
4
+ /** The descriptor at a dotted path below `d` (object subfields only — page fields hold no lists). */
5
+ function descriptorAt(d, path) {
6
+ let cur = d;
7
+ for (const seg of path.split("."))
8
+ cur = cur.fields[seg];
9
+ return cur;
10
+ }
11
+ function assertPageField(page, key, d) {
12
+ const bad = findMatch(d, (sub) => !PAGE_FIELD_TYPES.has(sub.type), key);
13
+ if (bad !== null) {
14
+ const at = bad === key ? d : descriptorAt(d, bad.slice(key.length + 1));
15
+ const kind = at.type === "ref" || at.type === "refList" ? "refs" : at.type;
16
+ throw new Error(`smoodly: page "${page}": field ${describePath(bad)} — ${kind} cannot live in page fields.`);
17
+ }
18
+ const loc = findMatch(d, (sub) => sub.localized === true, key);
19
+ if (loc !== null) {
20
+ throw new Error(`smoodly: page "${page}": field ${describePath(loc)} uses .localized(), but page fields are per locale already.`);
21
+ }
22
+ }
1
23
  /** Policies arrive as z.* results (possibly carrying builder methods) — store plain data only. */
2
24
  function normalizePolicy(p) {
3
25
  switch (p.kind) {
@@ -31,6 +53,9 @@ export function pageSchema(input) {
31
53
  throw new Error(`smoodly: page "${input.name}" declares an invalid slug — a fixed slug must be a non-empty string or a per-locale object of them.`);
32
54
  }
33
55
  }
56
+ const fields = Object.fromEntries(Object.entries(input.fields ?? {}).map(([k, b]) => [k, b.descriptor]));
57
+ for (const [key, d] of Object.entries(fields))
58
+ assertPageField(input.name, key, d);
34
59
  return {
35
60
  kind: "page",
36
61
  name: input.name,
@@ -38,6 +63,7 @@ export function pageSchema(input) {
38
63
  ...(input.slug !== undefined ? { slug: input.slug } : {}),
39
64
  zones: Object.fromEntries(Object.entries(input.zones).map(([k, p]) => [k, normalizePolicy(p)])),
40
65
  meta: input.meta ?? {},
66
+ fields,
41
67
  };
42
68
  }
43
69
  /** The default-locale segment of a fixed slug — what the admin lists and
package/dist/pairing.d.ts CHANGED
@@ -9,9 +9,10 @@ import type { PageContext, ZoneContent } from "./render.tsx";
9
9
  export type Props<S> = S extends {
10
10
  kind: "page";
11
11
  __zones?: infer Z extends Record<string, unknown>;
12
+ __fields?: infer F extends BuilderMap;
12
13
  } ? {
13
14
  zones: Record<keyof Z & string, ZoneContent>;
14
- page: PageContext;
15
+ page: PageContext<NamespaceValues<F>>;
15
16
  } : S extends {
16
17
  __f?: infer F extends BuilderMap;
17
18
  __s?: infer St extends BuilderMap;
package/dist/pairing.js CHANGED
@@ -1,14 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- function styleDefaultsOf(styles) {
3
- const out = {};
4
- for (const [key, d] of Object.entries(styles)) {
5
- if (d.default !== undefined)
6
- out[key] = d.default;
7
- }
8
- return out;
9
- }
2
+ import { defaultsOf } from "./field-defaults.js";
10
3
  function pair(schema, View) {
11
- const defaults = styleDefaultsOf(schema.styles);
4
+ const defaults = defaultsOf(schema.styles);
12
5
  const Component = (props) => {
13
6
  const { styles: styleOverrides, ...fields } = props ?? {};
14
7
  const styles = { ...defaults, ...styleOverrides };
package/dist/render.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { ComponentType, ReactElement } from "react";
2
2
  import type { PageSchema } from "./page.ts";
3
+ import type { BuilderMap, NamespaceValues } from "./schema.ts";
3
4
  export type TreeNode = {
4
5
  id: string;
5
6
  type: string;
@@ -15,26 +16,32 @@ export type TreeNode = {
15
16
  export type PageTree = {
16
17
  zones: Record<string, TreeNode[]>;
17
18
  meta?: Record<string, unknown>;
19
+ fields?: Record<string, unknown>;
18
20
  };
19
21
  /** Where a rendered page sits in the tree — what a view needs for
20
22
  * breadcrumbs and sub-navigation without a query (spec 2026-09-05 §5).
21
23
  * `path` is the locale-relative CMS path (what the content API returns);
22
24
  * `url` is that path through the app's `href` seam — `/fi/yritys` under
23
25
  * next-intl, `/yritys` without it — so a page shape links the same way
24
- * in both layouts. */
26
+ * in both layouts. `fields` is the linked page's own page fields (spec
27
+ * 2026-09-10 page fields §3): the published tree's outside draft mode,
28
+ * raw (no asset resolution), defaults applied, `{}` for a folder. */
25
29
  export type PageLink = {
26
30
  title: string;
27
31
  path: string;
28
32
  hasPage: boolean;
33
+ fields: Record<string, unknown>;
29
34
  };
30
35
  export type PageContextLink = PageLink & {
31
36
  url: string;
32
37
  };
33
- export type PageContext = {
38
+ export type PageContext<Fields = Record<string, unknown>> = {
34
39
  id: string;
35
40
  title: string;
36
41
  path: string;
37
42
  url: string;
43
+ /** The page's own fields, typed from its schema on the view; resolved, defaults applied. */
44
+ fields: Fields;
38
45
  /** Root first, parent last. */
39
46
  ancestors: ReadonlyArray<PageContextLink>;
40
47
  children: ReadonlyArray<PageContextLink>;
@@ -58,14 +65,14 @@ export type ZoneContent = {
58
65
  export declare function Zone({ of: content }: {
59
66
  of: ZoneContent;
60
67
  }): import("react").JSX.Element | null;
61
- export type PageViewProps<Zones extends Record<string, unknown>> = {
68
+ export type PageViewProps<Zones extends Record<string, unknown>, Fields = Record<string, unknown>> = {
62
69
  zones: Record<keyof Zones & string, ZoneContent>;
63
- page: PageContext;
70
+ page: PageContext<Fields>;
64
71
  };
65
- export type PairedPage<Zones extends Record<string, unknown>> = ((props: PageViewProps<Zones>) => ReactElement) & {
66
- schema: PageSchema<Zones>;
72
+ export type PairedPage<Zones extends Record<string, unknown>, F extends BuilderMap = BuilderMap> = ((props: PageViewProps<Zones, NamespaceValues<F>>) => ReactElement) & {
73
+ schema: PageSchema<Zones, F>;
67
74
  };
68
- export declare function page<Zones extends Record<string, unknown>>(schema: PageSchema<Zones>, View: ComponentType<PageViewProps<Zones>>): PairedPage<Zones>;
75
+ export declare function page<Zones extends Record<string, unknown>, F extends BuilderMap = {}>(schema: PageSchema<Zones, F>, View: ComponentType<PageViewProps<Zones, NamespaceValues<F>>>): PairedPage<Zones, F>;
69
76
  export type Registry = {
70
77
  sections?: AnyPaired[];
71
78
  elements?: AnyPaired[];
package/dist/render.js CHANGED
@@ -5,7 +5,7 @@ import { EMPTY_ATTR, ZONE_ATTR } from "./marks.js";
5
5
  * no position in the tree. Frozen: one singleton shared by every such
6
6
  * render, so a view that pushed onto it would leak into the next. */
7
7
  export const EMPTY_PAGE_CONTEXT = Object.freeze({
8
- id: "", title: "", path: "/", url: "/", ancestors: Object.freeze([]), children: Object.freeze([]),
8
+ id: "", title: "", path: "/", url: "/", fields: Object.freeze({}), ancestors: Object.freeze([]), children: Object.freeze([]),
9
9
  });
10
10
  export function Zone({ of: content }) {
11
11
  // A malformed tree (bad data, a client that skipped isPageTree) must not
@@ -45,5 +45,5 @@ export function renderPage(Page, tree, registry, options) {
45
45
  zone,
46
46
  { name: zone, nodes: tree.zones?.[zone] ?? [], components, editor: options?.editor ?? false },
47
47
  ]));
48
- return _jsx(Page, { zones: zones, page: options?.page ?? EMPTY_PAGE_CONTEXT });
48
+ return _jsx(Page, { zones: zones, page: (options?.page ?? EMPTY_PAGE_CONTEXT) });
49
49
  }
package/dist/resolve.d.ts CHANGED
@@ -32,6 +32,10 @@ type ResolveOptions = {
32
32
  elements?: SectionLike[];
33
33
  /** Optional safety valve on chain length; unlimited by default. */
34
34
  maxDepth?: number;
35
+ /** The page's own field descriptors (spec 2026-09-10 page fields §3): `tree.fields` resolves like a node's fields, and `tree.meta.ogImage` as an image. */
36
+ page?: {
37
+ fields: Record<string, Descriptor>;
38
+ };
35
39
  };
36
40
  export declare function resolveTree(tree: PageTree, options: ResolveOptions): Promise<PageTree>;
37
41
  /** One value set's media, resolved in place. Exported for the site layer's shared-item styles. */
package/dist/resolve.js CHANGED
@@ -9,6 +9,8 @@
9
9
  import { sharedLink, SHARED_NODE_TYPE, STYLES_KEY } from "./shared.js";
10
10
  import { applyAssets, collectAssetIds } from "./media.js";
11
11
  import { elementSchemas } from "./richtext-walk.js";
12
+ /** The built-in SEO set's one media value (spec 2026-09-10 page fields §2). */
13
+ const META_DESCRIPTORS = { ogImage: { type: "image" } };
12
14
  function targetOf(d) {
13
15
  const t = d.target?.();
14
16
  if (!t?.name)
@@ -40,6 +42,8 @@ export async function resolveTree(tree, options) {
40
42
  collectTasks(node.fields, schema.fields, tasks);
41
43
  }
42
44
  }
45
+ if (options.page && resolved.fields)
46
+ collectTasks(resolved.fields, options.page.fields, tasks);
43
47
  const hydrated = await runResolution(tasks, { collections: options.collections, fetch: options.fetch, maxDepth });
44
48
  if (options.assets) {
45
49
  const sets = [];
@@ -50,6 +54,10 @@ export async function resolveTree(tree, options) {
50
54
  sets.push({ values: node.fields, descriptors: schema.fields });
51
55
  sets.push({ values: node.styles, descriptors: schema.styles ?? {} });
52
56
  }
57
+ if (options.page && resolved.fields)
58
+ sets.push({ values: resolved.fields, descriptors: options.page.fields });
59
+ if (resolved.meta)
60
+ sets.push({ values: resolved.meta, descriptors: META_DESCRIPTORS });
53
61
  await resolveMediaSets([...sets, ...hydrated], options.assets, elementSchemas(options.elements));
54
62
  }
55
63
  return resolved;
@@ -12,6 +12,12 @@ export declare const entryTag: (id: string) => string;
12
12
  * locale, and code owns the name): the Next glue's getSmoodlyShared
13
13
  * caches under it; ops.shared expires it. */
14
14
  export declare const sharedTag: (name: string) => string;
15
+ /** The page-tree listing's cache unit (spec 2026-09-11 §2): one tree-wide
16
+ * tag, fired by every page op that changes what the listing shows. */
17
+ export declare const pagesTreeTag: () => string;
18
+ /** A collection's list units: fired by every entry write in the
19
+ * collection, since a non-versioned collection's published read is its live row. */
20
+ export declare const collectionTag: (name: string) => string;
15
21
  export declare function affectedTargets(stores: {
16
22
  pages: PageStore;
17
23
  entries: EntryStore;
@@ -11,6 +11,12 @@ export const entryTag = (id) => `entry:${id}`;
11
11
  * locale, and code owns the name): the Next glue's getSmoodlyShared
12
12
  * caches under it; ops.shared expires it. */
13
13
  export const sharedTag = (name) => `shared:${name}`;
14
+ /** The page-tree listing's cache unit (spec 2026-09-11 §2): one tree-wide
15
+ * tag, fired by every page op that changes what the listing shows. */
16
+ export const pagesTreeTag = () => "pages:tree";
17
+ /** A collection's list units: fired by every entry write in the
18
+ * collection, since a non-versioned collection's published read is its live row. */
19
+ export const collectionTag = (name) => `entries:${name}`;
14
20
  export async function affectedTargets(stores, entryId, options = {}) {
15
21
  const maxDepth = options.maxDepth ?? Infinity;
16
22
  const seen = new Set([entryId]);
package/dist/shared.d.ts CHANGED
@@ -6,7 +6,7 @@ import type { BuilderMap, NamespaceValues, SectionSchema } from "./schema.ts";
6
6
  export declare const SHARED_NODE_TYPE = "shared";
7
7
  /** The reserved key a visual item's styles live under in the node's shared JSONB. */
8
8
  export declare const STYLES_KEY = "$styles";
9
- export type SharedSchema<V = Record<string, unknown>> = CollectionSchema<any> & {
9
+ export type SharedSchema<V = Record<string, unknown>, N extends string = string> = CollectionSchema<any, N> & {
10
10
  kind: "shared";
11
11
  /** The registered section a visual item renders through; absent = an object item. */
12
12
  section?: string;
@@ -28,19 +28,19 @@ export declare function sharedLink(node: {
28
28
  type: string;
29
29
  fields?: Record<string, unknown>;
30
30
  }): SharedLink | null;
31
- type SharedInput = {
32
- name: string;
31
+ type SharedInput<N extends string = string> = {
32
+ name: N;
33
33
  title: string;
34
34
  versions?: boolean;
35
35
  };
36
- export declare function shared<F extends BuilderMap>(input: SharedInput & {
36
+ export declare function shared<F extends BuilderMap, const N extends string = string>(input: SharedInput<N> & {
37
37
  fields: F;
38
38
  section?: undefined;
39
- }): SharedSchema<NamespaceValues<F>>;
40
- export declare function shared<F extends BuilderMap, St extends BuilderMap>(input: SharedInput & {
39
+ }): SharedSchema<NamespaceValues<F>, N>;
40
+ export declare function shared<F extends BuilderMap, St extends BuilderMap, const N extends string = string>(input: SharedInput<N> & {
41
41
  section: PairedComponent<SectionSchema<F, St>, F, St>;
42
42
  fields?: undefined;
43
43
  }): SharedSchema<NamespaceValues<F> & {
44
44
  styles: NamespaceValues<St>;
45
- }>;
45
+ }, N>;
46
46
  export {};
package/dist/site.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { AssetStore } from "./asset-store.ts";
2
2
  import type { CollectionSchema, EntryOrder } from "./collections.ts";
3
- import { type EntryRecord, type EntryStore } from "./entry-store.ts";
3
+ import type { Descriptor } from "./fields.ts";
4
+ import { type EntryRecord, type EntryStore, type Where } from "./entry-store.ts";
4
5
  import { type LocaleSet, type PathTarget } from "./paths.ts";
5
6
  import type { SectionLike } from "./refs.ts";
6
7
  import type { PageContext, PageLink, PageTree } from "./render.tsx";
@@ -19,6 +20,16 @@ export type SiteOptions = {
19
20
  shared?: SharedSchema[];
20
21
  locales: LocaleSet;
21
22
  homePageSlug?: string;
23
+ /** Page shapes (entry pages are ignored): the source of each template's page-field descriptors (spec 2026-09-10 page fields §3). */
24
+ pages?: ReadonlyArray<SitePageSchema>;
25
+ };
26
+ /** A page registration as the site needs it: the template's page fields. */
27
+ export type SitePageSchema = {
28
+ schema: {
29
+ kind: string;
30
+ name: string;
31
+ fields?: Record<string, Descriptor>;
32
+ };
22
33
  };
23
34
  export type SitePage = {
24
35
  kind: "page";
@@ -27,6 +38,8 @@ export type SitePage = {
27
38
  tree: PageTree;
28
39
  title: string;
29
40
  path: string;
41
+ /** The page's own fields (spec 2026-09-10 page fields §3): resolved, defaults applied. */
42
+ fields: Record<string, unknown>;
30
43
  ancestors: PageLink[];
31
44
  children: PageLink[];
32
45
  /** locale → path, for metadata alternates: the locales the page is
@@ -50,6 +63,7 @@ export type PageTreeItem = {
50
63
  title: string;
51
64
  path: string;
52
65
  hasPage: boolean;
66
+ fields: Record<string, unknown>;
53
67
  children: PageTreeItem[];
54
68
  };
55
69
  export type SiteEntryItem = {
@@ -59,10 +73,14 @@ export type SiteEntryItem = {
59
73
  export type EntryTreeItem = SiteEntryItem & {
60
74
  children: EntryTreeItem[];
61
75
  };
76
+ /** `where`, `limit`, `parent` and `order` run in the store (spec 2026-09-11 §3); `draft` reads live rows inside the editor preview. */
62
77
  export type EntryQuery = {
63
78
  locale?: string;
64
79
  parent?: string | null;
65
80
  order?: EntryOrder;
81
+ where?: Where;
82
+ limit?: number;
83
+ draft?: boolean;
66
84
  };
67
85
  export type LoadOptions = {
68
86
  draft?: boolean;
@@ -77,16 +95,20 @@ export interface SmoodlySite {
77
95
  resolve(locale: string, path: string, options?: LoadOptions): Promise<SiteHit | null>;
78
96
  getPagesTree(options?: {
79
97
  locale?: string;
98
+ draft?: boolean;
80
99
  }): Promise<PageTreeItem[]>;
81
100
  getPage(pathOrId: string, options?: {
82
101
  locale?: string;
102
+ draft?: boolean;
83
103
  }): Promise<SitePage | null>;
84
104
  getEntries(collection: string, options?: EntryQuery): Promise<SiteEntryItem[]>;
85
105
  getEntry(collection: string, idOrPath: string, options?: {
86
106
  locale?: string;
107
+ draft?: boolean;
87
108
  }): Promise<SiteEntryItem | null>;
88
109
  getEntriesTree(collection: string, options?: {
89
110
  locale?: string;
111
+ draft?: boolean;
90
112
  }): Promise<EntryTreeItem[]>;
91
113
  /** A shared item's content in a locale (spec 2026-09-07 §4): an object
92
114
  * item's merged fields, or a visual item's section props (fields plus