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.
Files changed (61) hide show
  1. package/README.md +94 -18
  2. package/dist/admin/editor/CanvasOverlay.d.ts +21 -0
  3. package/dist/admin/editor/CanvasOverlay.js +39 -0
  4. package/dist/admin/editor/EditorView.js +56 -237
  5. package/dist/admin/editor/InsertPicker.d.ts +18 -0
  6. package/dist/admin/editor/InsertPicker.js +18 -0
  7. package/dist/admin/editor/NoticePane.d.ts +4 -0
  8. package/dist/admin/editor/NoticePane.js +4 -0
  9. package/dist/admin/editor/Outline.d.ts +18 -0
  10. package/dist/admin/editor/Outline.js +12 -0
  11. package/dist/admin/editor/locale-url.d.ts +12 -0
  12. package/dist/admin/editor/locale-url.js +32 -0
  13. package/dist/admin/editor/overlay-geometry.d.ts +14 -0
  14. package/dist/admin/editor/overlay-geometry.js +27 -0
  15. package/dist/admin/editor/useCanvasBridge.d.ts +15 -0
  16. package/dist/admin/editor/useCanvasBridge.js +53 -0
  17. package/dist/admin/editor/usePageLocale.d.ts +23 -0
  18. package/dist/admin/editor/usePageLocale.js +88 -0
  19. package/dist/admin/forms/FieldWidget.js +4 -22
  20. package/dist/admin/forms/RichtextToolbar.d.ts +20 -0
  21. package/dist/admin/forms/RichtextToolbar.js +37 -0
  22. package/dist/admin/forms/RichtextWidget.d.ts +5 -0
  23. package/dist/admin/forms/RichtextWidget.js +123 -0
  24. package/dist/admin/forms/richtext-extensions.d.ts +5 -0
  25. package/dist/admin/forms/richtext-extensions.js +74 -0
  26. package/dist/admin/forms/richtext-paste.d.ts +22 -0
  27. package/dist/admin/forms/richtext-paste.js +59 -0
  28. package/dist/admin/index.d.ts +0 -1
  29. package/dist/admin/index.js +0 -1
  30. package/dist/admin/next/create-admin.d.ts +3 -7
  31. package/dist/admin/next/create-admin.js +14 -5
  32. package/dist/admin/richtext-doc.d.ts +7 -0
  33. package/dist/admin/richtext-doc.js +18 -0
  34. package/dist/admin/shell/AdminApp.d.ts +7 -2
  35. package/dist/admin/shell/AdminApp.js +12 -6
  36. package/dist/admin/shell/EntriesList.js +3 -2
  37. package/dist/admin/shell/EntryForm.js +8 -24
  38. package/dist/admin/shell/ListView.js +3 -1
  39. package/dist/admin/shell/PagesList.js +3 -2
  40. package/dist/admin/shell/RouteLink.d.ts +4 -0
  41. package/dist/admin/shell/RouteLink.js +12 -0
  42. package/dist/admin/shell/SharedForm.js +3 -21
  43. package/dist/admin/shell/router.d.ts +37 -0
  44. package/dist/admin/shell/router.js +84 -0
  45. package/dist/admin/shell/session.js +3 -1
  46. package/dist/admin/ui/primitives.d.ts +2 -1
  47. package/dist/admin/ui/primitives.js +5 -3
  48. package/dist/admin/ui/theme.d.ts +1 -1
  49. package/dist/admin/ui/theme.js +27 -1
  50. package/dist/admin/validate.js +10 -0
  51. package/dist/collections.d.ts +0 -14
  52. package/dist/collections.js +0 -10
  53. package/dist/fields.d.ts +2 -1
  54. package/dist/fields.js +2 -1
  55. package/dist/index.d.ts +4 -2
  56. package/dist/index.js +3 -1
  57. package/dist/toolset.d.ts +30 -0
  58. package/dist/toolset.js +49 -0
  59. package/package.json +11 -1
  60. package/dist/admin/richtext.d.ts +0 -16
  61. 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, segment-based internal routing),
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 renders and round-trips: `RichText` (package root, pure React,
328
- no wrapper) turns the TipTap document into paragraphs, headings, lists,
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. The interim textarea widget speaks a line
331
- syntax (`#` headings, `- ` and `1. ` lists, `> ` quotes, blank line
332
- between paragraphs, hard break between adjacent lines) over the same
333
- JSON, so seeded structure survives an edit.
334
- A link mark reaches the page only with a safe scheme (http(s), mailto,
335
- tel, site-relative).
336
-
337
- Not yet: the TipTap richtext widget (the textarea's line syntax is the
338
- interim editor; `RichText` renders), Elements in richtext, drag-and-drop
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
- (`localeFromUrl`, `writeLocaleToUrl`, `applyResult`, `InfoRow`, the
501
- ref-options effect) and `SharedList.tsx` is the third copy of the
502
- remembered-locale block (`EntriesList`, `PagesList`); extract
503
- `shell/locale-url.ts` and a `useFormLocale` hook when the next form
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, remaining seams (autosave extracted 2026-09-03): the locale lifecycle `localeFromUrl`, `LocaleSwitcher`, `switchLocale`, `doAddLocale`, `doRemoveLocale` is the cleanest extraction available now (self-contained, no canvas geometry); pull the canvas overlay and the insert picker into components when the dnd-kit outline work starts; the sidebar form stays put until inline editing lands (DESIGN.md §3 puts TipTap in the canvas, not the sidebar). (logged 2026-09-06)
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
+ }