smoodly 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +259 -38
  2. package/dist/admin/editor/CanvasOverlay.d.ts +6 -2
  3. package/dist/admin/editor/CanvasOverlay.js +9 -11
  4. package/dist/admin/editor/EditorView.js +130 -25
  5. package/dist/admin/editor/FloatingToolbar.d.ts +16 -0
  6. package/dist/admin/editor/FloatingToolbar.js +14 -0
  7. package/dist/admin/editor/InlineRichtext.d.ts +9 -0
  8. package/dist/admin/editor/InlineRichtext.js +50 -0
  9. package/dist/admin/editor/Outline.d.ts +16 -10
  10. package/dist/admin/editor/Outline.js +13 -10
  11. package/dist/admin/editor/inline-dom.d.ts +27 -0
  12. package/dist/admin/editor/inline-dom.js +43 -0
  13. package/dist/admin/editor/inline-session.d.ts +30 -0
  14. package/dist/admin/editor/inline-session.js +19 -0
  15. package/dist/admin/editor/protocol.d.ts +22 -1
  16. package/dist/admin/editor/useCanvasBridge.d.ts +8 -4
  17. package/dist/admin/editor/useCanvasBridge.js +12 -7
  18. package/dist/admin/editor/useInlineSession.d.ts +46 -0
  19. package/dist/admin/editor/useInlineSession.js +244 -0
  20. package/dist/admin/field-paths.d.ts +13 -0
  21. package/dist/admin/field-paths.js +49 -0
  22. package/dist/admin/forms/FieldWidget.d.ts +7 -2
  23. package/dist/admin/forms/FieldWidget.js +12 -40
  24. package/dist/admin/forms/ListField.d.ts +10 -0
  25. package/dist/admin/forms/ListField.js +42 -0
  26. package/dist/admin/forms/MediaWidget.js +10 -3
  27. package/dist/admin/forms/RichtextElementCard.d.ts +3 -0
  28. package/dist/admin/forms/RichtextElementCard.js +37 -0
  29. package/dist/admin/forms/RichtextImageCard.d.ts +3 -0
  30. package/dist/admin/forms/RichtextImageCard.js +42 -0
  31. package/dist/admin/forms/RichtextInsertMenu.d.ts +6 -0
  32. package/dist/admin/forms/RichtextInsertMenu.js +6 -0
  33. package/dist/admin/forms/RichtextPopovers.d.ts +7 -0
  34. package/dist/admin/forms/RichtextPopovers.js +25 -0
  35. package/dist/admin/forms/RichtextToolbar.d.ts +4 -2
  36. package/dist/admin/forms/RichtextToolbar.js +4 -2
  37. package/dist/admin/forms/RichtextWidget.d.ts +4 -1
  38. package/dist/admin/forms/RichtextWidget.js +23 -120
  39. package/dist/admin/forms/elements-context.d.ts +7 -0
  40. package/dist/admin/forms/elements-context.js +13 -0
  41. package/dist/admin/forms/reveal.d.ts +5 -0
  42. package/dist/admin/forms/reveal.js +47 -0
  43. package/dist/admin/forms/richtext-extensions.d.ts +8 -0
  44. package/dist/admin/forms/richtext-extensions.js +11 -0
  45. package/dist/admin/forms/richtext-form-context.d.ts +16 -0
  46. package/dist/admin/forms/richtext-form-context.js +18 -0
  47. package/dist/admin/forms/richtext-insert.d.ts +38 -0
  48. package/dist/admin/forms/richtext-insert.js +37 -0
  49. package/dist/admin/forms/richtext-nodes.d.ts +37 -0
  50. package/dist/admin/forms/richtext-nodes.js +85 -0
  51. package/dist/admin/forms/richtext-paste.js +5 -0
  52. package/dist/admin/forms/tabs.d.ts +3 -0
  53. package/dist/admin/forms/tabs.js +6 -0
  54. package/dist/admin/forms/useRichtextEditor.d.ts +51 -0
  55. package/dist/admin/forms/useRichtextEditor.js +281 -0
  56. package/dist/admin/next/bridge.js +51 -20
  57. package/dist/admin/ops-impl.js +13 -8
  58. package/dist/admin/richtext-doc.d.ts +3 -0
  59. package/dist/admin/richtext-doc.js +12 -0
  60. package/dist/admin/serialize.d.ts +7 -0
  61. package/dist/admin/serialize.js +16 -1
  62. package/dist/admin/shell/AdminApp.js +2 -1
  63. package/dist/admin/shell/EntriesList.js +34 -12
  64. package/dist/admin/shell/EntryForm.js +22 -15
  65. package/dist/admin/shell/IconRail.js +6 -5
  66. package/dist/admin/shell/ListView.d.ts +9 -2
  67. package/dist/admin/shell/ListView.js +7 -6
  68. package/dist/admin/shell/LoginView.js +2 -2
  69. package/dist/admin/shell/PagesList.js +53 -24
  70. package/dist/admin/shell/SharedForm.js +17 -11
  71. package/dist/admin/shell/SharedList.js +5 -3
  72. package/dist/admin/shell/entries-list.d.ts +4 -0
  73. package/dist/admin/shell/entries-list.js +6 -0
  74. package/dist/admin/shell/pages-tree.d.ts +24 -0
  75. package/dist/admin/shell/pages-tree.js +59 -1
  76. package/dist/admin/tree-ops.d.ts +9 -1
  77. package/dist/admin/tree-ops.js +27 -0
  78. package/dist/admin/ui/LocaleSwitcher.d.ts +10 -0
  79. package/dist/admin/ui/LocaleSwitcher.js +28 -8
  80. package/dist/admin/ui/Modal.js +14 -2
  81. package/dist/admin/ui/ResizeHandle.d.ts +1 -0
  82. package/dist/admin/ui/ResizeHandle.js +127 -0
  83. package/dist/admin/ui/primitives.d.ts +29 -4
  84. package/dist/admin/ui/primitives.js +29 -10
  85. package/dist/admin/ui/resize.d.ts +16 -0
  86. package/dist/admin/ui/resize.js +23 -0
  87. package/dist/admin/ui/sortable.d.ts +23 -0
  88. package/dist/admin/ui/sortable.js +57 -0
  89. package/dist/admin/ui/theme.d.ts +3 -1
  90. package/dist/admin/ui/theme.js +512 -165
  91. package/dist/admin/ui/useSortable.d.ts +35 -0
  92. package/dist/admin/ui/useSortable.js +220 -0
  93. package/dist/admin/validate.d.ts +6 -1
  94. package/dist/admin/validate.js +28 -9
  95. package/dist/config.d.ts +1 -0
  96. package/dist/config.js +36 -0
  97. package/dist/entry-store.d.ts +5 -2
  98. package/dist/entry-store.js +4 -3
  99. package/dist/fields.d.ts +4 -0
  100. package/dist/fields.js +2 -2
  101. package/dist/image/SmoodlyImage.d.ts +4 -1
  102. package/dist/image/SmoodlyImage.js +2 -2
  103. package/dist/index.d.ts +8 -1
  104. package/dist/index.js +5 -1
  105. package/dist/marks.d.ts +53 -0
  106. package/dist/marks.js +43 -0
  107. package/dist/media.d.ts +3 -2
  108. package/dist/media.js +48 -6
  109. package/dist/refs.d.ts +2 -2
  110. package/dist/refs.js +33 -12
  111. package/dist/render.js +2 -1
  112. package/dist/resolve.d.ts +8 -2
  113. package/dist/resolve.js +8 -7
  114. package/dist/richtext/index.d.ts +12 -0
  115. package/dist/richtext/index.js +9 -0
  116. package/dist/richtext-render.d.ts +22 -2
  117. package/dist/richtext-render.js +40 -13
  118. package/dist/richtext-walk.d.ts +44 -0
  119. package/dist/richtext-walk.js +46 -0
  120. package/dist/schema.d.ts +1 -1
  121. package/dist/schema.js +13 -1
  122. package/dist/section-wrapper.js +2 -1
  123. package/dist/site.d.ts +2 -0
  124. package/dist/site.js +7 -4
  125. package/dist/store.d.ts +3 -1
  126. package/dist/store.js +4 -4
  127. package/dist/supabase-entry-store.d.ts +1 -0
  128. package/dist/supabase-entry-store.js +2 -1
  129. package/dist/supabase-store.js +1 -1
  130. package/dist/toolset.d.ts +7 -1
  131. package/dist/toolset.js +13 -4
  132. package/package.json +10 -1
