smoodly 0.0.9 → 0.0.11

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 (125) hide show
  1. package/README.md +167 -18
  2. package/dist/admin/client-ops.js +1 -0
  3. package/dist/admin/editor/CanvasOverlay.d.ts +21 -0
  4. package/dist/admin/editor/CanvasOverlay.js +39 -0
  5. package/dist/admin/editor/EditorView.js +56 -237
  6. package/dist/admin/editor/InsertPicker.d.ts +18 -0
  7. package/dist/admin/editor/InsertPicker.js +18 -0
  8. package/dist/admin/editor/NoticePane.d.ts +4 -0
  9. package/dist/admin/editor/NoticePane.js +4 -0
  10. package/dist/admin/editor/Outline.d.ts +18 -0
  11. package/dist/admin/editor/Outline.js +12 -0
  12. package/dist/admin/editor/locale-url.d.ts +12 -0
  13. package/dist/admin/editor/locale-url.js +32 -0
  14. package/dist/admin/editor/overlay-geometry.d.ts +14 -0
  15. package/dist/admin/editor/overlay-geometry.js +27 -0
  16. package/dist/admin/editor/useCanvasBridge.d.ts +15 -0
  17. package/dist/admin/editor/useCanvasBridge.js +53 -0
  18. package/dist/admin/editor/usePageLocale.d.ts +23 -0
  19. package/dist/admin/editor/usePageLocale.js +88 -0
  20. package/dist/admin/forms/CropEditor.d.ts +10 -0
  21. package/dist/admin/forms/CropEditor.js +61 -0
  22. package/dist/admin/forms/FieldWidget.js +8 -26
  23. package/dist/admin/forms/MediaGrid.d.ts +11 -0
  24. package/dist/admin/forms/MediaGrid.js +16 -0
  25. package/dist/admin/forms/MediaPicker.d.ts +8 -0
  26. package/dist/admin/forms/MediaPicker.js +44 -0
  27. package/dist/admin/forms/MediaWidget.d.ts +9 -0
  28. package/dist/admin/forms/MediaWidget.js +83 -0
  29. package/dist/admin/forms/RichtextToolbar.d.ts +20 -0
  30. package/dist/admin/forms/RichtextToolbar.js +37 -0
  31. package/dist/admin/forms/RichtextWidget.d.ts +5 -0
  32. package/dist/admin/forms/RichtextWidget.js +123 -0
  33. package/dist/admin/forms/assets-context.d.ts +19 -0
  34. package/dist/admin/forms/assets-context.js +35 -0
  35. package/dist/admin/forms/crop-editor.d.ts +13 -0
  36. package/dist/admin/forms/crop-editor.js +72 -0
  37. package/dist/admin/forms/measure.d.ts +2 -0
  38. package/dist/admin/forms/measure.js +39 -0
  39. package/dist/admin/forms/richtext-extensions.d.ts +5 -0
  40. package/dist/admin/forms/richtext-extensions.js +74 -0
  41. package/dist/admin/forms/richtext-paste.d.ts +22 -0
  42. package/dist/admin/forms/richtext-paste.js +59 -0
  43. package/dist/admin/forms/upload.d.ts +17 -0
  44. package/dist/admin/forms/upload.js +33 -0
  45. package/dist/admin/index.d.ts +1 -2
  46. package/dist/admin/index.js +0 -1
  47. package/dist/admin/next/create-admin.d.ts +3 -7
  48. package/dist/admin/next/create-admin.js +14 -5
  49. package/dist/admin/next/op-handler.d.ts +3 -0
  50. package/dist/admin/next/op-handler.js +1 -0
  51. package/dist/admin/ops-impl.d.ts +3 -0
  52. package/dist/admin/ops-impl.js +83 -2
  53. package/dist/admin/ops.d.ts +27 -0
  54. package/dist/admin/richtext-doc.d.ts +7 -0
  55. package/dist/admin/richtext-doc.js +18 -0
  56. package/dist/admin/serialize.d.ts +5 -0
  57. package/dist/admin/serialize.js +1 -0
  58. package/dist/admin/shell/AdminApp.d.ts +1 -2
  59. package/dist/admin/shell/AdminApp.js +18 -19
  60. package/dist/admin/shell/EntriesList.js +3 -2
  61. package/dist/admin/shell/EntryForm.js +8 -24
  62. package/dist/admin/shell/IconRail.d.ts +7 -0
  63. package/dist/admin/shell/IconRail.js +73 -0
  64. package/dist/admin/shell/ListView.js +3 -1
  65. package/dist/admin/shell/MediaScreen.d.ts +8 -0
  66. package/dist/admin/shell/MediaScreen.js +86 -0
  67. package/dist/admin/shell/PagesList.js +3 -2
  68. package/dist/admin/shell/RouteLink.d.ts +4 -0
  69. package/dist/admin/shell/RouteLink.js +12 -0
  70. package/dist/admin/shell/SharedForm.js +3 -21
  71. package/dist/admin/shell/media-list.d.ts +8 -0
  72. package/dist/admin/shell/media-list.js +14 -0
  73. package/dist/admin/shell/router.d.ts +37 -0
  74. package/dist/admin/shell/router.js +84 -0
  75. package/dist/admin/shell/session.js +3 -1
  76. package/dist/admin/ui/Modal.d.ts +8 -0
  77. package/dist/admin/ui/Modal.js +15 -0
  78. package/dist/admin/ui/primitives.d.ts +3 -2
  79. package/dist/admin/ui/primitives.js +8 -6
  80. package/dist/admin/ui/theme.d.ts +1 -1
  81. package/dist/admin/ui/theme.js +133 -24
  82. package/dist/admin/validate.js +16 -0
  83. package/dist/asset-store.d.ts +86 -0
  84. package/dist/asset-store.js +141 -0
  85. package/dist/collections.d.ts +0 -14
  86. package/dist/collections.js +2 -10
  87. package/dist/config.d.ts +12 -0
  88. package/dist/config.js +17 -5
  89. package/dist/fields.d.ts +10 -11
  90. package/dist/fields.js +16 -2
  91. package/dist/image/SmoodlyImage.d.ts +18 -0
  92. package/dist/image/SmoodlyImage.js +16 -0
  93. package/dist/image/SmoodlyVideo.d.ts +7 -0
  94. package/dist/image/SmoodlyVideo.js +5 -0
  95. package/dist/image/crop-math.d.ts +14 -0
  96. package/dist/image/crop-math.js +38 -0
  97. package/dist/image/index.d.ts +6 -0
  98. package/dist/image/index.js +5 -0
  99. package/dist/image/loaders.d.ts +15 -0
  100. package/dist/image/loaders.js +5 -0
  101. package/dist/image/next-config.d.ts +15 -0
  102. package/dist/image/next-config.js +27 -0
  103. package/dist/image-dimensions.d.ts +4 -0
  104. package/dist/image-dimensions.js +25 -0
  105. package/dist/index.d.ts +12 -4
  106. package/dist/index.js +8 -2
  107. package/dist/media.d.ts +108 -0
  108. package/dist/media.js +129 -0
  109. package/dist/refs.d.ts +2 -0
  110. package/dist/refs.js +25 -4
  111. package/dist/resolve.d.ts +19 -4
  112. package/dist/resolve.js +55 -2
  113. package/dist/site.d.ts +2 -0
  114. package/dist/site.js +21 -3
  115. package/dist/sql-space.d.ts +8 -1
  116. package/dist/sql-space.js +28 -0
  117. package/dist/sql.js +30 -0
  118. package/dist/store.js +3 -0
  119. package/dist/supabase-asset-store.d.ts +21 -0
  120. package/dist/supabase-asset-store.js +170 -0
  121. package/dist/toolset.d.ts +30 -0
  122. package/dist/toolset.js +49 -0
  123. package/package.json +15 -1
  124. package/dist/admin/richtext.d.ts +0 -16
  125. package/dist/admin/richtext.js +0 -98
