create-smoodly-app 0.0.16 → 0.0.17
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/package.json +1 -1
- package/templates/blank/AGENTS.md +2 -2
- package/templates/blank/README.md +5 -5
- package/templates/blank/app/(site)/layout.tsx +3 -3
- package/templates/blank/app/globals.css +19 -9
- package/templates/blank/components/pages/Home.tsx +1 -1
- package/templates/blank/components/pages/Page.tsx +1 -1
- package/templates/blank/components/sections/Text.tsx +8 -4
- package/templates/blank/package.json +1 -1
- package/templates/blank/scripts/seed.ts +14 -2
- package/templates/blank/smoodly/config.ts +4 -11
- package/templates/blank/smoodly/{shared.ts → globals.ts} +4 -4
- package/templates/blank/smoodly/server.ts +9 -6
- package/templates/blank/supabase/migrations/00000000000000_smoodly.sql +1 -1
- package/templates/blank-intl/AGENTS.md +2 -2
- package/templates/blank-intl/README.md +5 -5
- package/templates/blank-intl/app/[locale]/layout.tsx +3 -3
- package/templates/blank-intl/app/globals.css +19 -9
- package/templates/blank-intl/components/pages/Home.tsx +1 -1
- package/templates/blank-intl/components/pages/Page.tsx +1 -1
- package/templates/blank-intl/components/sections/Text.tsx +8 -4
- package/templates/blank-intl/package.json +1 -1
- package/templates/blank-intl/scripts/seed.ts +14 -2
- package/templates/blank-intl/smoodly/config.ts +4 -11
- package/templates/blank-intl/smoodly/{shared.ts → globals.ts} +4 -4
- package/templates/blank-intl/smoodly/server.ts +9 -6
- package/templates/blank-intl/supabase/migrations/00000000000000_smoodly.sql +1 -1
- package/templates/lorem/AGENTS.md +2 -2
- package/templates/lorem/README.md +4 -4
- package/templates/lorem/app/(site)/layout.tsx +4 -4
- package/templates/lorem/app/globals.css +53 -44
- package/templates/lorem/components/elements/Callout.tsx +3 -3
- package/templates/lorem/components/elements/Cta.tsx +4 -4
- package/templates/lorem/components/pages/Home.tsx +4 -4
- package/templates/lorem/components/pages/Page.tsx +1 -1
- package/templates/lorem/components/sections/Footer.tsx +4 -2
- package/templates/lorem/components/sections/Header.tsx +4 -2
- package/templates/lorem/components/sections/ImageText.tsx +5 -5
- package/templates/lorem/components/sections/Quote.tsx +9 -6
- package/templates/lorem/package.json +1 -1
- package/templates/lorem/scripts/seed.ts +28 -9
- package/templates/lorem/smoodly/config.ts +18 -11
- package/templates/lorem/smoodly/{shared.ts → globals.ts} +7 -7
- package/templates/lorem/smoodly/sections.ts +3 -3
- package/templates/lorem/smoodly/server.ts +9 -6
- package/templates/lorem/supabase/migrations/00000000000000_smoodly.sql +1 -1
- package/templates/lorem-intl/AGENTS.md +2 -2
- package/templates/lorem-intl/README.md +4 -4
- package/templates/lorem-intl/app/[locale]/layout.tsx +4 -4
- package/templates/lorem-intl/app/globals.css +53 -44
- package/templates/lorem-intl/components/elements/Callout.tsx +3 -3
- package/templates/lorem-intl/components/elements/Cta.tsx +4 -4
- package/templates/lorem-intl/components/pages/Home.tsx +4 -4
- package/templates/lorem-intl/components/pages/Page.tsx +1 -1
- package/templates/lorem-intl/components/sections/Footer.tsx +4 -2
- package/templates/lorem-intl/components/sections/Header.tsx +4 -2
- package/templates/lorem-intl/components/sections/ImageText.tsx +5 -5
- package/templates/lorem-intl/components/sections/Quote.tsx +9 -6
- package/templates/lorem-intl/package.json +1 -1
- package/templates/lorem-intl/scripts/seed.ts +28 -9
- package/templates/lorem-intl/smoodly/config.ts +18 -11
- package/templates/lorem-intl/smoodly/{shared.ts → globals.ts} +7 -7
- package/templates/lorem-intl/smoodly/sections.ts +3 -3
- package/templates/lorem-intl/smoodly/server.ts +9 -6
- package/templates/lorem-intl/supabase/migrations/00000000000000_smoodly.sql +1 -1
package/package.json
CHANGED
|
@@ -8,8 +8,8 @@ where things are.
|
|
|
8
8
|
## Rules
|
|
9
9
|
|
|
10
10
|
- **Register everything in `smoodly/`.** Every section, element,
|
|
11
|
-
collection and
|
|
12
|
-
`elements.ts`, `collections.ts`, `
|
|
11
|
+
collection and global item is listed in its barrel (`sections.ts`,
|
|
12
|
+
`elements.ts`, `collections.ts`, `globals.ts`) and `smoodly/config.ts`
|
|
13
13
|
is the one registry. A component that is not registered does not exist
|
|
14
14
|
for the admin.
|
|
15
15
|
- **A new collection is a new migration.** Write its view with
|
|
@@ -14,7 +14,7 @@ for an agent (or a person) building the site.
|
|
|
14
14
|
- `components/pages/` — the page shapes and the entry page: `Home` (fixed at /, one freeform zone), `Page` (open, editors create many), `Post` (the entry page for posts).
|
|
15
15
|
- `components/sections/Text.tsx` — the one section: a title and a richtext body. Listed in `smoodly/sections.ts`.
|
|
16
16
|
- `components/elements/Button.tsx` — the one element: a link styled as a button, embedded in richtext through `smoodly/toolsets.ts`.
|
|
17
|
-
- `smoodly/` — the wiring: `config.ts` (the registry and settings), `collections.ts` (posts), `
|
|
17
|
+
- `smoodly/` — the wiring: `config.ts` (the registry and settings), `collections.ts` (posts), `globals.ts` (the site item), `sections.ts`, `elements.ts`, `toolsets.ts`, `server.ts` (stores, site layer, renderer, the read API `q`), `ops.ts` (the admin's one server action), `admin.tsx` (the admin mount), `richtext.tsx` (the bound renderer).
|
|
18
18
|
- `scripts/seed.ts` — the home page and the site item, written through the stores. `npm run seed` is idempotent.
|
|
19
19
|
- `app/globals.css` — a reset, a container, the two tones. Replace it.
|
|
20
20
|
- `.env.local` — `SMOODLY_URL`, `SMOODLY_SECRET_KEY`, `SMOODLY_PUBLISHABLE_KEY`.
|
|
@@ -48,12 +48,12 @@ Then create a migration with its view:
|
|
|
48
48
|
Save the output as `supabase/migrations/<timestamp>_people.sql` and apply
|
|
49
49
|
it with `supabase db push` (hosted) or `supabase db reset` (local).
|
|
50
50
|
|
|
51
|
-
## Add a
|
|
51
|
+
## Add a global item
|
|
52
52
|
|
|
53
|
-
Copy the `site` block in `smoodly/
|
|
54
|
-
object item (`fields`) is read in code with `q().
|
|
53
|
+
Copy the `site` block in `smoodly/globals.ts` and add it to `globals`. An
|
|
54
|
+
object item (`fields`) is read in code with `q().getGlobal("name")`; a
|
|
55
55
|
visual item (`section: SomeSection`) renders through its section and can
|
|
56
|
-
be placed on pages from the picker's
|
|
56
|
+
be placed on pages from the picker's Global group. Edit both under
|
|
57
57
|
Globals in the admin.
|
|
58
58
|
|
|
59
59
|
Reseed an empty database any time with `npm run seed`.
|
|
@@ -4,16 +4,16 @@ import "../globals.css";
|
|
|
4
4
|
|
|
5
5
|
// The site's root layout, in its own route group so the admin's layout
|
|
6
6
|
// under app/(smoodly)/admin is a second root layout beside it rather than
|
|
7
|
-
// a child of this one. The `site`
|
|
7
|
+
// a child of this one. The `site` global item (smoodly/globals.ts) gives
|
|
8
8
|
// the document title and the footer line: read through `q`, cached under
|
|
9
9
|
// its tag, null until published.
|
|
10
10
|
export async function generateMetadata() {
|
|
11
|
-
const site = await q().
|
|
11
|
+
const site = await q().getGlobal("site");
|
|
12
12
|
return { title: site?.name ?? "Smoodly site" };
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export default async function RootLayout({ children }: { children: ReactNode }) {
|
|
16
|
-
const site = await q().
|
|
16
|
+
const site = await q().getGlobal("site");
|
|
17
17
|
return (
|
|
18
18
|
<html lang="en">
|
|
19
19
|
<body>
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
/* The blank starter's one stylesheet: a reset, a system font, a
|
|
2
|
-
container, prose defaults, the button element, and the two
|
|
3
|
-
|
|
2
|
+
container, prose defaults, the button element, and the two backgrounds
|
|
3
|
+
the Text section's setting selects. The editor's Spacing & layout
|
|
4
|
+
steps (Smoodly's styles set, smoodly/config.ts) render inline through
|
|
5
|
+
the section wrapper and read the --space-* / --width-* variables.
|
|
6
|
+
Replace freely. */
|
|
4
7
|
|
|
5
8
|
:root {
|
|
9
|
+
/* Smoodly's styles set reads these (smoodly/config.ts): editor steps become padding, width, alignment. */
|
|
10
|
+
--space-sm: 1.5rem; --space-md: 3rem; --space-lg: 4.5rem;
|
|
11
|
+
--width-narrow: 40rem; --width-wide: 90rem;
|
|
12
|
+
|
|
6
13
|
--paper: #ffffff;
|
|
7
14
|
--ink: #1a1a1a;
|
|
8
15
|
--muted: #666666;
|
|
9
16
|
--container: 720px;
|
|
17
|
+
--smd-gutter: 1rem; /* the custom section's rows share the container's side gutter */
|
|
10
18
|
}
|
|
19
|
+
@media (min-width: 768px) { :root { --space-sm: 2rem; --space-md: 4rem; --space-lg: 6rem; } }
|
|
11
20
|
|
|
12
21
|
* { box-sizing: border-box; }
|
|
13
22
|
html, body { margin: 0; padding: 0; }
|
|
@@ -23,15 +32,16 @@ body {
|
|
|
23
32
|
main { flex: 1; }
|
|
24
33
|
a { color: inherit; }
|
|
25
34
|
|
|
26
|
-
|
|
35
|
+
/* The container reads the editor's width step; without one, the design's width. */
|
|
36
|
+
.container { width: min(var(--smd-width, var(--container)), 100% - 2rem); margin-inline: auto; }
|
|
27
37
|
|
|
28
|
-
/*
|
|
29
|
-
|
|
30
|
-
[data-
|
|
31
|
-
|
|
38
|
+
/* The Text section: its own padding, and the background its setting selects. */
|
|
39
|
+
.text { padding-block: 3rem; }
|
|
40
|
+
[data-background="paper"] { background: var(--paper); color: var(--ink); }
|
|
41
|
+
[data-background="ink"] { background: var(--ink); color: var(--paper); }
|
|
32
42
|
|
|
33
|
-
.prose > * + * { margin-top: 1em; }
|
|
34
|
-
.prose h1, .prose h2, .prose h3 { line-height: 1.2; }
|
|
43
|
+
.prose > * + *, .smd-col > * + * { margin-top: 1em; } /* the custom section's columns read like prose */
|
|
44
|
+
.prose h1, .prose h2, .prose h3, .smd-col h1, .smd-col h2, .smd-col h3 { line-height: 1.2; }
|
|
35
45
|
|
|
36
46
|
/* The Button element, inside richtext. */
|
|
37
47
|
.button {
|
|
@@ -5,8 +5,9 @@ import { text } from "@/smoodly/toolsets";
|
|
|
5
5
|
// A section: a title and a richtext body. Registered in
|
|
6
6
|
// smoodly/sections.ts and allowed in the page shapes' zones
|
|
7
7
|
// (components/pages/*.tsx). Copy this file to add another section.
|
|
8
|
-
// The section wrapper renders the root element
|
|
9
|
-
//
|
|
8
|
+
// The section wrapper renders the root element, Smoodly's styles set
|
|
9
|
+
// inline (spacing, width, align) and the node id; a setting the CSS
|
|
10
|
+
// reads — here `background` — is the view's own data attribute.
|
|
10
11
|
export const textSchema = smoodly.schema.section({
|
|
11
12
|
name: "text",
|
|
12
13
|
title: "Text",
|
|
@@ -15,6 +16,9 @@ export const textSchema = smoodly.schema.section({
|
|
|
15
16
|
title: f.text(),
|
|
16
17
|
body: f.richtext({ toolset: text }),
|
|
17
18
|
},
|
|
19
|
+
settings: {
|
|
20
|
+
background: f.radio([{ value: "paper", label: "Paper" }, { value: "ink", label: "Ink" }]).default("paper"),
|
|
21
|
+
},
|
|
18
22
|
sample: {
|
|
19
23
|
title: "Hello",
|
|
20
24
|
body: { type: "doc", content: [{ type: "paragraph", content: [{ type: "text", text: "Write something here." }] }] },
|
|
@@ -25,9 +29,9 @@ export const textSchema = smoodly.schema.section({
|
|
|
25
29
|
// select and inline-edit them on the canvas.
|
|
26
30
|
const m = smoodly.marks(textSchema);
|
|
27
31
|
|
|
28
|
-
function TextView({ title, body, styles }: Props<typeof textSchema>) {
|
|
32
|
+
function TextView({ title, body, settings, styles }: Props<typeof textSchema>) {
|
|
29
33
|
return (
|
|
30
|
-
<smoodly.Section styles={styles}>
|
|
34
|
+
<smoodly.Section styles={styles} className="text" data-background={settings.background}>
|
|
31
35
|
<div className="container prose">
|
|
32
36
|
<h2 {...m.field("title")}>{title}</h2>
|
|
33
37
|
<div {...m.field("body")}><RichText doc={body} /></div>
|
|
@@ -26,7 +26,7 @@ if (await pages.resolvePath(DEFAULT, "/")) {
|
|
|
26
26
|
// Richtext is TipTap JSON, the stored format; RichText renders it.
|
|
27
27
|
type Node = Record<string, unknown>;
|
|
28
28
|
const p = (s: string): Node => ({ type: "paragraph", content: [{ type: "text", text: s }] });
|
|
29
|
-
const button = (label: string, href: string): Node => ({ type: "element", attrs: { name: "button", fields: { label, href },
|
|
29
|
+
const button = (label: string, href: string): Node => ({ type: "element", attrs: { name: "button", fields: { label, href }, settings: {} } });
|
|
30
30
|
const doc = (...content: Node[]) => ({ type: "doc", content });
|
|
31
31
|
|
|
32
32
|
// --- the site item: name and tagline, published in every locale ------
|
|
@@ -38,7 +38,7 @@ for (const locale of OTHERS) {
|
|
|
38
38
|
await entries.setStatus("site", site.id, locale, "published");
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
// --- the home page: one Text section
|
|
41
|
+
// --- the home page: one Text section and one custom section ----------
|
|
42
42
|
|
|
43
43
|
const tree = {
|
|
44
44
|
meta: { description: "A new site, built with Smoodly." },
|
|
@@ -47,6 +47,7 @@ const tree = {
|
|
|
47
47
|
{
|
|
48
48
|
id: "node-hello",
|
|
49
49
|
type: "text",
|
|
50
|
+
settings: { background: "paper" },
|
|
50
51
|
fields: {
|
|
51
52
|
title: "Hello",
|
|
52
53
|
body: doc(
|
|
@@ -56,6 +57,17 @@ const tree = {
|
|
|
56
57
|
),
|
|
57
58
|
},
|
|
58
59
|
},
|
|
60
|
+
// The custom section: the editor's own rows of columns, rendered by Smoodly.
|
|
61
|
+
{
|
|
62
|
+
id: "node-custom",
|
|
63
|
+
type: "custom",
|
|
64
|
+
fields: { rows: [
|
|
65
|
+
{ spans: [6, 6], gap: "md", align: "top", reverse: false, columns: [
|
|
66
|
+
doc(p("A custom section holds rows of columns. Drag the width bar in the inspector to resize them.")),
|
|
67
|
+
doc(p("Each column is richtext. Columns stack on small screens; the developer's stylesheet needs nothing.")),
|
|
68
|
+
] },
|
|
69
|
+
] },
|
|
70
|
+
},
|
|
59
71
|
],
|
|
60
72
|
},
|
|
61
73
|
} as unknown as PageTree;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { defineConfig
|
|
1
|
+
import { defineConfig } from "smoodly";
|
|
2
2
|
import { collections } from "./collections";
|
|
3
3
|
import { sections } from "./sections";
|
|
4
|
-
import {
|
|
4
|
+
import { globals } from "./globals";
|
|
5
5
|
import { elements } from "./elements";
|
|
6
6
|
import { HomePage } from "@/components/pages/Home";
|
|
7
7
|
import { StandardPage } from "@/components/pages/Page";
|
|
8
8
|
import { PostPage } from "@/components/pages/Post";
|
|
9
9
|
|
|
10
|
-
export { collections, sections,
|
|
10
|
+
export { collections, sections, globals, elements };
|
|
11
11
|
|
|
12
12
|
// The registry and the plain settings, one file. Runtime-free: nothing
|
|
13
13
|
// here imports a route file or Next, so a script and the CLI's template
|
|
@@ -17,7 +17,7 @@ export const config = defineConfig({
|
|
|
17
17
|
sections,
|
|
18
18
|
elements,
|
|
19
19
|
collections,
|
|
20
|
-
|
|
20
|
+
globals,
|
|
21
21
|
pages: [HomePage, StandardPage, PostPage],
|
|
22
22
|
},
|
|
23
23
|
locales: { default: "en", supported: ["en"] },
|
|
@@ -25,11 +25,4 @@ export const config = defineConfig({
|
|
|
25
25
|
homePageSlug: "home",
|
|
26
26
|
// Editors can nest pages three deep.
|
|
27
27
|
pages: { tree: { depth: 3 } },
|
|
28
|
-
// Every section gets this select beside its own styles; the section
|
|
29
|
-
// wrapper emits it as data-tone, and app/globals.css styles it.
|
|
30
|
-
styles: {
|
|
31
|
-
sections: {
|
|
32
|
-
tone: f.select(["paper", "ink"]).default("paper"),
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
28
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Global content: code-declared singletons, edited once under Globals
|
|
2
2
|
// and read anywhere. `site` is an object item — fields code consumes;
|
|
3
|
-
// the layout reads it through q().
|
|
3
|
+
// the layout reads it through q().getGlobal("site"). A visual item
|
|
4
4
|
// (`section: SomeSection`) renders through a section instead.
|
|
5
5
|
import { f, smoodly } from "smoodly";
|
|
6
6
|
|
|
7
|
-
export const site = smoodly.
|
|
7
|
+
export const site = smoodly.global({
|
|
8
8
|
name: "site",
|
|
9
9
|
title: "Site",
|
|
10
10
|
fields: {
|
|
@@ -13,4 +13,4 @@ export const site = smoodly.shared({
|
|
|
13
13
|
},
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
export const
|
|
16
|
+
export const globals = [site];
|
|
@@ -14,10 +14,10 @@ import {
|
|
|
14
14
|
import { supabaseAdminAuth, type AdminAuth, type AuthConfig } from "smoodly/admin/next";
|
|
15
15
|
import { createPageRenderer, createSmoodlyQueries, type PageRegistration } from "smoodly/next";
|
|
16
16
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
17
|
-
import { collections, config, elements, sections,
|
|
17
|
+
import { collections, config, elements, sections, globals } from "./config";
|
|
18
18
|
|
|
19
19
|
type Renderer = ReturnType<typeof createPageRenderer>;
|
|
20
|
-
type Queries = ReturnType<typeof createSmoodlyQueries<typeof collections, typeof
|
|
20
|
+
type Queries = ReturnType<typeof createSmoodlyQueries<typeof collections, typeof globals>>;
|
|
21
21
|
type Bound = {
|
|
22
22
|
db: SupabaseClient; pages: SupabasePageStore; entries: SupabaseEntryStore; assets: SupabaseAssetStore; auth: AdminAuth; site: SmoodlySite; renderer: Renderer; q: Queries;
|
|
23
23
|
};
|
|
@@ -31,9 +31,11 @@ function bound(): Bound {
|
|
|
31
31
|
// ONE path index for both stores: pages and entries share the URL space.
|
|
32
32
|
const paths = new SupabasePathIndex(db);
|
|
33
33
|
const locales = config.locales;
|
|
34
|
-
const entries = new SupabaseEntryStore(db, collections, { paths, locales,
|
|
34
|
+
const entries = new SupabaseEntryStore(db, collections, { paths, locales, globals, elements });
|
|
35
35
|
const pages = new SupabasePageStore(db, {
|
|
36
36
|
sections, elements, entries, paths, locales,
|
|
37
|
+
// The custom section's columns toolset, so a column's images count as usage (spec 2026-09-14 §6).
|
|
38
|
+
custom: config.custom,
|
|
37
39
|
homePageSlug: config.homePageSlug,
|
|
38
40
|
tree: config.pages?.tree,
|
|
39
41
|
});
|
|
@@ -46,15 +48,16 @@ function bound(): Bound {
|
|
|
46
48
|
// The site layer: (locale, path) → page or entry, and the content API
|
|
47
49
|
// `q` wraps below. Not exported — the app reads through `q`, which adds
|
|
48
50
|
// draft awareness and the cache.
|
|
49
|
-
const site = createSmoodlySite({ stores, collections,
|
|
51
|
+
const site = createSmoodlySite({ stores, collections, globals, sections, elements, locales, homePageSlug: config.homePageSlug, styles: config.styles, custom: config.custom, pages: config.registry.pages ?? [] });
|
|
50
52
|
const renderer = createPageRenderer({
|
|
51
53
|
site,
|
|
52
54
|
sections,
|
|
55
|
+
elements,
|
|
53
56
|
pages: (config.registry.pages ?? []) as PageRegistration[],
|
|
54
57
|
href,
|
|
55
58
|
});
|
|
56
59
|
// The read API (spec 2026-09-11): typed by the registry, draft-aware, cached under tags the admin expires.
|
|
57
|
-
const q = createSmoodlyQueries({ site, collections,
|
|
60
|
+
const q = createSmoodlyQueries({ site, collections, globals });
|
|
58
61
|
cached = { db, pages, entries, assets, auth, site, renderer, q };
|
|
59
62
|
return cached;
|
|
60
63
|
}
|
|
@@ -83,7 +86,7 @@ export function publicAuth(): AuthConfig {
|
|
|
83
86
|
export const href = (path: string, _locale: string) => path;
|
|
84
87
|
|
|
85
88
|
/** The read API (spec 2026-09-11) for layouts and page views that fetch:
|
|
86
|
-
* `q().getEntries("articles", { locale, limit: 3 })`, `q().
|
|
89
|
+
* `q().getEntries("articles", { locale, limit: 3 })`, `q().getGlobal("header",
|
|
87
90
|
* { locale })`, `q().getPageTree({ locale })`. Typed by the registry, the
|
|
88
91
|
* live draft in the editor preview, cached under tags the admin expires.
|
|
89
92
|
* Rename it if `q` is not to your taste — this file is yours. */
|
|
@@ -110,7 +110,7 @@ create index if not exists "entry_locales_status_idx"
|
|
|
110
110
|
-- A version belongs to ONE locale of one entry and stores the MERGED
|
|
111
111
|
-- fields (shared + localized + slug) as they were at that save, so
|
|
112
112
|
-- restoring an old Finnish version restores the shared fields too.
|
|
113
|
-
-- Rows for every collection and every
|
|
113
|
+
-- Rows for every collection and every global item (spec 2026-09-14 §1).
|
|
114
114
|
-- One row per publish plus the live draft (spec 2026-09-13 history §1).
|
|
115
115
|
create table if not exists "entry_versions" (
|
|
116
116
|
"id" uuid primary key default gen_random_uuid(),
|
|
@@ -8,8 +8,8 @@ where things are.
|
|
|
8
8
|
## Rules
|
|
9
9
|
|
|
10
10
|
- **Register everything in `smoodly/`.** Every section, element,
|
|
11
|
-
collection and
|
|
12
|
-
`elements.ts`, `collections.ts`, `
|
|
11
|
+
collection and global item is listed in its barrel (`sections.ts`,
|
|
12
|
+
`elements.ts`, `collections.ts`, `globals.ts`) and `smoodly/config.ts`
|
|
13
13
|
is the one registry. A component that is not registered does not exist
|
|
14
14
|
for the admin.
|
|
15
15
|
- **A new collection is a new migration.** Write its view with
|
|
@@ -15,7 +15,7 @@ for an agent (or a person) building the site.
|
|
|
15
15
|
- `components/pages/` — the page shapes and the entry page: `Home` (fixed at /, one freeform zone), `Page` (open, editors create many), `Post` (the entry page for posts).
|
|
16
16
|
- `components/sections/Text.tsx` — the one section: a title and a richtext body. Listed in `smoodly/sections.ts`.
|
|
17
17
|
- `components/elements/Button.tsx` — the one element: a link styled as a button, embedded in richtext through `smoodly/toolsets.ts`.
|
|
18
|
-
- `smoodly/` — the wiring: `config.ts` (the registry and settings), `collections.ts` (posts), `
|
|
18
|
+
- `smoodly/` — the wiring: `config.ts` (the registry and settings), `collections.ts` (posts), `globals.ts` (the site item), `sections.ts`, `elements.ts`, `toolsets.ts`, `server.ts` (stores, site layer, renderer, the read API `q`), `ops.ts` (the admin's one server action), `admin.tsx` (the admin mount), `richtext.tsx` (the bound renderer).
|
|
19
19
|
- `scripts/seed.ts` — the home page and the site item, written through the stores. `npm run seed` is idempotent.
|
|
20
20
|
- `app/globals.css` — a reset, a container, the two tones. Replace it.
|
|
21
21
|
- `.env.local` — `SMOODLY_URL`, `SMOODLY_SECRET_KEY`, `SMOODLY_PUBLISHABLE_KEY`.
|
|
@@ -49,12 +49,12 @@ Then create a migration with its view:
|
|
|
49
49
|
Save the output as `supabase/migrations/<timestamp>_people.sql` and apply
|
|
50
50
|
it with `supabase db push` (hosted) or `supabase db reset` (local).
|
|
51
51
|
|
|
52
|
-
## Add a
|
|
52
|
+
## Add a global item
|
|
53
53
|
|
|
54
|
-
Copy the `site` block in `smoodly/
|
|
55
|
-
object item (`fields`) is read in code with `q().
|
|
54
|
+
Copy the `site` block in `smoodly/globals.ts` and add it to `globals`. An
|
|
55
|
+
object item (`fields`) is read in code with `q().getGlobal("name")`; a
|
|
56
56
|
visual item (`section: SomeSection`) renders through its section and can
|
|
57
|
-
be placed on pages from the picker's
|
|
57
|
+
be placed on pages from the picker's Global group. Edit both under
|
|
58
58
|
Globals in the admin.
|
|
59
59
|
|
|
60
60
|
Reseed an empty database any time with `npm run seed`.
|
|
@@ -7,20 +7,20 @@ import "../globals.css";
|
|
|
7
7
|
|
|
8
8
|
// The site's root layout under the locale segment; the admin has its own
|
|
9
9
|
// under app/(smoodly)/admin, outside [locale], as a second root layout.
|
|
10
|
-
// The `site`
|
|
10
|
+
// The `site` global item gives the document title and the footer line,
|
|
11
11
|
// read per locale through `q`, null until published in this locale. The
|
|
12
12
|
// locale links are the visitor's only language switch, since locale
|
|
13
13
|
// detection is off (see i18n/routing.ts).
|
|
14
14
|
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
|
|
15
15
|
const { locale } = await params;
|
|
16
|
-
const site = await q().
|
|
16
|
+
const site = await q().getGlobal("site", { locale });
|
|
17
17
|
return { title: site?.name ?? "Smoodly site" };
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export default async function LocaleLayout({ children, params }: { children: ReactNode; params: Promise<{ locale: string }> }) {
|
|
21
21
|
const { locale } = await params;
|
|
22
22
|
if (!hasLocale(routing.locales, locale)) notFound();
|
|
23
|
-
const site = await q().
|
|
23
|
+
const site = await q().getGlobal("site", { locale });
|
|
24
24
|
const locales = routing.locales.map((code) => ({ code, url: href("/", code), current: code === locale }));
|
|
25
25
|
return (
|
|
26
26
|
<html lang={locale}>
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
/* The blank starter's one stylesheet: a reset, a system font, a
|
|
2
|
-
container, prose defaults, the button element, and the two
|
|
3
|
-
|
|
2
|
+
container, prose defaults, the button element, and the two backgrounds
|
|
3
|
+
the Text section's setting selects. The editor's Spacing & layout
|
|
4
|
+
steps (Smoodly's styles set, smoodly/config.ts) render inline through
|
|
5
|
+
the section wrapper and read the --space-* / --width-* variables.
|
|
6
|
+
Replace freely. */
|
|
4
7
|
|
|
5
8
|
:root {
|
|
9
|
+
/* Smoodly's styles set reads these (smoodly/config.ts): editor steps become padding, width, alignment. */
|
|
10
|
+
--space-sm: 1.5rem; --space-md: 3rem; --space-lg: 4.5rem;
|
|
11
|
+
--width-narrow: 40rem; --width-wide: 90rem;
|
|
12
|
+
|
|
6
13
|
--paper: #ffffff;
|
|
7
14
|
--ink: #1a1a1a;
|
|
8
15
|
--muted: #666666;
|
|
9
16
|
--container: 720px;
|
|
17
|
+
--smd-gutter: 1rem; /* the custom section's rows share the container's side gutter */
|
|
10
18
|
}
|
|
19
|
+
@media (min-width: 768px) { :root { --space-sm: 2rem; --space-md: 4rem; --space-lg: 6rem; } }
|
|
11
20
|
|
|
12
21
|
* { box-sizing: border-box; }
|
|
13
22
|
html, body { margin: 0; padding: 0; }
|
|
@@ -23,15 +32,16 @@ body {
|
|
|
23
32
|
main { flex: 1; }
|
|
24
33
|
a { color: inherit; }
|
|
25
34
|
|
|
26
|
-
|
|
35
|
+
/* The container reads the editor's width step; without one, the design's width. */
|
|
36
|
+
.container { width: min(var(--smd-width, var(--container)), 100% - 2rem); margin-inline: auto; }
|
|
27
37
|
|
|
28
|
-
/*
|
|
29
|
-
|
|
30
|
-
[data-
|
|
31
|
-
|
|
38
|
+
/* The Text section: its own padding, and the background its setting selects. */
|
|
39
|
+
.text { padding-block: 3rem; }
|
|
40
|
+
[data-background="paper"] { background: var(--paper); color: var(--ink); }
|
|
41
|
+
[data-background="ink"] { background: var(--ink); color: var(--paper); }
|
|
32
42
|
|
|
33
|
-
.prose > * + * { margin-top: 1em; }
|
|
34
|
-
.prose h1, .prose h2, .prose h3 { line-height: 1.2; }
|
|
43
|
+
.prose > * + *, .smd-col > * + * { margin-top: 1em; } /* the custom section's columns read like prose */
|
|
44
|
+
.prose h1, .prose h2, .prose h3, .smd-col h1, .smd-col h2, .smd-col h3 { line-height: 1.2; }
|
|
35
45
|
|
|
36
46
|
/* The Button element, inside richtext. */
|
|
37
47
|
.button {
|
|
@@ -5,8 +5,9 @@ import { text } from "@/smoodly/toolsets";
|
|
|
5
5
|
// A section: a title and a richtext body. Registered in
|
|
6
6
|
// smoodly/sections.ts and allowed in the page shapes' zones
|
|
7
7
|
// (components/pages/*.tsx). Copy this file to add another section.
|
|
8
|
-
// The section wrapper renders the root element
|
|
9
|
-
//
|
|
8
|
+
// The section wrapper renders the root element, Smoodly's styles set
|
|
9
|
+
// inline (spacing, width, align) and the node id; a setting the CSS
|
|
10
|
+
// reads — here `background` — is the view's own data attribute.
|
|
10
11
|
export const textSchema = smoodly.schema.section({
|
|
11
12
|
name: "text",
|
|
12
13
|
title: "Text",
|
|
@@ -15,6 +16,9 @@ export const textSchema = smoodly.schema.section({
|
|
|
15
16
|
title: f.text(),
|
|
16
17
|
body: f.richtext({ toolset: text }),
|
|
17
18
|
},
|
|
19
|
+
settings: {
|
|
20
|
+
background: f.radio([{ value: "paper", label: "Paper" }, { value: "ink", label: "Ink" }]).default("paper"),
|
|
21
|
+
},
|
|
18
22
|
sample: {
|
|
19
23
|
title: "Hello",
|
|
20
24
|
body: { type: "doc", content: [{ type: "paragraph", content: [{ type: "text", text: "Write something here." }] }] },
|
|
@@ -25,9 +29,9 @@ export const textSchema = smoodly.schema.section({
|
|
|
25
29
|
// select and inline-edit them on the canvas.
|
|
26
30
|
const m = smoodly.marks(textSchema);
|
|
27
31
|
|
|
28
|
-
function TextView({ title, body, styles }: Props<typeof textSchema>) {
|
|
32
|
+
function TextView({ title, body, settings, styles }: Props<typeof textSchema>) {
|
|
29
33
|
return (
|
|
30
|
-
<smoodly.Section styles={styles}>
|
|
34
|
+
<smoodly.Section styles={styles} className="text" data-background={settings.background}>
|
|
31
35
|
<div className="container prose">
|
|
32
36
|
<h2 {...m.field("title")}>{title}</h2>
|
|
33
37
|
<div {...m.field("body")}><RichText doc={body} /></div>
|
|
@@ -26,7 +26,7 @@ if (await pages.resolvePath(DEFAULT, "/")) {
|
|
|
26
26
|
// Richtext is TipTap JSON, the stored format; RichText renders it.
|
|
27
27
|
type Node = Record<string, unknown>;
|
|
28
28
|
const p = (s: string): Node => ({ type: "paragraph", content: [{ type: "text", text: s }] });
|
|
29
|
-
const button = (label: string, href: string): Node => ({ type: "element", attrs: { name: "button", fields: { label, href },
|
|
29
|
+
const button = (label: string, href: string): Node => ({ type: "element", attrs: { name: "button", fields: { label, href }, settings: {} } });
|
|
30
30
|
const doc = (...content: Node[]) => ({ type: "doc", content });
|
|
31
31
|
|
|
32
32
|
// --- the site item: name and tagline, published in every locale ------
|
|
@@ -38,7 +38,7 @@ for (const locale of OTHERS) {
|
|
|
38
38
|
await entries.setStatus("site", site.id, locale, "published");
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
// --- the home page: one Text section
|
|
41
|
+
// --- the home page: one Text section and one custom section ----------
|
|
42
42
|
|
|
43
43
|
const tree = {
|
|
44
44
|
meta: { description: "A new site, built with Smoodly." },
|
|
@@ -47,6 +47,7 @@ const tree = {
|
|
|
47
47
|
{
|
|
48
48
|
id: "node-hello",
|
|
49
49
|
type: "text",
|
|
50
|
+
settings: { background: "paper" },
|
|
50
51
|
fields: {
|
|
51
52
|
title: "Hello",
|
|
52
53
|
body: doc(
|
|
@@ -56,6 +57,17 @@ const tree = {
|
|
|
56
57
|
),
|
|
57
58
|
},
|
|
58
59
|
},
|
|
60
|
+
// The custom section: the editor's own rows of columns, rendered by Smoodly.
|
|
61
|
+
{
|
|
62
|
+
id: "node-custom",
|
|
63
|
+
type: "custom",
|
|
64
|
+
fields: { rows: [
|
|
65
|
+
{ spans: [6, 6], gap: "md", align: "top", reverse: false, columns: [
|
|
66
|
+
doc(p("A custom section holds rows of columns. Drag the width bar in the inspector to resize them.")),
|
|
67
|
+
doc(p("Each column is richtext. Columns stack on small screens; the developer's stylesheet needs nothing.")),
|
|
68
|
+
] },
|
|
69
|
+
] },
|
|
70
|
+
},
|
|
59
71
|
],
|
|
60
72
|
},
|
|
61
73
|
} as unknown as PageTree;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { defineConfig
|
|
1
|
+
import { defineConfig } from "smoodly";
|
|
2
2
|
import { routing } from "@/i18n/routing";
|
|
3
3
|
import { collections } from "./collections";
|
|
4
4
|
import { sections } from "./sections";
|
|
5
|
-
import {
|
|
5
|
+
import { globals } from "./globals";
|
|
6
6
|
import { elements } from "./elements";
|
|
7
7
|
import { HomePage } from "@/components/pages/Home";
|
|
8
8
|
import { StandardPage } from "@/components/pages/Page";
|
|
9
9
|
import { PostPage } from "@/components/pages/Post";
|
|
10
10
|
|
|
11
|
-
export { collections, sections,
|
|
11
|
+
export { collections, sections, globals, elements };
|
|
12
12
|
|
|
13
13
|
// The registry and the plain settings, one file. Runtime-free: nothing
|
|
14
14
|
// here imports a route file or Next, so a script and the CLI's template
|
|
@@ -20,7 +20,7 @@ export const config = defineConfig({
|
|
|
20
20
|
sections,
|
|
21
21
|
elements,
|
|
22
22
|
collections,
|
|
23
|
-
|
|
23
|
+
globals,
|
|
24
24
|
pages: [HomePage, StandardPage, PostPage],
|
|
25
25
|
},
|
|
26
26
|
locales: { default: routing.defaultLocale, supported: [...routing.locales] },
|
|
@@ -28,11 +28,4 @@ export const config = defineConfig({
|
|
|
28
28
|
homePageSlug: "home",
|
|
29
29
|
// Editors can nest pages three deep.
|
|
30
30
|
pages: { tree: { depth: 3 } },
|
|
31
|
-
// Every section gets this select beside its own styles; the section
|
|
32
|
-
// wrapper emits it as data-tone, and app/globals.css styles it.
|
|
33
|
-
styles: {
|
|
34
|
-
sections: {
|
|
35
|
-
tone: f.select(["paper", "ink"]).default("paper"),
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
31
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Global content: code-declared singletons, edited once under Globals
|
|
2
2
|
// and read anywhere. `site` is an object item — fields code consumes;
|
|
3
|
-
// the layout reads it through q().
|
|
3
|
+
// the layout reads it through q().getGlobal("site"). A visual item
|
|
4
4
|
// (`section: SomeSection`) renders through a section instead.
|
|
5
5
|
import { f, smoodly } from "smoodly";
|
|
6
6
|
|
|
7
|
-
export const site = smoodly.
|
|
7
|
+
export const site = smoodly.global({
|
|
8
8
|
name: "site",
|
|
9
9
|
title: "Site",
|
|
10
10
|
fields: {
|
|
@@ -13,4 +13,4 @@ export const site = smoodly.shared({
|
|
|
13
13
|
},
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
export const
|
|
16
|
+
export const globals = [site];
|