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.
Files changed (65) hide show
  1. package/package.json +1 -1
  2. package/templates/blank/AGENTS.md +2 -2
  3. package/templates/blank/README.md +5 -5
  4. package/templates/blank/app/(site)/layout.tsx +3 -3
  5. package/templates/blank/app/globals.css +19 -9
  6. package/templates/blank/components/pages/Home.tsx +1 -1
  7. package/templates/blank/components/pages/Page.tsx +1 -1
  8. package/templates/blank/components/sections/Text.tsx +8 -4
  9. package/templates/blank/package.json +1 -1
  10. package/templates/blank/scripts/seed.ts +14 -2
  11. package/templates/blank/smoodly/config.ts +4 -11
  12. package/templates/blank/smoodly/{shared.ts → globals.ts} +4 -4
  13. package/templates/blank/smoodly/server.ts +9 -6
  14. package/templates/blank/supabase/migrations/00000000000000_smoodly.sql +1 -1
  15. package/templates/blank-intl/AGENTS.md +2 -2
  16. package/templates/blank-intl/README.md +5 -5
  17. package/templates/blank-intl/app/[locale]/layout.tsx +3 -3
  18. package/templates/blank-intl/app/globals.css +19 -9
  19. package/templates/blank-intl/components/pages/Home.tsx +1 -1
  20. package/templates/blank-intl/components/pages/Page.tsx +1 -1
  21. package/templates/blank-intl/components/sections/Text.tsx +8 -4
  22. package/templates/blank-intl/package.json +1 -1
  23. package/templates/blank-intl/scripts/seed.ts +14 -2
  24. package/templates/blank-intl/smoodly/config.ts +4 -11
  25. package/templates/blank-intl/smoodly/{shared.ts → globals.ts} +4 -4
  26. package/templates/blank-intl/smoodly/server.ts +9 -6
  27. package/templates/blank-intl/supabase/migrations/00000000000000_smoodly.sql +1 -1
  28. package/templates/lorem/AGENTS.md +2 -2
  29. package/templates/lorem/README.md +4 -4
  30. package/templates/lorem/app/(site)/layout.tsx +4 -4
  31. package/templates/lorem/app/globals.css +53 -44
  32. package/templates/lorem/components/elements/Callout.tsx +3 -3
  33. package/templates/lorem/components/elements/Cta.tsx +4 -4
  34. package/templates/lorem/components/pages/Home.tsx +4 -4
  35. package/templates/lorem/components/pages/Page.tsx +1 -1
  36. package/templates/lorem/components/sections/Footer.tsx +4 -2
  37. package/templates/lorem/components/sections/Header.tsx +4 -2
  38. package/templates/lorem/components/sections/ImageText.tsx +5 -5
  39. package/templates/lorem/components/sections/Quote.tsx +9 -6
  40. package/templates/lorem/package.json +1 -1
  41. package/templates/lorem/scripts/seed.ts +28 -9
  42. package/templates/lorem/smoodly/config.ts +18 -11
  43. package/templates/lorem/smoodly/{shared.ts → globals.ts} +7 -7
  44. package/templates/lorem/smoodly/sections.ts +3 -3
  45. package/templates/lorem/smoodly/server.ts +9 -6
  46. package/templates/lorem/supabase/migrations/00000000000000_smoodly.sql +1 -1
  47. package/templates/lorem-intl/AGENTS.md +2 -2
  48. package/templates/lorem-intl/README.md +4 -4
  49. package/templates/lorem-intl/app/[locale]/layout.tsx +4 -4
  50. package/templates/lorem-intl/app/globals.css +53 -44
  51. package/templates/lorem-intl/components/elements/Callout.tsx +3 -3
  52. package/templates/lorem-intl/components/elements/Cta.tsx +4 -4
  53. package/templates/lorem-intl/components/pages/Home.tsx +4 -4
  54. package/templates/lorem-intl/components/pages/Page.tsx +1 -1
  55. package/templates/lorem-intl/components/sections/Footer.tsx +4 -2
  56. package/templates/lorem-intl/components/sections/Header.tsx +4 -2
  57. package/templates/lorem-intl/components/sections/ImageText.tsx +5 -5
  58. package/templates/lorem-intl/components/sections/Quote.tsx +9 -6
  59. package/templates/lorem-intl/package.json +1 -1
  60. package/templates/lorem-intl/scripts/seed.ts +28 -9
  61. package/templates/lorem-intl/smoodly/config.ts +18 -11
  62. package/templates/lorem-intl/smoodly/{shared.ts → globals.ts} +7 -7
  63. package/templates/lorem-intl/smoodly/sections.ts +3 -3
  64. package/templates/lorem-intl/smoodly/server.ts +9 -6
  65. package/templates/lorem-intl/supabase/migrations/00000000000000_smoodly.sql +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-smoodly-app",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "Create a Smoodly site: a fresh Next.js app with the admin mounted, a Supabase schema, an env file and a first editor.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -8,8 +8,8 @@ where things are.
8
8
  ## Rules
9
9
 
10
10
  - **Register everything in `smoodly/`.** Every section, element,
