smoodly 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +294 -40
  2. package/dist/admin/editor/CanvasOverlay.d.ts +6 -2
  3. package/dist/admin/editor/CanvasOverlay.js +9 -11
  4. package/dist/admin/editor/EditorView.js +135 -28
  5. package/dist/admin/editor/FloatingToolbar.d.ts +16 -0
  6. package/dist/admin/editor/FloatingToolbar.js +14 -0
  7. package/dist/admin/editor/InlineRichtext.d.ts +9 -0
  8. package/dist/admin/editor/InlineRichtext.js +50 -0
  9. package/dist/admin/editor/Outline.d.ts +16 -10
  10. package/dist/admin/editor/Outline.js +13 -10
  11. package/dist/admin/editor/PageInspector.d.ts +21 -0
  12. package/dist/admin/editor/PageInspector.js +24 -0
  13. package/dist/admin/editor/PageSettings.d.ts +1 -3
  14. package/dist/admin/editor/PageSettings.js +7 -6
  15. package/dist/admin/editor/SeoSettings.d.ts +7 -0
  16. package/dist/admin/editor/SeoSettings.js +15 -0
  17. package/dist/admin/editor/inline-dom.d.ts +27 -0
  18. package/dist/admin/editor/inline-dom.js +43 -0
  19. package/dist/admin/editor/inline-session.d.ts +30 -0
  20. package/dist/admin/editor/inline-session.js +19 -0
  21. package/dist/admin/editor/protocol.d.ts +22 -1
  22. package/dist/admin/editor/useCanvasBridge.d.ts +8 -4
  23. package/dist/admin/editor/useCanvasBridge.js +12 -7
  24. package/dist/admin/editor/useInlineSession.d.ts +46 -0
  25. package/dist/admin/editor/useInlineSession.js +244 -0
  26. package/dist/admin/field-paths.d.ts +13 -0
  27. package/dist/admin/field-paths.js +49 -0
  28. package/dist/admin/forms/FieldWidget.d.ts +7 -2
  29. package/dist/admin/forms/FieldWidget.js +35 -43
  30. package/dist/admin/forms/ListField.d.ts +10 -0
  31. package/dist/admin/forms/ListField.js +42 -0
  32. package/dist/admin/forms/MediaWidget.js +10 -3
  33. package/dist/admin/forms/RichtextElementCard.d.ts +3 -0
  34. package/dist/admin/forms/RichtextElementCard.js +37 -0
  35. package/dist/admin/forms/RichtextImageCard.d.ts +3 -0
  36. package/dist/admin/forms/RichtextImageCard.js +42 -0
  37. package/dist/admin/forms/RichtextInsertMenu.d.ts +6 -0
  38. package/dist/admin/forms/RichtextInsertMenu.js +6 -0
  39. package/dist/admin/forms/RichtextPopovers.d.ts +7 -0
  40. package/dist/admin/forms/RichtextPopovers.js +25 -0
  41. package/dist/admin/forms/RichtextToolbar.d.ts +4 -2
  42. package/dist/admin/forms/RichtextToolbar.js +4 -2
  43. package/dist/admin/forms/RichtextWidget.d.ts +4 -1
  44. package/dist/admin/forms/RichtextWidget.js +23 -120
  45. package/dist/admin/forms/elements-context.d.ts +7 -0
  46. package/dist/admin/forms/elements-context.js +13 -0
  47. package/dist/admin/forms/reveal.d.ts +5 -0
  48. package/dist/admin/forms/reveal.js +47 -0
  49. package/dist/admin/forms/richtext-extensions.d.ts +8 -0
  50. package/dist/admin/forms/richtext-extensions.js +11 -0
  51. package/dist/admin/forms/richtext-form-context.d.ts +16 -0
  52. package/dist/admin/forms/richtext-form-context.js +18 -0
  53. package/dist/admin/forms/richtext-insert.d.ts +38 -0
  54. package/dist/admin/forms/richtext-insert.js +37 -0
  55. package/dist/admin/forms/richtext-nodes.d.ts +37 -0
  56. package/dist/admin/forms/richtext-nodes.js +85 -0
  57. package/dist/admin/forms/richtext-paste.js +5 -0
  58. package/dist/admin/forms/tabs.d.ts +3 -0
  59. package/dist/admin/forms/tabs.js +6 -0
  60. package/dist/admin/forms/useRichtextEditor.d.ts +51 -0
  61. package/dist/admin/forms/useRichtextEditor.js +281 -0
  62. package/dist/admin/next/bridge.js +51 -20
  63. package/dist/admin/ops-impl.js +85 -19
  64. package/dist/admin/richtext-doc.d.ts +3 -0
  65. package/dist/admin/richtext-doc.js +12 -0
  66. package/dist/admin/serialize.d.ts +11 -0
  67. package/dist/admin/serialize.js +18 -1
  68. package/dist/admin/shell/AdminApp.js +2 -1
  69. package/dist/admin/shell/EntriesList.js +34 -12
  70. package/dist/admin/shell/EntryForm.js +22 -15
  71. package/dist/admin/shell/IconRail.js +6 -5
  72. package/dist/admin/shell/ListView.d.ts +9 -2
  73. package/dist/admin/shell/ListView.js +7 -6
  74. package/dist/admin/shell/LoginView.js +2 -2
  75. package/dist/admin/shell/PagesList.js +53 -24
  76. package/dist/admin/shell/SharedForm.js +17 -11
  77. package/dist/admin/shell/SharedList.js +5 -3
  78. package/dist/admin/shell/entries-list.d.ts +4 -0
  79. package/dist/admin/shell/entries-list.js +6 -0
  80. package/dist/admin/shell/pages-tree.d.ts +24 -0
  81. package/dist/admin/shell/pages-tree.js +59 -1
  82. package/dist/admin/tree-ops.d.ts +11 -1
  83. package/dist/admin/tree-ops.js +32 -1
  84. package/dist/admin/ui/LocaleSwitcher.d.ts +10 -0
  85. package/dist/admin/ui/LocaleSwitcher.js +28 -8
  86. package/dist/admin/ui/Modal.js +14 -2
  87. package/dist/admin/ui/ResizeHandle.d.ts +1 -0
  88. package/dist/admin/ui/ResizeHandle.js +127 -0
  89. package/dist/admin/ui/primitives.d.ts +38 -4
  90. package/dist/admin/ui/primitives.js +35 -10
  91. package/dist/admin/ui/resize.d.ts +16 -0
  92. package/dist/admin/ui/resize.js +23 -0
  93. package/dist/admin/ui/sortable.d.ts +23 -0
  94. package/dist/admin/ui/sortable.js +57 -0
  95. package/dist/admin/ui/theme.d.ts +3 -1
  96. package/dist/admin/ui/theme.js +527 -165
  97. package/dist/admin/ui/useSortable.d.ts +35 -0
  98. package/dist/admin/ui/useSortable.js +220 -0
  99. package/dist/admin/validate.d.ts +6 -1
  100. package/dist/admin/validate.js +52 -12
  101. package/dist/collections.d.ts +6 -5
  102. package/dist/config.d.ts +1 -0
  103. package/dist/config.js +36 -0
  104. package/dist/entry-store.d.ts +23 -3
  105. package/dist/entry-store.js +31 -5
  106. package/dist/field-defaults.d.ts +8 -0
  107. package/dist/field-defaults.js +18 -0
  108. package/dist/fields.d.ts +30 -1
  109. package/dist/fields.js +33 -3
  110. package/dist/image/SmoodlyImage.d.ts +4 -1
  111. package/dist/image/SmoodlyImage.js +2 -2
  112. package/dist/index.d.ts +16 -7
  113. package/dist/index.js +10 -4
  114. package/dist/marks.d.ts +53 -0
  115. package/dist/marks.js +43 -0
  116. package/dist/media.d.ts +3 -2
  117. package/dist/media.js +48 -6
  118. package/dist/next/index.d.ts +2 -0
  119. package/dist/next/index.js +1 -0
  120. package/dist/next/meta.d.ts +7 -3
  121. package/dist/next/meta.js +18 -9
  122. package/dist/next/page-renderer.js +1 -1
  123. package/dist/next/queries.d.ts +36 -0
  124. package/dist/next/queries.js +87 -0
  125. package/dist/page-tree.d.ts +3 -0
  126. package/dist/page-tree.js +6 -0
  127. package/dist/page.d.ts +11 -3
  128. package/dist/page.js +26 -0
  129. package/dist/pairing.d.ts +2 -1
  130. package/dist/pairing.js +2 -9
  131. package/dist/refs.d.ts +2 -2
  132. package/dist/refs.js +33 -12
  133. package/dist/render.d.ts +14 -7
  134. package/dist/render.js +4 -3
  135. package/dist/resolve.d.ts +12 -2
  136. package/dist/resolve.js +16 -7
  137. package/dist/revalidate.d.ts +6 -0
  138. package/dist/revalidate.js +6 -0
  139. package/dist/richtext/index.d.ts +12 -0
  140. package/dist/richtext/index.js +9 -0
  141. package/dist/richtext-render.d.ts +22 -2
  142. package/dist/richtext-render.js +40 -13
  143. package/dist/richtext-walk.d.ts +44 -0
  144. package/dist/richtext-walk.js +46 -0
  145. package/dist/schema.d.ts +1 -1
  146. package/dist/schema.js +13 -1
  147. package/dist/section-wrapper.js +2 -1
  148. package/dist/shared.d.ts +7 -7
  149. package/dist/site.d.ts +25 -1
  150. package/dist/site.js +89 -22
  151. package/dist/store.d.ts +7 -1
  152. package/dist/store.js +13 -4
  153. package/dist/supabase-entry-store.d.ts +1 -0
  154. package/dist/supabase-entry-store.js +34 -11
  155. package/dist/supabase-store.d.ts +1 -0
  156. package/dist/supabase-store.js +14 -1
  157. package/dist/toolset.d.ts +7 -1
  158. package/dist/toolset.js +13 -4
  159. package/package.json +6 -1
