smoodly 0.0.8 → 0.0.10

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 (79) hide show
  1. package/README.md +228 -18
  2. package/dist/admin/editor/CanvasOverlay.d.ts +21 -0
  3. package/dist/admin/editor/CanvasOverlay.js +39 -0
  4. package/dist/admin/editor/EditorView.js +56 -237
  5. package/dist/admin/editor/InsertPicker.d.ts +18 -0
  6. package/dist/admin/editor/InsertPicker.js +18 -0
  7. package/dist/admin/editor/NoticePane.d.ts +4 -0
  8. package/dist/admin/editor/NoticePane.js +4 -0
  9. package/dist/admin/editor/Outline.d.ts +18 -0
  10. package/dist/admin/editor/Outline.js +12 -0
  11. package/dist/admin/editor/locale-url.d.ts +12 -0
  12. package/dist/admin/editor/locale-url.js +32 -0
  13. package/dist/admin/editor/overlay-geometry.d.ts +14 -0
  14. package/dist/admin/editor/overlay-geometry.js +27 -0
  15. package/dist/admin/editor/useCanvasBridge.d.ts +15 -0
  16. package/dist/admin/editor/useCanvasBridge.js +53 -0
  17. package/dist/admin/editor/usePageLocale.d.ts +23 -0
  18. package/dist/admin/editor/usePageLocale.js +88 -0
  19. package/dist/admin/forms/FieldWidget.d.ts +7 -3
  20. package/dist/admin/forms/FieldWidget.js +68 -8
  21. package/dist/admin/forms/RichtextToolbar.d.ts +20 -0
  22. package/dist/admin/forms/RichtextToolbar.js +37 -0
  23. package/dist/admin/forms/RichtextWidget.d.ts +5 -0
  24. package/dist/admin/forms/RichtextWidget.js +123 -0
  25. package/dist/admin/forms/list.d.ts +18 -0
  26. package/dist/admin/forms/list.js +81 -0
  27. package/dist/admin/forms/richtext-extensions.d.ts +5 -0
  28. package/dist/admin/forms/richtext-extensions.js +74 -0
  29. package/dist/admin/forms/richtext-paste.d.ts +22 -0
  30. package/dist/admin/forms/richtext-paste.js +59 -0
  31. package/dist/admin/index.d.ts +0 -1
  32. package/dist/admin/index.js +0 -1
  33. package/dist/admin/next/create-admin.d.ts +3 -7
  34. package/dist/admin/next/create-admin.js +14 -5
  35. package/dist/admin/richtext-doc.d.ts +7 -0
  36. package/dist/admin/richtext-doc.js +18 -0
  37. package/dist/admin/serialize.d.ts +4 -0
  38. package/dist/admin/serialize.js +3 -0
  39. package/dist/admin/shell/AdminApp.d.ts +7 -2
  40. package/dist/admin/shell/AdminApp.js +12 -6
  41. package/dist/admin/shell/EntriesList.js +3 -2
  42. package/dist/admin/shell/EntryForm.js +9 -25
  43. package/dist/admin/shell/ListView.js +3 -1
  44. package/dist/admin/shell/PagesList.js +3 -2
  45. package/dist/admin/shell/RouteLink.d.ts +4 -0
  46. package/dist/admin/shell/RouteLink.js +12 -0
  47. package/dist/admin/shell/SharedForm.js +4 -22
  48. package/dist/admin/shell/router.d.ts +37 -0
  49. package/dist/admin/shell/router.js +84 -0
  50. package/dist/admin/shell/session.js +3 -1
  51. package/dist/admin/ui/primitives.d.ts +2 -1
  52. package/dist/admin/ui/primitives.js +5 -3
  53. package/dist/admin/ui/theme.d.ts +1 -1
  54. package/dist/admin/ui/theme.js +40 -1
  55. package/dist/admin/validate.d.ts +7 -2
  56. package/dist/admin/validate.js +53 -13
  57. package/dist/collections.d.ts +0 -14
  58. package/dist/collections.js +0 -10
  59. package/dist/entry-store.js +7 -1
  60. package/dist/fields.d.ts +18 -2
  61. package/dist/fields.js +51 -5
  62. package/dist/index.d.ts +7 -2
  63. package/dist/index.js +6 -1
  64. package/dist/refs.js +5 -0
  65. package/dist/resolve.js +3 -0
  66. package/dist/richtext-render.d.ts +3 -0
  67. package/dist/richtext-render.js +62 -0
  68. package/dist/shared-id.d.ts +3 -0
  69. package/dist/shared-id.js +36 -0
  70. package/dist/sql-space.d.ts +4 -0
  71. package/dist/sql-space.js +5 -1
  72. package/dist/sql.js +1 -1
  73. package/dist/supabase-entry-store.d.ts +8 -0
  74. package/dist/supabase-entry-store.js +37 -2
  75. package/dist/toolset.d.ts +30 -0
  76. package/dist/toolset.js +49 -0
  77. package/package.json +11 -1
  78. package/dist/admin/richtext.d.ts +0 -14
  79. package/dist/admin/richtext.js +0 -17
