smoodly 0.0.34 → 0.0.36
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 +807 -1326
- package/dist/admin/drift.js +3 -2
- package/dist/admin/editor/EditorView.js +10 -3
- package/dist/admin/editor/SeoSettings.d.ts +7 -1
- package/dist/admin/editor/SeoSettings.js +2 -2
- package/dist/admin/editor/autosave.d.ts +6 -1
- package/dist/admin/editor/autosave.js +43 -3
- package/dist/admin/field-label.d.ts +2 -0
- package/dist/admin/field-label.js +2 -0
- package/dist/admin/forms/FieldWidget.js +2 -2
- package/dist/admin/forms/list.js +10 -1
- package/dist/admin/next/create-admin.d.ts +4 -4
- package/dist/admin/next/create-admin.js +5 -5
- package/dist/admin/next/index.d.ts +1 -1
- package/dist/admin/serialize.d.ts +8 -1
- package/dist/admin/serialize.js +23 -7
- package/dist/admin/shell/AdminApp.d.ts +3 -3
- package/dist/admin/shell/AdminApp.js +2 -2
- package/dist/admin/shell/EntryForm.js +11 -3
- package/dist/admin/shell/GlobalForm.js +3 -2
- package/dist/admin/shell/presence-realtime.d.ts +2 -2
- package/dist/admin/shell/router.d.ts +5 -0
- package/dist/admin/shell/router.js +13 -0
- package/dist/cli/space-io.js +1 -1
- package/dist/collections.d.ts +4 -0
- package/dist/collections.js +9 -1
- package/dist/entry-page.d.ts +12 -10
- package/dist/entry-store.d.ts +29 -0
- package/dist/entry-store.js +53 -0
- package/dist/env.d.ts +5 -5
- package/dist/env.js +2 -2
- package/dist/fields.d.ts +13 -7
- package/dist/fields.js +2 -2
- package/dist/global.d.ts +4 -7
- package/dist/index.d.ts +10 -39
- package/dist/index.js +1 -1
- package/dist/migrations.js +1 -1
- package/dist/next/canvas-slot.d.ts +7 -0
- package/dist/next/canvas-slot.js +9 -0
- package/dist/next/index.d.ts +3 -1
- package/dist/next/index.js +3 -1
- package/dist/next/meta.d.ts +6 -4
- package/dist/next/meta.js +14 -17
- package/dist/next/page-renderer.d.ts +2 -0
- package/dist/next/page-renderer.js +18 -11
- package/dist/next/preview-pill.d.ts +9 -0
- package/dist/next/preview-pill.js +56 -0
- package/dist/next/preview-route.d.ts +1 -0
- package/dist/next/preview-route.js +30 -0
- package/dist/next/queries.d.ts +10 -11
- package/dist/next/queries.js +7 -2
- package/dist/page.d.ts +5 -2
- package/dist/page.js +3 -1
- package/dist/pairing.d.ts +10 -10
- package/dist/path-index.d.ts +3 -0
- package/dist/path-index.js +7 -0
- package/dist/read-shapes.d.ts +64 -0
- package/dist/read-shapes.js +52 -0
- package/dist/render.d.ts +3 -3
- package/dist/resolve.d.ts +12 -0
- package/dist/resolve.js +29 -7
- package/dist/schema-version.d.ts +1 -1
- package/dist/schema-version.js +1 -1
- package/dist/schema.d.ts +22 -1
- package/dist/seo.d.ts +18 -0
- package/dist/seo.js +53 -0
- package/dist/site.d.ts +15 -16
- package/dist/site.js +64 -58
- package/dist/sql-space.d.ts +15 -0
- package/dist/sql-space.js +54 -39
- package/dist/sql.d.ts +11 -1
- package/dist/sql.js +78 -25
- package/dist/supabase-entry-store.d.ts +3 -1
- package/dist/supabase-entry-store.js +39 -0
- package/dist/supabase-path-index.d.ts +1 -0
- package/dist/supabase-path-index.js +16 -0
- package/migrations/20260929135523_smoodly_v6.sql +200 -0
- package/migrations/20260930040956_smoodly_v7.sql +84 -0
- package/package.json +1 -5
package/README.md
CHANGED
|
@@ -265,12 +265,13 @@ Implemented:
|
|
|
265
265
|
(`createSmoodlyAdmin` binds registry + op action into `AdminPage`/
|
|
266
266
|
`AdminLayout` for the app's `[[...segments]]` route group;
|
|
267
267
|
`createAdminOpHandler` is the one server action the client ops call),
|
|
268
|
-
and `
|
|
269
|
-
|
|
268
|
+
and `EditorBridge` (never on a live page: the renderer loads it lazily
|
|
269
|
+
through `next/canvas-slot.tsx` in editor mode only, spec 2026-09-29
|
|
270
|
+
pre-install fixes §1 — runs inside the draft-route iframe, reports node, zone and
|
|
270
271
|
marked-field geometry, forwards select/hover with the field under the
|
|
271
272
|
pointer, executes refresh/scroll, reports each committed render). Shell
|
|
272
273
|
(registry-derived nav, list views, its own client-side router —
|
|
273
|
-
`shell/router.ts`, `RouteLink`, a
|
|
274
|
+
`shell/router.ts`, `RouteLink`, a tab-close guard for pending saves; the
|
|
274
275
|
shell mounts from the layout and the catch-all page only makes the
|
|
275
276
|
URL exist, spec 2026-09-08),
|
|
276
277
|
entry form (widgets by field type, validation errors surfaced inline,
|
|
@@ -295,7 +296,8 @@ Implemented:
|
|
|
295
296
|
`entries.addLocale`/`removeLocale`.
|
|
296
297
|
`pages.list` materializes the code-owned root nodes before an editor
|
|
297
298
|
touches them (`admin/fixed-nodes.ts` derives fixed pages and collection
|
|
298
|
-
mounts from the registry
|
|
299
|
+
mounts from the registry and finds their records by the `pages.fixed`
|
|
300
|
+
owner key, schema v5), and every guard asks
|
|
299
301
|
the same `fixedNodeOf`: a fixed node is never renamed, moved or
|
|
300
302
|
deleted, and no page is created under a mount, and a `homePageSlug`
|
|
301
303
|
guard keeps the site root from being deleted, moved or re-slugged
|
|
@@ -521,6 +523,22 @@ Implemented:
|
|
|
521
523
|
(the rule table, both adapters), the store contracts, `admin-ops`
|
|
522
524
|
("redirects on rename and move", "redirects ops"),
|
|
523
525
|
`next-page-renderer` ("redirects"), `examples/site/e2e/redirects.spec.ts`.
|
|
526
|
+
- Content API contract (spec 2026-09-29 content API contract): every
|
|
527
|
+
read returns the public shape — `EntryItem`, `PageItem`, `GlobalItem`
|
|
528
|
+
(`src/read-shapes.ts`), nothing of other locales, pointers or editors
|
|
529
|
+
leaves the site layer; a reference resolves as `{ id, path, fields }`
|
|
530
|
+
as deep as its field says, in lists and single reads alike, and the
|
|
531
|
+
types follow the depth; `smoodly_list_entries` (schema version 7)
|
|
532
|
+
filters, orders and limits over the snapshot shown, so `where` sees
|
|
533
|
+
localized fields and never the live row; a list is tagged by every
|
|
534
|
+
collection it resolved into; `seo: true` or `{ description, image }`
|
|
535
|
+
on a collection adds the SEO & Social panel, stored per locale under
|
|
536
|
+
`$seo` (`src/seo.ts`); every content table admits a write key only, the
|
|
537
|
+
publishable key serves sign-in and presence (`presenceConfig`). Tests:
|
|
538
|
+
`test/entry-store-contract.ts` ("query"), `test/site.test.tsx`,
|
|
539
|
+
`test/next-queries.test.tsx`, `test/types-refs.test.ts`,
|
|
540
|
+
`test/seo.test.ts`, `test/isolation.supabase.test.ts`,
|
|
541
|
+
`test/rls.supabase.test.ts`.
|
|
524
542
|
- Editor safety (spec 2026-09-27 editor safety): schema drift — stored
|
|
525
543
|
content the code no longer describes — is found by one pure walk
|
|
526
544
|
(`admin/drift.ts`) over the serialized registry. Broken findings
|
|
@@ -623,12 +641,41 @@ Implemented:
|
|
|
623
641
|
`site`, the page and entry store contracts,
|
|
624
642
|
`examples/site/e2e/entries.spec.ts`.
|
|
625
643
|
|
|
644
|
+
- Pre-install fixes (spec 2026-09-29 pre-install fixes, schema v6): one
|
|
645
|
+
permissive policy per table and action (the write key reads through
|
|
646
|
+
`smoodly_read`; `smoodly_write` splits into insert, update and delete
|
|
647
|
+
where a public read exists, the media pair too), the missing
|
|
648
|
+
foreign-key and published-version indexes, `smoodly_space_row_cap`
|
|
649
|
+
revoked from anon. A live page runs no Smoodly client code: the canvas
|
|
650
|
+
bridge and the section net load lazily from one stub under 1 KB
|
|
651
|
+
(`next/canvas-slot.tsx`), and `smoodly/bridge` is no longer an export.
|
|
652
|
+
Draft mode shows a preview pill (`next/preview-pill.tsx`: an inline
|
|
653
|
+
Shadow DOM script, bottom centre, movable left/centre/right, hidden
|
|
654
|
+
until reload, Exit preview through `previewRoute`, which every starter
|
|
655
|
+
mounts at `app/api/preview/route.ts`; `previewPill()` for routes the
|
|
656
|
+
renderer does not draw). The page editor, the entry form and the
|
|
657
|
+
Globals form save a pending edit on unmount (`close()` on the autosave
|
|
658
|
+
controller, reopened by the hook's effect for StrictMode; it waits for a
|
|
659
|
+
save in flight and sends the rest with that save's token) and ask only
|
|
660
|
+
before a tab close (`useUnloadGuard`); `useLeaveGuard`'s in-app "Discard"
|
|
661
|
+
confirm has no caller now. Draft mode's pill is absent from History's
|
|
662
|
+
version iframe (`smoodly-version`), whose Exit would end draft mode under
|
|
663
|
+
the editor.
|
|
664
|
+
The cached unit is keyed by (locale, path, kind, id). A new list item's
|
|
665
|
+
required select or radio stores its first option. Every starter sets
|
|
666
|
+
`metadataBase` from `SITE_URL`, which the installer writes. Tests:
|
|
667
|
+
`migration-policies`, `sql`, `canvas-slot`, `site-entries`,
|
|
668
|
+
`next-preview-route`, `next-preview-pill`, `next-page-renderer`,
|
|
669
|
+
`admin-autosave`, `admin-list-helpers`, `env` (installer),
|
|
670
|
+
`e2e/publish.spec.ts`, `e2e/editor.spec.ts`, `e2e/languages.spec.ts`,
|
|
671
|
+
`e2e/site-bundle.spec.ts` (CI-only).
|
|
672
|
+
|
|
626
673
|
Not yet: canvas drag-and-drop (list fields, the outline, manual
|
|
627
674
|
collections and the pages tree drag since 2026-09-09 — spec
|
|
628
675
|
`docs/superpowers/specs/2026-09-09-drag-and-drop-design.md`), undo/redo, sample/placeholder content
|
|
629
|
-
marking
|
|
630
|
-
|
|
631
|
-
|
|
676
|
+
marking. Per-locale section visibility is not coming: each locale has its
|
|
677
|
+
own tree, so a locale that does not want a section removes it (DESIGN.md
|
|
678
|
+
§3, 2026-09-14).
|
|
632
679
|
|
|
633
680
|
## Releasing
|
|
634
681
|
|
|
@@ -649,78 +696,105 @@ after both are on the registry. From the repo root:
|
|
|
649
696
|
`prepublishOnly` rebuilds each package. The Claude Code skill
|
|
650
697
|
`.claude/skills/releasing-smoodly` carries the same order with its checks.
|
|
651
698
|
|
|
652
|
-
## Follow-ups
|
|
699
|
+
## Follow-ups
|
|
700
|
+
|
|
701
|
+
The code-level debt register, triaged 2026-09-29 into four buckets:
|
|
702
|
+
**Now** (before the first client install), **Next** (hardening soon
|
|
703
|
+
after), **Later** (by area) and **Cloud** (only once Smoodly Cloud
|
|
704
|
+
exists). Each item ends with where it was logged. A fixed item is
|
|
705
|
+
deleted — git history and the specs keep the record — and a new one
|
|
706
|
+
goes straight into its bucket. Design-level questions live in
|
|
707
|
+
`docs/DESIGN.md`'s Open questions, not here.
|
|
708
|
+
|
|
709
|
+
### Now
|
|
710
|
+
|
|
711
|
+
Nothing open.
|
|
712
|
+
|
|
713
|
+
### Next
|
|
714
|
+
|
|
715
|
+
#### Data integrity
|
|
716
|
+
|
|
717
|
+
- Older than this plan: the Supabase `addLocale` rollback on
|
|
718
|
+
`alreadyInLocale` (`supabase-store.ts`, the insertLocale catch)
|
|
719
|
+
deletes the racing WINNER's locale row and versions. Two admins
|
|
720
|
+
adding one locale at once converge on the next list; fix by rolling
|
|
721
|
+
back only what this call inserted. *(2026-09-29, nested collection paths)*
|
|
722
|
+
- A ready asset whose `poster_id` names a pending asset would fail the
|
|
723
|
+
export's consistency check on every run (pending assets are not
|
|
724
|
+
exported). Unverified whether the upload flow can leave that state;
|
|
725
|
+
if it can, export the poster or null the pointer with a warning. *(2026-09-28, export/import)*
|
|
726
|
+
- Publish, discard and restore compare the token, then move the pointer under a
|
|
727
|
+
pointer filter: a save that rewrites the same draft row in place between the
|
|
728
|
+
two is published unseen (one round trip wide). A SQL function would close it. *(2026-09-26, concurrency)*
|
|
729
|
+
- Publish integrity for an entry's own refs: an entry can publish while
|
|
730
|
+
pointing at an unpublished entry; pages are gated, entries are not. *(2026-09-14, collections draft/publish)*
|
|
731
|
+
- Unpublishing a page does not cascade: a published child keeps serving
|
|
732
|
+
under an unpublished parent's path. Decide whether it should. *(2026-09-13, publish flow)*
|
|
733
|
+
- `entries.move` expires the entry and its descendants but does not fan
|
|
734
|
+
out to the pages referencing them, nor to the old and the new parent
|
|
735
|
+
entries the way `pages.move` does — asymmetric with `entries.save` and
|
|
736
|
+
`entries.setStatus`, which both fan out. *(2026-09-06)*
|
|
737
|
+
|
|
738
|
+
#### Multi-statement writes (no transaction on the Supabase adapters)
|
|
739
|
+
|
|
740
|
+
- The Supabase entry adapter's `removeLocale` deletes the row and its
|
|
741
|
+
versions before the paths rewrite with no rollback; its residue
|
|
742
|
+
comment describes the opposite case (a surviving path row for a
|
|
743
|
+
removed locale, not a present node without its path row). *(2026-09-06)*
|
|
744
|
+
- The Supabase entry adapter's rollbacks are best-effort and can mask the
|
|
745
|
+
original throw: `create`'s catch runs `paths.remove` before the node
|
|
746
|
+
delete (a failing remove strands the node and replaces the real error),
|
|
747
|
+
`update` and `move` restore by writing, and those writes can throw in
|
|
748
|
+
place of the rewrite's rejection. `recordVersion` is two statements
|
|
749
|
+
(insert, then point at it), so a failure between them leaves an
|
|
750
|
+
unreferenced `entry_versions` row — invisible to every policy and
|
|
751
|
+
harmless, but it accumulates. *(2026-09-06)*
|
|
752
|
+
- `move` is best-effort across two statements (reorder, then path rewrite):
|
|
753
|
+
a failed rewrite restores each destination sibling's prior `sort` row by
|
|
754
|
+
row, so a failure in the rollback itself still leaves the order changed. *(2026-09-05)*
|
|
755
|
+
- Rollback gaps that remain: a `smoodly_reorder`/`smoodly_reorder_pages`
|
|
756
|
+
failure AFTER the re-parent update has no rollback at all; the entry
|
|
757
|
+
`create` rollback discards its delete result (an orphan row survives
|
|
758
|
+
silently); and a throw inside any rollback replaces — masks — the original
|
|
759
|
+
error. *(2026-09-05)*
|
|
760
|
+
- `setTemplate`/`saveDraft` insert the version row before the pointer update:
|
|
761
|
+
an infra failure between the two orphans a version. *(2026-09-05)*
|
|
762
|
+
|
|
763
|
+
#### Addresses and code-owned records
|
|
653
764
|
|
|
654
765
|
- `homePageSlug` is compared with a collection's whole default path, so
|
|
655
766
|
`"home/news"` passes boot with the home page as its middle part: the
|
|
656
767
|
home record serves `/`, so `/home` is no page while `/home/news` is.
|
|
657
768
|
Refuse an address whose first part is the home slug if a site trips on
|
|
658
|
-
it.
|
|
769
|
+
it. *(2026-09-29, nested collection paths)*
|
|
659
770
|
- An owner key that changes in code — a fixed page that becomes a
|
|
660
771
|
collection's index page (`page:x` → `collection:y`), a collection
|
|
661
772
|
renamed — leaves the old record "No longer in code" with its
|
|
662
773
|
sections and history, and a new empty record takes the address.
|
|
663
|
-
Carry the record over (rewrite `fixed`) when a site asks.
|
|
774
|
+
Carry the record over (rewrite `fixed`) when a site asks. *(2026-09-29, nested collection paths)*
|
|
664
775
|
- An index page registration renamed (`a` → `b`) leaves the record on
|
|
665
776
|
template `a`: `ensureFixedNodes` only handles a folder gaining an
|
|
666
|
-
index page, and `setTemplate` refuses a record that has one.
|
|
667
|
-
- The rename/move confirm's code-address line names the addresses in
|
|
668
|
-
the selected locale only, though a move re-paths every locale; the
|
|
669
|
-
Pages list's conflict notice is one ellipsized line (the whole text
|
|
670
|
-
on hover), and an editor's flagged page cannot be moved from the
|
|
671
|
-
notice itself.
|
|
672
|
-
- An index page removed from code leaves the collection's record with
|
|
673
|
-
its template (spec amendment 3): the record keeps rendering as that
|
|
674
|
-
page type's stale name until an editor or a later pass clears it.
|
|
675
|
-
- The code-owned part of a URL is never stored (spec 2026-09-29 anchored
|
|
676
|
-
paths): a row names its anchor and the index joins the config's
|
|
677
|
-
address, so an address changed in code moves its URLs at the deploy
|
|
678
|
-
with no write, and the old URLs need a `next.config.ts` redirect.
|
|
679
|
-
Rows written before schema v5's anchors (none installed) would sit
|
|
680
|
-
under `''` and go dark under a code address until re-saved.
|
|
681
|
-
- `listLive` / `listRedirects` on Supabase read at most PostgREST's
|
|
682
|
-
`max_rows` (1000) per locale, as before, now ordered by (anchor, path)
|
|
683
|
-
and sorted by URL in JS; page them if a locale's redirect list grows
|
|
684
|
-
past it.
|
|
685
|
-
- Older than this plan: the Supabase `addLocale` rollback on
|
|
686
|
-
`alreadyInLocale` (`supabase-store.ts`, the insertLocale catch)
|
|
687
|
-
deletes the racing WINNER's locale row and versions. Two admins
|
|
688
|
-
adding one locale at once converge on the next list; fix by rolling
|
|
689
|
-
back only what this call inserted.
|
|
690
|
-
|
|
691
|
-
## Follow-ups (logged 2026-09-29, pre-install fixes)
|
|
692
|
-
|
|
693
|
-
- Content health lists an entry slug outside the grammar, but its link
|
|
694
|
-
opens an entry form that marks nothing on the slug: drift leaves the
|
|
695
|
-
slug out on purpose (a marker would flash while it is typed), so only
|
|
696
|
-
the publish refusal marks it. Mark it in the form once the box has lost
|
|
697
|
-
focus, if editors miss it.
|
|
698
|
-
- Content health does not check page slugs: pages refuse a bad slug at
|
|
699
|
-
create and rename, so only an import of older data or a hand edit could
|
|
700
|
-
hold one.
|
|
777
|
+
index page, and `setTemplate` refuses a record that has one. *(2026-09-29, nested collection paths)*
|
|
701
778
|
|
|
702
|
-
|
|
779
|
+
#### Security
|
|
703
780
|
|
|
704
|
-
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
- Import cannot expire the app's cache tags; it prints a restart note. A
|
|
720
|
-
revalidate call through the app (an op the CLI could reach) would make
|
|
721
|
-
the note unnecessary.
|
|
781
|
+
- The bridge posts with `"*"` on both sides. Safe today: the admin
|
|
782
|
+
accepts a message only when `e.source` is its own iframe's window, and
|
|
783
|
+
the canvas acts on two kinds, refresh and scroll. The target-origin
|
|
784
|
+
pass belongs to the content-surface sweep (spec §Out of scope). *(2026-09-26, security pass)*
|
|
785
|
+
- The invite's `redirectTo` is a client-supplied string bounded by the
|
|
786
|
+
auth project's redirect allow-list; a same-origin check in the op is
|
|
787
|
+
one line when wanted. *(2026-09-26, security pass)*
|
|
788
|
+
- The content-surface sweep: `f.link` values reach section components
|
|
789
|
+
unfiltered, uploads allow SVG and PDF on a public bucket (SVG served
|
|
790
|
+
with `?download=`), the custom CSS refusal list, richtext hrefs. *(2026-09-26, security pass)*
|
|
791
|
+
- `RichText` drops the anchor of a link mark whose href is not http(s),
|
|
792
|
+
mailto, tel or site-relative (`/`, `#`, `?`, `.`). The href is normalised
|
|
793
|
+
the way URL parsers do (tab, LF and CR removed) before the scheme test;
|
|
794
|
+
`javascript:`, `data:`, protocol-relative `//host` and the backslash form
|
|
795
|
+
`/\host` are rejected. The admin's link field does not validate on input yet. *(2026-09-07, list field)*
|
|
722
796
|
|
|
723
|
-
|
|
797
|
+
#### The live site
|
|
724
798
|
|
|
725
799
|
- A section's crash on a live page after data changed outside the page
|
|
726
800
|
editor (an entry or a global item edited in its form): the guard
|
|
@@ -728,1406 +802,813 @@ after both are on the registry. From the repo root:
|
|
|
728
802
|
it renders, in a richtext element's view or in a `'use client'`
|
|
729
803
|
section still fails the page, and no canvas previews those forms.
|
|
730
804
|
Catching it live needs `<Suspense>` (spec 2026-09-27 section guard,
|
|
731
|
-
measured). Revisit if a real site hits it.
|
|
805
|
+
measured). Revisit if a real site hits it. *(2026-09-27, shadowed-page detector)*
|
|
806
|
+
- Publishing a global the site layout reads (the footer) serves the old
|
|
807
|
+
layout on the first public request made right after the publish returns;
|
|
808
|
+
every request after it, and a first request made ~3 s later, is fresh.
|
|
809
|
+
Pages do not show it (an unpublish answers 404 at once). Seen on
|
|
810
|
+
`next dev`; `e2e/globals.spec.ts` polls the home page for it. Find out
|
|
811
|
+
whether `updateTag` over a layout's `getGlobal` read expires late, and
|
|
812
|
+
whether `next start` behaves the same. *(2026-09-26, browser suite)*
|
|
813
|
+
- A robots `Disallow` for the admin path from `next/meta`. *(2026-09-17, admin path)*
|
|
814
|
+
- Scripts and the seed write through the stores and expire nothing; a
|
|
815
|
+
`revalidate` hook for out-of-band writers. *(2026-09-11, queries)*
|
|
816
|
+
- Delivery caching (checked 2026-09-08): Supabase Storage's basic CDN
|
|
817
|
+
fronts every public-bucket object on a hosted project with no code on
|
|
818
|
+
our side, and the Smart CDN (Pro plan, automatic) only adds
|
|
819
|
+
invalidation on overwrite, which never happens — objects are
|
|
820
|
+
immutable, no upsert on either path, Replace picks a new id. So both
|
|
821
|
+
upload paths now set a year-long `cache-control`
|
|
822
|
+
(`MEDIA_CACHE_SECONDS`): the `cacheControl` option on the server-side
|
|
823
|
+
upload, the header on the browser's signed PUT (without it a signed
|
|
824
|
+
upload lands as `no-cache`; Storage's default is one hour). The live
|
|
825
|
+
suite reads the header back from the local stack. Not yet verified
|
|
826
|
+
against a hosted project's CDN edge; check `cf-cache-status` and
|
|
827
|
+
`cache-control` on a real upload there once. Remaining: in v1 the
|
|
828
|
+
site's `<SmoodlyImage>` goes through `next/image`, so the visitor hits
|
|
829
|
+
the app server's image cache and only its origin fetch touches the
|
|
830
|
+
CDN; video, files and SVGs hit the CDN directly. A Supabase Image
|
|
831
|
+
Transformations loader (`/storage/v1/render/image/public/...`, Pro
|
|
832
|
+
only, must stay optional) would move resizing to the CDN — the transform
|
|
833
|
+
loader under Cloud. *(2026-09-08, media library)*
|
|
834
|
+
- `examples/site/smoodly/server.ts` should carry `import "server-only"` now that it also exports `publicAuth` — a client-side import would fail loudly at build instead of at runtime. *(2026-09-04)*
|
|
835
|
+
- Empty locked zones on the site side: `renderPage` renders nothing for a locked zone with no node. A page that predates the zone shows no prefooter on the published site until an editor opens it (the editor fills and saves). Options: render the component from `sample` when the zone is empty, or fill on `createPage`/schema load server-side. Fine for now — seeded and admin-created pages both arrive filled. *(2026-09-03)*
|
|
732
836
|
|
|
733
|
-
|
|
837
|
+
#### The editors
|
|
734
838
|
|
|
735
|
-
- Scan paging: `health.scan` makes one bulk read per table (versions by
|
|
736
|
-
id in chunks of 100), and PostgREST's max-rows would cap a very large
|
|
737
|
-
space. Page it, and add a rail badge, when a real site's size asks.
|
|
738
|
-
- Refs inside unknown nodes are not indexed: without a schema nothing
|
|
739
|
-
says which values are refs, so safe-delete cannot see them. Publish
|
|
740
|
-
refuses such nodes and a live one renders nothing.
|
|
741
|
-
- Leftover shared values after `.localized()` is turned on stay on the
|
|
742
|
-
entry as every locale's fallback; removing them for good waits on
|
|
743
|
-
the fix tooling (OQ 35). `localization-on` is reported only where a
|
|
744
|
-
locale has no value of its own, so a fully translated entry no
|
|
745
|
-
longer lists it.
|
|
746
839
|
- A visual global item's settings misfits (`settings.x`) don't reach
|
|
747
840
|
their widgets in the Globals form: `GlobalFields` keys errors by the
|
|
748
841
|
bare field key, the same gap the validator's refusal keys had before.
|
|
749
|
-
The notice and the refusal list still name them.
|
|
750
|
-
- A
|
|
751
|
-
the
|
|
842
|
+
The notice and the refusal list still name them. *(2026-09-27, editor safety)*
|
|
843
|
+
- A `refList` on a global item's `settings` still loads no options in
|
|
844
|
+
the Globals form or the page editor: both collect targets from the
|
|
845
|
+
item's `fields` only. *(2026-09-17, reference picker and tags)*
|
|
846
|
+
- A validation failure on a shared save retries with backoff like a
|
|
847
|
+
transport failure; the field error shows on the widget meanwhile. A
|
|
848
|
+
distinct "fix the field" state would stop the retries. *(2026-09-13, shared inline editing)*
|
|
849
|
+
- Escape inside any inspector control bubbles to the editor's window
|
|
850
|
+
handler and clears the block selection — the link popover stops it, a
|
|
851
|
+
textarea or the TipTap content does not. A `keydown` guard on
|
|
852
|
+
`.sm-inspector` would fix it for every control. *(2026-09-08, richtext widget)*
|
|
853
|
+
|
|
854
|
+
#### Admin
|
|
855
|
+
|
|
856
|
+
- Left open by the pre-install fixes' final review (2026-09-29): the pill
|
|
857
|
+
has `href="#"` on Exit and no keyboard way to move it; it appears after
|
|
858
|
+
a soft navigation from a non-Smoodly route only once the page reloads
|
|
859
|
+
(React never runs a script it creates on the client); a remounted view's
|
|
860
|
+
`pages.get` can read the draft before the outgoing view's `close()` save
|
|
861
|
+
commits (the next edit then meets `StaleBanner`); an in-flight save that
|
|
862
|
+
fails after `close()` is not retried; a required `radio` with no value
|
|
863
|
+
shows no pill pressed while the list item now stores the first option,
|
|
864
|
+
and `nodeFromSample` seeds a required `select` as `""`; `useLeaveGuard`
|
|
865
|
+
has no caller left; `examples/site/.env.local.example` lacks `SITE_URL`.
|
|
866
|
+
*(2026-09-29, pre-install fixes)*
|
|
867
|
+
- `admin/ui/theme.tsx` pulls Public Sans from Google Fonts with an `@import`.
|
|
868
|
+
Fine today; a self-hosted font (or `next/font` in the glue layer) removes the
|
|
869
|
+
third-party request and the flash before it loads. *(2026-09-05)*
|
|
752
870
|
|
|
753
|
-
|
|
871
|
+
#### Performance
|
|
754
872
|
|
|
755
|
-
-
|
|
756
|
-
|
|
757
|
-
`
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
- A redirect row has no audit columns (who, when); add them with a
|
|
761
|
-
need. The Redirects page is not paged.
|
|
762
|
-
- The browser suite covers rename, the unticked box, no chains, the
|
|
763
|
-
Settings tab's list and the manual add/remove; a parent's MOVE
|
|
764
|
-
redirecting a child's URL is proven at the store contract and ops
|
|
765
|
-
level only (no tree drag in the suite yet).
|
|
766
|
-
- `remove` on the Supabase index deletes by target, one statement per
|
|
767
|
-
target (the RPC now spares redirect rows); fine at subtree sizes.
|
|
873
|
+
- `pages.get` makes four sequential store round trips for a non-root page
|
|
874
|
+
(`getPage`, `getDraftTree`, `pathsOf`, and the parent read behind
|
|
875
|
+
`parentLocales`; three for a root, which has no parent to read), and the
|
|
876
|
+
editor calls it on load and after every rename — one store call
|
|
877
|
+
returning them all would cut the editor's latency. *(2026-09-06)*
|
|
768
878
|
|
|
769
|
-
|
|
879
|
+
#### Code health
|
|
770
880
|
|
|
771
|
-
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
auth project's redirect allow-list; a same-origin check in the op is
|
|
777
|
-
one line when wanted.
|
|
778
|
-
- Every op call with a junk token costs one `auth.getUser` against the
|
|
779
|
-
auth project; rate limiting is the session-model spec's (OQ 20).
|
|
780
|
-
- The content-surface sweep: `f.link` values reach section components
|
|
781
|
-
unfiltered, uploads allow SVG and PDF on a public bucket (SVG served
|
|
782
|
-
with `?download=`), the custom CSS refusal list, richtext hrefs.
|
|
783
|
-
- A refused private-channel join surfaces as `CHANNEL_ERROR` (about 80
|
|
784
|
-
ms on the local stack); the suite asserts "not SUBSCRIBED".
|
|
785
|
-
- The isolation suite's `join` helper still uses a bare
|
|
786
|
-
`realtime.setAuth(key)`: its positive cases resolve on the first
|
|
787
|
-
SUBSCRIBED, before realtime-js re-asks the default `accessToken`
|
|
788
|
-
callback and the server closes the channel. Fine for proving the
|
|
789
|
-
policy; a client that must stay joined passes `accessToken` (what
|
|
790
|
-
`connectRealtime` does since 3ed8931).
|
|
791
|
-
|
|
792
|
-
## Follow-ups (logged 2026-09-26, concurrency spec)
|
|
793
|
-
|
|
794
|
-
- A hard lock with takeover, if a real team asks: presence has the facts.
|
|
795
|
-
- Presence on the members and media screens; today only pages, entries and
|
|
796
|
-
global items track.
|
|
797
|
-
- Postgres Changes for live list refresh; the lists still load on navigation.
|
|
798
|
-
- Field-level merging of two editors' changes; the guard refuses whole documents.
|
|
799
|
-
- Publish, discard and restore compare the token, then move the pointer under a
|
|
800
|
-
pointer filter: a save that rewrites the same draft row in place between the
|
|
801
|
-
two is published unseen (one round trip wide). A SQL function would close it.
|
|
802
|
-
- Publish leaves the token unchanged (the pointer and its saved time do not move),
|
|
803
|
-
so a publish with a token from before another person's publish of the same
|
|
804
|
-
draft succeeds — the content is the same. Recorded, not a bug.
|
|
805
|
-
- The read client over the read key (DESIGN.md OQ 31) can reuse `publicContent()`.
|
|
806
|
-
- A dropped socket clears presence on Realtime's timeout (tens of seconds); the
|
|
807
|
-
lock outlives a closed tab by that much.
|
|
808
|
-
- The live suites wipe `members` and the content tables; a pane walk after
|
|
809
|
-
them needs `npm run reseed` (or `scripts/demo-editor.ts`) first.
|
|
881
|
+
- `classify` in `ops-impl.ts` is ~20 order-sensitive substring checks over
|
|
882
|
+
store messages — `"already exists in locale"` must precede
|
|
883
|
+
`"already exists"`, or an addLocale conflict reads as a slug conflict. A
|
|
884
|
+
typed code on `pageErrors` (thrown and matched, not re-parsed) would
|
|
885
|
+
remove the ordering hazard before plan 2 adds the entry messages. *(2026-09-06)*
|
|
810
886
|
|
|
811
|
-
|
|
887
|
+
#### Tests and CI
|
|
812
888
|
|
|
889
|
+
- The blank-intl starter is typechecked, not walked: the first tester-agent
|
|
890
|
+
mission (each tester site on its own local Supabase stack, two at a time),
|
|
891
|
+
after the 0.0.35 release. *(2026-09-15, locale alternates)*
|
|
813
892
|
- Drag and drop has no browser test (spec §4): the outline, the pages
|
|
814
893
|
tree and list rows reorder by real pointer drags; the first plan that
|
|
815
|
-
touches `useSortable` adds them.
|
|
894
|
+
touches `useSortable` adds them. *(2026-09-26, browser suite)*
|
|
816
895
|
- The suite runs Chromium only; the admin's WebKit and Firefox
|
|
817
|
-
behaviour is unverified.
|
|
818
|
-
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
896
|
+
behaviour is unverified. *(2026-09-26, browser suite)*
|
|
897
|
+
- The seed's early exit probes `/` alone, and `createPage` writes the path
|
|
898
|
+
row before the tree is published, so a crash between the home record and
|
|
899
|
+
the last page leaves a database every later run reports as "already
|
|
900
|
+
seeded"; recovery is `supabase db reset`. A probe on the last thing the
|
|
901
|
+
seed writes, or a per-step check, would make it resumable. *(2026-09-07, starter)*
|
|
902
|
+
- The package root (`src/index.ts`) must stay free of Node builtins:
|
|
903
|
+
every scaffolded section does `import { f, smoodly } from "smoodly"`,
|
|
904
|
+
and a `"use client"` section's browser build would fail on one.
|
|
905
|
+
`sharedEntryId` (`src/shared-id.ts`) moved off `node:crypto` onto Web
|
|
906
|
+
Crypto for this reason (fix pass, 2026-09-07). Nothing tests the
|
|
907
|
+
invariant itself — a future addition that reaches for `node:fs`,
|
|
908
|
+
`node:crypto`, etc. from a module reachable through the root would
|
|
909
|
+
regress it silently. *(2026-09-07, shared content)*
|
|
910
|
+
- The single-language branch — no locale selector in the Pages list, no
|
|
911
|
+
switcher, no add/remove — has no automated coverage; only three sites
|
|
912
|
+
branch on the locale count (`PagesList.tsx`, `EditorView.tsx` and
|
|
913
|
+
`page-renderer.tsx`'s alternates), so a regression there shows up only
|
|
914
|
+
in a scaffolded single-language app. *(2026-09-06)*
|
|
915
|
+
- 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. — Partly resolved 2026-09-26: the `browser` job builds the example site and drives it, but with a database; the no-database build is still unscripted. *(2026-09-03)*
|
|
916
|
+
|
|
917
|
+
### Later
|
|
918
|
+
|
|
919
|
+
#### Page editor and inline editing
|
|
920
|
+
|
|
921
|
+
- The page editor's publish banner lists fields as plain text; the forms'
|
|
922
|
+
lines focus the widget. Selecting the node from a `node.field` path is
|
|
923
|
+
the missing piece. *(2026-09-14, collections draft/publish)*
|
|
924
|
+
- Richtext instant write from the inspector: the pure `RichText` render
|
|
925
|
+
swapped into the container while the widget is typed in. *(2026-09-10, inline editing)*
|
|
926
|
+
- Item-level chrome in the canvas ("Key points · 3 of 4" on hover, add,
|
|
927
|
+
remove, reorder in place, later drag within a list): the item marks are
|
|
928
|
+
the hook. *(2026-09-10, inline editing)*
|
|
929
|
+
- Placeholder and validation marks on the page (the sample-sourced
|
|
930
|
+
placeholder treatment, "required, empty" outlines) wait for the tree's
|
|
931
|
+
placeholder marking. *(2026-09-10, inline editing)*
|
|
932
|
+
- A coverage hint for developers: the admin knows a section's text fields
|
|
933
|
+
and could list the ones without a mark. *(2026-09-10, inline editing)*
|
|
934
|
+
- AI verbs ("rewrite this heading") need a field under the cursor, which
|
|
935
|
+
a mark is. *(2026-09-10, inline editing)*
|
|
936
|
+
- Direct-child spacing rules on a richtext container (Tailwind `space-y-*`,
|
|
937
|
+
`.prose > p`) do not reach the canvas editor's blocks during a session;
|
|
938
|
+
flex and grid gaps are copied (onto the ProseMirror root since the
|
|
939
|
+
walk, 69aecc6). Documented, not fixed. *(2026-09-10, inline editing)*
|
|
940
|
+
- The floating toolbar flips below the caret only near the top of the
|
|
941
|
+
DOCUMENT (where the page box clips it); it scrolls with the content, so
|
|
942
|
+
a caret at the top of the scrolled canvas has its toolbar out of view
|
|
943
|
+
until the editor scrolls. A flip keyed to the scroller's viewport would
|
|
944
|
+
fix it. *(2026-09-10, inline editing)*
|
|
945
|
+
- No starter section gives the canvas a richtext with blocks: the Image
|
|
946
|
+
and text body is marks-only, and the article body (the `article`
|
|
947
|
+
toolset) is edited in the entry form. The walk borrowed the article
|
|
948
|
+
toolset for the block steps; a starter section with a long-form body
|
|
949
|
+
would let the canvas show them. *(2026-09-10, inline editing)*
|
|
950
|
+
- The caret placed right after an existing `/` reopens the slash menu
|
|
951
|
+
(TipTap's suggestion re-triggers on the character before the caret).
|
|
952
|
+
Harmless; a trigger keyed to typing only would be stricter. *(2026-09-10, inline editing)*
|
|
869
953
|
|
|
870
|
-
|
|
954
|
+
#### Richtext and elements
|
|
871
955
|
|
|
872
|
-
-
|
|
956
|
+
- Image sizing and float in a body (full width, wide, small, left/right):
|
|
957
|
+
a later additive `styles` on the image node; the site's CSS answers it. *(2026-09-08, block nodes)*
|
|
958
|
+
- Inline elements: `inline: true` on the registration; cursor, marks and
|
|
959
|
+
selection around an inline atom are a separate node-view effort. *(2026-09-08, block nodes)*
|
|
960
|
+
- The column context (`contexts: ["column"]`) waits for Phase 3 rows;
|
|
961
|
+
accepted and unused today. *(2026-09-08, block nodes)*
|
|
962
|
+
- Element copy between fields: a pasted element the target toolset
|
|
963
|
+
forbids is dropped silently; a notice would be kinder. *(2026-09-08, block nodes)*
|
|
964
|
+
- `hasContent` treats an unresolved image node as content, so a body
|
|
965
|
+
whose only image was deleted from the library still passes "required". *(2026-09-08, block nodes)*
|
|
966
|
+
- The "+" menu and the slash menu share `RichtextInsertMenu` but keep
|
|
967
|
+
separate active-row state and key handling; one keyboard model would
|
|
968
|
+
be smaller. *(2026-09-08, block nodes)*
|
|
969
|
+
- A page and entry picker in the link popover: a link inside a richtext
|
|
970
|
+
document is a reference the refs index, the resolver and the localizer
|
|
971
|
+
cannot see (they address refs by top-level key). Its own spec. *(2026-09-08, richtext widget)*
|
|
972
|
+
- Every richtext descriptor now carries its toolset, so the serialized
|
|
973
|
+
registry grew by a few bytes per field; if the AI-facing schema wants
|
|
974
|
+
the vocabulary once instead of per field, dedupe there. *(2026-09-08, richtext widget)*
|
|
873
975
|
|
|
874
|
-
|
|
976
|
+
#### Forms, fields and pickers
|
|
875
977
|
|
|
876
978
|
- A `Create…` button in the reference picker: create the target entry
|
|
877
979
|
without leaving the form. Needs its own answer for modal vs navigation
|
|
878
|
-
and for the parent form's pending autosave.
|
|
879
|
-
- Server search for ref candidates: `useRefOptions` loads the whole
|
|
880
|
-
target collection once per locale. When a collection outgrows that,
|
|
881
|
-
the hook changes (a search op, plus "titles for these ids" for the
|
|
882
|
-
picked rows); `RefPicker` takes options as a prop and does not.
|
|
980
|
+
and for the parent form's pending autosave. *(2026-09-17, reference picker and tags)*
|
|
883
981
|
- Thumbnails in picker rows: collections have no preview-image
|
|
884
|
-
convention (`titleField` only).
|
|
885
|
-
-
|
|
886
|
-
`jsonb_array_elements` when a collection outgrows it.
|
|
887
|
-
- Tags keep insertion order; no drag reorder.
|
|
982
|
+
convention (`titleField` only). *(2026-09-17, reference picker and tags)*
|
|
983
|
+
- Tags keep insertion order; no drag reorder. *(2026-09-17, reference picker and tags)*
|
|
888
984
|
- Tag suggestions exist on collection entry forms only, and only for
|
|
889
985
|
top-level `f.tags()` fields: a tags field inside an object, on a
|
|
890
|
-
section, a global or a page is a plain chip input.
|
|
986
|
+
section, a global or a page is a plain chip input. *(2026-09-17, reference picker and tags)*
|
|
891
987
|
- The picker's "open" link goes to `/admin/<collection>/<id>` without
|
|
892
|
-
the edited locale; the form opens in the URL's or the default locale.
|
|
988
|
+
the edited locale; the form opens in the URL's or the default locale. *(2026-09-17, reference picker and tags)*
|
|
893
989
|
- The Combobox list is portaled into the admin wrapper and fixed to the
|
|
894
990
|
viewport (2026-09-27), so a modal body or scrolling form no longer
|
|
895
991
|
clips it; it flips upwards when the viewport has no room under the
|
|
896
992
|
input (`listPlacement`) and follows the input on scroll. It does not
|
|
897
993
|
yet shrink when neither side has room for it. A single select (a
|
|
898
994
|
single `f.ref`, the redirect target) shows its pick inside the input;
|
|
899
|
-
a single ref's status, open link and remove sit in the input too.
|
|
900
|
-
-
|
|
901
|
-
|
|
902
|
-
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
-
|
|
909
|
-
`
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
995
|
+
a single ref's status, open link and remove sit in the input too. *(2026-09-17, reference picker and tags)*
|
|
996
|
+
- A create dialog (title and slug) before "+ New entry", if empty Untitled
|
|
997
|
+
drafts turn out to litter lists. *(2026-09-14, collections draft/publish)*
|
|
998
|
+
- `f.date()` and `f.datetime()` share one in-house calendar
|
|
999
|
+
(`admin/forms/DatePicker.tsx` over the pure `date-picker.ts`); the
|
|
1000
|
+
browsers' own pickers were rejected (they differ and take no tokens).
|
|
1001
|
+
Weeks start on Monday and month names come from the browser's language
|
|
1002
|
+
— `weekStart` and a month-name locale as config keys are follow-ups. *(2026-09-25, date pickers)*
|
|
1003
|
+
- Not built: a time-only field, a date range, `.min` / `.max` on dates. *(2026-09-25, date pickers)*
|
|
1004
|
+
- Refs inside lists and objects are refused at build time (`f.list`,
|
|
1005
|
+
`f.object`). A list of objects each holding a ref (a featured entry with
|
|
1006
|
+
its own caption) needs the resolver, the refs index and the admin's
|
|
1007
|
+
ref-option loading to address nested paths; add when a site asks. *(2026-09-07, list field)*
|
|
1008
|
+
- `emptyValue` for an `image`/`video`/`file` item is `undefined`, so a new
|
|
1009
|
+
media row starts empty; a required image item is then a validation
|
|
1010
|
+
error until filled, which is the intended nudge but reads as an error
|
|
1011
|
+
on a row the editor just added. *(2026-09-07, list field)*
|
|
1012
|
+
- A list item's subtree still uses the row index as its React key, so a list
|
|
1013
|
+
of objects each holding a list hands the inner rows another item's collapse
|
|
1014
|
+
state when an outer row moves; fix with a stable per-item key when nested
|
|
1015
|
+
lists get real use. *(2026-09-07, list field)*
|
|
915
1016
|
|
|
916
|
-
|
|
1017
|
+
#### Custom sections and section settings
|
|
917
1018
|
|
|
918
1019
|
- Inline editing of a column on the canvas is OFF (2026-09-15, after the
|
|
919
1020
|
first day's use): a click on a column reveals its editor in the
|
|
920
1021
|
inspector (`onFieldClick` in `EditorView.tsx` skips the session for the
|
|
921
1022
|
`custom` type). The marks, paths and session code are in place and
|
|
922
|
-
tested; turn it on once a walk on a real device passes.
|
|
923
|
-
- A canvas drag on column edges (today the bar in the inspector).
|
|
924
|
-
- A background for a custom section from a config palette.
|
|
925
|
-
- Elements as direct column children (today through richtext's menus).
|
|
926
|
-
- A per-zone cap on columns (`custom: { columns: 2 }` or similar).
|
|
927
|
-
-
|
|
928
|
-
|
|
929
|
-
a project without those variables gets full-width rows with a 1rem
|
|
930
|
-
gutter. Documented, not enforced.
|
|
931
|
-
- Presets exist for 2, 3 and 4 columns; 5 and 6 have the bar only.
|
|
932
|
-
- Rows reorder with the ↑/↓ buttons only; no grip, no keyboard drag.
|
|
1023
|
+
tested; turn it on once a walk on a real device passes. *(2026-09-15, custom section)*
|
|
1024
|
+
- A canvas drag on column edges (today the bar in the inspector). *(2026-09-15, custom section)*
|
|
1025
|
+
- A background for a custom section from a config palette. *(2026-09-15, custom section)*
|
|
1026
|
+
- Elements as direct column children (today through richtext's menus). *(2026-09-15, custom section)*
|
|
1027
|
+
- A per-zone cap on columns (`custom: { columns: 2 }` or similar). *(2026-09-15, custom section)*
|
|
1028
|
+
- Presets exist for 2, 3 and 4 columns; 5 and 6 have the bar only. *(2026-09-15, custom section)*
|
|
1029
|
+
- Rows reorder with the ↑/↓ buttons only; no grip, no keyboard drag. *(2026-09-15, custom section)*
|
|
933
1030
|
- An empty column has no box on the live page (an empty paragraph
|
|
934
1031
|
renders 0px tall), so it cannot be clicked into from the canvas; the
|
|
935
1032
|
inspector's `Column N` editor is the way in. A `min-height` in editor
|
|
936
|
-
mode would give it a target.
|
|
1033
|
+
mode would give it a target. *(2026-09-15, custom section)*
|
|
937
1034
|
- An empty column publishes (its richtext descriptor is `optional`);
|
|
938
|
-
whether publish should refuse a row of only empty columns is open.
|
|
1035
|
+
whether publish should refuse a row of only empty columns is open. *(2026-09-15, custom section)*
|
|
939
1036
|
- Growing a row copies the LAST span before renormalizing, so `[5, 7]` →
|
|
940
1037
|
`[3, 5, 4]`, not thirds; a preset is one click away. Equal thirds on
|
|
941
|
-
grow would be the other reasonable rule.
|
|
942
|
-
- The picker's click cannot be unit-tested (no DOM in the suite); the
|
|
943
|
-
walk covers it. A jsdom environment for the admin tests is a future
|
|
944
|
-
tooling item.
|
|
945
|
-
- No migration: the `rows: true` zone flag became `custom: true` and
|
|
946
|
-
`config.rows` is gone; no stored data changed shape.
|
|
947
|
-
- A column carries no project class: the starters extend their `.prose`
|
|
948
|
-
rules to `.smd-col` so headings, figures and callouts inside a column
|
|
949
|
-
read like body text. A project on its own stylesheet does the same, or
|
|
950
|
-
targets `[data-custom]`. A `columnClass` in `config.custom` would
|
|
951
|
-
remove that step.
|
|
952
|
-
|
|
953
|
-
## Follow-ups (logged 2026-09-15, section settings spec, plan A)
|
|
954
|
-
|
|
955
|
-
Upgrade note: stored trees, global entries and element nodes changed
|
|
956
|
-
shape (`styles` → `settings`, `meta` gone, the `global` node type and
|
|
957
|
-
`global:` ids, `$settings`/`$styles`/`$css` on global rows). No migration
|
|
958
|
-
tool — reset and reseed a development database (`supabase db reset`,
|
|
959
|
-
`npm run seed`); no installs predate the change.
|
|
960
|
-
|
|
1038
|
+
grow would be the other reasonable rule. *(2026-09-15, custom section)*
|
|
961
1039
|
- A class-mapping API for projects that want the wrapper to emit
|
|
962
|
-
classes instead of inline values (DESIGN.md OQ 13).
|
|
963
|
-
- `f.global()` — a ref field on a section that renders a global item.
|
|
1040
|
+
classes instead of inline values (DESIGN.md OQ 13). *(2026-09-15, section settings)*
|
|
1041
|
+
- `f.global()` — a ref field on a section that renders a global item. *(2026-09-15, section settings)*
|
|
964
1042
|
- An AI helper filling the CSS box from a prompt (the box stores CSS,
|
|
965
|
-
never a prompt, so the helper only fills it).
|
|
966
|
-
- `and` / `or` conditions; today one `f.when()` per field.
|
|
1043
|
+
never a prompt, so the helper only fills it). *(2026-09-15, section settings)*
|
|
1044
|
+
- `and` / `or` conditions; today one `f.when()` per field. *(2026-09-15, section settings)*
|
|
967
1045
|
- A condition inside a list item can read the item's siblings and the
|
|
968
1046
|
top-level namespaces, not another item; per-item scoped `settings.`
|
|
969
|
-
references inside lists are not a thing yet.
|
|
1047
|
+
references inside lists are not a thing yet. *(2026-09-15, section settings)*
|
|
970
1048
|
- An element's settings cannot be hidden by the enclosing section's
|
|
971
1049
|
settings: an element card's scope is the element's own fields and
|
|
972
|
-
settings.
|
|
1050
|
+
settings. *(2026-09-15, section settings)*
|
|
973
1051
|
- A hidden field's refs still count as usage in the refs index (its value
|
|
974
|
-
is kept, so the edge is real; whether usage should hide with it is open).
|
|
975
|
-
- The css refusal list grew past the spec's verbatim six in a security
|
|
976
|
-
review of the wrapper: the regexes run on the escape-decoded snippet too
|
|
977
|
-
(`\75 rl(` cannot spell `url(`) and unbalanced braces are refused as
|
|
978
|
-
`Braces must balance.` (a `}` cannot step out of the `[data-smd-node]`
|
|
979
|
-
block). Recorded here and in DESIGN.md.
|
|
980
|
-
- The starters' `.prose figure img` is now inline-block without
|
|
981
|
-
`width: 100%`, so a richtext image narrower than the column follows the
|
|
982
|
-
section's `align` step; a photo wider than the column still fills it.
|
|
983
|
-
- The Globals page has no tab row: Content's groups, then a `Settings`
|
|
984
|
-
label and the Settings groups, stacked. A tab row there would match the
|
|
985
|
-
canvas card.
|
|
986
|
-
- `InspectorFields` filters hidden entries per render; a section with
|
|
987
|
-
many conditions re-evaluates every keystroke. Fine at the sizes seen.
|
|
1052
|
+
is kept, so the edge is real; whether usage should hide with it is open). *(2026-09-15, section settings)*
|
|
988
1053
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
- Publish integrity for an entry's own refs: an entry can publish while
|
|
992
|
-
pointing at an unpublished entry; pages are gated, entries are not.
|
|
993
|
-
- A create dialog (title and slug) before "+ New entry", if empty Untitled
|
|
994
|
-
drafts turn out to litter lists.
|
|
995
|
-
- The page editor's publish banner lists fields as plain text; the forms'
|
|
996
|
-
lines focus the widget. Selecting the node from a `node.field` path is
|
|
997
|
-
the missing piece.
|
|
998
|
-
- Discard on a published row from before versioning that nobody has
|
|
999
|
-
written since is refused as "nothing published" (its pointers are null);
|
|
1000
|
-
the control never offers it there, so nothing shows — the store could
|
|
1001
|
-
answer it as a no-op instead.
|
|
1002
|
-
|
|
1003
|
-
## Follow-ups (logged 2026-09-25, settings + members spec)
|
|
1004
|
-
|
|
1005
|
-
- Existing databases (every install so far is ours; DESIGN.md OQ #2):
|
|
1006
|
-
`alter table "editors" rename to "members";`
|
|
1007
|
-
`alter table "members" add column "email" text;`
|
|
1008
|
-
`alter table "space_caps" rename to "spaces";`
|
|
1009
|
-
`alter table "spaces" alter column "row_cap" drop not null;`
|
|
1010
|
-
`alter table "spaces" add column "console_url" text;`
|
|
1011
|
-
`update "members" set "role" = 'admin' where "role" = 'owner';`
|
|
1012
|
-
then re-run the regenerated migration's `smoodly_space_row_cap` function
|
|
1013
|
-
and the `smoodly_read` policy on `spaces` (both `create or replace` /
|
|
1014
|
-
`drop … if exists`, so the whole migration file can be re-applied).
|
|
1015
|
-
Policies and triggers follow a table rename. Rows written before the
|
|
1016
|
-
column show their user id on the Members page until re-inserted (the
|
|
1017
|
-
demo-editor upsert fills it on the next seed).
|
|
1018
|
-
- Members management shipped 2026-09-25 (spec
|
|
1019
|
-
`docs/superpowers/specs/2026-09-25-members-management-design.md`); out
|
|
1020
|
-
of its scope: a pending-invite status and "resend invite" on a row
|
|
1021
|
-
(needs the Auth user's `last_sign_in_at`, which the cloud projection
|
|
1022
|
-
does not carry); removing the Auth account together with the member
|
|
1023
|
-
row; invites from the cloud's `/admin` (they are smoodly.io's); a
|
|
1024
|
-
project whose Auth is OAuth-only (the invite mail asks for a password);
|
|
1025
|
-
redirects on a published rename (the confirm says "there are none
|
|
1026
|
-
yet").
|
|
1027
|
-
- 2026-09-26: the Role column is a badge and the row menu has Change role
|
|
1028
|
-
(confirm, to the other role) and Remove member; `ActionMenu` popovers are
|
|
1029
|
-
fixed-positioned from the button — the cell's overflow clipped them.
|
|
1030
|
-
- The members list is not paged (tens of rows, not thousands).
|
|
1031
|
-
- The Roles and API Keys pages were dropped 2026-09-25 (DESIGN.md §2
|
|
1032
|
-
"Settings"): Members is the only Settings page, the invite modal
|
|
1033
|
-
describes the two roles, and `/settings/roles` and `/settings/api-keys`
|
|
1034
|
-
are Not found.
|
|
1035
|
-
|
|
1036
|
-
## Follow-ups (logged 2026-09-13, history spec)
|
|
1037
|
-
|
|
1038
|
-
- Existing databases (every install so far is ours; DESIGN.md OQ #2):
|
|
1039
|
-
`alter table "page_locales" add column "updated_by" text;`
|
|
1040
|
-
`alter table "entry_locales" add column "updated_by" text;`
|
|
1041
|
-
`alter table "page_versions" rename column "created_at" to "saved_at";`
|
|
1042
|
-
`alter table "page_versions" drop column "created_by";`
|
|
1043
|
-
`alter table "entry_versions" rename column "created_at" to "saved_at";`
|
|
1044
|
-
`alter table "entry_versions" drop column "created_by";`
|
|
1045
|
-
The indexes follow the rename. Autosave rows from before read as
|
|
1046
|
-
extra history rows until the page is published again; harmless.
|
|
1047
|
-
Since 2026-09-14 every collection is versioned and `collectionSQL` emits one
|
|
1048
|
-
view shape: re-apply `collectionSQL(collection)` for each collection on an
|
|
1049
|
-
existing install (`supabase db reset` locally), or a published row from
|
|
1050
|
-
before versioning reads through the view's live-row fallback until its
|
|
1051
|
-
first write gives it a snapshot — either way nothing goes dark.
|
|
1052
|
-
- Client-side undo in the editor (a stack of tree snapshots); the store
|
|
1053
|
-
is no longer an undo stack by design.
|
|
1054
|
-
- The author per version row and in the lists; named versions.
|
|
1055
|
-
- History on the canvas card's menu and in the list row menus.
|
|
1056
|
-
- A diff between two versions.
|
|
1057
|
-
- Ops are built per call (`createAdminOps` inside the handler) so the
|
|
1058
|
-
session rides in as `actor`; if that ever measures, memoize the
|
|
1059
|
-
actor-free parts.
|
|
1060
|
-
|
|
1061
|
-
## Follow-ups (logged 2026-09-13, publish flow spec)
|
|
1062
|
-
|
|
1063
|
-
- Unpublishing a page does not cascade: a published child keeps serving
|
|
1064
|
-
under an unpublished parent's path. Decide whether it should.
|
|
1065
|
-
- Versioning collections by default — decided 2026-09-14: every
|
|
1066
|
-
collection, no flag, the forms autosave (spec
|
|
1067
|
-
`docs/superpowers/specs/2026-09-14-collections-draft-publish-design.md`).
|
|
1068
|
-
- A draft-mode banner on the site, or an "exit preview" action in the
|
|
1069
|
-
admin: the same browser shows drafts of unpublished items after an
|
|
1070
|
-
Unpublish, which reads as "it published my draft".
|
|
1071
|
-
- The lists' row menus (Pages, Entries) do not offer Unpublish; the
|
|
1072
|
-
document's own control does.
|
|
1073
|
-
|
|
1074
|
-
## Follow-ups (logged 2026-09-13, shared inline editing spec)
|
|
1054
|
+
#### Globals
|
|
1075
1055
|
|
|
1076
1056
|
- Layout items in the outline: a "From the layout" group the bridge could
|
|
1077
|
-
populate from the `shared:` ids it reports; canvas-only today.
|
|
1078
|
-
- The page autosave loses an edit made within the debounce window before
|
|
1079
|
-
navigating away (`useDraftAutosave` only clears timers on unmount). The
|
|
1080
|
-
shared controller flushes on unmount; the page's should too.
|
|
1081
|
-
- A validation failure on a shared save retries with backoff like a
|
|
1082
|
-
transport failure; the field error shows on the widget meanwhile. A
|
|
1083
|
-
distinct "fix the field" state would stop the retries.
|
|
1057
|
+
populate from the `shared:` ids it reports; canvas-only today. *(2026-09-13, shared inline editing)*
|
|
1084
1058
|
- The shared card's usage line lists page ids; titles need a pages
|
|
1085
|
-
lookup the editor does not hold (the usage rows carry only ids).
|
|
1059
|
+
lookup the editor does not hold (the usage rows carry only ids). *(2026-09-13, shared inline editing)*
|
|
1086
1060
|
- A "save and publish" action for sites that want no draft stage on a
|
|
1087
1061
|
shared item (every item is versioned since 2026-09-13; the flag is
|
|
1088
|
-
gone).
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
- `
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
the
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
- OQ 26: whether page units should carry `pages:tree` too, replacing
|
|
1108
|
-
the neighbour list.
|
|
1109
|
-
- Scripts and the seed write through the stores and expire nothing; a
|
|
1110
|
-
`revalidate` hook for out-of-band writers.
|
|
1111
|
-
- `getEntriesTree` has no `q` counterpart yet; add one when a site
|
|
1112
|
-
needs a nested collection nav.
|
|
1113
|
-
- `getShared(["header", "footer", "contact"], { locale })` returning a
|
|
1114
|
-
typed tuple, as sugar over the `Promise.all` the layout writes today.
|
|
1115
|
-
Keep one cache unit per item under its own tag — a single unit tagged
|
|
1116
|
-
with all three would expire the header whenever the footer is edited.
|
|
1117
|
-
- `getEntry` by id goes through `site.getEntry`, which lists the whole
|
|
1118
|
-
collection to find one row; a primary-key read first would be cheaper
|
|
1119
|
-
on the uncached path (draft mode, first miss).
|
|
1120
|
-
- The read API returns the site layer's shapes unprojected: `getEntries`
|
|
1121
|
-
gives `{ entry: EntryRecord, path }` (the locales map, sort, parentId,
|
|
1122
|
-
collection and timestamps ride along under `entry`), `getPage` gives
|
|
1123
|
-
the renderer's `SitePage` (`record`, `tree`). Project in `queries.ts`,
|
|
1124
|
-
at the public boundary, to `{ id, path, fields, updatedAt }` per entry
|
|
1125
|
-
and a page without renderer internals; the tree item is already lean.
|
|
1126
|
-
Do it before the Astro glue (OQ 29) wraps the same reads and before
|
|
1127
|
-
a second site depends on the shape — it is a breaking change to `q`.
|
|
1128
|
-
|
|
1129
|
-
## Follow-ups (logged 2026-09-10, page fields and SEO spec)
|
|
1130
|
-
|
|
1131
|
-
- SEO for entry pages: entries have no `meta`; a `titleField`-based
|
|
1132
|
-
title and a description field are the next step.
|
|
1133
|
-
- A `styles` block on pages, when a case needs presentation separate
|
|
1134
|
-
from content.
|
|
1135
|
-
- Refs in page fields, once the refs index walks them.
|
|
1136
|
-
- A canonical URL field and a search-result preview snippet in the
|
|
1137
|
-
SEO & Social tab.
|
|
1138
|
-
- Configurable SEO set (`meta: { seo: { twitter: true } }`) if the four
|
|
1139
|
-
values prove too few.
|
|
1140
|
-
- Link fields are raw: an image in a page field reaches a nav link as
|
|
1141
|
-
`{ asset, alt }`, not a URL. Resolve on demand if a nav ever wants one.
|
|
1142
|
-
- The social image URL goes through the default `next` loader at 1200px;
|
|
1143
|
-
the cloud's transform loader (OQ 28) should serve a real 1200×630.
|
|
1144
|
-
- Page-field labels derive from the key (`showInNavigation` → "Show In
|
|
1145
|
-
Navigation"), as every field's do; a `.label()` builder option would
|
|
1146
|
-
read better for long keys.
|
|
1062
|
+
gone). *(2026-09-13, shared inline editing)*
|
|
1063
|
+
- The picker offers a shared item only in zones whose allow list includes
|
|
1064
|
+
its section (`allowedShared` in `admin/tree-ops.ts`), so a shared item's
|
|
1065
|
+
section is always also offered as an ordinary section. A "placeable
|
|
1066
|
+
only" mode (a shared item whose section no zone lists) would let the
|
|
1067
|
+
starter keep the quote shared-only. *(2026-09-07, starter)*
|
|
1068
|
+
- `f.shared(() => item)` — a ref field on a section that renders a shared
|
|
1069
|
+
item inside the section's own markup (spec 2026-09-07 decision 3). *(2026-09-07, shared content)*
|
|
1070
|
+
- Instance mode: a shared *type* with many editor-created items; today
|
|
1071
|
+
the answer is a collection plus a section with `f.ref` (decision 1). *(2026-09-07, shared content)*
|
|
1072
|
+
- Zone-shaped shared items (`smoodly.shared({ zone })`) (decision 2). *(2026-09-07, shared content)*
|
|
1073
|
+
- Standalone live preview on a visual item's admin page — would need a
|
|
1074
|
+
preview route in the glue. Less pressing since 2026-09-13: every shared
|
|
1075
|
+
render is edited in the page canvas, and the Globals page is the form. *(2026-09-07, shared content)*
|
|
1076
|
+
- Built-in meta on shared items and on placements (anchor per placement
|
|
1077
|
+
is the likely shape). *(2026-09-07, shared content)*
|
|
1078
|
+
- A placement of an object item or an unregistered item gates the page's
|
|
1079
|
+
publish (the refs edge is emitted without a registry lookup) yet never
|
|
1080
|
+
renders; only a hand-edited tree can produce one. *(2026-09-07, shared content)*
|
|
1147
1081
|
|
|
1148
|
-
|
|
1082
|
+
#### Drag and drop
|
|
1149
1083
|
|
|
1150
|
-
-
|
|
1151
|
-
|
|
1152
|
-
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
- A
|
|
1159
|
-
|
|
1160
|
-
- AI verbs ("rewrite this heading") need a field under the cursor, which
|
|
1161
|
-
a mark is.
|
|
1162
|
-
- A cross-origin admin needs the bridge-side editing variant (spec §3).
|
|
1163
|
-
- Direct-child spacing rules on a richtext container (Tailwind `space-y-*`,
|
|
1164
|
-
`.prose > p`) do not reach the canvas editor's blocks during a session;
|
|
1165
|
-
flex and grid gaps are copied (onto the ProseMirror root since the
|
|
1166
|
-
walk, 69aecc6). Documented, not fixed.
|
|
1167
|
-
- The text session's Enter commits: a plain text field has no line breaks.
|
|
1168
|
-
A multi-line text control would be its own descriptor.
|
|
1169
|
-
- The floating toolbar flips below the caret only near the top of the
|
|
1170
|
-
DOCUMENT (where the page box clips it); it scrolls with the content, so
|
|
1171
|
-
a caret at the top of the scrolled canvas has its toolbar out of view
|
|
1172
|
-
until the editor scrolls. A flip keyed to the scroller's viewport would
|
|
1173
|
-
fix it.
|
|
1174
|
-
- No starter section gives the canvas a richtext with blocks: the Image
|
|
1175
|
-
and text body is marks-only, and the article body (the `article`
|
|
1176
|
-
toolset) is edited in the entry form. The walk borrowed the article
|
|
1177
|
-
toolset for the block steps; a starter section with a long-form body
|
|
1178
|
-
would let the canvas show them.
|
|
1179
|
-
- The caret placed right after an existing `/` reopens the slash menu
|
|
1180
|
-
(TipTap's suggestion re-triggers on the character before the caret).
|
|
1181
|
-
Harmless; a trigger keyed to typing only would be stricter.
|
|
1182
|
-
- Walk findings, all fixed the same day (69aecc6): the copied flex gap
|
|
1183
|
-
landed on the editor root instead of the ProseMirror element that holds
|
|
1184
|
-
the blocks, so paragraphs lost their spacing; a click on a marked image
|
|
1185
|
-
revealed nothing because the reveal forced the Content tab while the
|
|
1186
|
-
media projection puts the image on Media; Escape after a card's button
|
|
1187
|
-
had taken the focus did not end the session.
|
|
1084
|
+
- Cross-zone drag in the outline: each zone is its own sortable list;
|
|
1085
|
+
moving a block between zones is still insert-and-remove. *(2026-09-09)*
|
|
1086
|
+
- Nested manual collections show no grip (`canReorder` requires
|
|
1087
|
+
`depth === 1`) until the entry tree UI exists — a flat index is not a
|
|
1088
|
+
sibling index there. *(2026-09-09)*
|
|
1089
|
+
- Canvas drag: the overlay should reuse `ui/sortable.ts` across the
|
|
1090
|
+
iframe boundary; `useSortable` measures container children, which the
|
|
1091
|
+
overlay's ghost rows can be. *(2026-09-09)*
|
|
1092
|
+
- A dragged subtree stays collapsed after the drop (`onDragStart`
|
|
1093
|
+
collapses it through `toggle` and nothing re-expands it). *(2026-09-09)*
|
|
1188
1094
|
|
|
1189
|
-
|
|
1095
|
+
#### Admin UI
|
|
1190
1096
|
|
|
1097
|
+
- The Globals page has no tab row: Content's groups, then a `Settings`
|
|
1098
|
+
label and the Settings groups, stacked. A tab row there would match the
|
|
1099
|
+
canvas card. *(2026-09-15, section settings)*
|
|
1191
1100
|
- The warm look is the comparison block now (`WARM_*` + `WARM_EXTRA` in
|
|
1192
1101
|
`src/admin/ui/theme.tsx`, `localStorage.smoodly-look = "warm"`); the
|
|
1193
1102
|
technical block is deleted (tag `look-technical` keeps it). Remove the
|
|
1194
|
-
warm block once the neutral look is judged, the same way.
|
|
1195
|
-
- Kept from the app on purpose, against the handoff: the 14px base and
|
|
1196
|
-
18px titles (handoff 13/20/22), the 64px top bar (56), the 360px
|
|
1197
|
-
inspector (296), the Lucide rail icons, the `--table-top` air above the
|
|
1198
|
-
list, the 12px block-tag padding. Inputs stay white on focus (the
|
|
1199
|
-
handoff's focused textarea takes the tile grey) and the locked outline
|
|
1200
|
-
row is a white row like the rest (the handoff's is the ground, invisible
|
|
1201
|
-
on a tile of the same grey).
|
|
1202
|
-
- The handoff's status chips collapse Draft into amber; the app keeps its
|
|
1203
|
-
three states (Published green, Unpublished changes amber, Draft grey on
|
|
1204
|
-
`--control-hover`).
|
|
1205
|
-
- The Pages list's prompts are dialogs on the same card since the same
|
|
1206
|
-
day (`shell/PageDialogs.tsx` over `ui/Dialog.tsx`): New page / New
|
|
1207
|
-
path / New subpage / New subpath (title, slug following the title
|
|
1208
|
-
through `slugify` in `paths.ts`, page type when more than
|
|
1209
|
-
one open template), Add page here (page type; skipped with one), Rename
|
|
1210
|
-
(title and slug; the home page's title alone). Still native: the
|
|
1211
|
-
`window.alert`s on a refused list op outside a dialog (`run` in
|
|
1212
|
-
PagesList, EntriesList's add/remove) — a notice banner is the follow-up.
|
|
1213
|
-
- `slugify` (`paths.ts`, exported) is the one slugifier; the slug-box
|
|
1214
|
-
helpers in `admin/forms/slug-field.ts` (`followsTitle`, `tidySlug`,
|
|
1215
|
-
`slugHint`, `withFollowingSlug`) serve the page dialogs, page Settings,
|
|
1216
|
-
the `f.slug` widget and the entry form (spec 2026-09-28 ASCII slugs).
|
|
1217
|
-
- The dialog's fields take the tile tone on the white card
|
|
1218
|
-
(`.sm-dialog__form .sm-input`), the reverse of fields on tiles.
|
|
1219
|
-
- The confirm's copy lives at each call site (`title` / `body` /
|
|
1220
|
-
`confirmLabel` / `subject`); the shared ones are `DISCARD_CONFIRM`
|
|
1221
|
-
(`ui/confirm.ts`), `LEAVE_CONFIRM` (router) and `LIVE_URL_CONFIRM`
|
|
1222
|
-
(live-url). A subject path is shown where the screen already holds it;
|
|
1223
|
-
the entries list and the media screen show the name only.
|
|
1224
|
-
- `confirm()` without a mounted `<ConfirmHost/>` falls back to
|
|
1225
|
-
`window.confirm` so nothing can hang; the host is mounted once in
|
|
1226
|
-
`AdminApp` beside the shell. The tab-close guard stays native
|
|
1227
|
-
(`beforeunload` cannot await).
|
|
1228
|
-
- The handoff's `Styles` tab in the inspector is not built: it exists
|
|
1229
|
-
only where a section registers styles, which is the existing `tabsFor`.
|
|
1230
|
-
- The wordmark still takes `--logo-color` (accent); the rail's app icon
|
|
1231
|
-
takes `--logo-outer` / `--logo-inner` and is hidden in the warm look.
|
|
1232
|
-
- Polish round the same evening (2026-09-25): list row rules are a
|
|
1233
|
-
`.sm-row::after` pseudo-element, since a border follows the rounded
|
|
1234
|
-
hover corners; the admin remembers ONE locale across lists and forms
|
|
1235
|
-
(`shell/locale-memory.ts`, key `smoodly-locale`; the lists' two
|
|
1236
|
-
session-only keys are gone, `localeFromUrl` falls back to it and
|
|
1237
|
-
remembers what it resolved); the section insert picker opens upward
|
|
1238
|
-
when the room below is short and scrolls inside the room it has
|
|
1239
|
-
(`placePicker` in `editor/InsertPicker.tsx`, measured in a layout
|
|
1240
|
-
effect, no window-resize listener); the prefixed slug input keeps a
|
|
1241
|
-
22px line box so its caret reads as one; `.sm-input--mono` is scoped
|
|
1242
|
-
under `.sm-admin` — the `input{font-family:inherit}` reset had beaten
|
|
1243
|
-
it, so every mono input (slugs, IDs) had rendered in the sans font;
|
|
1244
|
-
richtext block-node cards take `--rtcard-bg` / `--rtcard-border` (tile
|
|
1245
|
-
grey with the soft accent border in the neutral look, white and
|
|
1246
|
-
hairlined in the warm). The custom section's row cards and the list
|
|
1247
|
-
field's rows keep the white-on-tile look — only the richtext cards sat
|
|
1248
|
-
on a white field.
|
|
1249
|
-
- Richtext cards, second pass (2026-09-25): the default card border is
|
|
1250
|
-
the hairline (`--rtcard-border:var(--line)`), the accent outline marks
|
|
1251
|
-
the selected card alone, and the widget's focus ring steps back while a
|
|
1252
|
-
block node is selected (`:focus:not(:has(.ProseMirror-selectednode))`).
|
|
1253
|
-
The image card's head carries Edit / Replace / Remove as
|
|
1254
|
-
`.sm-field__action` text buttons, the same three the media field now
|
|
1255
|
-
shows in its field head (`MediaWidget` renders its own `Field`;
|
|
1256
|
-
`FieldWidget` returns it directly, `SeoSettings` passes the label). Edit
|
|
1257
|
-
on a richtext image is the crop editor over the node's `image` attr
|
|
1258
|
-
(an `ImageValue`, so crop/hotspot/aspect store there). The bound
|
|
1259
|
-
`smoodly/richtext` renderer applies it through `<SmoodlyImage>`; the
|
|
1260
|
-
root `RichText`'s plain figure does NOT (it may not import the image
|
|
1261
|
-
layer, `richtext-entry.test.ts`), so a framework-neutral renderer
|
|
1262
|
-
(the Astro adapter, OQ 29) must frame it with `frameFor` itself. The
|
|
1263
|
-
card shows the cropped frame (`forms/ImagePreview.tsx`, shared with the
|
|
1264
|
-
media field), the filename and the caption as text; alt and caption are
|
|
1265
|
-
edited in the crop modal (`CropEditor` takes `caption`), so the card
|
|
1266
|
-
holds no inputs. Fields on a modal panel take the tile tone.
|
|
1267
|
-
- A refused publish's per-field errors clear as the fields are fixed
|
|
1268
|
-
(2026-09-25): the entry and global forms revalidate on every change with
|
|
1269
|
-
the op's own rules (`validateFields` / `validateNode` over
|
|
1270
|
-
`elementSchemas(registry.elements)`) and keep only what still fails
|
|
1271
|
-
(`stillFailing` in `validate.ts`); the notice's list shrinks and the
|
|
1272
|
-
notice goes when it empties. Typing never adds an error — only a
|
|
1273
|
-
publish does. The page editor's banner list and the canvas global
|
|
1274
|
-
inspector (save errors) are unchanged.
|
|
1275
|
-
|
|
1276
|
-
## Follow-ups (logged 2026-09-25, date pickers)
|
|
1277
|
-
|
|
1278
|
-
- `f.date()` and `f.datetime()` share one in-house calendar
|
|
1279
|
-
(`admin/forms/DatePicker.tsx` over the pure `date-picker.ts`); the
|
|
1280
|
-
browsers' own pickers were rejected (they differ and take no tokens).
|
|
1281
|
-
Weeks start on Monday and month names come from the browser's language
|
|
1282
|
-
— `weekStart` and a month-name locale as config keys are follow-ups.
|
|
1283
|
-
- `defineConfig({ timeZone })` is validated by constructing an
|
|
1284
|
-
`Intl.DateTimeFormat`, so an alias the runtime's database still knows
|
|
1285
|
-
(`Europe/Kiev`) passes and an offset never does. Admin-owned site
|
|
1286
|
-
settings (a per-space `settings` table with a Site page) were designed
|
|
1287
|
-
and dropped for the zone; the design is in the spec's history should a
|
|
1288
|
-
setting turn up that editors must change without a deploy.
|
|
1289
|
-
- Not built: a time-only field, a date range, `.min` / `.max` on dates.
|
|
1290
|
-
- Publish refuses a malformed date or datetime now (`validate.ts`); a
|
|
1291
|
-
row saved before this with a bad string shows the message on its next
|
|
1292
|
-
publish, as designed.
|
|
1293
|
-
|
|
1294
|
-
## Follow-ups (logged 2026-09-08, block nodes spec)
|
|
1295
|
-
|
|
1296
|
-
- Image sizing and float in a body (full width, wide, small, left/right):
|
|
1297
|
-
a later additive `styles` on the image node; the site's CSS answers it.
|
|
1298
|
-
- Inline elements: `inline: true` on the registration; cursor, marks and
|
|
1299
|
-
selection around an inline atom are a separate node-view effort.
|
|
1300
|
-
- The column context (`contexts: ["column"]`) waits for Phase 3 rows;
|
|
1301
|
-
accepted and unused today.
|
|
1302
|
-
- A page or entry picker for `f.link()` inside an element — the link
|
|
1303
|
-
picker spec.
|
|
1304
|
-
- Element copy between fields: a pasted element the target toolset
|
|
1305
|
-
forbids is dropped silently; a notice would be kinder.
|
|
1306
|
-
- Placeholder marking for sample-populated element fields waits for the
|
|
1307
|
-
tree's own placeholder marking.
|
|
1308
|
-
- An element's styles are not merged with the config's `styles.elements`
|
|
1309
|
-
standard set (sections get `resolvedStyles`); add the merge when the
|
|
1310
|
-
set is non-empty somewhere.
|
|
1311
|
-
- Element styles are not validated on publish (sections' aren't either).
|
|
1312
|
-
- `hasContent` treats an unresolved image node as content, so a body
|
|
1313
|
-
whose only image was deleted from the library still passes "required".
|
|
1314
|
-
- Publishing a saved version of a versioned entry: the entry form's
|
|
1315
|
-
status control returns early when the target equals the current status,
|
|
1316
|
-
so a new version of a published entry goes live only by toggling Draft
|
|
1317
|
-
then Published (seen in the block-nodes walk; pre-existing). A "Publish
|
|
1318
|
-
changes" action when the draft version is ahead of the published one.
|
|
1319
|
-
- The insert position is recorded when the "+" menu opens and handed to
|
|
1320
|
-
`insertContentAt`, because after the menu or the media picker takes
|
|
1321
|
-
focus the refocused contenteditable placed its DOM caret at the first
|
|
1322
|
-
card and the editor's selection followed it (the walk's first inserts
|
|
1323
|
-
landed at the document top). The explicit position closes the hole;
|
|
1324
|
-
the DOM-caret drift itself was not chased to its root.
|
|
1325
|
-
- The "+" menu and the slash menu share `RichtextInsertMenu` but keep
|
|
1326
|
-
separate active-row state and key handling; one keyboard model would
|
|
1327
|
-
be smaller.
|
|
1328
|
-
|
|
1329
|
-
## Follow-ups (logged 2026-09-08, tooling session)
|
|
1330
|
-
|
|
1331
|
-
- **CI live job red since e09a632, 0.0.11 fresh install broken — fixed
|
|
1332
|
-
2026-09-08, released as 0.0.12: `smoodly/image/config` is the entry
|
|
1333
|
-
`next.config.ts` imports; `test/image-config-entry.test.ts` loads the
|
|
1334
|
-
built file under bare node.** The
|
|
1335
|
-
scaffolded `next.config.ts` imports `smoodlyImages` from `smoodly/image`,
|
|
1336
|
-
whose index re-exports `SmoodlyImage.tsx` and its `import Image from
|
|
1337
|
-
"next/image"`. Next loads the config in plain Node ESM and next@16.3.4
|
|
1338
|
-
has no `exports` map, so the extensionless specifier fails
|
|
1339
|
-
(`ERR_MODULE_NOT_FOUND ... Did you mean "next/image.js"?`) before
|
|
1340
|
-
`next build` starts; the bundler resolves it, which is why nothing else
|
|
1341
|
-
caught it. Reproduce from `examples/site`:
|
|
1342
|
-
`node --input-type=module -e "await import('smoodly/image')"`. Fix: a
|
|
1343
|
-
component-free entry for the config helper (the template and the
|
|
1344
|
-
example comment then import from it), a plain-Node import test, then
|
|
1345
|
-
release 0.0.12. Appending `.js` alone is weaker: Node hands back the
|
|
1346
|
-
CommonJS `module.exports` object as the default, not the component.
|
|
1347
|
-
|
|
1348
|
-
## Follow-ups (logged 2026-09-08, client routing spec)
|
|
1349
|
-
|
|
1103
|
+
warm block once the neutral look is judged, the same way. *(2026-09-25, neutral restyle + confirm dialog)*
|
|
1350
1104
|
- Keep the previous view painted until the next one has data: today a
|
|
1351
1105
|
navigation switches instantly and the new view shows its own loading
|
|
1352
1106
|
state; a transition that holds the old view, or prefetches the next
|
|
1353
|
-
view's ops call on hover, is the next step.
|
|
1354
|
-
- Browser Back with unsaved changes is not guarded: `popstate` cannot be
|
|
1355
|
-
cancelled cleanly, so a dirty form only asks on a click and on reload.
|
|
1356
|
-
Re-pushing the current URL and asking is possible if editors lose work.
|
|
1357
|
-
- Every revalidating op (publish, delete, a save of published content)
|
|
1358
|
-
answers with a fresh RSC render of the admin route — the layout's
|
|
1359
|
-
styles included, ~30 KB — because `updateTag` runs inside the server
|
|
1360
|
-
action and Next re-renders the tree it was posted to. Harmless now
|
|
1361
|
-
that the shell lives in the layout (the empty page subtree is what
|
|
1362
|
-
remounts), but it is the price of server actions as the transport.
|
|
1363
|
-
- The bundle split: the editor bridge out of the admin tree and the
|
|
1364
|
-
editor lazy-loaded, so site pages stop loading the admin chunk (the
|
|
1365
|
-
richtext widget register entry below measures it). Its own spec.
|
|
1366
|
-
- `SharedForm` and `EntryForm` now share the locale helpers; the rest
|
|
1367
|
-
of the fork (`applyResult`, `InfoRow`, the ref-options effect) is still
|
|
1368
|
-
the 2026-09-07 entry below.
|
|
1369
|
-
|
|
1370
|
-
## Follow-ups (logged 2026-09-08, media library spec)
|
|
1371
|
-
|
|
1372
|
-
- Video transcoding and adaptive streaming: a Mux or Cloudflare Stream
|
|
1373
|
-
loader behind the `media.loader` seam, if a site ever needs more than
|
|
1374
|
-
an MP4 over Storage's CDN.
|
|
1375
|
-
- Sweeping abandoned `pending` asset rows (an upload that began and
|
|
1376
|
-
never called `finishUpload`); a periodic op or a cleanup on `list`.
|
|
1377
|
-
- A private bucket with signed reads, for sites that must not expose
|
|
1378
|
-
draft-only media by URL before publish.
|
|
1379
|
-
- Pre-generated image variants at upload time, for hosts without sharp.
|
|
1380
|
-
- Transform loaders beyond the Next default: Supabase Image
|
|
1381
|
-
Transformations, imgix, Cloudflare Images (DESIGN.md OQ 28).
|
|
1382
|
-
- Delivery caching (checked 2026-09-08): Supabase Storage's basic CDN
|
|
1383
|
-
fronts every public-bucket object on a hosted project with no code on
|
|
1384
|
-
our side, and the Smart CDN (Pro plan, automatic) only adds
|
|
1385
|
-
invalidation on overwrite, which never happens — objects are
|
|
1386
|
-
immutable, no upsert on either path, Replace picks a new id. So both
|
|
1387
|
-
upload paths now set a year-long `cache-control`
|
|
1388
|
-
(`MEDIA_CACHE_SECONDS`): the `cacheControl` option on the server-side
|
|
1389
|
-
upload, the header on the browser's signed PUT (without it a signed
|
|
1390
|
-
upload lands as `no-cache`; Storage's default is one hour). The live
|
|
1391
|
-
suite reads the header back from the local stack. Not yet verified
|
|
1392
|
-
against a hosted project's CDN edge; check `cf-cache-status` and
|
|
1393
|
-
`cache-control` on a real upload there once. Remaining: in v1 the
|
|
1394
|
-
site's `<SmoodlyImage>` goes through `next/image`, so the visitor hits
|
|
1395
|
-
the app server's image cache and only its origin fetch touches the
|
|
1396
|
-
CDN; video, files and SVGs hit the CDN directly. A Supabase Image
|
|
1397
|
-
Transformations loader (`/storage/v1/render/image/public/...`, Pro
|
|
1398
|
-
only, must stay optional) would move resizing to the CDN — the loader
|
|
1399
|
-
bullet above.
|
|
1400
|
-
- The asset refs inherit the index's scope: it reflects the latest saved
|
|
1401
|
-
version of a source, so a published version still using an asset the
|
|
1402
|
-
draft dropped is not protected from delete — the same gap entry refs
|
|
1403
|
-
have today.
|
|
1404
|
-
- Deviations from the spec, decided while building: the starter's Hero
|
|
1405
|
-
declares `aspect: "4:5"` (the design's own ratio), not `16:9`; `assets.list`
|
|
1406
|
-
pages by a `created_at` cursor, 60 per page; the loader seam is the
|
|
1407
|
-
`loader` prop of `<SmoodlyImage>` and `imageUrl`'s third argument — a
|
|
1408
|
-
config-level `media.loader` waits for a second loader to exist.
|
|
1409
|
-
- Image dimensions server-side cover JPEG and PNG (`imageDimensions`); a
|
|
1410
|
-
WebP or AVIF seeded through `AssetStore.create` lands without a size
|
|
1411
|
-
and resolves to nothing until the row is edited. Browser uploads
|
|
1412
|
-
measure every type.
|
|
1413
|
-
- A section's `styles` are walked for media, the config's standard style
|
|
1414
|
-
set is not (it holds selects). A standard-set image would need the
|
|
1415
|
-
walk to read `resolvedStyles`.
|
|
1416
|
-
- `<SmoodlyImage>` sizes the image by the crop factor, so `sizes` picks a
|
|
1417
|
-
candidate for the box, not for the framed image; a CDN loader with
|
|
1418
|
-
`handlesCrop` removes the overfetch.
|
|
1419
|
-
- `smoodlyImages()` returns the whole `images` config, not just the
|
|
1420
|
-
patterns (decided 2026-09-08 in the browser walk): Next 16's optimizer
|
|
1421
|
-
answers 400 for an upstream that resolves to a private IP unless
|
|
1422
|
-
`images.dangerouslyAllowLocalIP` is set, and the local stack is
|
|
1423
|
-
`127.0.0.1`. The helper sets the flag only for `localhost`, loopback
|
|
1424
|
-
and RFC 1918 hosts, so a hosted project stays strict; a Docker alias
|
|
1425
|
-
that resolves privately needs the flag by hand. The spec's "a helper
|
|
1426
|
-
on `smoodly/next`" lives on `smoodly/image` instead, beside the
|
|
1427
|
-
components it serves.
|
|
1428
|
-
- Build deviations from the plan (2026-09-08, sessions A–C):
|
|
1429
|
-
`test/admin-pages-tree.test.ts` builds its `AdminRegistry` literal by
|
|
1430
|
-
hand and carries `media: { maxSize: 1, accept: [] }`; `MediaPicker`'s
|
|
1431
|
-
`filters` is `readonly KindFilter[]` and the `Segmented` calls pin
|
|
1432
|
-
`<KindFilter>`; `MediaScreen` seeds its metadata state from
|
|
1433
|
-
`initialSelected` because the plan's effect never runs under
|
|
1434
|
-
`renderToStaticMarkup`; `forms/measure.ts` is browser-only and
|
|
1435
|
-
untested by design (`putSigned` is unit-tested over a stubbed fetch
|
|
1436
|
-
and exercised live since 2026-09-08); the installer's templates are
|
|
1437
|
-
generated (gitignored), so "templates rebuilt" is a build step, not a
|
|
1438
|
-
diff.
|
|
1439
|
-
|
|
1440
|
-
## Follow-ups (logged 2026-09-09, admin tweaks)
|
|
1441
|
-
|
|
1442
|
-
The 2026-09-09 tweak round: view titles sans 18px, the entry and
|
|
1443
|
-
Globals forms in one 680px column with a globe-off mark on fields that
|
|
1444
|
-
are not `.localized()` (the "Shared across languages" / "In <locale>"
|
|
1445
|
-
groups are gone), the image field on a 300px canvas, every locale
|
|
1446
|
-
switch a segmented pill (a dropdown past three locales), Media before
|
|
1447
|
-
Globals (née Shared) on the rail, richtext paragraph and card spacing,
|
|
1448
|
-
element cards collapsed unless blank, and the "+" menu floating under
|
|
1449
|
-
the toolbar instead of pushing the content.
|
|
1450
|
-
|
|
1107
|
+
view's ops call on hover, is the next step. *(2026-09-08, client routing)*
|
|
1451
1108
|
- `LocaleSwitcher` in pill form has no "+ add" affordance: an absent
|
|
1452
1109
|
locale reads subtle and switching to it lands on the form's or the
|
|
1453
1110
|
editor's "doesn't exist yet · Add" pane, which adds. The dropdown form
|
|
1454
1111
|
(four locales and up) still adds straight from its "Add locale" group,
|
|
1455
1112
|
so the two forms differ by one click; a "+" inside the pill would close
|
|
1456
|
-
that gap.
|
|
1113
|
+
that gap. *(2026-09-09, admin tweaks)*
|
|
1457
1114
|
- The image field's canvas height (300px) lives twice: `CANVAS_H` in
|
|
1458
1115
|
`MediaWidget.tsx` sizes the frame, `.sm-media__canvas` /
|
|
1459
|
-
`.sm-media__thumb` cap it in the stylesheet. One token would do.
|
|
1116
|
+
`.sm-media__thumb` cap it in the stylesheet. One token would do. *(2026-09-09, admin tweaks)*
|
|
1460
1117
|
- `.sm-form>*{max-width:680px}` caps the banner too; if a wide
|
|
1461
|
-
field type ever appears (a table), give it an opt-out class.
|
|
1118
|
+
field type ever appears (a table), give it an opt-out class. *(2026-09-09, admin tweaks)*
|
|
1462
1119
|
- An element card decides "open" from its fields alone (blank = open);
|
|
1463
1120
|
an element whose fields are all optional and empty by design stays
|
|
1464
|
-
open on every load. The richtext image card is never collapsible.
|
|
1121
|
+
open on every load. The richtext image card is never collapsible. *(2026-09-09, admin tweaks)*
|
|
1465
1122
|
- The "+" menu anchors to the toolbar's right end (`right:0` in
|
|
1466
1123
|
`.sm-richtext__head`), which is under the "+" only while the toolbar
|
|
1467
1124
|
fits on one line; in the 320px inspector the toolbar wraps and the
|
|
1468
|
-
menu sits below the wrapped row's end.
|
|
1469
|
-
- The technical look's `TECHNICAL_SHAPE` tokens were not touched: its
|
|
1470
|
-
title stays sans at `--text-xl`, its label has no mark styling beyond
|
|
1471
|
-
the shared `.sm-label__mark` rule.
|
|
1472
|
-
- Browser-pane sign-in (2026-09-09): the magic-link hash on
|
|
1473
|
-
`/admin/...` was NOT consumed by the admin's Supabase client in the
|
|
1474
|
-
pane (the login form stayed); fetching `/auth/v1/user` with the
|
|
1475
|
-
access token and writing the session object to `localStorage`
|
|
1476
|
-
under `smoodly-auth`, then reloading, signed in. The pane was hidden
|
|
1477
|
-
during the walk, so scrolling went through `.sm-form.scrollTop` and
|
|
1478
|
-
clicks through `element.click()` in `javascript_tool`.
|
|
1479
|
-
|
|
1480
|
-
## Follow-ups (logged 2026-09-08, richtext widget spec)
|
|
1481
|
-
|
|
1482
|
-
- ~~Drag-to-resize the inspector~~ — DONE 2026-09-10 (spec
|
|
1483
|
-
`docs/superpowers/specs/2026-09-10-inspector-resize-design.md`):
|
|
1484
|
-
`ui/ResizeHandle.tsx` writes `--inspector-w` inline on the wrapper,
|
|
1485
|
-
clamped to 260px..the row less the handle and a 440px canvas (`ui/resize.ts`,
|
|
1486
|
-
half the row until 2026-09-26), remembered under
|
|
1487
|
-
`smoodly-inspector-w` and applied by the theme's boot script. Walk
|
|
1488
|
-
finding: the desktop pane hides its tab on every `navigate`, and a
|
|
1489
|
-
hidden tab lays out at zero width, so the handle treats a zero
|
|
1490
|
-
measurement as unknown and re-measures on focus. Real drags through
|
|
1491
|
-
the pane's `left_click_drag` worked (synthetic `PointerEvent`s cannot
|
|
1492
|
-
satisfy `setPointerCapture`); coordinates are the screenshot frame,
|
|
1493
|
-
0.625 × CSS px at the pane's 1280 width.
|
|
1494
|
-
- A page and entry picker in the link popover: a link inside a richtext
|
|
1495
|
-
document is a reference the refs index, the resolver and the localizer
|
|
1496
|
-
cannot see (they address refs by top-level key). Its own spec.
|
|
1497
|
-
- Escape inside any inspector control bubbles to the editor's window
|
|
1498
|
-
handler and clears the block selection — the link popover stops it, a
|
|
1499
|
-
textarea or the TipTap content does not. A `keydown` guard on
|
|
1500
|
-
`.sm-inspector` would fix it for every control.
|
|
1501
|
-
- Every richtext descriptor now carries its toolset, so the serialized
|
|
1502
|
-
registry grew by a few bytes per field; if the AI-facing schema wants
|
|
1503
|
-
the vocabulary once instead of per field, dedupe there.
|
|
1125
|
+
menu sits below the wrapped row's end. *(2026-09-09, admin tweaks)*
|
|
1504
1126
|
- The entry form's column is narrower than the inspector, so the default
|
|
1505
|
-
toolbar wraps to two rows there; the inspector shows it on one.
|
|
1127
|
+
toolbar wraps to two rows there; the inspector shows it on one. *(2026-09-08, richtext widget)*
|
|
1128
|
+
- A list row's collapsed summary shows the item's first string subfield —
|
|
1129
|
+
the header nav rows read `/`, `/posts` — where the label would identify
|
|
1130
|
+
the row; `itemSummary` could prefer a `label`/`title`/`heading` key
|
|
1131
|
+
(walk, 2026-09-07). *(2026-09-07, starter)*
|
|
1132
|
+
- Page settings prints `paths[locale]` under each slug field — the
|
|
1133
|
+
locale-relative CMS path, not `urls[locale]`, the site URL the canvas
|
|
1134
|
+
and "View live" actually open. On a translated site the Finnish hint
|
|
1135
|
+
reads `/yritys/tietoa-meista` while the page lives at
|
|
1136
|
+
`/fi/yritys/tietoa-meista`. Both maps are already in `EditorView`;
|
|
1137
|
+
which one belongs under the field is a UI decision, not a missing
|
|
1138
|
+
value. *(2026-09-06)*
|
|
1139
|
+
- The row menu in `PagesList.tsx` does not close on Escape — only a click
|
|
1140
|
+
on the backdrop closes it. *(2026-09-06)*
|
|
1141
|
+
- `PageSettings` re-seeds its inputs from `record` on every rename (and
|
|
1142
|
+
every publish), so keystrokes typed while a rename is in flight can be
|
|
1143
|
+
dropped. The blur- and Enter-driven commits themselves are unaffected. *(2026-09-06)*
|
|
1144
|
+
- A childless folder row is inert on click (a mount opens its collection,
|
|
1145
|
+
a page opens the editor, a folder with children expands); "Add page
|
|
1146
|
+
here" lives in the row menu only. *(2026-09-06)*
|
|
1147
|
+
- The tree rows nest interactive buttons — the chevron and the `···`
|
|
1148
|
+
menu — inside a `role="button"` row. It works, because both stop the
|
|
1149
|
+
click and the keydown, but it is an accessibility smell a real treegrid
|
|
1150
|
+
would not have. *(2026-09-06)*
|
|
1151
|
+
- The Pages list offers "Add <locale>" on a dimmed row whose parent lacks
|
|
1152
|
+
that locale, and learns the refusal only after the round trip.
|
|
1153
|
+
`pagesTreeRows` already carries every node's locales, so the parent's
|
|
1154
|
+
presence could disable the item with its reason up front, exactly as
|
|
1155
|
+
the editor's switcher does with `parentLocales`. `EntriesList` has the
|
|
1156
|
+
same gap (and `entryRows` would have to carry the locales again — the
|
|
1157
|
+
dead field was deleted rather than left unused); it is unreachable
|
|
1158
|
+
today, since nothing in the admin creates a child entry. *(2026-09-06)*
|
|
1159
|
+
- Design system pass (DESIGN.md §2, "The admin's visual system"): the shell,
|
|
1160
|
+
lists, entry form, login and editor were restyled onto `admin/ui/theme.tsx`.
|
|
1161
|
+
Deferred deliberately, because each is a feature rather than a restyle: the
|
|
1162
|
+
list search field and status filter, the locale select in the shell's top bar
|
|
1163
|
+
(the editor has one; the lists always read the default locale), the article
|
|
1164
|
+
sidebar's taxonomy/SEO/publish-date sections, and the rich-text toolbar. The
|
|
1165
|
+
pages list's tree shape (chevrons, depth indent) and its row `···` menu
|
|
1166
|
+
shipped 2026-09-06, without the drag handle. The tokens for all of them are
|
|
1167
|
+
already in place. *(2026-09-05)*
|
|
1168
|
+
- `lucide-react` is now a runtime dependency, for the toggle's two icons. The
|
|
1169
|
+
rest of the UI still uses the design system's unicode glyphs (↑ ↓ ⧉ × ··· ▾
|
|
1170
|
+
← ⋮⋮); swapping them for Lucide is the next step whenever it is wanted. *(2026-09-05)*
|
|
1171
|
+
|
|
1172
|
+
#### Addresses and the Pages tree
|
|
1173
|
+
|
|
1174
|
+
- The rename/move confirm's code-address line names the addresses in
|
|
1175
|
+
the selected locale only, though a move re-paths every locale; the
|
|
1176
|
+
Pages list's conflict notice is one ellipsized line (the whole text
|
|
1177
|
+
on hover), and an editor's flagged page cannot be moved from the
|
|
1178
|
+
notice itself. *(2026-09-29, nested collection paths)*
|
|
1179
|
+
- An index page removed from code leaves the collection's record with
|
|
1180
|
+
its template (spec amendment 3): the record keeps rendering as that
|
|
1181
|
+
page type's stale name until an editor or a later pass clears it. *(2026-09-29, nested collection paths)*
|
|
1182
|
+
|
|
1183
|
+
#### Collections
|
|
1184
|
+
|
|
1185
|
+
- Nested entries have no admin UI beyond `entries.move`: nothing creates
|
|
1186
|
+
a child entry or shows the entry tree, and the entry form's slug prefix
|
|
1187
|
+
is the collection's mount base only, not the parent chain. *(2026-09-06)*
|
|
1188
|
+
- A published entry under a draft parent is dropped from `getEntriesTree`
|
|
1189
|
+
(the parent is not in the list to hang it from) but returned flat by
|
|
1190
|
+
`getEntries`. Untested either way. *(2026-09-05)*
|
|
1191
|
+
|
|
1192
|
+
#### Queries (`q`)
|
|
1193
|
+
|
|
1194
|
+
- `presence()` serves the presence channel only; a future read gateway (DESIGN.md OQ 31) is not built on it — no key but a write key reads content. *(2026-09-26, concurrency; 2026-09-30 §8)*
|
|
1195
|
+
- `where` ranges and negation. *(2026-09-11, queries)*
|
|
1196
|
+
- `EntryItem.updatedAt`, and `order: updatedAt`, read the LIVE locale row, so a draft autosave bumps the public timestamp and can reorder a public list; read the published version's `saved_at` for published reads. *(2026-09-30, content API contract final review)*
|
|
1197
|
+
- The bare-slug `getEntry` fallback re-reads the whole collection and resolves every entry's references and media before matching one slug; match on `record` first, then resolve only the hit (or query by slug). *(2026-09-30, content API contract final review)*
|
|
1198
|
+
- `SmoodlySite.resolve` (and `load`) still return the full `EntryRecord` (every locale, `updatedBy`) on the object the developer holds; the types are unexported and the spec calls it internal — mark it `@internal` or move both off the public interface. *(2026-09-30, content API contract final review)*
|
|
1199
|
+
- A nested `.resolve({ a: { b: true } })` types only the top level (`Rebase<V, keyof P>`) while the runtime resolves deeper; type the nesting. *(2026-09-30, content API contract final review)*
|
|
1200
|
+
- Any-of over an array-valued field: any-of compares the text form of
|
|
1201
|
+
the field, so it covers selects, refs and numbers; on a checkboxes,
|
|
1202
|
+
refList or list field `site.getEntries` refuses it rather than match
|
|
1203
|
+
nothing — both adapters agree on the text form, so a one-element array
|
|
1204
|
+
never matches its element. *(2026-09-11, queries)*
|
|
1205
|
+
- `getEntriesTree` has no `q` counterpart yet; add one when a site
|
|
1206
|
+
needs a nested collection nav. *(2026-09-11, queries)*
|
|
1207
|
+
- `getShared(["header", "footer", "contact"], { locale })` returning a
|
|
1208
|
+
typed tuple, as sugar over the `Promise.all` the layout writes today.
|
|
1209
|
+
Keep one cache unit per item under its own tag — a single unit tagged
|
|
1210
|
+
with all three would expire the header whenever the footer is edited. *(2026-09-11, queries)*
|
|
1211
|
+
- An explicit JSON `null` VALUE in a declared `order.by` field sorts
|
|
1212
|
+
FIRST in Supabase (a jsonb null is the lowest jsonb value, not SQL
|
|
1213
|
+
NULL, so `nullsFirst: false` does not reach it) and LAST in memory. A
|
|
1214
|
+
MISSING key is SQL NULL and sorts last in both, and the admin only ever
|
|
1215
|
+
produces that — a cleared number field emits `undefined`, which the
|
|
1216
|
+
JSON body drops — so this needs a programmatic or external writer.
|
|
1217
|
+
Mixed types in one field diverge for the same reason: Postgres orders
|
|
1218
|
+
jsonb by type first (null < string < number), memory by `String(...)`. *(2026-09-05)*
|
|
1219
|
+
|
|
1220
|
+
#### SEO and page fields
|
|
1221
|
+
|
|
1222
|
+
- A `styles` block on pages, when a case needs presentation separate
|
|
1223
|
+
from content. *(2026-09-10, page fields and SEO)*
|
|
1224
|
+
- Refs in page fields, once the refs index walks them. *(2026-09-10, page fields and SEO)*
|
|
1225
|
+
- A canonical URL field and a search-result preview snippet in the
|
|
1226
|
+
SEO & Social tab. *(2026-09-10, page fields and SEO)*
|
|
1227
|
+
- Configurable SEO set (`meta: { seo: { twitter: true } }`) if the four
|
|
1228
|
+
values prove too few. *(2026-09-10, page fields and SEO)*
|
|
1229
|
+
- Link fields are raw: an image in a page field reaches a nav link as
|
|
1230
|
+
`{ asset, alt }`, not a URL. Resolve on demand if a nav ever wants one. *(2026-09-10, page fields and SEO)*
|
|
1231
|
+
- Page-field labels derive from the key (`showInNavigation` → "Show In
|
|
1232
|
+
Navigation"), as every field's do; a `.label()` builder option would
|
|
1233
|
+
read better for long keys. *(2026-09-10, page fields and SEO)*
|
|
1506
1234
|
|
|
1507
|
-
|
|
1235
|
+
#### Site rendering and starters
|
|
1508
1236
|
|
|
1237
|
+
- A separate canvas route would take the last Smoodly bytes off live
|
|
1238
|
+
pages (the lazy stub that loads the bridge and the canvas boundary in
|
|
1239
|
+
the editor frame), at the cost of the frame no longer loading the
|
|
1240
|
+
page's real address: the site's layout would have to be reached from
|
|
1241
|
+
a sibling route, and a developer's own route file or the escape hatch
|
|
1242
|
+
would preview differently from live. Only if a site needs zero.
|
|
1243
|
+
*(2026-09-29, pre-install fixes)*
|
|
1244
|
+
- A layout-only language switch, if a real site needs one: a
|
|
1245
|
+
`q().getAlternates(path)` over the `paths` index plus a proxy header
|
|
1246
|
+
carrying the request path. Not built until asked for — the starters
|
|
1247
|
+
render the switch from the page (`components/PageFrame.tsx`,
|
|
1248
|
+
`components/LocaleNav.tsx`). *(2026-09-15, locale alternates)*
|
|
1509
1249
|
- A paired component (`smoodly.section`) accepts only its schema's props, so
|
|
1510
1250
|
a layout cannot hand the footer the contact item or the header the locale
|
|
1511
1251
|
links through the paired `Footer`/`Header`; the starter's layouts render
|
|
1512
1252
|
`FooterView`/`HeaderView` directly with the extra prop. A supported way
|
|
1513
1253
|
to pass layout-only props through a paired component (or a `children`
|
|
1514
|
-
slot) would let the layouts use the registered component.
|
|
1515
|
-
- Page views and entry pages receive no `locale`; the starter's views get
|
|
1516
|
-
it from the app's strings seam (`next-intl`'s `getLocale`, or a constant
|
|
1517
|
-
in the single-language scaffold). Adding `locale` to `PageContext` and
|
|
1518
|
-
`EntryPageProps` would remove that dependency and let sections that
|
|
1519
|
-
format dates stay synchronous.
|
|
1520
|
-
- The image widget edits the URL only; `alt` is set by the seed and cannot
|
|
1521
|
-
be changed in the admin.
|
|
1254
|
+
slot) would let the layouts use the registered component. *(2026-09-07, starter)*
|
|
1522
1255
|
- A per-locale collection path or fixed slug must name the default locale;
|
|
1523
1256
|
the installer rewrites the starter's two lines to name exactly the
|
|
1524
1257
|
chosen locales (`articlesSegments` in `scaffold.ts`), since a path
|
|
1525
1258
|
claiming a locale the site lacks fails the index-page rule at config
|
|
1526
1259
|
load. A "fall back to the first declared segment" rule in
|
|
1527
|
-
`defineConfig` would make the patch unnecessary.
|
|
1528
|
-
- The picker offers a shared item only in zones whose allow list includes
|
|
1529
|
-
its section (`allowedShared` in `admin/tree-ops.ts`), so a shared item's
|
|
1530
|
-
section is always also offered as an ordinary section. A "placeable
|
|
1531
|
-
only" mode (a shared item whose section no zone lists) would let the
|
|
1532
|
-
starter keep the quote shared-only.
|
|
1260
|
+
`defineConfig` would make the patch unnecessary. *(2026-09-07, starter)*
|
|
1533
1261
|
- Registry names are one namespace across sections, elements, collections
|
|
1534
1262
|
and page shapes (`assertUniqueNames`); the starter names its People
|
|
1535
1263
|
section `peopleSection` and its Articles page `articlesIndex` to stay
|
|
1536
1264
|
clear of the collections. Namespacing by kind would let a section and a
|
|
1537
|
-
collection share a name.
|
|
1538
|
-
- The seed's early exit probes `/` alone, and `createPage` writes the path
|
|
1539
|
-
row before the tree is published, so a crash between the home record and
|
|
1540
|
-
the last page leaves a database every later run reports as "already
|
|
1541
|
-
seeded"; recovery is `supabase db reset`. A probe on the last thing the
|
|
1542
|
-
seed writes, or a per-step check, would make it resumable.
|
|
1265
|
+
collection share a name. *(2026-09-07, starter)*
|
|
1543
1266
|
- With `fi` as the default locale the seed writes the English content into
|
|
1544
1267
|
`fi`. Before this fix the index page was seeded at `posts`
|
|
1545
1268
|
while the collection's `fi` path is `artikkelit`, so `ensureFixedNodes`
|
|
1546
1269
|
materialized a second, empty Articles page at `artikkelit` and the
|
|
1547
1270
|
links pointed at `/posts`; the seed now derives the segment from the
|
|
1548
1271
|
collection's `path` per locale. Left: the starter's copy in a `fi`
|
|
1549
|
-
default is still English.
|
|
1550
|
-
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
`points.3.icon: Required` until the editor re-picks the value that is
|
|
1559
|
-
already on screen. Seed the first option, or render a blank option so
|
|
1560
|
-
the screen matches the value (walk, 2026-09-07).
|
|
1561
|
-
- Publish validates the last saved draft, so a click inside the autosave
|
|
1562
|
-
debounce reports fields the editor has just filled as `Required`. Flush
|
|
1563
|
-
the pending save before validating, or disable Publish while a save is
|
|
1564
|
-
pending (walk, 2026-09-07).
|
|
1565
|
-
- The entry form cannot republish a published entry's newer draft: Save
|
|
1566
|
-
writes a new draft version, the status control's `setPublished` returns
|
|
1567
|
-
early when the status is unchanged, and nothing shows that the draft
|
|
1568
|
-
differs from the published version. The workaround unpublishes and
|
|
1569
|
-
republishes, which 404s the entry in between. Needs a "Publish changes"
|
|
1570
|
-
action and a draft-edits indicator like the page editor's (walk,
|
|
1571
|
-
2026-09-07).
|
|
1572
|
-
- Versions accumulate (`entry_versions` grew to four rows after one edit)
|
|
1573
|
-
but no admin screen lists or restores them (walk, 2026-09-07).
|
|
1574
|
-
- A list row's collapsed summary shows the item's first string subfield —
|
|
1575
|
-
the header nav rows read `/`, `/posts` — where the label would identify
|
|
1576
|
-
the row; `itemSummary` could prefer a `label`/`title`/`heading` key
|
|
1577
|
-
(walk, 2026-09-07).
|
|
1578
|
-
- New page / New path (a folder, "path" in the UI since 2026-09-09) ask for the slug through `window.prompt`, which
|
|
1579
|
-
browser automation blocks and which cannot validate as you type; an
|
|
1580
|
-
inline field would do both (walk, 2026-09-07).
|
|
1581
|
-
|
|
1582
|
-
## Follow-ups (logged 2026-09-07, list field spec)
|
|
1583
|
-
|
|
1584
|
-
- Saving the interim richtext textarea drops marks (bold, italic, link);
|
|
1585
|
-
flattens a hard break inside a heading, list item or blockquote line to a
|
|
1586
|
-
space (a prefixed line cannot carry one, DESIGN.md §3 Richtext); and drops
|
|
1587
|
-
anything the line syntax cannot express at all — nested lists,
|
|
1588
|
-
multi-paragraph list items (joined with a space instead), and unknown
|
|
1589
|
-
nodes such as a horizontal rule. All of it goes away with the TipTap
|
|
1590
|
-
widget.
|
|
1591
|
-
- Refs inside lists and objects are refused at build time (`f.list`,
|
|
1592
|
-
`f.object`). A list of objects each holding a ref (a featured entry with
|
|
1593
|
-
its own caption) needs the resolver, the refs index and the admin's
|
|
1594
|
-
ref-option loading to address nested paths; add when a site asks.
|
|
1595
|
-
- Heading levels 4–6 render but the line syntax writes `####`…`######`
|
|
1596
|
-
for them; the spec names 1–3 as the supported set.
|
|
1597
|
-
- `emptyValue` for an `image`/`video`/`file` item is `undefined`, so a new
|
|
1598
|
-
media row starts empty; a required image item is then a validation
|
|
1599
|
-
error until filled, which is the intended nudge but reads as an error
|
|
1600
|
-
on a row the editor just added.
|
|
1601
|
-
- `RichText` drops the anchor of a link mark whose href is not http(s),
|
|
1602
|
-
mailto, tel or site-relative (`/`, `#`, `?`, `.`). The href is normalised
|
|
1603
|
-
the way URL parsers do (tab, LF and CR removed) before the scheme test;
|
|
1604
|
-
`javascript:`, `data:`, protocol-relative `//host` and the backslash form
|
|
1605
|
-
`/\host` are rejected. The admin's link field does not validate on input yet.
|
|
1606
|
-
- The list widget has no test for an object item holding a nested list
|
|
1607
|
-
subfield.
|
|
1608
|
-
- A list item's subtree still uses the row index as its React key, so a list
|
|
1609
|
-
of objects each holding a list hands the inner rows another item's collapse
|
|
1610
|
-
state when an outer row moves; fix with a stable per-item key when nested
|
|
1611
|
-
lists get real use.
|
|
1272
|
+
default is still English. *(2026-09-07, starter)*
|
|
1273
|
+
- Cache tags are still `page:<id>`: publishing one locale expires every
|
|
1274
|
+
locale's cached unit of that page (spec 2026-09-06 §4). A
|
|
1275
|
+
`page:<id>:<locale>` tag would halve the revalidations on a bilingual
|
|
1276
|
+
site. *(2026-09-06)*
|
|
1277
|
+
- Examples and guides write `url(entry.path!)`: `path` is `string | null` on every entry, though a collection that declares `path` always has one — type it `string` for those and drop the assertions. *(2026-09-30, content API contract final review)*
|
|
1278
|
+
- `smoodlyMetadata` does not apply the registration check `renderSmoodlyPath`
|
|
1279
|
+
applies: a record whose `template` has no registration is a 404 body with
|
|
1280
|
+
its meta emitted beside it. *(2026-09-05)*
|
|
1612
1281
|
|
|
1613
|
-
|
|
1282
|
+
#### Media
|
|
1614
1283
|
|
|
1615
|
-
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
- `fillLockedZones` for a zone bound to a shared item waits for
|
|
1628
|
-
`ops.shared.list()`; a page opened before that resolves fills on the
|
|
1629
|
-
next effect run. Not seen in the walk — the example binds no zone, so
|
|
1630
|
-
the path was never exercised — but a double fill is unreachable by
|
|
1631
|
-
construction: the effect depends on `sharedIds`, `record`, `mutate` and
|
|
1632
|
-
`tree === null`, and `fillLockedZones` only touches EMPTY zones.
|
|
1633
|
-
`z.locked(item)` still has no example usage.
|
|
1634
|
-
- `SharedForm` lists linking pages by id; a title needs `ops.pages.get`
|
|
1635
|
-
per page (or a usage op returning titles).
|
|
1636
|
-
- `getSmoodlyShared` reads the store per request in draft mode and once
|
|
1637
|
-
per (item, locale) otherwise; a layout calling it for several items
|
|
1638
|
-
makes one round trip each — batch if it shows.
|
|
1639
|
-
- A shared item's row id is now uuid v5 of (space, name), so the primary
|
|
1640
|
-
key ends the materialization race — but only for rows created from now
|
|
1641
|
-
on: an item materialized before this change keeps its random
|
|
1642
|
-
`gen_random_uuid()` id, and a second load would still be able to insert
|
|
1643
|
-
a duplicate beside it. Re-materializing such an item (delete the row in
|
|
1644
|
-
SQL, reload the admin) moves it onto the derived id.
|
|
1645
|
-
- `SharedForm.tsx` is a near-verbatim fork of `EntryForm.tsx`
|
|
1646
|
-
(`applyResult`, `InfoRow`, the ref-options effect; the locale helpers
|
|
1647
|
-
are shared since 2026-09-08) and `SharedList.tsx` is the third copy of
|
|
1648
|
-
the remembered-locale block (`EntriesList`, `PagesList`); extract a
|
|
1649
|
-
`useFormLocale` hook when the next form arrives.
|
|
1650
|
-
- A placement selected in a bound locked zone shows the canvas toolbar's
|
|
1651
|
-
"fields only" label though a placement has no page-level fields; the
|
|
1652
|
-
inspector notice is the real message.
|
|
1653
|
-
- A zone whose policy allows a shared item's section also offers that
|
|
1654
|
-
section as an ordinary block, so the picker lists e.g. Testimonials
|
|
1655
|
-
twice (once under Shared); decide whether a section used by a shared
|
|
1656
|
-
item should be hidden from the ordinary list.
|
|
1657
|
-
- A placement of an object item or an unregistered item gates the page's
|
|
1658
|
-
publish (the refs edge is emitted without a registry lookup) yet never
|
|
1659
|
-
renders; only a hand-edited tree can produce one.
|
|
1660
|
-
- `ops.shared.get` runs `ensureSharedItems` and then lists the item
|
|
1661
|
-
again; one round trip could go.
|
|
1662
|
-
- The example's Testimonials `quotes` richtext is required, so the first
|
|
1663
|
-
admin save of the seeded band asks for it; make it optional or seed it.
|
|
1664
|
-
- `CollectionSchema.kind` was widened to carry `"shared"`, so a
|
|
1665
|
-
`SharedSchema` typechecks where `registry.collections` is expected;
|
|
1666
|
-
`defineConfig` should throw on a `kind === "shared"` entry listed there.
|
|
1667
|
-
- `ensureSharedItems` runs on every `ops.shared.list()` — which is every
|
|
1668
|
-
editor mount, not only the Shared content page — costing one
|
|
1669
|
-
`entries.list` per registration per open.
|
|
1670
|
-
- `fanOut` resolves every shared registration's row id (one
|
|
1671
|
-
`entries.list` each) on every entry write, so it can expire
|
|
1672
|
-
`shared:<name>` when something the item references changes; a row-id
|
|
1673
|
-
cache would remove the reads but has to survive materialization.
|
|
1674
|
-
- Because `fanOut` lists every shared registration, an entry store built
|
|
1675
|
-
without `options.shared` now fails on every ordinary entry write with
|
|
1676
|
-
"no collection named", not only on `ops.shared.*`; both scaffolds pass
|
|
1677
|
-
the same `shared` array to the store and the config, so this bites only
|
|
1678
|
-
a hand-wired app.
|
|
1679
|
-
- `ops.entries.move` expires entry tags directly and does not `fanOut`,
|
|
1680
|
-
so a reorder that changes what a shared item's `refList` renders does
|
|
1681
|
-
not expire `shared:<name>`.
|
|
1682
|
-
- The package root (`src/index.ts`) must stay free of Node builtins:
|
|
1683
|
-
every scaffolded section does `import { f, smoodly } from "smoodly"`,
|
|
1684
|
-
and a `"use client"` section's browser build would fail on one.
|
|
1685
|
-
`sharedEntryId` (`src/shared-id.ts`) moved off `node:crypto` onto Web
|
|
1686
|
-
Crypto for this reason (fix pass, 2026-09-07). Nothing tests the
|
|
1687
|
-
invariant itself — a future addition that reaches for `node:fs`,
|
|
1688
|
-
`node:crypto`, etc. from a module reachable through the root would
|
|
1689
|
-
regress it silently.
|
|
1284
|
+
- Video transcoding and adaptive streaming: a Mux or Cloudflare Stream
|
|
1285
|
+
loader behind the `media.loader` seam, if a site ever needs more than
|
|
1286
|
+
an MP4 over Storage's CDN. *(2026-09-08, media library)*
|
|
1287
|
+
- Sweeping abandoned `pending` asset rows (an upload that began and
|
|
1288
|
+
never called `finishUpload`); a periodic op or a cleanup on `list`. *(2026-09-08, media library)*
|
|
1289
|
+
- A private bucket with signed reads, for sites that must not expose
|
|
1290
|
+
draft-only media by URL before publish. *(2026-09-08, media library)*
|
|
1291
|
+
- Pre-generated image variants at upload time, for hosts without sharp. *(2026-09-08, media library)*
|
|
1292
|
+
- Image dimensions server-side cover JPEG and PNG (`imageDimensions`); a
|
|
1293
|
+
WebP or AVIF seeded through `AssetStore.create` lands without a size
|
|
1294
|
+
and resolves to nothing until the row is edited. Browser uploads
|
|
1295
|
+
measure every type. *(2026-09-08, media library)*
|
|
1690
1296
|
|
|
1691
|
-
|
|
1297
|
+
#### History and publishing
|
|
1692
1298
|
|
|
1693
|
-
-
|
|
1694
|
-
|
|
1695
|
-
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
-
|
|
1699
|
-
|
|
1700
|
-
overlay's ghost rows can be.
|
|
1701
|
-
- ~~The Pages row menu's Move up / Move down could retire once the grip's
|
|
1702
|
-
arrow keys are familiar; they stay for now as the discoverable path.~~
|
|
1703
|
-
Retired 2026-09-15, with the outline rows' ↑/↓; list-field rows keep theirs.
|
|
1704
|
-
- The grip's arrow keys in the pages tree step one VISIBLE slot with the
|
|
1705
|
-
depth clamped — among siblings in a flat run, but across a depth change
|
|
1706
|
-
the step follows the clamp. Documented as is.
|
|
1707
|
-
- A dragged subtree stays collapsed after the drop (`onDragStart`
|
|
1708
|
-
collapses it through `toggle` and nothing re-expands it).
|
|
1709
|
-
- Walk findings (2026-09-09, every surface walked signed-in; the pointer
|
|
1710
|
-
path with synthetic pointer events, autoscroll in Playwright because
|
|
1711
|
-
the desktop pane pauses animation frames while hidden):
|
|
1712
|
-
- FIXED: the lifted row's transform grew the scroller's overflow, so
|
|
1713
|
-
the bottom-edge autoscroll chased it forever and the list scrolled
|
|
1714
|
-
away under the pointer; `clampOffset` now holds the transform inside
|
|
1715
|
-
the rows' extent while the slot keeps following the pointer.
|
|
1716
|
-
- A depth-1 row dragged to the very end of the tree with no drift
|
|
1717
|
-
projects onto the last root row's child slot, which a mount or the
|
|
1718
|
-
home page refuses (muted row); the editor must drift left 20px to
|
|
1719
|
-
land at the root. By spec §4 (depth = own depth + drift), noted as a
|
|
1720
|
-
discoverability question for the canvas-drag round.
|
|
1721
|
-
- The rows are held in place until the reload lands (checked with a
|
|
1722
|
-
1.2 s fetch delay): no flicker on a fast stack either.
|
|
1723
|
-
|
|
1724
|
-
## Follow-ups (logged 2026-09-06)
|
|
1299
|
+
- Client-side undo in the editor (a stack of tree snapshots); the store
|
|
1300
|
+
is no longer an undo stack by design. *(2026-09-13, history)*
|
|
1301
|
+
- The author per version row and in the lists; named versions. *(2026-09-13, history)*
|
|
1302
|
+
- History on the canvas card's menu and in the list row menus. *(2026-09-13, history)*
|
|
1303
|
+
- A diff between two versions. *(2026-09-13, history)*
|
|
1304
|
+
- The lists' row menus (Pages, Entries) do not offer Unpublish; the
|
|
1305
|
+
document's own control does. *(2026-09-13, publish flow)*
|
|
1725
1306
|
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
-
|
|
1739
|
-
`
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
`
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1307
|
+
#### Collaboration
|
|
1308
|
+
|
|
1309
|
+
- A hard lock with takeover, if a real team asks: presence has the facts. *(2026-09-26, concurrency)*
|
|
1310
|
+
- Presence on the members and media screens; today only pages, entries and
|
|
1311
|
+
global items track. *(2026-09-26, concurrency)*
|
|
1312
|
+
- Postgres Changes for live list refresh; the lists still load on navigation. *(2026-09-26, concurrency)*
|
|
1313
|
+
- Field-level merging of two editors' changes; the guard refuses whole documents. *(2026-09-26, concurrency)*
|
|
1314
|
+
- A dropped socket clears presence on Realtime's timeout (tens of seconds); the
|
|
1315
|
+
lock outlives a closed tab by that much. *(2026-09-26, concurrency)*
|
|
1316
|
+
|
|
1317
|
+
#### Members
|
|
1318
|
+
|
|
1319
|
+
- Members management shipped 2026-09-25 (spec
|
|
1320
|
+
`docs/superpowers/specs/2026-09-25-members-management-design.md`); out
|
|
1321
|
+
of its scope: a pending-invite status and "resend invite" on a row
|
|
1322
|
+
(needs the Auth user's `last_sign_in_at`, which the cloud projection
|
|
1323
|
+
does not carry); removing the Auth account together with the member
|
|
1324
|
+
row; invites from the cloud's `/admin` (they are smoodly.io's); a
|
|
1325
|
+
project whose Auth is OAuth-only (the invite mail asks for a password). *(2026-09-25, settings + members)*
|
|
1326
|
+
- The members list is not paged (tens of rows, not thousands). *(2026-09-25, settings + members)*
|
|
1327
|
+
|
|
1328
|
+
#### Redirects
|
|
1329
|
+
|
|
1330
|
+
- Destinations outside Smoodly, extension and query-string addresses,
|
|
1331
|
+
code-owned renames (a collection `path`, a fixed slug): the guide's
|
|
1332
|
+
`next.config` recipe; a `redirects` table is OQ 34. *(2026-09-26, redirects)*
|
|
1333
|
+
- The content API's `getPage("/old")` returns null for a redirect row
|
|
1334
|
+
(spec §7); a `follow` option is a one-line addition if a site asks. *(2026-09-26, redirects)*
|
|
1335
|
+
- A redirect row has no audit columns (who, when); add them with a
|
|
1336
|
+
need. The Redirects page is not paged. *(2026-09-26, redirects)*
|
|
1337
|
+
|
|
1338
|
+
#### Export and import
|
|
1339
|
+
|
|
1340
|
+
- `import --replace`: delete the space's content and media first, after
|
|
1341
|
+
typing the space to confirm — a backup restored over a live project.
|
|
1342
|
+
Mention it in the not-empty refusal when it lands. *(2026-09-28, export/import)*
|
|
1343
|
+
- A readable export for leaving Smoodly: one document per page or entry,
|
|
1344
|
+
fields resolved, no internal IDs. *(2026-09-28, export/import)*
|
|
1345
|
+
- A "Download backup" button in the admin: the same export behind an op,
|
|
1346
|
+
zipped for an editor. *(2026-09-28, export/import)*
|
|
1347
|
+
- Import cannot expire the app's cache tags; it prints a restart note. A
|
|
1348
|
+
revalidate call through the app (an op the CLI could reach) would make
|
|
1349
|
+
the note unnecessary. *(2026-09-28, export/import)*
|
|
1350
|
+
|
|
1351
|
+
#### Content health and drift
|
|
1352
|
+
|
|
1353
|
+
- Content health lists an entry slug outside the grammar, but its link
|
|
1354
|
+
opens an entry form that marks nothing on the slug: drift leaves the
|
|
1355
|
+
slug out on purpose (a marker would flash while it is typed), so only
|
|
1356
|
+
the publish refusal marks it. Mark it in the form once the box has lost
|
|
1357
|
+
focus, if editors miss it. *(2026-09-29, pre-install fixes)*
|
|
1358
|
+
- Content health does not check page slugs: pages refuse a bad slug at
|
|
1359
|
+
create and rename, so only an import of older data or a hand edit could
|
|
1360
|
+
hold one. *(2026-09-29, pre-install fixes)*
|
|
1361
|
+
- Refs inside unknown nodes are not indexed: without a schema nothing
|
|
1362
|
+
says which values are refs, so safe-delete cannot see them. Publish
|
|
1363
|
+
refuses such nodes and a live one renders nothing. *(2026-09-27, editor safety)*
|
|
1364
|
+
- Leftover shared values after `.localized()` is turned on stay on the
|
|
1365
|
+
entry as every locale's fallback; removing them for good waits on
|
|
1366
|
+
the fix tooling (OQ 35). `localization-on` is reported only where a
|
|
1367
|
+
locale has no value of its own, so a fully translated entry no
|
|
1368
|
+
longer lists it. *(2026-09-27, editor safety)*
|
|
1369
|
+
- A CLI drift report was left out (the admin already has the config and
|
|
1370
|
+
the database; a CLI needs the service key and a TS config loader). *(2026-09-27, editor safety)*
|
|
1371
|
+
- Turning `.localized()` off for a field with per-locale values leaves
|
|
1372
|
+
those values in `entry_locales.fields` until the next save of each
|
|
1373
|
+
locale (a save writes only the current split) — the drift/codemod
|
|
1374
|
+
pipeline (Phase 2) is where "keep en, discard fi" belongs. *(2026-09-06)*
|
|
1375
|
+
|
|
1376
|
+
#### Schema and migrations
|
|
1377
|
+
|
|
1378
|
+
- The views of a removed collection stay (spec 2026-09-26 §3); a
|
|
1379
|
+
`drop view` is the developer's. *(2026-09-26, schema migrations)*
|
|
1380
|
+
- `smoodly migrate` never pushes; a `--push` flag if the two-step
|
|
1381
|
+
proves annoying. *(2026-09-26, schema migrations)*
|
|
1382
|
+
|
|
1383
|
+
#### Data lifecycle
|
|
1384
|
+
|
|
1385
|
+
- A locale dropped from `locales.supported` leaves its `page_locales`,
|
|
1386
|
+
`page_versions` and `paths` rows in place but unreachable — nothing
|
|
1387
|
+
lists or serves them, and nothing cleans them up. It is the mirror of
|
|
1388
|
+
the materializer's top-up, and wants the same deliberate answer (an
|
|
1389
|
+
admin-visible orphan list, or a prune op). *(2026-09-06)*
|
|
1390
|
+
- `addLocale` does not bump the entry's `updatedAt` while `removeLocale`
|
|
1391
|
+
does. Both adapters agree, so the contract is honest, but "adding a
|
|
1392
|
+
language is not an edit and removing one is" is arbitrary — decide it
|
|
1393
|
+
deliberately before someone sorts a list by Updated. *(2026-09-06)*
|
|
1394
|
+
- No `rebuildPaths()`: the index is documented as derived and rebuildable, but
|
|
1395
|
+
nothing rebuilds it. *(2026-09-05)*
|
|
1396
|
+
|
|
1397
|
+
#### Config and registration
|
|
1398
|
+
|
|
1399
|
+
- `LOCALE_CODE_RE` accepts any 2–4 letter subtag as script or region
|
|
1400
|
+
(`en-ZZZZ` passes), and a duplicate entry in `supported` passes
|
|
1401
|
+
silently — boot validation catches typos in shape, not in fact. *(2026-09-06)*
|
|
1402
|
+
- `entryPathRows` relies on the stores validating locale keys on write
|
|
1403
|
+
(no guard of its own); `list`/`getMany` accept an unsupported locale
|
|
1404
|
+
silently and read as "no content". *(2026-09-06)*
|
|
1405
|
+
- `sections` is passed twice — to `createSmoodlySite` (ref resolution) and to
|
|
1406
|
+
`createPageRenderer` (the component map) — and `elements` cannot be passed
|
|
1407
|
+
at all; the two lists can drift silently. *(2026-09-05)*
|
|
1408
|
+
- `SmoodlyConfig.registry.pages` (`Registered` in `src/config.ts`) has no
|
|
1409
|
+
call signature, so every app's glue casts it `as PageRegistration[]`. *(2026-09-05)*
|
|
1410
|
+
- Two `entryPage` registrations for one collection are silently accepted.
|
|
1411
|
+
`PageStore.rename` still accepts a locale outside `supported`; the admin
|
|
1412
|
+
op rejects it first, so only a direct store caller reaches it. *(2026-09-05)*
|
|
1413
|
+
|
|
1414
|
+
#### Scale and performance
|
|
1415
|
+
|
|
1416
|
+
- `listLive` / `listRedirects` on Supabase read at most PostgREST's
|
|
1417
|
+
`max_rows` (1000) per locale, as before, now ordered by (anchor, path)
|
|
1418
|
+
and sorted by URL in JS; page them if a locale's redirect list grows
|
|
1419
|
+
past it. *(2026-09-29, nested collection paths)*
|
|
1420
|
+
- Scan paging: `health.scan` makes one bulk read per table (versions by
|
|
1421
|
+
id in chunks of 100), and PostgREST's max-rows would cap a very large
|
|
1422
|
+
space. Page it, and add a rail badge, when a real site's size asks. *(2026-09-27, editor safety)*
|
|
1423
|
+
- Server search for ref candidates: `useRefOptions` loads the whole
|
|
1424
|
+
target collection once per locale. When a collection outgrows that,
|
|
1425
|
+
the hook changes (a search op, plus "titles for these ids" for the
|
|
1426
|
+
picked rows); `RefPicker` takes options as a prop and does not. *(2026-09-17, reference picker and tags)*
|
|
1427
|
+
- `entries.distinct` flattens in JS over `entries.list`; an RPC over
|
|
1428
|
+
`jsonb_array_elements` when a collection outgrows it. *(2026-09-17, reference picker and tags)*
|
|
1429
|
+
- `InspectorFields` filters hidden entries per render; a section with
|
|
1430
|
+
many conditions re-evaluates every keystroke. Fine at the sizes seen. *(2026-09-15, section settings)*
|
|
1431
|
+
- Ops are built per call (`createAdminOps` inside the handler) so the
|
|
1432
|
+
session rides in as `actor`; if that ever measures, memoize the
|
|
1433
|
+
actor-free parts. *(2026-09-13, history)*
|
|
1434
|
+
- A GIN index on `entries.fields` when a customer's collection is large
|
|
1435
|
+
enough to need it; additive, no migration. *(2026-09-11, queries)*
|
|
1436
|
+
- Every revalidating op (publish, delete, a save of published content)
|
|
1437
|
+
answers with a fresh RSC render of the admin route — the layout's
|
|
1438
|
+
styles included, ~30 KB — because `updateTag` runs inside the server
|
|
1439
|
+
action and Next re-renders the tree it was posted to. Harmless now
|
|
1440
|
+
that the shell lives in the layout (the empty page subtree is what
|
|
1441
|
+
remounts), but it is the price of server actions as the transport. *(2026-09-08, client routing)*
|
|
1442
|
+
- `getSmoodlyShared` reads the store per request in draft mode and once
|
|
1443
|
+
per (item, locale) otherwise; a layout calling it for several items
|
|
1444
|
+
makes one round trip each — batch if it shows. *(2026-09-07, shared content)*
|
|
1445
|
+
- `ops.shared.get` runs `ensureSharedItems` and then lists the item
|
|
1446
|
+
again; one round trip could go. *(2026-09-07, shared content)*
|
|
1447
|
+
- `ensureSharedItems` runs on every `ops.shared.list()` — which is every
|
|
1448
|
+
editor mount, not only the Shared content page — costing one
|
|
1449
|
+
`entries.list` per registration per open. *(2026-09-07, shared content)*
|
|
1450
|
+
- `fanOut` resolves every shared registration's row id (one
|
|
1451
|
+
`entries.list` each) on every entry write, so it can expire
|
|
1452
|
+
`shared:<name>` when something the item references changes; a row-id
|
|
1453
|
+
cache would remove the reads but has to survive materialization. *(2026-09-07, shared content)*
|
|
1758
1454
|
- `ensureFixedNodes` runs on every `pages.list` and every ROOT
|
|
1759
1455
|
`pages.create` — one `listPages()` each, and a second one when it
|
|
1760
1456
|
tried to create something. It also treats a root record sitting at a
|
|
1761
1457
|
fixed slug as that node even when its `template` is a foreign one: the
|
|
1762
1458
|
node is adopted, never recreated, and the ops then guard it as
|
|
1763
|
-
code-owned.
|
|
1764
|
-
- Two admins loading at once race on materialization. Any spec whose
|
|
1765
|
-
create fails is warned about and skipped, not fatal to the rest of the
|
|
1766
|
-
list — except the loser's `slugTaken`, which means the other admin won
|
|
1767
|
-
the same race: that one message is swallowed on purpose, silently,
|
|
1768
|
-
with no warn.
|
|
1769
|
-
- The `smoodly_reorder` / `smoodly_reorder_pages` RPCs keep their
|
|
1770
|
-
implicit sibling scope (decided 2026-09-06: every caller passes exactly
|
|
1771
|
-
one sibling set). A parent argument waits for a caller that needs it.
|
|
1772
|
-
- The row menu in `PagesList.tsx` does not close on Escape — only a click
|
|
1773
|
-
on the backdrop closes it.
|
|
1774
|
-
- `PageSettings` re-seeds its inputs from `record` on every rename (and
|
|
1775
|
-
every publish), so keystrokes typed while a rename is in flight can be
|
|
1776
|
-
dropped. The blur- and Enter-driven commits themselves are unaffected.
|
|
1777
|
-
- `subtreeIds` and `entrySubtreeIds` in `ops-impl.ts` are the same O(n²)
|
|
1778
|
-
walk over a full list, written twice. One generic descendant walk when
|
|
1779
|
-
the list sizes make it matter.
|
|
1780
|
-
- `publish`'s "has no draft to publish" branch is unreachable with the
|
|
1781
|
-
memory store — `createPage` gives every templated page a first version
|
|
1782
|
-
— and is untested. It is defence for the Supabase adapter.
|
|
1783
|
-
- Classifications the ops tests do not assert: a move that breaks the
|
|
1784
|
-
home page's no-children rule or exceeds the tree depth (create asserts
|
|
1785
|
-
both), and `entries.move`'s cross-collection and sibling-slug guards.
|
|
1786
|
-
`MemoryEntryStore.move` has no children guard at all — the subtree
|
|
1787
|
-
rides along — so `has child` is reachable from delete only.
|
|
1788
|
-
- The seed's early exit still probes `/` alone: a run that dies between
|
|
1789
|
-
the two publishes reports "Already seeded" on the next run and never
|
|
1790
|
-
gets its `company` folder.
|
|
1791
|
-
- `create-smoodly-app` scaffolds now ship `pages: { tree: { depth: 3 } }`
|
|
1792
|
-
— the template build rewrites only the locale set in `smoodly/config.ts`
|
|
1793
|
-
and `href` in `smoodly/server.ts` (each with its comment) — so nesting
|
|
1794
|
-
is on by default in a fresh app.
|
|
1795
|
-
- A childless folder row is inert on click (a mount opens its collection,
|
|
1796
|
-
a page opens the editor, a folder with children expands); "Add page
|
|
1797
|
-
here" lives in the row menu only.
|
|
1798
|
-
- The tree rows nest interactive buttons — the chevron and the `···`
|
|
1799
|
-
menu — inside a `role="button"` row. It works, because both stop the
|
|
1800
|
-
click and the keydown, but it is an accessibility smell a real treegrid
|
|
1801
|
-
would not have.
|
|
1802
|
-
- `ensureFixedNodes` warns on every `pages.list` while a fixed node's
|
|
1803
|
-
locale cannot land (an editor page holds its path); a one-time notice
|
|
1804
|
-
in the admin would be kinder than a warning per listing.
|
|
1805
|
-
- Cache tags are still `page:<id>`: publishing one locale expires every
|
|
1806
|
-
locale's cached unit of that page (spec 2026-09-06 §4). A
|
|
1807
|
-
`page:<id>:<locale>` tag would halve the revalidations on a bilingual
|
|
1808
|
-
site.
|
|
1809
|
-
- The Pages list's confirms (`Add <locale>`, `Remove from <locale>`,
|
|
1810
|
-
`Delete …`) are the design system's dialog since 2026-09-25;
|
|
1811
|
-
`EntriesList` still reports a refused add or remove with
|
|
1812
|
-
`window.alert` — the follow-up, together with the row menu extraction
|
|
1813
|
-
already logged above.
|
|
1814
|
-
- `EditorView.tsx` is ~850 lines now that it also holds the locale switch,
|
|
1815
|
-
add and remove flows — see the 2026-09-03 follow-up below for the seams
|
|
1816
|
-
(that entry is the one to act on).
|
|
1817
|
-
- `i18n/request.ts` returns `{ locale, messages: {} }`. next-intl 4.14.2
|
|
1818
|
-
throws `No messages found` when the provider inherits none, and the
|
|
1819
|
-
example has no UI strings of its own, so the object is empty; a
|
|
1820
|
-
scaffolded site that translates its own UI fills it in (the comment in
|
|
1821
|
-
the file points at next-intl's configuration docs). No `messages/`
|
|
1822
|
-
folder is scaffolded — UI-string translation is next-intl's job, not
|
|
1823
|
-
Smoodly's.
|
|
1824
|
-
- `i18n/request.ts` reads `next/root-params` (next-intl's current
|
|
1825
|
-
guidance; `requestLocale` is deprecated). Its real types are generated
|
|
1826
|
-
into `.next/types/` by `next dev` / `next build` / `next typegen`;
|
|
1827
|
-
`tsc` before a first build still passes, because `next` ships a bare
|
|
1828
|
-
`declare module 'next/root-params'` for exactly that window.
|
|
1829
|
-
- The four single-language route files in
|
|
1830
|
-
`packages/create-smoodly-app/variants/single/` are compiled by nothing
|
|
1831
|
-
but the CI hosted-mode e2e's `next build` — the example is the
|
|
1832
|
-
multilingual layout, so `npm test` never type-checks them. A renderer
|
|
1833
|
-
or admin signature change breaks them there, a whole CI job later than
|
|
1834
|
-
it should. The fix is a `tsconfig` project for `variants/` with the
|
|
1835
|
-
scaffold's `@/*` paths mapped into the example, or a scaffold-then-
|
|
1836
|
-
`tsc` smoke test.
|
|
1837
|
-
- `ArticleList` renders titles, not links. A section that linked to its
|
|
1838
|
-
entries would put the entry's `path` through the app's `Link` / `href`
|
|
1839
|
-
the way `components/pages/Page.tsx` does — `getEntries` returns `path`
|
|
1840
|
-
already; the section simply does not use it.
|
|
1841
|
-
- `smoodlyMetadata` emits `alternates.languages` as relative URLs and
|
|
1842
|
-
neither the example nor either template sets `metadataBase`, so the
|
|
1843
|
-
rendered `hreflang` hrefs are relative (`/fi/yritys/tietoa-meista`).
|
|
1844
|
-
Crawlers want absolute ones: a real site sets `metadataBase` in its
|
|
1845
|
-
root layout. `next build` on the example (2026-09-06, Next 16.3.4) does
|
|
1846
|
-
not warn — every route is request-rendered, so nothing resolves the
|
|
1847
|
-
relative URLs at build time.
|
|
1848
|
-
- The proxy's matcher hard-codes `admin` and `api`
|
|
1849
|
-
(`/((?!admin|api|_next|_vercel|.*\..*).*)`). An app that mounts the
|
|
1850
|
-
admin somewhere else edits `proxy.ts` by hand; nothing derives the
|
|
1851
|
-
matcher from the config.
|
|
1852
|
-
- `classify` in `ops-impl.ts` is ~20 order-sensitive substring checks over
|
|
1853
|
-
store messages — `"already exists in locale"` must precede
|
|
1854
|
-
`"already exists"`, or an addLocale conflict reads as a slug conflict. A
|
|
1855
|
-
typed code on `pageErrors` (thrown and matched, not re-parsed) would
|
|
1856
|
-
remove the ordering hazard before plan 2 adds the entry messages.
|
|
1857
|
-
- `ops-impl.ts` is ~436 lines with a ~165-line `pages` group. The guard
|
|
1858
|
-
helpers (`nameOf`, `isHomeNode`, `supportedLocale`, `mountGuard`,
|
|
1859
|
-
`owned`) are the extraction seam — a `pages-ops.ts` taking them as
|
|
1860
|
-
collaborators. The `entries` group now duplicates two of them outright:
|
|
1861
|
-
the `supportedLocale` guard and the `parentLocales` computation exist
|
|
1862
|
-
once per group.
|
|
1459
|
+
code-owned. *(2026-09-06)*
|
|
1863
1460
|
- `fixedNodeSpecs(config)` rebuilds the whole spec list on every
|
|
1864
1461
|
`fixedNodeOf` call, and the ops call `fixedNodeOf` per guarded write. A
|
|
1865
|
-
`WeakMap` memo keyed by the config object is the whole fix.
|
|
1462
|
+
`WeakMap` memo keyed by the config object is the whole fix. *(2026-09-06)*
|
|
1866
1463
|
- `SupabasePageStore.updateLocale` re-reads the record (`must(id)`) after
|
|
1867
1464
|
every update, so `setTemplate` is N+1 in the page's locales: one update
|
|
1868
1465
|
and one full read each. Only the last read is used. The Supabase entry
|
|
1869
1466
|
adapter is the same shape: `create` is insert → insert → read →
|
|
1870
1467
|
version → read → paths → refs, and `removeLocale` re-fetches the entry
|
|
1871
|
-
three times.
|
|
1872
|
-
- The single-language branch — no locale selector in the Pages list, no
|
|
1873
|
-
switcher, no add/remove — has no automated coverage; only three sites
|
|
1874
|
-
branch on the locale count (`PagesList.tsx`, `EditorView.tsx` and
|
|
1875
|
-
`page-renderer.tsx`'s alternates), so a regression there shows up only
|
|
1876
|
-
in a scaffolded single-language app.
|
|
1877
|
-
- No test pins that a published child's breadcrumb carries an UNPUBLISHED
|
|
1878
|
-
ancestor's title with `hasPage: false`; `site.test.tsx` covers the
|
|
1879
|
-
folder ancestor only, and the two paths share one `hasPage` predicate.
|
|
1880
|
-
- A locale dropped from `locales.supported` leaves its `page_locales`,
|
|
1881
|
-
`page_versions` and `paths` rows in place but unreachable — nothing
|
|
1882
|
-
lists or serves them, and nothing cleans them up. It is the mirror of
|
|
1883
|
-
the materializer's top-up, and wants the same deliberate answer (an
|
|
1884
|
-
admin-visible orphan list, or a prune op).
|
|
1885
|
-
- `LOCALE_CODE_RE` accepts any 2–4 letter subtag as script or region
|
|
1886
|
-
(`en-ZZZZ` passes), and a duplicate entry in `supported` passes
|
|
1887
|
-
silently — boot validation catches typos in shape, not in fact.
|
|
1888
|
-
- `MemoryPageStore.publish` throws the "is a folder" message when a
|
|
1889
|
-
templated row has no draft version. Unreachable through the ops (a
|
|
1890
|
-
templated page always gets a first version), but the message would be
|
|
1891
|
-
wrong if it ever were.
|
|
1892
|
-
- The Pages list offers "Add <locale>" on a dimmed row whose parent lacks
|
|
1893
|
-
that locale, and learns the refusal only after the round trip.
|
|
1894
|
-
`pagesTreeRows` already carries every node's locales, so the parent's
|
|
1895
|
-
presence could disable the item with its reason up front, exactly as
|
|
1896
|
-
the editor's switcher does with `parentLocales`. `EntriesList` has the
|
|
1897
|
-
same gap (and `entryRows` would have to carry the locales again — the
|
|
1898
|
-
dead field was deleted rather than left unused); it is unreachable
|
|
1899
|
-
today, since nothing in the admin creates a child entry.
|
|
1900
|
-
- No version history UI for entries (nor pages): `listVersions` exists on
|
|
1901
|
-
both stores; a history pane with restore is the follow-up. Restoring a
|
|
1902
|
-
versioned entry's snapshot must also restore the shared fields (the
|
|
1903
|
-
snapshot is merged).
|
|
1468
|
+
three times. *(2026-09-06)*
|
|
1904
1469
|
- `entries.list` filters `{ locale, status }` in memory after fetching
|
|
1905
1470
|
every row (PostgREST cannot filter the parent by an embedded row
|
|
1906
1471
|
without `!inner`, which would drop the other rows); fine at collection
|
|
1907
|
-
sizes today.
|
|
1908
|
-
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1472
|
+
sizes today. *(2026-09-06)*
|
|
1473
|
+
- `assertPublishable` (`store.ts`) awaits one `entries.get` per
|
|
1474
|
+
referenced entry, sequentially, on every publish — a `refList` of a
|
|
1475
|
+
dozen articles is a dozen serial reads, now per locale. `getMany(ids,
|
|
1476
|
+
locale)` takes exactly that shape and would make it one call. *(2026-09-06)*
|
|
1477
|
+
- `loadPage` calls `listPages()` for ancestors and children on every cache
|
|
1478
|
+
miss; fine for hundreds of nodes, a `children(id)`/`chain(id)` store query
|
|
1479
|
+
is the fix when a site outgrows it. *(2026-09-05)*
|
|
1480
|
+
- `loadEntry` probes each mounted collection in turn: the `paths` row
|
|
1481
|
+
carries the target id but not its collection. *(2026-09-05)*
|
|
1482
|
+
|
|
1483
|
+
#### Code health
|
|
1484
|
+
|
|
1485
|
+
- `SharedForm.tsx` is a near-verbatim fork of `EntryForm.tsx`
|
|
1486
|
+
(`applyResult`, `InfoRow`, the ref-options effect; the locale helpers
|
|
1487
|
+
are shared since 2026-09-08) and `SharedList.tsx` is the third copy of
|
|
1488
|
+
the remembered-locale block (`EntriesList`, `PagesList`); extract a
|
|
1489
|
+
`useFormLocale` hook when the next form arrives. *(2026-09-07, shared content)*
|
|
1490
|
+
- `CollectionSchema.kind` was widened to carry `"shared"`, so a
|
|
1491
|
+
`SharedSchema` typechecks where `registry.collections` is expected;
|
|
1492
|
+
`defineConfig` should throw on a `kind === "shared"` entry listed there. *(2026-09-07, shared content)*
|
|
1493
|
+
- Because `fanOut` lists every shared registration, an entry store built
|
|
1494
|
+
without `options.shared` now fails on every ordinary entry write with
|
|
1495
|
+
"no collection named", not only on `ops.shared.*`; both scaffolds pass
|
|
1496
|
+
the same `shared` array to the store and the config, so this bites only
|
|
1497
|
+
a hand-wired app. *(2026-09-07, shared content)*
|
|
1498
|
+
- `subtreeIds` and `entrySubtreeIds` in `ops-impl.ts` are the same O(n²)
|
|
1499
|
+
walk over a full list, written twice. One generic descendant walk when
|
|
1500
|
+
the list sizes make it matter. *(2026-09-06)*
|
|
1501
|
+
- `ops-impl.ts` is ~436 lines with a ~165-line `pages` group. The guard
|
|
1502
|
+
helpers (`nameOf`, `isHomeNode`, `supportedLocale`, `mountGuard`,
|
|
1503
|
+
`owned`) are the extraction seam — a `pages-ops.ts` taking them as
|
|
1504
|
+
collaborators. The `entries` group now duplicates two of them outright:
|
|
1505
|
+
the `supportedLocale` guard and the `parentLocales` computation exist
|
|
1506
|
+
once per group. *(2026-09-06)*
|
|
1919
1507
|
- `EntryForm.tsx` grew the locale lifecycle beside the fields; the
|
|
1920
1508
|
`LocaleSwitcher` is shared with the editor now, the add/remove flows
|
|
1921
|
-
are not (`doAddLocale`/`doRemoveLocale` exist twice).
|
|
1922
|
-
- Turning `.localized()` off for a field with per-locale values leaves
|
|
1923
|
-
those values in `entry_locales.fields` until the next save of each
|
|
1924
|
-
locale (a save writes only the current split) — the drift/codemod
|
|
1925
|
-
pipeline (Phase 2) is where "keep en, discard fi" belongs.
|
|
1926
|
-
- Resolved 2026-09-26 (schema migrations spec): the repo-root
|
|
1927
|
-
`supabase/migrations/` is the package's core files plus a views file
|
|
1928
|
-
that `npm run db:migrate` writes from
|
|
1929
|
-
`examples/site/smoodly/collections.ts` — the same source the
|
|
1930
|
-
create-smoodly-app templates use; `scripts/emit-sql.ts` and its own
|
|
1931
|
-
fixture collections are gone (`scripts/emit-migration.ts` drafts the
|
|
1932
|
-
next core file).
|
|
1933
|
-
- In the entry form, a shared ref field (e.g. an article's author) shows
|
|
1934
|
-
`—` in a locale where the target is absent, because the picker offers
|
|
1935
|
-
only targets present in the edited locale; the value is kept on the
|
|
1936
|
-
node and the other locale's byline is unchanged, but it looks empty
|
|
1937
|
-
and cannot be re-set from that locale.
|
|
1938
|
-
- `entryPathRows` relies on the stores validating locale keys on write
|
|
1939
|
-
(no guard of its own); `list`/`getMany` accept an unsupported locale
|
|
1940
|
-
silently and read as "no content".
|
|
1941
|
-
- The Supabase entry adapter's `removeLocale` deletes the row and its
|
|
1942
|
-
versions before the paths rewrite with no rollback; its residue
|
|
1943
|
-
comment describes the opposite case (a surviving path row for a
|
|
1944
|
-
removed locale, not a present node without its path row).
|
|
1945
|
-
- The entry form's locale switch, add and remove confirm before
|
|
1946
|
-
discarding unsaved edits (a `dirty` flag; the form has no autosave,
|
|
1947
|
-
unlike the page editor's flush).
|
|
1948
|
-
- `addLocale` does not bump the entry's `updatedAt` while `removeLocale`
|
|
1949
|
-
does. Both adapters agree, so the contract is honest, but "adding a
|
|
1950
|
-
language is not an edit and removing one is" is arbitrary — decide it
|
|
1951
|
-
deliberately before someone sorts a list by Updated.
|
|
1952
|
-
- The Supabase entry adapter's rollbacks are best-effort and can mask the
|
|
1953
|
-
original throw: `create`'s catch runs `paths.remove` before the node
|
|
1954
|
-
delete (a failing remove strands the node and replaces the real error),
|
|
1955
|
-
`update` and `move` restore by writing, and those writes can throw in
|
|
1956
|
-
place of the rewrite's rejection. `recordVersion` is two statements
|
|
1957
|
-
(insert, then point at it), so a failure between them leaves an
|
|
1958
|
-
unreferenced `entry_versions` row — invisible to every policy and
|
|
1959
|
-
harmless, but it accumulates.
|
|
1509
|
+
are not (`doAddLocale`/`doRemoveLocale` exist twice). *(2026-09-06)*
|
|
1960
1510
|
- `removeLocale`'s blocker list (`localeInUse`) is unordered in the
|
|
1961
1511
|
Supabase adapter — the children query has no `.order()` — so a
|
|
1962
1512
|
multi-blocker message can name the titles in a different order than the
|
|
1963
|
-
memory adapter's. Message text only; the contract case has one blocker.
|
|
1513
|
+
memory adapter's. Message text only; the contract case has one blocker. *(2026-09-06)*
|
|
1964
1514
|
- `entry-store.ts` is ~624 lines. The pure helpers (`localizedKeys`,
|
|
1965
1515
|
`splitFields`, `entryFieldsIn`, `entryRefEdges`, `entryTitleIn`) are the
|
|
1966
1516
|
extraction seam from the memory adapter, the same shape as the
|
|
1967
|
-
`EditorView.tsx` and `ops-impl.ts` entries
|
|
1968
|
-
- In a single-language project the entry form renders `slug` last:
|
|
1969
|
-
`localizedFieldKeys` puts it in the `own` group, which follows
|
|
1970
|
-
`shared`, and no section labels are shown — so the field order the
|
|
1971
|
-
schema declares is not the order on screen.
|
|
1972
|
-
- `admin-entries-list.test.ts` has no case for a row with differing
|
|
1973
|
-
pointers (the `draft edits` status), nor one asserting that an absent
|
|
1974
|
-
row's `updatedAt` falls back to the node's. Two assertions.
|
|
1975
|
-
- `assertPublishable` (`store.ts`) awaits one `entries.get` per
|
|
1976
|
-
referenced entry, sequentially, on every publish — a `refList` of a
|
|
1977
|
-
dozen articles is a dozen serial reads, now per locale. `getMany(ids,
|
|
1978
|
-
locale)` takes exactly that shape and would make it one call.
|
|
1979
|
-
|
|
1980
|
-
## Follow-ups (logged 2026-09-05)
|
|
1981
|
-
|
|
1982
|
-
- A page's cached unit embeds its ancestors' and children's titles and
|
|
1983
|
-
paths (the `page` prop) but is tagged `page:<own id>` only. The admin
|
|
1984
|
-
ops now expire the neighbours they change — publish and delete the
|
|
1985
|
-
parent, rename and move the subtree and both parents (2026-09-06) — so
|
|
1986
|
-
what remains stale is a write that does not go through them: the seed,
|
|
1987
|
-
a script, any out-of-band writer. Closing that needs one tree-wide tag
|
|
1988
|
-
instead of a neighbour list (DESIGN.md OQ #26).
|
|
1989
|
-
- The cached unit is keyed by (locale, path) but tagged by the id found at
|
|
1990
|
-
the first miss, so a path that changes hands without both ids' tags firing
|
|
1991
|
-
serves the previous occupant. Delete fires its own tag and the rename/move
|
|
1992
|
-
fan-out covers the moving record; re-keying the unit by
|
|
1993
|
-
(locale, kind, id) would remove the class (DESIGN.md OQ #26).
|
|
1994
|
-
- `ancestors` on a page hit are NOT filtered by locale (children are): a
|
|
1995
|
-
page ancestor whose `locales` exclude `fi` (`hasPage` true) still
|
|
1996
|
-
produces a breadcrumb link whose path 404s in `fi`.
|
|
1997
|
-
- `smoodlyMetadata` does not apply the registration check `renderSmoodlyPath`
|
|
1998
|
-
applies: a record whose `template` has no registration is a 404 body with
|
|
1999
|
-
its meta emitted beside it.
|
|
2000
|
-
- `sections` is passed twice — to `createSmoodlySite` (ref resolution) and to
|
|
2001
|
-
`createPageRenderer` (the component map) — and `elements` cannot be passed
|
|
2002
|
-
at all; the two lists can drift silently.
|
|
2003
|
-
- `getEntry` by bare slug matches `entrySegment` — the locale row's own
|
|
2004
|
-
`slug`, the same value `paths` is written from — so the two cannot
|
|
2005
|
-
diverge (the double-overlay hazard went with `EntryRecord.i18n`).
|
|
2006
|
-
`getEntry` by id still reads the whole collection to find one row.
|
|
2007
|
-
- `locate` (one primary-key hit on `paths`) runs per request outside the
|
|
2008
|
-
cache, because the cache tag needs the id before the unit can be cached; a
|
|
2009
|
-
later `"use cache"` migration (OQ #19) can fold it in.
|
|
2010
|
-
- Entry-page metadata is alternates only — entries have no `meta`. A
|
|
2011
|
-
`titleField`-based title is a later nicety.
|
|
2012
|
-
- Per-locale section visibility conditions are not modelled (OQ #3). Tree
|
|
2013
|
-
content itself is per locale now — each `page_locales` row points at its
|
|
2014
|
-
own version — so the section fields a locale sees are its own.
|
|
2015
|
-
- `getEntries`/`getEntriesTree` return refs as ids (cheap lists); `getEntry`
|
|
2016
|
-
by path resolves them, `getEntry` by id or slug does not — document or
|
|
2017
|
-
unify when a site needs resolved lists.
|
|
2018
|
-
- `loadPage` calls `listPages()` for ancestors and children on every cache
|
|
2019
|
-
miss; fine for hundreds of nodes, a `children(id)`/`chain(id)` store query
|
|
2020
|
-
is the fix when a site outgrows it.
|
|
2021
|
-
- Views rendered outside the site get `EMPTY_PAGE_CONTEXT`: tests and any
|
|
2022
|
-
preview of a shape that isn't going through `site.load`.
|
|
2023
|
-
- `loadEntry` probes each mounted collection in turn: the `paths` row
|
|
2024
|
-
carries the target id but not its collection.
|
|
2025
|
-
- A published entry under a draft parent is dropped from `getEntriesTree`
|
|
2026
|
-
(the parent is not in the list to hang it from) but returned flat by
|
|
2027
|
-
`getEntries`. Untested either way.
|
|
2028
|
-
- `SmoodlyConfig.registry.pages` (`Registered` in `src/config.ts`) has no
|
|
2029
|
-
call signature, so every app's glue casts it `as PageRegistration[]`.
|
|
2030
|
-
- The entry-page view receives the whole `EntryRecord`, every locale's
|
|
2031
|
-
row in `locales` included (fields, slug, status and version pointers); a
|
|
2032
|
-
client entry page would ship all of it to the browser.
|
|
2033
|
-
- `move` is best-effort across two statements (reorder, then path rewrite):
|
|
2034
|
-
a failed rewrite restores each destination sibling's prior `sort` row by
|
|
2035
|
-
row, so a failure in the rollback itself still leaves the order changed.
|
|
2036
|
-
- The live suites run on vitest's DEFAULT 5s per-test timeout against a real
|
|
2037
|
-
database, while a single contract test makes dozens of round trips. On a
|
|
2038
|
-
loaded or degraded local stack `npm run test:supabase` then fails with bare
|
|
2039
|
-
timeouts on arbitrary tests (every file passes on its own, and the whole
|
|
2040
|
-
run passes with `--testTimeout=30000`). Either raise the timeout in
|
|
2041
|
-
`scripts/test-supabase.sh` or accept the flake in CI.
|
|
2042
|
-
- The `smoodly_reorder` RPC takes no parent scope — the caller always passes
|
|
2043
|
-
one sibling set, so scoping is implicit. Give it a parent argument if a
|
|
2044
|
-
caller ever passes a mixed list.
|
|
2045
|
-
- An explicit JSON `null` VALUE in a declared `order.by` field sorts
|
|
2046
|
-
FIRST in Supabase (a jsonb null is the lowest jsonb value, not SQL
|
|
2047
|
-
NULL, so `nullsFirst: false` does not reach it) and LAST in memory. A
|
|
2048
|
-
MISSING key is SQL NULL and sorts last in both, and the admin only ever
|
|
2049
|
-
produces that — a cleared number field emits `undefined`, which the
|
|
2050
|
-
JSON body drops — so this needs a programmatic or external writer.
|
|
2051
|
-
Mixed types in one field diverge for the same reason: Postgres orders
|
|
2052
|
-
jsonb by type first (null < string < number), memory by `String(...)`.
|
|
2053
|
-
- Rollback gaps that remain: a `smoodly_reorder`/`smoodly_reorder_pages`
|
|
2054
|
-
failure AFTER the re-parent update has no rollback at all; the entry
|
|
2055
|
-
`create` rollback discards its delete result (an orphan row survives
|
|
2056
|
-
silently); and a throw inside any rollback replaces — masks — the original
|
|
2057
|
-
error.
|
|
2058
|
-
- Duplicate sibling `sort` values are reachable — create uses count + 1 while
|
|
2059
|
-
delete and move leave gaps — so `sort` is an ordering hint and creation
|
|
2060
|
-
order breaks ties.
|
|
2061
|
-
- Folders have path rows and resolve as `kind: "page"`; the published-only
|
|
2062
|
-
read policy makes them public 404s, while the admin (write key) sees them.
|
|
2063
|
-
- `setTemplate`/`saveDraft` insert the version row before the pointer update:
|
|
2064
|
-
an infra failure between the two orphans a version.
|
|
2065
|
-
- No `rebuildPaths()`: the index is documented as derived and rebuildable, but
|
|
2066
|
-
nothing rebuilds it.
|
|
2067
|
-
- Two `entryPage` registrations for one collection are silently accepted.
|
|
2068
|
-
`PageStore.rename` still accepts a locale outside `supported`; the admin
|
|
2069
|
-
op rejects it first, so only a direct store caller reaches it.
|
|
2070
|
-
- Entry `move` coverage is thinner than pages' — no omitted-index, clamp or
|
|
2071
|
-
cross-collection tests.
|
|
1517
|
+
`EditorView.tsx` and `ops-impl.ts` entries in this list. *(2026-09-06)*
|
|
2072
1518
|
- The memory stores default to SEPARATE private path indexes while the
|
|
2073
1519
|
Supabase stores share one `paths` table: pages and entries share a URL space
|
|
2074
1520
|
only when the wiring passes one `paths` option to both (convention, not
|
|
2075
|
-
structure).
|
|
1521
|
+
structure). *(2026-09-05)*
|
|
2076
1522
|
- `SupabasePathIndex` falls back to `pathTakenError("?", "?")` when
|
|
2077
1523
|
Postgres reports a unique violation without `details` — the message then
|
|
2078
|
-
names no path.
|
|
1524
|
+
names no path. *(2026-09-05)*
|
|
2079
1525
|
- `chainOf` has no cycle guard: a corrupted `parent_id` loop would spin
|
|
2080
|
-
forever. The stores prevent cycles on `move`, so it is unreachable today.
|
|
2081
|
-
- Design system pass (DESIGN.md §2, "The admin's visual system"): the shell,
|
|
2082
|
-
lists, entry form, login and editor were restyled onto `admin/ui/theme.tsx`.
|
|
2083
|
-
Deferred deliberately, because each is a feature rather than a restyle: the
|
|
2084
|
-
list search field and status filter, the locale select in the shell's top bar
|
|
2085
|
-
(the editor has one; the lists always read the default locale), the article
|
|
2086
|
-
sidebar's taxonomy/SEO/publish-date sections, and the rich-text toolbar. The
|
|
2087
|
-
pages list's tree shape (chevrons, depth indent) and its row `···` menu
|
|
2088
|
-
shipped 2026-09-06, without the drag handle. The tokens for all of them are
|
|
2089
|
-
already in place.
|
|
1526
|
+
forever. The stores prevent cycles on `move`, so it is unreachable today. *(2026-09-05)*
|
|
2090
1527
|
- The `scroll` message in `admin/editor/protocol.ts` is no longer sent: with the
|
|
2091
1528
|
canvas column as the scroller, `EditorView` scrolls it directly instead of
|
|
2092
1529
|
asking the bridge to `scrollIntoView` across the frame. `EditorBridge` still
|
|
2093
1530
|
handles the message, so old and new admins interoperate; drop the message on
|
|
2094
|
-
the next protocol change.
|
|
1531
|
+
the next protocol change. *(2026-09-05)*
|
|
1532
|
+
- `exports` has no `"./package.json"` entry, so tooling that reads a dependency's `package.json` through the exports map cannot. One line to add when something needs it. *(2026-09-04)*
|
|
1533
|
+
- `src/ref-index.ts` contains two raw NUL bytes used as key separators, which makes git show the file as binary in diffs. Replacing them with `\0` escapes would make it diffable again. *(2026-09-04)*
|
|
1534
|
+
- `serialize.ts` structural cast duplicates `PageSchema`; tighten `Registered.schema` so the cast goes. *(2026-09-03)*
|
|
1535
|
+
- `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 became `forms/useRefOptions.ts` (2026-09-17). (logged 2026-09-06, updated 2026-09-08) *(2026-09-03)*
|
|
1536
|
+
|
|
1537
|
+
#### Tests
|
|
1538
|
+
|
|
1539
|
+
- The media test uploads a starter photo under a suffixed name rather
|
|
1540
|
+
than a fixture file, so `e2e/fixtures/` from the spec does not exist. *(2026-09-26, browser suite)*
|
|
1541
|
+
- ListView rows that are not clickable (Members) carry no role; the
|
|
1542
|
+
members test finds its row by `.sm-table__body > div`. A table role
|
|
1543
|
+
set (`table`/`row`/`cell`) on ListView would give every list a proper
|
|
1544
|
+
locator. *(2026-09-26, browser suite)*
|
|
1545
|
+
- The picker's click cannot be unit-tested (no DOM in the suite); the
|
|
1546
|
+
walk covers it. A jsdom environment for the admin tests is a future
|
|
1547
|
+
tooling item. *(2026-09-15, custom section)*
|
|
1548
|
+
- The list widget has no test for an object item holding a nested list
|
|
1549
|
+
subfield. *(2026-09-07, list field)*
|
|
1550
|
+
- The row menu's item set (`menuItems`) and the row-click routing live in
|
|
1551
|
+
`PagesList.tsx`, untested; extract them into `pages-tree.ts` before
|
|
1552
|
+
plan 4 adds entry rows to the same menu. *(2026-09-06)*
|
|
1553
|
+
- `publish`'s "has no draft to publish" branch is unreachable with the
|
|
1554
|
+
memory store — `createPage` gives every templated page a first version
|
|
1555
|
+
— and is untested. It is defence for the Supabase adapter. *(2026-09-06)*
|
|
1556
|
+
- Classifications the ops tests do not assert: a move that breaks the
|
|
1557
|
+
home page's no-children rule or exceeds the tree depth (create asserts
|
|
1558
|
+
both), and `entries.move`'s cross-collection and sibling-slug guards.
|
|
1559
|
+
`MemoryEntryStore.move` has no children guard at all — the subtree
|
|
1560
|
+
rides along — so `has child` is reachable from delete only. *(2026-09-06)*
|
|
1561
|
+
- No test pins that a published child's breadcrumb carries an UNPUBLISHED
|
|
1562
|
+
ancestor's title with `hasPage: false`; `site.test.tsx` covers the
|
|
1563
|
+
folder ancestor only, and the two paths share one `hasPage` predicate. *(2026-09-06)*
|
|
1564
|
+
- `MemoryPageStore.publish` throws the "is a folder" message when a
|
|
1565
|
+
templated row has no draft version. Unreachable through the ops (a
|
|
1566
|
+
templated page always gets a first version), but the message would be
|
|
1567
|
+
wrong if it ever were. *(2026-09-06)*
|
|
1568
|
+
- `admin-entries-list.test.ts` has no case for a row with differing
|
|
1569
|
+
pointers (the `draft edits` status), nor one asserting that an absent
|
|
1570
|
+
row's `updatedAt` falls back to the node's. Two assertions. *(2026-09-06)*
|
|
1571
|
+
- The live suites run on vitest's DEFAULT 5s per-test timeout against a real
|
|
1572
|
+
database, while a single contract test makes dozens of round trips. On a
|
|
1573
|
+
loaded or degraded local stack `npm run test:supabase` then fails with bare
|
|
1574
|
+
timeouts on arbitrary tests (every file passes on its own, and the whole
|
|
1575
|
+
run passes with `--testTimeout=30000`). Either raise the timeout in
|
|
1576
|
+
`scripts/test-supabase.sh` or accept the flake in CI. *(2026-09-05)*
|
|
1577
|
+
- Entry `move` coverage is thinner than pages' — no omitted-index, clamp or
|
|
1578
|
+
cross-collection tests. *(2026-09-05)*
|
|
1579
|
+
|
|
1580
|
+
### Cloud
|
|
1581
|
+
|
|
1582
|
+
- A copy inside one database with fresh IDs — the cloud's "duplicate
|
|
1583
|
+
environment", staging → production; needs the reference walk (parent
|
|
1584
|
+
links, version pointers, refs, paths, asset ids in trees, fields and
|
|
1585
|
+
richtext) the eject avoids by keeping IDs. *(2026-09-28, export/import)*
|
|
1586
|
+
- Every op call with a junk token costs one `auth.getUser` against the
|
|
1587
|
+
auth project; rate limiting is the session-model spec's (OQ 20). *(2026-09-26, security pass)*
|
|
1588
|
+
- The social image URL goes through the default `next` loader at 1200px;
|
|
1589
|
+
the cloud's transform loader (OQ 28) should serve a real 1200×630. *(2026-09-10, page fields and SEO)*
|
|
1590
|
+
- A cross-origin admin needs the bridge-side editing variant (spec §3). *(2026-09-10, inline editing)*
|
|
1591
|
+
- Transform loaders beyond the Next default: Supabase Image
|
|
1592
|
+
Transformations, imgix, Cloudflare Images (DESIGN.md OQ 28). *(2026-09-08, media library)*
|
|
1593
|
+
- `<SmoodlyImage>` sizes the image by the crop factor, so `sizes` picks a
|
|
1594
|
+
candidate for the box, not for the framed image; a CDN loader with
|
|
1595
|
+
`handlesCrop` removes the overfetch. *(2026-09-08, media library)*
|
|
2095
1596
|
- Light/dark toggle (nav foot): the choice is stored in `localStorage`
|
|
2096
1597
|
(`smoodly-theme`) and written as `data-theme` on the admin wrapper, never on
|
|
2097
1598
|
the host `<html>`. It is per browser, not per editor account — a server-side
|
|
2098
|
-
preference would need a place to live (`members`, or a cloud user profile).
|
|
2099
|
-
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
- `
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
run
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
- Once the policy-based published read exists, decide whether it becomes the canonical headless read path and the generated per-collection views turn optional (cloud has no views).
|
|
2115
|
-
- Add an index on `pages(published_version_id)`: the `page_versions` read policy's `exists` probe runs on every anon/read-key read and has no supporting index.
|
|
2116
|
-
- A malformed `space_id`/`key_id` claim raises `22P02` inside the policy: fails closed but surfaces as a 500. Only reachable from a validly-signed key, so it is a cloud key-minting guard, not a request-path one.
|
|
2117
|
-
- **Verified and kept 2026-09-26 (security pass §3)** — Authorization trusts the JWT claims alone; `smoodly_space_key_revoked()` checks only `revoked_at`. When cloud key-minting exists, make it a full key check (the claims must match the `space_keys` row).
|
|
2118
|
-
- **Verified and kept 2026-09-26 (security pass §3)** — `memberOf` and `SupabaseMemberStore` read `members` (and `spaces`) with no space filter: under a cloud write key RLS scopes it, but a service-role client bypasses RLS and would admit an editor of any space. Harmless in self-host (one space); never use a service-role client against the shared content project.
|
|
2119
|
-
- The `sb_secret_` branch of `clientOptions` (secret key as the sole `apikey`, no bearer) was exercised live on 2026-09-05 against a hosted project: PostgREST reads with the service-role template and the GoTrue admin API (`listUsers`, `createUser`) both accept it; a password sign-in with the matching `sb_publishable_` key returns a session. Unit tests still cover the shape only; the local stack stays on legacy JWT keys.
|
|
2120
|
-
- The migration is not re-appliable to a pre-rename (`project_id`) database: `create table if not exists` leaves it untouched and the `space_id` policies then fail. No installs predate the rename; fold into DESIGN.md OQ #2 (schema upgrades).
|
|
2121
|
-
- `examples/site/smoodly/server.ts` should carry `import "server-only"` now that it also exports `publicAuth` — a client-side import would fail loudly at build instead of at runtime.
|
|
2122
|
-
- `exports` has no `"./package.json"` entry, so tooling that reads a dependency's `package.json` through the exports map cannot. One line to add when something needs it.
|
|
2123
|
-
- `src/ref-index.ts` contains two raw NUL bytes used as key separators, which makes git show the file as binary in diffs. Replacing them with `\0` escapes would make it diffable again.
|
|
2124
|
-
|
|
2125
|
-
## Follow-ups (logged 2026-09-03)
|
|
2126
|
-
|
|
2127
|
-
- Empty locked zones on the site side: `renderPage` renders nothing for a locked zone with no node. A page that predates the zone shows no prefooter on the published site until an editor opens it (the editor fills and saves). Options: render the component from `sample` when the zone is empty, or fill on `createPage`/schema load server-side. Fine for now — seeded and admin-created pages both arrive filled.
|
|
2128
|
-
|
|
2129
|
-
- Editor canvas URL mapping: now `homePageSlug` config (done 2026-09-03); remaining follow-up is a registration-declared `path` for fixed pages at arbitrary paths.
|
|
2130
|
-
- `serialize.ts` structural cast duplicates `PageSchema`; tighten `Registered.schema` so the cast goes.
|
|
2131
|
-
- Catch-all `templates` map should filter to open registrations to state its intent (unreachable today).
|
|
2132
|
-
- 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. — Partly resolved 2026-09-26: the `browser` job builds the example site and drives it, but with a database; the no-database build is still unscripted.
|
|
2133
|
-
- `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 became `forms/useRefOptions.ts` (2026-09-17). (logged 2026-09-06, updated 2026-09-08)
|
|
1599
|
+
preference would need a place to live (`members`, or a cloud user profile). *(2026-09-05)*
|
|
1600
|
+
- Once the policy-based published read exists, decide whether it becomes the canonical headless read path and the generated per-collection views turn optional (cloud has no views). *(2026-09-04)*
|
|
1601
|
+
- A malformed `space_id`/`key_id` claim raises `22P02` inside the policy: fails closed but surfaces as a 500. Only reachable from a validly-signed key, so it is a cloud key-minting guard, not a request-path one. *(2026-09-04)*
|
|
1602
|
+
- **Verified and kept 2026-09-26 (security pass §3)** — Authorization trusts the JWT claims alone; `smoodly_space_key_revoked()` checks only `revoked_at`. When cloud key-minting exists, make it a full key check (the claims must match the `space_keys` row). *(2026-09-04)*
|
|
1603
|
+
- **Verified and kept 2026-09-26 (security pass §3)** — `memberOf` and `SupabaseMemberStore` read `members` (and `spaces`) with no space filter: under a cloud write key RLS scopes it, but a service-role client bypasses RLS and would admit an editor of any space. Harmless in self-host (one space); never use a service-role client against the shared content project. *(2026-09-04)*
|
|
1604
|
+
|
|
1605
|
+
## Notes for maintainers
|
|
1606
|
+
|
|
1607
|
+
- The live suites wipe `members` and the content tables; a pane walk after
|
|
1608
|
+
them needs `npm run reseed` (or `scripts/demo-editor.ts`) first. *(2026-09-26, concurrency)*
|
|
1609
|
+
- Drafting version N+1: `npx tsx scripts/emit-migration.ts` prints the
|
|
1610
|
+
whole schema plus the upsert; keep only the delta, name the file with
|
|
1611
|
+
the current UTC stamp and `_smoodly_v<N+1>.sql`, bump `SCHEMA_VERSION`,
|
|
1612
|
+
run `npm run db:migrate` at the root so the repo copy follows, and
|
|
1613
|
+
`npm run check:migrations`. Additive only; a contraction waits for the
|
|
1614
|
+
compatibility window and carries `-- smoodly:contraction`. *(2026-09-26, schema migrations)*
|