package/README.md CHANGED
@@ -205,8 +205,9 @@ Implemented:
205
205
  `AdminLayout` for the app's `[[...segments]]` route group;
206
206
  `createAdminOpHandler` is the one server action the client ops call),
207
207
  and `smoodly/bridge` (`EditorBridge`, the only piece that ships to the
208
- SITE side — runs inside the draft-route iframe, reports node/zone
209
- geometry, forwards select/hover, executes refresh/scroll). Shell
208
+ SITE side — runs inside the draft-route iframe, reports node, zone and
209
+ marked-field geometry, forwards select/hover with the field under the
210
+ pointer, executes refresh/scroll, reports each committed render). Shell
210
211
  (registry-derived nav, list views, its own client-side router —
211
212
  `shell/router.ts`, `RouteLink`, a leave guard for dirty forms; the
212
213
  shell mounts from the layout and the catch-all page only makes the
@@ -217,8 +218,9 @@ Implemented:
217
218
  canvas — the iframe is the site's own draft route, pixel-identical by
218
219
  construction; every gesture mutates client tree state through pure
219
220
  ops, a debounced `saveDraft` persists, and the bridge refreshes the
220
- iframe on the save ack — refresh-on-change preview, not a live DOM
221
- sync) are all real, in-house. The autosave itself is a framework-free
221
+ iframe on the save ack — refresh-on-change preview for structure; text
222
+ and richtext are edited in place in the canvas and inspector text edits
223
+ land there the same tick, spec 2026-09-10) are all real, in-house. The autosave itself is a framework-free
222
224
  controller (`createDraftAutosave`: debounce, doubling capped retry,
223
225
  pre-publish `flush`) wrapped by `useDraftAutosave`; the controller is
