smoodly 0.0.12 → 0.0.14

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 (159) hide show
  1. package/README.md +294 -40
  2. package/dist/admin/editor/CanvasOverlay.d.ts +6 -2
  3. package/dist/admin/editor/CanvasOverlay.js +9 -11
  4. package/dist/admin/editor/EditorView.js +135 -28
  5. package/dist/admin/editor/FloatingToolbar.d.ts +16 -0
  6. package/dist/admin/editor/FloatingToolbar.js +14 -0
  7. package/dist/admin/editor/InlineRichtext.d.ts +9 -0
  8. package/dist/admin/editor/InlineRichtext.js +50 -0
  9. package/dist/admin/editor/Outline.d.ts +16 -10
  10. package/dist/admin/editor/Outline.js +13 -10
  11. package/dist/admin/editor/PageInspector.d.ts +21 -0
  12. package/dist/admin/editor/PageInspector.js +24 -0
  13. package/dist/admin/editor/PageSettings.d.ts +1 -3
  14. package/dist/admin/editor/PageSettings.js +7 -6
  15. package/dist/admin/editor/SeoSettings.d.ts +7 -0
  16. package/dist/admin/editor/SeoSettings.js +15 -0
  17. package/dist/admin/editor/inline-dom.d.ts +27 -0
  18. package/dist/admin/editor/inline-dom.js +43 -0
  19. package/dist/admin/editor/inline-session.d.ts +30 -0
  20. package/dist/admin/editor/inline-session.js +19 -0
  21. package/dist/admin/editor/protocol.d.ts +22 -1
  22. package/dist/admin/editor/useCanvasBridge.d.ts +8 -4
  23. package/dist/admin/editor/useCanvasBridge.js +12 -7
  24. package/dist/admin/editor/useInlineSession.d.ts +46 -0
  25. package/dist/admin/editor/useInlineSession.js +244 -0
  26. package/dist/admin/field-paths.d.ts +13 -0
  27. package/dist/admin/field-paths.js +49 -0
  28. package/dist/admin/forms/FieldWidget.d.ts +7 -2
  29. package/dist/admin/forms/FieldWidget.js +35 -43
  30. package/dist/admin/forms/ListField.d.ts +10 -0
  31. package/dist/admin/forms/ListField.js +42 -0
  32. package/dist/admin/forms/MediaWidget.js +10 -3
  33. package/dist/admin/forms/RichtextElementCard.d.ts +3 -0
  34. package/dist/admin/forms/RichtextElementCard.js +37 -0
  35. package/dist/admin/forms/RichtextImageCard.d.ts +3 -0
  36. package/dist/admin/forms/RichtextImageCard.js +42 -0
  37. package/dist/admin/forms/RichtextInsertMenu.d.ts +6 -0
  38. package/dist/admin/forms/RichtextInsertMenu.js +6 -0
  39. package/dist/admin/forms/RichtextPopovers.d.ts +7 -0
  40. package/dist/admin/forms/RichtextPopovers.js +25 -0
  41. package/dist/admin/forms/RichtextToolbar.d.ts +4 -2
  42. package/dist/admin/forms/RichtextToolbar.js +4 -2
  43. package/dist/admin/forms/RichtextWidget.d.ts +4 -1
  44. package/dist/admin/forms/RichtextWidget.js +23 -120
  45. package/dist/admin/forms/elements-context.d.ts +7 -0
  46. package/dist/admin/forms/elements-context.js +13 -0
  47. package/dist/admin/forms/reveal.d.ts +5 -0
  48. package/dist/admin/forms/reveal.js +47 -0
  49. package/dist/admin/forms/richtext-extensions.d.ts +8 -0
  50. package/dist/admin/forms/richtext-extensions.js +11 -0
  51. package/dist/admin/forms/richtext-form-context.d.ts +16 -0
  52. package/dist/admin/forms/richtext-form-context.js +18 -0
  53. package/dist/admin/forms/richtext-insert.d.ts +38 -0
  54. package/dist/admin/forms/richtext-insert.js +37 -0
  55. package/dist/admin/forms/richtext-nodes.d.ts +37 -0
  56. package/dist/admin/forms/richtext-nodes.js +85 -0
  57. package/dist/admin/forms/richtext-paste.js +5 -0
  58. package/dist/admin/forms/tabs.d.ts +3 -0
  59. package/dist/admin/forms/tabs.js +6 -0
  60. package/dist/admin/forms/useRichtextEditor.d.ts +51 -0
  61. package/dist/admin/forms/useRichtextEditor.js +281 -0
  62. package/dist/admin/next/bridge.js +51 -20
  63. package/dist/admin/ops-impl.js +85 -19
  64. package/dist/admin/richtext-doc.d.ts +3 -0
  65. package/dist/admin/richtext-doc.js +12 -0
  66. package/dist/admin/serialize.d.ts +11 -0
  67. package/dist/admin/serialize.js +18 -1
  68. package/dist/admin/shell/AdminApp.js +2 -1
  69. package/dist/admin/shell/EntriesList.js +34 -12
  70. package/dist/admin/shell/EntryForm.js +22 -15
  71. package/dist/admin/shell/IconRail.js +6 -5
  72. package/dist/admin/shell/ListView.d.ts +9 -2
  73. package/dist/admin/shell/ListView.js +7 -6
  74. package/dist/admin/shell/LoginView.js +2 -2
  75. package/dist/admin/shell/PagesList.js +53 -24
  76. package/dist/admin/shell/SharedForm.js +17 -11
  77. package/dist/admin/shell/SharedList.js +5 -3
  78. package/dist/admin/shell/entries-list.d.ts +4 -0
  79. package/dist/admin/shell/entries-list.js +6 -0
  80. package/dist/admin/shell/pages-tree.d.ts +24 -0
  81. package/dist/admin/shell/pages-tree.js +59 -1
  82. package/dist/admin/tree-ops.d.ts +11 -1
  83. package/dist/admin/tree-ops.js +32 -1
  84. package/dist/admin/ui/LocaleSwitcher.d.ts +10 -0
  85. package/dist/admin/ui/LocaleSwitcher.js +28 -8
  86. package/dist/admin/ui/Modal.js +14 -2
  87. package/dist/admin/ui/ResizeHandle.d.ts +1 -0
  88. package/dist/admin/ui/ResizeHandle.js +127 -0
  89. package/dist/admin/ui/primitives.d.ts +38 -4
  90. package/dist/admin/ui/primitives.js +35 -10
  91. package/dist/admin/ui/resize.d.ts +16 -0
  92. package/dist/admin/ui/resize.js +23 -0
  93. package/dist/admin/ui/sortable.d.ts +23 -0
  94. package/dist/admin/ui/sortable.js +57 -0
  95. package/dist/admin/ui/theme.d.ts +3 -1
  96. package/dist/admin/ui/theme.js +527 -165
  97. package/dist/admin/ui/useSortable.d.ts +35 -0
  98. package/dist/admin/ui/useSortable.js +220 -0
  99. package/dist/admin/validate.d.ts +6 -1
  100. package/dist/admin/validate.js +52 -12
  101. package/dist/collections.d.ts +6 -5
  102. package/dist/config.d.ts +1 -0
  103. package/dist/config.js +36 -0
  104. package/dist/entry-store.d.ts +23 -3
  105. package/dist/entry-store.js +31 -5
  106. package/dist/field-defaults.d.ts +8 -0
  107. package/dist/field-defaults.js +18 -0
  108. package/dist/fields.d.ts +30 -1
  109. package/dist/fields.js +33 -3
  110. package/dist/image/SmoodlyImage.d.ts +4 -1
  111. package/dist/image/SmoodlyImage.js +2 -2
  112. package/dist/index.d.ts +16 -7
  113. package/dist/index.js +10 -4
  114. package/dist/marks.d.ts +53 -0
  115. package/dist/marks.js +43 -0
  116. package/dist/media.d.ts +3 -2
  117. package/dist/media.js +48 -6
  118. package/dist/next/index.d.ts +2 -0
  119. package/dist/next/index.js +1 -0
  120. package/dist/next/meta.d.ts +7 -3
  121. package/dist/next/meta.js +18 -9
  122. package/dist/next/page-renderer.js +1 -1
  123. package/dist/next/queries.d.ts +36 -0
  124. package/dist/next/queries.js +87 -0
  125. package/dist/page-tree.d.ts +3 -0
  126. package/dist/page-tree.js +6 -0
  127. package/dist/page.d.ts +11 -3
  128. package/dist/page.js +26 -0
  129. package/dist/pairing.d.ts +2 -1
  130. package/dist/pairing.js +2 -9
  131. package/dist/refs.d.ts +2 -2
  132. package/dist/refs.js +33 -12
  133. package/dist/render.d.ts +14 -7
  134. package/dist/render.js +4 -3
  135. package/dist/resolve.d.ts +12 -2
  136. package/dist/resolve.js +16 -7
  137. package/dist/revalidate.d.ts +6 -0
  138. package/dist/revalidate.js +6 -0
  139. package/dist/richtext/index.d.ts +12 -0
  140. package/dist/richtext/index.js +9 -0
  141. package/dist/richtext-render.d.ts +22 -2
  142. package/dist/richtext-render.js +40 -13
  143. package/dist/richtext-walk.d.ts +44 -0
  144. package/dist/richtext-walk.js +46 -0
  145. package/dist/schema.d.ts +1 -1
  146. package/dist/schema.js +13 -1
  147. package/dist/section-wrapper.js +2 -1
  148. package/dist/shared.d.ts +7 -7
  149. package/dist/site.d.ts +25 -1
  150. package/dist/site.js +89 -22
  151. package/dist/store.d.ts +7 -1
  152. package/dist/store.js +13 -4
  153. package/dist/supabase-entry-store.d.ts +1 -0
  154. package/dist/supabase-entry-store.js +34 -11
  155. package/dist/supabase-store.d.ts +1 -0
  156. package/dist/supabase-store.js +14 -1
  157. package/dist/toolset.d.ts +7 -1
  158. package/dist/toolset.js +13 -4
  159. package/package.json +6 -1