package/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,8 +249,10 @@ 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
258
  selected, page settings (`admin/editor/PageSettings.tsx`: the title and
@@ -312,11 +316,18 @@ Implemented:
312
316
  admin no longer derives a path of its own.
313
317
  - `smoodly/image` — the site's media components: `<SmoodlyImage>`,
314
318
  `<SmoodlyVideo>`, `imageUrl(image, { width, quality? }, loader?)`,
315
- `frameFor`/`boxAspectOf` (the crop math) and `smoodlyImages()` for
316
- `next.config`'s `images`: the Supabase host as a remote pattern, plus
317
- `dangerouslyAllowLocalIP` when that host is loopback or private Next
318
- 16's optimizer refuses a private upstream otherwise, and the local stack
319
- is one. Server-safe, no `"use client"`.
319
+ `frameFor`/`boxAspectOf` (the crop math). Server-safe, no `"use client"`.
320
+ - `smoodly/image/config` — `smoodlyImages()` for `next.config`'s `images`:
321
+ the Supabase host as a remote pattern, plus `dangerouslyAllowLocalIP`
322
+ when that host is loopback or private Next 16's optimizer refuses a
323
+ private upstream otherwise, and the local stack is one. Its own entry
324
+ because Next loads `next.config.ts` in plain Node, where anything that
325
+ reaches `next/image` fails to resolve (next@16 has no `exports` map);
326
+ `smoodly/image` still re-exports the helper for code the bundler sees.
327
+ - `smoodly/richtext` — the bound richtext renderer, `createRichText({
328
+ elements, sizes })`: the root `RichText` with the project's elements and
329
+ body images through `<SmoodlyImage>`. Its own entry for the same reason
330
+ as `smoodly/image`: it reaches `next/image`.
320
331
 
321
332
  - Locked zones are usable: `fillLockedZones` (a pure tree op) materializes
322
333
  the one node of every empty locked zone from the component's `sample`
@@ -359,28 +370,169 @@ Implemented:
359
370
  a toolset forbids survives every round trip. Links carry `href` only. An
360
371
  empty document is missing for a required field. The widget emits plain
361
372
  JSON: ProseMirror's node attrs have a null prototype, which a server
362
- action cannot serialize.
373
+ action cannot serialize. Block nodes (spec 2026-09-08 block nodes): an
374
+ `image` node (media library, caption, alt override) and one generic
375
+ `element` node for every registered Element, always in the schema,
376
+ inserted through the toolbar's "+" or a `/` menu, edited as cards (the
377
+ element card renders its fields through `FieldWidget`, nested richtext
378
+ included). `RichText` takes `elements` and an `image` render prop;
379
+ `smoodly/richtext` exports `createRichText` bound over `<SmoodlyImage>`.
380
+ The media and refs walks descend into documents against the element
381
+ schemas (`elements` beside `sections` in the store, resolve and site
382
+ options).
363
383
 
