smoodly 0.0.11 → 0.0.13

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 (132) hide show
  1. package/README.md +259 -38
  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 +130 -25
  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/inline-dom.d.ts +27 -0
  12. package/dist/admin/editor/inline-dom.js +43 -0
  13. package/dist/admin/editor/inline-session.d.ts +30 -0
  14. package/dist/admin/editor/inline-session.js +19 -0
  15. package/dist/admin/editor/protocol.d.ts +22 -1
  16. package/dist/admin/editor/useCanvasBridge.d.ts +8 -4
  17. package/dist/admin/editor/useCanvasBridge.js +12 -7
  18. package/dist/admin/editor/useInlineSession.d.ts +46 -0
  19. package/dist/admin/editor/useInlineSession.js +244 -0
  20. package/dist/admin/field-paths.d.ts +13 -0
  21. package/dist/admin/field-paths.js +49 -0
  22. package/dist/admin/forms/FieldWidget.d.ts +7 -2
  23. package/dist/admin/forms/FieldWidget.js +12 -40
  24. package/dist/admin/forms/ListField.d.ts +10 -0
  25. package/dist/admin/forms/ListField.js +42 -0
  26. package/dist/admin/forms/MediaWidget.js +10 -3
  27. package/dist/admin/forms/RichtextElementCard.d.ts +3 -0
  28. package/dist/admin/forms/RichtextElementCard.js +37 -0
  29. package/dist/admin/forms/RichtextImageCard.d.ts +3 -0
  30. package/dist/admin/forms/RichtextImageCard.js +42 -0
  31. package/dist/admin/forms/RichtextInsertMenu.d.ts +6 -0
  32. package/dist/admin/forms/RichtextInsertMenu.js +6 -0
  33. package/dist/admin/forms/RichtextPopovers.d.ts +7 -0
  34. package/dist/admin/forms/RichtextPopovers.js +25 -0
  35. package/dist/admin/forms/RichtextToolbar.d.ts +4 -2
  36. package/dist/admin/forms/RichtextToolbar.js +4 -2
  37. package/dist/admin/forms/RichtextWidget.d.ts +4 -1
  38. package/dist/admin/forms/RichtextWidget.js +23 -120
  39. package/dist/admin/forms/elements-context.d.ts +7 -0
  40. package/dist/admin/forms/elements-context.js +13 -0
  41. package/dist/admin/forms/reveal.d.ts +5 -0
  42. package/dist/admin/forms/reveal.js +47 -0
  43. package/dist/admin/forms/richtext-extensions.d.ts +8 -0
  44. package/dist/admin/forms/richtext-extensions.js +11 -0
  45. package/dist/admin/forms/richtext-form-context.d.ts +16 -0
  46. package/dist/admin/forms/richtext-form-context.js +18 -0
  47. package/dist/admin/forms/richtext-insert.d.ts +38 -0
  48. package/dist/admin/forms/richtext-insert.js +37 -0
  49. package/dist/admin/forms/richtext-nodes.d.ts +37 -0
  50. package/dist/admin/forms/richtext-nodes.js +85 -0
  51. package/dist/admin/forms/richtext-paste.js +5 -0
  52. package/dist/admin/forms/tabs.d.ts +3 -0
  53. package/dist/admin/forms/tabs.js +6 -0
  54. package/dist/admin/forms/useRichtextEditor.d.ts +51 -0
  55. package/dist/admin/forms/useRichtextEditor.js +281 -0
  56. package/dist/admin/next/bridge.js +51 -20
  57. package/dist/admin/ops-impl.js +13 -8
  58. package/dist/admin/richtext-doc.d.ts +3 -0
  59. package/dist/admin/richtext-doc.js +12 -0
  60. package/dist/admin/serialize.d.ts +7 -0
  61. package/dist/admin/serialize.js +16 -1
  62. package/dist/admin/shell/AdminApp.js +2 -1
  63. package/dist/admin/shell/EntriesList.js +34 -12
  64. package/dist/admin/shell/EntryForm.js +22 -15
  65. package/dist/admin/shell/IconRail.js +6 -5
  66. package/dist/admin/shell/ListView.d.ts +9 -2
  67. package/dist/admin/shell/ListView.js +7 -6
  68. package/dist/admin/shell/LoginView.js +2 -2
  69. package/dist/admin/shell/PagesList.js +53 -24
  70. package/dist/admin/shell/SharedForm.js +17 -11
  71. package/dist/admin/shell/SharedList.js +5 -3
  72. package/dist/admin/shell/entries-list.d.ts +4 -0
  73. package/dist/admin/shell/entries-list.js +6 -0
  74. package/dist/admin/shell/pages-tree.d.ts +24 -0
  75. package/dist/admin/shell/pages-tree.js +59 -1
  76. package/dist/admin/tree-ops.d.ts +9 -1
  77. package/dist/admin/tree-ops.js +27 -0
  78. package/dist/admin/ui/LocaleSwitcher.d.ts +10 -0
  79. package/dist/admin/ui/LocaleSwitcher.js +28 -8
  80. package/dist/admin/ui/Modal.js +14 -2
  81. package/dist/admin/ui/ResizeHandle.d.ts +1 -0
  82. package/dist/admin/ui/ResizeHandle.js +127 -0
  83. package/dist/admin/ui/primitives.d.ts +29 -4
  84. package/dist/admin/ui/primitives.js +29 -10
  85. package/dist/admin/ui/resize.d.ts +16 -0
  86. package/dist/admin/ui/resize.js +23 -0
  87. package/dist/admin/ui/sortable.d.ts +23 -0
  88. package/dist/admin/ui/sortable.js +57 -0
  89. package/dist/admin/ui/theme.d.ts +3 -1
  90. package/dist/admin/ui/theme.js +512 -165
  91. package/dist/admin/ui/useSortable.d.ts +35 -0
  92. package/dist/admin/ui/useSortable.js +220 -0
  93. package/dist/admin/validate.d.ts +6 -1
  94. package/dist/admin/validate.js +28 -9
  95. package/dist/config.d.ts +1 -0
  96. package/dist/config.js +36 -0
  97. package/dist/entry-store.d.ts +5 -2
  98. package/dist/entry-store.js +4 -3
  99. package/dist/fields.d.ts +4 -0
  100. package/dist/fields.js +2 -2
  101. package/dist/image/SmoodlyImage.d.ts +4 -1
  102. package/dist/image/SmoodlyImage.js +2 -2
  103. package/dist/index.d.ts +8 -1
  104. package/dist/index.js +5 -1
  105. package/dist/marks.d.ts +53 -0
  106. package/dist/marks.js +43 -0
  107. package/dist/media.d.ts +3 -2
  108. package/dist/media.js +48 -6
  109. package/dist/refs.d.ts +2 -2
  110. package/dist/refs.js +33 -12
  111. package/dist/render.js +2 -1
  112. package/dist/resolve.d.ts +8 -2
  113. package/dist/resolve.js +8 -7
  114. package/dist/richtext/index.d.ts +12 -0
  115. package/dist/richtext/index.js +9 -0
  116. package/dist/richtext-render.d.ts +22 -2
  117. package/dist/richtext-render.js +40 -13
  118. package/dist/richtext-walk.d.ts +44 -0
  119. package/dist/richtext-walk.js +46 -0
  120. package/dist/schema.d.ts +1 -1
  121. package/dist/schema.js +13 -1
  122. package/dist/section-wrapper.js +2 -1
  123. package/dist/site.d.ts +2 -0
  124. package/dist/site.js +7 -4
  125. package/dist/store.d.ts +3 -1
  126. package/dist/store.js +4 -4
  127. package/dist/supabase-entry-store.d.ts +1 -0
  128. package/dist/supabase-entry-store.js +2 -1
  129. package/dist/supabase-store.js +1 -1
  130. package/dist/toolset.d.ts +7 -1
  131. package/dist/toolset.js +13 -4
  132. package/package.json +10 -1