package/README.md CHANGED
@@ -131,6 +131,19 @@ Implemented:
131
131
  store constructor options (`sections`, `entries`, shared
132
132
  `MemoryRefIndex` in memory); the cross-store contract
133
133
  (`test/page-refs-contract.ts`) runs against memory AND Supabase.
134
+ - Media (spec 2026-09-08): `f.image({ aspect? })` / `f.video()` / `f.file()`
135
+ store `{ asset, alt?, crop?, hotspot?, aspect? }`; `AssetStore` (memory +
136
+ `SupabaseAssetStore`: rows in `assets`, bytes in the public `media`
137
+ bucket, two-op signed uploads); the refs index carries `asset` edges
138
+ through lists, objects and styles, so delete is guarded and usage is
139
+ listed; `resolveTree`/`resolveFields`/the site layer resolve media to
140
+ `{ url, width, height, mime, alt, crop?, hotspot?, aspect? }` with one
141
+ fetch per page; `smoodly/image` ships `<SmoodlyImage>` (a box at the
142
+ aspect, the image framed by crop and hotspot over `next/image`),
143
+ `<SmoodlyVideo>`, `imageUrl` behind the loader seam (v1: the Next
144
+ loader only) and `smoodlyImages()` for next.config's `images`. Admin: the media
145
+ widget in every form, the library picker, the crop-and-hotspot editor,
146
+ the Media screen with metadata, usage links and guarded delete.
134
147
  - `EntryStore` contract + `MemoryEntryStore` + `SupabaseEntryStore` —