364
- Not yet: Elements in richtext, drag-and-drop
365
- reordering (move-up/move-down buttons in the editor, Move up / Move down
366
- in the Pages list's row menu), undo/redo, sample/placeholder content
384
+ Not yet: canvas drag-and-drop (list fields, the outline, manual
385
+ collections and the pages tree drag since 2026-09-09 spec
386
+ `docs/superpowers/specs/2026-09-09-drag-and-drop-design.md`), undo/redo, sample/placeholder content
367
387
  marking, per-locale section visibility conditions (OQ #3 — each locale
368
388
  now has its own tree, so a section's fields are already per locale; what
369
389
  is open is hiding a section by a locale-dependent condition).
370
390
 
371
391
  ## Releasing
372
392
 
373
- Both packages move together. From the repo root:
393
+ Both packages move together. Bump before build (the CLI's templates are
394
+ regenerated from `examples/site` in the build and pin `smoodly@<version>`),
395
+ the user runs the two publishes (npm 2FA is a browser passkey), tag only
396
+ after both are on the registry. From the repo root:
374
397
 
375
- npm version 0.0.4 -w smoodly -w create-smoodly-app --no-git-tag-version
398
+ npm version 0.0.12 -w smoodly -w create-smoodly-app --no-git-tag-version
376
399
  npm run build
377
- npm publish -w smoodly --access public
378
- npm publish -w create-smoodly-app --access public
379
- git commit -am "release: 0.0.4" && git tag v0.0.4 && git push --tags origin main
400
+ npm publish -w smoodly --dry-run && npm publish -w create-smoodly-app --dry-run
401
+ npm publish -w smoodly --access public # user; first, the scaffold pins it
402
+ npm publish -w create-smoodly-app --access public # user
403
+ npm view smoodly@0.0.12 version # 404s for a minute or two after a good publish; poll
404
+ npm view create-smoodly-app@0.0.12 version
405
+ git commit -am "release: 0.0.12 — <changes since 0.0.11>" && git tag v0.0.12 && git push --tags origin main
380
406
 
381
- `prepublishOnly` rebuilds each package; the CLI's template is regenerated from
382
- `examples/site` in that build, so the template always matches the example
383
- site at the tagged commit.
407
+ `prepublishOnly` rebuilds each package. The Claude Code skill
408
+ `.claude/skills/releasing-smoodly` carries the same order with its checks.
409
+
410
+ ## Follow-ups (logged 2026-09-10, inline editing spec)
411
+
412
+ - Shared placements ignore field clicks: their fields live in the shared
413
+ item under Globals, which the page editor cannot patch. The starter's
414
+ Header and Footer carry marks already; inline editing of shared content
415
+ needs the shared form's ops behind the canvas.
416
+ - Richtext instant write from the inspector: the pure `RichText` render
417
+ swapped into the container while the widget is typed in.
418
+ - Item-level chrome in the canvas ("Key points · 3 of 4" on hover, add,
419
+ remove, reorder in place, later drag within a list): the item marks are
420
+ the hook.
421
+ - Placeholder and validation marks on the page (the sample-sourced
422
+ placeholder treatment, "required, empty" outlines) wait for the tree's
423
+ placeholder marking.
424
+ - A coverage hint for developers: the admin knows a section's text fields
425
+ and could list the ones without a mark.
426
+ - AI verbs ("rewrite this heading") need a field under the cursor, which
427
+ a mark is.
428
+ - A cross-origin admin needs the bridge-side editing variant (spec §3).
429
+ - Direct-child spacing rules on a richtext container (Tailwind `space-y-*`,
430
+ `.prose > p`) do not reach the canvas editor's blocks during a session;
431
+ flex and grid gaps are copied (onto the ProseMirror root since the
432
+ walk, 69aecc6). Documented, not fixed.
433
+ - The text session's Enter commits: a plain text field has no line breaks.
434
+ A multi-line text control would be its own descriptor.
435
+ - The floating toolbar flips below the caret only near the top of the
436
+ DOCUMENT (where the page box clips it); it scrolls with the content, so
437
+ a caret at the top of the scrolled canvas has its toolbar out of view
438
+ until the editor scrolls. A flip keyed to the scroller's viewport would
439
+ fix it.
440
+ - No starter section gives the canvas a richtext with blocks: the Image
441
+ and text body is marks-only, and the article body (the `article`
442
+ toolset) is edited in the entry form. The walk borrowed the article
443
+ toolset for the block steps; a starter section with a long-form body
444
+ would let the canvas show them.
445
+ - The caret placed right after an existing `/` reopens the slash menu
446
+ (TipTap's suggestion re-triggers on the character before the caret).
447
+ Harmless; a trigger keyed to typing only would be stricter.
448
+ - Walk findings, all fixed the same day (69aecc6): the copied flex gap
449
+ landed on the editor root instead of the ProseMirror element that holds
450
+ the blocks, so paragraphs lost their spacing; a click on a marked image
451
+ revealed nothing because the reveal forced the Content tab while the
452
+ media projection puts the image on Media; Escape after a card's button
453
+ had taken the focus did not end the session.
454
+
455
+ ## Follow-ups (logged 2026-09-08, admin restyle)
456
+
457
+ - Remove the temporary `technical` look once the warm restyle is decided:
458
+ the `TECHNICAL_*` strings (frozen tokens plus the `_SHAPE`/`_EXTRA`
459
+ additions), the second `cascade` call and the boot script's
460
+ `smoodly-look` branch in `src/admin/ui/theme.tsx`. The original look
461
+ stays at tag `look-technical`; restore the file from there if the
462
+ restyle is dropped instead.
463
+ - The technical block is a token snapshot, not a pixel restore: shared
464
+ class rules moved with the warm look, so under `data-look="technical"`
465
+ the inspector is flush cards with a hairline instead of one column with
466
+ a sticky head, tabs are stretched, inputs have a 1px (not 1.5px)
467
+ border, rail tiles are 42×34, and a few weights and gaps drifted. Exact
468
+ comparison is the tag.
469
+ - Signed-in walk of the warm look still pending: the sign-in screen was
470
+ checked in the browser pane (light and dark), the list, entry form and
471
+ visual editor were not — the magic-link sign-in carries a token in the
472
+ URL, which the pane's policy blocks. Walk them, then tune: inspector
473
+ width (320px here, the handoff says 296px; the field widgets need the
474
+ room), the canvas meta bar (the handoff has none; ours holds the locale
475
+ switcher), and the block-toolbar note.
476
+ - The handoff's `Styles` tab in the inspector is not built: it exists
477
+ only where a section registers styles, which is the existing `tabsFor`.
478
+ - Wordmark and app icon colour: the wordmark now takes `--logo-color`
479
+ (accent in the warm look); the rail's app icon is hidden in the warm
480
+ look and still brand blue in the technical one. A warm app icon is a
481
+ design question.
482
+
483
+ ## Follow-ups (logged 2026-09-08, block nodes spec)
484
+
485
+ - Image sizing and float in a body (full width, wide, small, left/right):
486
+ a later additive `styles` on the image node; the site's CSS answers it.
487
+ - Inline elements: `inline: true` on the registration; cursor, marks and
488
+ selection around an inline atom are a separate node-view effort.
489
+ - The column context (`contexts: ["column"]`) waits for Phase 3 rows;
490
+ accepted and unused today.
491
+ - A page or entry picker for `f.link()` inside an element — the link
492
+ picker spec.
493
+ - Element copy between fields: a pasted element the target toolset
494
+ forbids is dropped silently; a notice would be kinder.
495
+ - Placeholder marking for sample-populated element fields waits for the
496
+ tree's own placeholder marking.
497
+ - An element's styles are not merged with the config's `styles.elements`
498
+ standard set (sections get `resolvedStyles`); add the merge when the
499
+ set is non-empty somewhere.
500
+ - Element styles are not validated on publish (sections' aren't either).
501
+ - `hasContent` treats an unresolved image node as content, so a body
502
+ whose only image was deleted from the library still passes "required".
503
+ - Publishing a saved version of a versioned entry: the entry form's
504
+ status control returns early when the target equals the current status,
505
+ so a new version of a published entry goes live only by toggling Draft
506
+ then Published (seen in the block-nodes walk; pre-existing). A "Publish
507
+ changes" action when the draft version is ahead of the published one.
508
+ - The insert position is recorded when the "+" menu opens and handed to
509
+ `insertContentAt`, because after the menu or the media picker takes
510
+ focus the refocused contenteditable placed its DOM caret at the first
511
+ card and the editor's selection followed it (the walk's first inserts
512
+ landed at the document top). The explicit position closes the hole;
513
+ the DOM-caret drift itself was not chased to its root.
514
+ - The "+" menu and the slash menu share `RichtextInsertMenu` but keep
515
+ separate active-row state and key handling; one keyboard model would
516
+ be smaller.
517
+
518
+ ## Follow-ups (logged 2026-09-08, tooling session)
519
+
520
+ - **CI live job red since e09a632, 0.0.11 fresh install broken — fixed
521
+ 2026-09-08, released as 0.0.12: `smoodly/image/config` is the entry
522
+ `next.config.ts` imports; `test/image-config-entry.test.ts` loads the
523
+ built file under bare node.** The
524
+ scaffolded `next.config.ts` imports `smoodlyImages` from `smoodly/image`,
525
+ whose index re-exports `SmoodlyImage.tsx` and its `import Image from
526
+ "next/image"`. Next loads the config in plain Node ESM and next@16.3.4
527
+ has no `exports` map, so the extensionless specifier fails
528
+ (`ERR_MODULE_NOT_FOUND ... Did you mean "next/image.js"?`) before
529
+ `next build` starts; the bundler resolves it, which is why nothing else
530
+ caught it. Reproduce from `examples/site`:
531
+ `node --input-type=module -e "await import('smoodly/image')"`. Fix: a
532
+ component-free entry for the config helper (the template and the
533
+ example comment then import from it), a plain-Node import test, then
534
+ release 0.0.12. Appending `.js` alone is weaker: Node hands back the
535
+ CommonJS `module.exports` object as the default, not the component.
384
536
 
385
537
  ## Follow-ups (logged 2026-09-08, client routing spec)
386
538
 
@@ -409,8 +561,6 @@ site at the tagged commit.
409
561
  - Video transcoding and adaptive streaming: a Mux or Cloudflare Stream
410
562
  loader behind the `media.loader` seam, if a site ever needs more than
411
563
  an MP4 over Storage's CDN.
412
- - Richtext images: the `image` node from the Phase 0 sketch, using the
413
- media picker once it exists.
414
564
  - Sweeping abandoned `pending` asset rows (an upload that began and
415
565
  never called `finishUpload`); a periodic op or a cleanup on `list`.
416
566
  - A private bucket with signed reads, for sites that must not expose
@@ -476,18 +626,62 @@ site at the tagged commit.
476
626
  generated (gitignored), so "templates rebuilt" is a build step, not a
477
627
  diff.
478
628
 
629
+ ## Follow-ups (logged 2026-09-09, admin tweaks)
630
+
631
+ The 2026-09-09 tweak round: view titles sans 18px, the entry and
632
+ Globals forms in one 680px column with a globe-off mark on fields that
633
+ are not `.localized()` (the "Shared across languages" / "In <locale>"
634
+ groups are gone), the image field on a 300px canvas, every locale
635
+ switch a segmented pill (a dropdown past three locales), Media before
636
+ Globals (née Shared) on the rail, richtext paragraph and card spacing,
637
+ element cards collapsed unless blank, and the "+" menu floating under
638
+ the toolbar instead of pushing the content.
639
+
640
+ - `LocaleSwitcher` in pill form has no "+ add" affordance: an absent
641
+ locale reads subtle and switching to it lands on the form's or the
642
+ editor's "doesn't exist yet · Add" pane, which adds. The dropdown form
643
+ (four locales and up) still adds straight from its "Add locale" group,
644
+ so the two forms differ by one click; a "+" inside the pill would close
645
+ that gap.
646
+ - The image field's canvas height (300px) lives twice: `CANVAS_H` in
647
+ `MediaWidget.tsx` sizes the frame, `.sm-media__canvas` /
648
+ `.sm-media__thumb` cap it in the stylesheet. One token would do.
649
+ - `.sm-form>*{max-width:680px}` caps the banner too; if a wide
650
+ field type ever appears (a table), give it an opt-out class.
651
+ - An element card decides "open" from its fields alone (blank = open);
652
+ an element whose fields are all optional and empty by design stays
653
+ open on every load. The richtext image card is never collapsible.
654
+ - The "+" menu anchors to the toolbar's right end (`right:0` in
655
+ `.sm-richtext__head`), which is under the "+" only while the toolbar
656
+ fits on one line; in the 320px inspector the toolbar wraps and the
657
+ menu sits below the wrapped row's end.
658
+ - The technical look's `TECHNICAL_SHAPE` tokens were not touched: its
659
+ title stays sans at `--text-xl`, its label has no mark styling beyond
660
+ the shared `.sm-label__mark` rule.
661
+ - Browser-pane sign-in (2026-09-09): the magic-link hash on
662
+ `/admin/...` was NOT consumed by the admin's Supabase client in the
663
+ pane (the login form stayed); fetching `/auth/v1/user` with the
664
+ access token and writing the session object to `localStorage`
665
+ under `smoodly-auth`, then reloading, signed in. The pane was hidden
666
+ during the walk, so scrolling went through `.sm-form.scrollTop` and
667
+ clicks through `element.click()` in `javascript_tool`.
668
+
479
669
  ## Follow-ups (logged 2026-09-08, richtext widget spec)