224
226
  unit-tested with fake timers, the hook only for its initial snapshot. Proven end-to-end in
@@ -247,13 +249,18 @@ Implemented:
247
249
  derived path as the Path column, and per row its kind, status, sibling
248
250
  position and whether it may take children), rendered by
249
251
  `admin/shell/PagesList.tsx` with a row menu over the tree ops (New
250
- subpage, New subfolder, Add page here, Rename…, Move up / Move down,
251
- Delete, and Edit index page on a mount a fixed page claims). The editor
252
+ subpage, New subpath, Add page here, Rename…, Move up / Move down,
253
+ Delete); a mount a fixed page claims opens that index page's editor
254
+ on click like any page row (2026-09-09), a bare mount opens the
255
+ collection list. The editor
252
256
  is keyed by id, with a locale switcher loading that locale's own tree
253
257
  and pointing the canvas at `urls[locale]` and, while no block is
254
- selected, page settings (`admin/editor/PageSettings.tsx`: the title and
255
- slug of the ACTIVE locale through `pages.rename`; SEO/meta fields stay
256
- OQ #17). The entry form prefixes its slug field with the collection's
258
+ selected, page settings (`admin/editor/PageInspector.tsx`, spec
259
+ 2026-09-10 page fields: **SEO & Social** — title, description, social
260
+ image, hide from search engines, under the tree's `meta` — and
261
+ **Settings** — the ACTIVE locale's title and slug through `pages.rename`,
262
+ the template's page fields under the tree's `fields`, remove from
263
+ locale). The entry form prefixes its slug field with the collection's
257
264
  mount path. The serialized
258
265
  registry carries `collections[].path`/`depth`, `pages[].slugs` and
259
266
  `pageDepth`, and `defineConfig` validates `homePageSlug` — a real
@@ -274,12 +281,27 @@ Implemented:
274
281
  is the two in sequence — `locate` then `load` — for a caller that just
275
282
  wants the hit. A folder (null template), an
276
283
  unpublished record outside draft mode, or a missing one is `null`.
277
- On top of it the published-only content API for menus, sitemaps and
278
- custom routes: `getPagesTree({ locale })` (nested
279
- `{ id, title, path, hasPage, children }`), `getPage(pathOrId)`,
280
- `getEntries(collection, { locale, parent?, order? })`,
281
- `getEntry(collection, idOrPath)` (an id, a path, or a bare slug naming a
282
- root entry) and `getEntriesTree(collection)`.
284
+ On top of it the content API for menus, sitemaps and custom routes,
285
+ published-only unless `draft: true` (spec 2026-09-11):
286
+ `getPagesTree({ locale, draft? })` (nested
287
+ `{ id, title, path, hasPage, fields, children }`), `getPage(pathOrId)`,
288
+ `getEntries(collection, { locale, parent?, order?, where?, limit?, draft? })`
289
+ — `where` is equality / any-of on shared fields and `limit` a SQL limit,
290
+ both run by the store — `getEntry(collection, idOrPath)` (an id, a
291
+ path, or a bare slug naming a root entry) and `getEntriesTree(collection)`.
292
+ In the app, read through `q` (next bullet) rather than the site layer.
293
+ - `createSmoodlyQueries` (`smoodly/next`, spec 2026-09-11) — the read
294
+ API the scaffold exports as `q`: `getPageTree({ locale })`,
295
+ `getPage(pathOrId)`, `getEntries("articles", { where, order, limit,
296
+ parent })`, `getEntry("articles", idOrPath)` (an id or a path — a bare
297
+ slug is refused, since its unit would sit under a tag no op fires) and
298
+ `getShared("header")`. Typed by the registry (names are checked, fields
299
+ typed), the live draft inside the editor preview, and outside it one
300
+ `unstable_cache` unit per distinct call under a tag the admin ops
301
+ expire: `pages:tree` (every page op that changes the listing),
302
+ `entries:<collection>` (every entry write), `page:<id>`, `entry:<id>`,
303
+ `shared:<name>`. `filterPageTree(items, keep)` prunes a tree for a nav
304
+ (a dropped parent drops its subtree).
283
305
  - `smoodly/next` — `createPageRenderer`, the site-route seam over that
284
306
  layer, bound once by the app like `createAdminOpHandler`. The two route
285
307
  files call `renderSmoodlyPath(segments, { locale?, searchParams? })` and
@@ -320,6 +342,10 @@ Implemented:
320
342
  because Next loads `next.config.ts` in plain Node, where anything that
321
343
  reaches `next/image` fails to resolve (next@16 has no `exports` map);
322
344
  `smoodly/image` still re-exports the helper for code the bundler sees.
345
+ - `smoodly/richtext` — the bound richtext renderer, `createRichText({
346
+ elements, sizes })`: the root `RichText` with the project's elements and
347
+ body images through `<SmoodlyImage>`. Its own entry for the same reason
348
+ as `smoodly/image`: it reaches `next/image`.
323
349
 
324
350
  - Locked zones are usable: `fillLockedZones` (a pure tree op) materializes
325
351
  the one node of every empty locked zone from the component's `sample`
@@ -332,7 +358,8 @@ Implemented:
332
358
  registers a visual item, `smoodly.shared({ name, title, fields })` an
333
359
  object item; one registration materializes as a singleton entry in
334
360
  every supported locale. `site.getShared(item, { locale })` and the Next
335
- glue's `getSmoodlyShared(item, { locale })` fetch it code-side; the
361
+ glue's `q.getShared("footer", { locale })` fetch it code-side (the
362
+ renderer's `getSmoodlyShared(item, { locale })` still works); the
336
363
  editor places a visual item in a zone from the "+" picker as a linked
337
364
  `{ type: "shared", fields: { item, ref } }` node the resolve pass
338
365
  rewrites into the section, and `z.locked(footer)` binds a zone to one.
@@ -362,11 +389,20 @@ Implemented:
362
389
  a toolset forbids survives every round trip. Links carry `href` only. An
363
390
  empty document is missing for a required field. The widget emits plain
364
391
  JSON: ProseMirror's node attrs have a null prototype, which a server
365
- action cannot serialize.
392
+ action cannot serialize. Block nodes (spec 2026-09-08 block nodes): an
393
+ `image` node (media library, caption, alt override) and one generic
394
+ `element` node for every registered Element, always in the schema,
395
+ inserted through the toolbar's "+" or a `/` menu, edited as cards (the
396
+ element card renders its fields through `FieldWidget`, nested richtext
397
+ included). `RichText` takes `elements` and an `image` render prop;
398
+ `smoodly/richtext` exports `createRichText` bound over `<SmoodlyImage>`.
399
+ The media and refs walks descend into documents against the element
400
+ schemas (`elements` beside `sections` in the store, resolve and site
401
+ options).
366
402
 