package/README.md CHANGED
@@ -194,7 +194,10 @@ Implemented:
194
194
  and `smoodly/bridge` (`EditorBridge`, the only piece that ships to the
195
195
  SITE side — runs inside the draft-route iframe, reports node/zone
196
196
  geometry, forwards select/hover, executes refresh/scroll). Shell
197
- (registry-derived nav, list views, segment-based internal routing),
197
+ (registry-derived nav, list views, its own client-side router
198
+ `shell/router.ts`, `RouteLink`, a leave guard for dirty forms; the
199
+ shell mounts from the layout and the catch-all page only makes the
200
+ URL exist, spec 2026-09-08),
198
201
  entry form (widgets by field type, validation errors surfaced inline,
199
202
  save/publish/unpublish/delete with the safe-delete guard's message
200
203
  shown verbatim), and the editor view (dark instrument around a bright
@@ -314,8 +317,31 @@ Implemented:
314
317
  with "used on N pages" from `refs`. Wired through the `shared` option
315
318
  on the entry store and the site layer.
316
319
 
317
- Not yet: richtext components (TipTap; `plainToRichtext`/
318
- `richtextToPlain` exist but no rich editor widget), drag-and-drop
320
+ - Lists and editable objects (spec 2026-09-07 list field): `f.list(item)`
321
+ holds any builder but another list and refuses a ref or `.localized()`
322
+ anywhere inside (refs are top-level only; localization is whole-value on
323
+ a list or object). `.min`/`.max` count items. The admin edits an object
324
+ as an indented fieldset and a list as rows — summary, collapse, move up
325
+ and down, remove, "N of max", Add disabled at max — recursively through
326
+ `FieldWidget`, with the row logic as pure functions in `admin/forms/list.ts`.
327
+ `validateFields` recurses with dotted paths (`points.2.heading`) and
328
+ checks list length; the serializer ships a list's `item`.
329
+
330
+ - Richtext, both directions: `RichText` (package root, pure React, no
331
+ wrapper) turns the TipTap document into paragraphs, headings, lists,
332
+ blockquotes, hard breaks and bold/italic/link marks, unknown nodes
333
+ degrading to their children; a link mark reaches the page only with a
334
+ safe scheme (http(s), mailto, tel, site-relative). The admin edits it
335
+ with a TipTap widget under a fixed toolbar (`admin/forms/RichtextWidget.tsx`,
336
+ spec 2026-09-08): the schema is always the full one, and the field's
337
+ toolset — typed names, `DEFAULT_TOOLSET` when a field declares none —
338
+ gates only creation (toolbar, shortcuts, input rules, paste), so content
339
+ a toolset forbids survives every round trip. Links carry `href` only. An
340
+ empty document is missing for a required field. The widget emits plain
341
+ JSON: ProseMirror's node attrs have a null prototype, which a server
342
+ action cannot serialize.
343
+
344
+ Not yet: Elements in richtext, drag-and-drop
319
345
  reordering (move-up/move-down buttons in the editor, Move up / Move down
320
346
  in the Pages list's row menu), undo/redo, sample/placeholder content
321
347
  marking, per-locale section visibility conditions (OQ #3 — each locale
@@ -336,6 +362,186 @@ Both packages move together. From the repo root:
336
362
  `examples/site` in that build, so the template always matches the example
337
363
  site at the tagged commit.
338
364
 
365
+ ## Follow-ups (logged 2026-09-08, client routing spec)
366
+
367
+ - Keep the previous view painted until the next one has data: today a
368
+ navigation switches instantly and the new view shows its own loading
369
+ state; a transition that holds the old view, or prefetches the next
370
+ view's ops call on hover, is the next step.
371
+ - Browser Back with unsaved changes is not guarded: `popstate` cannot be
372
+ cancelled cleanly, so a dirty form only asks on a click and on reload.
373
+ Re-pushing the current URL and asking is possible if editors lose work.
374
+ - Every revalidating op (publish, delete, a save of published content)
375
+ answers with a fresh RSC render of the admin route — the layout's
376
+ styles included, ~30 KB — because `updateTag` runs inside the server
377
+ action and Next re-renders the tree it was posted to. Harmless now
378
+ that the shell lives in the layout (the empty page subtree is what
379
+ remounts), but it is the price of server actions as the transport.
380
+ - The bundle split: the editor bridge out of the admin tree and the
381
+ editor lazy-loaded, so site pages stop loading the admin chunk (the
382
+ richtext widget register entry below measures it). Its own spec.
383
+ - `SharedForm` and `EntryForm` now share the locale helpers; the rest
384
+ of the fork (`applyResult`, `InfoRow`, the ref-options effect) is still
385
+ the 2026-09-07 entry below.
386
+
387
+ ## Follow-ups (logged 2026-09-08, media library spec)
388
+
389
+ - Video transcoding and adaptive streaming: a Mux or Cloudflare Stream
390
+ loader behind the `media.loader` seam, if a site ever needs more than
391
+ an MP4 over Storage's CDN.
392
+ - Richtext images: the `image` node from the Phase 0 sketch, using the
393
+ media picker once it exists.
394
+ - Sweeping abandoned `pending` asset rows (an upload that began and
395
+ never called `finishUpload`); a periodic op or a cleanup on `list`.
396
+ - A private bucket with signed reads, for sites that must not expose
397
+ draft-only media by URL before publish.
398
+ - Pre-generated image variants at upload time, for hosts without sharp.
399
+ - Transform loaders beyond the Next default: Supabase Image
400
+ Transformations, imgix, Cloudflare Images (DESIGN.md OQ 28).
401
+ - The asset refs inherit the index's scope: it reflects the latest saved
402
+ version of a source, so a published version still using an asset the
403
+ draft dropped is not protected from delete — the same gap entry refs
404
+ have today.
405
+
406
+ ## Follow-ups (logged 2026-09-08, richtext widget spec)
407
+
408
+ - Drag-to-resize the inspector: `--inspector-w` is one variable (380px
409
+ since the widget); a handle on the column edge writing it, persisted
410
+ per browser like the theme, is the follow-up.
411
+ - A page and entry picker in the link popover: a link inside a richtext
412
+ document is a reference the refs index, the resolver and the localizer
413
+ cannot see (they address refs by top-level key). Its own spec.
414
+ - Elements in richtext: a registered Element as a TipTap node view with a
415
+ small form for its fields, rendered on the site by `RichText` through
416
+ the registry. The next spec.
417
+ - Images in richtext: the Phase 0 sketch's `image` node; waits for media.
418
+ - Escape inside any inspector control bubbles to the editor's window
419
+ handler and clears the block selection — the link popover stops it, a
420
+ textarea or the TipTap content does not. A `keydown` guard on
421
+ `.sm-inspector` would fix it for every control.
422
+ - Every richtext descriptor now carries its toolset, so the serialized
423
+ registry grew by a few bytes per field; if the AI-facing schema wants
424
+ the vocabulary once instead of per field, dedupe there.
425
+ - The entry form's column is narrower than the inspector, so the default
426
+ toolbar wraps to two rows there; the inspector shows it on one.
427
+ - The example site's production bundle loads one shared chunk (~750 KB)
428
+ on every site page holding the package's admin client modules — the
429
+ shell, the Supabase client and now the TipTap widget — because
430
+ `next/page-renderer.tsx` imports the editor bridge from
431
+ `admin/next/bridge.tsx` and Turbopack bundles the package's client
432
+ modules together. Measured on the 2026-09-08 build; whether the shell
433
+ was already there before the widget is unverified. The fix is to give
434
+ the bridge its own entry outside the admin tree and re-measure.
435
+
436
+ ## Follow-ups (logged 2026-09-07, starter)
437
+
438
+ - A paired component (`smoodly.section`) accepts only its schema's props, so
439
+ a layout cannot hand the footer the contact item or the header the locale
440
+ links through the paired `Footer`/`Header`; the starter's layouts render
441
+ `FooterView`/`HeaderView` directly with the extra prop. A supported way
442
+ to pass layout-only props through a paired component (or a `children`
443
+ slot) would let the layouts use the registered component.
444
+ - Page views and entry pages receive no `locale`; the starter's views get
445
+ it from the app's strings seam (`next-intl`'s `getLocale`, or a constant
446
+ in the single-language scaffold). Adding `locale` to `PageContext` and
447
+ `EntryPageProps` would remove that dependency and let sections that
448
+ format dates stay synchronous.
449
+ - The content API is published-only, so the Articles index in the editor
450
+ canvas lists published articles while the hero above it is the draft.
451
+ A draft-aware `getEntries` for draft mode is the fix if that confuses.
452
+ - The image widget edits the URL only; `alt` is set by the seed and cannot
453
+ be changed in the admin.
454
+ - A per-locale collection path or fixed slug must name the default locale;
455
+ the installer rewrites the starter's two lines to name exactly the
456
+ chosen locales (`articlesSegments` in `scaffold.ts`), since a path
457
+ claiming a locale the site lacks fails the index-page rule at config
458
+ load. A "fall back to the first declared segment" rule in
459
+ `defineConfig` would make the patch unnecessary.
460
+ - The picker offers a shared item only in zones whose allow list includes
461
+ its section (`allowedShared` in `admin/tree-ops.ts`), so a shared item's
462
+ section is always also offered as an ordinary section. A "placeable
463
+ only" mode (a shared item whose section no zone lists) would let the
464
+ starter keep the quote shared-only.
465
+ - Registry names are one namespace across sections, elements, collections
466
+ and page shapes (`assertUniqueNames`); the starter names its People
467
+ section `peopleSection` and its Articles page `articlesIndex` to stay
468
+ clear of the collections. Namespacing by kind would let a section and a
469
+ collection share a name.
470
+ - The seed's early exit probes `/` alone, and `createPage` writes the path
471
+ row before the tree is published, so a crash between the home record and
472
+ the last page leaves a database every later run reports as "already
473
+ seeded"; recovery is `supabase db reset`. A probe on the last thing the
474
+ seed writes, or a per-step check, would make it resumable.
475
+ - With `fi` as the default locale the seed writes the English content into
476
+ `fi`. Before this fix the index page was seeded at `posts`
477
+ while the collection's `fi` path is `artikkelit`, so `ensureFixedNodes`
478
+ materialized a second, empty Articles page at `artikkelit` and the
479
+ links pointed at `/posts`; the seed now derives the segment from the
480
+ collection's `path` per locale. Left: the starter's copy in a `fi`
481
+ default is still English.
482
+ - Config-level section style defaults (`styles.sections` in `defineConfig`)
483
+ are not filled at render: a node saved without an explicit style emits
484
+ no `data-spacing`/`data-tone`, so a stylesheet cannot key the default
485
+ on the attribute. The starter pads body sections by element instead.
486
+ Filling the resolved defaults in the render pass (as the admin form
487
+ does) would make the attributes reliable (walk, 2026-09-07).
488
+ - A new list item's `select` subfield shows the first option but stores
489
+ nothing (`emptyValue` leaves it undefined), so publish fails with
490
+ `points.3.icon: Required` until the editor re-picks the value that is
491
+ already on screen. Seed the first option, or render a blank option so
492
+ the screen matches the value (walk, 2026-09-07).
493
+ - Publish validates the last saved draft, so a click inside the autosave
494
+ debounce reports fields the editor has just filled as `Required`. Flush
495
+ the pending save before validating, or disable Publish while a save is
496
+ pending (walk, 2026-09-07).
497
+ - The entry form cannot republish a published entry's newer draft: Save
498
+ writes a new draft version, the status control's `setPublished` returns
499
+ early when the status is unchanged, and nothing shows that the draft
500
+ differs from the published version. The workaround unpublishes and
501
+ republishes, which 404s the entry in between. Needs a "Publish changes"
502
+ action and a draft-edits indicator like the page editor's (walk,
503
+ 2026-09-07).
504
+ - Versions accumulate (`entry_versions` grew to four rows after one edit)
505
+ but no admin screen lists or restores them (walk, 2026-09-07).
506
+ - A list row's collapsed summary shows the item's first string subfield —
507
+ the header nav rows read `/`, `/posts` — where the label would identify
508
+ the row; `itemSummary` could prefer a `label`/`title`/`heading` key
509
+ (walk, 2026-09-07).
510
+ - New page / New folder ask for the slug through `window.prompt`, which
511
+ browser automation blocks and which cannot validate as you type; an
512
+ inline field would do both (walk, 2026-09-07).
513
+
514
+ ## Follow-ups (logged 2026-09-07, list field spec)
515
+
516
+ - Saving the interim richtext textarea drops marks (bold, italic, link);
517
+ flattens a hard break inside a heading, list item or blockquote line to a
518
+ space (a prefixed line cannot carry one, DESIGN.md §3 Richtext); and drops
519
+ anything the line syntax cannot express at all — nested lists,
520
+ multi-paragraph list items (joined with a space instead), and unknown
521
+ nodes such as a horizontal rule. All of it goes away with the TipTap
522
+ widget.
523
+ - Refs inside lists and objects are refused at build time (`f.list`,
524
+ `f.object`). A list of objects each holding a ref (a featured entry with
525
+ its own caption) needs the resolver, the refs index and the admin's
526
+ ref-option loading to address nested paths; add when a site asks.
527
+ - Heading levels 4–6 render but the line syntax writes `####`…`######`
528
+ for them; the spec names 1–3 as the supported set.
529
+ - `emptyValue` for an `image`/`video`/`file` item is `undefined`, so a new
530
+ media row starts empty; a required image item is then a validation
531
+ error until filled, which is the intended nudge but reads as an error
532
+ on a row the editor just added.
533
+ - `RichText` drops the anchor of a link mark whose href is not http(s),
534
+ mailto, tel or site-relative (`/`, `#`, `?`, `.`). The href is normalised
535
+ the way URL parsers do (tab, LF and CR removed) before the scheme test;
536
+ `javascript:`, `data:`, protocol-relative `//host` and the backslash form
537
+ `/\host` are rejected. The admin's link field does not validate on input yet.
538
+ - The list widget has no test for an object item holding a nested list
539
+ subfield.
540
+ - A list item's subtree still uses the row index as its React key, so a list
541
+ of objects each holding a list hands the inner rows another item's collapse
542
+ state when an outer row moves; fix with a stable per-item key when nested
543
+ lists get real use.
544
+
339
545
  ## Follow-ups (logged 2026-09-07, shared content spec)
340
546
 
341
547
  - `f.shared(() => item)` — a ref field on a section that renders a shared
@@ -361,21 +567,17 @@ site at the tagged commit.
361
567
  - `getSmoodlyShared` reads the store per request in draft mode and once
362
568
  per (item, locale) otherwise; a layout calling it for several items
363
569
  makes one round trip each — batch if it shows.
364
- - The Supabase entry store's singleton check is SELECT-then-INSERT with
365
- no unique index; two admin loads racing on an item's first
366
- materialization can create two rows. `ensureSharedItems` reads the
367
- first row either way, and the duplicate cannot be removed through the
368
- API `delete` refuses every shared row. Two ways to make the race
369
- impossible: a deterministic row id (uuid v5 of the item name, so the
370
- second insert fails on the primary key — no schema change), or a
371
- per-item partial unique index emitted by the SQL generator the way
372
- `collectionSQL` emits the rest.
570
+ - A shared item's row id is now uuid v5 of (space, name), so the primary
571
+ key ends the materialization race but only for rows created from now
572
+ on: an item materialized before this change keeps its random
573
+ `gen_random_uuid()` id, and a second load would still be able to insert
574
+ a duplicate beside it. Re-materializing such an item (delete the row in
575
+ SQL, reload the admin) moves it onto the derived id.
373
576
  - `SharedForm.tsx` is a near-verbatim fork of `EntryForm.tsx`
374
- (`localeFromUrl`, `writeLocaleToUrl`, `applyResult`, `InfoRow`, the
375
- ref-options effect) and `SharedList.tsx` is the third copy of the
376
- remembered-locale block (`EntriesList`, `PagesList`); extract
377
- `shell/locale-url.ts` and a `useFormLocale` hook when the next form
378
- arrives.
577
+ (`applyResult`, `InfoRow`, the ref-options effect; the locale helpers
578
+ are shared since 2026-09-08) and `SharedList.tsx` is the third copy of
579
+ the remembered-locale block (`EntriesList`, `PagesList`); extract a
580
+ `useFormLocale` hook when the next form arrives.
379
581
  - A placement selected in a bound locked zone shows the canvas toolbar's
380
582
  "fields only" label though a placement has no page-level fields; the
381
583
  inspector notice is the real message.
@@ -408,6 +610,14 @@ site at the tagged commit.
408
610
  - `ops.entries.move` expires entry tags directly and does not `fanOut`,
409
611
  so a reorder that changes what a shared item's `refList` renders does
410
612
  not expire `shared:<name>`.
613
+ - The package root (`src/index.ts`) must stay free of Node builtins:
614
+ every scaffolded section does `import { f, smoodly } from "smoodly"`,
615
+ and a `"use client"` section's browser build would fail on one.
616
+ `sharedEntryId` (`src/shared-id.ts`) moved off `node:crypto` onto Web
617
+ Crypto for this reason (fix pass, 2026-09-07). Nothing tests the
618
+ invariant itself — a future addition that reaches for `node:fs`,
619
+ `node:crypto`, etc. from a module reachable through the root would
620
+ regress it silently.
411
621
 
412
622
  ## Follow-ups (logged 2026-09-06)
413
623
 
@@ -826,4 +1036,4 @@ site at the tagged commit.
826
1036
  - `serialize.ts` structural cast duplicates `PageSchema`; tighten `Registered.schema` so the cast goes.
827
1037
  - Catch-all `templates` map should filter to open registrations to state its intent (unreachable today).
828
1038
  - The no-database build is the real proof that routes never prerender against the stores; the unit test only stubs `next/server`. Script it (`SUPABASE_URL=http://127.0.0.1:9 npx next build` in `examples/site`, expect `ƒ /`) and add it to CI, which today runs only the package's tests and typecheck — so a regression is caught when Next changes `connection()` semantics.
829
- - `EditorView` split, remaining seams (autosave extracted 2026-09-03): the locale lifecycle `localeFromUrl`, `LocaleSwitcher`, `switchLocale`, `doAddLocale`, `doRemoveLocale` is the cleanest extraction available now (self-contained, no canvas geometry); pull the canvas overlay and the insert picker into components when the dnd-kit outline work starts; the sidebar form stays put until inline editing lands (DESIGN.md §3 puts TipTap in the canvas, not the sidebar). (logged 2026-09-06)
1039
+ - `EditorView` split (autosave extracted 2026-09-03; the rest 2026-09-08, before the TipTap widget): `usePageLocale` (switch/add/remove, over `locale-url.ts`), `useCanvasBridge` (geometry, select/hover, `postToCanvas`, the geometry timeout), `CanvasOverlay` over `overlay-geometry.ts` (`plusButtonsFor`, pure and tested), `InsertPicker`, `Outline`, `NoticePane`. `EditorView.tsx` is ~660 lines of load, gestures, publish and layout. Remaining seam: the inspector's field form (`FieldWidget` rows plus the tabs) stays put until inline editing lands (DESIGN.md §3 puts TipTap in the canvas, not the sidebar); the ref-option loading (`refTargets` + the `ops.entries.list` effect) duplicates `EntryForm`'s and could be one hook. (logged 2026-09-06, updated 2026-09-08)
@@ -0,0 +1,21 @@
1
+ import type { TreeNode } from "../../render.tsx";
2
+ import type { PlusButton } from "./overlay-geometry.ts";
3
+ import type { BridgeRect, ZoneRect } from "./protocol.ts";
4
+ export type OverlaySelection = {
5
+ node: TreeNode;
6
+ rect: BridgeRect;
7
+ title: string;
8
+ shared: boolean;
9
+ locked: boolean;
10
+ };
11
+ export declare function CanvasOverlay({ emptyZones, hoverRect, selection, plusButtons, onPlus, onMove, onDuplicate, onRemove, }: {
12
+ emptyZones: ZoneRect[];
13
+ hoverRect: BridgeRect | undefined;
14
+ selection: OverlaySelection | null;
15
+ plusButtons: PlusButton[];
16
+ /** A "+" was clicked, at these viewport coordinates. */
17
+ onPlus(button: PlusButton, clientX: number, clientY: number): void;
18
+ onMove(id: string, dir: -1 | 1): void;
19
+ onDuplicate(node: TreeNode): void;
20
+ onRemove(id: string): void;
21
+ }): import("react").JSX.Element;
@@ -0,0 +1,39 @@
1
+ "use client";
2
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
+ // The overlay layer above the canvas iframe — every pixel of chrome is
4
+ // ours: empty-zone placeholders, the hover and selection outlines, the
5
+ // selected block's tag and toolbar, and a "+" per insertable boundary.
6
+ // Positions come from the geometry the bridge reported; the layer
7
+ // itself never touches the iframe.
8
+ import { useState } from "react";
9
+ import { IconButton } from "../ui/primitives.js";
10
+ export function CanvasOverlay({ emptyZones, hoverRect, selection, plusButtons, onPlus, onMove, onDuplicate, onRemove, }) {
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: {
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)",
17
+ }, children: [z.zone, " \u00B7 empty"] }, `empty-${z.zone}`))), hoverRect && (_jsx("div", { style: {
18
+ position: "absolute", left: hoverRect.x, top: hoverRect.y,
19
+ width: hoverRect.w, height: hoverRect.h,
20
+ 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: {
23
+ position: "absolute", left: selection.rect.x, top: selection.rect.y,
24
+ width: selection.rect.w, height: selection.rect.h,
25
+ outline: "var(--outline-selection)", outlineOffset: -1.5,
26
+ borderRadius: "var(--radius-sm)",
27
+ } }), _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
+ position: "absolute", left: selection.rect.x + selection.rect.w - 8, top: selection.rect.y + 8,
29
+ 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) => {
31
+ const key = `${p.zone}-${p.index}`;
32
+ 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: {
34
+ position: "absolute", left: "50%", top: 0, transform: "translate(-50%,-50%)",
35
+ display: "flex", flexDirection: "column", alignItems: "center", gap: 6,
36
+ 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));
38
+ })] }));
39
+ }