480
670
 
481
- - Drag-to-resize the inspector: `--inspector-w` is one variable (380px
482
- since the widget); a handle on the column edge writing it, persisted
483
- per browser like the theme, is the follow-up.
671
+ - ~~Drag-to-resize the inspector~~ DONE 2026-09-10 (spec
672
+ `docs/superpowers/specs/2026-09-10-inspector-resize-design.md`):
673
+ `ui/ResizeHandle.tsx` writes `--inspector-w` inline on the wrapper,
674
+ clamped to 260px..half the row (`ui/resize.ts`), remembered under
675
+ `smoodly-inspector-w` and applied by the theme's boot script. Walk
676
+ finding: the desktop pane hides its tab on every `navigate`, and a
677
+ hidden tab lays out at zero width, so the handle treats a zero
678
+ measurement as unknown and re-measures on focus. Real drags through
679
+ the pane's `left_click_drag` worked (synthetic `PointerEvent`s cannot
680
+ satisfy `setPointerCapture`); coordinates are the screenshot frame,
681
+ 0.625 × CSS px at the pane's 1280 width.
484
682
  - A page and entry picker in the link popover: a link inside a richtext
485
683
  document is a reference the refs index, the resolver and the localizer
486
684
  cannot see (they address refs by top-level key). Its own spec.
