smoodly 0.0.15 → 0.0.17
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 +238 -36
- package/dist/admin/auth.d.ts +7 -4
- package/dist/admin/auth.js +1 -1
- package/dist/admin/client-ops.js +3 -3
- package/dist/admin/custom-section.d.ts +10 -0
- package/dist/admin/custom-section.js +10 -0
- package/dist/admin/editor/CanvasOverlay.d.ts +6 -2
- package/dist/admin/editor/CanvasOverlay.js +6 -6
- package/dist/admin/editor/EditorView.d.ts +2 -0
- package/dist/admin/editor/EditorView.js +322 -62
- package/dist/admin/editor/GlobalInspector.d.ts +24 -0
- package/dist/admin/editor/GlobalInspector.js +13 -0
- package/dist/admin/editor/InsertPicker.d.ts +6 -5
- package/dist/admin/editor/InsertPicker.js +6 -4
- package/dist/admin/editor/InspectorFields.d.ts +21 -0
- package/dist/admin/editor/InspectorFields.js +22 -0
- package/dist/admin/editor/Outline.d.ts +0 -1
- package/dist/admin/editor/Outline.js +3 -3
- package/dist/admin/editor/PageSettings.js +3 -2
- package/dist/admin/editor/global-edit.d.ts +39 -0
- package/dist/admin/editor/global-edit.js +124 -0
- package/dist/admin/editor/useGlobalEdit.d.ts +6 -0
- package/dist/admin/editor/useGlobalEdit.js +18 -0
- package/dist/admin/forms/FieldWidget.d.ts +14 -1
- package/dist/admin/forms/FieldWidget.js +38 -11
- package/dist/admin/forms/ListField.d.ts +9 -1
- package/dist/admin/forms/ListField.js +16 -8
- package/dist/admin/forms/RichtextElementCard.js +11 -5
- package/dist/admin/forms/RowsWidget.d.ts +13 -0
- package/dist/admin/forms/RowsWidget.js +26 -0
- package/dist/admin/forms/SpanBar.d.ts +5 -0
- package/dist/admin/forms/SpanBar.js +37 -0
- package/dist/admin/forms/richtext-insert.d.ts +2 -2
- package/dist/admin/forms/richtext-insert.js +3 -3
- package/dist/admin/forms/richtext-nodes.js +1 -1
- package/dist/admin/forms/tabs.d.ts +20 -8
- package/dist/admin/forms/tabs.js +19 -23
- package/dist/admin/forms/useRichtextEditor.js +8 -1
- package/dist/admin/global-items.d.ts +9 -0
- package/dist/admin/{shared-items.js → global-items.js} +29 -14
- package/dist/admin/index.d.ts +3 -3
- package/dist/admin/index.js +1 -1
- package/dist/admin/next/op-handler.js +4 -3
- package/dist/admin/ops-impl.d.ts +3 -0
- package/dist/admin/ops-impl.js +242 -56
- package/dist/admin/ops.d.ts +38 -6
- package/dist/admin/serialize.d.ts +18 -7
- package/dist/admin/serialize.js +27 -5
- package/dist/admin/shell/AdminApp.js +7 -7
- package/dist/admin/shell/EntriesList.js +19 -3
- package/dist/admin/shell/EntryForm.js +183 -77
- package/dist/admin/shell/GlobalFields.d.ts +24 -0
- package/dist/admin/shell/GlobalFields.js +19 -0
- package/dist/admin/shell/{SharedForm.d.ts → GlobalForm.d.ts} +1 -1
- package/dist/admin/shell/GlobalForm.js +258 -0
- package/dist/admin/shell/{SharedList.d.ts → GlobalList.d.ts} +1 -1
- package/dist/admin/shell/{SharedList.js → GlobalList.js} +12 -11
- package/dist/admin/shell/IconRail.js +14 -7
- package/dist/admin/shell/PagesList.js +4 -8
- package/dist/admin/shell/entries-list.d.ts +2 -2
- package/dist/admin/shell/entries-list.js +3 -3
- package/dist/admin/shell/{shared-list.d.ts → global-list.d.ts} +3 -3
- package/dist/admin/shell/{shared-list.js → global-list.js} +2 -2
- package/dist/admin/supabase-auth.js +1 -1
- package/dist/admin/tree-ops.d.ts +10 -8
- package/dist/admin/tree-ops.js +28 -16
- package/dist/admin/ui/ActionMenu.d.ts +11 -0
- package/dist/admin/ui/ActionMenu.js +25 -0
- package/dist/admin/ui/HistoryModal.d.ts +23 -0
- package/dist/admin/ui/HistoryModal.js +40 -0
- package/dist/admin/ui/Modal.d.ts +5 -1
- package/dist/admin/ui/Modal.js +3 -3
- package/dist/admin/ui/PublishControl.d.ts +31 -0
- package/dist/admin/ui/PublishControl.js +24 -0
- package/dist/admin/ui/format.d.ts +4 -0
- package/dist/admin/ui/format.js +9 -0
- package/dist/admin/ui/primitives.d.ts +16 -1
- package/dist/admin/ui/primitives.js +20 -4
- package/dist/admin/ui/status-label.d.ts +10 -0
- package/dist/admin/ui/status-label.js +14 -0
- package/dist/admin/ui/theme.js +93 -13
- package/dist/admin/validate.d.ts +27 -2
- package/dist/admin/validate.js +47 -10
- package/dist/collections.d.ts +3 -9
- package/dist/collections.js +1 -2
- package/dist/conditions.d.ts +33 -0
- package/dist/conditions.js +79 -0
- package/dist/config.d.ts +26 -19
- package/dist/config.js +26 -25
- package/dist/css.d.ts +7 -0
- package/dist/css.js +57 -0
- package/dist/custom/CustomSection.d.ts +26 -0
- package/dist/custom/CustomSection.js +20 -0
- package/dist/custom/css.d.ts +3 -0
- package/dist/custom/css.js +28 -0
- package/dist/custom/index.d.ts +26 -0
- package/dist/custom/index.js +16 -0
- package/dist/custom/rows.d.ts +28 -0
- package/dist/custom/rows.js +129 -0
- package/dist/custom/schema.d.ts +32 -0
- package/dist/custom/schema.js +39 -0
- package/dist/entry-store.d.ts +42 -17
- package/dist/entry-store.js +107 -40
- package/dist/fields.d.ts +5 -2
- package/dist/fields.js +4 -2
- package/dist/{shared-id.d.ts → global-id.d.ts} +1 -1
- package/dist/{shared-id.js → global-id.js} +4 -4
- package/dist/global.d.ts +57 -0
- package/dist/global.js +51 -0
- package/dist/index.d.ts +28 -18
- package/dist/index.js +10 -5
- package/dist/media.js +1 -1
- package/dist/next/index.d.ts +1 -1
- package/dist/next/page-renderer.d.ts +4 -2
- package/dist/next/page-renderer.js +17 -13
- package/dist/next/queries.d.ts +6 -6
- package/dist/next/queries.js +7 -7
- package/dist/page.js +2 -2
- package/dist/pairing.d.ts +13 -7
- package/dist/pairing.js +4 -4
- package/dist/refs.d.ts +2 -2
- package/dist/refs.js +4 -4
- package/dist/render.d.ts +13 -1
- package/dist/render.js +10 -5
- package/dist/resolve.d.ts +11 -6
- package/dist/resolve.js +22 -12
- package/dist/revalidate.d.ts +4 -4
- package/dist/revalidate.js +4 -4
- package/dist/richtext-render.js +2 -2
- package/dist/richtext-walk.d.ts +5 -5
- package/dist/richtext-walk.js +4 -4
- package/dist/schema.d.ts +15 -10
- package/dist/schema.js +19 -7
- package/dist/section-wrapper.d.ts +18 -6
- package/dist/section-wrapper.js +24 -17
- package/dist/site.d.ts +17 -5
- package/dist/site.js +36 -12
- package/dist/sql-space.js +1 -1
- package/dist/sql.js +19 -16
- package/dist/store.d.ts +45 -13
- package/dist/store.js +88 -12
- package/dist/styles.d.ts +27 -0
- package/dist/styles.js +60 -0
- package/dist/supabase-entry-store.d.ts +11 -6
- package/dist/supabase-entry-store.js +115 -39
- package/dist/supabase-store.d.ts +13 -6
- package/dist/supabase-store.js +90 -20
- package/dist/zones.d.ts +9 -7
- package/dist/zones.js +5 -5
- package/package.json +1 -1
- package/dist/admin/shared-items.d.ts +0 -9
- package/dist/admin/shell/SharedForm.js +0 -134
- package/dist/shared.d.ts +0 -46
- package/dist/shared.js +0 -36
package/README.md
CHANGED
|
@@ -24,15 +24,73 @@ Implemented:
|
|
|
24
24
|
real component carrying `.schema`; drift between schema and view is a
|
|
25
25
|
compile error at the pairing; style select defaults apply in hard-coded
|
|
26
26
|
JSX use; `Props<typeof schema>` infers required/optional/select types
|
|
27
|
-
- `z.sections([...]).max(n)` / `z.freeform` / `z.locked` — component
|
|
28
|
-
references in, names stored
|
|
27
|
+
- `z.sections([...]).max(n)` / `z.freeform({ sections, custom? })` / `z.locked` — component
|
|
28
|
+
references in, names stored; `custom: true` lets editors add a custom section there
|
|
29
29
|
- `smoodly.toolset` — plain values, element refs serialized to names
|
|
30
30
|
- `smoodly.collection` — descriptors + admin hints; ref thunks resolve
|
|
31
31
|
post-evaluation and infer the target entry shape
|
|
32
|
-
- `defineConfig` — registry name-uniqueness
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
- `defineConfig` — registry name-uniqueness, the `styles` CSS values
|
|
33
|
+
behind Smoodly's styles set (`spacing.sm/md/lg`, `width.narrow/wide/full`;
|
|
34
|
+
package fallbacks fill the rest) and the `css: true` gate for the
|
|
35
|
+
custom-CSS box (spec 2026-09-14)
|
|
36
|
+
- `<smoodly.Section>` — the root wrapper every section renders through:
|
|
37
|
+
`as`, Smoodly's `styles` steps rendered INLINE (`padding-top`,
|
|
38
|
+
`padding-bottom`, `text-align`, `--smd-width`; Default renders nothing),
|
|
39
|
+
the node's scoped `<style>` child (`[data-smd-node="…"]{…}`),
|
|
40
|
+
`data-smd-node` always, every other prop spread onto the root; settings
|
|
41
|
+
are never turned into attributes — the view writes its own
|
|
42
|
+
- Four namespaces on a node (spec 2026-09-14, plan A): `fields`
|
|
43
|
+
(content), `settings` (the developer's knobs — any builder, never
|
|
44
|
+
localized, interpreted by the view; `meta` is gone), `styles`
|
|
45
|
+
(Smoodly's set: `spacingTop`, `spacingBottom`, `width`, `align`, each
|
|
46
|
+
an optional radio — `styles: false | { omit }` on a schema opts out),
|
|
47
|
+
`css` (the snippet; `css: false` on a schema removes the box).
|
|
48
|
+
`styles.ts` holds the descriptors and `inlineStyles`; `css.ts` refuses
|
|
49
|
+
`@import`, `url(`, `expression(`, `behavior:`, `</`, more than 4000
|
|
50
|
+
characters and unbalanced braces — on the escape-decoded snippet too —
|
|
51
|
+
and `safeCss`/`scopedCss` strip and scope at render. Publish validates
|
|
52
|
+
settings and css (`validateNode`); saves stay lenient.
|
|
53
|
+
- Conditional fields (spec 2026-09-14 §12): every builder takes
|
|
54
|
+
`.hidden(f.when(path).is(v))` (`isNot`, `in`, `isEmpty`, `isNotEmpty`);
|
|
55
|
+
a bare path is a sibling, `fields.`/`settings.` crosses namespaces, an
|
|
56
|
+
unknown path throws at registration (`assertConditions`). One
|
|
57
|
+
`isHidden` in `conditions.ts` drives the form and the validator; a
|
|
58
|
+
hidden field's stored value is kept.
|
|
59
|
+
- The custom section (spec 2026-09-14 §6, plan B, 2026-09-15): the
|
|
60
|
+
editor's own block, `Custom section` at the bottom of the "+" picker in
|
|
61
|
+
a `z.freeform({ custom: true })` zone. One flat node `{ type: "custom",
|
|
62
|
+
fields: { rows: [{ spans, gap, align, reverse, columns: [richtext…] }] },
|
|
63
|
+
styles, css? }` — the shape is ordinary descriptors (`custom/schema.ts`,
|
|
64
|
+
the list carries `widget: "rows"`), so `descriptorAt`, the media walk,
|
|
65
|
+
the validator and inline editing understand it with no special case.
|
|
66
|
+
`custom/rows.ts` is the pure reconcile: `renormalize` (integers ≥ 1
|
|
67
|
+
summing to 12, largest remainder), `setColumnCount` (a new column takes
|
|
68
|
+
the last span; a removed column folds into the last survivor, blank
|
|
69
|
+
paragraphs dropped — never silent deletion), `setSpan`, `spanAtX`,
|
|
70
|
+
`presetsFor`, `rowsProblems` (`Spans must sum to 12.`, `Between 1 and 6
|
|
71
|
+
columns.`, `One span per column.`), `columnsLabel`. `custom/css.ts`
|
|
72
|
+
emits the scoped layout CSS from the stored numbers and whitelisted
|
|
73
|
+
keywords only — one grid per row by `data-row`, gap `1rem · 2rem ·
|
|
74
|
+
3rem`, one `@media (max-width: <breakpoint>)` that stacks every row
|
|
75
|
+
(`.smd-row--reverse` flips the stack); `createCustomSection` renders
|
|
76
|
+
through `<smoodly.Section>` with `data-custom`, `.smd-row` /
|
|
77
|
+
`.smd-col` marked `rows.<i>` / `columns.<j>`, richtext through the
|
|
78
|
+
project's bound `RichText` or the root renderer over `elements`, the
|
|
79
|
+
layout `<style>` before the editor's css so the box wins.
|
|
80
|
+
`config.custom: { breakpoint?, toolset?, RichText? }` (default `48rem`
|
|
81
|
+
and the richtext default toolset plus `image`; the breakpoint must be a
|
|
82
|
+
length) resolves onto `config.custom` / `site.custom`; `renderPage`
|
|
83
|
+
registers the renderer under the `custom` type; the resolver and both
|
|
84
|
+
page stores carry the custom schema (`withCustomSchema`,
|
|
85
|
+
`PageStoreOptions.custom`) so column images resolve and count as usage;
|
|
86
|
+
publish runs `validateCustomNode` (structure through `rowsProblems`,
|
|
87
|
+
the columns' richtext, the css). In the admin: `AdminRegistry.custom`
|
|
88
|
+
(one synthetic section), `admin/custom-section.ts`, the picker entry
|
|
89
|
+
`Custom section · Your own rows of columns`, `RowsWidget` (a card per
|
|
90
|
+
row: `Columns` stepper, `Widths` bar with `1/2 · 1/2`-style presets,
|
|
91
|
+
`Gap`, `Vertical align`, `Reverse on mobile`, `Column N` editors,
|
|
92
|
+
`+ Add row`) over `SpanBar` (pointer drag in twelfths, arrow keys),
|
|
93
|
+
`Custom section · N columns` labels, deep-copy on duplicate.
|
|
36
94
|
|
|
37
95
|
- `smoodly.schema.page` / `smoodly.page` / `<Zone>` / `renderPage` — the
|
|
38
96
|
pages runtime: policies normalized to plain data, tree nodes mapped to
|
|
@@ -61,8 +119,10 @@ Implemented:
|
|
|
61
119
|
`pages.tree.depth` capping how deep editors may nest), and expose
|
|
62
120
|
`rename` (a locale's slug/title), `move`, `setTemplate`, `addLocale`,
|
|
63
121
|
`removeLocale`, `resolvePath(locale, path)` and `pathsOf(id)` alongside
|
|
64
|
-
the draft/publish semantics (
|
|
65
|
-
|
|
122
|
+
the draft/publish semantics (one version row per publish plus the live
|
|
123
|
+
draft, rewritten in place; publish moves that locale's pointer and
|
|
124
|
+
freezes the row; discard deletes the abandoned draft; `restore` writes
|
|
125
|
+
an old version as the draft; drafts never leak into published reads). The contract
|
|
66
126
|
is a reusable suite (`test/page-store-contract.ts`); every adapter runs
|
|
67
127
|
the same tests.
|
|
68
128
|
- `SupabasePageStore` — the same id-keyed contract over
|
|
@@ -148,10 +208,11 @@ Implemented:
|
|
|
148
208
|
collection-entry persistence over entries/entry_locales/entry_versions/paths
|
|
149
209
|
— one node, one row per locale, each locale with its own slug, status
|
|
150
210
|
and `.localized()` fields, shared fields on the node, and a per-locale
|
|
151
|
-
version history for
|
|
152
|
-
|
|
211
|
+
version history for every collection (spec 2026-09-14; the 2026-09-06
|
|
212
|
+
opt-in is gone): `create({ locale })`, `update(id, locale, fields)`,
|
|
153
213
|
`setStatus(id, locale, status)`, `addLocale`, `removeLocale`,
|
|
154
|
-
`listVersions
|
|
214
|
+
`listVersions`, `getVersion` and `restore`; the draft row is rewritten
|
|
215
|
+
in place until published (spec 2026-09-13 history); saves rewrite the entry's refs rows over every locale's
|
|
155
216
|
live field set, `referencesTo` answers the reverse
|
|
156
217
|
question, `getMany` is the batched read whose signature is
|
|
157
218
|
`resolveTree`'s `EntryFetcher`, now `(collection, ids, locale, { status })`,
|
|
@@ -249,8 +310,9 @@ Implemented:
|
|
|
249
310
|
derived path as the Path column, and per row its kind, status, sibling
|
|
250
311
|
position and whether it may take children), rendered by
|
|
251
312
|
`admin/shell/PagesList.tsx` with a row menu over the tree ops (New
|
|
252
|
-
subpage, New subpath, Add page here, Rename…,
|
|
253
|
-
|
|
313
|
+
subpage, New subpath, Add page here, Rename…, Delete — reordering is
|
|
314
|
+
the grip's: drag or its arrow keys, since 2026-09-15); a mount a fixed
|
|
315
|
+
page claims opens that index page's editor
|
|
254
316
|
on click like any page row (2026-09-09), a bare mount opens the
|
|
255
317
|
collection list. The editor
|
|
256
318
|
is keyed by id, with a locale switcher loading that locale's own tree
|
|
@@ -295,12 +357,12 @@ Implemented:
|
|
|
295
357
|
`getPage(pathOrId)`, `getEntries("articles", { where, order, limit,
|
|
296
358
|
parent })`, `getEntry("articles", idOrPath)` (an id or a path — a bare
|
|
297
359
|
slug is refused, since its unit would sit under a tag no op fires) and
|
|
298
|
-
`
|
|
360
|
+
`getGlobal("header")`. Typed by the registry (names are checked, fields
|
|
299
361
|
typed), the live draft inside the editor preview, and outside it one
|
|
300
362
|
`unstable_cache` unit per distinct call under a tag the admin ops
|
|
301
363
|
expire: `pages:tree` (every page op that changes the listing),
|
|
302
364
|
`entries:<collection>` (every entry write), `page:<id>`, `entry:<id>`,
|
|
303
|
-
`
|
|
365
|
+
`global:<name>`. `filterPageTree(items, keep)` prunes a tree for a nav
|
|
304
366
|
(a dropped parent drops its subtree).
|
|
305
367
|
- `smoodly/next` — `createPageRenderer`, the site-route seam over that
|
|
306
368
|
layer, bound once by the app like `createAdminOpHandler`. The two route
|
|
@@ -354,18 +416,20 @@ Implemented:
|
|
|
354
416
|
the only way the node gets there — it also covers pages that existed
|
|
355
417
|
before a locked zone was added to their schema.
|
|
356
418
|
|
|
357
|
-
-
|
|
358
|
-
|
|
419
|
+
- Global content (spec 2026-09-07; `shared` → `global` in code
|
|
420
|
+
2026-09-15, spec 2026-09-14 §7): `smoodly.global({ name, title, section })`
|
|
421
|
+
registers a visual item, `smoodly.global({ name, title, fields })` an
|
|
359
422
|
object item; one registration materializes as a singleton entry in
|
|
360
|
-
every supported locale. `site.
|
|
361
|
-
glue's `q.
|
|
362
|
-
renderer's `
|
|
423
|
+
every supported locale. `site.getGlobal(item, { locale })` and the Next
|
|
424
|
+
glue's `q.getGlobal("footer", { locale })` fetch it code-side (the
|
|
425
|
+
renderer's `getSmoodlyGlobal(item, { locale })` still works); the
|
|
363
426
|
editor places a visual item in a zone from the "+" picker as a linked
|
|
364
|
-
`{ type: "
|
|
427
|
+
`{ type: "global", fields: { item, ref } }` node the resolve pass
|
|
365
428
|
rewrites into the section, and `z.locked(footer)` binds a zone to one.
|
|
366
|
-
Edited on its own admin page under
|
|
367
|
-
with "used on N pages" from `refs`. Wired through the `
|
|
368
|
-
on the entry store and the site layer
|
|
429
|
+
Edited on its own admin page under Globals (`/admin/globals/<name>`),
|
|
430
|
+
with "used on N pages" from `refs`. Wired through the `globals` option
|
|
431
|
+
on the entry store and the site layer; a visual item's settings, styles
|
|
432
|
+
and css ride its row under `$settings`, `$styles` and `$css`.
|
|
369
433
|
|
|
370
434
|
- Lists and editable objects (spec 2026-09-07 list field): `f.list(item)`
|
|
371
435
|
holds any builder but another list and refuses a ref or `.localized()`
|
|
@@ -426,6 +490,147 @@ after both are on the registry. From the repo root:
|
|
|
426
490
|
`prepublishOnly` rebuilds each package. The Claude Code skill
|
|
427
491
|
`.claude/skills/releasing-smoodly` carries the same order with its checks.
|
|
428
492
|
|
|
493
|
+
## Follow-ups (logged 2026-09-15, custom section spec §6, plan B)
|
|
494
|
+
|
|
495
|
+
- Inline editing of a column on the canvas is OFF (2026-09-15, after the
|
|
496
|
+
first day's use): a click on a column reveals its editor in the
|
|
497
|
+
inspector (`onFieldClick` in `EditorView.tsx` skips the session for the
|
|
498
|
+
`custom` type). The marks, paths and session code are in place and
|
|
499
|
+
tested; turn it on once a walk on a real device passes.
|
|
500
|
+
- A canvas drag on column edges (today the bar in the inspector).
|
|
501
|
+
- A background for a custom section from a config palette.
|
|
502
|
+
- Elements as direct column children (today through richtext's menus).
|
|
503
|
+
- A per-zone cap on columns (`custom: { columns: 2 }` or similar).
|
|
504
|
+
- `--container` is a soft convention: the rows read
|
|
505
|
+
`var(--smd-width, var(--container, none))` and `var(--smd-gutter, 1rem)`;
|
|
506
|
+
a project without those variables gets full-width rows with a 1rem
|
|
507
|
+
gutter. Documented, not enforced.
|
|
508
|
+
- Presets exist for 2, 3 and 4 columns; 5 and 6 have the bar only.
|
|
509
|
+
- Rows reorder with the ↑/↓ buttons only; no grip, no keyboard drag.
|
|
510
|
+
- An empty column has no box on the live page (an empty paragraph
|
|
511
|
+
renders 0px tall), so it cannot be clicked into from the canvas; the
|
|
512
|
+
inspector's `Column N` editor is the way in. A `min-height` in editor
|
|
513
|
+
mode would give it a target.
|
|
514
|
+
- An empty column publishes (its richtext descriptor is `optional`);
|
|
515
|
+
whether publish should refuse a row of only empty columns is open.
|
|
516
|
+
- Growing a row copies the LAST span before renormalizing, so `[5, 7]` →
|
|
517
|
+
`[3, 5, 4]`, not thirds; a preset is one click away. Equal thirds on
|
|
518
|
+
grow would be the other reasonable rule.
|
|
519
|
+
- The picker's click cannot be unit-tested (no DOM in the suite); the
|
|
520
|
+
walk covers it. A jsdom environment for the admin tests is a future
|
|
521
|
+
tooling item.
|
|
522
|
+
- No migration: the `rows: true` zone flag became `custom: true` and
|
|
523
|
+
`config.rows` is gone; no stored data changed shape.
|
|
524
|
+
- A column carries no project class: the starters extend their `.prose`
|
|
525
|
+
rules to `.smd-col` so headings, figures and callouts inside a column
|
|
526
|
+
read like body text. A project on its own stylesheet does the same, or
|
|
527
|
+
targets `[data-custom]`. A `columnClass` in `config.custom` would
|
|
528
|
+
remove that step.
|
|
529
|
+
|
|
530
|
+
## Follow-ups (logged 2026-09-15, section settings spec, plan A)
|
|
531
|
+
|
|
532
|
+
Upgrade note: stored trees, global entries and element nodes changed
|
|
533
|
+
shape (`styles` → `settings`, `meta` gone, the `global` node type and
|
|
534
|
+
`global:` ids, `$settings`/`$styles`/`$css` on global rows). No migration
|
|
535
|
+
tool — reset and reseed a development database (`supabase db reset`,
|
|
536
|
+
`npm run seed`); no installs predate the change.
|
|
537
|
+
|
|
538
|
+
- A class-mapping API for projects that want the wrapper to emit
|
|
539
|
+
classes instead of inline values (DESIGN.md OQ 13).
|
|
540
|
+
- `f.global()` — a ref field on a section that renders a global item.
|
|
541
|
+
- An AI helper filling the CSS box from a prompt (the box stores CSS,
|
|
542
|
+
never a prompt, so the helper only fills it).
|
|
543
|
+
- `and` / `or` conditions; today one `f.when()` per field.
|
|
544
|
+
- A condition inside a list item can read the item's siblings and the
|
|
545
|
+
top-level namespaces, not another item; per-item scoped `settings.`
|
|
546
|
+
references inside lists are not a thing yet.
|
|
547
|
+
- An element's settings cannot be hidden by the enclosing section's
|
|
548
|
+
settings: an element card's scope is the element's own fields and
|
|
549
|
+
settings.
|
|
550
|
+
- A hidden field's refs still count as usage in the refs index (its value
|
|
551
|
+
is kept, so the edge is real; whether usage should hide with it is open).
|
|
552
|
+
- The css refusal list grew past the spec's verbatim six in a security
|
|
553
|
+
review of the wrapper: the regexes run on the escape-decoded snippet too
|
|
554
|
+
(`\75 rl(` cannot spell `url(`) and unbalanced braces are refused as
|
|
555
|
+
`Braces must balance.` (a `}` cannot step out of the `[data-smd-node]`
|
|
556
|
+
block). Recorded here and in DESIGN.md.
|
|
557
|
+
- The starters' `.prose figure img` is now inline-block without
|
|
558
|
+
`width: 100%`, so a richtext image narrower than the column follows the
|
|
559
|
+
section's `align` step; a photo wider than the column still fills it.
|
|
560
|
+
- The Globals page has no tab row: Content's groups, then a `Settings`
|
|
561
|
+
label and the Settings groups, stacked. A tab row there would match the
|
|
562
|
+
canvas card.
|
|
563
|
+
- `InspectorFields` filters hidden entries per render; a section with
|
|
564
|
+
many conditions re-evaluates every keystroke. Fine at the sizes seen.
|
|
565
|
+
|
|
566
|
+
## Follow-ups (logged 2026-09-14, collections draft/publish spec)
|
|
567
|
+
|
|
568
|
+
- Publish integrity for an entry's own refs: an entry can publish while
|
|
569
|
+
pointing at an unpublished entry; pages are gated, entries are not.
|
|
570
|
+
- A create dialog (title and slug) before "+ New entry", if empty Untitled
|
|
571
|
+
drafts turn out to litter lists.
|
|
572
|
+
- The page editor's publish banner lists fields as plain text; the forms'
|
|
573
|
+
lines focus the widget. Selecting the node from a `node.field` path is
|
|
574
|
+
the missing piece.
|
|
575
|
+
- Discard on a published row from before versioning that nobody has
|
|
576
|
+
written since is refused as "nothing published" (its pointers are null);
|
|
577
|
+
the control never offers it there, so nothing shows — the store could
|
|
578
|
+
answer it as a no-op instead.
|
|
579
|
+
|
|
580
|
+
## Follow-ups (logged 2026-09-13, history spec)
|
|
581
|
+
|
|
582
|
+
- Existing databases (every install so far is ours; DESIGN.md OQ #2):
|
|
583
|
+
`alter table "page_locales" add column "updated_by" text;`
|
|
584
|
+
`alter table "entry_locales" add column "updated_by" text;`
|
|
585
|
+
`alter table "page_versions" rename column "created_at" to "saved_at";`
|
|
586
|
+
`alter table "page_versions" drop column "created_by";`
|
|
587
|
+
`alter table "entry_versions" rename column "created_at" to "saved_at";`
|
|
588
|
+
`alter table "entry_versions" drop column "created_by";`
|
|
589
|
+
The indexes follow the rename. Autosave rows from before read as
|
|
590
|
+
extra history rows until the page is published again; harmless.
|
|
591
|
+
Since 2026-09-14 every collection is versioned and `collectionSQL` emits one
|
|
592
|
+
view shape: re-apply `collectionSQL(collection)` for each collection on an
|
|
593
|
+
existing install (`supabase db reset` locally), or a published row from
|
|
594
|
+
before versioning reads through the view's live-row fallback until its
|
|
595
|
+
first write gives it a snapshot — either way nothing goes dark.
|
|
596
|
+
- Client-side undo in the editor (a stack of tree snapshots); the store
|
|
597
|
+
is no longer an undo stack by design.
|
|
598
|
+
- The author per version row and in the lists; named versions.
|
|
599
|
+
- History on the canvas card's menu and in the list row menus.
|
|
600
|
+
- A diff between two versions.
|
|
601
|
+
- Ops are built per call (`createAdminOps` inside the handler) so the
|
|
602
|
+
session rides in as `actor`; if that ever measures, memoize the
|
|
603
|
+
actor-free parts.
|
|
604
|
+
|
|
605
|
+
## Follow-ups (logged 2026-09-13, publish flow spec)
|
|
606
|
+
|
|
607
|
+
- Unpublishing a page does not cascade: a published child keeps serving
|
|
608
|
+
under an unpublished parent's path. Decide whether it should.
|
|
609
|
+
- Versioning collections by default — decided 2026-09-14: every
|
|
610
|
+
collection, no flag, the forms autosave (spec
|
|
611
|
+
`docs/superpowers/specs/2026-09-14-collections-draft-publish-design.md`).
|
|
612
|
+
- A draft-mode banner on the site, or an "exit preview" action in the
|
|
613
|
+
admin: the same browser shows drafts of unpublished items after an
|
|
614
|
+
Unpublish, which reads as "it published my draft".
|
|
615
|
+
- The lists' row menus (Pages, Entries) do not offer Unpublish; the
|
|
616
|
+
document's own control does.
|
|
617
|
+
|
|
618
|
+
## Follow-ups (logged 2026-09-13, shared inline editing spec)
|
|
619
|
+
|
|
620
|
+
- Layout items in the outline: a "From the layout" group the bridge could
|
|
621
|
+
populate from the `shared:` ids it reports; canvas-only today.
|
|
622
|
+
- The page autosave loses an edit made within the debounce window before
|
|
623
|
+
navigating away (`useDraftAutosave` only clears timers on unmount). The
|
|
624
|
+
shared controller flushes on unmount; the page's should too.
|
|
625
|
+
- A validation failure on a shared save retries with backoff like a
|
|
626
|
+
transport failure; the field error shows on the widget meanwhile. A
|
|
627
|
+
distinct "fix the field" state would stop the retries.
|
|
628
|
+
- The shared card's usage line lists page ids; titles need a pages
|
|
629
|
+
lookup the editor does not hold (the usage rows carry only ids).
|
|
630
|
+
- A "save and publish" action for sites that want no draft stage on a
|
|
631
|
+
shared item (every item is versioned since 2026-09-13; the flag is
|
|
632
|
+
gone).
|
|
633
|
+
|
|
429
634
|
## Follow-ups (logged 2026-09-11, queries spec)
|
|
430
635
|
|
|
431
636
|
- `where` on localized fields (a join on the locale row), ranges and
|
|
@@ -486,10 +691,6 @@ after both are on the registry. From the repo root:
|
|
|
486
691
|
|
|
487
692
|
## Follow-ups (logged 2026-09-10, inline editing spec)
|
|
488
693
|
|
|
489
|
-
- Shared placements ignore field clicks: their fields live in the shared
|
|
490
|
-
item under Globals, which the page editor cannot patch. The starter's
|
|
491
|
-
Header and Footer carry marks already; inline editing of shared content
|
|
492
|
-
needs the shared form's ops behind the canvas.
|
|
493
694
|
- Richtext instant write from the inspector: the pure `RichText` render
|
|
494
695
|
swapped into the container while the widget is typed in.
|
|
495
696
|
- Item-level chrome in the canvas ("Key points · 3 of 4" on hover, add,
|
|
@@ -890,8 +1091,9 @@ the toolbar instead of pushing the content.
|
|
|
890
1091
|
- Instance mode: a shared *type* with many editor-created items; today
|
|
891
1092
|
the answer is a collection plus a section with `f.ref` (decision 1).
|
|
892
1093
|
- Zone-shaped shared items (`smoodly.shared({ zone })`) (decision 2).
|
|
893
|
-
- Standalone live preview on a visual item's admin page —
|
|
894
|
-
preview route in the glue
|
|
1094
|
+
- Standalone live preview on a visual item's admin page — would need a
|
|
1095
|
+
preview route in the glue. Less pressing since 2026-09-13: every shared
|
|
1096
|
+
render is edited in the page canvas, and the Globals page is the form.
|
|
895
1097
|
- Built-in meta on shared items and on placements (anchor per placement
|
|
896
1098
|
is the likely shape).
|
|
897
1099
|
- Per-placement overrides (a shared band with a per-page heading) — out
|
|
@@ -970,8 +1172,9 @@ the toolbar instead of pushing the content.
|
|
|
970
1172
|
- Canvas drag: the overlay should reuse `ui/sortable.ts` across the
|
|
971
1173
|
iframe boundary; `useSortable` measures container children, which the
|
|
972
1174
|
overlay's ghost rows can be.
|
|
973
|
-
- The Pages row menu's Move up / Move down could retire once the grip's
|
|
974
|
-
arrow keys are familiar; they stay for now as the discoverable path
|
|
1175
|
+
- ~~The Pages row menu's Move up / Move down could retire once the grip's
|
|
1176
|
+
arrow keys are familiar; they stay for now as the discoverable path.~~
|
|
1177
|
+
Retired 2026-09-15, with the outline rows' ↑/↓; list-field rows keep theirs.
|
|
975
1178
|
- The grip's arrow keys in the pages tree step one VISIBLE slot with the
|
|
976
1179
|
depth clamped — among siblings in a flat run, but across a depth change
|
|
977
1180
|
the step follows the clamp. Documented as is.
|
|
@@ -1198,10 +1401,9 @@ the toolbar instead of pushing the content.
|
|
|
1198
1401
|
is generated by `packages/smoodly/scripts/emit-sql.ts` from that
|
|
1199
1402
|
script's OWN fixture collections, not from
|
|
1200
1403
|
`examples/site/smoodly/collections.ts`; the create-smoodly-app template
|
|
1201
|
-
migration IS generated from the example. Since
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
the two should share one source.
|
|
1404
|
+
migration IS generated from the example. Since 2026-09-14 both emit the
|
|
1405
|
+
same view shape for every collection, but the repo-root fixture's field
|
|
1406
|
+
lists still drift from the example's — the two should share one source.
|
|
1205
1407
|
- In the entry form, a shared ref field (e.g. an article's author) shows
|
|
1206
1408
|
`—` in a locale where the target is absent, because the picker offers
|
|
1207
1409
|
only targets present in the edited locale; the value is kept on the
|
package/dist/admin/auth.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { AdminOpCall, OpResult } from "./ops.ts";
|
|
2
|
+
/** The verified editor: the Auth user id and the email the locale rows record as `updated_by` (spec 2026-09-13 history §2). */
|
|
3
|
+
export type AdminSession = {
|
|
4
|
+
userId: string;
|
|
5
|
+
email: string | null;
|
|
6
|
+
};
|
|
2
7
|
export type AdminAuth = {
|
|
3
8
|
/** The user behind a session token, or null when the auth project rejects it. */
|
|
4
|
-
verify(token: string): Promise<
|
|
5
|
-
userId: string;
|
|
6
|
-
} | null>;
|
|
9
|
+
verify(token: string): Promise<AdminSession | null>;
|
|
7
10
|
/** Does `editors` hold this user for the current space? */
|
|
8
11
|
isEditor(userId: string): Promise<boolean>;
|
|
9
12
|
};
|
|
10
|
-
export declare function gateAdminOp(auth: AdminAuth, call: AdminOpCall, run: (call: AdminOpCall) => Promise<OpResult<unknown>>): Promise<OpResult<unknown>>;
|
|
13
|
+
export declare function gateAdminOp(auth: AdminAuth, call: AdminOpCall, run: (call: AdminOpCall, session: AdminSession) => Promise<OpResult<unknown>>): Promise<OpResult<unknown>>;
|
package/dist/admin/auth.js
CHANGED
package/dist/admin/client-ops.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const GROUPS = {
|
|
2
|
-
pages: ["list", "get", "create", "saveDraft", "publish", "delete", "rename", "move", "setTemplate", "addLocale", "removeLocale"],
|
|
3
|
-
entries: ["list", "get", "create", "save", "setStatus", "delete", "move", "addLocale", "removeLocale"],
|
|
4
|
-
|
|
2
|
+
pages: ["list", "get", "create", "saveDraft", "publish", "unpublish", "discardDraft", "delete", "rename", "move", "setTemplate", "addLocale", "removeLocale", "listVersions", "restore"],
|
|
3
|
+
entries: ["list", "get", "create", "save", "setStatus", "discardDraft", "delete", "move", "addLocale", "removeLocale", "listVersions", "getVersion", "restore"],
|
|
4
|
+
globals: ["list", "get", "save", "setStatus", "discardDraft", "listVersions", "getVersion", "restore"],
|
|
5
5
|
assets: ["list", "get", "beginUpload", "finishUpload", "update", "delete", "usage"],
|
|
6
6
|
preview: ["enable", "disable"],
|
|
7
7
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AdminRegistry, AdminSection } from "./serialize.ts";
|
|
2
|
+
import type { TreeNode } from "../render.tsx";
|
|
3
|
+
import type { ZonePolicy } from "../zones.ts";
|
|
4
|
+
export declare const customSection: (registry: Pick<AdminRegistry, "custom">) => AdminSection;
|
|
5
|
+
/** Only a freeform zone that says so takes a custom section (the developer's per-zone switch). */
|
|
6
|
+
export declare const allowsCustom: (policy: ZonePolicy | undefined) => boolean;
|
|
7
|
+
/** A fresh node from the registry's sample — deep-copied, so two inserts never share a rows array. */
|
|
8
|
+
export declare function customNode(registry: Pick<AdminRegistry, "custom">, id: string): TreeNode;
|
|
9
|
+
/** The outline, hover and selection label: `Custom section · 2 columns` (the first row's count). */
|
|
10
|
+
export declare const customNodeLabel: (node: TreeNode) => string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { CUSTOM_NODE_TYPE, columnsLabel } from "../custom/index.js";
|
|
2
|
+
export const customSection = (registry) => registry.custom;
|
|
3
|
+
/** Only a freeform zone that says so takes a custom section (the developer's per-zone switch). */
|
|
4
|
+
export const allowsCustom = (policy) => policy?.kind === "freeform" && policy.custom === true;
|
|
5
|
+
/** A fresh node from the registry's sample — deep-copied, so two inserts never share a rows array. */
|
|
6
|
+
export function customNode(registry, id) {
|
|
7
|
+
return { id, type: CUSTOM_NODE_TYPE, fields: structuredClone(registry.custom.sample ?? { rows: [] }) };
|
|
8
|
+
}
|
|
9
|
+
/** The outline, hover and selection label: `Custom section · 2 columns` (the first row's count). */
|
|
10
|
+
export const customNodeLabel = (node) => columnsLabel(node.fields?.rows);
|
|
@@ -5,12 +5,16 @@ export type OverlaySelection = {
|
|
|
5
5
|
node: TreeNode;
|
|
6
6
|
rect: BridgeRect;
|
|
7
7
|
title: string;
|
|
8
|
-
|
|
8
|
+
global: boolean;
|
|
9
9
|
locked: boolean;
|
|
10
|
+
/** Rendered by the site's layout, not placed in a zone: no tree gestures (spec 2026-09-13 §2). */
|
|
11
|
+
layout?: boolean;
|
|
10
12
|
};
|
|
11
|
-
export declare function CanvasOverlay({ emptyZones, hoverRect, selection, plusButtons, fieldHover, editing, onPlus, onMove, onDuplicate, onRemove, }: {
|
|
13
|
+
export declare function CanvasOverlay({ emptyZones, hoverRect, hoverGlobal, selection, plusButtons, fieldHover, editing, onPlus, onMove, onDuplicate, onRemove, }: {
|
|
12
14
|
emptyZones: ZoneRect[];
|
|
13
15
|
hoverRect: BridgeRect | undefined;
|
|
16
|
+
/** The hovered block is a global item: the ring takes the global colour. */
|
|
17
|
+
hoverGlobal?: boolean;
|
|
14
18
|
selection: OverlaySelection | null;
|
|
15
19
|
plusButtons: PlusButton[];
|
|
16
20
|
/** The marked field under the pointer, inside the selected section (spec 2026-09-10 §2). */
|
|
@@ -7,7 +7,7 @@ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-run
|
|
|
7
7
|
// itself never touches the iframe.
|
|
8
8
|
import { useState } from "react";
|
|
9
9
|
import { IconButton } from "../ui/primitives.js";
|
|
10
|
-
export function CanvasOverlay({ emptyZones, hoverRect, selection, plusButtons, fieldHover, editing, onPlus, onMove, onDuplicate, onRemove, }) {
|
|
10
|
+
export function CanvasOverlay({ emptyZones, hoverRect, hoverGlobal, selection, plusButtons, fieldHover, editing, onPlus, onMove, onDuplicate, onRemove, }) {
|
|
11
11
|
const [hoverPlus, setHoverPlus] = useState(null);
|
|
12
12
|
return (_jsxs("div", { style: { position: "absolute", inset: 0, pointerEvents: "none" }, children: [emptyZones.map((z) => (_jsxs("div", { className: "sm-dashed", style: {
|
|
13
13
|
position: "absolute", left: z.x + 12, top: z.y, width: Math.max(0, z.w - 24),
|
|
@@ -15,23 +15,23 @@ export function CanvasOverlay({ emptyZones, hoverRect, selection, plusButtons, f
|
|
|
15
15
|
}, children: [z.zone, " \u00B7 empty"] }, `empty-${z.zone}`))), hoverRect && (_jsx("div", { style: {
|
|
16
16
|
position: "absolute", left: hoverRect.x, top: hoverRect.y,
|
|
17
17
|
width: hoverRect.w, height: hoverRect.h,
|
|
18
|
-
outline:
|
|
18
|
+
outline: `1.5px solid color-mix(in srgb, var(${hoverGlobal ? "--selection-global" : "--selection"}) 45%, transparent)`,
|
|
19
19
|
outlineOffset: -1.5, borderRadius: "var(--selection-radius)",
|
|
20
20
|
} })), fieldHover && !(editing && editing.node === fieldHover.node && editing.path === fieldHover.path) && (_jsx("div", { className: "sm-fieldhover", style: { position: "absolute", left: fieldHover.x, top: fieldHover.y, width: fieldHover.w, height: fieldHover.h } })), editing && (_jsx("div", { className: "sm-fieldedit", style: { position: "absolute", left: editing.x, top: editing.y, width: editing.w, height: editing.h } })), selection && (_jsxs(_Fragment, { children: [_jsx("div", { style: {
|
|
21
21
|
position: "absolute", left: selection.rect.x, top: selection.rect.y,
|
|
22
22
|
width: selection.rect.w, height: selection.rect.h,
|
|
23
|
-
outline: "var(--outline-selection)", outlineOffset: -1.5,
|
|
23
|
+
outline: selection.global ? "var(--outline-selection-global)" : "var(--outline-selection)", outlineOffset: -1.5,
|
|
24
24
|
borderRadius: "var(--selection-radius)",
|
|
25
|
-
} }), _jsxs("span", { className: "sm-blocktag", style: { position: "absolute", left: selection.rect.x, top: selection.rect.y }, children: [selection.title, selection.
|
|
25
|
+
} }), _jsxs("span", { className: selection.global ? "sm-blocktag sm-blocktag--global" : "sm-blocktag", style: { position: "absolute", left: selection.rect.x, top: selection.rect.y }, children: [selection.title, selection.global && " · global", selection.locked && " · locked"] }), !selection.layout && (_jsx("div", { className: "sm-blocktoolbar", style: {
|
|
26
26
|
position: "absolute", left: selection.rect.x + selection.rect.w - 8, top: selection.rect.y + 8,
|
|
27
27
|
transform: "translateX(-100%)", pointerEvents: "auto",
|
|
28
|
-
}, children: selection.locked ? (_jsx("span", { className: "sm-blocktoolbar__note", children: "fields only" })) : (_jsxs(_Fragment, { children: [_jsx(IconButton, { label: "Move up", tone: "inverse", onClick: () => onMove(selection.node.id, -1), children: "\u2191" }), _jsx(IconButton, { label: "Move down", tone: "inverse", onClick: () => onMove(selection.node.id, 1), children: "\u2193" }), _jsx(IconButton, { label: "Duplicate", tone: "inverse", onClick: () => onDuplicate(selection.node), children: "\u29C9" }), _jsx(IconButton, { label: "Remove", tone: "inverse", onClick: () => onRemove(selection.node.id), children: "\u00D7" })] })) })] })), plusButtons.map((p) => {
|
|
28
|
+
}, children: selection.locked ? (_jsx("span", { className: "sm-blocktoolbar__note", children: "fields only" })) : (_jsxs(_Fragment, { children: [_jsx(IconButton, { label: "Move up", tone: "inverse", onClick: () => onMove(selection.node.id, -1), children: "\u2191" }), _jsx(IconButton, { label: "Move down", tone: "inverse", onClick: () => onMove(selection.node.id, 1), children: "\u2193" }), _jsx(IconButton, { label: "Duplicate", tone: "inverse", onClick: () => onDuplicate(selection.node), children: "\u29C9" }), _jsx(IconButton, { label: "Remove", tone: "inverse", onClick: () => onRemove(selection.node.id), children: "\u00D7" })] })) }))] })), plusButtons.map((p) => {
|
|
29
29
|
const key = `${p.zone}-${p.index}`;
|
|
30
30
|
const hot = hoverPlus === key;
|
|
31
31
|
return (_jsxs("div", { style: { position: "absolute", left: 0, right: 0, top: p.y }, children: [hot && _jsx("div", { className: "sm-guide", style: { margin: "0 12px" } }), _jsx("div", { style: {
|
|
32
32
|
position: "absolute", left: "50%", top: 0, transform: "translate(-50%,-50%)",
|
|
33
33
|
display: "flex", flexDirection: "column", alignItems: "center", gap: 6,
|
|
34
34
|
pointerEvents: "auto",
|
|
35
|
-
}, children: _jsxs("button", { className: "sm-plus", "aria-label": `Add to ${p.zone}`, onMouseEnter: () => setHoverPlus(key), onMouseLeave: () => setHoverPlus((k) => (k === key ? null : k)), onClick: (e) => onPlus(p, e.clientX, e.clientY), children: [_jsx("span", { className: "sm-plus__icon", children: "+" }), _jsx("span", { className: "sm-plus__label", children: "Add
|
|
35
|
+
}, children: _jsxs("button", { className: "sm-plus", "aria-label": `Add to ${p.zone}`, onMouseEnter: () => setHoverPlus(key), onMouseLeave: () => setHoverPlus((k) => (k === key ? null : k)), onClick: (e) => onPlus(p, e.clientX, e.clientY), children: [_jsx("span", { className: "sm-plus__icon", children: "+" }), _jsx("span", { className: "sm-plus__label", children: "Add section" })] }) })] }, key));
|
|
36
36
|
})] }));
|
|
37
37
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { AdminOps } from "../ops.ts";
|
|
2
2
|
import type { AdminRegistry } from "../serialize.ts";
|
|
3
3
|
export declare const withEditorParam: (url: string) => string;
|
|
4
|
+
/** The site URL of a stored version — History's iframe (spec 2026-09-13 history §5); no editor parameter, so no bridge. */
|
|
5
|
+
export declare const withVersionParam: (url: string, versionId: string) => string;
|
|
4
6
|
export declare function EditorView({ ops, registry, id }: {
|
|
5
7
|
ops: AdminOps;
|
|
6
8
|
registry: AdminRegistry;
|