367
- Not yet: Elements in richtext, drag-and-drop
368
- reordering (move-up/move-down buttons in the editor, Move up / Move down
369
- in the Pages list's row menu), undo/redo, sample/placeholder content
403
+ Not yet: canvas drag-and-drop (list fields, the outline, manual
404
+ collections and the pages tree drag since 2026-09-09 — spec
405
+ `docs/superpowers/specs/2026-09-09-drag-and-drop-design.md`), undo/redo, sample/placeholder content
370
406
  marking, per-locale section visibility conditions (OQ #3 — each locale
371
407
  now has its own tree, so a section's fields are already per locale; what
372
408
  is open is hiding a section by a locale-dependent condition).
@@ -390,6 +426,160 @@ after both are on the registry. From the repo root:
390
426
  `prepublishOnly` rebuilds each package. The Claude Code skill
391
427
  `.claude/skills/releasing-smoodly` carries the same order with its checks.
392
428
 
429
+ ## Follow-ups (logged 2026-09-11, queries spec)
430
+
431
+ - `where` on localized fields (a join on the locale row), ranges and
432
+ negation; any-of over an array-valued field (any-of compares the text
433
+ form of the field, so it covers selects, refs and numbers; on a
434
+ checkboxes, refList or list field `site.getEntries` refuses it rather
435
+ than match nothing — both adapters agree on the text form, so a
436
+ one-element array never matches its element).
437
+ - `where` filters a versioned collection's LIVE row, not its published
438
+ snapshot: an entry whose published version had `kind: "x"` and whose
439
+ draft changed it to `"y"` drops out of `where: { kind: "x" }` before
440
+ the change is published. The published gate is per locale row, the
441
+ filter is on the node's shared fields. Fix by filtering the snapshot
442
+ (a join on the version row) or by documenting that a versioned
443
+ collection's shared fields are live for filtering.
444
+ - A GIN index on `entries.fields` when a customer's collection is large
445
+ enough to need it; additive, no migration.
446
+ - OQ 26: whether page units should carry `pages:tree` too, replacing
447
+ the neighbour list.
448
+ - Scripts and the seed write through the stores and expire nothing; a
449
+ `revalidate` hook for out-of-band writers.
450
+ - `getEntriesTree` has no `q` counterpart yet; add one when a site
451
+ needs a nested collection nav.
452
+ - `getEntry` by id goes through `site.getEntry`, which lists the whole
453
+ collection to find one row; a primary-key read first would be cheaper
454
+ on the uncached path (draft mode, first miss).
455
+
456
+ ## Follow-ups (logged 2026-09-10, page fields and SEO spec)
457
+
458
+ - SEO for entry pages: entries have no `meta`; a `titleField`-based
459
+ title and a description field are the next step.
460
+ - A `styles` block on pages, when a case needs presentation separate
461
+ from content.
462
+ - Refs in page fields, once the refs index walks them.
463
+ - A canonical URL field and a search-result preview snippet in the
464
+ SEO & Social tab.
465
+ - Configurable SEO set (`meta: { seo: { twitter: true } }`) if the four
466
+ values prove too few.
467
+ - Link fields are raw: an image in a page field reaches a nav link as
468
+ `{ asset, alt }`, not a URL. Resolve on demand if a nav ever wants one.
469
+ - The social image URL goes through the default `next` loader at 1200px;
470
+ the cloud's transform loader (OQ 28) should serve a real 1200×630.
471
+ - Page-field labels derive from the key (`showInNavigation` → "Show In
472
+ Navigation"), as every field's do; a `.label()` builder option would
473
+ read better for long keys.
474
+
475
+ ## Follow-ups (logged 2026-09-10, inline editing spec)
476
+
477
+ - Shared placements ignore field clicks: their fields live in the shared
478
+ item under Globals, which the page editor cannot patch. The starter's
479
+ Header and Footer carry marks already; inline editing of shared content
480
+ needs the shared form's ops behind the canvas.
481
+ - Richtext instant write from the inspector: the pure `RichText` render
482
+ swapped into the container while the widget is typed in.
483
+ - Item-level chrome in the canvas ("Key points · 3 of 4" on hover, add,
484
+ remove, reorder in place, later drag within a list): the item marks are
485
+ the hook.
486
+ - Placeholder and validation marks on the page (the sample-sourced
487
+ placeholder treatment, "required, empty" outlines) wait for the tree's
488
+ placeholder marking.
489
+ - A coverage hint for developers: the admin knows a section's text fields
490
+ and could list the ones without a mark.
491
+ - AI verbs ("rewrite this heading") need a field under the cursor, which
492
+ a mark is.
493
+ - A cross-origin admin needs the bridge-side editing variant (spec §3).
494
+ - Direct-child spacing rules on a richtext container (Tailwind `space-y-*`,
495
+ `.prose > p`) do not reach the canvas editor's blocks during a session;
496
+ flex and grid gaps are copied (onto the ProseMirror root since the
497
+ walk, 69aecc6). Documented, not fixed.
498
+ - The text session's Enter commits: a plain text field has no line breaks.
499
+ A multi-line text control would be its own descriptor.
500
+ - The floating toolbar flips below the caret only near the top of the
501
+ DOCUMENT (where the page box clips it); it scrolls with the content, so
502
+ a caret at the top of the scrolled canvas has its toolbar out of view
503
+ until the editor scrolls. A flip keyed to the scroller's viewport would
504
+ fix it.
505
+ - No starter section gives the canvas a richtext with blocks: the Image
506
+ and text body is marks-only, and the article body (the `article`
507
+ toolset) is edited in the entry form. The walk borrowed the article
508
+ toolset for the block steps; a starter section with a long-form body
509
+ would let the canvas show them.
510
+ - The caret placed right after an existing `/` reopens the slash menu
511
+ (TipTap's suggestion re-triggers on the character before the caret).
512
+ Harmless; a trigger keyed to typing only would be stricter.
513
+ - Walk findings, all fixed the same day (69aecc6): the copied flex gap
514
+ landed on the editor root instead of the ProseMirror element that holds
515
+ the blocks, so paragraphs lost their spacing; a click on a marked image
516
+ revealed nothing because the reveal forced the Content tab while the
517
+ media projection puts the image on Media; Escape after a card's button
518
+ had taken the focus did not end the session.
519
+
520
+ ## Follow-ups (logged 2026-09-08, admin restyle)
521
+
522
+ - Remove the temporary `technical` look once the warm restyle is decided:
523
+ the `TECHNICAL_*` strings (frozen tokens plus the `_SHAPE`/`_EXTRA`
524
+ additions), the second `cascade` call and the boot script's
525
+ `smoodly-look` branch in `src/admin/ui/theme.tsx`. The original look
526
+ stays at tag `look-technical`; restore the file from there if the
527
+ restyle is dropped instead.
528
+ - The technical block is a token snapshot, not a pixel restore: shared
529
+ class rules moved with the warm look, so under `data-look="technical"`
530
+ the inspector is flush cards with a hairline instead of one column with
531
+ a sticky head, tabs are stretched, inputs have a 1px (not 1.5px)
532
+ border, rail tiles are 42×34, and a few weights and gaps drifted. Exact
533
+ comparison is the tag.
534
+ - Signed-in walk of the warm look still pending: the sign-in screen was
535
+ checked in the browser pane (light and dark), the list, entry form and
536
+ visual editor were not — the magic-link sign-in carries a token in the
537
+ URL, which the pane's policy blocks. Walk them, then tune: inspector
538
+ width (320px here, the handoff says 296px; the field widgets need the
539
+ room), the canvas meta bar (the handoff has none; ours holds the locale
540
+ switcher), and the block-toolbar note.
541
+ - The handoff's `Styles` tab in the inspector is not built: it exists
542
+ only where a section registers styles, which is the existing `tabsFor`.
543
+ - Wordmark and app icon colour: the wordmark now takes `--logo-color`
544
+ (accent in the warm look); the rail's app icon is hidden in the warm
545
+ look and still brand blue in the technical one. A warm app icon is a
546
+ design question.
547
+
548
+ ## Follow-ups (logged 2026-09-08, block nodes spec)
549
+
550
+ - Image sizing and float in a body (full width, wide, small, left/right):
551
+ a later additive `styles` on the image node; the site's CSS answers it.
552
+ - Inline elements: `inline: true` on the registration; cursor, marks and
553
+ selection around an inline atom are a separate node-view effort.
554
+ - The column context (`contexts: ["column"]`) waits for Phase 3 rows;
555
+ accepted and unused today.
556
+ - A page or entry picker for `f.link()` inside an element — the link
557
+ picker spec.
558
+ - Element copy between fields: a pasted element the target toolset
559
+ forbids is dropped silently; a notice would be kinder.
560
+ - Placeholder marking for sample-populated element fields waits for the
561
+ tree's own placeholder marking.
562
+ - An element's styles are not merged with the config's `styles.elements`
563
+ standard set (sections get `resolvedStyles`); add the merge when the
564
+ set is non-empty somewhere.
565
+ - Element styles are not validated on publish (sections' aren't either).
566
+ - `hasContent` treats an unresolved image node as content, so a body
567
+ whose only image was deleted from the library still passes "required".
568
+ - Publishing a saved version of a versioned entry: the entry form's
569
+ status control returns early when the target equals the current status,
570
+ so a new version of a published entry goes live only by toggling Draft
571
+ then Published (seen in the block-nodes walk; pre-existing). A "Publish
572
+ changes" action when the draft version is ahead of the published one.
573
+ - The insert position is recorded when the "+" menu opens and handed to
574
+ `insertContentAt`, because after the menu or the media picker takes
575
+ focus the refocused contenteditable placed its DOM caret at the first
576
+ card and the editor's selection followed it (the walk's first inserts
577
+ landed at the document top). The explicit position closes the hole;
578
+ the DOM-caret drift itself was not chased to its root.
579
+ - The "+" menu and the slash menu share `RichtextInsertMenu` but keep
580
+ separate active-row state and key handling; one keyboard model would
581
+ be smaller.
582
+
393
583
  ## Follow-ups (logged 2026-09-08, tooling session)
394
584
 
395
585
  - **CI live job red since e09a632, 0.0.11 fresh install broken — fixed
@@ -436,8 +626,6 @@ after both are on the registry. From the repo root:
436
626
  - Video transcoding and adaptive streaming: a Mux or Cloudflare Stream
437
627
  loader behind the `media.loader` seam, if a site ever needs more than
438
628
  an MP4 over Storage's CDN.
439
- - Richtext images: the `image` node from the Phase 0 sketch, using the
440
- media picker once it exists.
441
629
  - Sweeping abandoned `pending` asset rows (an upload that began and
442
630
  never called `finishUpload`); a periodic op or a cleanup on `list`.
443
631
  - A private bucket with signed reads, for sites that must not expose
@@ -503,18 +691,62 @@ after both are on the registry. From the repo root:
503
691
  generated (gitignored), so "templates rebuilt" is a build step, not a
504
692
  diff.
505
693
 
694
+ ## Follow-ups (logged 2026-09-09, admin tweaks)
695
+
696
+ The 2026-09-09 tweak round: view titles sans 18px, the entry and
697
+ Globals forms in one 680px column with a globe-off mark on fields that
698
+ are not `.localized()` (the "Shared across languages" / "In <locale>"
699
+ groups are gone), the image field on a 300px canvas, every locale
700
+ switch a segmented pill (a dropdown past three locales), Media before
701
+ Globals (née Shared) on the rail, richtext paragraph and card spacing,
702
+ element cards collapsed unless blank, and the "+" menu floating under
703
+ the toolbar instead of pushing the content.
704
+
705
+ - `LocaleSwitcher` in pill form has no "+ add" affordance: an absent
706
+ locale reads subtle and switching to it lands on the form's or the
707
+ editor's "doesn't exist yet · Add" pane, which adds. The dropdown form
708
+ (four locales and up) still adds straight from its "Add locale" group,
709
+ so the two forms differ by one click; a "+" inside the pill would close
710
+ that gap.
711
+ - The image field's canvas height (300px) lives twice: `CANVAS_H` in
712
+ `MediaWidget.tsx` sizes the frame, `.sm-media__canvas` /
713
+ `.sm-media__thumb` cap it in the stylesheet. One token would do.
714
+ - `.sm-form>*{max-width:680px}` caps the banner too; if a wide
715
+ field type ever appears (a table), give it an opt-out class.
716
+ - An element card decides "open" from its fields alone (blank = open);
717
+ an element whose fields are all optional and empty by design stays
718
+ open on every load. The richtext image card is never collapsible.
719
+ - The "+" menu anchors to the toolbar's right end (`right:0` in
720
+ `.sm-richtext__head`), which is under the "+" only while the toolbar
721
+ fits on one line; in the 320px inspector the toolbar wraps and the
722
+ menu sits below the wrapped row's end.
723
+ - The technical look's `TECHNICAL_SHAPE` tokens were not touched: its
724
+ title stays sans at `--text-xl`, its label has no mark styling beyond
725
+ the shared `.sm-label__mark` rule.
726
+ - Browser-pane sign-in (2026-09-09): the magic-link hash on
727
+ `/admin/...` was NOT consumed by the admin's Supabase client in the
728
+ pane (the login form stayed); fetching `/auth/v1/user` with the
729
+ access token and writing the session object to `localStorage`
730
+ under `smoodly-auth`, then reloading, signed in. The pane was hidden
731
+ during the walk, so scrolling went through `.sm-form.scrollTop` and
732
+ clicks through `element.click()` in `javascript_tool`.
733
+
506
734
  ## Follow-ups (logged 2026-09-08, richtext widget spec)
507
735
 
508
- - Drag-to-resize the inspector: `--inspector-w` is one variable (380px
509
- since the widget); a handle on the column edge writing it, persisted
510
- per browser like the theme, is the follow-up.
736
+ - ~~Drag-to-resize the inspector~~ — DONE 2026-09-10 (spec
737
+ `docs/superpowers/specs/2026-09-10-inspector-resize-design.md`):
738
+ `ui/ResizeHandle.tsx` writes `--inspector-w` inline on the wrapper,
739
+ clamped to 260px..half the row (`ui/resize.ts`), remembered under
740
+ `smoodly-inspector-w` and applied by the theme's boot script. Walk
741
+ finding: the desktop pane hides its tab on every `navigate`, and a
742
+ hidden tab lays out at zero width, so the handle treats a zero
743
+ measurement as unknown and re-measures on focus. Real drags through
744
+ the pane's `left_click_drag` worked (synthetic `PointerEvent`s cannot
745
+ satisfy `setPointerCapture`); coordinates are the screenshot frame,
746
+ 0.625 × CSS px at the pane's 1280 width.
511
747
  - A page and entry picker in the link popover: a link inside a richtext