487
- - Elements in richtext: a registered Element as a TipTap node view with a
488
- small form for its fields, rendered on the site by `RichText` through
489
- the registry. The next spec.
490
- - Images in richtext: the Phase 0 sketch's `image` node; waits for media.
491
685
  - Escape inside any inspector control bubbles to the editor's window
492
686
  handler and clears the block selection — the link popover stops it, a
493
687
  textarea or the TipTap content does not. A `keydown` guard on
@@ -580,7 +774,7 @@ site at the tagged commit.
580
774
  the header nav rows read `/`, `/posts` — where the label would identify
581
775
  the row; `itemSummary` could prefer a `label`/`title`/`heading` key
582
776
  (walk, 2026-09-07).
583
- - New page / New folder ask for the slug through `window.prompt`, which
777
+ - New page / New path (a folder, "path" in the UI since 2026-09-09) ask for the slug through `window.prompt`, which
584
778
  browser automation blocks and which cannot validate as you type; an
585
779
  inline field would do both (walk, 2026-09-07).
586
780
 
@@ -692,11 +886,40 @@ site at the tagged commit.
692
886
  `node:crypto`, etc. from a module reachable through the root would
693
887
  regress it silently.
694
888
 
889
+ ## Follow-ups (logged 2026-09-09)
890
+
891
+ - Cross-zone drag in the outline: each zone is its own sortable list;
892
+ moving a block between zones is still insert-and-remove.
893
+ - Nested manual collections show no grip (`canReorder` requires
894
+ `depth === 1`) until the entry tree UI exists — a flat index is not a
895
+ sibling index there.
896
+ - Canvas drag: the overlay should reuse `ui/sortable.ts` across the
897
+ iframe boundary; `useSortable` measures container children, which the
898
+ overlay's ghost rows can be.
899
+ - The Pages row menu's Move up / Move down could retire once the grip's
900
+ arrow keys are familiar; they stay for now as the discoverable path.
901
+ - The grip's arrow keys in the pages tree step one VISIBLE slot with the
902
+ depth clamped — among siblings in a flat run, but across a depth change
903
+ the step follows the clamp. Documented as is.
904
+ - A dragged subtree stays collapsed after the drop (`onDragStart`
905
+ collapses it through `toggle` and nothing re-expands it).
906
+ - Walk findings (2026-09-09, every surface walked signed-in; the pointer
907
+ path with synthetic pointer events, autoscroll in Playwright because
908
+ the desktop pane pauses animation frames while hidden):
909
+ - FIXED: the lifted row's transform grew the scroller's overflow, so
910
+ the bottom-edge autoscroll chased it forever and the list scrolled
911
+ away under the pointer; `clampOffset` now holds the transform inside
912
+ the rows' extent while the slot keeps following the pointer.
913
+ - A depth-1 row dragged to the very end of the tree with no drift
914
+ projects onto the last root row's child slot, which a mount or the
915
+ home page refuses (muted row); the editor must drift left 20px to
916
+ land at the root. By spec §4 (depth = own depth + drift), noted as a
917
+ discoverability question for the canvas-drag round.
918
+ - The rows are held in place until the reload lands (checked with a
919
+ 1.2 s fetch delay): no flicker on a fast stack either.
920
+
695
921
  ## Follow-ups (logged 2026-09-06)
696
922
 
697
- - Drag-and-drop in the Pages list — the ops it needs exist
698
- (`pages.move(id, { parentId, index })`); the row menu's Move up / Move
699
- down is the interim.
700
923
  - Nested entries have no admin UI beyond `entries.move`: nothing creates
701
924
  a child entry or shows the entry tree, and the entry form's slug prefix
702
925
  is the collection's mount base only, not the parent chain.
@@ -790,8 +1013,6 @@ site at the tagged commit.
790
1013
  - `EditorView.tsx` is ~850 lines now that it also holds the locale switch,
791
1014
  add and remove flows — see the 2026-09-03 follow-up below for the seams
792
1015
  (that entry is the one to act on).
793
- - The `LocaleSwitcher` is a native `<select>`; a `Segmented` with an
794
- "+ add" affordance would match the Pages list.
795
1016
  - `i18n/request.ts` returns `{ locale, messages: {} }`. next-intl 4.14.2
796
1017
  throws `No messages found` when the provider inherits none, and the
797
1018
  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
  }