11
- collection and shared item is listed in its barrel (`sections.ts`,
12
- `elements.ts`, `collections.ts`, `shared.ts`) and `smoodly/config.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), `shared.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).
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 shared item
51
+ ## Add a global item
52
52
 
53
- Copy the `site` block in `smoodly/shared.ts` and add it to `shared`. An
54
- object item (`fields`) is read in code with `q().getShared("name")`; a
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 Shared group. Edit both under
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` shared item (smoodly/shared.ts) gives
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().getShared("site");
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().getShared("site");
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 tones the
3
- config's section style emits as data-tone. Replace freely. */
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
- .container { width: min(var(--container), 100% - 2rem); margin-inline: auto; }
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
- /* Sections carry data-tone from the config's section styles. */
29
- [data-tone="paper"] { background: var(--paper); color: var(--ink); }
30
- [data-tone="ink"] { background: var(--ink); color: var(--paper); }
31
- section[data-tone] { padding-block: 3rem; }
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 {
@@ -10,7 +10,7 @@ export const homeSchema = smoodly.schema.page({
10
10
  title: "Home",
11
11
  slug: "home",
12
12
  zones: {
13
- body: z.freeform({ sections: [Text] }),
13
+ body: z.freeform({ sections: [Text], custom: true }),
14
14
  },
15
15
  meta: { seo: true },
16
16
  });
@@ -8,7 +8,7 @@ export const pageSchema = smoodly.schema.page({
8
8
  name: "page",
9
9
  title: "Page",
10
10
  zones: {
11
- body: z.freeform({ sections: [Text] }),
11
+ body: z.freeform({ sections: [Text], custom: true }),
12
12
  },
13
13
  meta: { seo: true },
14
14
  });
@@ -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 and emits the config's
9
- // section styles as data attributes (data-tone here) for app/globals.css.
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>
@@ -14,7 +14,7 @@
14
14
  "next": "^16.3.4",
15
15
  "react": "^19",
16
16
  "react-dom": "^19",
17
- "smoodly": "0.0.16"
17
+ "smoodly": "0.0.17"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@next/env": "^16.3.4",
@@ -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 }, styles: {} } });
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, f } from "smoodly";
1
+ import { defineConfig } from "smoodly";
2
2
  import { collections } from "./collections";
3
3
  import { sections } from "./sections";
4
- import { shared } from "./shared";
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, shared, elements };
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
- shared,
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
- // Shared content: code-declared singletons, edited once under Globals
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().getShared("site"). A visual item
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.shared({
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 shared = [site];
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, shared } from "./config";
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 shared>>;
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, shared, elements });
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, shared, sections, elements, locales, homePageSlug: config.homePageSlug, pages: config.registry.pages ?? [] });
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, shared });
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().getShared("header",
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 shared item (spec 2026-09-14 §1).
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 shared item is listed in its barrel (`sections.ts`,
12
- `elements.ts`, `collections.ts`, `shared.ts`) and `smoodly/config.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), `shared.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
+ - `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 shared item
52
+ ## Add a global item
53
53
 
54
- Copy the `site` block in `smoodly/shared.ts` and add it to `shared`. An
55
- object item (`fields`) is read in code with `q().getShared("name")`; a
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 Shared group. Edit both under
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` shared item gives the document title and the footer line,
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().getShared("site", { locale });
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().getShared("site", { locale });
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 tones the
3
- config's section style emits as data-tone. Replace freely. */
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
- .container { width: min(var(--container), 100% - 2rem); margin-inline: auto; }
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
- /* Sections carry data-tone from the config's section styles. */
29
- [data-tone="paper"] { background: var(--paper); color: var(--ink); }
30
- [data-tone="ink"] { background: var(--ink); color: var(--paper); }
31
- section[data-tone] { padding-block: 3rem; }
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 {
@@ -10,7 +10,7 @@ export const homeSchema = smoodly.schema.page({
10
10
  title: "Home",
11
11
  slug: "home",
12
12
  zones: {
13
- body: z.freeform({ sections: [Text] }),
13
+ body: z.freeform({ sections: [Text], custom: true }),
14
14
  },
15
15
  meta: { seo: true },
16
16
  });
@@ -8,7 +8,7 @@ export const pageSchema = smoodly.schema.page({
8
8
  name: "page",
9
9
  title: "Page",
10
10
  zones: {
11
- body: z.freeform({ sections: [Text] }),
11
+ body: z.freeform({ sections: [Text], custom: true }),
12
12
  },
13
13
  meta: { seo: true },
14
14
  });
@@ -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 and emits the config's
9
- // section styles as data attributes (data-tone here) for app/globals.css.
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>
@@ -14,7 +14,7 @@
14
14
  "next": "^16.3.4",
15
15
  "react": "^19",
16
16
  "react-dom": "^19",
17
- "smoodly": "0.0.16",
17
+ "smoodly": "0.0.17",
18
18
  "next-intl": "^4.14.2"
19
19
  },
20
20
  "devDependencies": {
@@ -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 }, styles: {} } });
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, f } from "smoodly";
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 { shared } from "./shared";
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, shared, elements };
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
- shared,
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
- // Shared content: code-declared singletons, edited once under Globals
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().getShared("site"). A visual item
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.shared({
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 shared = [site];
16
+ export const globals = [site];