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.
- package/README.md +167 -18
- package/dist/admin/client-ops.js +1 -0
- package/dist/admin/editor/CanvasOverlay.d.ts +21 -0
- package/dist/admin/editor/CanvasOverlay.js +39 -0
- package/dist/admin/editor/EditorView.js +56 -237
- package/dist/admin/editor/InsertPicker.d.ts +18 -0
- package/dist/admin/editor/InsertPicker.js +18 -0
- package/dist/admin/editor/NoticePane.d.ts +4 -0
- package/dist/admin/editor/NoticePane.js +4 -0
- package/dist/admin/editor/Outline.d.ts +18 -0
- package/dist/admin/editor/Outline.js +12 -0
- package/dist/admin/editor/locale-url.d.ts +12 -0
- package/dist/admin/editor/locale-url.js +32 -0
- package/dist/admin/editor/overlay-geometry.d.ts +14 -0
- package/dist/admin/editor/overlay-geometry.js +27 -0
- package/dist/admin/editor/useCanvasBridge.d.ts +15 -0
- package/dist/admin/editor/useCanvasBridge.js +53 -0
- package/dist/admin/editor/usePageLocale.d.ts +23 -0
- package/dist/admin/editor/usePageLocale.js +88 -0
- package/dist/admin/forms/CropEditor.d.ts +10 -0
- package/dist/admin/forms/CropEditor.js +61 -0
- package/dist/admin/forms/FieldWidget.js +8 -26
- package/dist/admin/forms/MediaGrid.d.ts +11 -0
- package/dist/admin/forms/MediaGrid.js +16 -0
- package/dist/admin/forms/MediaPicker.d.ts +8 -0
- package/dist/admin/forms/MediaPicker.js +44 -0
- package/dist/admin/forms/MediaWidget.d.ts +9 -0
- package/dist/admin/forms/MediaWidget.js +83 -0
- package/dist/admin/forms/RichtextToolbar.d.ts +20 -0
- package/dist/admin/forms/RichtextToolbar.js +37 -0
- package/dist/admin/forms/RichtextWidget.d.ts +5 -0
- package/dist/admin/forms/RichtextWidget.js +123 -0
- package/dist/admin/forms/assets-context.d.ts +19 -0
- package/dist/admin/forms/assets-context.js +35 -0
- package/dist/admin/forms/crop-editor.d.ts +13 -0
- package/dist/admin/forms/crop-editor.js +72 -0
- package/dist/admin/forms/measure.d.ts +2 -0
- package/dist/admin/forms/measure.js +39 -0
- package/dist/admin/forms/richtext-extensions.d.ts +5 -0
- package/dist/admin/forms/richtext-extensions.js +74 -0
- package/dist/admin/forms/richtext-paste.d.ts +22 -0
- package/dist/admin/forms/richtext-paste.js +59 -0
- package/dist/admin/forms/upload.d.ts +17 -0
- package/dist/admin/forms/upload.js +33 -0
- package/dist/admin/index.d.ts +1 -2
- package/dist/admin/index.js +0 -1
- package/dist/admin/next/create-admin.d.ts +3 -7
- package/dist/admin/next/create-admin.js +14 -5
- package/dist/admin/next/op-handler.d.ts +3 -0
- package/dist/admin/next/op-handler.js +1 -0
- package/dist/admin/ops-impl.d.ts +3 -0
- package/dist/admin/ops-impl.js +83 -2
- package/dist/admin/ops.d.ts +27 -0
- package/dist/admin/richtext-doc.d.ts +7 -0
- package/dist/admin/richtext-doc.js +18 -0
- package/dist/admin/serialize.d.ts +5 -0
- package/dist/admin/serialize.js +1 -0
- package/dist/admin/shell/AdminApp.d.ts +1 -2
- package/dist/admin/shell/AdminApp.js +18 -19
- package/dist/admin/shell/EntriesList.js +3 -2
- package/dist/admin/shell/EntryForm.js +8 -24
- package/dist/admin/shell/IconRail.d.ts +7 -0
- package/dist/admin/shell/IconRail.js +73 -0
- package/dist/admin/shell/ListView.js +3 -1
- package/dist/admin/shell/MediaScreen.d.ts +8 -0
- package/dist/admin/shell/MediaScreen.js +86 -0
- package/dist/admin/shell/PagesList.js +3 -2
- package/dist/admin/shell/RouteLink.d.ts +4 -0
- package/dist/admin/shell/RouteLink.js +12 -0
- package/dist/admin/shell/SharedForm.js +3 -21
- package/dist/admin/shell/media-list.d.ts +8 -0
- package/dist/admin/shell/media-list.js +14 -0
- package/dist/admin/shell/router.d.ts +37 -0
- package/dist/admin/shell/router.js +84 -0
- package/dist/admin/shell/session.js +3 -1
- package/dist/admin/ui/Modal.d.ts +8 -0
- package/dist/admin/ui/Modal.js +15 -0
- package/dist/admin/ui/primitives.d.ts +3 -2
- package/dist/admin/ui/primitives.js +8 -6
- package/dist/admin/ui/theme.d.ts +1 -1
- package/dist/admin/ui/theme.js +133 -24
- package/dist/admin/validate.js +16 -0
- package/dist/asset-store.d.ts +86 -0
- package/dist/asset-store.js +141 -0
- package/dist/collections.d.ts +0 -14
- package/dist/collections.js +2 -10
- package/dist/config.d.ts +12 -0
- package/dist/config.js +17 -5
- package/dist/fields.d.ts +10 -11
- package/dist/fields.js +16 -2
- package/dist/image/SmoodlyImage.d.ts +18 -0
- package/dist/image/SmoodlyImage.js +16 -0
- package/dist/image/SmoodlyVideo.d.ts +7 -0
- package/dist/image/SmoodlyVideo.js +5 -0
- package/dist/image/crop-math.d.ts +14 -0
- package/dist/image/crop-math.js +38 -0
- package/dist/image/index.d.ts +6 -0
- package/dist/image/index.js +5 -0
- package/dist/image/loaders.d.ts +15 -0
- package/dist/image/loaders.js +5 -0
- package/dist/image/next-config.d.ts +15 -0
- package/dist/image/next-config.js +27 -0
- package/dist/image-dimensions.d.ts +4 -0
- package/dist/image-dimensions.js +25 -0
- package/dist/index.d.ts +12 -4
- package/dist/index.js +8 -2
- package/dist/media.d.ts +108 -0
- package/dist/media.js +129 -0
- package/dist/refs.d.ts +2 -0
- package/dist/refs.js +25 -4
- package/dist/resolve.d.ts +19 -4
- package/dist/resolve.js +55 -2
- package/dist/site.d.ts +2 -0
- package/dist/site.js +21 -3
- package/dist/sql-space.d.ts +8 -1
- package/dist/sql-space.js +28 -0
- package/dist/sql.js +30 -0
- package/dist/store.js +3 -0
- package/dist/supabase-asset-store.d.ts +21 -0
- package/dist/supabase-asset-store.js +170 -0
- package/dist/toolset.d.ts +30 -0
- package/dist/toolset.js +49 -0
- package/package.json +15 -1
- package/dist/admin/richtext.d.ts +0 -16
- 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,
|
|
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
|
|
328
|
-
|
|
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
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
(`
|
|
501
|
-
|
|
502
|
-
remembered-locale block (`EntriesList`, `PagesList`); extract
|
|
503
|
-
`
|
|
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
|
|
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)
|
package/dist/admin/client-ops.js
CHANGED
|
@@ -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
|
+
}
|