package/dist/site.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { entryFieldsIn, schemaOf } from "./entry-store.js";
2
2
  import { buildPageTree, chainOf, entryPathRows, entrySegment, pagePath, } from "./paths.js";
3
3
  import { resolveFields, resolveMedia, resolveMediaSets, resolveTree } from "./resolve.js";
4
+ import { elementSchemas } from "./richtext-walk.js";
4
5
  import { STYLES_KEY } from "./shared.js";
5
6
  /** The `page` prop for a loaded page. `toUrl` is the app's `href` seam
6
7
  * bound to the render locale; the default keeps CMS paths as URLs. */
@@ -18,6 +19,7 @@ export function pageContextOf(hit, toUrl = (p) => p) {
18
19
  export function createSmoodlySite(options) {
19
20
  const locales = options.locales;
20
21
  const homePageSlug = options.homePageSlug ?? "home";
22
+ const elements = elementSchemas(options.elements);
21
23
  const pathOpts = { locales, homePageSlug };
22
24
  const mounted = options.collections.filter((c) => c.path !== undefined);
23
25
  /** The resolver's fetcher, bound to a locale: the merged fields of that
@@ -65,6 +67,7 @@ export function createSmoodlySite(options) {
65
67
  sections: options.sections,
66
68
  collections: options.collections,
67
69
  shared: options.shared,
70
+ elements: options.elements,
68
71
  fetch: fetcher(draft, locale),
69
72
  assets: assetFetcher(),
70
73
  });
@@ -107,7 +110,7 @@ export function createSmoodlySite(options) {
107
110
  : (await entries.getMany(schema.name, [id], locale, { status: "published" }))[id];
108
111
  if (!raw)
109
112
  return null;
110
- const fields = await resolveFields(raw, schema.fields, { collections: options.collections, fetch: fetcher(draft, locale), assets: assetFetcher() });
113
+ const fields = await resolveFields(raw, schema.fields, { collections: options.collections, elements: options.elements, fetch: fetcher(draft, locale), assets: assetFetcher() });
111
114
  // Alternates advertise PUBLISHED locales only; the editor (draft) sees every present one.
112
115
  const paths = Object.fromEntries(Object.entries(await entries.pathsOf(schema.name, id)).filter(([l]) => draft || record.locales[l]?.status === "published"));
113
116
  return { kind: "entry", collection: schema.name, entry: { ...record, fields }, path, paths };
@@ -141,7 +144,7 @@ export function createSmoodlySite(options) {
141
144
  // the whole list), because a list page's cards need their image URLs.
142
145
  const fetchAssets = assetFetcher();
143
146
  if (fetchAssets)
144
- await resolveMediaSets(Object.values(published).map((values) => ({ values, descriptors: schema.fields })), fetchAssets);
147
+ await resolveMediaSets(Object.values(published).map((values) => ({ values, descriptors: schema.fields })), fetchAssets, elements);
145
148
  return entryItems(schema, wanted, all, locale, published);
146
149
  }
147
150
  const api = {
@@ -207,13 +210,13 @@ export function createSmoodlySite(options) {
207
210
  : (await entries.getMany(item.name, [record.id], locale, { status: "published" }))[record.id];
208
211
  if (!raw)
209
212
  return null;
210
- const fields = await resolveFields(raw, item.fields, { collections: options.collections, fetch: fetcher(draft, locale), assets: assetFetcher() });
213
+ const fields = await resolveFields(raw, item.fields, { collections: options.collections, elements: options.elements, fetch: fetcher(draft, locale), assets: assetFetcher() });
211
214
  if (!item.section)
212
215
  return fields;
213
216
  const { [STYLES_KEY]: styles, ...rest } = fields;
214
217
  const fetch = assetFetcher();
215
218
  if (fetch && styles && typeof styles === "object")
216
- await resolveMedia(styles, sectionStyles(item.section), fetch);
219
+ await resolveMedia(styles, sectionStyles(item.section), fetch, elements);
217
220
  return { ...rest, styles: styles !== null && typeof styles === "object" ? styles : {} };
218
221
  },
219
222
  };
package/dist/store.d.ts CHANGED
@@ -86,6 +86,8 @@ export interface PageStore {
86
86
  * written; without `entries` publishing is not integrity-checked. */
87
87
  export type PageStoreOptions = {
88
88
  sections?: SectionLike[];
89
+ /** Registered Elements a richtext document may embed (spec 2026-09-08 block nodes §2). */
90
+ elements?: SectionLike[];
89
91
  entries?: EntryStore;
90
92
  /** The shared URL index (one instance for pages AND entries). */
91
93
  paths?: PathIndex;
@@ -123,7 +125,7 @@ export declare function localeRow(page: PageRecord, locale: string): PageLocale;
123
125
  /** A page's refs rows cover EVERY live tree — each locale's draft and
124
126
  * published trees — so an entry stays delete-guarded while any of them
125
127
  * shows it. Callers dedupe on write (the SQL PK does it for real). */
126
- export declare function pageRefEdges(trees: PageTree[], sections: SectionLike[]): RefEdge[];
128
+ export declare function pageRefEdges(trees: PageTree[], sections: SectionLike[], elements?: SectionLike[]): RefEdge[];
127
129
  /** The publish-integrity gate (DESIGN.md §3), per locale: a page must not
128
130
  * go live in a locale showing entries the published site cannot resolve
129
131
  * THERE — every referenced entry must exist, have a row in that locale,
package/dist/store.js CHANGED
@@ -50,10 +50,10 @@ export function localeRow(page, locale) {
50
50
  /** A page's refs rows cover EVERY live tree — each locale's draft and
51
51
  * published trees — so an entry stays delete-guarded while any of them
52
52
  * shows it. Callers dedupe on write (the SQL PK does it for real). */
53
- export function pageRefEdges(trees, sections) {
53
+ export function pageRefEdges(trees, sections, elements) {
54
54
  const edges = [];
55
55
  for (const tree of trees)
56
- edges.push(...collectTreeRefs(tree, sections));
56
+ edges.push(...collectTreeRefs(tree, sections, elements));
57
57
  return edges;
58
58
  }
59
59
  /** The publish-integrity gate (DESIGN.md §3), per locale: a page must not
@@ -159,10 +159,10 @@ export class MemoryPageStore {
159
159
  return trees;
160
160
  }
161
161
  writeRefs(page) {
162
- const { sections, refs } = this.options;
162
+ const { sections, elements, refs } = this.options;
163
163
  if (!sections || !refs)
164
164
  return;
165
- refs.rewrite({ sourceKind: "page", sourceId: page.id }, pageRefEdges(this.liveTrees(page), sections));
165
+ refs.rewrite({ sourceKind: "page", sourceId: page.id }, pageRefEdges(this.liveTrees(page), sections, elements));
166
166
  }
167
167
  newVersion(pageId, locale, tree) {
168
168
  const version = { id: uid(), pageId, locale, tree: structuredClone(tree), createdAt: Date.now() };
@@ -6,6 +6,7 @@ export declare class SupabaseEntryStore implements EntryStore {
6
6
  private paths;
7
7
  private locales;
8
8
  private schemas;
9
+ private elements;
9
10
  private spaceId?;
10
11
  constructor(db: SupabaseClient, collections: CollectionSchema<any>[], options?: EntryStoreOptions);
11
12
  private fail;
@@ -48,6 +48,7 @@ export class SupabaseEntryStore {
48
48
  this.paths = options.paths ?? new SupabasePathIndex(db);
49
49
  this.locales = options.locales ?? DEFAULT_ENTRY_LOCALES;
50
50
  this.schemas = [...collections, ...(options.shared ?? [])];
51
+ this.elements = options.elements;
51
52
  }
52
53
  fail(message) {
53
54
  throw new Error(`smoodly: ${message}`);
@@ -167,7 +168,7 @@ export class SupabaseEntryStore {
167
168
  this.fail(deleted.error.message);
168
169
  // dedupe: the refs PK is (source_kind, source_id, source_field, target_id)
169
170
  const rows = new Map();
170
- for (const edge of entryRefEdges(await this.liveFieldSets(entry), schema)) {
171
+ for (const edge of entryRefEdges(await this.liveFieldSets(entry), schema, this.elements)) {
171
172
  rows.set(`${edge.field}\x00${edge.targetId}`, {
172
173
  source_kind: "entry",
173
174
  source_id: entry.id,
@@ -189,7 +189,7 @@ export class SupabasePageStore {
189
189
  async writeRefs(page) {
190
190
  if (!this.options.sections)
191
191
  return;
192
- const edges = pageRefEdges(await this.liveTrees(page), this.options.sections);
192
+ const edges = pageRefEdges(await this.liveTrees(page), this.options.sections, this.options.elements);
193
193
  const deleted = await this.db.from("refs").delete().eq("source_kind", "page").eq("source_id", page.id);
194
194
  if (deleted.error)
195
195
  this.fail(deleted.error.message);
package/dist/toolset.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export declare const TOOLSET_MARKS: readonly ["bold", "italic", "link"];
2
- export declare const TOOLSET_NODES: readonly ["h1", "h2", "h3", "h4", "h5", "h6", "bulletList", "orderedList", "blockquote"];
2
+ export declare const TOOLSET_NODES: readonly ["h1", "h2", "h3", "h4", "h5", "h6", "bulletList", "orderedList", "blockquote", "image"];
3
3
  export type ToolsetMark = (typeof TOOLSET_MARKS)[number];
4
4
  export type ToolsetNode = (typeof TOOLSET_NODES)[number];
5
5
  export type Toolset = {
@@ -13,9 +13,11 @@ export declare const DEFAULT_TOOLSET: Toolset;
13
13
  export declare function toolset(input: {
14
14
  marks?: readonly ToolsetMark[];
15
15
  nodes?: readonly ToolsetNode[];
16
+ /** Registered Elements embeddable in this toolset, by value (spec 2026-09-08 block nodes §1). */
16
17
  elements?: {
17
18
  schema: {
18
19
  name: string;
20
+ contexts?: readonly string[];
19
21
  };
20
22
  }[];
21
23
  }): Toolset;
@@ -26,5 +28,9 @@ export type Allowed = {
26
28
  nodes: ReadonlySet<ToolsetNode>;
27
29
  /** The allowed heading levels, ascending. */
28
30
  headingLevels: number[];
31
+ /** Whether the image block may be inserted. */
32
+ image: boolean;
33
+ /** The element names that may be inserted, in toolset order. */
34
+ elements: string[];
29
35
  };
30
36
  export declare function allowedOf(input: unknown): Allowed;
package/dist/toolset.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // registration; a field without a toolset carries the default. `Allowed`
5
5
  // is the resolved form the widget reads (spec 2026-09-08 §1).
6
6
  export const TOOLSET_MARKS = ["bold", "italic", "link"];
7
- export const TOOLSET_NODES = ["h1", "h2", "h3", "h4", "h5", "h6", "bulletList", "orderedList", "blockquote"];
7
+ export const TOOLSET_NODES = ["h1", "h2", "h3", "h4", "h5", "h6", "bulletList", "orderedList", "blockquote", "image"];
8
8
  /** What a richtext field gets when it declares nothing. `h1` is left out:
9
9
  * the section or page shape owns the page's first heading. */
10
10
  export const DEFAULT_TOOLSET = {
@@ -29,21 +29,30 @@ export function toolset(input) {
29
29
  }
30
30
  out.nodes = [...input.nodes];
31
31
  }
32
- if (input.elements)
32
+ if (input.elements) {
33
+ for (const e of input.elements) {
34
+ if (!(e.schema.contexts ?? []).includes("richtext")) {
35
+ throw new Error(`smoodly: element "${e.schema.name}" is not registered for the richtext context — add "richtext" to its contexts.`);
36
+ }
37
+ }
33
38
  out.elements = input.elements.map((e) => e.schema.name);
39
+ }
34
40
  return out;
35
41
  }
36
42
  const HEADING = /^h([1-6])$/;
37
43
  export function allowedOf(input) {
38
44
  // A missing or malformed toolset is the default; a declared one is exact,
39
45
  // so a group it leaves out allows nothing (a marks-only toolset has no headings).
40
- const t = input && typeof input === "object" ? input : DEFAULT_TOOLSET;
46
+ const t = input && typeof input === "object"
47
+ ? input
48
+ : DEFAULT_TOOLSET;
41
49
  const marks = Array.isArray(t.marks) ? t.marks.filter((m) => typeof m === "string" && isMark(m)) : [];
42
50
  const nodes = Array.isArray(t.nodes) ? t.nodes.filter((n) => typeof n === "string" && isNode(n)) : [];
51
+ const elements = Array.isArray(t.elements) ? t.elements.filter((e) => typeof e === "string") : [];
43
52
  const headingLevels = nodes
44
53
  .map((n) => HEADING.exec(n)?.[1])
45
54
  .filter((l) => l !== undefined)
46
55
  .map(Number)
47
56
  .sort((a, b) => a - b);
48
- return { marks: new Set(marks), nodes: new Set(nodes), headingLevels };
57
+ return { marks: new Set(marks), nodes: new Set(nodes), headingLevels, image: nodes.includes("image"), elements };
49
58
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoodly",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "Open-source, code-first CMS for Next.js on Supabase: define a component once, it becomes a typed, editable building block.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -24,6 +24,14 @@
24
24
  "types": "./dist/image/index.d.ts",
25
25
  "default": "./dist/image/index.js"
26
26
  },
27
+ "./image/config": {
28
+ "types": "./dist/image/next-config.d.ts",
29
+ "default": "./dist/image/next-config.js"
30
+ },
31
+ "./richtext": {
32
+ "types": "./dist/richtext/index.d.ts",
33
+ "default": "./dist/richtext/index.js"
34
+ },
27
35
  "./admin": {
28
36
  "types": "./dist/admin/index.d.ts",
29
37
  "default": "./dist/admin/index.js"
@@ -79,6 +87,7 @@
79
87
  "@tiptap/pm": "3.31.3",
80
88
  "@tiptap/react": "3.31.3",
81
89
  "@tiptap/starter-kit": "3.31.3",
90
+ "@tiptap/suggestion": "3.31.3",
82
91
  "lucide-react": "^1.41.0"
83
92
  }
84
93
  }