smoodly 0.0.15 → 0.0.16
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 +82 -15
- package/dist/admin/auth.d.ts +7 -4
- package/dist/admin/auth.js +1 -1
- package/dist/admin/client-ops.js +3 -3
- package/dist/admin/editor/CanvasOverlay.d.ts +5 -1
- package/dist/admin/editor/CanvasOverlay.js +5 -5
- package/dist/admin/editor/EditorView.d.ts +2 -0
- package/dist/admin/editor/EditorView.js +251 -23
- package/dist/admin/editor/PageSettings.js +3 -2
- package/dist/admin/editor/SharedInspector.d.ts +23 -0
- package/dist/admin/editor/SharedInspector.js +13 -0
- package/dist/admin/editor/shared-edit.d.ts +39 -0
- package/dist/admin/editor/shared-edit.js +124 -0
- package/dist/admin/editor/useSharedEdit.d.ts +6 -0
- package/dist/admin/editor/useSharedEdit.js +18 -0
- package/dist/admin/forms/tabs.d.ts +9 -3
- package/dist/admin/next/op-handler.js +4 -3
- package/dist/admin/ops-impl.d.ts +3 -0
- package/dist/admin/ops-impl.js +198 -23
- package/dist/admin/ops.d.ts +32 -0
- package/dist/admin/serialize.d.ts +0 -1
- package/dist/admin/serialize.js +0 -1
- package/dist/admin/shared-items.js +14 -0
- package/dist/admin/shell/AdminApp.js +1 -1
- package/dist/admin/shell/EntriesList.js +19 -3
- package/dist/admin/shell/EntryForm.js +183 -77
- package/dist/admin/shell/IconRail.js +3 -3
- package/dist/admin/shell/PagesList.js +3 -2
- package/dist/admin/shell/SharedFields.d.ts +21 -0
- package/dist/admin/shell/SharedFields.js +15 -0
- package/dist/admin/shell/SharedForm.js +166 -52
- package/dist/admin/shell/SharedList.js +3 -2
- package/dist/admin/shell/entries-list.d.ts +2 -2
- package/dist/admin/shell/entries-list.js +3 -3
- package/dist/admin/supabase-auth.js +1 -1
- package/dist/admin/ui/ActionMenu.d.ts +11 -0
- package/dist/admin/ui/ActionMenu.js +25 -0
- package/dist/admin/ui/HistoryModal.d.ts +23 -0
- package/dist/admin/ui/HistoryModal.js +40 -0
- package/dist/admin/ui/Modal.d.ts +5 -1
- package/dist/admin/ui/Modal.js +3 -3
- package/dist/admin/ui/PublishControl.d.ts +31 -0
- package/dist/admin/ui/PublishControl.js +24 -0
- package/dist/admin/ui/format.d.ts +4 -0
- package/dist/admin/ui/format.js +9 -0
- package/dist/admin/ui/primitives.d.ts +13 -0
- package/dist/admin/ui/primitives.js +18 -2
- package/dist/admin/ui/status-label.d.ts +10 -0
- package/dist/admin/ui/status-label.js +14 -0
- package/dist/admin/ui/theme.js +46 -6
- package/dist/collections.d.ts +0 -6
- package/dist/collections.js +0 -1
- package/dist/entry-store.d.ts +38 -13
- package/dist/entry-store.js +90 -23
- package/dist/next/page-renderer.js +8 -4
- package/dist/shared.d.ts +8 -1
- package/dist/shared.js +10 -1
- package/dist/site.d.ts +2 -0
- package/dist/site.js +17 -7
- package/dist/sql.js +18 -15
- package/dist/store.d.ts +38 -13
- package/dist/store.js +82 -12
- package/dist/supabase-entry-store.d.ts +11 -6
- package/dist/supabase-entry-store.js +101 -25
- package/dist/supabase-store.d.ts +13 -6
- package/dist/supabase-store.js +88 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,8 +61,10 @@ Implemented:
|
|
|
61
61
|
`pages.tree.depth` capping how deep editors may nest), and expose
|
|
62
62
|
`rename` (a locale's slug/title), `move`, `setTemplate`, `addLocale`,
|
|
63
63
|
`removeLocale`, `resolvePath(locale, path)` and `pathsOf(id)` alongside
|
|
64
|
-
the draft/publish semantics (
|
|
65
|
-
|
|
64
|
+
the draft/publish semantics (one version row per publish plus the live
|
|
65
|
+
draft, rewritten in place; publish moves that locale's pointer and
|
|
66
|
+
freezes the row; discard deletes the abandoned draft; `restore` writes
|
|
67
|
+
an old version as the draft; drafts never leak into published reads). The contract
|
|
66
68
|
is a reusable suite (`test/page-store-contract.ts`); every adapter runs
|
|
67
69
|
the same tests.
|
|
68
70
|
- `SupabasePageStore` — the same id-keyed contract over
|
|
@@ -148,10 +150,11 @@ Implemented:
|
|
|
148
150
|
collection-entry persistence over entries/entry_locales/entry_versions/paths
|
|
149
151
|
— one node, one row per locale, each locale with its own slug, status
|
|
150
152
|
and `.localized()` fields, shared fields on the node, and a per-locale
|
|
151
|
-
version history for
|
|
152
|
-
|
|
153
|
+
version history for every collection (spec 2026-09-14; the 2026-09-06
|
|
154
|
+
opt-in is gone): `create({ locale })`, `update(id, locale, fields)`,
|
|
153
155
|
`setStatus(id, locale, status)`, `addLocale`, `removeLocale`,
|
|
154
|
-
`listVersions
|
|
156
|
+
`listVersions`, `getVersion` and `restore`; the draft row is rewritten
|
|
157
|
+
in place until published (spec 2026-09-13 history); saves rewrite the entry's refs rows over every locale's
|
|
155
158
|
live field set, `referencesTo` answers the reverse
|
|
156
159
|
question, `getMany` is the batched read whose signature is
|
|
157
160
|
`resolveTree`'s `EntryFetcher`, now `(collection, ids, locale, { status })`,
|
|
@@ -426,6 +429,74 @@ after both are on the registry. From the repo root:
|
|
|
426
429
|
`prepublishOnly` rebuilds each package. The Claude Code skill
|
|
427
430
|
`.claude/skills/releasing-smoodly` carries the same order with its checks.
|
|
428
431
|
|
|
432
|
+
## Follow-ups (logged 2026-09-14, collections draft/publish spec)
|
|
433
|
+
|
|
434
|
+
- Publish integrity for an entry's own refs: an entry can publish while
|
|
435
|
+
pointing at an unpublished entry; pages are gated, entries are not.
|
|
436
|
+
- A create dialog (title and slug) before "+ New entry", if empty Untitled
|
|
437
|
+
drafts turn out to litter lists.
|
|
438
|
+
- The page editor's publish banner lists fields as plain text; the forms'
|
|
439
|
+
lines focus the widget. Selecting the node from a `node.field` path is
|
|
440
|
+
the missing piece.
|
|
441
|
+
- Discard on a published row from before versioning that nobody has
|
|
442
|
+
written since is refused as "nothing published" (its pointers are null);
|
|
443
|
+
the control never offers it there, so nothing shows — the store could
|
|
444
|
+
answer it as a no-op instead.
|
|
445
|
+
|
|
446
|
+
## Follow-ups (logged 2026-09-13, history spec)
|
|
447
|
+
|
|
448
|
+
- Existing databases (every install so far is ours; DESIGN.md OQ #2):
|
|
449
|
+
`alter table "page_locales" add column "updated_by" text;`
|
|
450
|
+
`alter table "entry_locales" add column "updated_by" text;`
|
|
451
|
+
`alter table "page_versions" rename column "created_at" to "saved_at";`
|
|
452
|
+
`alter table "page_versions" drop column "created_by";`
|
|
453
|
+
`alter table "entry_versions" rename column "created_at" to "saved_at";`
|
|
454
|
+
`alter table "entry_versions" drop column "created_by";`
|
|
455
|
+
The indexes follow the rename. Autosave rows from before read as
|
|
456
|
+
extra history rows until the page is published again; harmless.
|
|
457
|
+
Since 2026-09-14 every collection is versioned and `collectionSQL` emits one
|
|
458
|
+
view shape: re-apply `collectionSQL(collection)` for each collection on an
|
|
459
|
+
existing install (`supabase db reset` locally), or a published row from
|
|
460
|
+
before versioning reads through the view's live-row fallback until its
|
|
461
|
+
first write gives it a snapshot — either way nothing goes dark.
|
|
462
|
+
- Client-side undo in the editor (a stack of tree snapshots); the store
|
|
463
|
+
is no longer an undo stack by design.
|
|
464
|
+
- The author per version row and in the lists; named versions.
|
|
465
|
+
- History on the canvas card's menu and in the list row menus.
|
|
466
|
+
- A diff between two versions.
|
|
467
|
+
- Ops are built per call (`createAdminOps` inside the handler) so the
|
|
468
|
+
session rides in as `actor`; if that ever measures, memoize the
|
|
469
|
+
actor-free parts.
|
|
470
|
+
|
|
471
|
+
## Follow-ups (logged 2026-09-13, publish flow spec)
|
|
472
|
+
|
|
473
|
+
- Unpublishing a page does not cascade: a published child keeps serving
|
|
474
|
+
under an unpublished parent's path. Decide whether it should.
|
|
475
|
+
- Versioning collections by default — decided 2026-09-14: every
|
|
476
|
+
collection, no flag, the forms autosave (spec
|
|
477
|
+
`docs/superpowers/specs/2026-09-14-collections-draft-publish-design.md`).
|
|
478
|
+
- A draft-mode banner on the site, or an "exit preview" action in the
|
|
479
|
+
admin: the same browser shows drafts of unpublished items after an
|
|
480
|
+
Unpublish, which reads as "it published my draft".
|
|
481
|
+
- The lists' row menus (Pages, Entries) do not offer Unpublish; the
|
|
482
|
+
document's own control does.
|
|
483
|
+
|
|
484
|
+
## Follow-ups (logged 2026-09-13, shared inline editing spec)
|
|
485
|
+
|
|
486
|
+
- Layout items in the outline: a "From the layout" group the bridge could
|
|
487
|
+
populate from the `shared:` ids it reports; canvas-only today.
|
|
488
|
+
- The page autosave loses an edit made within the debounce window before
|
|
489
|
+
navigating away (`useDraftAutosave` only clears timers on unmount). The
|
|
490
|
+
shared controller flushes on unmount; the page's should too.
|
|
491
|
+
- A validation failure on a shared save retries with backoff like a
|
|
492
|
+
transport failure; the field error shows on the widget meanwhile. A
|
|
493
|
+
distinct "fix the field" state would stop the retries.
|
|
494
|
+
- The shared card's usage line lists page ids; titles need a pages
|
|
495
|
+
lookup the editor does not hold (the usage rows carry only ids).
|
|
496
|
+
- A "save and publish" action for sites that want no draft stage on a
|
|
497
|
+
shared item (every item is versioned since 2026-09-13; the flag is
|
|
498
|
+
gone).
|
|
499
|
+
|
|
429
500
|
## Follow-ups (logged 2026-09-11, queries spec)
|
|
430
501
|
|
|
431
502
|
- `where` on localized fields (a join on the locale row), ranges and
|
|
@@ -486,10 +557,6 @@ after both are on the registry. From the repo root:
|
|
|
486
557
|
|
|
487
558
|
## Follow-ups (logged 2026-09-10, inline editing spec)
|
|
488
559
|
|
|
489
|
-
- Shared placements ignore field clicks: their fields live in the shared
|
|
490
|
-
item under Globals, which the page editor cannot patch. The starter's
|
|
491
|
-
Header and Footer carry marks already; inline editing of shared content
|
|
492
|
-
needs the shared form's ops behind the canvas.
|
|
493
560
|
- Richtext instant write from the inspector: the pure `RichText` render
|
|
494
561
|
swapped into the container while the widget is typed in.
|
|
495
562
|
- Item-level chrome in the canvas ("Key points · 3 of 4" on hover, add,
|
|
@@ -890,8 +957,9 @@ the toolbar instead of pushing the content.
|
|
|
890
957
|
- Instance mode: a shared *type* with many editor-created items; today
|
|
891
958
|
the answer is a collection plus a section with `f.ref` (decision 1).
|
|
892
959
|
- Zone-shaped shared items (`smoodly.shared({ zone })`) (decision 2).
|
|
893
|
-
- Standalone live preview on a visual item's admin page —
|
|
894
|
-
preview route in the glue
|
|
960
|
+
- Standalone live preview on a visual item's admin page — would need a
|
|
961
|
+
preview route in the glue. Less pressing since 2026-09-13: every shared
|
|
962
|
+
render is edited in the page canvas, and the Globals page is the form.
|
|
895
963
|
- Built-in meta on shared items and on placements (anchor per placement
|
|
896
964
|
is the likely shape).
|
|
897
965
|
- Per-placement overrides (a shared band with a per-page heading) — out
|
|
@@ -1198,10 +1266,9 @@ the toolbar instead of pushing the content.
|
|
|
1198
1266
|
is generated by `packages/smoodly/scripts/emit-sql.ts` from that
|
|
1199
1267
|
script's OWN fixture collections, not from
|
|
1200
1268
|
`examples/site/smoodly/collections.ts`; the create-smoodly-app template
|
|
1201
|
-
migration IS generated from the example. Since
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
the two should share one source.
|
|
1269
|
+
migration IS generated from the example. Since 2026-09-14 both emit the
|
|
1270
|
+
same view shape for every collection, but the repo-root fixture's field
|
|
1271
|
+
lists still drift from the example's — the two should share one source.
|
|
1205
1272
|
- In the entry form, a shared ref field (e.g. an article's author) shows
|
|
1206
1273
|
`—` in a locale where the target is absent, because the picker offers
|
|
1207
1274
|
only targets present in the edited locale; the value is kept on the
|
package/dist/admin/auth.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { AdminOpCall, OpResult } from "./ops.ts";
|
|
2
|
+
/** The verified editor: the Auth user id and the email the locale rows record as `updated_by` (spec 2026-09-13 history §2). */
|
|
3
|
+
export type AdminSession = {
|
|
4
|
+
userId: string;
|
|
5
|
+
email: string | null;
|
|
6
|
+
};
|
|
2
7
|
export type AdminAuth = {
|
|
3
8
|
/** The user behind a session token, or null when the auth project rejects it. */
|
|
4
|
-
verify(token: string): Promise<
|
|
5
|
-
userId: string;
|
|
6
|
-
} | null>;
|
|
9
|
+
verify(token: string): Promise<AdminSession | null>;
|
|
7
10
|
/** Does `editors` hold this user for the current space? */
|
|
8
11
|
isEditor(userId: string): Promise<boolean>;
|
|
9
12
|
};
|
|
10
|
-
export declare function gateAdminOp(auth: AdminAuth, call: AdminOpCall, run: (call: AdminOpCall) => Promise<OpResult<unknown>>): Promise<OpResult<unknown>>;
|
|
13
|
+
export declare function gateAdminOp(auth: AdminAuth, call: AdminOpCall, run: (call: AdminOpCall, session: AdminSession) => Promise<OpResult<unknown>>): Promise<OpResult<unknown>>;
|
package/dist/admin/auth.js
CHANGED
package/dist/admin/client-ops.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const GROUPS = {
|
|
2
|
-
pages: ["list", "get", "create", "saveDraft", "publish", "delete", "rename", "move", "setTemplate", "addLocale", "removeLocale"],
|
|
3
|
-
entries: ["list", "get", "create", "save", "setStatus", "delete", "move", "addLocale", "removeLocale"],
|
|
4
|
-
shared: ["list", "get", "save", "setStatus"],
|
|
2
|
+
pages: ["list", "get", "create", "saveDraft", "publish", "unpublish", "discardDraft", "delete", "rename", "move", "setTemplate", "addLocale", "removeLocale", "listVersions", "restore"],
|
|
3
|
+
entries: ["list", "get", "create", "save", "setStatus", "discardDraft", "delete", "move", "addLocale", "removeLocale", "listVersions", "getVersion", "restore"],
|
|
4
|
+
shared: ["list", "get", "save", "setStatus", "discardDraft", "listVersions", "getVersion", "restore"],
|
|
5
5
|
assets: ["list", "get", "beginUpload", "finishUpload", "update", "delete", "usage"],
|
|
6
6
|
preview: ["enable", "disable"],
|
|
7
7
|
};
|
|
@@ -7,10 +7,14 @@ export type OverlaySelection = {
|
|
|
7
7
|
title: string;
|
|
8
8
|
shared: boolean;
|
|
9
9
|
locked: boolean;
|
|
10
|
+
/** Rendered by the site's layout, not placed in a zone: no tree gestures (spec 2026-09-13 §2). */
|
|
11
|
+
layout?: boolean;
|
|
10
12
|
};
|
|
11
|
-
export declare function CanvasOverlay({ emptyZones, hoverRect, selection, plusButtons, fieldHover, editing, onPlus, onMove, onDuplicate, onRemove, }: {
|
|
13
|
+
export declare function CanvasOverlay({ emptyZones, hoverRect, hoverShared, selection, plusButtons, fieldHover, editing, onPlus, onMove, onDuplicate, onRemove, }: {
|
|
12
14
|
emptyZones: ZoneRect[];
|
|
13
15
|
hoverRect: BridgeRect | undefined;
|
|
16
|
+
/** The hovered block is a shared item: the ring takes the shared colour. */
|
|
17
|
+
hoverShared?: boolean;
|
|
14
18
|
selection: OverlaySelection | null;
|
|
15
19
|
plusButtons: PlusButton[];
|
|
16
20
|
/** The marked field under the pointer, inside the selected section (spec 2026-09-10 §2). */
|
|
@@ -7,7 +7,7 @@ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-run
|
|
|
7
7
|
// itself never touches the iframe.
|
|
8
8
|
import { useState } from "react";
|
|
9
9
|
import { IconButton } from "../ui/primitives.js";
|
|
10
|
-
export function CanvasOverlay({ emptyZones, hoverRect, selection, plusButtons, fieldHover, editing, onPlus, onMove, onDuplicate, onRemove, }) {
|
|
10
|
+
export function CanvasOverlay({ emptyZones, hoverRect, hoverShared, selection, plusButtons, fieldHover, editing, onPlus, onMove, onDuplicate, onRemove, }) {
|
|
11
11
|
const [hoverPlus, setHoverPlus] = useState(null);
|
|
12
12
|
return (_jsxs("div", { style: { position: "absolute", inset: 0, pointerEvents: "none" }, children: [emptyZones.map((z) => (_jsxs("div", { className: "sm-dashed", style: {
|
|
13
13
|
position: "absolute", left: z.x + 12, top: z.y, width: Math.max(0, z.w - 24),
|
|
@@ -15,17 +15,17 @@ export function CanvasOverlay({ emptyZones, hoverRect, selection, plusButtons, f
|
|
|
15
15
|
}, children: [z.zone, " \u00B7 empty"] }, `empty-${z.zone}`))), hoverRect && (_jsx("div", { style: {
|
|
16
16
|
position: "absolute", left: hoverRect.x, top: hoverRect.y,
|
|
17
17
|
width: hoverRect.w, height: hoverRect.h,
|
|
18
|
-
outline:
|
|
18
|
+
outline: `1.5px solid color-mix(in srgb, var(${hoverShared ? "--selection-shared" : "--selection"}) 45%, transparent)`,
|
|
19
19
|
outlineOffset: -1.5, borderRadius: "var(--selection-radius)",
|
|
20
20
|
} })), fieldHover && !(editing && editing.node === fieldHover.node && editing.path === fieldHover.path) && (_jsx("div", { className: "sm-fieldhover", style: { position: "absolute", left: fieldHover.x, top: fieldHover.y, width: fieldHover.w, height: fieldHover.h } })), editing && (_jsx("div", { className: "sm-fieldedit", style: { position: "absolute", left: editing.x, top: editing.y, width: editing.w, height: editing.h } })), selection && (_jsxs(_Fragment, { children: [_jsx("div", { style: {
|
|
21
21
|
position: "absolute", left: selection.rect.x, top: selection.rect.y,
|
|
22
22
|
width: selection.rect.w, height: selection.rect.h,
|
|
23
|
-
outline: "var(--outline-selection)", outlineOffset: -1.5,
|
|
23
|
+
outline: selection.shared ? "var(--outline-selection-shared)" : "var(--outline-selection)", outlineOffset: -1.5,
|
|
24
24
|
borderRadius: "var(--selection-radius)",
|
|
25
|
-
} }), _jsxs("span", { className: "sm-blocktag", style: { position: "absolute", left: selection.rect.x, top: selection.rect.y }, children: [selection.title, selection.shared && " · shared", selection.locked && " · locked"] }), _jsx("div", { className: "sm-blocktoolbar", style: {
|
|
25
|
+
} }), _jsxs("span", { className: selection.shared ? "sm-blocktag sm-blocktag--shared" : "sm-blocktag", style: { position: "absolute", left: selection.rect.x, top: selection.rect.y }, children: [selection.title, selection.shared && " · shared", selection.locked && " · locked"] }), !selection.layout && (_jsx("div", { className: "sm-blocktoolbar", style: {
|
|
26
26
|
position: "absolute", left: selection.rect.x + selection.rect.w - 8, top: selection.rect.y + 8,
|
|
27
27
|
transform: "translateX(-100%)", pointerEvents: "auto",
|
|
28
|
-
}, children: selection.locked ? (_jsx("span", { className: "sm-blocktoolbar__note", children: "fields only" })) : (_jsxs(_Fragment, { children: [_jsx(IconButton, { label: "Move up", tone: "inverse", onClick: () => onMove(selection.node.id, -1), children: "\u2191" }), _jsx(IconButton, { label: "Move down", tone: "inverse", onClick: () => onMove(selection.node.id, 1), children: "\u2193" }), _jsx(IconButton, { label: "Duplicate", tone: "inverse", onClick: () => onDuplicate(selection.node), children: "\u29C9" }), _jsx(IconButton, { label: "Remove", tone: "inverse", onClick: () => onRemove(selection.node.id), children: "\u00D7" })] })) })] })), plusButtons.map((p) => {
|
|
28
|
+
}, children: selection.locked ? (_jsx("span", { className: "sm-blocktoolbar__note", children: "fields only" })) : (_jsxs(_Fragment, { children: [_jsx(IconButton, { label: "Move up", tone: "inverse", onClick: () => onMove(selection.node.id, -1), children: "\u2191" }), _jsx(IconButton, { label: "Move down", tone: "inverse", onClick: () => onMove(selection.node.id, 1), children: "\u2193" }), _jsx(IconButton, { label: "Duplicate", tone: "inverse", onClick: () => onDuplicate(selection.node), children: "\u29C9" }), _jsx(IconButton, { label: "Remove", tone: "inverse", onClick: () => onRemove(selection.node.id), children: "\u00D7" })] })) }))] })), plusButtons.map((p) => {
|
|
29
29
|
const key = `${p.zone}-${p.index}`;
|
|
30
30
|
const hot = hoverPlus === key;
|
|
31
31
|
return (_jsxs("div", { style: { position: "absolute", left: 0, right: 0, top: p.y }, children: [hot && _jsx("div", { className: "sm-guide", style: { margin: "0 12px" } }), _jsx("div", { style: {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { AdminOps } from "../ops.ts";
|
|
2
2
|
import type { AdminRegistry } from "../serialize.ts";
|
|
3
3
|
export declare const withEditorParam: (url: string) => string;
|
|
4
|
+
/** The site URL of a stored version — History's iframe (spec 2026-09-13 history §5); no editor parameter, so no bridge. */
|
|
5
|
+
export declare const withVersionParam: (url: string, versionId: string) => string;
|
|
4
6
|
export declare function EditorView({ ops, registry, id }: {
|
|
5
7
|
ops: AdminOps;
|
|
6
8
|
registry: AdminRegistry;
|