512
748
  document is a reference the refs index, the resolver and the localizer
513
749
  cannot see (they address refs by top-level key). Its own spec.
514
- - Elements in richtext: a registered Element as a TipTap node view with a
515
- small form for its fields, rendered on the site by `RichText` through
516
- the registry. The next spec.
517
- - Images in richtext: the Phase 0 sketch's `image` node; waits for media.
518
750
  - Escape inside any inspector control bubbles to the editor's window
519
751
  handler and clears the block selection — the link popover stops it, a
520
752
  textarea or the TipTap content does not. A `keydown` guard on
@@ -546,9 +778,6 @@ after both are on the registry. From the repo root:
546
778
  in the single-language scaffold). Adding `locale` to `PageContext` and
547
779
  `EntryPageProps` would remove that dependency and let sections that
548
780
  format dates stay synchronous.
549
- - The content API is published-only, so the Articles index in the editor
550
- canvas lists published articles while the hero above it is the draft.
551
- A draft-aware `getEntries` for draft mode is the fix if that confuses.
552
781
  - The image widget edits the URL only; `alt` is set by the seed and cannot
553
782
  be changed in the admin.
554
783
  - A per-locale collection path or fixed slug must name the default locale;
@@ -607,7 +836,7 @@ after both are on the registry. From the repo root:
607
836
  the header nav rows read `/`, `/posts` — where the label would identify
