smoodly 0.0.45 → 0.0.46
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/index.d.ts +1 -1
- package/dist/admin/ops-impl.d.ts +7 -1
- package/dist/admin/ops-impl.js +110 -33
- 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/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 +3 -0
- package/dist/cli/migrate.js +8 -1
- 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/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 +69 -0
- package/dist/project.js +210 -0
- package/dist/published.d.ts +96 -0
- package/dist/published.js +125 -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 +38 -14
- package/dist/site.js +159 -286
- package/dist/smd-key.d.ts +9 -0
- package/dist/smd-key.js +1 -0
- package/dist/sql-space.d.ts +4 -0
- package/dist/sql-space.js +10 -5
- package/dist/sql.d.ts +2 -0
- package/dist/sql.js +117 -0
- package/dist/store.d.ts +1 -1
- package/dist/supabase-published-store.d.ts +35 -0
- package/dist/supabase-published-store.js +215 -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/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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// The bound stores and site layer of an Astro project (spec 2026-10-02 astro cloud fit §1), built from its
|
|
2
|
+
// smoodly.config.ts through the virtual module: what the Next starter's server.ts glue does, with no file
|
|
3
|
+
// in the app. Built on first use, so a build that renders nothing needs no keys.
|
|
4
|
+
import { i18n } from "astro:config/server";
|
|
5
|
+
import { config as loaded } from "smoodly:config";
|
|
6
|
+
import { supabaseAdminAuth } from "../admin/supabase-auth.js";
|
|
7
|
+
import { createSupabaseStores, siteOptions } from "../bind.js";
|
|
8
|
+
import { createSmoodlyClient } from "../client.js";
|
|
9
|
+
import { presenceConfig, smoodlyEnv } from "../env.js";
|
|
10
|
+
import { createSmoodlySite } from "../site.js";
|
|
11
|
+
import { astroRouting, localeHref } from "./locale-paths.js";
|
|
12
|
+
export const config = loaded;
|
|
13
|
+
if (!config?.registry) {
|
|
14
|
+
throw new Error("smoodly: smoodly.config.ts must export `config`, the result of defineConfig(…).");
|
|
15
|
+
}
|
|
16
|
+
/** The address rule: astro.config's i18n when it is set, else the default locale at the root. */
|
|
17
|
+
export const routing = astroRouting(i18n, config.locales);
|
|
18
|
+
/** Locale-relative CMS path → site URL, for the operations and the `page` prop. */
|
|
19
|
+
export const href = localeHref(config.locales, routing);
|
|
20
|
+
function build() {
|
|
21
|
+
const db = createSmoodlyClient(smoodlyEnv());
|
|
22
|
+
const stores = createSupabaseStores(db, config);
|
|
23
|
+
const site = createSmoodlySite(siteOptions(config, () => ({ pages: stores.pages, entries: stores.entries, assets: stores.assets, published: stores.published })));
|
|
24
|
+
return { db, ...stores, site };
|
|
25
|
+
}
|
|
26
|
+
let cached = null;
|
|
27
|
+
const bound = () => (cached ?? (cached = build()));
|
|
28
|
+
export const site = () => bound().site;
|
|
29
|
+
/** The draft cookie's key: the server-only key every Smoodly site already holds. */
|
|
30
|
+
export const secret = () => smoodlyEnv().secretKey;
|
|
31
|
+
let admin = null;
|
|
32
|
+
/** The admin's wiring. The publishable key is asked for here, not on a live page. */
|
|
33
|
+
export function adminDeps() {
|
|
34
|
+
if (admin)
|
|
35
|
+
return admin;
|
|
36
|
+
const env = smoodlyEnv("admin");
|
|
37
|
+
const { db, pages, entries, assets, members, site } = bound();
|
|
38
|
+
admin = {
|
|
39
|
+
config, pages, entries, assets, members, href, projector: site.projector,
|
|
40
|
+
auth: supabaseAdminAuth({ authUrl: env.authUrl, authKey: env.authKey, db }),
|
|
41
|
+
signIn: { url: env.authUrl, key: env.authKey },
|
|
42
|
+
presence: () => presenceConfig(env),
|
|
43
|
+
};
|
|
44
|
+
return admin;
|
|
45
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ResolvedConfig } from "../config.ts";
|
|
2
|
+
import { type ReadAdapter } from "../reads.ts";
|
|
3
|
+
import { type EntryItem } from "../read-shapes.ts";
|
|
4
|
+
import type { PageContext } from "../page-context.ts";
|
|
5
|
+
import { type SmoodlySite } from "../site.ts";
|
|
6
|
+
import { type PageHead } from "./head.ts";
|
|
7
|
+
import { type LocaleRouting } from "./locale-paths.ts";
|
|
8
|
+
import type { ZoneContent } from "./zone.ts";
|
|
9
|
+
/** What the middleware decides once per request and the routes read from `Astro.locals.smoodly`. */
|
|
10
|
+
export type SmoodlyLocals = {
|
|
11
|
+
/** A verified preview: reads return drafts, and nothing is cached. */
|
|
12
|
+
draft: boolean;
|
|
13
|
+
/** The canvas asked (`smoodly-editor=1`), inside a preview only. */
|
|
14
|
+
editor: boolean;
|
|
15
|
+
/** History's stored version, inside a preview only. */
|
|
16
|
+
version?: string;
|
|
17
|
+
/** Set by the shell when it rendered a page read from the database: the response may be cached under its tags. */
|
|
18
|
+
cacheable: boolean;
|
|
19
|
+
/** Set by the shell when it rendered a page at all: in a preview, only that response is a canvas. */
|
|
20
|
+
shell: boolean;
|
|
21
|
+
/** The locale the address was read in; set by the shell. */
|
|
22
|
+
locale?: string;
|
|
23
|
+
};
|
|
24
|
+
declare global {
|
|
25
|
+
namespace App {
|
|
26
|
+
interface Locals {
|
|
27
|
+
smoodly?: SmoodlyLocals;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export type ShellDeps = {
|
|
32
|
+
site: SmoodlySite;
|
|
33
|
+
config: ResolvedConfig;
|
|
34
|
+
href: (path: string, locale: string) => string;
|
|
35
|
+
routing: LocaleRouting;
|
|
36
|
+
run: ReadAdapter["run"];
|
|
37
|
+
/** Seeds the request's scope with the values the lookup brought along (scope.ts `preload`); absent in a static build. */
|
|
38
|
+
preload?: (entries: {
|
|
39
|
+
key: string[];
|
|
40
|
+
tags: string[];
|
|
41
|
+
value: unknown;
|
|
42
|
+
}[]) => void;
|
|
43
|
+
};
|
|
44
|
+
/** Whether the project has a view file for a page type or a collection's entries. */
|
|
45
|
+
export type ShellViews = {
|
|
46
|
+
page(template: string): boolean;
|
|
47
|
+
entry(collection: string): boolean;
|
|
48
|
+
};
|
|
49
|
+
export type Shell = {
|
|
50
|
+
kind: "notFound";
|
|
51
|
+
} | {
|
|
52
|
+
kind: "redirect";
|
|
53
|
+
location: string;
|
|
54
|
+
status: 302 | 308;
|
|
55
|
+
} | {
|
|
56
|
+
kind: "page";
|
|
57
|
+
template: string;
|
|
58
|
+
page: PageContext;
|
|
59
|
+
zones: Record<string, ZoneContent>;
|
|
60
|
+
head: PageHead;
|
|
61
|
+
} | {
|
|
62
|
+
kind: "entry";
|
|
63
|
+
collection: string;
|
|
64
|
+
entry: EntryItem;
|
|
65
|
+
head: PageHead;
|
|
66
|
+
};
|
|
67
|
+
export declare function resolveShell(deps: ShellDeps, pathname: string, search: string, state: SmoodlyLocals, views: ShellViews): Promise<Shell>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { addressWithGlobalsUnit, globalUnit } from "../reads.js";
|
|
2
|
+
import { toEntryItem } from "../read-shapes.js";
|
|
3
|
+
import { pageContextOf } from "../site.js";
|
|
4
|
+
import { headOf } from "./head.js";
|
|
5
|
+
import { redirectsRoot, splitLocale } from "./locale-paths.js";
|
|
6
|
+
const NOT_FOUND = { kind: "notFound" };
|
|
7
|
+
export async function resolveShell(deps, pathname, search, state, views) {
|
|
8
|
+
const { site } = deps;
|
|
9
|
+
const split = splitLocale(pathname, site.locales, deps.routing);
|
|
10
|
+
if (!split) {
|
|
11
|
+
// Prefix-always, and Astro let "/" through: the default locale's home, as Astro's own root redirect would (nothing read, never cached).
|
|
12
|
+
return redirectsRoot(pathname, deps.routing) ? { kind: "redirect", location: deps.href("/", site.locales.default) + search, status: 302 } : NOT_FOUND;
|
|
13
|
+
}
|
|
14
|
+
const { locale, path } = split;
|
|
15
|
+
state.locale = locale;
|
|
16
|
+
// One read: a page or entry, a redirect row, or null; a code-owned address with no record yet comes back
|
|
17
|
+
// as its code page, as in Next (spec 2026-09-30 tester findings §2). The layout globals (`layout: true`) ride
|
|
18
|
+
// along (schema v9) and are seeded under their units' keys, so the layout's loaders cost no call; one with
|
|
19
|
+
// no published row is simply not there. The scope is seeded even with nothing, which marks a page render.
|
|
20
|
+
const items = (deps.config.registry.globals ?? []).filter((item) => item.layout);
|
|
21
|
+
const { hit: got, globals } = await deps.run(addressWithGlobalsUnit(site, locale, path, items, state.version));
|
|
22
|
+
if (!state.draft) {
|
|
23
|
+
deps.preload?.(items.filter((item) => globals.has(item.name)).map((item) => {
|
|
24
|
+
const unit = globalUnit(site, item, locale);
|
|
25
|
+
return { key: unit.key, tags: unit.tags, value: globals.get(item.name) };
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
if (got?.kind === "redirect") {
|
|
29
|
+
// A redirect row (spec 2026-09-26 redirects §7): the node's CURRENT path in this locale, through href, the query kept; no path there is a 404.
|
|
30
|
+
const current = (await site.targetPaths(got.target))[locale];
|
|
31
|
+
return current ? { kind: "redirect", location: deps.href(current, locale) + search, status: 308 } : NOT_FOUND;
|
|
32
|
+
}
|
|
33
|
+
const hit = got;
|
|
34
|
+
if (!hit)
|
|
35
|
+
return NOT_FOUND;
|
|
36
|
+
const head = headOf(hit, locale, deps.href, site.locales);
|
|
37
|
+
if (hit.kind === "entry") {
|
|
38
|
+
if (!views.entry(hit.collection)) {
|
|
39
|
+
console.warn(`smoodly: no src/entry-pages/${hit.collection}.astro — the entry at ${hit.path} is a 404.`);
|
|
40
|
+
return NOT_FOUND;
|
|
41
|
+
}
|
|
42
|
+
state.cacheable = !state.draft;
|
|
43
|
+
state.shell = true;
|
|
44
|
+
return { kind: "entry", collection: hit.collection, entry: toEntryItem(hit.entry, hit.entry.fields, hit), head };
|
|
45
|
+
}
|
|
46
|
+
const schema = deps.config.registry.pages.find((p) => p.name === hit.template);
|
|
47
|
+
if (!schema) {
|
|
48
|
+
console.warn(`smoodly: no page type named "${hit.template}" in the config — the record at ${hit.path} is a 404.`);
|
|
49
|
+
return NOT_FOUND;
|
|
50
|
+
}
|
|
51
|
+
if (!views.page(hit.template)) {
|
|
52
|
+
console.warn(`smoodly: no src/page-types/${hit.template}.astro — the record at ${hit.path} is a 404.`);
|
|
53
|
+
return NOT_FOUND;
|
|
54
|
+
}
|
|
55
|
+
// Only a page read from the database has a tag to expire it by.
|
|
56
|
+
state.cacheable = !hit.record.id.startsWith("code:") && !state.draft;
|
|
57
|
+
state.shell = true;
|
|
58
|
+
const zoneNames = Object.keys(schema.zones ?? {});
|
|
59
|
+
const zones = Object.fromEntries(zoneNames.map((name) => [
|
|
60
|
+
name, { name, nodes: hit.tree.zones?.[name] ?? [], editor: state.editor, styleValues: site.styles },
|
|
61
|
+
]));
|
|
62
|
+
return { kind: "page", template: hit.template, page: pageContextOf(hit, deps.href), zones, head };
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function styleAttribute(own: string | undefined, inline: Record<string, unknown>): string | undefined;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// The style attribute Section.astro writes (what Astro does for an object `style`, kept here because a string
|
|
2
|
+
// `style` is merged in too): camelCase keys become kebab-case, a custom property (`--x`) keeps its name, and a
|
|
3
|
+
// value that is undefined, null or "" is left out.
|
|
4
|
+
const kebab = (key) => (key.startsWith("--") ? key : key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`));
|
|
5
|
+
export function styleAttribute(own, inline) {
|
|
6
|
+
const declarations = Object.entries(inline)
|
|
7
|
+
.filter(([, v]) => v !== undefined && v !== null && v !== "")
|
|
8
|
+
.map(([k, v]) => `${kebab(k)}:${v}`)
|
|
9
|
+
.join(";");
|
|
10
|
+
return [own ?? "", declarations].filter(Boolean).join(";") || undefined;
|
|
11
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function missingViews(registry, views) {
|
|
2
|
+
const out = [];
|
|
3
|
+
/** Both directions for one kind: `dir` is the folder under src, `key` the glob's name in the views object. */
|
|
4
|
+
const both = (dir, key, names, wants, orphan) => {
|
|
5
|
+
// An own name only, as the shell and the renderers look a view up: an inherited `constructor` is no view.
|
|
6
|
+
for (const name of names)
|
|
7
|
+
if (!Object.prototype.hasOwnProperty.call(views[key], name))
|
|
8
|
+
out.push(`src/${dir}/${name}.astro — ${wants(name)}`);
|
|
9
|
+
for (const file of Object.keys(views[key]))
|
|
10
|
+
if (!names.includes(file))
|
|
11
|
+
out.push(`src/${dir}/${file}.astro — ${orphan(file)}`);
|
|
12
|
+
};
|
|
13
|
+
both("sections", "sections", registry.sections.map((s) => s.name), (n) => `the section "${n}" has no view`, (f) => `no section named "${f}" is registered`);
|
|
14
|
+
both("elements", "elements", registry.elements.map((e) => e.name), (n) => `the element "${n}" has no view`, (f) => `no element named "${f}" is registered`);
|
|
15
|
+
both("page-types", "pages", registry.pages.map((p) => p.name), (n) => `the page type "${n}" has no view`, (f) => `no page type named "${f}" is registered`);
|
|
16
|
+
const mounted = registry.collections.filter((c) => c.path !== undefined).map((c) => c.name);
|
|
17
|
+
both("entry-pages", "entries", mounted, (n) => `the collection "${n}" has a path and no entry page`, (f) => `no collection named "${f}" has a path`);
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { TreeNode } from "../page-context.ts";
|
|
2
|
+
import type { StyleValues } from "../styles.ts";
|
|
3
|
+
import type { SchemaInfo } from "./defaults.ts";
|
|
4
|
+
export type ZoneContent = {
|
|
5
|
+
name: string;
|
|
6
|
+
nodes: TreeNode[];
|
|
7
|
+
/** The canvas asked: emit the editor's boxes. */
|
|
8
|
+
editor: boolean;
|
|
9
|
+
/** The config's CSS values behind the styles set. */
|
|
10
|
+
styleValues: StyleValues;
|
|
11
|
+
};
|
|
12
|
+
/** What the zone asks of the registry and the project's views: whether the registry has a node type, whether
|
|
13
|
+
* a view file does, and what the registry says about it. */
|
|
14
|
+
export type ZoneViews = {
|
|
15
|
+
registered(type: string): boolean;
|
|
16
|
+
has(type: string): boolean;
|
|
17
|
+
about(type: string): SchemaInfo;
|
|
18
|
+
};
|
|
19
|
+
export type ZoneItem = {
|
|
20
|
+
kind: "empty";
|
|
21
|
+
zone: string;
|
|
22
|
+
} | {
|
|
23
|
+
kind: "unknown";
|
|
24
|
+
id: string;
|
|
25
|
+
type: string;
|
|
26
|
+
} | {
|
|
27
|
+
kind: "missing";
|
|
28
|
+
id: string;
|
|
29
|
+
type: string;
|
|
30
|
+
} | {
|
|
31
|
+
kind: "section";
|
|
32
|
+
id: string;
|
|
33
|
+
type: string;
|
|
34
|
+
title: string;
|
|
35
|
+
props: Record<string, unknown>;
|
|
36
|
+
};
|
|
37
|
+
/** The canvas placeholders' look — a node whose view is missing, and a crashed section (section-crash.tsx has the React one). */
|
|
38
|
+
export declare const PLACEHOLDER_CSS = "padding:24px 16px;border:1px dashed #b54708;color:#b54708;font:500 14px/1.4 system-ui,sans-serif;text-align:center";
|
|
39
|
+
export declare function zoneItems(content: ZoneContent | undefined, views: ZoneViews, warn?: (message: string) => void): ZoneItem[];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SMD_KEY } from "../smd-key.js";
|
|
2
|
+
/** The canvas placeholders' look — a node whose view is missing, and a crashed section (section-crash.tsx has the React one). */
|
|
3
|
+
export const PLACEHOLDER_CSS = "padding:24px 16px;border:1px dashed #b54708;color:#b54708;font:500 14px/1.4 system-ui,sans-serif;text-align:center";
|
|
4
|
+
export function zoneItems(content, views, warn = console.warn) {
|
|
5
|
+
// A malformed tree must not crash the render: non-array nodes are an empty zone.
|
|
6
|
+
const nodes = content && Array.isArray(content.nodes) ? content.nodes : [];
|
|
7
|
+
if (!content || nodes.length === 0)
|
|
8
|
+
return content?.editor ? [{ kind: "empty", zone: content.name }] : [];
|
|
9
|
+
const items = [];
|
|
10
|
+
for (const node of nodes) {
|
|
11
|
+
// A type the code no longer registers (drift): the React zone's "Unknown section", which the editor's report names too.
|
|
12
|
+
if (!views.registered(node.type)) {
|
|
13
|
+
if (content.editor)
|
|
14
|
+
items.push({ kind: "unknown", id: node.id, type: node.type });
|
|
15
|
+
else
|
|
16
|
+
warn(`smoodly: no registered section for node type "${node.type}" — skipping.`);
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (!views.has(node.type)) {
|
|
20
|
+
if (content.editor)
|
|
21
|
+
items.push({ kind: "missing", id: node.id, type: node.type });
|
|
22
|
+
else
|
|
23
|
+
warn(`smoodly: no src/sections/${node.type}.astro for node type "${node.type}" — skipping.`);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const { title, defaults } = views.about(node.type);
|
|
27
|
+
// The node id, the css and the values ride the styles under one reserved key; Section.astro reads and strips it.
|
|
28
|
+
const styles = { ...(node.styles ?? {}), [SMD_KEY]: { node: node.id, css: typeof node.css === "string" ? node.css : undefined, values: content.styleValues } };
|
|
29
|
+
// Settings complete, as pair() hands them to a React view: the defaults under what the editor stored.
|
|
30
|
+
items.push({ kind: "section", id: node.id, type: node.type, title, props: { ...node.fields, settings: { ...defaults, ...(node.settings ?? {}) }, styles } });
|
|
31
|
+
}
|
|
32
|
+
return items;
|
|
33
|
+
}
|
package/dist/bind.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
import type { ResolvedConfig } from "./config.ts";
|
|
3
|
+
import type { SiteOptions } from "./site.ts";
|
|
4
|
+
import { SupabaseAssetStore } from "./supabase-asset-store.ts";
|
|
5
|
+
import { SupabaseEntryStore } from "./supabase-entry-store.ts";
|
|
6
|
+
import { SupabaseMemberStore } from "./supabase-member-store.ts";
|
|
7
|
+
import { SupabasePathIndex } from "./supabase-path-index.ts";
|
|
8
|
+
import { SupabasePublishedStore } from "./supabase-published-store.ts";
|
|
9
|
+
import { SupabasePageStore } from "./supabase-store.ts";
|
|
10
|
+
export declare function createSupabaseStores(db: SupabaseClient, config: ResolvedConfig): {
|
|
11
|
+
paths: SupabasePathIndex;
|
|
12
|
+
pages: SupabasePageStore;
|
|
13
|
+
entries: SupabaseEntryStore;
|
|
14
|
+
assets: SupabaseAssetStore;
|
|
15
|
+
members: SupabaseMemberStore;
|
|
16
|
+
published: SupabasePublishedStore;
|
|
17
|
+
};
|
|
18
|
+
/** What createSmoodlySite takes, read off the config. */
|
|
19
|
+
export declare function siteOptions(config: ResolvedConfig, stores: SiteOptions["stores"]): SiteOptions;
|
package/dist/bind.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { SupabaseAssetStore } from "./supabase-asset-store.js";
|
|
2
|
+
import { SupabaseEntryStore } from "./supabase-entry-store.js";
|
|
3
|
+
import { SupabaseMemberStore } from "./supabase-member-store.js";
|
|
4
|
+
import { SupabasePathIndex } from "./supabase-path-index.js";
|
|
5
|
+
import { SupabasePublishedStore } from "./supabase-published-store.js";
|
|
6
|
+
import { SupabasePageStore } from "./supabase-store.js";
|
|
7
|
+
export function createSupabaseStores(db, config) {
|
|
8
|
+
const { sections, elements, collections, globals } = config.registry;
|
|
9
|
+
const locales = config.locales;
|
|
10
|
+
// ONE path index for both stores: pages and entries share the URL space.
|
|
11
|
+
const paths = new SupabasePathIndex(db, config.anchors);
|
|
12
|
+
const entries = new SupabaseEntryStore(db, collections, { paths, locales, globals, elements: elements });
|
|
13
|
+
const pages = new SupabasePageStore(db, {
|
|
14
|
+
sections: sections, elements: elements, entries, paths, locales,
|
|
15
|
+
custom: config.custom,
|
|
16
|
+
homePageSlug: config.homePageSlug,
|
|
17
|
+
addresses: config.addresses,
|
|
18
|
+
tree: config.pages?.tree,
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
paths, pages, entries, assets: new SupabaseAssetStore(db), members: new SupabaseMemberStore(db),
|
|
22
|
+
published: new SupabasePublishedStore(db, collections),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** What createSmoodlySite takes, read off the config. */
|
|
26
|
+
export function siteOptions(config, stores) {
|
|
27
|
+
const { sections, elements, collections, globals, pages } = config.registry;
|
|
28
|
+
return {
|
|
29
|
+
stores, collections, globals,
|
|
30
|
+
// defineConfig's checks guarantee a section and an element carry `fields`.
|
|
31
|
+
sections: sections, elements: elements,
|
|
32
|
+
locales: config.locales, homePageSlug: config.homePageSlug, styles: config.styles, custom: config.custom, pages,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type Bridge = {
|
|
2
|
+
/** New markup is in the DOM: the adapter calls it after each refresh that is not a reload. */
|
|
3
|
+
rendered(): void;
|
|
4
|
+
stop(): void;
|
|
5
|
+
};
|
|
6
|
+
/** `refresh` is the framework's way to render the page again in place; the default reloads the frame. */
|
|
7
|
+
export declare function startBridge(options?: {
|
|
8
|
+
refresh?: () => void;
|
|
9
|
+
}): Bridge;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// The editor bridge, plain (spec 2026-10-02 astro cloud fit §5): no React, no framework. It runs INSIDE
|
|
2
|
+
// the canvas frame — the site's own draft route — and only there. It reports geometry (section nodes,
|
|
3
|
+
// empty zones and, since spec 2026-09-10, every marked field with its resolved path), forwards select
|
|
4
|
+
// and hover with the field under the pointer, refreshes on request and says when a render has
|
|
5
|
+
// committed. The admin overlay draws all chrome, bar one rule that ships as BRIDGE_CSS (marks.ts): a section that
|
|
6
|
+
// renders nothing is stamped blank and takes a min-height, so the editor can see and select it; the
|
|
7
|
+
// live site never runs the bridge, so it keeps its true height. Rects are in document coordinates: the
|
|
8
|
+
// editor sizes the frame to the full document height, so client rects + scroll offsets map 1:1 onto
|
|
9
|
+
// the overlay. Each adapter starts it: Next from a wrapper that passes the router's refresh
|
|
10
|
+
// (next/bridge.tsx), Astro as a script.
|
|
11
|
+
import { toBridgeMessage } from "../admin/editor/protocol.js";
|
|
12
|
+
import { BLANK_ATTR, CRASHED_ATTR, EDITOR_ATTR, FIELD_ATTR, NODE_ATTR, ZONE_ATTR, crashedOf, resolveMarkPath } from "../marks.js";
|
|
13
|
+
const MEDIA = "img,picture,video,audio,svg,canvas,iframe,object";
|
|
14
|
+
/** No visible text (innerText skips <style> and hidden content) and no media. */
|
|
15
|
+
const isBlank = (el) => el.innerText.trim() === "" && el.querySelector(MEDIA) === null;
|
|
16
|
+
/** `refresh` is the framework's way to render the page again in place; the default reloads the frame. */
|
|
17
|
+
export function startBridge(options = {}) {
|
|
18
|
+
const refresh = options.refresh ?? (() => window.location.reload());
|
|
19
|
+
// To the framing window, at this origin: a canvas that was navigated elsewhere tells nobody anything.
|
|
20
|
+
const post = (m) => window.parent.postMessage(m, window.location.origin);
|
|
21
|
+
const docRect = (el) => {
|
|
22
|
+
const r = el.getBoundingClientRect();
|
|
23
|
+
return { x: r.left + window.scrollX, y: r.top + window.scrollY, w: r.width, h: r.height };
|
|
24
|
+
};
|
|
25
|
+
// Array.from, not spread: the package's lib set is ES2020 + DOM (no DOM.Iterable).
|
|
26
|
+
const all = (selector) => Array.from(document.querySelectorAll(selector));
|
|
27
|
+
const inEditor = (el) => Boolean(el?.closest?.(`[${EDITOR_ATTR}]`));
|
|
28
|
+
const report = () => {
|
|
29
|
+
const roots = all(`[${NODE_ATTR}]`);
|
|
30
|
+
// Stamp before measuring, so the min-height is in the rects; only on a
|
|
31
|
+
// change, so the attribute mutation this causes converges instead of looping.
|
|
32
|
+
for (const el of roots) {
|
|
33
|
+
const blank = isBlank(el);
|
|
34
|
+
if (blank !== el.hasAttribute(BLANK_ATTR)) {
|
|
35
|
+
if (blank)
|
|
36
|
+
el.setAttribute(BLANK_ATTR, "");
|
|
37
|
+
else
|
|
38
|
+
el.removeAttribute(BLANK_ATTR);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const nodes = roots.map((el) => ({ id: el.getAttribute(NODE_ATTR), ...docRect(el) }));
|
|
42
|
+
const emptyZones = all(`[${ZONE_ATTR}]`).map((el) => ({ zone: el.getAttribute(ZONE_ATTR), ...docRect(el) }));
|
|
43
|
+
const fields = [];
|
|
44
|
+
for (const el of all(`[${FIELD_ATTR}]`)) {
|
|
45
|
+
// A mark inside the admin's own editor root (a nested element card) is not the page's.
|
|
46
|
+
if (inEditor(el))
|
|
47
|
+
continue;
|
|
48
|
+
const mark = resolveMarkPath(el);
|
|
49
|
+
if (mark)
|
|
50
|
+
fields.push({ ...mark, ...docRect(el) });
|
|
51
|
+
}
|
|
52
|
+
// Rounded up from the fractional box, so the frame is never a hair too short.
|
|
53
|
+
const docH = Math.ceil(Math.max(document.documentElement.scrollHeight, document.documentElement.getBoundingClientRect().height));
|
|
54
|
+
// Crashed sections (spec 2026-09-27 section guard §6): the placeholders the guard and the canvas net left.
|
|
55
|
+
const crashed = crashedOf(all(`[${CRASHED_ATTR}]`));
|
|
56
|
+
post({ source: "smoodly", kind: "rects", nodes, emptyZones, fields, docH, crashed });
|
|
57
|
+
};
|
|
58
|
+
const raf = () => requestAnimationFrame(report);
|
|
59
|
+
const ro = new ResizeObserver(raf);
|
|
60
|
+
ro.observe(document.documentElement);
|
|
61
|
+
// characterData too: a text session edits a text node in place, and the outline must follow.
|
|
62
|
+
const mo = new MutationObserver(raf);
|
|
63
|
+
mo.observe(document.body, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
64
|
+
const elementOf = (target) => target;
|
|
65
|
+
const nodeIdAt = (target) => elementOf(target)?.closest?.(`[${NODE_ATTR}]`)?.getAttribute(NODE_ATTR) ?? null;
|
|
66
|
+
const pathAt = (target) => {
|
|
67
|
+
const el = elementOf(target)?.closest?.(`[${FIELD_ATTR}]`);
|
|
68
|
+
return el ? resolveMarkPath(el)?.path ?? null : null;
|
|
69
|
+
};
|
|
70
|
+
const onClick = (e) => {
|
|
71
|
+
// The canvas must never navigate away from under the editor.
|
|
72
|
+
if (elementOf(e.target)?.closest?.("a"))
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
// Inside its own editor root the admin owns every click.
|
|
75
|
+
if (inEditor(elementOf(e.target)))
|
|
76
|
+
return;
|
|
77
|
+
post({ source: "smoodly", kind: "select", id: nodeIdAt(e.target), path: pathAt(e.target), x: e.clientX, y: e.clientY });
|
|
78
|
+
};
|
|
79
|
+
const onMove = (e) => {
|
|
80
|
+
if (inEditor(elementOf(e.target)))
|
|
81
|
+
return;
|
|
82
|
+
post({ source: "smoodly", kind: "hover", id: nodeIdAt(e.target), path: pathAt(e.target) });
|
|
83
|
+
};
|
|
84
|
+
const onMessage = (e) => {
|
|
85
|
+
if (toBridgeMessage(e, window))
|
|
86
|
+
refresh();
|
|
87
|
+
};
|
|
88
|
+
document.addEventListener("click", onClick, true);
|
|
89
|
+
document.addEventListener("mousemove", onMove, true);
|
|
90
|
+
window.addEventListener("message", onMessage);
|
|
91
|
+
// The mount is a committed render too (spec 2026-09-10 §5: the inline richtext editor lets go of its
|
|
92
|
+
// container only when the editor hears one).
|
|
93
|
+
post({ source: "smoodly", kind: "rendered" });
|
|
94
|
+
report();
|
|
95
|
+
const late = setTimeout(report, 500); // fonts/images settle
|
|
96
|
+
return {
|
|
97
|
+
rendered: () => post({ source: "smoodly", kind: "rendered" }),
|
|
98
|
+
stop() {
|
|
99
|
+
ro.disconnect();
|
|
100
|
+
mo.disconnect();
|
|
101
|
+
clearTimeout(late);
|
|
102
|
+
document.removeEventListener("click", onClick, true);
|
|
103
|
+
document.removeEventListener("mousemove", onMove, true);
|
|
104
|
+
window.removeEventListener("message", onMessage);
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// The bridge as a script file (spec 2026-10-02 astro cloud fit §5), for an adapter that appends a script
|
|
2
|
+
// tag to a preview response: Astro's. It starts with the default refresh, a reload of the frame. Loaded
|
|
3
|
+
// as a module script, so the body exists when it runs.
|
|
4
|
+
import { startBridge } from "./bridge.js";
|
|
5
|
+
startBridge();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const PILL_TAG = "smoodly-preview-pill";
|
|
2
|
+
declare const POSITIONS: readonly ["left", "center", "right"];
|
|
3
|
+
export type PillPosition = (typeof POSITIONS)[number];
|
|
4
|
+
/** Where Exit goes: the preview route's `disable`, then back to the page of the moment. */
|
|
5
|
+
export declare function exitUrl(route: string, loc: {
|
|
6
|
+
pathname: string;
|
|
7
|
+
search: string;
|
|
8
|
+
}): string;
|
|
9
|
+
/** One place left or right, stopping at the ends. */
|
|
10
|
+
export declare function stepPosition(from: PillPosition, by: -1 | 1): PillPosition;
|
|
11
|
+
/** The third of the window a drag ended in. */
|
|
12
|
+
export declare function positionAt(x: number, width: number): PillPosition;
|
|
13
|
+
/** What the pill reads of the window: the test seam. */
|
|
14
|
+
export type PillEnv = Pick<Window & typeof globalThis, "customElements" | "HTMLElement" | "localStorage" | "location" | "innerWidth">;
|
|
15
|
+
/** Define the element, once per document. `closed` lives in the closure, so a hidden pill stays hidden
|
|
16
|
+
* across client navigations and returns on a reload. */
|
|
17
|
+
export declare function definePreviewPill(env?: PillEnv): void;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// The preview pill, plain (spec 2026-09-29 pre-install fixes §2; plan 2026-10-03 plain bridge): while
|
|
2
|
+
// draft mode is on, outside the editor's canvas, a floating pill says the page shows unpublished changes
|
|
3
|
+
// and offers Exit preview. A custom element with its styles in a Shadow DOM — no React, no framework.
|
|
4
|
+
// The page's server renders the element in draft mode only; each adapter defines it: Next from a lazy
|
|
5
|
+
// chunk behind the canvas stub (next/preview-pill-define.tsx), Astro as a script. The route rides on the
|
|
6
|
+
// element, so nothing is interpolated into code.
|
|
7
|
+
export const PILL_TAG = "smoodly-preview-pill";
|
|
8
|
+
const POSITIONS = ["left", "center", "right"];
|
|
9
|
+
const KEY = "smoodly-pill-position";
|
|
10
|
+
/** Where Exit goes: the preview route's `disable`, then back to the page of the moment. */
|
|
11
|
+
export function exitUrl(route, loc) {
|
|
12
|
+
return `${route}${route.includes("?") ? "&" : "?"}disable&path=${encodeURIComponent(loc.pathname + loc.search)}`;
|
|
13
|
+
}
|
|
14
|
+
/** One place left or right, stopping at the ends. */
|
|
15
|
+
export function stepPosition(from, by) {
|
|
16
|
+
return POSITIONS[Math.min(POSITIONS.length - 1, Math.max(0, POSITIONS.indexOf(from) + by))];
|
|
17
|
+
}
|
|
18
|
+
/** The third of the window a drag ended in. */
|
|
19
|
+
export function positionAt(x, width) {
|
|
20
|
+
const at = x / width;
|
|
21
|
+
return at < 1 / 3 ? "left" : at > 2 / 3 ? "right" : "center";
|
|
22
|
+
}
|
|
23
|
+
const CSS = ":host{all:initial}:host([hidden]){display:none}" +
|
|
24
|
+
".pill{position:fixed;bottom:16px;z-index:2147483647;display:flex;align-items:center;gap:8px;padding:6px 6px 6px 14px;" +
|
|
25
|
+
"border-radius:999px;background:#151727;color:#fff;font:500 13px/1.2 system-ui,-apple-system,sans-serif;" +
|
|
26
|
+
"box-shadow:0 4px 16px rgba(0,0,0,.25);cursor:grab;user-select:none;touch-action:none}" +
|
|
27
|
+
// The ring must show on any page: a dark outline for a light one, a white halo under it for a dark one.
|
|
28
|
+
".pill:focus-visible{outline:2px solid #151727;outline-offset:2px;box-shadow:0 0 0 2px #fff,0 4px 16px rgba(0,0,0,.25)}" +
|
|
29
|
+
".pill[data-pos=left]{left:16px}.pill[data-pos=center]{left:50%;transform:translateX(-50%)}.pill[data-pos=right]{right:16px}" +
|
|
30
|
+
".pill.dragging{cursor:grabbing}" +
|
|
31
|
+
"a{color:#151727;background:#fff;border-radius:999px;padding:5px 10px;text-decoration:none;font-weight:600}" +
|
|
32
|
+
"button{all:unset;cursor:pointer;width:24px;height:24px;border-radius:999px;display:grid;place-items:center;color:#fff;opacity:.7}" +
|
|
33
|
+
"button:hover,button:focus-visible{opacity:1;outline:2px solid #fff}";
|
|
34
|
+
const HTML = '<div class="pill" role="region" aria-label="Smoodly preview" tabindex="0" title="Drag it, or press the left and right arrow keys, to move it">' +
|
|
35
|
+
"<span>Preview · unpublished changes</span>" +
|
|
36
|
+
"<a>Exit preview</a>" +
|
|
37
|
+
'<button type="button" aria-label="Hide until reload">×</button>' +
|
|
38
|
+
"</div>";
|
|
39
|
+
/** Define the element, once per document. `closed` lives in the closure, so a hidden pill stays hidden
|
|
40
|
+
* across client navigations and returns on a reload. */
|
|
41
|
+
export function definePreviewPill(env = window) {
|
|
42
|
+
if (env.customElements.get(PILL_TAG))
|
|
43
|
+
return;
|
|
44
|
+
let closed = false;
|
|
45
|
+
// Private mode and blocked site data both throw; the pill still works and simply forgets its place.
|
|
46
|
+
const load = () => {
|
|
47
|
+
try {
|
|
48
|
+
const v = env.localStorage.getItem(KEY);
|
|
49
|
+
return v === "left" || v === "right" ? v : "center";
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return "center";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const save = (v) => { try {
|
|
56
|
+
env.localStorage.setItem(KEY, v);
|
|
57
|
+
}
|
|
58
|
+
catch { /* not remembered */ } };
|
|
59
|
+
class Pill extends env.HTMLElement {
|
|
60
|
+
connectedCallback() {
|
|
61
|
+
if (closed) {
|
|
62
|
+
this.hidden = true;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (this.shadowRoot)
|
|
66
|
+
return;
|
|
67
|
+
const root = this.attachShadow({ mode: "open" });
|
|
68
|
+
root.innerHTML = `<style>${CSS}</style>${HTML}`;
|
|
69
|
+
const pill = root.querySelector(".pill");
|
|
70
|
+
const link = root.querySelector("a");
|
|
71
|
+
const close = root.querySelector("button");
|
|
72
|
+
const place = (pos, remember) => { pill.setAttribute("data-pos", pos); if (remember)
|
|
73
|
+
save(pos); };
|
|
74
|
+
const exit = () => exitUrl(this.getAttribute("data-route") || "/api/preview", env.location);
|
|
75
|
+
place(load(), false);
|
|
76
|
+
// A real link, so it opens like any other. The address moves under a client navigation while this
|
|
77
|
+
// element stays, so it is read again when the link is about to be used.
|
|
78
|
+
const fresh = () => link.setAttribute("href", exit());
|
|
79
|
+
fresh();
|
|
80
|
+
link.addEventListener("pointerenter", fresh);
|
|
81
|
+
link.addEventListener("focus", fresh);
|
|
82
|
+
link.addEventListener("click", (e) => {
|
|
83
|
+
// A modified click (a new tab, a new window) is the browser's: the link's href is already fresh.
|
|
84
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button)
|
|
85
|
+
return;
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
env.location.href = exit();
|
|
88
|
+
});
|
|
89
|
+
close.addEventListener("click", () => { closed = true; this.hidden = true; });
|
|
90
|
+
pill.addEventListener("keydown", (e) => {
|
|
91
|
+
// With a modifier the key is the browser's: Alt+Left is Back.
|
|
92
|
+
if ((e.key !== "ArrowLeft" && e.key !== "ArrowRight") || e.altKey || e.metaKey || e.ctrlKey || e.shiftKey)
|
|
93
|
+
return;
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
place(stepPosition(pill.getAttribute("data-pos") ?? "center", e.key === "ArrowLeft" ? -1 : 1), true);
|
|
96
|
+
});
|
|
97
|
+
let start = null;
|
|
98
|
+
const end = () => { start = null; pill.classList.remove("dragging"); pill.style.translate = ""; };
|
|
99
|
+
pill.addEventListener("pointerdown", (e) => {
|
|
100
|
+
if (e.target.closest("a,button"))
|
|
101
|
+
return;
|
|
102
|
+
start = e.clientX;
|
|
103
|
+
pill.setPointerCapture(e.pointerId);
|
|
104
|
+
pill.classList.add("dragging");
|
|
105
|
+
});
|
|
106
|
+
pill.addEventListener("pointermove", (e) => { if (start !== null)
|
|
107
|
+
pill.style.translate = `${e.clientX - start}px 0`; });
|
|
108
|
+
pill.addEventListener("pointerup", (e) => { if (start === null)
|
|
109
|
+
return; end(); place(positionAt(e.clientX, env.innerWidth), true); });
|
|
110
|
+
pill.addEventListener("pointercancel", () => { if (start !== null)
|
|
111
|
+
end(); });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
env.customElements.define(PILL_TAG, Pill);
|
|
115
|
+
}
|
package/dist/cli/import.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { type ContentTable } from "../sql-space.ts";
|
|
2
2
|
import { type SpaceConnection } from "./space-io.ts";
|
|
3
|
+
import type { ResolvedConfig } from "../config.ts";
|
|
3
4
|
export type ImportArgs = {
|
|
4
5
|
dir: string;
|
|
5
6
|
};
|
|
6
7
|
export type ImportDeps = {
|
|
7
8
|
cwd?: string;
|
|
8
9
|
connection?: SpaceConnection;
|
|
10
|
+
/** The app's config, which the closing rebuild assembles with; default: loaded from the folder (load-config.ts). */
|
|
11
|
+
config?: ResolvedConfig;
|
|
9
12
|
batchSize?: number;
|
|
10
13
|
/** Test seam: runs before each batch; a throw stops the import there. */
|
|
11
14
|
beforeBatch?: (table: ContentTable, index: number) => void;
|