smoodly 0.0.9 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -18
- package/dist/admin/editor/CanvasOverlay.d.ts +21 -0
- package/dist/admin/editor/CanvasOverlay.js +39 -0
- package/dist/admin/editor/EditorView.js +56 -237
- package/dist/admin/editor/InsertPicker.d.ts +18 -0
- package/dist/admin/editor/InsertPicker.js +18 -0
- package/dist/admin/editor/NoticePane.d.ts +4 -0
- package/dist/admin/editor/NoticePane.js +4 -0
- package/dist/admin/editor/Outline.d.ts +18 -0
- package/dist/admin/editor/Outline.js +12 -0
- package/dist/admin/editor/locale-url.d.ts +12 -0
- package/dist/admin/editor/locale-url.js +32 -0
- package/dist/admin/editor/overlay-geometry.d.ts +14 -0
- package/dist/admin/editor/overlay-geometry.js +27 -0
- package/dist/admin/editor/useCanvasBridge.d.ts +15 -0
- package/dist/admin/editor/useCanvasBridge.js +53 -0
- package/dist/admin/editor/usePageLocale.d.ts +23 -0
- package/dist/admin/editor/usePageLocale.js +88 -0
- package/dist/admin/forms/FieldWidget.js +4 -22
- package/dist/admin/forms/RichtextToolbar.d.ts +20 -0
- package/dist/admin/forms/RichtextToolbar.js +37 -0
- package/dist/admin/forms/RichtextWidget.d.ts +5 -0
- package/dist/admin/forms/RichtextWidget.js +123 -0
- package/dist/admin/forms/richtext-extensions.d.ts +5 -0
- package/dist/admin/forms/richtext-extensions.js +74 -0
- package/dist/admin/forms/richtext-paste.d.ts +22 -0
- package/dist/admin/forms/richtext-paste.js +59 -0
- package/dist/admin/index.d.ts +0 -1
- package/dist/admin/index.js +0 -1
- package/dist/admin/next/create-admin.d.ts +3 -7
- package/dist/admin/next/create-admin.js +14 -5
- package/dist/admin/richtext-doc.d.ts +7 -0
- package/dist/admin/richtext-doc.js +18 -0
- package/dist/admin/shell/AdminApp.d.ts +7 -2
- package/dist/admin/shell/AdminApp.js +12 -6
- package/dist/admin/shell/EntriesList.js +3 -2
- package/dist/admin/shell/EntryForm.js +8 -24
- package/dist/admin/shell/ListView.js +3 -1
- package/dist/admin/shell/PagesList.js +3 -2
- package/dist/admin/shell/RouteLink.d.ts +4 -0
- package/dist/admin/shell/RouteLink.js +12 -0
- package/dist/admin/shell/SharedForm.js +3 -21
- package/dist/admin/shell/router.d.ts +37 -0
- package/dist/admin/shell/router.js +84 -0
- package/dist/admin/shell/session.js +3 -1
- package/dist/admin/ui/primitives.d.ts +2 -1
- package/dist/admin/ui/primitives.js +5 -3
- package/dist/admin/ui/theme.d.ts +1 -1
- package/dist/admin/ui/theme.js +27 -1
- package/dist/admin/validate.js +10 -0
- package/dist/collections.d.ts +0 -14
- package/dist/collections.js +0 -10
- package/dist/fields.d.ts +2 -1
- package/dist/fields.js +2 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/toolset.d.ts +30 -0
- package/dist/toolset.js +49 -0
- package/package.json +11 -1
- package/dist/admin/richtext.d.ts +0 -16
- package/dist/admin/richtext.js +0 -98
package/README.md
CHANGED
|
@@ -194,7 +194,10 @@ Implemented:
|
|
|
194
194
|
and `smoodly/bridge` (`EditorBridge`, the only piece that ships to the
|
|
195
195
|
SITE side — runs inside the draft-route iframe, reports node/zone
|
|
196
196
|
geometry, forwards select/hover, executes refresh/scroll). Shell
|
|
197
|
-
(registry-derived nav, list views,
|
|
197
|
+
(registry-derived nav, list views, its own client-side router —
|
|
198
|
+
`shell/router.ts`, `RouteLink`, a leave guard for dirty forms; the
|
|
199
|
+
shell mounts from the layout and the catch-all page only makes the
|
|
200
|
+
URL exist, spec 2026-09-08),
|
|
198
201
|
entry form (widgets by field type, validation errors surfaced inline,
|
|
199
202
|
save/publish/unpublish/delete with the safe-delete guard's message
|
|
200
203
|
shown verbatim), and the editor view (dark instrument around a bright
|
|
@@ -324,18 +327,21 @@ Implemented:
|
|
|
324
327
|
`validateFields` recurses with dotted paths (`points.2.heading`) and
|
|
325
328
|
checks list length; the serializer ships a list's `item`.
|
|
326
329
|
|
|
327
|
-
- Richtext
|
|
328
|
-
|
|
330
|
+
- Richtext, both directions: `RichText` (package root, pure React, no
|
|
331
|
+
wrapper) turns the TipTap document into paragraphs, headings, lists,
|
|
329
332
|
blockquotes, hard breaks and bold/italic/link marks, unknown nodes
|
|
330
|
-
degrading to their children
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
333
|
+
degrading to their children; a link mark reaches the page only with a
|
|
334
|
+
safe scheme (http(s), mailto, tel, site-relative). The admin edits it
|
|
335
|
+
with a TipTap widget under a fixed toolbar (`admin/forms/RichtextWidget.tsx`,
|
|
336
|
+
spec 2026-09-08): the schema is always the full one, and the field's
|
|
337
|
+
toolset — typed names, `DEFAULT_TOOLSET` when a field declares none —
|
|
338
|
+
gates only creation (toolbar, shortcuts, input rules, paste), so content
|
|
339
|
+
a toolset forbids survives every round trip. Links carry `href` only. An
|
|
340
|
+
empty document is missing for a required field. The widget emits plain
|
|
341
|
+
JSON: ProseMirror's node attrs have a null prototype, which a server
|
|
342
|
+
action cannot serialize.
|
|
343
|
+
|
|
344
|
+
Not yet: Elements in richtext, drag-and-drop
|
|
339
345
|
reordering (move-up/move-down buttons in the editor, Move up / Move down
|
|
340
346
|
in the Pages list's row menu), undo/redo, sample/placeholder content
|
|
341
347
|
marking, per-locale section visibility conditions (OQ #3 — each locale
|
|
@@ -356,6 +362,77 @@ Both packages move together. From the repo root:
|
|
|
356
362
|
`examples/site` in that build, so the template always matches the example
|
|
357
363
|
site at the tagged commit.
|
|
358
364
|
|
|
365
|
+
## Follow-ups (logged 2026-09-08, client routing spec)
|
|
366
|
+
|
|
367
|
+
- Keep the previous view painted until the next one has data: today a
|
|
368
|
+
navigation switches instantly and the new view shows its own loading
|
|
369
|
+
state; a transition that holds the old view, or prefetches the next
|
|
370
|
+
view's ops call on hover, is the next step.
|
|
371
|
+
- Browser Back with unsaved changes is not guarded: `popstate` cannot be
|
|
372
|
+
cancelled cleanly, so a dirty form only asks on a click and on reload.
|
|
373
|
+
Re-pushing the current URL and asking is possible if editors lose work.
|
|
374
|
+
- Every revalidating op (publish, delete, a save of published content)
|
|
375
|
+
answers with a fresh RSC render of the admin route — the layout's
|
|
376
|
+
styles included, ~30 KB — because `updateTag` runs inside the server
|
|
377
|
+
action and Next re-renders the tree it was posted to. Harmless now
|
|
378
|
+
that the shell lives in the layout (the empty page subtree is what
|
|
379
|
+
remounts), but it is the price of server actions as the transport.
|
|
380
|
+
- The bundle split: the editor bridge out of the admin tree and the
|
|
381
|
+
editor lazy-loaded, so site pages stop loading the admin chunk (the
|
|
382
|
+
richtext widget register entry below measures it). Its own spec.
|
|
383
|
+
- `SharedForm` and `EntryForm` now share the locale helpers; the rest
|
|
384
|
+
of the fork (`applyResult`, `InfoRow`, the ref-options effect) is still
|
|
385
|
+
the 2026-09-07 entry below.
|
|
386
|
+
|
|
387
|
+
## Follow-ups (logged 2026-09-08, media library spec)
|
|
388
|
+
|
|
389
|
+
- Video transcoding and adaptive streaming: a Mux or Cloudflare Stream
|
|
390
|
+
loader behind the `media.loader` seam, if a site ever needs more than
|
|
391
|
+
an MP4 over Storage's CDN.
|
|
392
|
+
- Richtext images: the `image` node from the Phase 0 sketch, using the
|
|
393
|
+
media picker once it exists.
|
|
394
|
+
- Sweeping abandoned `pending` asset rows (an upload that began and
|
|
395
|
+
never called `finishUpload`); a periodic op or a cleanup on `list`.
|
|
396
|
+
- A private bucket with signed reads, for sites that must not expose
|
|
397
|
+
draft-only media by URL before publish.
|
|
398
|
+
- Pre-generated image variants at upload time, for hosts without sharp.
|
|
399
|
+
- Transform loaders beyond the Next default: Supabase Image
|
|
400
|
+
Transformations, imgix, Cloudflare Images (DESIGN.md OQ 28).
|
|
401
|
+
- The asset refs inherit the index's scope: it reflects the latest saved
|
|
402
|
+
version of a source, so a published version still using an asset the
|
|
403
|
+
draft dropped is not protected from delete — the same gap entry refs
|
|
404
|
+
have today.
|
|
405
|
+
|
|
406
|
+
## Follow-ups (logged 2026-09-08, richtext widget spec)
|
|
407
|
+
|
|
408
|
+
- Drag-to-resize the inspector: `--inspector-w` is one variable (380px
|
|
409
|
+
since the widget); a handle on the column edge writing it, persisted
|
|
410
|
+
per browser like the theme, is the follow-up.
|
|
411
|
+
- A page and entry picker in the link popover: a link inside a richtext
|
|
412
|
+
document is a reference the refs index, the resolver and the localizer
|
|
413
|
+
cannot see (they address refs by top-level key). Its own spec.
|
|
414
|
+
- Elements in richtext: a registered Element as a TipTap node view with a
|
|
415
|
+
small form for its fields, rendered on the site by `RichText` through
|
|
416
|
+
the registry. The next spec.
|
|
417
|
+
- Images in richtext: the Phase 0 sketch's `image` node; waits for media.
|
|
418
|
+
- Escape inside any inspector control bubbles to the editor's window
|
|
419
|
+
handler and clears the block selection — the link popover stops it, a
|
|
420
|
+
textarea or the TipTap content does not. A `keydown` guard on
|
|
421
|
+
`.sm-inspector` would fix it for every control.
|
|
422
|
+
- Every richtext descriptor now carries its toolset, so the serialized
|
|
423
|
+
registry grew by a few bytes per field; if the AI-facing schema wants
|
|
424
|
+
the vocabulary once instead of per field, dedupe there.
|
|
425
|
+
- The entry form's column is narrower than the inspector, so the default
|
|
426
|
+
toolbar wraps to two rows there; the inspector shows it on one.
|
|
427
|
+
- The example site's production bundle loads one shared chunk (~750 KB)
|
|
428
|
+
on every site page holding the package's admin client modules — the
|
|
429
|
+
shell, the Supabase client and now the TipTap widget — because
|
|
430
|
+
`next/page-renderer.tsx` imports the editor bridge from
|
|
431
|
+
`admin/next/bridge.tsx` and Turbopack bundles the package's client
|
|
432
|
+
modules together. Measured on the 2026-09-08 build; whether the shell
|
|
433
|
+
was already there before the widget is unverified. The fix is to give
|
|
434
|
+
the bridge its own entry outside the admin tree and re-measure.
|
|
435
|
+
|
|
359
436
|
## Follow-ups (logged 2026-09-07, starter)
|
|
360
437
|
|
|
361
438
|
- A paired component (`smoodly.section`) accepts only its schema's props, so
|
|
@@ -497,11 +574,10 @@ site at the tagged commit.
|
|
|
497
574
|
a duplicate beside it. Re-materializing such an item (delete the row in
|
|
498
575
|
SQL, reload the admin) moves it onto the derived id.
|
|
499
576
|
- `SharedForm.tsx` is a near-verbatim fork of `EntryForm.tsx`
|
|
500
|
-
(`
|
|
501
|
-
|
|
502
|
-
remembered-locale block (`EntriesList`, `PagesList`); extract
|
|
503
|
-
`
|
|
504
|
-
arrives.
|
|
577
|
+
(`applyResult`, `InfoRow`, the ref-options effect; the locale helpers
|
|
578
|
+
are shared since 2026-09-08) and `SharedList.tsx` is the third copy of
|
|
579
|
+
the remembered-locale block (`EntriesList`, `PagesList`); extract a
|
|
580
|
+
`useFormLocale` hook when the next form arrives.
|
|
505
581
|
- A placement selected in a bound locked zone shows the canvas toolbar's
|
|
506
582
|
"fields only" label though a placement has no page-level fields; the
|
|
507
583
|
inspector notice is the real message.
|
|
@@ -960,4 +1036,4 @@ site at the tagged commit.
|
|
|
960
1036
|
- `serialize.ts` structural cast duplicates `PageSchema`; tighten `Registered.schema` so the cast goes.
|
|
961
1037
|
- Catch-all `templates` map should filter to open registrations to state its intent (unreachable today).
|
|
962
1038
|
- The no-database build is the real proof that routes never prerender against the stores; the unit test only stubs `next/server`. Script it (`SUPABASE_URL=http://127.0.0.1:9 npx next build` in `examples/site`, expect `ƒ /`) and add it to CI, which today runs only the package's tests and typecheck — so a regression is caught when Next changes `connection()` semantics.
|
|
963
|
-
- `EditorView` split
|
|
1039
|
+
- `EditorView` split (autosave extracted 2026-09-03; the rest 2026-09-08, before the TipTap widget): `usePageLocale` (switch/add/remove, over `locale-url.ts`), `useCanvasBridge` (geometry, select/hover, `postToCanvas`, the geometry timeout), `CanvasOverlay` over `overlay-geometry.ts` (`plusButtonsFor`, pure and tested), `InsertPicker`, `Outline`, `NoticePane`. `EditorView.tsx` is ~660 lines of load, gestures, publish and layout. Remaining seam: the inspector's field form (`FieldWidget` rows plus the tabs) stays put until inline editing lands (DESIGN.md §3 puts TipTap in the canvas, not the sidebar); the ref-option loading (`refTargets` + the `ops.entries.list` effect) duplicates `EntryForm`'s and could be one hook. (logged 2026-09-06, updated 2026-09-08)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { TreeNode } from "../../render.tsx";
|
|
2
|
+
import type { PlusButton } from "./overlay-geometry.ts";
|
|
3
|
+
import type { BridgeRect, ZoneRect } from "./protocol.ts";
|
|
4
|
+
export type OverlaySelection = {
|
|
5
|
+
node: TreeNode;
|
|
6
|
+
rect: BridgeRect;
|
|
7
|
+
title: string;
|
|
8
|
+
shared: boolean;
|
|
9
|
+
locked: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function CanvasOverlay({ emptyZones, hoverRect, selection, plusButtons, onPlus, onMove, onDuplicate, onRemove, }: {
|
|
12
|
+
emptyZones: ZoneRect[];
|
|
13
|
+
hoverRect: BridgeRect | undefined;
|
|
14
|
+
selection: OverlaySelection | null;
|
|
15
|
+
plusButtons: PlusButton[];
|
|
16
|
+
/** A "+" was clicked, at these viewport coordinates. */
|
|
17
|
+
onPlus(button: PlusButton, clientX: number, clientY: number): void;
|
|
18
|
+
onMove(id: string, dir: -1 | 1): void;
|
|
19
|
+
onDuplicate(node: TreeNode): void;
|
|
20
|
+
onRemove(id: string): void;
|
|
21
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
// The overlay layer above the canvas iframe — every pixel of chrome is
|
|
4
|
+
// ours: empty-zone placeholders, the hover and selection outlines, the
|
|
5
|
+
// selected block's tag and toolbar, and a "+" per insertable boundary.
|
|
6
|
+
// Positions come from the geometry the bridge reported; the layer
|
|
7
|
+
// itself never touches the iframe.
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
import { IconButton } from "../ui/primitives.js";
|
|
10
|
+
export function CanvasOverlay({ emptyZones, hoverRect, selection, plusButtons, onPlus, onMove, onDuplicate, onRemove, }) {
|
|
11
|
+
const [hoverPlus, setHoverPlus] = useState(null);
|
|
12
|
+
return (_jsxs("div", { style: { position: "absolute", inset: 0, pointerEvents: "none" }, children: [emptyZones.map((z) => (_jsxs("div", { className: "sm-dashed sm-mono", style: {
|
|
13
|
+
position: "absolute", left: z.x + 12, top: z.y, width: Math.max(0, z.w - 24),
|
|
14
|
+
height: Math.max(56, z.h), fontSize: "var(--text-xs)",
|
|
15
|
+
letterSpacing: ".04em", textTransform: "uppercase", color: "var(--gray-500)",
|
|
16
|
+
borderColor: "var(--gray-300)",
|
|
17
|
+
}, children: [z.zone, " \u00B7 empty"] }, `empty-${z.zone}`))), hoverRect && (_jsx("div", { style: {
|
|
18
|
+
position: "absolute", left: hoverRect.x, top: hoverRect.y,
|
|
19
|
+
width: hoverRect.w, height: hoverRect.h,
|
|
20
|
+
outline: "1.5px solid color-mix(in srgb, var(--selection) 45%, transparent)",
|
|
21
|
+
outlineOffset: -1.5, borderRadius: "var(--radius-sm)",
|
|
22
|
+
} })), selection && (_jsxs(_Fragment, { children: [_jsx("div", { style: {
|
|
23
|
+
position: "absolute", left: selection.rect.x, top: selection.rect.y,
|
|
24
|
+
width: selection.rect.w, height: selection.rect.h,
|
|
25
|
+
outline: "var(--outline-selection)", outlineOffset: -1.5,
|
|
26
|
+
borderRadius: "var(--radius-sm)",
|
|
27
|
+
} }), _jsxs("span", { className: "sm-blocktag", style: { position: "absolute", left: selection.rect.x, top: selection.rect.y }, children: [selection.title, selection.shared && " · shared", selection.locked && " · locked"] }), _jsx("div", { className: "sm-blocktoolbar", style: {
|
|
28
|
+
position: "absolute", left: selection.rect.x + selection.rect.w - 8, top: selection.rect.y + 8,
|
|
29
|
+
transform: "translateX(-100%)", pointerEvents: "auto",
|
|
30
|
+
}, children: selection.locked ? (_jsx("span", { className: "sm-mono", style: { fontSize: "var(--text-xs)", color: "#8C94A8", padding: "4px 8px" }, children: "fields only" })) : (_jsxs(_Fragment, { children: [_jsx(IconButton, { label: "Move up", tone: "inverse", onClick: () => onMove(selection.node.id, -1), children: "\u2191" }), _jsx(IconButton, { label: "Move down", tone: "inverse", onClick: () => onMove(selection.node.id, 1), children: "\u2193" }), _jsx(IconButton, { label: "Duplicate", tone: "inverse", onClick: () => onDuplicate(selection.node), children: "\u29C9" }), _jsx(IconButton, { label: "Remove", tone: "inverse", onClick: () => onRemove(selection.node.id), children: "\u00D7" })] })) })] })), plusButtons.map((p) => {
|
|
31
|
+
const key = `${p.zone}-${p.index}`;
|
|
32
|
+
const hot = hoverPlus === key;
|
|
33
|
+
return (_jsxs("div", { style: { position: "absolute", left: 0, right: 0, top: p.y }, children: [hot && _jsx("div", { className: "sm-guide", style: { margin: "0 12px" } }), _jsxs("div", { style: {
|
|
34
|
+
position: "absolute", left: "50%", top: 0, transform: "translate(-50%,-50%)",
|
|
35
|
+
display: "flex", flexDirection: "column", alignItems: "center", gap: 6,
|
|
36
|
+
pointerEvents: "auto",
|
|
37
|
+
}, children: [_jsx("button", { className: "sm-plus", "aria-label": `Add to ${p.zone}`, onMouseEnter: () => setHoverPlus(key), onMouseLeave: () => setHoverPlus((k) => (k === key ? null : k)), onClick: (e) => onPlus(p, e.clientX, e.clientY), children: "+" }), hot && _jsxs("span", { className: "sm-tooltip", children: ["Add to ", p.zone] })] })] }, key));
|
|
38
|
+
})] }));
|
|
39
|
+
}
|