608
837
  the row; `itemSummary` could prefer a `label`/`title`/`heading` key
609
838
  (walk, 2026-09-07).
610
- - New page / New folder ask for the slug through `window.prompt`, which
839
+ - New page / New path (a folder, "path" in the UI since 2026-09-09) ask for the slug through `window.prompt`, which
611
840
  browser automation blocks and which cannot validate as you type; an
612
841
  inline field would do both (walk, 2026-09-07).
613
842
 
@@ -719,16 +948,43 @@ after both are on the registry. From the repo root:
719
948
  `node:crypto`, etc. from a module reachable through the root would
720
949
  regress it silently.
721
950
 
951
+ ## Follow-ups (logged 2026-09-09)
952
+
953
+ - Cross-zone drag in the outline: each zone is its own sortable list;
954
+ moving a block between zones is still insert-and-remove.
955
+ - Nested manual collections show no grip (`canReorder` requires
956
+ `depth === 1`) until the entry tree UI exists — a flat index is not a
957
+ sibling index there.
958
+ - Canvas drag: the overlay should reuse `ui/sortable.ts` across the
959
+ iframe boundary; `useSortable` measures container children, which the
960
+ overlay's ghost rows can be.
961
+ - The Pages row menu's Move up / Move down could retire once the grip's
962
+ arrow keys are familiar; they stay for now as the discoverable path.
963
+ - The grip's arrow keys in the pages tree step one VISIBLE slot with the
964
+ depth clamped — among siblings in a flat run, but across a depth change
965
+ the step follows the clamp. Documented as is.
966
+ - A dragged subtree stays collapsed after the drop (`onDragStart`
967
+ collapses it through `toggle` and nothing re-expands it).
968
+ - Walk findings (2026-09-09, every surface walked signed-in; the pointer
969
+ path with synthetic pointer events, autoscroll in Playwright because
970
+ the desktop pane pauses animation frames while hidden):
971
+ - FIXED: the lifted row's transform grew the scroller's overflow, so
972
+ the bottom-edge autoscroll chased it forever and the list scrolled
973
+ away under the pointer; `clampOffset` now holds the transform inside
974
+ the rows' extent while the slot keeps following the pointer.
975
+ - A depth-1 row dragged to the very end of the tree with no drift
976
+ projects onto the last root row's child slot, which a mount or the
977
+ home page refuses (muted row); the editor must drift left 20px to
978
+ land at the root. By spec §4 (depth = own depth + drift), noted as a
979
+ discoverability question for the canvas-drag round.
980
+ - The rows are held in place until the reload lands (checked with a
981
+ 1.2 s fetch delay): no flicker on a fast stack either.
982
+
722
983
  ## Follow-ups (logged 2026-09-06)
