smoodly 0.0.45 → 0.0.47
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 +3 -3
- package/dist/addresses.d.ts +6 -7
- package/dist/addresses.js +6 -4
- package/dist/admin/PublicSans-LICENSE.txt +93 -0
- package/dist/admin/auth-config.d.ts +6 -0
- package/dist/admin/auth-config.js +1 -0
- package/dist/admin/browser/fetch-op.d.ts +10 -0
- package/dist/admin/browser/fetch-op.js +44 -0
- package/dist/admin/browser/main.d.ts +1 -0
- package/dist/admin/browser/main.js +21 -0
- package/dist/admin/custom-section.d.ts +1 -1
- package/dist/admin/drift.d.ts +1 -1
- package/dist/admin/editor/EditorView.js +14 -14
- package/dist/admin/editor/autosave.js +1 -1
- package/dist/admin/editor/canvas-address.d.ts +2 -0
- package/dist/admin/editor/canvas-address.js +8 -0
- package/dist/admin/editor/overlay-geometry.d.ts +1 -1
- package/dist/admin/editor/protocol.d.ts +18 -4
- package/dist/admin/editor/protocol.js +15 -1
- package/dist/admin/editor/useCanvasBridge.d.ts +1 -1
- package/dist/admin/editor/useCanvasBridge.js +5 -6
- package/dist/admin/editor/useInlineSession.js +4 -2
- package/dist/admin/files.d.ts +11 -0
- package/dist/admin/files.generated.js +2 -0
- package/dist/admin/files.js +8 -0
- package/dist/admin/forms/useRefOptions.d.ts +2 -2
- package/dist/admin/forms/useRefOptions.js +2 -2
- package/dist/admin/forms/useRichtextEditor.js +4 -4
- package/dist/admin/handler.d.ts +40 -0
- package/dist/admin/handler.js +151 -0
- package/dist/admin/health.d.ts +4 -0
- package/dist/admin/index.d.ts +1 -1
- package/dist/admin/ops-impl.d.ts +7 -1
- package/dist/admin/ops-impl.js +112 -35
- package/dist/admin/ops.d.ts +3 -2
- package/dist/admin/page-data.d.ts +21 -0
- package/dist/admin/page-data.js +4 -0
- package/dist/admin/page.d.ts +6 -0
- package/dist/admin/page.js +35 -0
- package/dist/admin/serialize.js +7 -7
- package/dist/admin/shell/AdminApp.js +2 -2
- package/dist/admin/shell/ContentHealthPage.js +3 -1
- package/dist/admin/shell/EntryForm.js +1 -1
- package/dist/admin/shell/router.d.ts +3 -16
- package/dist/admin/shell/router.js +3 -39
- package/dist/admin/shell/session.d.ts +2 -5
- package/dist/admin/supabase-auth.js +1 -1
- package/dist/admin/tree-ops.d.ts +1 -1
- package/dist/admin/ui/fonts.d.ts +12 -0
- package/dist/admin/ui/fonts.js +22 -0
- package/dist/admin/ui/theme.d.ts +13 -4
- package/dist/admin/ui/theme.js +28 -157
- package/dist/assemble.d.ts +60 -0
- package/dist/assemble.js +359 -0
- package/dist/astro/cache.d.ts +34 -0
- package/dist/astro/cache.js +69 -0
- package/dist/astro/components/Custom.astro +21 -0
- package/dist/astro/components/Guarded.astro +24 -0
- package/dist/astro/components/Head.astro +26 -0
- package/dist/astro/components/Image.astro +20 -0
- package/dist/astro/components/RichText.astro +23 -0
- package/dist/astro/components/Section.astro +20 -0
- package/dist/astro/components/Video.astro +5 -0
- package/dist/astro/components/Zone.astro +19 -0
- package/dist/astro/components.d.ts +6 -0
- package/dist/astro/components.js +7 -0
- package/dist/astro/content-loaders.d.ts +40 -0
- package/dist/astro/content-loaders.js +100 -0
- package/dist/astro/content.d.ts +9 -0
- package/dist/astro/content.js +17 -0
- package/dist/astro/custom.d.ts +1 -0
- package/dist/astro/custom.js +3 -0
- package/dist/astro/defaults.d.ts +20 -0
- package/dist/astro/defaults.js +21 -0
- package/dist/astro/env-files.d.ts +1 -0
- package/dist/astro/env-files.js +21 -0
- package/dist/astro/head.d.ts +13 -0
- package/dist/astro/head.js +19 -0
- package/dist/astro/image.d.ts +27 -0
- package/dist/astro/image.js +29 -0
- package/dist/astro/index.d.ts +43 -0
- package/dist/astro/index.js +111 -0
- package/dist/astro/locale-paths.d.ts +30 -0
- package/dist/astro/locale-paths.js +38 -0
- package/dist/astro/middleware.d.ts +1 -0
- package/dist/astro/middleware.js +83 -0
- package/dist/astro/preview-document.d.ts +1 -0
- package/dist/astro/preview-document.js +18 -0
- package/dist/astro/preview.d.ts +10 -0
- package/dist/astro/preview.js +72 -0
- package/dist/astro/richtext.d.ts +15 -0
- package/dist/astro/richtext.js +78 -0
- package/dist/astro/routes/admin.d.ts +3 -0
- package/dist/astro/routes/admin.js +41 -0
- package/dist/astro/routes/page.astro +17 -0
- package/dist/astro/routes/preview.astro +11 -0
- package/dist/astro/runtime.d.ts +32 -0
- package/dist/astro/runtime.js +46 -0
- package/dist/astro/scope.d.ts +45 -0
- package/dist/astro/scope.js +103 -0
- package/dist/astro/server.d.ts +14 -0
- package/dist/astro/server.js +45 -0
- package/dist/astro/shell.d.ts +67 -0
- package/dist/astro/shell.js +63 -0
- package/dist/astro/style.d.ts +1 -0
- package/dist/astro/style.js +11 -0
- package/dist/astro/views-check.d.ts +3 -0
- package/dist/astro/views-check.js +19 -0
- package/dist/astro/zone.d.ts +39 -0
- package/dist/astro/zone.js +33 -0
- package/dist/bind.d.ts +19 -0
- package/dist/bind.js +34 -0
- package/dist/bridge/bridge.d.ts +9 -0
- package/dist/bridge/bridge.js +107 -0
- package/dist/bridge/entry.d.ts +1 -0
- package/dist/bridge/entry.js +5 -0
- package/dist/bridge/preview-pill.d.ts +18 -0
- package/dist/bridge/preview-pill.js +115 -0
- package/dist/cli/import.d.ts +3 -0
- package/dist/cli/import.js +10 -1
- package/dist/cli/load-collections.js +4 -3
- package/dist/cli/load-config.d.ts +4 -0
- package/dist/cli/load-config.js +26 -0
- package/dist/cli/main.d.ts +5 -1
- package/dist/cli/main.js +24 -6
- package/dist/cli/migrate.d.ts +13 -0
- package/dist/cli/migrate.js +30 -2
- package/dist/cli/rebuild.d.ts +23 -0
- package/dist/cli/rebuild.js +22 -0
- package/dist/cli/space-io.d.ts +15 -1
- package/dist/cli/space-io.js +14 -0
- package/dist/client.js +5 -0
- package/dist/config.d.ts +28 -20
- package/dist/config.js +71 -75
- package/dist/content/content-api.d.ts +1 -1
- package/dist/content/node.d.ts +2 -3
- package/dist/content/node.js +2 -1
- package/dist/crash-message.d.ts +1 -0
- package/dist/crash-message.js +8 -0
- package/dist/css.d.ts +4 -0
- package/dist/css.js +5 -2
- package/dist/custom/CustomSection.d.ts +1 -1
- package/dist/custom/CustomSection.js +4 -8
- package/dist/custom/css.js +2 -2
- package/dist/custom/index.d.ts +6 -10
- package/dist/custom/index.js +1 -2
- package/dist/custom/layout.d.ts +5 -0
- package/dist/custom/layout.js +14 -0
- package/dist/document-stamp.d.ts +7 -0
- package/dist/document-stamp.js +48 -0
- package/dist/entry-page.d.ts +3 -18
- package/dist/env.js +1 -1
- package/dist/global-id.js +2 -2
- package/dist/global.d.ts +5 -2
- package/dist/global.js +8 -5
- package/dist/image/next-config.d.ts +3 -0
- package/dist/image/next-config.js +12 -7
- package/dist/image/remote-pattern.d.ts +1 -0
- package/dist/image/remote-pattern.js +5 -0
- package/dist/index.d.ts +15 -17
- package/dist/index.js +10 -19
- package/dist/marks.d.ts +7 -0
- package/dist/marks.js +7 -0
- package/dist/next/admin-route.d.ts +7 -0
- package/dist/next/admin-route.js +28 -0
- package/dist/next/bridge.d.ts +1 -0
- package/dist/next/bridge.js +26 -0
- package/dist/next/canvas-boundary.d.ts +1 -1
- package/dist/next/canvas-slot.d.ts +1 -0
- package/dist/next/canvas-slot.js +4 -3
- package/dist/next/index.d.ts +2 -0
- package/dist/next/index.js +1 -0
- package/dist/next/page-renderer.d.ts +5 -1
- package/dist/next/page-renderer.js +19 -33
- package/dist/next/preview-pill-define.d.ts +1 -0
- package/dist/next/preview-pill-define.js +10 -0
- package/dist/next/preview-pill.d.ts +0 -3
- package/dist/next/preview-pill.js +7 -50
- package/dist/next/queries.d.ts +3 -28
- package/dist/next/queries.js +3 -90
- package/dist/next/reads.d.ts +2 -0
- package/dist/next/reads.js +18 -0
- package/dist/page-context.d.ts +89 -0
- package/dist/page-context.js +8 -0
- package/dist/pairing.d.ts +2 -22
- package/dist/project.d.ts +85 -0
- package/dist/project.js +228 -0
- package/dist/published.d.ts +115 -0
- package/dist/published.js +136 -0
- package/dist/react.d.ts +61 -0
- package/dist/react.js +17 -0
- package/dist/read-shapes.d.ts +1 -1
- package/dist/reads.d.ts +80 -0
- package/dist/reads.js +142 -0
- package/dist/refs.d.ts +1 -1
- package/dist/registration.d.ts +8 -0
- package/dist/registration.js +17 -0
- package/dist/render.d.ts +5 -83
- package/dist/render.js +5 -10
- package/dist/resolve.d.ts +1 -1
- package/dist/richtext/index.d.ts +6 -2
- package/dist/richtext/index.js +3 -2
- package/dist/richtext-href.d.ts +4 -0
- package/dist/richtext-href.js +17 -0
- package/dist/richtext-render.js +1 -14
- package/dist/schema-version.d.ts +1 -1
- package/dist/schema-version.js +1 -1
- package/dist/section-crash.d.ts +2 -2
- package/dist/section-crash.js +2 -7
- package/dist/section-wrapper.d.ts +3 -10
- package/dist/section-wrapper.js +3 -2
- package/dist/site.d.ts +40 -14
- package/dist/site.js +199 -284
- package/dist/smd-key.d.ts +9 -0
- package/dist/smd-key.js +1 -0
- package/dist/sql-space.d.ts +5 -0
- package/dist/sql-space.js +11 -5
- package/dist/sql.d.ts +2 -0
- package/dist/sql.js +127 -1
- package/dist/store.d.ts +1 -1
- package/dist/supabase-published-store.d.ts +45 -0
- package/dist/supabase-published-store.js +247 -0
- package/dist/supabase-store.d.ts +1 -1
- package/dist/toolset.d.ts +2 -4
- package/dist/toolset.js +6 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/view-props.d.ts +40 -0
- package/dist/view-props.js +1 -0
- package/dist/views.d.ts +36 -0
- package/dist/views.js +52 -0
- package/dist/zones.d.ts +7 -7
- package/dist/zones.js +7 -6
- package/migrations/20261007120000_smoodly_v8.sql +154 -0
- package/migrations/20261007200000_smoodly_v9.sql +30 -0
- package/migrations/20261008120000_smoodly_v10.sql +47 -0
- package/package.json +46 -19
- package/dist/admin/next/bridge.d.ts +0 -2
- package/dist/admin/next/bridge.js +0 -129
- package/dist/admin/next/create-admin.d.ts +0 -19
- package/dist/admin/next/create-admin.js +0 -44
- package/dist/admin/next/index.d.ts +0 -8
- package/dist/admin/next/index.js +0 -8
- package/dist/admin/next/op-handler.d.ts +0 -18
- package/dist/admin/next/op-handler.js +0 -31
package/dist/views.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function match(what, key, schemas, views) {
|
|
2
|
+
const registered = new Map(schemas.map((s) => [s.name, s]));
|
|
3
|
+
const viewed = new Set();
|
|
4
|
+
for (const v of views) {
|
|
5
|
+
const schema = registered.get(v.schema.name);
|
|
6
|
+
if (!schema)
|
|
7
|
+
throw new Error(`smoodly: the view for ${what} "${v.schema.name}" has no schema in the config — add its schema to registry.${key}.`);
|
|
8
|
+
if (schema !== v.schema) {
|
|
9
|
+
throw new Error(`smoodly: the view for ${what} "${v.schema.name}" is paired with a different schema object than the one in the config — pair it with the registered schema.`);
|
|
10
|
+
}
|
|
11
|
+
viewed.add(v.schema.name);
|
|
12
|
+
}
|
|
13
|
+
for (const s of schemas) {
|
|
14
|
+
if (!viewed.has(s.name))
|
|
15
|
+
throw new Error(`smoodly: ${what} "${s.name}" is registered in the config but has no view — add its component to defineViews({ ${key} }).`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function defineViews(config, views) {
|
|
19
|
+
const elements = views.elements ?? [];
|
|
20
|
+
match("section", "sections", config.registry.sections, views.sections);
|
|
21
|
+
match("element", "elements", config.registry.elements, elements);
|
|
22
|
+
match("page type", "pages", config.registry.pages, views.pages.filter((p) => p.schema.kind === "page"));
|
|
23
|
+
const collections = new Map(config.registry.collections.map((c) => [c.name, c]));
|
|
24
|
+
const rendered = new Set();
|
|
25
|
+
for (const p of views.pages) {
|
|
26
|
+
if (p.schema.kind !== "entryPage")
|
|
27
|
+
continue;
|
|
28
|
+
const name = p.schema.collection;
|
|
29
|
+
if (!collections.has(name))
|
|
30
|
+
throw new Error(`smoodly: an entry page renders collection "${name}", which is not in registry.collections.`);
|
|
31
|
+
if (rendered.has(name))
|
|
32
|
+
throw new Error(`smoodly: two entry pages render collection "${name}" — keep one in defineViews({ pages }).`);
|
|
33
|
+
rendered.add(name);
|
|
34
|
+
}
|
|
35
|
+
for (const c of config.registry.collections) {
|
|
36
|
+
if (c.path !== undefined && !rendered.has(c.name)) {
|
|
37
|
+
throw new Error(`smoodly: collection "${c.name}" declares a path but no entry page — add smoodly.entryPage(${c.name}, View) to defineViews({ pages }).`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// A bound RichText (createRichText) carries the element components it renders. One it lacks would
|
|
41
|
+
// vanish from every body without a word, so it is named here (final review of the registration split).
|
|
42
|
+
const bound = views.RichText?.elements;
|
|
43
|
+
if (Array.isArray(bound)) {
|
|
44
|
+
const rendered = new Set(bound.map((e) => e?.schema?.name));
|
|
45
|
+
for (const e of config.registry.elements) {
|
|
46
|
+
if ((e.contexts ?? []).includes("richtext") && !rendered.has(e.name)) {
|
|
47
|
+
throw new Error(`smoodly: element "${e.name}" has a view in defineViews({ elements }), but the bound RichText does not render it — add its component to createRichText({ elements }) in smoodly/richtext.tsx.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { sections: views.sections, elements, pages: views.pages, ...(views.RichText ? { RichText: views.RichText } : {}) };
|
|
52
|
+
}
|
package/dist/zones.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { GlobalSchema } from "./global.ts";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
/** A section schema as a zone needs it. `kind` keeps a component out at compile time: a function has a `name` too. */
|
|
3
|
+
type Named = {
|
|
4
|
+
kind: string;
|
|
5
|
+
name: string;
|
|
6
6
|
};
|
|
7
7
|
export type ZonePolicy = {
|
|
8
8
|
kind: "sections";
|
|
@@ -22,16 +22,16 @@ export type ZonePolicy = {
|
|
|
22
22
|
global?: string;
|
|
23
23
|
};
|
|
24
24
|
export declare const z: {
|
|
25
|
-
sections(
|
|
25
|
+
sections(schemas: Named[]): {
|
|
26
26
|
kind: "sections";
|
|
27
27
|
allow: string[];
|
|
28
28
|
} & {
|
|
29
29
|
max(n: number): ZonePolicy;
|
|
30
30
|
};
|
|
31
31
|
freeform(opts: {
|
|
32
|
-
sections:
|
|
32
|
+
sections: Named[] | "*";
|
|
33
33
|
custom?: boolean;
|
|
34
34
|
}): ZonePolicy;
|
|
35
|
-
locked(target:
|
|
35
|
+
locked(target: Named | GlobalSchema): ZonePolicy;
|
|
36
36
|
};
|
|
37
37
|
export {};
|
package/dist/zones.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
// Zone policies — the freedom dial. Policies reference
|
|
2
|
-
//
|
|
1
|
+
// Zone policies — the freedom dial. Policies reference schemas directly;
|
|
2
|
+
// only their registered names are stored (plain data).
|
|
3
|
+
import { schemaOnly } from "./registration.js";
|
|
3
4
|
const isGlobal = (target) => "kind" in target && target.kind === "global";
|
|
4
|
-
const names = (
|
|
5
|
+
const names = (schemas) => schemas.map((s) => schemaOnly(s).name);
|
|
5
6
|
export const z = {
|
|
6
|
-
sections(
|
|
7
|
-
const allow = names(
|
|
7
|
+
sections(schemas) {
|
|
8
|
+
const allow = names(schemas);
|
|
8
9
|
return {
|
|
9
10
|
kind: "sections",
|
|
10
11
|
allow,
|
|
@@ -24,6 +25,6 @@ export const z = {
|
|
|
24
25
|
throw new Error(`smoodly: global item "${target.name}" has no section — a locked zone needs a visual item.`);
|
|
25
26
|
return { kind: "locked", component: target.section, global: target.name };
|
|
26
27
|
}
|
|
27
|
-
return { kind: "locked", component: target.
|
|
28
|
+
return { kind: "locked", component: schemaOnly(target).name };
|
|
28
29
|
},
|
|
29
30
|
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
-- Smoodly core schema, version 8 (docs/superpowers/specs/2026-10-07-delivery-model-design.md).
|
|
2
|
+
-- Shipped in the smoodly package; `npx smoodly migrate` copies it into supabase/migrations/.
|
|
3
|
+
-- Never edit a published file: the next change is the next file.
|
|
4
|
+
-- The delta over version 7: the delivery model's two derived tables,
|
|
5
|
+
-- published_pages and published_entries, with the space trigger and the
|
|
6
|
+
-- write-key-only policies every content table has (no row cap: they are
|
|
7
|
+
-- derived), smoodly_list_published over the raw published snapshot, and
|
|
8
|
+
-- the published_routes view, one lookup for any address.
|
|
9
|
+
|
|
10
|
+
set client_min_messages = warning;
|
|
11
|
+
|
|
12
|
+
-- The delivery model (spec 2026-10-07): Publish writes the finished document of a page, an entry
|
|
13
|
+
-- or a global here; a live read is one lookup. Derived and rebuildable from the editing tables.
|
|
14
|
+
create table if not exists "published_pages" (
|
|
15
|
+
"space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
|
|
16
|
+
"page_id" uuid not null references "pages" ("id") on delete cascade,
|
|
17
|
+
"locale" text not null,
|
|
18
|
+
"parent_id" uuid,
|
|
19
|
+
"sort" numeric,
|
|
20
|
+
"path" text,
|
|
21
|
+
"title" text not null,
|
|
22
|
+
"has_page" boolean not null,
|
|
23
|
+
"link_fields" jsonb not null default '{}'::jsonb,
|
|
24
|
+
"document" jsonb,
|
|
25
|
+
"tags" text[] not null default '{}',
|
|
26
|
+
"model" text not null,
|
|
27
|
+
"stale" boolean not null default false,
|
|
28
|
+
"published_at" timestamptz not null default now(),
|
|
29
|
+
primary key ("page_id", "locale")
|
|
30
|
+
);
|
|
31
|
+
create unique index if not exists "published_pages_address_idx"
|
|
32
|
+
on "published_pages" ("space_id", "locale", "path") where "path" is not null;
|
|
33
|
+
create index if not exists "published_pages_tree_idx"
|
|
34
|
+
on "published_pages" ("space_id", "locale", "parent_id", "sort");
|
|
35
|
+
|
|
36
|
+
create table if not exists "published_entries" (
|
|
37
|
+
"space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
|
|
38
|
+
"entry_id" uuid not null references "entries" ("id") on delete cascade,
|
|
39
|
+
"locale" text not null,
|
|
40
|
+
"collection" text not null,
|
|
41
|
+
"parent_id" uuid,
|
|
42
|
+
"sort" numeric,
|
|
43
|
+
"slug" text,
|
|
44
|
+
"path" text,
|
|
45
|
+
"created_at" timestamptz not null,
|
|
46
|
+
"updated_at" timestamptz not null,
|
|
47
|
+
"fields" jsonb not null,
|
|
48
|
+
"document" jsonb not null,
|
|
49
|
+
"tags" text[] not null default '{}',
|
|
50
|
+
"model" text not null,
|
|
51
|
+
"stale" boolean not null default false,
|
|
52
|
+
"published_at" timestamptz not null default now(),
|
|
53
|
+
primary key ("entry_id", "locale")
|
|
54
|
+
);
|
|
55
|
+
create index if not exists "published_entries_list_idx"
|
|
56
|
+
on "published_entries" ("space_id", "collection", "locale");
|
|
57
|
+
create unique index if not exists "published_entries_address_idx"
|
|
58
|
+
on "published_entries" ("space_id", "locale", "path") where "path" is not null;
|
|
59
|
+
create index if not exists "published_entries_fields_idx"
|
|
60
|
+
on "published_entries" using gin ("fields");
|
|
61
|
+
|
|
62
|
+
alter table "published_pages" enable row level security;
|
|
63
|
+
alter table "published_entries" enable row level security;
|
|
64
|
+
|
|
65
|
+
-- smoodly_list_entries without the draft branch, over the delivery table (spec 2026-10-07 §3):
|
|
66
|
+
-- `where` and `order` read the raw published snapshot, the row answers with its document.
|
|
67
|
+
-- Invoker rights, so RLS decides what a key sees.
|
|
68
|
+
create or replace function "smoodly_list_published"(
|
|
69
|
+
p_collection text, p_locale text, p_where jsonb, p_order jsonb,
|
|
70
|
+
p_limit integer, p_parent uuid, p_has_parent boolean, p_ids uuid[]
|
|
71
|
+
) returns table ("id" uuid, "document" jsonb, "model" text, "stale" boolean) language sql stable set search_path = public as $$
|
|
72
|
+
with shown as (
|
|
73
|
+
select p."entry_id" as "id", p."sort", p."created_at", p."updated_at", p."fields", p."document", p."model", p."stale"
|
|
74
|
+
from "published_entries" p
|
|
75
|
+
where p."collection" = p_collection and p."locale" = p_locale
|
|
76
|
+
and (not p_has_parent or p."parent_id" is not distinct from p_parent)
|
|
77
|
+
and (p_ids is null or p."entry_id" = any(p_ids))
|
|
78
|
+
)
|
|
79
|
+
select s."id", s."document", s."model", s."stale" from shown s
|
|
80
|
+
where not exists (
|
|
81
|
+
select 1 from jsonb_each(coalesce(p_where, '{}'::jsonb)) w
|
|
82
|
+
where not (case when w.value ? 'in'
|
|
83
|
+
then (s."fields" ->> w.key) = any (select jsonb_array_elements_text(w.value -> 'in'))
|
|
84
|
+
else s."fields" @> jsonb_build_object(w.key, w.value -> 'contains') end))
|
|
85
|
+
order by
|
|
86
|
+
case when p_order ->> 'by' = 'manual' then s."sort" end asc nulls last,
|
|
87
|
+
case when p_order ->> 'direction' = 'asc' then
|
|
88
|
+
case p_order ->> 'by' when 'createdAt' then to_jsonb(s."created_at") when 'updatedAt' then to_jsonb(s."updated_at")
|
|
89
|
+
when 'manual' then null else nullif(nullif(s."fields" -> (p_order ->> 'by'), 'null'::jsonb), '""'::jsonb) end end asc nulls last,
|
|
90
|
+
case when p_order ->> 'direction' = 'desc' then
|
|
91
|
+
case p_order ->> 'by' when 'createdAt' then to_jsonb(s."created_at") when 'updatedAt' then to_jsonb(s."updated_at")
|
|
92
|
+
when 'manual' then null else nullif(nullif(s."fields" -> (p_order ->> 'by'), 'null'::jsonb), '""'::jsonb) end end desc nulls last,
|
|
93
|
+
s."created_at" asc, s."id" asc
|
|
94
|
+
limit p_limit;
|
|
95
|
+
$$;
|
|
96
|
+
|
|
97
|
+
-- One lookup for any address (spec 2026-10-07 §3): a page or an entry, under the caller's own policies.
|
|
98
|
+
create or replace view "published_routes" with (security_invoker = true) as
|
|
99
|
+
select "space_id", "locale", "path", 'page' as "kind", "page_id" as "id", "document", "model", "stale", "tags", null::text as "collection"
|
|
100
|
+
from "published_pages" where "path" is not null and "has_page"
|
|
101
|
+
union all
|
|
102
|
+
select "space_id", "locale", "path", 'entry' as "kind", "entry_id" as "id", "document", "model", "stale", "tags", "collection"
|
|
103
|
+
from "published_entries" where "path" is not null;
|
|
104
|
+
|
|
105
|
+
-- Stamped with the caller's space on insert, as every content table.
|
|
106
|
+
drop trigger if exists "smoodly_a_set_space" on "published_pages";
|
|
107
|
+
create trigger "smoodly_a_set_space" before insert on "published_pages"
|
|
108
|
+
for each row execute function "smoodly_set_space"();
|
|
109
|
+
|
|
110
|
+
drop trigger if exists "smoodly_a_set_space" on "published_entries";
|
|
111
|
+
create trigger "smoodly_a_set_space" before insert on "published_entries"
|
|
112
|
+
for each row execute function "smoodly_set_space"();
|
|
113
|
+
|
|
114
|
+
-- A write key reads and writes its own space; no other key reads (spec 2026-09-29 content API contract §8).
|
|
115
|
+
drop policy if exists "smoodly_write" on "published_pages";
|
|
116
|
+
|
|
117
|
+
drop policy if exists "smoodly_insert" on "published_pages";
|
|
118
|
+
create policy "smoodly_insert" on "published_pages" for insert
|
|
119
|
+
with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
120
|
+
|
|
121
|
+
drop policy if exists "smoodly_update" on "published_pages";
|
|
122
|
+
create policy "smoodly_update" on "published_pages" for update
|
|
123
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
|
|
124
|
+
with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
125
|
+
|
|
126
|
+
drop policy if exists "smoodly_delete" on "published_pages";
|
|
127
|
+
create policy "smoodly_delete" on "published_pages" for delete
|
|
128
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
129
|
+
|
|
130
|
+
drop policy if exists "smoodly_read" on "published_pages";
|
|
131
|
+
create policy "smoodly_read" on "published_pages" for select
|
|
132
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
133
|
+
|
|
134
|
+
drop policy if exists "smoodly_write" on "published_entries";
|
|
135
|
+
|
|
136
|
+
drop policy if exists "smoodly_insert" on "published_entries";
|
|
137
|
+
create policy "smoodly_insert" on "published_entries" for insert
|
|
138
|
+
with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
139
|
+
|
|
140
|
+
drop policy if exists "smoodly_update" on "published_entries";
|
|
141
|
+
create policy "smoodly_update" on "published_entries" for update
|
|
142
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
|
|
143
|
+
with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
144
|
+
|
|
145
|
+
drop policy if exists "smoodly_delete" on "published_entries";
|
|
146
|
+
create policy "smoodly_delete" on "published_entries" for delete
|
|
147
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
148
|
+
|
|
149
|
+
drop policy if exists "smoodly_read" on "published_entries";
|
|
150
|
+
create policy "smoodly_read" on "published_entries" for select
|
|
151
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
152
|
+
|
|
153
|
+
insert into "smoodly_meta" ("key", "value") values ('schema_version', '8')
|
|
154
|
+
on conflict ("key") do update set "value" = excluded."value";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- Smoodly core schema, version 9 (packages/smoodly/MAINTAINERS.md, register 2026-10-07: what the delivery model left).
|
|
2
|
+
-- Shipped in the smoodly package; `npx smoodly migrate` copies it into supabase/migrations/.
|
|
3
|
+
-- Never edit a published file: the next change is the next file.
|
|
4
|
+
-- The delta over version 8: smoodly_lookup_address, the route row and the
|
|
5
|
+
-- locale's global items in one call, so a live page is one database call
|
|
6
|
+
-- instead of the page then its layout's globals.
|
|
7
|
+
|
|
8
|
+
set client_min_messages = warning;
|
|
9
|
+
|
|
10
|
+
-- The address and the locale's global items in one call (schema v9; register 2026-10-07: a miss was two
|
|
11
|
+
-- rounds, the page then the layout's globals). The route row first, kind 'page' or 'entry'; then one row per
|
|
12
|
+
-- named global, kind 'global', the collection its name. A miss answers nothing: the globals ride with a page
|
|
13
|
+
-- only. Invoker rights, so RLS decides what a key sees.
|
|
14
|
+
create or replace function "smoodly_lookup_address"(p_locale text, p_path text, p_globals text[])
|
|
15
|
+
returns table ("kind" text, "id" uuid, "collection" text, "document" jsonb, "model" text, "stale" boolean, "tags" text[])
|
|
16
|
+
language sql stable set search_path = public as $$
|
|
17
|
+
with route as (
|
|
18
|
+
select r."kind", r."id", r."collection", r."document", r."model", r."stale", r."tags"
|
|
19
|
+
from "published_routes" r where r."locale" = p_locale and r."path" = p_path limit 1
|
|
20
|
+
)
|
|
21
|
+
select * from route
|
|
22
|
+
union all
|
|
23
|
+
select 'global' as "kind", e."entry_id", e."collection", e."document", e."model", e."stale", e."tags"
|
|
24
|
+
from "published_entries" e
|
|
25
|
+
where e."locale" = p_locale and e."collection" = any(coalesce(p_globals, '{}'::text[]))
|
|
26
|
+
and exists (select 1 from route);
|
|
27
|
+
$$;
|
|
28
|
+
|
|
29
|
+
insert into "smoodly_meta" ("key", "value") values ('schema_version', '9')
|
|
30
|
+
on conflict ("key") do update set "value" = excluded."value";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
-- Smoodly core schema, version 10 (DESIGN §3, the document stamp, decided 2026-10-08).
|
|
2
|
+
-- Shipped in the smoodly package; `npx smoodly migrate` copies it into supabase/migrations/.
|
|
3
|
+
-- Never edit a published file: the next change is the next file.
|
|
4
|
+
-- The delta over version 9: published_models, one row per space holding the
|
|
5
|
+
-- document stamp the space recorded as current, so only the deployment at that
|
|
6
|
+
-- stamp rewrites delivery rows (a preview deployment on a newer package serves
|
|
7
|
+
-- assembled documents and writes nothing). Additive: no function changes.
|
|
8
|
+
|
|
9
|
+
set client_min_messages = warning;
|
|
10
|
+
|
|
11
|
+
-- The document stamp a space recorded as current: set by `npx smoodly rebuild` and by the first writer
|
|
12
|
+
-- into an empty record; only a reader at this stamp rewrites published_pages and published_entries.
|
|
13
|
+
create table if not exists "published_models" (
|
|
14
|
+
"space_id" uuid primary key default '00000000-0000-0000-0000-000000000000',
|
|
15
|
+
"model" text not null,
|
|
16
|
+
"recorded_at" timestamptz not null default now()
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
alter table "published_models" enable row level security;
|
|
20
|
+
|
|
21
|
+
-- Stamped with the caller's space on insert, as every content table.
|
|
22
|
+
drop trigger if exists "smoodly_a_set_space" on "published_models";
|
|
23
|
+
create trigger "smoodly_a_set_space" before insert on "published_models"
|
|
24
|
+
for each row execute function "smoodly_set_space"();
|
|
25
|
+
|
|
26
|
+
-- A write key reads and writes its own space's record; no other key reads (spec 2026-09-29 content API contract §8).
|
|
27
|
+
drop policy if exists "smoodly_write" on "published_models";
|
|
28
|
+
|
|
29
|
+
drop policy if exists "smoodly_insert" on "published_models";
|
|
30
|
+
create policy "smoodly_insert" on "published_models" for insert
|
|
31
|
+
with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
32
|
+
|
|
33
|
+
drop policy if exists "smoodly_update" on "published_models";
|
|
34
|
+
create policy "smoodly_update" on "published_models" for update
|
|
35
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
|
|
36
|
+
with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
37
|
+
|
|
38
|
+
drop policy if exists "smoodly_delete" on "published_models";
|
|
39
|
+
create policy "smoodly_delete" on "published_models" for delete
|
|
40
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
41
|
+
|
|
42
|
+
drop policy if exists "smoodly_read" on "published_models";
|
|
43
|
+
create policy "smoodly_read" on "published_models" for select
|
|
44
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
45
|
+
|
|
46
|
+
insert into "smoodly_meta" ("key", "value") values ('schema_version', '10')
|
|
47
|
+
on conflict ("key") do update set "value" = excluded."value";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smoodly",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Open-source, code-first CMS for Next.js on Supabase: define a component once, it becomes a typed, editable building block.",
|
|
3
|
+
"version": "0.0.47",
|
|
4
|
+
"description": "Open-source, code-first CMS for Astro and Next.js on Supabase: define a component once, it becomes a typed, editable building block.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -19,10 +19,30 @@
|
|
|
19
19
|
"types": "./dist/index.d.ts",
|
|
20
20
|
"default": "./dist/index.js"
|
|
21
21
|
},
|
|
22
|
+
"./react": {
|
|
23
|
+
"types": "./dist/react.d.ts",
|
|
24
|
+
"default": "./dist/react.js"
|
|
25
|
+
},
|
|
22
26
|
"./next": {
|
|
23
27
|
"types": "./dist/next/index.d.ts",
|
|
24
28
|
"default": "./dist/next/index.js"
|
|
25
29
|
},
|
|
30
|
+
"./astro": {
|
|
31
|
+
"types": "./dist/astro/index.d.ts",
|
|
32
|
+
"default": "./dist/astro/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./astro/runtime": {
|
|
35
|
+
"types": "./dist/astro/runtime.d.ts",
|
|
36
|
+
"default": "./dist/astro/runtime.js"
|
|
37
|
+
},
|
|
38
|
+
"./astro/components": {
|
|
39
|
+
"types": "./dist/astro/components.d.ts",
|
|
40
|
+
"default": "./dist/astro/components.js"
|
|
41
|
+
},
|
|
42
|
+
"./astro/content": {
|
|
43
|
+
"types": "./dist/astro/content.d.ts",
|
|
44
|
+
"default": "./dist/astro/content.js"
|
|
45
|
+
},
|
|
26
46
|
"./image": {
|
|
27
47
|
"types": "./dist/image/index.d.ts",
|
|
28
48
|
"default": "./dist/image/index.js"
|
|
@@ -39,10 +59,6 @@
|
|
|
39
59
|
"types": "./dist/admin/index.d.ts",
|
|
40
60
|
"default": "./dist/admin/index.js"
|
|
41
61
|
},
|
|
42
|
-
"./admin/next": {
|
|
43
|
-
"types": "./dist/admin/next/index.d.ts",
|
|
44
|
-
"default": "./dist/admin/next/index.js"
|
|
45
|
-
},
|
|
46
62
|
"./migrations": {
|
|
47
63
|
"types": "./dist/migrations.d.ts",
|
|
48
64
|
"default": "./dist/migrations.js"
|
|
@@ -54,35 +70,33 @@
|
|
|
54
70
|
"bin"
|
|
55
71
|
],
|
|
56
72
|
"scripts": {
|
|
57
|
-
"build": "
|
|
73
|
+
"build:admin": "tsx scripts/build-admin.ts",
|
|
74
|
+
"build": "node scripts/write-version.mjs && rm -rf dist && tsc -p tsconfig.build.json && node scripts/copy-astro.mjs && npm run build:admin",
|
|
58
75
|
"prepublishOnly": "npm run build",
|
|
59
|
-
"pretest": "node scripts/write-version.mjs",
|
|
76
|
+
"pretest": "node scripts/write-version.mjs && npm run -s build:admin",
|
|
60
77
|
"test": "vitest run",
|
|
61
78
|
"test:watch": "vitest",
|
|
62
79
|
"typecheck": "tsc --noEmit",
|
|
63
80
|
"test:supabase": "bash scripts/test-supabase.sh"
|
|
64
81
|
},
|
|
65
82
|
"peerDependencies": {
|
|
83
|
+
"astro": "^7.3.5",
|
|
66
84
|
"next": ">=16",
|
|
67
85
|
"react": "^18 || ^19"
|
|
68
86
|
},
|
|
69
87
|
"peerDependenciesMeta": {
|
|
88
|
+
"astro": {
|
|
89
|
+
"optional": true
|
|
90
|
+
},
|
|
70
91
|
"next": {
|
|
71
92
|
"optional": true
|
|
93
|
+
},
|
|
94
|
+
"react": {
|
|
95
|
+
"optional": true
|
|
72
96
|
}
|
|
73
97
|
},
|
|
74
98
|
"devDependencies": {
|
|
75
|
-
"@
|
|
76
|
-
"@types/react": "^19",
|
|
77
|
-
"@types/react-dom": "^19.2.5",
|
|
78
|
-
"next": "^16.3.4",
|
|
79
|
-
"react": "^19",
|
|
80
|
-
"react-dom": "^19",
|
|
81
|
-
"typescript": "^5",
|
|
82
|
-
"vitest": "^3"
|
|
83
|
-
},
|
|
84
|
-
"dependencies": {
|
|
85
|
-
"@supabase/supabase-js": "^2.113.0",
|
|
99
|
+
"@fontsource-variable/public-sans": "^5.3.0",
|
|
86
100
|
"@tiptap/core": "3.31.4",
|
|
87
101
|
"@tiptap/extension-blockquote": "3.31.4",
|
|
88
102
|
"@tiptap/extension-bold": "3.31.4",
|
|
@@ -96,7 +110,20 @@
|
|
|
96
110
|
"@tiptap/react": "3.31.4",
|
|
97
111
|
"@tiptap/starter-kit": "3.31.4",
|
|
98
112
|
"@tiptap/suggestion": "3.31.4",
|
|
113
|
+
"@types/node": "^26.4.1",
|
|
114
|
+
"@types/react": "^19",
|
|
115
|
+
"@types/react-dom": "^19.2.5",
|
|
116
|
+
"astro": "^7.3.5",
|
|
117
|
+
"esbuild": "^0.28.2",
|
|
99
118
|
"lucide-react": "^1.41.0",
|
|
119
|
+
"next": "^16.3.4",
|
|
120
|
+
"react": "^19",
|
|
121
|
+
"react-dom": "^19",
|
|
122
|
+
"typescript": "^5",
|
|
123
|
+
"vitest": "^3"
|
|
124
|
+
},
|
|
125
|
+
"dependencies": {
|
|
126
|
+
"@supabase/supabase-js": "^2.113.0",
|
|
100
127
|
"tsx": "^4.23.15"
|
|
101
128
|
}
|
|
102
129
|
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
// Runs INSIDE the draft-route iframe (glue adds it only when draft mode +
|
|
4
|
-
// the editor param are on). Reports geometry — section nodes, empty zones
|
|
5
|
-
// and, since spec 2026-09-10, every marked field with its resolved path —
|
|
6
|
-
// forwards select/hover with the field under the pointer, executes
|
|
7
|
-
// refresh/scroll, and says when a render has committed. The admin overlay
|
|
8
|
-
// draws all chrome — bar one rule this component renders: a section that
|
|
9
|
-
// renders nothing (a fresh custom section, an all-optional section left
|
|
10
|
-
// empty) is stamped blank and takes a min-height, so the editor can see
|
|
11
|
-
// and select it; the live site never mounts the bridge, so it keeps its
|
|
12
|
-
// true height. Rects are in document coordinates: the iframe is sized
|
|
13
|
-
// to the full document height by the editor, so client rects + scroll
|
|
14
|
-
// offsets map 1:1 onto the overlay.
|
|
15
|
-
import { useEffect } from "react";
|
|
16
|
-
import { useRouter } from "next/navigation";
|
|
17
|
-
import { BLANK_ATTR, CRASHED_ATTR, EDITOR_ATTR, FIELD_ATTR, NODE_ATTR, ZONE_ATTR, crashedOf, resolveMarkPath } from "../../marks.js";
|
|
18
|
-
const post = (m) => window.parent.postMessage(m, "*");
|
|
19
|
-
/** The height of an md-spaced section with nothing inside (3.5rem + 3.5rem). */
|
|
20
|
-
// The editor sizes the iframe to the reported height; the document's own
|
|
21
|
-
// viewport must never scroll (2026-09-26): a fractional content height
|
|
22
|
-
// (4595.3px in a 4595px frame) left it scrollable by a hair, and a
|
|
23
|
-
// trackpad scroll over the canvas showed the frame's overlay scrollbar.
|
|
24
|
-
// Hidden overflow drops no layout — programmatic scrollIntoView still
|
|
25
|
-
// works, and the live site never mounts this.
|
|
26
|
-
const BRIDGE_CSS = `html{overflow:hidden}[${NODE_ATTR}][${BLANK_ATTR}]{min-height:7rem}`;
|
|
27
|
-
const MEDIA = "img,picture,video,audio,svg,canvas,iframe,object";
|
|
28
|
-
/** No visible text (innerText skips <style> and hidden content) and no media. */
|
|
29
|
-
const isBlank = (el) => el.innerText.trim() === "" && el.querySelector(MEDIA) === null;
|
|
30
|
-
export function EditorBridge() {
|
|
31
|
-
const router = useRouter();
|
|
32
|
-
// Every commit of this component — the mount, then each RSC refresh,
|
|
33
|
-
// which re-renders the route — tells the editor the new markup is in
|
|
34
|
-
// the DOM (spec 2026-09-10 §5: the inline richtext editor lets go of
|
|
35
|
-
// its container only then).
|
|
36
|
-
useEffect(() => { post({ source: "smoodly", kind: "rendered" }); });
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
const docRect = (el) => {
|
|
39
|
-
const r = el.getBoundingClientRect();
|
|
40
|
-
return { x: r.left + window.scrollX, y: r.top + window.scrollY, w: r.width, h: r.height };
|
|
41
|
-
};
|
|
42
|
-
const all = (selector) => Array.from(document.querySelectorAll(selector));
|
|
43
|
-
const inEditor = (el) => Boolean(el?.closest?.(`[${EDITOR_ATTR}]`));
|
|
44
|
-
const report = () => {
|
|
45
|
-
// Array.from, not spread: the package's lib set is ES2020 + DOM
|
|
46
|
-
// (no DOM.Iterable), so a NodeList isn't statically iterable here.
|
|
47
|
-
const roots = all(`[${NODE_ATTR}]`);
|
|
48
|
-
// Stamp before measuring, so the min-height is in the rects; only on a
|
|
49
|
-
// change, so the attribute mutation this causes converges instead of looping.
|
|
50
|
-
for (const el of roots) {
|
|
51
|
-
const blank = isBlank(el);
|
|
52
|
-
if (blank !== el.hasAttribute(BLANK_ATTR)) {
|
|
53
|
-
if (blank)
|
|
54
|
-
el.setAttribute(BLANK_ATTR, "");
|
|
55
|
-
else
|
|
56
|
-
el.removeAttribute(BLANK_ATTR);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
const nodes = roots.map((el) => ({ id: el.getAttribute(NODE_ATTR), ...docRect(el) }));
|
|
60
|
-
const emptyZones = all(`[${ZONE_ATTR}]`).map((el) => ({ zone: el.getAttribute(ZONE_ATTR), ...docRect(el) }));
|
|
61
|
-
const fields = [];
|
|
62
|
-
for (const el of all(`[${FIELD_ATTR}]`)) {
|
|
63
|
-
// A mark inside the admin's own editor root (a nested element card) is not the page's.
|
|
64
|
-
if (inEditor(el))
|
|
65
|
-
continue;
|
|
66
|
-
const mark = resolveMarkPath(el);
|
|
67
|
-
if (mark)
|
|
68
|
-
fields.push({ ...mark, ...docRect(el) });
|
|
69
|
-
}
|
|
70
|
-
// Rounded up from the fractional box, so the frame is never a hair too short.
|
|
71
|
-
const docH = Math.ceil(Math.max(document.documentElement.scrollHeight, document.documentElement.getBoundingClientRect().height));
|
|
72
|
-
// Crashed sections (spec 2026-09-27 section guard §6): the placeholders the guard and the canvas net left.
|
|
73
|
-
const crashed = crashedOf(all(`[${CRASHED_ATTR}]`));
|
|
74
|
-
post({ source: "smoodly", kind: "rects", nodes, emptyZones, fields, docH, crashed });
|
|
75
|
-
};
|
|
76
|
-
const raf = () => requestAnimationFrame(report);
|
|
77
|
-
const ro = new ResizeObserver(raf);
|
|
78
|
-
ro.observe(document.documentElement);
|
|
79
|
-
// characterData too: a text session edits a text node in place, and the outline must follow.
|
|
80
|
-
const mo = new MutationObserver(raf);
|
|
81
|
-
mo.observe(document.body, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
82
|
-
const elementOf = (target) => target;
|
|
83
|
-
const nodeIdAt = (target) => elementOf(target)?.closest?.(`[${NODE_ATTR}]`)?.getAttribute(NODE_ATTR) ?? null;
|
|
84
|
-
const pathAt = (target) => {
|
|
85
|
-
const el = elementOf(target)?.closest?.(`[${FIELD_ATTR}]`);
|
|
86
|
-
return el ? resolveMarkPath(el)?.path ?? null : null;
|
|
87
|
-
};
|
|
88
|
-
const onClick = (e) => {
|
|
89
|
-
// The canvas must never navigate away from under the editor.
|
|
90
|
-
if (elementOf(e.target)?.closest?.("a"))
|
|
91
|
-
e.preventDefault();
|
|
92
|
-
// Inside its own editor root the admin owns every click.
|
|
93
|
-
if (inEditor(elementOf(e.target)))
|
|
94
|
-
return;
|
|
95
|
-
post({ source: "smoodly", kind: "select", id: nodeIdAt(e.target), path: pathAt(e.target), x: e.clientX, y: e.clientY });
|
|
96
|
-
};
|
|
97
|
-
const onMove = (e) => {
|
|
98
|
-
if (inEditor(elementOf(e.target)))
|
|
99
|
-
return;
|
|
100
|
-
post({ source: "smoodly", kind: "hover", id: nodeIdAt(e.target), path: pathAt(e.target) });
|
|
101
|
-
};
|
|
102
|
-
const onMessage = (e) => {
|
|
103
|
-
const m = e.data;
|
|
104
|
-
if (m?.source !== "smoodly")
|
|
105
|
-
return;
|
|
106
|
-
if (m.kind === "refresh")
|
|
107
|
-
router.refresh();
|
|
108
|
-
if (m.kind === "scroll") {
|
|
109
|
-
document
|
|
110
|
-
.querySelector(`[${NODE_ATTR}="${CSS.escape(m.id)}"]`)
|
|
111
|
-
?.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
document.addEventListener("click", onClick, true);
|
|
115
|
-
document.addEventListener("mousemove", onMove, true);
|
|
116
|
-
window.addEventListener("message", onMessage);
|
|
117
|
-
report();
|
|
118
|
-
const late = setTimeout(report, 500); // fonts/images settle
|
|
119
|
-
return () => {
|
|
120
|
-
ro.disconnect();
|
|
121
|
-
mo.disconnect();
|
|
122
|
-
clearTimeout(late);
|
|
123
|
-
document.removeEventListener("click", onClick, true);
|
|
124
|
-
document.removeEventListener("mousemove", onMove, true);
|
|
125
|
-
window.removeEventListener("message", onMessage);
|
|
126
|
-
};
|
|
127
|
-
}, [router]);
|
|
128
|
-
return _jsx("style", { "data-smoodly-bridge": "", children: BRIDGE_CSS });
|
|
129
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
import type { ResolvedConfig } from "../../config.ts";
|
|
3
|
-
import type { AdminOpAction } from "../ops.ts";
|
|
4
|
-
import type { AuthConfig } from "../shell/session.ts";
|
|
5
|
-
import type { PresenceConfig } from "../../env.ts";
|
|
6
|
-
/** `presence` (optional) is the content project's public config for editing presence (spec 2026-09-26
|
|
7
|
-
* concurrency §5; presence only, never content reads, spec 2026-09-29 content API contract §8), async because its space is asked of the database; absent or failed, presence
|
|
8
|
-
* is off and everything else works. */
|
|
9
|
-
export declare function createSmoodlyAdmin(input: {
|
|
10
|
-
config: ResolvedConfig;
|
|
11
|
-
op: AdminOpAction;
|
|
12
|
-
auth: () => AuthConfig;
|
|
13
|
-
presence?: () => PresenceConfig | Promise<PresenceConfig>;
|
|
14
|
-
}): {
|
|
15
|
-
AdminPage: () => null;
|
|
16
|
-
AdminLayout: ({ children }: {
|
|
17
|
-
children: ReactNode;
|
|
18
|
-
}) => Promise<import("react").JSX.Element>;
|
|
19
|
-
};
|