smoodly 0.0.16 → 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 +156 -21
- package/dist/admin/client-ops.js +1 -1
- package/dist/admin/custom-section.d.ts +10 -0
- package/dist/admin/custom-section.js +10 -0
- package/dist/admin/editor/CanvasOverlay.d.ts +4 -4
- package/dist/admin/editor/CanvasOverlay.js +5 -5
- package/dist/admin/editor/EditorView.js +140 -108
- package/dist/admin/editor/GlobalInspector.d.ts +24 -0
- package/dist/admin/editor/{SharedInspector.js → GlobalInspector.js} +4 -4
- 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/{shared-edit.d.ts → global-edit.d.ts} +10 -10
- package/dist/admin/editor/{shared-edit.js → global-edit.js} +4 -4
- package/dist/admin/editor/useGlobalEdit.d.ts +6 -0
- package/dist/admin/editor/{useSharedEdit.js → useGlobalEdit.js} +4 -4
- 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 +15 -9
- 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} +17 -16
- package/dist/admin/index.d.ts +3 -3
- package/dist/admin/index.js +1 -1
- package/dist/admin/ops-impl.js +65 -54
- package/dist/admin/ops.d.ts +6 -6
- package/dist/admin/serialize.d.ts +18 -6
- package/dist/admin/serialize.js +27 -4
- package/dist/admin/shell/AdminApp.js +6 -6
- 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/{SharedForm.js → GlobalForm.js} +36 -26
- package/dist/admin/shell/{SharedList.d.ts → GlobalList.d.ts} +1 -1
- package/dist/admin/shell/{SharedList.js → GlobalList.js} +9 -9
- package/dist/admin/shell/IconRail.js +13 -6
- package/dist/admin/shell/PagesList.js +1 -6
- 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/tree-ops.d.ts +10 -8
- package/dist/admin/tree-ops.js +28 -16
- package/dist/admin/ui/HistoryModal.js +1 -1
- package/dist/admin/ui/primitives.d.ts +3 -1
- package/dist/admin/ui/primitives.js +2 -2
- package/dist/admin/ui/theme.js +54 -14
- package/dist/admin/validate.d.ts +27 -2
- package/dist/admin/validate.js +47 -10
- package/dist/collections.d.ts +3 -3
- package/dist/collections.js +1 -1
- 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 +4 -4
- package/dist/entry-store.js +17 -17
- 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 +9 -9
- 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 +15 -5
- package/dist/site.js +24 -10
- package/dist/sql-space.js +1 -1
- package/dist/sql.js +2 -2
- package/dist/store.d.ts +7 -0
- package/dist/store.js +6 -0
- package/dist/styles.d.ts +27 -0
- package/dist/styles.js +60 -0
- package/dist/supabase-entry-store.js +14 -14
- package/dist/supabase-store.js +2 -1
- package/dist/zones.d.ts +9 -7
- package/dist/zones.js +5 -5
- package/package.json +1 -1
- package/dist/admin/editor/SharedInspector.d.ts +0 -23
- package/dist/admin/editor/useSharedEdit.d.ts +0 -6
- package/dist/admin/shared-items.d.ts +0 -9
- package/dist/admin/shell/SharedFields.d.ts +0 -21
- package/dist/admin/shell/SharedFields.js +0 -15
- package/dist/shared.d.ts +0 -53
- package/dist/shared.js +0 -45
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
|
|
@@ -252,8 +310,9 @@ Implemented:
|
|
|
252
310
|
derived path as the Path column, and per row its kind, status, sibling
|
|
253
311
|
position and whether it may take children), rendered by
|
|
254
312
|
`admin/shell/PagesList.tsx` with a row menu over the tree ops (New
|
|
255
|
-
subpage, New subpath, Add page here, Rename…,
|
|
256
|
-
|
|
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
|
|
257
316
|
on click like any page row (2026-09-09), a bare mount opens the
|
|
258
317
|
collection list. The editor
|
|
259
318
|
is keyed by id, with a locale switcher loading that locale's own tree
|
|
@@ -298,12 +357,12 @@ Implemented:
|
|
|
298
357
|
`getPage(pathOrId)`, `getEntries("articles", { where, order, limit,
|
|
299
358
|
parent })`, `getEntry("articles", idOrPath)` (an id or a path — a bare
|
|
300
359
|
slug is refused, since its unit would sit under a tag no op fires) and
|
|
301
|
-
`
|
|
360
|
+
`getGlobal("header")`. Typed by the registry (names are checked, fields
|
|
302
361
|
typed), the live draft inside the editor preview, and outside it one
|
|
303
362
|
`unstable_cache` unit per distinct call under a tag the admin ops
|
|
304
363
|
expire: `pages:tree` (every page op that changes the listing),
|
|
305
364
|
`entries:<collection>` (every entry write), `page:<id>`, `entry:<id>`,
|
|
306
|
-
`
|
|
365
|
+
`global:<name>`. `filterPageTree(items, keep)` prunes a tree for a nav
|
|
307
366
|
(a dropped parent drops its subtree).
|
|
308
367
|
- `smoodly/next` — `createPageRenderer`, the site-route seam over that
|
|
309
368
|
layer, bound once by the app like `createAdminOpHandler`. The two route
|
|
@@ -357,18 +416,20 @@ Implemented:
|
|
|
357
416
|
the only way the node gets there — it also covers pages that existed
|
|
358
417
|
before a locked zone was added to their schema.
|
|
359
418
|
|
|
360
|
-
-
|
|
361
|
-
|
|
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
|
|
362
422
|
object item; one registration materializes as a singleton entry in
|
|
363
|
-
every supported locale. `site.
|
|
364
|
-
glue's `q.
|
|
365
|
-
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
|
|
366
426
|
editor places a visual item in a zone from the "+" picker as a linked
|
|
367
|
-
`{ type: "
|
|
427
|
+
`{ type: "global", fields: { item, ref } }` node the resolve pass
|
|
368
428
|
rewrites into the section, and `z.locked(footer)` binds a zone to one.
|
|
369
|
-
Edited on its own admin page under
|
|
370
|
-
with "used on N pages" from `refs`. Wired through the `
|
|
371
|
-
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`.
|
|
372
433
|
|
|
373
434
|
- Lists and editable objects (spec 2026-09-07 list field): `f.list(item)`
|
|
374
435
|
holds any builder but another list and refuses a ref or `.localized()`
|
|
@@ -429,6 +490,79 @@ after both are on the registry. From the repo root:
|
|
|
429
490
|
`prepublishOnly` rebuilds each package. The Claude Code skill
|
|
430
491
|
`.claude/skills/releasing-smoodly` carries the same order with its checks.
|
|
431
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
|
+
|
|
432
566
|
## Follow-ups (logged 2026-09-14, collections draft/publish spec)
|
|
433
567
|
|
|
434
568
|
- Publish integrity for an entry's own refs: an entry can publish while
|
|
@@ -1038,8 +1172,9 @@ the toolbar instead of pushing the content.
|
|
|
1038
1172
|
- Canvas drag: the overlay should reuse `ui/sortable.ts` across the
|
|
1039
1173
|
iframe boundary; `useSortable` measures container children, which the
|
|
1040
1174
|
overlay's ghost rows can be.
|
|
1041
|
-
- The Pages row menu's Move up / Move down could retire once the grip's
|
|
1042
|
-
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.
|
|
1043
1178
|
- The grip's arrow keys in the pages tree step one VISIBLE slot with the
|
|
1044
1179
|
depth clamped — among siblings in a flat run, but across a depth change
|
|
1045
1180
|
the step follows the clamp. Documented as is.
|
package/dist/admin/client-ops.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const GROUPS = {
|
|
2
2
|
pages: ["list", "get", "create", "saveDraft", "publish", "unpublish", "discardDraft", "delete", "rename", "move", "setTemplate", "addLocale", "removeLocale", "listVersions", "restore"],
|
|
3
3
|
entries: ["list", "get", "create", "save", "setStatus", "discardDraft", "delete", "move", "addLocale", "removeLocale", "listVersions", "getVersion", "restore"],
|
|
4
|
-
|
|
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,16 +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
10
|
/** Rendered by the site's layout, not placed in a zone: no tree gestures (spec 2026-09-13 §2). */
|
|
11
11
|
layout?: boolean;
|
|
12
12
|
};
|
|
13
|
-
export declare function CanvasOverlay({ emptyZones, hoverRect,
|
|
13
|
+
export declare function CanvasOverlay({ emptyZones, hoverRect, hoverGlobal, selection, plusButtons, fieldHover, editing, onPlus, onMove, onDuplicate, onRemove, }: {
|
|
14
14
|
emptyZones: ZoneRect[];
|
|
15
15
|
hoverRect: BridgeRect | undefined;
|
|
16
|
-
/** The hovered block is a
|
|
17
|
-
|
|
16
|
+
/** The hovered block is a global item: the ring takes the global colour. */
|
|
17
|
+
hoverGlobal?: boolean;
|
|
18
18
|
selection: OverlaySelection | null;
|
|
19
19
|
plusButtons: PlusButton[];
|
|
20
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,
|
|
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,14 +15,14 @@ export function CanvasOverlay({ emptyZones, hoverRect, hoverShared, selection, p
|
|
|
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: `1.5px solid color-mix(in srgb, var(${
|
|
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: selection.
|
|
23
|
+
outline: selection.global ? "var(--outline-selection-global)" : "var(--outline-selection)", outlineOffset: -1.5,
|
|
24
24
|
borderRadius: "var(--selection-radius)",
|
|
25
|
-
} }), _jsxs("span", { className: 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
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) => {
|
|
@@ -32,6 +32,6 @@ export function CanvasOverlay({ emptyZones, hoverRect, hoverShared, selection, p
|
|
|
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
|
}
|