723
984
 
724
- - Drag-and-drop in the Pages list — the ops it needs exist
725
- (`pages.move(id, { parentId, index })`); the row menu's Move up / Move
726
- down is the interim.
727
985
  - Nested entries have no admin UI beyond `entries.move`: nothing creates
728
986
  a child entry or shows the entry tree, and the entry form's slug prefix
729
987
  is the collection's mount base only, not the parent chain.
730
- - SEO/meta fields in page settings — DESIGN.md OQ #17. Title and slug per
731
- locale shipped; nothing writes `PageTree.meta` yet.
732
988
  - `pages.get` makes four sequential store round trips for a non-root page
733
989
  (`getPage`, `getDraftTree`, `pathsOf`, and the parent read behind
734
990
  `parentLocales`; three for a root, which has no parent to read), and the
@@ -817,8 +1073,6 @@ after both are on the registry. From the repo root:
817
1073
  - `EditorView.tsx` is ~850 lines now that it also holds the locale switch,
818
1074
  add and remove flows — see the 2026-09-03 follow-up below for the seams
819
1075
  (that entry is the one to act on).
820
- - The `LocaleSwitcher` is a native `<select>`; a `Segmented` with an
821
- "+ add" affordance would match the Pages list.
822
1076
  - `i18n/request.ts` returns `{ locale, messages: {} }`. next-intl 4.14.2