package/dist/fields.d.ts CHANGED
@@ -4,6 +4,25 @@ export type Descriptor = {
4
4
  type: string;
5
5
  [key: string]: unknown;
6
6
  };
7
+ /** A choice: what is stored (`value`) and what the editor reads (`label`). */
8
+ export type Option<V extends string = string> = {
9
+ value: V;
10
+ label: string;
11
+ };
12
+ /** What a schema may write: an option object, or a bare string as shorthand for value = label. */
13
+ export type OptionInput<V extends string = string> = V | Option<V>;
14
+ /** The stored value of one option input. */
15
+ export type OptionValue<T> = T extends string ? T : T extends {
16
+ value: infer V extends string;
17
+ } ? V : never;
18
+ /** Normalizes a schema's option list to objects; refuses an empty list,
19
+ * since a choice with nothing to choose is a schema bug (spec 2026-09-10
20
+ * page fields §1). */
21
+ export declare function normalizeOptions<V extends string>(options: readonly OptionInput<V>[]): Option<V>[];
22
+ /** The options of a descriptor in either shape (objects since spec 2026-09-10; bare strings in older data). */
23
+ export declare function optionsOf(options: unknown): Option[];
24
+ /** The values a stored choice may take. */
25
+ export declare const optionValues: (options: unknown) => string[];
7
26
  export type FieldBuilder<Value = unknown, Flags extends BuilderFlags = {}> = {
8
27
  readonly descriptor: Descriptor;
9
28
  optional(): FieldBuilder<Value, Flags & {
@@ -42,6 +61,10 @@ type NotLocalized<B> = B extends {
42
61
  } ? Fl extends {
43
62
  localized: true;
44
63
  } ? never : B : B;
64
+ export declare const describePath: (path: string) => string;
65
+ /** First path at which `matches` is true of the descriptor, walking into
66
+ * object subfields and a list's item, or null. */
67
+ export declare function findMatch(d: Descriptor, matches: (d: Descriptor) => boolean, path?: string): string | null;
45
68
  export declare const f: {
46
69
  text: () => FieldBuilder<string, {}>;
47
70
  link: () => FieldBuilder<string, {}>;
@@ -61,7 +84,13 @@ export declare const f: {
61
84
  toolset?: Toolset;
62
85
  }) => FieldBuilder<unknown, {}>;
63
86
  dataAttributes: () => FieldBuilder<Record<string, string>, {}>;
64
- select: <const O extends readonly string[]>(options: O) => FieldBuilder<O[number], {}>;
87
+ boolean: () => FieldBuilder<boolean, {}>;
88
+ /** One value from a list, shown as a dropdown. */
89
+ select: <const O extends readonly OptionInput[]>(options: O) => FieldBuilder<OptionValue<O[number]>, {}>;
90
+ /** One value from a list, shown as a radio group. */
91
+ radio: <const O extends readonly OptionInput[]>(options: O) => FieldBuilder<OptionValue<O[number]>, {}>;
92
+ /** Several values from a list, stored as an array, shown as a checkbox list. */
93
+ checkboxes: <const O extends readonly OptionInput[]>(options: O) => FieldBuilder<OptionValue<O[number]>[], {}>;
65
94
  object: <S extends Record<string, FieldBuilder<any, any>>>(shape: S & { [K in keyof S]: NotLocalized<S[K]>; }) => FieldBuilder<{ [K in keyof S]: ValueOf<S[K]>; }, {}>;
66
95
  /** A repeating item: any builder, but no list at any depth inside the item
67
96
  * — a list in an object in a list is still a nested list on screen, and a
package/dist/fields.js CHANGED
@@ -3,6 +3,30 @@
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
+ /** Normalizes a schema's option list to objects; refuses an empty list,
7
+ * since a choice with nothing to choose is a schema bug (spec 2026-09-10
8
+ * page fields §1). */
9
+ export function normalizeOptions(options) {
10
+ if (!Array.isArray(options) || options.length === 0)
11
+ throw new Error("smoodly: a choice field needs at least one option.");
12
+ return options.map((o) => (typeof o === "string" ? { value: o, label: o } : { value: o.value, label: o.label }));
13
+ }
14
+ /** The options of a descriptor in either shape (objects since spec 2026-09-10; bare strings in older data). */
15
+ export function optionsOf(options) {
16
+ if (!Array.isArray(options))
17
+ return [];
18
+ return options.flatMap((o) => {
19
+ if (typeof o === "string")
20
+ return [{ value: o, label: o }];
21
+ if (o && typeof o === "object" && typeof o.value === "string") {
22
+ const opt = o;
23
+ return [{ value: opt.value, label: typeof opt.label === "string" ? opt.label : opt.value }];
24
+ }
25
+ return [];
26
+ });
27
+ }
28
+ /** The values a stored choice may take. */
29
+ export const optionValues = (options) => optionsOf(options).map((o) => o.value);
6
30
  function builder(descriptor) {
7
31
  const chain = (patch) => builder({ ...descriptor, ...patch });
8
32
  return {
@@ -22,10 +46,10 @@ function builder(descriptor) {
22
46
  tab: (tab) => chain({ tab }),
23
47
  };
24
48
  }
25
- const describePath = (path) => (path === "" ? "the item" : `"${path}"`);
49
+ export const describePath = (path) => (path === "" ? "the item" : `"${path}"`);
26
50
  /** First path at which `matches` is true of the descriptor, walking into
27
51
  * object subfields and a list's item, or null. */
28
- function findMatch(d, matches, path = "") {
52
+ export function findMatch(d, matches, path = "") {
29
53
  if (matches(d))
30
54
  return path;
31
55
  if (d.type === "object") {
@@ -74,7 +98,13 @@ export const f = {
74
98
  file: () => builder({ type: "file" }),
75
99
  richtext: (opts) => builder({ type: "richtext", toolset: opts?.toolset ?? DEFAULT_TOOLSET }),
76
100
  dataAttributes: () => builder({ type: "dataAttributes" }),
77
- select: (options) => builder({ type: "select", options: [...options] }),
101
+ boolean: () => builder({ type: "boolean" }),
102
+ /** One value from a list, shown as a dropdown. */
103
+ select: (options) => builder({ type: "select", options: normalizeOptions(options) }),
104
+ /** One value from a list, shown as a radio group. */
105
+ radio: (options) => builder({ type: "radio", options: normalizeOptions(options) }),
106
+ /** Several values from a list, stored as an array, shown as a checkbox list. */
107
+ checkboxes: (options) => builder({ type: "checkboxes", options: normalizeOptions(options) }),
78
108
  object: (shape) => {
79
109
  for (const [k, v] of Object.entries(shape))
80
110
  assertContainerItem("f.object", v.descriptor, k);
@@ -14,5 +14,8 @@ export type SmoodlyImageProps = {
14
14
  alt?: string;
15
15
  loader?: ImageLoader;
16
16
  unoptimized?: boolean;
17
+ } & {
18
+ /** Data attributes land on the wrapper box — a section's field mark (spec 2026-09-10 §1). */
19
+ [attr: `data-${string}`]: string | undefined;
17
20
  };
18
- export declare function SmoodlyImage({ image, aspect, sizes, priority, loading, className, style, alt, loader, unoptimized }: SmoodlyImageProps): ReactElement;
21
+ export declare function SmoodlyImage({ image, aspect, sizes, priority, loading, className, style, alt, loader, unoptimized, ...data }: SmoodlyImageProps): ReactElement;
@@ -5,11 +5,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
5
5
  // site imports from the media layer. Server-safe — no "use client".
6
6
  import Image from "next/image";
7
7
  import { boxAspectOf, frameFor } from "./crop-math.js";
8
- export function SmoodlyImage({ image, aspect, sizes, priority, loading, className, style, alt, loader, unoptimized }) {
8
+ export function SmoodlyImage({ image, aspect, sizes, priority, loading, className, style, alt, loader, unoptimized, ...data }) {
9
9
  const box = boxAspectOf(image, aspect);
10
10
  const frame = loader?.handlesCrop ? { left: 0, top: 0, width: 100, height: 100 } : frameFor(image, box);
11
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: {
12
+ return (_jsx("span", { className: className, ...data, 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
13
  position: "absolute", left: `${frame.left}%`, top: `${frame.top}%`,
14
14
  width: `${frame.width}%`, height: `${frame.height}%`, maxWidth: "none",
15
15
  } }) }));
package/dist/index.d.ts CHANGED
@@ -5,13 +5,17 @@ import { Section } from "./section-wrapper.tsx";
5
5
  import { page } from "./render.tsx";
6
6
  import { entryPage } from "./entry-page.tsx";
7
7
  import { shared } from "./shared.ts";
8
+ import { marks } from "./marks.ts";
8
9
  export { Zone, renderPage, EMPTY_PAGE_CONTEXT } from "./render.tsx";
9
10
  export type { PageTree, TreeNode, ZoneContent, PairedPage, Registry, PageContext, PageContextLink, PageLink, PageViewProps } from "./render.tsx";
10
- export { RichText } from "./richtext-render.tsx";
11
+ export { RichText, defaultRichTextImage } from "./richtext-render.tsx";
12
+ export type { RichTextProps, RichTextElement, RichTextImageProps } from "./richtext-render.tsx";
11
13
  export type { PageSchema } from "./page.ts";
12
14
  export { fixedSlug } from "./page.ts";
13
15
  export type { EntryPageSchema, PairedEntryPage, EntryPageProps } from "./entry-page.tsx";
14
- export { f } from "./fields.ts";
16
+ export { f, normalizeOptions, optionsOf, optionValues } from "./fields.ts";
17
+ export { defaultsOf, withDefaults } from "./field-defaults.ts";
18
+ export { filterPageTree } from "./page-tree.ts";
15
19
  export { ASSET_COLLECTION, DEFAULT_MEDIA, MEDIA_TYPES, FULL_CROP, CENTER_HOTSPOT, parseAspect, cropAspect, clamp01, isMediaValue, collectAssetIds, applyAssets, } from "./media.ts";
16
20
  export type { Crop, Hotspot, AssetKind, ImageValue, VideoValue, FileValue, AssetMeta, AssetRecord, ResolvedImage, ResolvedVideo, ResolvedFile, } from "./media.ts";
17
21
  export { z } from "./zones.ts";
@@ -25,15 +29,17 @@ export { SupabasePageStore } from "./supabase-store.ts";
25
29
  export { SupabaseAssetStore } from "./supabase-asset-store.ts";
26
30
  export { collectEntryRefs, collectTreeRefs } from "./refs.ts";
27
31
  export type { RefEdge, SectionLike } from "./refs.ts";
32
+ export { richtextBlocks, elementSchemas, IMAGE_NODE, ELEMENT_NODE } from "./richtext-walk.ts";
33
+ export type { BlockNode, ElementSchemas, ImageNodeAttrs, ElementNodeAttrs } from "./richtext-walk.ts";
28
34
  export { MemoryRefIndex } from "./ref-index.ts";
29
35
  export { MemoryPathIndex } from "./path-index.ts";
30
36
  export type { PathIndex } from "./path-index.ts";
31
37
  export { MemoryEntryStore } from "./entry-store.ts";
32
38
  export { SupabaseEntryStore } from "./supabase-entry-store.ts";
33
39
  export { SupabasePathIndex } from "./supabase-path-index.ts";
34
- export type { EntryStore, EntryRecord, EntryLocale, EntryVersion, EntryUsage, ListOptions, CreateEntryOptions, AddEntryLocaleOptions, EntryStoreOptions, } from "./entry-store.ts";
35
- export { entryErrors, compareEntries, DEFAULT_ENTRY_LOCALES, localizedKeys, splitFields, entryLocaleRow, entryFieldsIn, entryRefEdges, entryTitleIn, } from "./entry-store.ts";
36
- export { affectedTargets, pageTag, entryTag, sharedTag } from "./revalidate.ts";
40
+ export type { EntryStore, EntryRecord, EntryLocale, EntryVersion, EntryUsage, ListOptions, CreateEntryOptions, AddEntryLocaleOptions, EntryStoreOptions, Where, WhereValue, } from "./entry-store.ts";
41
+ export { entryErrors, compareEntries, DEFAULT_ENTRY_LOCALES, localizedKeys, splitFields, entryLocaleRow, entryFieldsIn, entryRefEdges, entryTitleIn, matchesWhere, } from "./entry-store.ts";
42
+ export { affectedTargets, pageTag, entryTag, sharedTag, pagesTreeTag, collectionTag } from "./revalidate.ts";
37
43
  export type { AffectedTargets } from "./revalidate.ts";
38
44
  export { resolveTree, resolveFields, resolveMedia } from "./resolve.ts";
39
45
  export type { EntryFetcher, ResolveFieldsOptions, AssetFetcher } from "./resolve.ts";
@@ -43,7 +49,7 @@ export { fixedPathFor } from "./localize.ts";
43
49
  export type { PageStore, PageRecord, PageLocale, PageVersion, PageStoreOptions, CreatePageInput, AddLocaleOptions } from "./store.ts";
44
50
  export { pathOptions, pageErrors, localeRow, pageRefEdges, DEFAULT_LOCALES } from "./store.ts";
45
51
  export type { SmoodlyConfig, ResolvedConfig } from "./config.ts";
46
- export type { FieldBuilder, Descriptor, ValueOf } from "./fields.ts";
52
+ export type { FieldBuilder, Descriptor, ValueOf, Option, OptionInput, OptionValue } from "./fields.ts";
47
53
  export type { SectionSchema, ElementSchema, BuilderMap } from "./schema.ts";
48
54
  export type { Props, PairedComponent } from "./pairing.tsx";
49
55
  export type { CollectionSchema, EntryOrder } from "./collections.ts";
@@ -126,9 +132,12 @@ export declare const smoodly: {
126
132
  toolset: typeof toolset;
127
133
  shared: typeof shared;
128
134
  Section: typeof Section;
135
+ marks: typeof marks;
129
136
  };
137
+ export { NODE_ATTR, ZONE_ATTR, EMPTY_ATTR, FIELD_ATTR, ITEM_ATTR, EDITOR_ATTR, resolveMarkPath } from "./marks.ts";
138
+ export type { MarkNode, MarkPath, Marks, Scope } from "./marks.ts";
130
139
  export { resolveSmoodlyEnv, smoodlyEnv } from "./env.ts";
131
140
  export type { SmoodlyEnv } from "./env.ts";
132
141
  export { clientOptions, createSmoodlyClient } from "./client.ts";
133
142
  export { createSmoodlySite, pageContextOf } from "./site.ts";
134
- export type { SiteOptions, SitePage, SiteEntry, SiteHit, SmoodlySite, LoadOptions, PageTreeItem, SiteEntryItem, EntryTreeItem, EntryQuery, } from "./site.ts";
143
+ export type { SiteOptions, SitePage, SitePageSchema, SiteEntry, SiteHit, SmoodlySite, LoadOptions, PageTreeItem, SiteEntryItem, EntryTreeItem, EntryQuery, } from "./site.ts";
package/dist/index.js CHANGED
@@ -6,10 +6,13 @@ import { Section } from "./section-wrapper.js";
6
6
  import { page } from "./render.js";
7
7
  import { entryPage } from "./entry-page.js";
8
8
  import { shared } from "./shared.js";
9
+ import { marks } from "./marks.js";
9
10
  export { Zone, renderPage, EMPTY_PAGE_CONTEXT } from "./render.js";
10
- export { RichText } from "./richtext-render.js";
11
+ export { RichText, defaultRichTextImage } from "./richtext-render.js";
11
12
  export { fixedSlug } from "./page.js";
12
- export { f } from "./fields.js";
13
+ export { f, normalizeOptions, optionsOf, optionValues } from "./fields.js";
14
+ export { defaultsOf, withDefaults } from "./field-defaults.js";
15
+ export { filterPageTree } from "./page-tree.js";
13
16
  export { ASSET_COLLECTION, DEFAULT_MEDIA, MEDIA_TYPES, FULL_CROP, CENTER_HOTSPOT, parseAspect, cropAspect, clamp01, isMediaValue, collectAssetIds, applyAssets, } from "./media.js";
14
17
  export { z } from "./zones.js";
15
18
  export { defineConfig } from "./config.js";
@@ -20,13 +23,14 @@ export { imageDimensions } from "./image-dimensions.js";
20
23
  export { SupabasePageStore } from "./supabase-store.js";
21
24
  export { SupabaseAssetStore } from "./supabase-asset-store.js";
22
25
  export { collectEntryRefs, collectTreeRefs } from "./refs.js";
26
+ export { richtextBlocks, elementSchemas, IMAGE_NODE, ELEMENT_NODE } from "./richtext-walk.js";
23
27
  export { MemoryRefIndex } from "./ref-index.js";
24
28
  export { MemoryPathIndex } from "./path-index.js";
25
29
  export { MemoryEntryStore } from "./entry-store.js";
26
30
  export { SupabaseEntryStore } from "./supabase-entry-store.js";
27
31
  export { SupabasePathIndex } from "./supabase-path-index.js";
28
- export { entryErrors, compareEntries, DEFAULT_ENTRY_LOCALES, localizedKeys, splitFields, entryLocaleRow, entryFieldsIn, entryRefEdges, entryTitleIn, } from "./entry-store.js";
29
- export { affectedTargets, pageTag, entryTag, sharedTag } from "./revalidate.js";
32
+ export { entryErrors, compareEntries, DEFAULT_ENTRY_LOCALES, localizedKeys, splitFields, entryLocaleRow, entryFieldsIn, entryRefEdges, entryTitleIn, matchesWhere, } from "./entry-store.js";
33
+ export { affectedTargets, pageTag, entryTag, sharedTag, pagesTreeTag, collectionTag } from "./revalidate.js";
30
34
  export { resolveTree, resolveFields, resolveMedia } from "./resolve.js";
31
35
  export { SEGMENT_RE, isSegment, assertSegment, joinPath, splitPath, segmentFor, isHomeNode, pagePathIn, chainOf, assertDepth, pagePath, pagePathRows, entryPathRows, collectionSegment, buildPageTree, perLocaleSegments, assertSupportedLocale, unsupportedLocale, } from "./paths.js";
32
36
  export { fixedPathFor } from "./localize.js";
@@ -46,7 +50,9 @@ export const smoodly = {
46
50
  toolset,
47
51
  shared,
48
52
  Section,
53
+ marks,
49
54
  };
55
+ export { NODE_ATTR, ZONE_ATTR, EMPTY_ATTR, FIELD_ATTR, ITEM_ATTR, EDITOR_ATTR, resolveMarkPath } from "./marks.js";
50
56
  export { resolveSmoodlyEnv, smoodlyEnv } from "./env.js";
51
57
  export { clientOptions, createSmoodlyClient } from "./client.js";
52
58
  export { createSmoodlySite, pageContextOf } from "./site.js";
@@ -0,0 +1,53 @@
1
+ import type { BuilderMap, NamespaceValues } from "./schema.ts";
2
+ /** A section's root, from `<smoodly.Section>` in editor mode. */
3
+ export declare const NODE_ATTR = "data-smd-node";
4
+ /** An empty zone's placeholder box, from `<Zone>` in editor mode. */
5
+ export declare const ZONE_ATTR = "data-smd-zone";
6
+ export declare const EMPTY_ATTR = "data-smd-empty";
7
+ /** The element that renders a field's value verbatim. */
8
+ export declare const FIELD_ATTR = "data-smd-field";
9
+ /** A list item's or object field's root: scopes the field names inside it. */
10
+ export declare const ITEM_ATTR = "data-smd-item";
11
+ /** The root of an inline editing session inside the canvas; the bridge
12
+ * forwards nothing from inside it, and reports no marks under it. */
13
+ export declare const EDITOR_ATTR = "data-smd-editor";
14
+ /** What the resolver needs from an element — a DOM Element satisfies it. */
15
+ export type MarkNode = {
16
+ getAttribute(name: string): string | null;
17
+ parentElement: MarkNode | null;
18
+ };
19
+ export type MarkPath = {
20
+ node: string;
21
+ path: string;
22
+ };
23
+ /** The section node and dotted field path of a marked element: its own
24
+ * field name, prefixed by every item scope from the element itself up to
25
+ * the section root. Null when the element carries no field mark or sits
26
+ * outside a section node. */
27
+ export declare function resolveMarkPath(el: MarkNode): MarkPath | null;
28
+ type Keys<T> = Extract<keyof T, string>;
29
+ type ElementOf<T> = NonNullable<T> extends readonly (infer E)[] ? E : never;
30
+ /** The helpers for one level of a section's values: the section itself,
31
+ * a list item, or an object field. Item attributes are relative to the
32
+ * parent scope — the resolver joins them — so the helpers carry no path. */
33
+ export type Marks<T> = {
34
+ /** Spread on the element that renders `key` verbatim. */
35
+ field<K extends Keys<T>>(key: K): {
36
+ "data-smd-field": K;
37
+ };
38
+ /** List item `index` of `key`: spread `.attrs` on the item's root and mark the fields inside with the returned helpers. */
39
+ item<K extends Keys<T>>(key: K, index: number): Scope<ElementOf<T[K]>>;
40
+ /** The object field `key`: spread `.attrs` on its root and mark the fields inside. */
41
+ object<K extends Keys<T>>(key: K): Scope<NonNullable<T[K]>>;
42
+ };
43
+ export type Scope<T> = Marks<T> & {
44
+ attrs: {
45
+ "data-smd-item": string;
46
+ };
47
+ };
48
+ /** Typed marks for a section or element schema: `const m = smoodly.marks(heroSchema)`,
49
+ * then `<h1 {...m.field("heading")}>`. The raw attributes stay the contract. */
50
+ export declare function marks<S extends {
51
+ __f?: BuilderMap;
52
+ }>(_schema: S): Marks<NamespaceValues<NonNullable<S["__f"]>>>;
53
+ export {};
package/dist/marks.js ADDED
@@ -0,0 +1,43 @@
1
+ /** A section's root, from `<smoodly.Section>` in editor mode. */
2
+ export const NODE_ATTR = "data-smd-node";
3
+ /** An empty zone's placeholder box, from `<Zone>` in editor mode. */
4
+ export const ZONE_ATTR = "data-smd-zone";
5
+ export const EMPTY_ATTR = "data-smd-empty";
6
+ /** The element that renders a field's value verbatim. */
7
+ export const FIELD_ATTR = "data-smd-field";
8
+ /** A list item's or object field's root: scopes the field names inside it. */
9
+ export const ITEM_ATTR = "data-smd-item";
10
+ /** The root of an inline editing session inside the canvas; the bridge
11
+ * forwards nothing from inside it, and reports no marks under it. */
12
+ export const EDITOR_ATTR = "data-smd-editor";
13
+ /** The section node and dotted field path of a marked element: its own
14
+ * field name, prefixed by every item scope from the element itself up to
15
+ * the section root. Null when the element carries no field mark or sits
16
+ * outside a section node. */
17
+ export function resolveMarkPath(el) {
18
+ const field = el.getAttribute(FIELD_ATTR);
19
+ if (!field)
20
+ return null;
21
+ const segments = [field];
22
+ for (let cur = el; cur; cur = cur.parentElement) {
23
+ const node = cur.getAttribute(NODE_ATTR);
24
+ if (node)
25
+ return { node, path: segments.join(".") };
26
+ const item = cur.getAttribute(ITEM_ATTR);
27
+ if (item)
28
+ segments.unshift(item);
29
+ }
30
+ return null;
31
+ }
32
+ function level() {
33
+ return {
34
+ field: (key) => ({ [FIELD_ATTR]: key }),
35
+ item: (key, index) => ({ ...level(), attrs: { [ITEM_ATTR]: `${key}.${index}` } }),
36
+ object: (key) => ({ ...level(), attrs: { [ITEM_ATTR]: key } }),
37
+ };
38
+ }
39
+ /** Typed marks for a section or element schema: `const m = smoodly.marks(heroSchema)`,
40
+ * then `<h1 {...m.field("heading")}>`. The raw attributes stay the contract. */
41
+ export function marks(_schema) {
42
+ return level();
43
+ }
package/dist/media.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Descriptor } from "./fields.ts";
2
+ import { type ElementSchemas } from "./richtext-walk.ts";
2
3
  export type Crop = {
3
4
  left: number;
4
5
  top: number;
@@ -102,7 +103,7 @@ export declare const isMediaValue: (v: unknown) => v is {
102
103
  };
103
104
  /** Every asset id a value set references, walking lists and objects —
104
105
  * 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
+ export declare function collectAssetIds(values: Record<string, unknown> | undefined, descriptors: Record<string, Descriptor>, out: Set<string>, elements?: ElementSchemas): void;
106
107
  /** Replace every media value in place with its resolved object — or delete
107
108
  * 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;
109
+ export declare function applyAssets(values: Record<string, unknown> | undefined, descriptors: Record<string, Descriptor>, assets: Map<string, AssetRecord>, elements?: ElementSchemas): void;
package/dist/media.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { ELEMENT_NODE, IMAGE_NODE, elementOf, elementSets, richtextBlocks } from "./richtext-walk.js";
1
2
  /** The refs target collection of every asset edge — a reserved registered name. */
2
3
  export const ASSET_COLLECTION = "asset";
3
4
  export const MEDIA_TYPES = new Set(["image", "video", "file"]);
@@ -30,7 +31,7 @@ export const cropAspect = (crop, imageAspect) => (crop.width * imageAspect) / cr
30
31
  export const isMediaValue = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && typeof v.asset === "string";
31
32
  /** Every asset id a value set references, walking lists and objects —
32
33
  * unlike refs, media may sit inside a container (spec §8). */
33
- export function collectAssetIds(values, descriptors, out) {
34
+ export function collectAssetIds(values, descriptors, out, elements = {}) {
34
35
  if (!values)
35
36
  return;
36
37
  for (const [key, d] of Object.entries(descriptors)) {
@@ -41,8 +42,24 @@ export function collectAssetIds(values, descriptors, out) {
41
42
  if (isMediaValue(value))
42
43
  out.add(value.asset);
43
44
  }
45
+ else if (d.type === "richtext") {
46
+ // Block nodes (spec 2026-09-08 block nodes §2): image nodes, and media inside a known element's fields and styles.
47
+ for (const node of richtextBlocks(value)) {
48
+ if (node.type === IMAGE_NODE) {
49
+ const image = node.attrs.image;
50
+ if (isMediaValue(image))
51
+ out.add(image.asset);
52
+ }
53
+ else if (node.type === ELEMENT_NODE) {
54
+ const schema = elementOf(node, elements);
55
+ if (schema)
56
+ for (const set of elementSets(node, schema))
57
+ collectAssetIds(set.values, set.descriptors, out, elements);
58
+ }
59
+ }
60
+ }
44
61
  else if (d.type === "object" && typeof value === "object" && !Array.isArray(value)) {
45
- collectAssetIds(value, d.fields, out);
62
+ collectAssetIds(value, d.fields, out, elements);
46
63
  }
47
64
  else if (d.type === "list" && Array.isArray(value)) {
48
65
  const item = d.item;
@@ -52,7 +69,10 @@ export function collectAssetIds(values, descriptors, out) {
52
69
  out.add(v.asset);
53
70
  }
54
71
  else if (item.type === "object" && v && typeof v === "object") {
55
- collectAssetIds(v, item.fields, out);
72
+ collectAssetIds(v, item.fields, out, elements);
73
+ }
74
+ else if (item.type === "richtext") {
75
+ collectAssetIds({ v }, { v: item }, out, elements);
56
76
  }
57
77
  }
58
78
  }
@@ -97,7 +117,7 @@ function resolveOne(value, d, assets) {
97
117
  }
98
118
  /** Replace every media value in place with its resolved object — or delete
99
119
  * it when the asset is missing or pending, so `image?.url` guards hold. */
100
- export function applyAssets(values, descriptors, assets) {
120
+ export function applyAssets(values, descriptors, assets, elements = {}) {
101
121
  if (!values)
102
122
  return;
103
123
  for (const [key, d] of Object.entries(descriptors)) {
@@ -111,8 +131,25 @@ export function applyAssets(values, descriptors, assets) {
111
131
  else
112
132
  values[key] = resolved;
113
133
  }
134
+ else if (d.type === "richtext") {
135
+ for (const node of richtextBlocks(value)) {
136
+ if (node.type === IMAGE_NODE) {
137
+ const resolved = resolveOne(node.attrs.image, { type: "image" }, assets);
138
+ if (resolved === undefined)
139
+ delete node.attrs.image;
140
+ else
141
+ node.attrs.image = resolved;
142
+ }
143
+ else if (node.type === ELEMENT_NODE) {
144
+ const schema = elementOf(node, elements);
145
+ if (schema)
146
+ for (const set of elementSets(node, schema))
147
+ applyAssets(set.values, set.descriptors, assets, elements);
148
+ }
149
+ }
150
+ }
114
151
  else if (d.type === "object" && typeof value === "object" && !Array.isArray(value)) {
115
- applyAssets(value, d.fields, assets);
152
+ applyAssets(value, d.fields, assets, elements);
116
153
  }
117
154
  else if (d.type === "list" && Array.isArray(value)) {
118
155
  const item = d.item;
@@ -122,7 +159,12 @@ export function applyAssets(values, descriptors, assets) {
122
159
  else if (item.type === "object") {
123
160
  for (const v of value)
124
161
  if (v && typeof v === "object")
125
- applyAssets(v, item.fields, assets);
162
+ applyAssets(v, item.fields, assets, elements);
163
+ }
164
+ else if (item.type === "richtext") {
165
+ // Resolves in place inside the document object `v`; the wrapper is throwaway.
166
+ for (const v of value)
167
+ applyAssets({ v }, { v: item }, assets, elements);
126
168
  }
127
169
  }
128
170
  }
@@ -1,3 +1,5 @@
1
1
  export { createPageRenderer } from "./page-renderer.tsx";
2
2
  export type { PageRendererOptions, PathRenderOptions, PageRegistration } from "./page-renderer.tsx";
3
3
  export { metadataFrom } from "./meta.ts";
4
+ export { createSmoodlyQueries } from "./queries.ts";
5
+ export type { SmoodlyQueries, QueryOptions, EntriesQuery, TypedEntryItem, SharedValue } from "./queries.ts";
@@ -1,2 +1,3 @@
1
1
  export { createPageRenderer } from "./page-renderer.js";
2
2
  export { metadataFrom } from "./meta.js";
3
+ export { createSmoodlyQueries } from "./queries.js";
@@ -1,4 +1,8 @@
1
1
  import type { Metadata } from "next";
2
- /** Maps the page tree's meta bag onto Next metadata. Absent meta returns
3
- * {} so the app's own layout.tsx defaults apply untouched. */
4
- export declare function metadataFrom(meta: unknown): Metadata;
2
+ /** Maps the page tree's meta onto Next metadata (spec 2026-09-10 page
3
+ * fields §4). The SEO title falls back to the page's own title so every
4
+ * page carries one; absent meta and no title returns {} so the app's
5
+ * layout.tsx defaults apply untouched. */
6
+ export declare function metadataFrom(meta: unknown, page?: {
7
+ title?: string;
8
+ }): Metadata;
package/dist/next/meta.js CHANGED
@@ -1,13 +1,15 @@
1
- /** Maps the page tree's meta bag onto Next metadata. Absent meta returns
2
- * {} so the app's own layout.tsx defaults apply untouched. */
3
- export function metadataFrom(meta) {
4
- if (typeof meta !== "object" || meta === null || Array.isArray(meta))
5
- return {};
6
- const m = meta;
1
+ import { imageUrl } from "../image/loaders.js";
2
+ const isResolvedImage = (v) => typeof v === "object" && v !== null && typeof v.url === "string";
3
+ /** Maps the page tree's meta onto Next metadata (spec 2026-09-10 page
4
+ * fields §4). The SEO title falls back to the page's own title so every
5
+ * page carries one; absent meta and no title returns {} so the app's
6
+ * layout.tsx defaults apply untouched. */
7
+ export function metadataFrom(meta, page) {
8
+ const m = typeof meta === "object" && meta !== null && !Array.isArray(meta) ? meta : {};
7
9
  const str = (key) => (typeof m[key] === "string" && m[key] ? m[key] : undefined);
8
- const title = str("title");
10
+ const title = str("title") ?? (page?.title ? page.title : undefined);
9
11
  const description = str("description");
10
- const ogImage = str("ogImage");
12
+ const ogImage = m.ogImage;
11
13
  const out = {};
12
14
  if (title)
13
15
  out.title = title;
@@ -18,9 +20,16 @@ export function metadataFrom(meta) {
18
20
  og.title = title;
19
21
  if (description)
20
22
  og.description = description;
21
- if (ogImage)
23
+ if (isResolvedImage(ogImage)) {
24
+ // The delivery seam decides the URL; the cloud's transform loader (OQ 28) can serve a real 1200-wide render.
25
+ og.images = [{ url: imageUrl(ogImage, { width: 1200 }), width: ogImage.width, height: ogImage.height, ...(ogImage.alt ? { alt: ogImage.alt } : {}) }];
26
+ }
27
+ else if (typeof ogImage === "string" && ogImage) {
22
28
  og.images = [ogImage];
29
+ }
23
30
  if (Object.keys(og).length > 0)
24
31
  out.openGraph = og;
32
+ if (m.noindex === true)
33
+ out.robots = { index: false, follow: false };
25
34
  return out;
26
35
  }
@@ -73,7 +73,7 @@ export function createPageRenderer(options) {
73
73
  const hit = await load(locale, path, draft);
74
74
  if (!hit || !accept(hit))
75
75
  return {};
76
- return { ...(hit.kind === "page" ? metadataFrom(hit.tree.meta) : {}), ...alternates(hit) };
76
+ return { ...(hit.kind === "page" ? metadataFrom(hit.tree.meta, { title: hit.title }) : {}), ...alternates(hit) };
77
77
  }
78
78
  const any = () => true;
79
79
  /** The escape hatch may only render ITS OWN registration's record. */
@@ -0,0 +1,36 @@
1
+ import type { CollectionSchema } from "../collections.ts";
2
+ import type { EntryRecord } from "../entry-store.ts";
3
+ import type { SharedSchema } from "../shared.ts";
4
+ import type { EntryQuery, PageTreeItem, SiteEntryItem, SitePage, SmoodlySite } from "../site.ts";
5
+ export type QueryOptions = {
6
+ locale?: string;
7
+ };
8
+ export type EntriesQuery = Omit<EntryQuery, "locale" | "draft"> & QueryOptions;
9
+ type ByName<T, N> = Extract<T, {
10
+ name: N;
11
+ }>;
12
+ /** Fields typed from the schema; a widened `CollectionSchema[]` array (names are `string`) falls back to the untyped record instead of `never`. */
13
+ type EntryFields<C extends readonly CollectionSchema<any, string>[], N> = string extends C[number]["name"] ? Record<string, unknown> : NonNullable<ByName<C[number], N>["__entry"]>;
14
+ /** A list item whose `entry.fields` are the collection's typed values. */
15
+ export type TypedEntryItem<C extends readonly CollectionSchema<any, string>[], N> = Omit<SiteEntryItem, "entry"> & {
16
+ entry: Omit<EntryRecord, "fields"> & {
17
+ fields: EntryFields<C, N>;
18
+ };
19
+ };
20
+ export type SharedValue<S extends readonly SharedSchema<any, string>[], N> = string extends S[number]["name"] ? Record<string, unknown> : NonNullable<ByName<S[number], N>["__value"]>;
21
+ export type SmoodlyQueries<C extends readonly CollectionSchema<any, string>[], S extends readonly SharedSchema<any, string>[]> = {
22
+ /** The published page tree in a locale, every item with its page fields. */
23
+ getPageTree(options?: QueryOptions): Promise<PageTreeItem[]>;
24
+ /** A page by locale-relative path ("/company/about-us") or id. */
25
+ getPage(pathOrId: string, options?: QueryOptions): Promise<SitePage | null>;
26
+ /** Published entries of a collection: `where` (shared fields), `order`, `limit`, `parent` run in the store. */
27
+ getEntries<N extends C[number]["name"]>(collection: N, options?: EntriesQuery): Promise<TypedEntryItem<C, N>[]>;
28
+ getEntry<N extends C[number]["name"]>(collection: N, idOrPath: string, options?: QueryOptions): Promise<TypedEntryItem<C, N> | null>;
29
+ getShared<N extends S[number]["name"]>(item: N, options?: QueryOptions): Promise<SharedValue<S, N> | null>;
30
+ };
31
+ export declare function createSmoodlyQueries<const C extends readonly CollectionSchema<any, string>[], const S extends readonly SharedSchema<any, string>[] = []>(options: {
32
+ site: SmoodlySite;
33
+ collections: C;
34
+ shared?: S;
35
+ }): SmoodlyQueries<C, S>;
36
+ export {};