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.
Files changed (133) hide show
  1. package/README.md +156 -21
  2. package/dist/admin/client-ops.js +1 -1
  3. package/dist/admin/custom-section.d.ts +10 -0
  4. package/dist/admin/custom-section.js +10 -0
  5. package/dist/admin/editor/CanvasOverlay.d.ts +4 -4
  6. package/dist/admin/editor/CanvasOverlay.js +5 -5
  7. package/dist/admin/editor/EditorView.js +140 -108
  8. package/dist/admin/editor/GlobalInspector.d.ts +24 -0
  9. package/dist/admin/editor/{SharedInspector.js → GlobalInspector.js} +4 -4
  10. package/dist/admin/editor/InsertPicker.d.ts +6 -5
  11. package/dist/admin/editor/InsertPicker.js +6 -4
  12. package/dist/admin/editor/InspectorFields.d.ts +21 -0
  13. package/dist/admin/editor/InspectorFields.js +22 -0
  14. package/dist/admin/editor/Outline.d.ts +0 -1
  15. package/dist/admin/editor/Outline.js +3 -3
  16. package/dist/admin/editor/{shared-edit.d.ts → global-edit.d.ts} +10 -10
  17. package/dist/admin/editor/{shared-edit.js → global-edit.js} +4 -4
  18. package/dist/admin/editor/useGlobalEdit.d.ts +6 -0
  19. package/dist/admin/editor/{useSharedEdit.js → useGlobalEdit.js} +4 -4
  20. package/dist/admin/forms/FieldWidget.d.ts +14 -1
  21. package/dist/admin/forms/FieldWidget.js +38 -11
  22. package/dist/admin/forms/ListField.d.ts +9 -1
  23. package/dist/admin/forms/ListField.js +16 -8
  24. package/dist/admin/forms/RichtextElementCard.js +11 -5
  25. package/dist/admin/forms/RowsWidget.d.ts +13 -0
  26. package/dist/admin/forms/RowsWidget.js +26 -0
  27. package/dist/admin/forms/SpanBar.d.ts +5 -0
  28. package/dist/admin/forms/SpanBar.js +37 -0
  29. package/dist/admin/forms/richtext-insert.d.ts +2 -2
  30. package/dist/admin/forms/richtext-insert.js +3 -3
  31. package/dist/admin/forms/richtext-nodes.js +1 -1
  32. package/dist/admin/forms/tabs.d.ts +15 -9
  33. package/dist/admin/forms/tabs.js +19 -23
  34. package/dist/admin/forms/useRichtextEditor.js +8 -1
  35. package/dist/admin/global-items.d.ts +9 -0
  36. package/dist/admin/{shared-items.js → global-items.js} +17 -16
  37. package/dist/admin/index.d.ts +3 -3
  38. package/dist/admin/index.js +1 -1
  39. package/dist/admin/ops-impl.js +65 -54
  40. package/dist/admin/ops.d.ts +6 -6
  41. package/dist/admin/serialize.d.ts +18 -6
  42. package/dist/admin/serialize.js +27 -4
  43. package/dist/admin/shell/AdminApp.js +6 -6
  44. package/dist/admin/shell/GlobalFields.d.ts +24 -0
  45. package/dist/admin/shell/GlobalFields.js +19 -0
  46. package/dist/admin/shell/{SharedForm.d.ts → GlobalForm.d.ts} +1 -1
  47. package/dist/admin/shell/{SharedForm.js → GlobalForm.js} +36 -26
  48. package/dist/admin/shell/{SharedList.d.ts → GlobalList.d.ts} +1 -1
  49. package/dist/admin/shell/{SharedList.js → GlobalList.js} +9 -9
  50. package/dist/admin/shell/IconRail.js +13 -6
  51. package/dist/admin/shell/PagesList.js +1 -6
  52. package/dist/admin/shell/{shared-list.d.ts → global-list.d.ts} +3 -3
  53. package/dist/admin/shell/{shared-list.js → global-list.js} +2 -2
  54. package/dist/admin/tree-ops.d.ts +10 -8
  55. package/dist/admin/tree-ops.js +28 -16
  56. package/dist/admin/ui/HistoryModal.js +1 -1
  57. package/dist/admin/ui/primitives.d.ts +3 -1
  58. package/dist/admin/ui/primitives.js +2 -2
  59. package/dist/admin/ui/theme.js +54 -14
  60. package/dist/admin/validate.d.ts +27 -2
  61. package/dist/admin/validate.js +47 -10
  62. package/dist/collections.d.ts +3 -3
  63. package/dist/collections.js +1 -1
  64. package/dist/conditions.d.ts +33 -0
  65. package/dist/conditions.js +79 -0
  66. package/dist/config.d.ts +26 -19
  67. package/dist/config.js +26 -25
  68. package/dist/css.d.ts +7 -0
  69. package/dist/css.js +57 -0
  70. package/dist/custom/CustomSection.d.ts +26 -0
  71. package/dist/custom/CustomSection.js +20 -0
  72. package/dist/custom/css.d.ts +3 -0
  73. package/dist/custom/css.js +28 -0
  74. package/dist/custom/index.d.ts +26 -0
  75. package/dist/custom/index.js +16 -0
  76. package/dist/custom/rows.d.ts +28 -0
  77. package/dist/custom/rows.js +129 -0
  78. package/dist/custom/schema.d.ts +32 -0
  79. package/dist/custom/schema.js +39 -0
  80. package/dist/entry-store.d.ts +4 -4
  81. package/dist/entry-store.js +17 -17
  82. package/dist/fields.d.ts +5 -2
  83. package/dist/fields.js +4 -2
  84. package/dist/{shared-id.d.ts → global-id.d.ts} +1 -1
  85. package/dist/{shared-id.js → global-id.js} +4 -4
  86. package/dist/global.d.ts +57 -0
  87. package/dist/global.js +51 -0
  88. package/dist/index.d.ts +28 -18
  89. package/dist/index.js +10 -5
  90. package/dist/media.js +1 -1
  91. package/dist/next/index.d.ts +1 -1
  92. package/dist/next/page-renderer.d.ts +4 -2
  93. package/dist/next/page-renderer.js +9 -9
  94. package/dist/next/queries.d.ts +6 -6
  95. package/dist/next/queries.js +7 -7
  96. package/dist/page.js +2 -2
  97. package/dist/pairing.d.ts +13 -7
  98. package/dist/pairing.js +4 -4
  99. package/dist/refs.d.ts +2 -2
  100. package/dist/refs.js +4 -4
  101. package/dist/render.d.ts +13 -1
  102. package/dist/render.js +10 -5
  103. package/dist/resolve.d.ts +11 -6
  104. package/dist/resolve.js +22 -12
  105. package/dist/revalidate.d.ts +4 -4
  106. package/dist/revalidate.js +4 -4
  107. package/dist/richtext-render.js +2 -2
  108. package/dist/richtext-walk.d.ts +5 -5
  109. package/dist/richtext-walk.js +4 -4
  110. package/dist/schema.d.ts +15 -10
  111. package/dist/schema.js +19 -7
  112. package/dist/section-wrapper.d.ts +18 -6
  113. package/dist/section-wrapper.js +24 -17
  114. package/dist/site.d.ts +15 -5
  115. package/dist/site.js +24 -10
  116. package/dist/sql-space.js +1 -1
  117. package/dist/sql.js +2 -2
  118. package/dist/store.d.ts +7 -0
  119. package/dist/store.js +6 -0
  120. package/dist/styles.d.ts +27 -0
  121. package/dist/styles.js +60 -0
  122. package/dist/supabase-entry-store.js +14 -14
  123. package/dist/supabase-store.js +2 -1
  124. package/dist/zones.d.ts +9 -7
  125. package/dist/zones.js +5 -5
  126. package/package.json +1 -1
  127. package/dist/admin/editor/SharedInspector.d.ts +0 -23
  128. package/dist/admin/editor/useSharedEdit.d.ts +0 -6
  129. package/dist/admin/shared-items.d.ts +0 -9
  130. package/dist/admin/shell/SharedFields.d.ts +0 -21
  131. package/dist/admin/shell/SharedFields.js +0 -15
  132. package/dist/shared.d.ts +0 -53
  133. 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 + the automatic standard-style