823
1077
  throws `No messages found` when the provider inherits none, and the
824
1078
  example has no UI strings of its own, so the object is empty; a
@@ -1,6 +1,6 @@
1
1
  import type { TreeNode } from "../../render.tsx";
2
2
  import type { PlusButton } from "./overlay-geometry.ts";
3
- import type { BridgeRect, ZoneRect } from "./protocol.ts";
3
+ import type { BridgeRect, FieldRect, ZoneRect } from "./protocol.ts";
4
4
  export type OverlaySelection = {
5
5
  node: TreeNode;
6
6
  rect: BridgeRect;
@@ -8,11 +8,15 @@ export type OverlaySelection = {
8
8
  shared: boolean;
9
9
  locked: boolean;
10
10
  };
11
- export declare function CanvasOverlay({ emptyZones, hoverRect, selection, plusButtons, onPlus, onMove, onDuplicate, onRemove, }: {
11
+ export declare function CanvasOverlay({ emptyZones, hoverRect, selection, plusButtons, fieldHover, editing, onPlus, onMove, onDuplicate, onRemove, }: {
12
12
  emptyZones: ZoneRect[];
13
13
  hoverRect: BridgeRect | undefined;
14
14
  selection: OverlaySelection | null;
15
15
  plusButtons: PlusButton[];
16
+ /** The marked field under the pointer, inside the selected section (spec 2026-09-10 §2). */
17
+ fieldHover?: FieldRect;
18
+ /** The field with an active inline session. */
19
+ editing?: FieldRect;
16
20
  /** A "+" was clicked, at these viewport coordinates. */
17
21
  onPlus(button: PlusButton, clientX: number, clientY: number): void;
18
22
  onMove(id: string, dir: -1 | 1): void;
@@ -7,33 +7,31 @@ 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, onPlus, onMove, onDuplicate, onRemove, }) {
10
+ export function CanvasOverlay({ emptyZones, hoverRect, selection, plusButtons, fieldHover, editing, onPlus, onMove, onDuplicate, onRemove, }) {
11
11
  const [hoverPlus, setHoverPlus] = useState(null);
12
- return (_jsxs("div", { style: { position: "absolute", inset: 0, pointerEvents: "none" }, children: [emptyZones.map((z) => (_jsxs("div", { className: "sm-dashed sm-mono", style: {
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),
14
- height: Math.max(56, z.h), fontSize: "var(--text-xs)",
15
- letterSpacing: ".04em", textTransform: "uppercase", color: "var(--gray-500)",
16
- borderColor: "var(--gray-300)",
14
+ height: Math.max(56, z.h),
17
15
  }, children: [z.zone, " \u00B7 empty"] }, `empty-${z.zone}`))), hoverRect && (_jsx("div", { style: {
18
16
  position: "absolute", left: hoverRect.x, top: hoverRect.y,
19
17
  width: hoverRect.w, height: hoverRect.h,
20
18
  outline: "1.5px solid color-mix(in srgb, var(--selection) 45%, transparent)",
21
- outlineOffset: -1.5, borderRadius: "var(--radius-sm)",
22
- } })), selection && (_jsxs(_Fragment, { children: [_jsx("div", { style: {
19
+ outlineOffset: -1.5, borderRadius: "var(--selection-radius)",
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: {
23
21
  position: "absolute", left: selection.rect.x, top: selection.rect.y,
24
22
  width: selection.rect.w, height: selection.rect.h,
25
23
  outline: "var(--outline-selection)", outlineOffset: -1.5,
26
- borderRadius: "var(--radius-sm)",
24
+ borderRadius: "var(--selection-radius)",
27
25
  } }), _jsxs("span", { className: "sm-blocktag", style: { position: "absolute", left: selection.rect.x, top: selection.rect.y }, children: [selection.title, selection.shared && " · shared", selection.locked && " · locked"] }), _jsx("div", { className: "sm-blocktoolbar", style: {
28
26
  position: "absolute", left: selection.rect.x + selection.rect.w - 8, top: selection.rect.y + 8,
29
27
  transform: "translateX(-100%)", pointerEvents: "auto",
30
- }, children: selection.locked ? (_jsx("span", { className: "sm-mono", style: { fontSize: "var(--text-xs)", color: "#8C94A8", padding: "4px 8px" }, 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) => {
31
29
  const key = `${p.zone}-${p.index}`;
32
30
  const hot = hoverPlus === key;
33
- return (_jsxs("div", { style: { position: "absolute", left: 0, right: 0, top: p.y }, children: [hot && _jsx("div", { className: "sm-guide", style: { margin: "0 12px" } }), _jsxs("div", { style: {
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: {
34
32
  position: "absolute", left: "50%", top: 0, transform: "translate(-50%,-50%)",
35
33
  display: "flex", flexDirection: "column", alignItems: "center", gap: 6,
36
34
  pointerEvents: "auto",
37
- }, children: [_jsx("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: "+" }), hot && _jsxs("span", { className: "sm-tooltip", children: ["Add to ", p.zone] })] })] }, 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 block" })] }) })] }, key));
38
36
  })] }));
39
37
  }