135
148
  collection-entry persistence over entries/entry_locales/entry_versions/paths
136
149
  — one node, one row per locale, each locale with its own slug, status
@@ -194,7 +207,10 @@ Implemented:
194
207
  and `smoodly/bridge` (`EditorBridge`, the only piece that ships to the
195
208
  SITE side — runs inside the draft-route iframe, reports node/zone
196
209
  geometry, forwards select/hover, executes refresh/scroll). Shell
197
- (registry-derived nav, list views, segment-based internal routing),
210
+ (registry-derived nav, list views, its own client-side router
211
+ `shell/router.ts`, `RouteLink`, a leave guard for dirty forms; the
212
+ shell mounts from the layout and the catch-all page only makes the
213
+ URL exist, spec 2026-09-08),
198
214
  entry form (widgets by field type, validation errors surfaced inline,
199
215
  save/publish/unpublish/delete with the safe-delete guard's message
200
216
  shown verbatim), and the editor view (dark instrument around a bright
@@ -294,6 +310,13 @@ Implemented:
294
310
  derives every URL from it, and `pages.get(id)` hands the admin the
295
311
  finished `paths` (locale → CMS path) and `urls` (`href` applied), so the
296
312
  admin no longer derives a path of its own.
313
+ - `smoodly/image` — the site's media components: `<SmoodlyImage>`,
314
+ `<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"`.
297
320
 
298
321
  - Locked zones are usable: `fillLockedZones` (a pure tree op) materializes
299
322
  the one node of every empty locked zone from the component's `sample`
@@ -324,18 +347,21 @@ Implemented:
324
347
  `validateFields` recurses with dotted paths (`points.2.heading`) and
325
348
  checks list length; the serializer ships a list's `item`.
326
349
 
327
- - Richtext renders and round-trips: `RichText` (package root, pure React,
328
- no wrapper) turns the TipTap document into paragraphs, headings, lists,
350
+ - Richtext, both directions: `RichText` (package root, pure React, no
351
+ wrapper) turns the TipTap document into paragraphs, headings, lists,
329
352
  blockquotes, hard breaks and bold/italic/link marks, unknown nodes
330
- degrading to their children. The interim textarea widget speaks a line
331
- syntax (`#` headings, `- ` and `1. ` lists, `> ` quotes, blank line
332
- between paragraphs, hard break between adjacent lines) over the same
333
- JSON, so seeded structure survives an edit.
334
- A link mark reaches the page only with a safe scheme (http(s), mailto,
335
- tel, site-relative).
336
-
337
- Not yet: the TipTap richtext widget (the textarea's line syntax is the
338
- interim editor; `RichText` renders), Elements in richtext, drag-and-drop
353
+ degrading to their children; a link mark reaches the page only with a
354
+ safe scheme (http(s), mailto, tel, site-relative). The admin edits it
355
+ with a TipTap widget under a fixed toolbar (`admin/forms/RichtextWidget.tsx`,
356
+ spec 2026-09-08): the schema is always the full one, and the field's
357
+ toolset typed names, `DEFAULT_TOOLSET` when a field declares none
358
+ gates only creation (toolbar, shortcuts, input rules, paste), so content
359
+ a toolset forbids survives every round trip. Links carry `href` only. An
360
+ empty document is missing for a required field. The widget emits plain
361
+ JSON: ProseMirror's node attrs have a null prototype, which a server
362
+ action cannot serialize.
363
+
364
+ Not yet: Elements in richtext, drag-and-drop
339
365
  reordering (move-up/move-down buttons in the editor, Move up / Move down
340
366
  in the Pages list's row menu), undo/redo, sample/placeholder content
341
367
  marking, per-locale section visibility conditions (OQ #3 — each locale
@@ -356,6 +382,130 @@ Both packages move together. From the repo root:
356
382
  `examples/site` in that build, so the template always matches the example
357
383
  site at the tagged commit.
358
384
 
385
+ ## Follow-ups (logged 2026-09-08, client routing spec)
386
+
387
+ - Keep the previous view painted until the next one has data: today a
388
+ navigation switches instantly and the new view shows its own loading
389
+ state; a transition that holds the old view, or prefetches the next
390
+ view's ops call on hover, is the next step.
391
+ - Browser Back with unsaved changes is not guarded: `popstate` cannot be
392
+ cancelled cleanly, so a dirty form only asks on a click and on reload.
393
+ Re-pushing the current URL and asking is possible if editors lose work.
394
+ - Every revalidating op (publish, delete, a save of published content)
395
+ answers with a fresh RSC render of the admin route — the layout's
396
+ styles included, ~30 KB — because `updateTag` runs inside the server
397
+ action and Next re-renders the tree it was posted to. Harmless now
398
+ that the shell lives in the layout (the empty page subtree is what
399
+ remounts), but it is the price of server actions as the transport.
400
+ - The bundle split: the editor bridge out of the admin tree and the
401
+ editor lazy-loaded, so site pages stop loading the admin chunk (the
402
+ richtext widget register entry below measures it). Its own spec.
403
+ - `SharedForm` and `EntryForm` now share the locale helpers; the rest
404
+ of the fork (`applyResult`, `InfoRow`, the ref-options effect) is still
405
+ the 2026-09-07 entry below.
406
+
407
+ ## Follow-ups (logged 2026-09-08, media library spec)
408
+
409
+ - Video transcoding and adaptive streaming: a Mux or Cloudflare Stream
410
+ loader behind the `media.loader` seam, if a site ever needs more than
411
+ 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
+ - Sweeping abandoned `pending` asset rows (an upload that began and
415
+ never called `finishUpload`); a periodic op or a cleanup on `list`.
416
+ - A private bucket with signed reads, for sites that must not expose
417
+ draft-only media by URL before publish.
418
+ - Pre-generated image variants at upload time, for hosts without sharp.
419
+ - Transform loaders beyond the Next default: Supabase Image
420
+ Transformations, imgix, Cloudflare Images (DESIGN.md OQ 28).
421
+ - Delivery caching (checked 2026-09-08): Supabase Storage's basic CDN
422
+ fronts every public-bucket object on a hosted project with no code on
423
+ our side, and the Smart CDN (Pro plan, automatic) only adds
424
+ invalidation on overwrite, which never happens — objects are
425
+ immutable, no upsert on either path, Replace picks a new id. So both
426
+ upload paths now set a year-long `cache-control`
427
+ (`MEDIA_CACHE_SECONDS`): the `cacheControl` option on the server-side
428
+ upload, the header on the browser's signed PUT (without it a signed
429
+ upload lands as `no-cache`; Storage's default is one hour). The live
430
+ suite reads the header back from the local stack. Not yet verified
431
+ against a hosted project's CDN edge; check `cf-cache-status` and
432
+ `cache-control` on a real upload there once. Remaining: in v1 the
433
+ site's `<SmoodlyImage>` goes through `next/image`, so the visitor hits
434
+ the app server's image cache and only its origin fetch touches the
435
+ CDN; video, files and SVGs hit the CDN directly. A Supabase Image
436
+ Transformations loader (`/storage/v1/render/image/public/...`, Pro
437
+ only, must stay optional) would move resizing to the CDN — the loader
438
+ bullet above.
439
+ - The asset refs inherit the index's scope: it reflects the latest saved
440
+ version of a source, so a published version still using an asset the
441
+ draft dropped is not protected from delete — the same gap entry refs
442
+ have today.
443
+ - Deviations from the spec, decided while building: the starter's Hero
444
+ declares `aspect: "4:5"` (the design's own ratio), not `16:9`; `assets.list`
445
+ pages by a `created_at` cursor, 60 per page; the loader seam is the
446
+ `loader` prop of `<SmoodlyImage>` and `imageUrl`'s third argument — a
447
+ config-level `media.loader` waits for a second loader to exist.
448
+ - Image dimensions server-side cover JPEG and PNG (`imageDimensions`); a
449
+ WebP or AVIF seeded through `AssetStore.create` lands without a size
450
+ and resolves to nothing until the row is edited. Browser uploads
451
+ measure every type.
452
+ - A section's `styles` are walked for media, the config's standard style
453
+ set is not (it holds selects). A standard-set image would need the
454
+ walk to read `resolvedStyles`.
455
+ - `<SmoodlyImage>` sizes the image by the crop factor, so `sizes` picks a
456
+ candidate for the box, not for the framed image; a CDN loader with
457
+ `handlesCrop` removes the overfetch.
458
+ - `smoodlyImages()` returns the whole `images` config, not just the
459
+ patterns (decided 2026-09-08 in the browser walk): Next 16's optimizer
460
+ answers 400 for an upstream that resolves to a private IP unless
461
+ `images.dangerouslyAllowLocalIP` is set, and the local stack is
462
+ `127.0.0.1`. The helper sets the flag only for `localhost`, loopback
463
+ and RFC 1918 hosts, so a hosted project stays strict; a Docker alias
464
+ that resolves privately needs the flag by hand. The spec's "a helper
465
+ on `smoodly/next`" lives on `smoodly/image` instead, beside the
466
+ components it serves.
467
+ - Build deviations from the plan (2026-09-08, sessions A–C):
468
+ `test/admin-pages-tree.test.ts` builds its `AdminRegistry` literal by
469
+ hand and carries `media: { maxSize: 1, accept: [] }`; `MediaPicker`'s
470
+ `filters` is `readonly KindFilter[]` and the `Segmented` calls pin
471
+ `<KindFilter>`; `MediaScreen` seeds its metadata state from
472
+ `initialSelected` because the plan's effect never runs under
473
+ `renderToStaticMarkup`; `forms/measure.ts` is browser-only and
474
+ untested by design (`putSigned` is unit-tested over a stubbed fetch
475
+ and exercised live since 2026-09-08); the installer's templates are
476
+ generated (gitignored), so "templates rebuilt" is a build step, not a
477
+ diff.
478
+
479
+ ## Follow-ups (logged 2026-09-08, richtext widget spec)
480
+
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.
484
+ - A page and entry picker in the link popover: a link inside a richtext
485
+ document is a reference the refs index, the resolver and the localizer
486
+ 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
+ - Escape inside any inspector control bubbles to the editor's window
492
+ handler and clears the block selection — the link popover stops it, a
493
+ textarea or the TipTap content does not. A `keydown` guard on
494
+ `.sm-inspector` would fix it for every control.
495
+ - Every richtext descriptor now carries its toolset, so the serialized
496
+ registry grew by a few bytes per field; if the AI-facing schema wants
497
+ the vocabulary once instead of per field, dedupe there.
498
+ - The entry form's column is narrower than the inspector, so the default
499
+ toolbar wraps to two rows there; the inspector shows it on one.
500
+ - The example site's production bundle loads one shared chunk (~750 KB)
501
+ on every site page holding the package's admin client modules — the
502
+ shell, the Supabase client and now the TipTap widget — because
503
+ `next/page-renderer.tsx` imports the editor bridge from
504
+ `admin/next/bridge.tsx` and Turbopack bundles the package's client
505
+ modules together. Measured on the 2026-09-08 build; whether the shell
506
+ was already there before the widget is unverified. The fix is to give
507
+ the bridge its own entry outside the admin tree and re-measure.
508
+
359
509
  ## Follow-ups (logged 2026-09-07, starter)
360
510
 
361
511
  - A paired component (`smoodly.section`) accepts only its schema's props, so
@@ -497,11 +647,10 @@ site at the tagged commit.
497
647
  a duplicate beside it. Re-materializing such an item (delete the row in
498
648
  SQL, reload the admin) moves it onto the derived id.
499
649
  - `SharedForm.tsx` is a near-verbatim fork of `EntryForm.tsx`
500
- (`localeFromUrl`, `writeLocaleToUrl`, `applyResult`, `InfoRow`, the
501
- ref-options effect) and `SharedList.tsx` is the third copy of the
502
- remembered-locale block (`EntriesList`, `PagesList`); extract
503
- `shell/locale-url.ts` and a `useFormLocale` hook when the next form
504
- arrives.
650
+ (`applyResult`, `InfoRow`, the ref-options effect; the locale helpers
651
+ are shared since 2026-09-08) and `SharedList.tsx` is the third copy of
652
+ the remembered-locale block (`EntriesList`, `PagesList`); extract a
653
+ `useFormLocale` hook when the next form arrives.
505
654
  - A placement selected in a bound locked zone shows the canvas toolbar's
506
655
  "fields only" label though a placement has no page-level fields; the
507
656
  inspector notice is the real message.
@@ -960,4 +1109,4 @@ site at the tagged commit.
960
1109
  - `serialize.ts` structural cast duplicates `PageSchema`; tighten `Registered.schema` so the cast goes.
961
1110
  - Catch-all `templates` map should filter to open registrations to state its intent (unreachable today).
962
1111
  - 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.
963
- - `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)
1112
+ - `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)
@@ -2,6 +2,7 @@ const GROUPS = {
2
2
  pages: ["list", "get", "create", "saveDraft", "publish", "delete", "rename", "move", "setTemplate", "addLocale", "removeLocale"],
3
3
  entries: ["list", "get", "create", "save", "setStatus", "delete", "move", "addLocale", "removeLocale"],
4
4
  shared: ["list", "get", "save", "setStatus"],
5
+ assets: ["list", "get", "beginUpload", "finishUpload", "update", "delete", "usage"],
5
6
  preview: ["enable", "disable"],
6
7
  };
7
8
  export function makeClientOps(action, token) {
@@ -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
+ }