33
- merge (`resolvedStyles`: section keys win, `styleDefaults: false|{omit}`)
34
- - `<smoodly.Section>` — root wrapper: `as`, string styles → kebab-cased
35
- data attributes, className/style passthrough
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…, Move up / Move down,
256
- Delete); a mount a fixed page claims opens that index page's editor
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
- `getShared("header")`. Typed by the registry (names are checked, fields
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
- `shared:<name>`. `filterPageTree(items, keep)` prunes a tree for a nav
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
- - Shared content (spec 2026-09-07): `smoodly.shared({ name, title, section })`
361
- registers a visual item, `smoodly.shared({ name, title, fields })` an
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.getShared(item, { locale })` and the Next
364
- glue's `q.getShared("footer", { locale })` fetch it code-side (the
365
- renderer's `getSmoodlyShared(item, { locale })` still works); the
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: "shared", fields: { item, ref } }` node the resolve pass
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 Shared content (`/admin/shared/<name>`),
370
- with "used on N pages" from `refs`. Wired through the `shared` option
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.
@@ -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
- shared: ["list", "get", "save", "setStatus", "discardDraft", "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,16 +5,16 @@ export type OverlaySelection = {
5
5
  node: TreeNode;
6
6
  rect: BridgeRect;
7
7
  title: string;
8
- shared: boolean;
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, hoverShared, selection, plusButtons, fieldHover, editing, onPlus, onMove, onDuplicate, onRemove, }: {
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 shared item: the ring takes the shared colour. */
17
- hoverShared?: boolean;
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, hoverShared, 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,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(${hoverShared ? "--selection-shared" : "--selection"}) 45%, transparent)`,
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.shared ? "var(--outline-selection-shared)" : "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: selection.shared ? "sm-blocktag sm-blocktag--shared" : "sm-blocktag", style: { position: "absolute", left: selection.rect.x, top: selection.rect.y }, children: [selection.title, selection.shared && " · shared", selection.locked && " · locked"] }), !selection.layout && (_jsx("div", { className: "sm-blocktoolbar", style: {
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 block" })] }) })] }, key));
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
  }