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.
- package/README.md +228 -18
- 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/FieldWidget.d.ts +7 -3
- package/dist/admin/forms/FieldWidget.js +68 -8
- 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/list.d.ts +18 -0
- package/dist/admin/forms/list.js +81 -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/index.d.ts +0 -1
- 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/richtext-doc.d.ts +7 -0
- package/dist/admin/richtext-doc.js +18 -0
- package/dist/admin/serialize.d.ts +4 -0
- package/dist/admin/serialize.js +3 -0
- package/dist/admin/shell/AdminApp.d.ts +7 -2
- package/dist/admin/shell/AdminApp.js +12 -6
- package/dist/admin/shell/EntriesList.js +3 -2
- package/dist/admin/shell/EntryForm.js +9 -25
- package/dist/admin/shell/ListView.js +3 -1
- 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 +4 -22
- 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/primitives.d.ts +2 -1
- package/dist/admin/ui/primitives.js +5 -3
- package/dist/admin/ui/theme.d.ts +1 -1
- package/dist/admin/ui/theme.js +40 -1
- package/dist/admin/validate.d.ts +7 -2
- package/dist/admin/validate.js +53 -13
- package/dist/collections.d.ts +0 -14
- package/dist/collections.js +0 -10
- package/dist/entry-store.js +7 -1
- package/dist/fields.d.ts +18 -2
- package/dist/fields.js +51 -5
- package/dist/index.d.ts +7 -2
- package/dist/index.js +6 -1
- package/dist/refs.js +5 -0
- package/dist/resolve.js +3 -0
- package/dist/richtext-render.d.ts +3 -0
- package/dist/richtext-render.js +62 -0
- package/dist/shared-id.d.ts +3 -0
- package/dist/shared-id.js +36 -0
- package/dist/sql-space.d.ts +4 -0
- package/dist/sql-space.js +5 -1
- package/dist/sql.js +1 -1
- package/dist/supabase-entry-store.d.ts +8 -0
- package/dist/supabase-entry-store.js +37 -2
- package/dist/toolset.d.ts +30 -0
- package/dist/toolset.js +49 -0
- package/package.json +11 -1
- package/dist/admin/richtext.d.ts +0 -14
- 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,
|
|
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
|
-
|
|
318
|
-
|
|
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
|
-
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
(`
|
|
375
|
-
|
|
376
|
-
remembered-locale block (`EntriesList`, `PagesList`); extract
|
|
377
|
-
`
|
|
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
|
|
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
|
+
}
|