create-smoodly-app 0.0.14 → 0.0.15
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 +38 -10
- package/dist/index.js +39 -15
- package/package.json +3 -2
- package/templates/blank/AGENTS.md +50 -0
- package/templates/blank/CLAUDE.md +1 -0
- package/templates/blank/README.md +75 -0
- package/templates/{next/app → blank/app/(site)}/[...path]/page.tsx +2 -2
- package/templates/blank/app/(site)/layout.tsx +25 -0
- package/templates/blank/app/globals.css +47 -0
- package/templates/blank/components/elements/Button.tsx +26 -0
- package/templates/blank/components/pages/Home.tsx +22 -0
- package/templates/blank/components/pages/Page.tsx +25 -0
- package/templates/blank/components/pages/Post.tsx +21 -0
- package/templates/blank/components/sections/Text.tsx +39 -0
- package/templates/{next → blank}/package.json +1 -1
- package/templates/blank/scripts/seed.ts +72 -0
- package/templates/blank/smoodly/collections.ts +26 -0
- package/templates/blank/smoodly/config.ts +35 -0
- package/templates/blank/smoodly/elements.ts +5 -0
- package/templates/blank/smoodly/sections.ts +5 -0
- package/templates/blank/smoodly/shared.ts +16 -0
- package/templates/blank/smoodly/toolsets.ts +14 -0
- package/templates/blank/supabase/migrations/00000000000000_smoodly.sql +518 -0
- package/templates/blank-intl/AGENTS.md +50 -0
- package/templates/blank-intl/CLAUDE.md +1 -0
- package/templates/blank-intl/README.md +76 -0
- package/templates/blank-intl/app/[locale]/layout.tsx +42 -0
- package/templates/blank-intl/app/globals.css +47 -0
- package/templates/blank-intl/components/elements/Button.tsx +26 -0
- package/templates/blank-intl/components/pages/Home.tsx +22 -0
- package/templates/blank-intl/components/pages/Page.tsx +25 -0
- package/templates/blank-intl/components/pages/Post.tsx +21 -0
- package/templates/blank-intl/components/sections/Text.tsx +39 -0
- package/templates/blank-intl/messages/en.json +1 -0
- package/templates/{next-intl → blank-intl}/package.json +1 -1
- package/templates/blank-intl/scripts/seed.ts +72 -0
- package/templates/blank-intl/smoodly/collections.ts +26 -0
- package/templates/blank-intl/smoodly/config.ts +38 -0
- package/templates/blank-intl/smoodly/elements.ts +5 -0
- package/templates/blank-intl/smoodly/sections.ts +5 -0
- package/templates/blank-intl/smoodly/shared.ts +16 -0
- package/templates/blank-intl/smoodly/toolsets.ts +14 -0
- package/templates/blank-intl/supabase/migrations/00000000000000_smoodly.sql +518 -0
- package/templates/blank-intl.tsconfig.json +28 -0
- package/templates/blank.tsconfig.json +28 -0
- package/templates/lorem/AGENTS.md +50 -0
- package/templates/lorem/CLAUDE.md +1 -0
- package/templates/{next → lorem}/README.md +5 -4
- package/templates/lorem/_gitignore +5 -0
- package/templates/lorem/app/(site)/[...path]/page.tsx +15 -0
- package/templates/{next/app → lorem/app/(site)}/layout.tsx +7 -6
- package/templates/lorem/app/(site)/page.tsx +11 -0
- package/templates/lorem/app/(smoodly)/admin/[[...segments]]/page.tsx +1 -0
- package/templates/lorem/app/(smoodly)/admin/layout.tsx +1 -0
- package/templates/lorem/next.config.ts +8 -0
- package/templates/lorem/package.json +27 -0
- package/templates/lorem/smoodly/admin.tsx +6 -0
- package/templates/lorem/smoodly/ops.ts +11 -0
- package/templates/lorem/smoodly/richtext.tsx +7 -0
- package/templates/lorem/smoodly/server.ts +96 -0
- package/templates/lorem/tsconfig.json +41 -0
- package/templates/lorem-intl/AGENTS.md +50 -0
- package/templates/lorem-intl/CLAUDE.md +1 -0
- package/templates/{next-intl → lorem-intl}/README.md +2 -1
- package/templates/lorem-intl/_gitignore +5 -0
- package/templates/lorem-intl/app/(smoodly)/admin/[[...segments]]/page.tsx +1 -0
- package/templates/lorem-intl/app/(smoodly)/admin/layout.tsx +15 -0
- package/templates/lorem-intl/app/[locale]/[...path]/page.tsx +18 -0
- package/templates/lorem-intl/app/[locale]/page.tsx +15 -0
- package/templates/lorem-intl/i18n/navigation.ts +6 -0
- package/templates/lorem-intl/i18n/request.ts +24 -0
- package/templates/lorem-intl/i18n/routing.ts +17 -0
- package/templates/lorem-intl/next.config.ts +11 -0
- package/templates/lorem-intl/package.json +28 -0
- package/templates/lorem-intl/proxy.ts +11 -0
- package/templates/lorem-intl/smoodly/admin.tsx +6 -0
- package/templates/lorem-intl/smoodly/ops.ts +11 -0
- package/templates/lorem-intl/smoodly/richtext.tsx +7 -0
- package/templates/lorem-intl/smoodly/server.ts +97 -0
- package/templates/lorem-intl/tsconfig.json +41 -0
- package/templates/lorem-intl.tsconfig.json +28 -0
- package/templates/lorem.tsconfig.json +28 -0
- /package/templates/{next → blank}/_gitignore +0 -0
- /package/templates/{next/app → blank/app/(site)}/page.tsx +0 -0
- /package/templates/{next → blank}/app/(smoodly)/admin/[[...segments]]/page.tsx +0 -0
- /package/templates/{next → blank}/app/(smoodly)/admin/layout.tsx +0 -0
- /package/templates/{next → blank}/next.config.ts +0 -0
- /package/templates/{next → blank}/smoodly/admin.tsx +0 -0
- /package/templates/{next → blank}/smoodly/ops.ts +0 -0
- /package/templates/{next → blank}/smoodly/richtext.tsx +0 -0
- /package/templates/{next → blank}/smoodly/server.ts +0 -0
- /package/templates/{next → blank}/tsconfig.json +0 -0
- /package/templates/{next-intl → blank-intl}/_gitignore +0 -0
- /package/templates/{next-intl → blank-intl}/app/(smoodly)/admin/[[...segments]]/page.tsx +0 -0
- /package/templates/{next-intl → blank-intl}/app/(smoodly)/admin/layout.tsx +0 -0
- /package/templates/{next-intl → blank-intl}/app/[locale]/[...path]/page.tsx +0 -0
- /package/templates/{next-intl → blank-intl}/app/[locale]/page.tsx +0 -0
- /package/templates/{next-intl → blank-intl}/i18n/navigation.ts +0 -0
- /package/templates/{next-intl → blank-intl}/i18n/request.ts +0 -0
- /package/templates/{next-intl → blank-intl}/i18n/routing.ts +0 -0
- /package/templates/{next-intl → blank-intl}/next.config.ts +0 -0
- /package/templates/{next-intl → blank-intl}/proxy.ts +0 -0
- /package/templates/{next-intl → blank-intl}/smoodly/admin.tsx +0 -0
- /package/templates/{next-intl → blank-intl}/smoodly/ops.ts +0 -0
- /package/templates/{next-intl → blank-intl}/smoodly/richtext.tsx +0 -0
- /package/templates/{next-intl → blank-intl}/smoodly/server.ts +0 -0
- /package/templates/{next-intl → blank-intl}/tsconfig.json +0 -0
- /package/templates/{next → lorem}/app/globals.css +0 -0
- /package/templates/{next → lorem}/components/ArticleCard.tsx +0 -0
- /package/templates/{next → lorem}/components/articles.ts +0 -0
- /package/templates/{next → lorem}/components/elements/Callout.tsx +0 -0
- /package/templates/{next → lorem}/components/elements/Cta.tsx +0 -0
- /package/templates/{next → lorem}/components/pages/Article.tsx +0 -0
- /package/templates/{next → lorem}/components/pages/Articles.tsx +0 -0
- /package/templates/{next → lorem}/components/pages/Home.tsx +0 -0
- /package/templates/{next → lorem}/components/pages/Page.tsx +0 -0
- /package/templates/{next → lorem}/components/sections/FavouriteArticles.tsx +0 -0
- /package/templates/{next → lorem}/components/sections/Footer.tsx +0 -0
- /package/templates/{next → lorem}/components/sections/Header.tsx +0 -0
- /package/templates/{next → lorem}/components/sections/Hero.tsx +0 -0
- /package/templates/{next → lorem}/components/sections/ImageText.tsx +0 -0
- /package/templates/{next → lorem}/components/sections/PageHero.tsx +0 -0
- /package/templates/{next → lorem}/components/sections/People.tsx +0 -0
- /package/templates/{next → lorem}/components/sections/Points.tsx +0 -0
- /package/templates/{next → lorem}/components/sections/Quote.tsx +0 -0
- /package/templates/{next → lorem}/components/strings.ts +0 -0
- /package/templates/{next → lorem}/messages/en.json +0 -0
- /package/templates/{next → lorem}/public/images/about_main.jpg +0 -0
- /package/templates/{next → lorem}/public/images/article_photo.jpg +0 -0
- /package/templates/{next → lorem}/public/images/hero_image.jpg +0 -0
- /package/templates/{next → lorem}/public/images/home_section_2.jpg +0 -0
- /package/templates/{next → lorem}/scripts/seed.ts +0 -0
- /package/templates/{next → lorem}/smoodly/collections.ts +0 -0
- /package/templates/{next → lorem}/smoodly/config.ts +0 -0
- /package/templates/{next → lorem}/smoodly/elements.ts +0 -0
- /package/templates/{next → lorem}/smoodly/sections.ts +0 -0
- /package/templates/{next → lorem}/smoodly/shared.ts +0 -0
- /package/templates/{next → lorem}/smoodly/toolsets.ts +0 -0
- /package/templates/{next → lorem}/supabase/migrations/00000000000000_smoodly.sql +0 -0
- /package/templates/{next-intl → lorem-intl}/app/[locale]/layout.tsx +0 -0
- /package/templates/{next-intl → lorem-intl}/app/globals.css +0 -0
- /package/templates/{next-intl → lorem-intl}/components/ArticleCard.tsx +0 -0
- /package/templates/{next-intl → lorem-intl}/components/articles.ts +0 -0
- /package/templates/{next-intl → lorem-intl}/components/elements/Callout.tsx +0 -0
- /package/templates/{next-intl → lorem-intl}/components/elements/Cta.tsx +0 -0
- /package/templates/{next-intl → lorem-intl}/components/pages/Article.tsx +0 -0
- /package/templates/{next-intl → lorem-intl}/components/pages/Articles.tsx +0 -0
- /package/templates/{next-intl → lorem-intl}/components/pages/Home.tsx +0 -0
- /package/templates/{next-intl → lorem-intl}/components/pages/Page.tsx +0 -0
- /package/templates/{next-intl → lorem-intl}/components/sections/FavouriteArticles.tsx +0 -0
- /package/templates/{next-intl → lorem-intl}/components/sections/Footer.tsx +0 -0
- /package/templates/{next-intl → lorem-intl}/components/sections/Header.tsx +0 -0
- /package/templates/{next-intl → lorem-intl}/components/sections/Hero.tsx +0 -0
- /package/templates/{next-intl → lorem-intl}/components/sections/ImageText.tsx +0 -0
- /package/templates/{next-intl → lorem-intl}/components/sections/PageHero.tsx +0 -0
- /package/templates/{next-intl → lorem-intl}/components/sections/People.tsx +0 -0
- /package/templates/{next-intl → lorem-intl}/components/sections/Points.tsx +0 -0
- /package/templates/{next-intl → lorem-intl}/components/sections/Quote.tsx +0 -0
- /package/templates/{next-intl → lorem-intl}/components/strings.ts +0 -0
- /package/templates/{next-intl → lorem-intl}/messages/en.json +0 -0
- /package/templates/{next-intl → lorem-intl}/messages/fi.json +0 -0
- /package/templates/{next-intl → lorem-intl}/public/images/about_main.jpg +0 -0
- /package/templates/{next-intl → lorem-intl}/public/images/article_photo.jpg +0 -0
- /package/templates/{next-intl → lorem-intl}/public/images/hero_image.jpg +0 -0
- /package/templates/{next-intl → lorem-intl}/public/images/home_section_2.jpg +0 -0
- /package/templates/{next-intl → lorem-intl}/scripts/seed.ts +0 -0
- /package/templates/{next-intl → lorem-intl}/smoodly/collections.ts +0 -0
- /package/templates/{next-intl → lorem-intl}/smoodly/config.ts +0 -0
- /package/templates/{next-intl → lorem-intl}/smoodly/elements.ts +0 -0
- /package/templates/{next-intl → lorem-intl}/smoodly/sections.ts +0 -0
- /package/templates/{next-intl → lorem-intl}/smoodly/shared.ts +0 -0
- /package/templates/{next-intl → lorem-intl}/smoodly/toolsets.ts +0 -0
- /package/templates/{next-intl → lorem-intl}/supabase/migrations/00000000000000_smoodly.sql +0 -0
package/README.md
CHANGED
|
@@ -43,15 +43,23 @@ A hidden `--smoodly-dep <spec>` overrides the `smoodly` dependency written into
|
|
|
43
43
|
|
|
44
44
|
## How it is built
|
|
45
45
|
|
|
46
|
-
`templates/
|
|
47
|
-
`
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
`templates/lorem`, `templates/lorem-intl`, `templates/blank` and
|
|
47
|
+
`templates/blank-intl` are all generated by `scripts/build-template.ts`.
|
|
48
|
+
The Lorem Co. pair comes from `examples/site` — the multilingual layout —
|
|
49
|
+
plus the five single-language files under `variants/lorem/single/`. The
|
|
50
|
+
blank pair (spec 2026-09-11) takes only the content-free glue from the
|
|
51
|
+
example (`smoodly/server.ts`, `ops.ts`, `admin.tsx`, `richtext.tsx`, the
|
|
52
|
+
admin route files, next-intl's files) and everything with content from
|
|
53
|
+
`variants/blank/{common,single,intl}/`. Each template also gets a generated
|
|
54
|
+
`package.json` (with `smoodly` pinned to this package's version),
|
|
55
|
+
`tsconfig`, `next.config`, README, `AGENTS.md`, a `CLAUDE.md` that imports
|
|
56
|
+
it, and the one migration (`coreTablesSQL` + the starter's collection
|
|
57
|
+
views). Two exact-line transforms turn the example's glue (locale config,
|
|
58
|
+
`href`) into the single-language one; a drifted example fails the build
|
|
59
|
+
rather than silently shipping stale copies. Every template gets a check
|
|
60
|
+
tsconfig beside it; `npm run typecheck` runs `tsc` over all four
|
|
61
|
+
(`typecheck:templates`), so hand-kept files cannot drift silently. All
|
|
62
|
+
four templates are git-ignored and rebuilt by `npm run build`.
|
|
55
63
|
|
|
56
64
|
`src/plan.ts` turns answers into steps; `src/run.ts` executes them with
|
|
57
65
|
injected `exec`, fs and Supabase client so the sequence and every fallback
|
|
@@ -59,12 +67,32 @@ message are unit-tested. `src/prompts.ts` is the only interactive code.
|
|
|
59
67
|
The CLI has no runtime dependency on `smoodly`.
|
|
60
68
|
|
|
61
69
|
npm test # unit tests (template build, planner, runner, editor)
|
|
62
|
-
npm run
|
|
70
|
+
npm run typecheck # tsc, then the four templates
|
|
71
|
+
npm run build # templates + esbuild bundle → dist/index.js
|
|
63
72
|
|
|
64
73
|
Walked 2026-09-06: both templates scaffolded against the repo's local stack
|
|
65
74
|
in hosted mode, `tsc`, `next build` and `/admin` 200 for each; the
|
|
66
75
|
multilingual one with `--locales en,de`.
|
|
67
76
|
|
|
77
|
+
Walked 2026-09-11: blank multilingual scaffolded in local mode with
|
|
78
|
+
`--locales en,fr` from the built CLI, `next build`, `/` and `/fr` seeded
|
|
79
|
+
with the Text section and the site footer line, the admin listing Home,
|
|
80
|
+
Posts and the Site item, a post with a Button element published and
|
|
81
|
+
served at `/posts/<slug>` in both locales; blank single-language
|
|
82
|
+
scaffolded in hosted mode against the repo stack and `next build` only.
|
|
83
|
+
|
|
84
|
+
## Follow-ups (logged 2026-09-11)
|
|
85
|
+
|
|
86
|
+
- **Blank single-language is typechecked, never built by CI.** The live
|
|
87
|
+
job scaffolds lorem single (hosted), lorem intl and blank intl (local).
|
|
88
|
+
A fourth stack costs ~2 minutes; add it if blank single ever breaks in
|
|
89
|
+
a way `tsc` cannot see (a runtime import, a missing file at a path
|
|
90
|
+
Next reads).
|
|
91
|
+
- **The guide inside the package.** `AGENTS.md` is generated per scaffold
|
|
92
|
+
and frozen at install time. Once its text settles, ship it in the
|
|
93
|
+
`smoodly` package (`smoodly/AGENTS.md`) and have the scaffold's copy
|
|
94
|
+
point there, so it tracks the package version.
|
|
95
|
+
|
|
68
96
|
## Follow-ups (logged 2026-09-06)
|
|
69
97
|
|
|
70
98
|
- Local mode on a fresh multilingual install (0.0.5, 2026-09-06) failed at
|
package/dist/index.js
CHANGED
|
@@ -29,6 +29,7 @@ function parseFlags(argv) {
|
|
|
29
29
|
"editor-email": { type: "string" },
|
|
30
30
|
"smoodly-dep": { type: "string" },
|
|
31
31
|
locales: { type: "string" },
|
|
32
|
+
starter: { type: "string" },
|
|
32
33
|
"skip-link": { type: "boolean", default: false },
|
|
33
34
|
yes: { type: "boolean", short: "y", default: false },
|
|
34
35
|
help: { type: "boolean", short: "h", default: false }
|
|
@@ -39,6 +40,10 @@ function parseFlags(argv) {
|
|
|
39
40
|
if (supabase !== void 0 && supabase !== "local" && supabase !== "hosted") {
|
|
40
41
|
throw new Error(`--supabase must be "local" or "hosted", got "${supabase}"`);
|
|
41
42
|
}
|
|
43
|
+
const starter = values.starter;
|
|
44
|
+
if (starter !== void 0 && starter !== "lorem" && starter !== "blank") {
|
|
45
|
+
throw new Error(`--starter must be "lorem" or "blank", got "${starter}"`);
|
|
46
|
+
}
|
|
42
47
|
return {
|
|
43
48
|
name: positionals[0],
|
|
44
49
|
supabase,
|
|
@@ -50,7 +55,8 @@ function parseFlags(argv) {
|
|
|
50
55
|
yes: values.yes ?? false,
|
|
51
56
|
help: values.help ?? false,
|
|
52
57
|
smoodlyDep: values["smoodly-dep"],
|
|
53
|
-
locales: values.locales === void 0 ? void 0 : parseLocales(values.locales)
|
|
58
|
+
locales: values.locales === void 0 ? void 0 : parseLocales(values.locales),
|
|
59
|
+
starter
|
|
54
60
|
};
|
|
55
61
|
}
|
|
56
62
|
var HELP = `create-smoodly-app \u2014 a fresh Next.js site with Smoodly installed
|
|
@@ -60,6 +66,7 @@ Usage: npx create-smoodly-app [name] [options]
|
|
|
60
66
|
Options:
|
|
61
67
|
--supabase local|hosted Where content lives (asked when omitted; local needs the Supabase CLI)
|
|
62
68
|
--locales <a,b,\u2026> Multilingual: the locales, first is the default (scaffolds next-intl; omitted = one language)
|
|
69
|
+
--starter lorem|blank lorem: the Lorem Co. tour of every feature (default); blank: one of each, for building your own
|
|
63
70
|
--url <url> Hosted: the Supabase project URL
|
|
64
71
|
--publishable-key <key> Hosted: the anon / publishable key
|
|
65
72
|
--secret-key <key> Hosted: the service-role / secret key
|
|
@@ -227,13 +234,15 @@ async function scaffold(opts) {
|
|
|
227
234
|
routingPath,
|
|
228
235
|
routing.replace(ROUTING_LOCALES, ` locales: [${opts.locales.map((l) => JSON.stringify(l)).join(", ")}],`).replace(ROUTING_DEFAULT, ` defaultLocale: ${JSON.stringify(opts.locales[0])},`)
|
|
229
236
|
);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
237
|
+
if (opts.starter !== "blank") {
|
|
238
|
+
const map = articlesSegments(opts.locales);
|
|
239
|
+
for (const [rel, line] of [["smoodly/collections.ts", ARTICLES_PATH_LINE], ["components/pages/Articles.tsx", ARTICLES_SLUG_LINE]]) {
|
|
240
|
+
const path = join(opts.dir, rel);
|
|
241
|
+
const source = await readFile(path, "utf8");
|
|
242
|
+
if (!source.includes(line)) throw new Error(`${rel} in the template has drifted; expected the line
|
|
235
243
|
${line}`);
|
|
236
|
-
|
|
244
|
+
await writeFile(path, source.replace(line, line.replace('{ en: "posts", fi: "artikkelit" }', map)));
|
|
245
|
+
}
|
|
237
246
|
}
|
|
238
247
|
}
|
|
239
248
|
}
|
|
@@ -262,7 +271,7 @@ async function resolveAnswers(flags2, ctx) {
|
|
|
262
271
|
const locales = flags2.locales ?? (flags2.yes ? void 0 : await answer(p.confirm({ message: "Multilingual site? (next-intl, translated URLs)", initialValue: false })) ? await answer(
|
|
263
272
|
p.text({
|
|
264
273
|
message: "Locales, comma-separated, the first is the default",
|
|
265
|
-
placeholder: "en,
|
|
274
|
+
placeholder: "en, fr",
|
|
266
275
|
validate: (v) => {
|
|
267
276
|
try {
|
|
268
277
|
parseLocales(v ?? "");
|
|
@@ -272,6 +281,15 @@ async function resolveAnswers(flags2, ctx) {
|
|
|
272
281
|
}
|
|
273
282
|
})
|
|
274
283
|
).then(parseLocales) : void 0);
|
|
284
|
+
const starter = flags2.starter ?? (flags2.yes ? "lorem" : await answer(
|
|
285
|
+
p.select({
|
|
286
|
+
message: "Starter?",
|
|
287
|
+
options: [
|
|
288
|
+
{ value: "lorem", label: "Lorem Co.", hint: "a tour of every feature \u2014 edit it into your site" },
|
|
289
|
+
{ value: "blank", label: "Blank", hint: "one of each \u2014 build your own" }
|
|
290
|
+
]
|
|
291
|
+
})
|
|
292
|
+
));
|
|
275
293
|
const supabase = flags2.supabase ?? (flags2.yes ? "local" : await answer(
|
|
276
294
|
p.select({
|
|
277
295
|
message: "Where should content live?",
|
|
@@ -283,7 +301,7 @@ async function resolveAnswers(flags2, ctx) {
|
|
|
283
301
|
));
|
|
284
302
|
if (supabase === "local") {
|
|
285
303
|
if (!ctx.hasSupabaseCli) bail("Local mode needs the Supabase CLI: https://supabase.com/docs/guides/cli (brew install supabase/tap/supabase)");
|
|
286
|
-
return { name, dir, supabase, hasSupabaseCli: true, smoodlyDep: flags2.smoodlyDep, locales };
|
|
304
|
+
return { name, dir, supabase, hasSupabaseCli: true, smoodlyDep: flags2.smoodlyDep, locales, starter };
|
|
287
305
|
}
|
|
288
306
|
const badUrl = "Supabase project URL must be an http(s) URL, like https://abcd.supabase.co";
|
|
289
307
|
const typedUrl = flags2.url ?? await answer(
|
|
@@ -302,7 +320,7 @@ async function resolveAnswers(flags2, ctx) {
|
|
|
302
320
|
p.log.warn(`No project ref in ${url}; skipping supabase link. Run \`supabase link --project-ref <project-ref>\` and \`supabase db push\` yourself.`);
|
|
303
321
|
}
|
|
304
322
|
const linkAndPush = !ctx.hasSupabaseCli ? false : flags2.skipLink || !ref ? false : flags2.yes ? true : await answer(p.confirm({ message: `Link this app to the project and push the migration now? (supabase link --project-ref ${ref}, supabase db push)` }));
|
|
305
|
-
return { name, dir, supabase, hasSupabaseCli: ctx.hasSupabaseCli, hosted: { keys: { url, publishableKey, secretKey }, linkAndPush }, smoodlyDep: flags2.smoodlyDep, locales };
|
|
323
|
+
return { name, dir, supabase, hasSupabaseCli: ctx.hasSupabaseCli, hosted: { keys: { url, publishableKey, secretKey }, linkAndPush }, smoodlyDep: flags2.smoodlyDep, locales, starter };
|
|
306
324
|
}
|
|
307
325
|
function askEditorWith(flags2, env = process.env) {
|
|
308
326
|
return async (existing) => {
|
|
@@ -335,6 +353,9 @@ function askEditorWith(flags2, env = process.env) {
|
|
|
335
353
|
import { join as join2 } from "node:path";
|
|
336
354
|
|
|
337
355
|
// src/plan.ts
|
|
356
|
+
function templateName(starter, locales) {
|
|
357
|
+
return locales === "intl" ? `${starter}-intl` : starter;
|
|
358
|
+
}
|
|
338
359
|
function plan(a) {
|
|
339
360
|
if (a.supabase === "local" && !a.hasSupabaseCli) {
|
|
340
361
|
throw new Error("Local mode needs the Supabase CLI: https://supabase.com/docs/guides/cli (brew install supabase/tap/supabase)");
|
|
@@ -409,11 +430,12 @@ async function run(answers2, deps) {
|
|
|
409
430
|
switch (step) {
|
|
410
431
|
case "copyTemplate":
|
|
411
432
|
await deps.fs.scaffold({
|
|
412
|
-
templateDir: answers2.locales ?
|
|
433
|
+
templateDir: deps.templateDirs[templateName(answers2.starter ?? "lorem", answers2.locales ? "intl" : "single")],
|
|
413
434
|
dir: cwd,
|
|
414
435
|
name: answers2.name,
|
|
415
436
|
smoodlyDep: answers2.smoodlyDep,
|
|
416
|
-
locales: answers2.locales
|
|
437
|
+
locales: answers2.locales,
|
|
438
|
+
starter: answers2.starter
|
|
417
439
|
});
|
|
418
440
|
break;
|
|
419
441
|
case "install":
|
|
@@ -519,8 +541,10 @@ function linkCommand(answers2) {
|
|
|
519
541
|
|
|
520
542
|
// src/index.ts
|
|
521
543
|
var TEMPLATE_DIRS = {
|
|
522
|
-
|
|
523
|
-
intl: fileURLToPath(new URL("../templates/
|
|
544
|
+
lorem: fileURLToPath(new URL("../templates/lorem/", import.meta.url)),
|
|
545
|
+
"lorem-intl": fileURLToPath(new URL("../templates/lorem-intl/", import.meta.url)),
|
|
546
|
+
blank: fileURLToPath(new URL("../templates/blank/", import.meta.url)),
|
|
547
|
+
"blank-intl": fileURLToPath(new URL("../templates/blank-intl/", import.meta.url))
|
|
524
548
|
};
|
|
525
549
|
var flags;
|
|
526
550
|
try {
|
|
@@ -569,7 +593,7 @@ p2.note(
|
|
|
569
593
|
`cd ${answers.name}`,
|
|
570
594
|
"npm run dev",
|
|
571
595
|
"",
|
|
572
|
-
"Site: http://localhost:3000 (the Lorem Co. starter \u2014 edit it at /admin)",
|
|
596
|
+
answers.starter === "blank" ? "Site: http://localhost:3000 (a blank site \u2014 build it in components/ and at /admin)" : "Site: http://localhost:3000 (the Lorem Co. starter \u2014 edit it at /admin)",
|
|
573
597
|
"Admin: http://localhost:3000/admin",
|
|
574
598
|
`Editor: ${outcome.editor.email ?? outcome.editor.id}`,
|
|
575
599
|
...answers.locales ? [`Locales: ${answers.locales.join(", ")} (i18n/routing.ts)`] : []
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-smoodly-app",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
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": {
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"build:cli": "esbuild src/index.ts --bundle --packages=external --platform=node --format=esm --target=node20 --outfile=dist/index.js --banner:js='#!/usr/bin/env node'",
|
|
26
26
|
"prepublishOnly": "npm run build",
|
|
27
27
|
"test": "vitest run",
|
|
28
|
-
"typecheck": "tsc --noEmit"
|
|
28
|
+
"typecheck": "tsc --noEmit && npm run typecheck:templates",
|
|
29
|
+
"typecheck:templates": "npm run build:template && tsc -p templates/lorem.tsconfig.json && tsc -p templates/lorem-intl.tsconfig.json && tsc -p templates/blank.tsconfig.json && tsc -p templates/blank-intl.tsconfig.json"
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
31
32
|
"@clack/prompts": "^1.7.0",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Working in this Smoodly site
|
|
2
|
+
|
|
3
|
+
This is a Next.js app with Smoodly installed: content lives in Supabase,
|
|
4
|
+
the admin is mounted at /admin from the `smoodly` package, and the site
|
|
5
|
+
reads content through `smoodly/server.ts`. Read README.md first: it says
|
|
6
|
+
where things are.
|
|
7
|
+
|
|
8
|
+
## Rules
|
|
9
|
+
|
|
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`
|
|
13
|
+
is the one registry. A component that is not registered does not exist
|
|
14
|
+
for the admin.
|
|
15
|
+
- **A new collection is a new migration.** Write its view with
|
|
16
|
+
`collectionSQL` from `smoodly` into a new file under
|
|
17
|
+
`supabase/migrations/`, then apply it: `supabase db push` (hosted) or
|
|
18
|
+
`supabase db reset` (local). The admin cannot list a collection whose
|
|
19
|
+
view is missing.
|
|
20
|
+
- **Seed through the stores.** `scripts/seed.ts` is starter content,
|
|
21
|
+
written through the page and entry stores, never raw SQL. It is
|
|
22
|
+
idempotent; run it with `npm run seed`.
|
|
23
|
+
- **Never import the admin into the site.** Nothing under `app/(site)`,
|
|
24
|
+
`app/[locale]` or `components/` may import `@/smoodly/admin` or
|
|
25
|
+
`smoodly/admin/next`; the admin's chunk must not reach visitors.
|
|
26
|
+
- **Two root layouts.** The site's layout lives in its route group or
|
|
27
|
+
locale segment and is never the admin's parent; `app/(smoodly)/admin`
|
|
28
|
+
is a second root layout. Do not add an `app/layout.tsx`.
|
|
29
|
+
- **Toolsets and elements.** A richtext field names a toolset
|
|
30
|
+
(`smoodly/toolsets.ts`); a toolset embeds elements by value; the config
|
|
31
|
+
must register those elements too. Files the toolset imports use
|
|
32
|
+
relative paths, not `@/`.
|
|
33
|
+
- **Section styles are data attributes.** `<smoodly.Section>` renders the
|
|
34
|
+
root element and emits the config's section styles as `data-*`
|
|
35
|
+
attributes; `app/globals.css` styles them. Marks
|
|
36
|
+
(`smoodly.marks(schema)`) tie rendered elements to fields so the
|
|
37
|
+
editor can select and edit them in place.
|
|
38
|
+
- **Multilingual sites.** Content locales are Smoodly's; URL locales are
|
|
39
|
+
next-intl's. The locale set is `i18n/routing.ts`. UI strings of the
|
|
40
|
+
site's own go through next-intl's messages in `messages/<locale>.json`.
|
|
41
|
+
- **Fixed versus open pages.** A page shape with a `slug` is fixed: one
|
|
42
|
+
record, created for you, never deleted. Without one it is open: editors
|
|
43
|
+
create as many as they like.
|
|
44
|
+
|
|
45
|
+
## Commands
|
|
46
|
+
|
|
47
|
+
npm run dev # http://localhost:3000, /admin is the editor
|
|
48
|
+
npm run build # next build
|
|
49
|
+
npm run seed # starter content, idempotent
|
|
50
|
+
npm run typecheck # tsc --noEmit
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Smoodly site
|
|
2
|
+
|
|
3
|
+
Created by `create-smoodly-app`. Content lives in Supabase; the admin is
|
|
4
|
+
mounted at `/admin` from the `smoodly` package. AGENTS.md holds the rules
|
|
5
|
+
for an agent (or a person) building the site.
|
|
6
|
+
|
|
7
|
+
npm run dev # http://localhost:3000 — /admin is the editor
|
|
8
|
+
|
|
9
|
+
## Where things are
|
|
10
|
+
|
|
11
|
+
- `app/(site)/page.tsx` — the home page: one line calling renderSmoodlyPath([]).
|
|
12
|
+
- `app/(site)/[...path]/page.tsx` — every other page and every collection entry, at any depth.
|
|
13
|
+
- `app/(site)/layout.tsx` — the site's root layout: `app/globals.css`, the footer line from the `site` item; the admin has its own under `app/(smoodly)/admin/`.
|
|
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
|
+
- `components/sections/Text.tsx` — the one section: a title and a richtext body. Listed in `smoodly/sections.ts`.
|
|
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).
|
|
18
|
+
- `scripts/seed.ts` — the home page and the site item, written through the stores. `npm run seed` is idempotent.
|
|
19
|
+
- `app/globals.css` — a reset, a container, the two tones. Replace it.
|
|
20
|
+
- `.env.local` — `SMOODLY_URL`, `SMOODLY_SECRET_KEY`, `SMOODLY_PUBLISHABLE_KEY`.
|
|
21
|
+
|
|
22
|
+
## Add a section
|
|
23
|
+
|
|
24
|
+
Copy `components/sections/Text.tsx`, give the schema a new `name` and
|
|
25
|
+
`title`, declare its fields, render them inside `<smoodly.Section>` with
|
|
26
|
+
marks on each field. Add it to `sections` in `smoodly/sections.ts`, then
|
|
27
|
+
to the `sections` list of every zone that may hold it in
|
|
28
|
+
`components/pages/*.tsx`. It appears in the editor's "+" picker.
|
|
29
|
+
|
|
30
|
+
## Add an element
|
|
31
|
+
|
|
32
|
+
Copy `components/elements/Button.tsx` (relative imports only). Add it to
|
|
33
|
+
`elements` in `smoodly/elements.ts` and to the `elements` of the toolset
|
|
34
|
+
in `smoodly/toolsets.ts` that may embed it. It appears in the richtext
|
|
35
|
+
editor's "/" menu.
|
|
36
|
+
|
|
37
|
+
## Add a collection
|
|
38
|
+
|
|
39
|
+
Copy the `posts` block in `smoodly/collections.ts`, add the new
|
|
40
|
+
collection to `collections`, and write its entry page by copying
|
|
41
|
+
`components/pages/Post.tsx` (register it in `config.ts` under `pages`).
|
|
42
|
+
Then create a migration with its view:
|
|
43
|
+
|
|
44
|
+
import { collectionSQL } from "smoodly";
|
|
45
|
+
import { people } from "./smoodly/collections";
|
|
46
|
+
console.log(collectionSQL(people));
|
|
47
|
+
|
|
48
|
+
Save the output as `supabase/migrations/<timestamp>_people.sql` and apply
|
|
49
|
+
it with `supabase db push` (hosted) or `supabase db reset` (local).
|
|
50
|
+
|
|
51
|
+
## Add a shared item
|
|
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
|
|
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
|
|
57
|
+
Globals in the admin.
|
|
58
|
+
|
|
59
|
+
Reseed an empty database any time with `npm run seed`.
|
|
60
|
+
|
|
61
|
+
## Editors
|
|
62
|
+
|
|
63
|
+
Anyone who may open `/admin` is a Supabase Auth user with a row in the
|
|
64
|
+
`editors` table. To add one: create the user in Supabase Studio
|
|
65
|
+
(Authentication → Users), then in the SQL editor:
|
|
66
|
+
|
|
67
|
+
insert into editors (user_id, role) values ('<the user id>', 'editor');
|
|
68
|
+
|
|
69
|
+
## Hosted Supabase
|
|
70
|
+
|
|
71
|
+
supabase init # if the CLI has not been set up here yet
|
|
72
|
+
supabase link # pick the project
|
|
73
|
+
supabase db push # applies supabase/migrations/
|
|
74
|
+
|
|
75
|
+
Then put the project's URL and keys in `.env.local`.
|
|
@@ -4,8 +4,8 @@ type Params = Promise<{ path: string[] }>;
|
|
|
4
4
|
type SearchParams = Promise<Record<string, string | string[] | undefined>>;
|
|
5
5
|
|
|
6
6
|
// Smoodly: everything else — pages at any depth and collection entries
|
|
7
|
-
// under their `path`. Add app/<segment>/page.tsx beside this file
|
|
8
|
-
// wins for that exact path; deeper paths still resolve here.
|
|
7
|
+
// under their `path`. Add app/(site)/<segment>/page.tsx beside this file
|
|
8
|
+
// and it wins for that exact path; deeper paths still resolve here.
|
|
9
9
|
export async function generateMetadata({ params }: { params: Params }) {
|
|
10
10
|
return smoodlyMetadata((await params).path);
|
|
11
11
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { q } from "@/smoodly/server";
|
|
3
|
+
import "../globals.css";
|
|
4
|
+
|
|
5
|
+
// The site's root layout, in its own route group so the admin's layout
|
|
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
|
|
8
|
+
// the document title and the footer line: read through `q`, cached under
|
|
9
|
+
// its tag, null until published.
|
|
10
|
+
export async function generateMetadata() {
|
|
11
|
+
const site = await q().getShared("site");
|
|
12
|
+
return { title: site?.name ?? "Smoodly site" };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default async function RootLayout({ children }: { children: ReactNode }) {
|
|
16
|
+
const site = await q().getShared("site");
|
|
17
|
+
return (
|
|
18
|
+
<html lang="en">
|
|
19
|
+
<body>
|
|
20
|
+
<main>{children}</main>
|
|
21
|
+
{site && <footer className="container site-footer">{site.name} · {site.tagline}</footer>}
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
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. */
|
|
4
|
+
|
|
5
|
+
:root {
|
|
6
|
+
--paper: #ffffff;
|
|
7
|
+
--ink: #1a1a1a;
|
|
8
|
+
--muted: #666666;
|
|
9
|
+
--container: 720px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
* { box-sizing: border-box; }
|
|
13
|
+
html, body { margin: 0; padding: 0; }
|
|
14
|
+
body {
|
|
15
|
+
background: var(--paper);
|
|
16
|
+
color: var(--ink);
|
|
17
|
+
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
18
|
+
line-height: 1.5;
|
|
19
|
+
min-height: 100vh;
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
}
|
|
23
|
+
main { flex: 1; }
|
|
24
|
+
a { color: inherit; }
|
|
25
|
+
|
|
26
|
+
.container { width: min(var(--container), 100% - 2rem); margin-inline: auto; }
|
|
27
|
+
|
|
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; }
|
|
32
|
+
|
|
33
|
+
.prose > * + * { margin-top: 1em; }
|
|
34
|
+
.prose h1, .prose h2, .prose h3 { line-height: 1.2; }
|
|
35
|
+
|
|
36
|
+
/* The Button element, inside richtext. */
|
|
37
|
+
.button {
|
|
38
|
+
display: inline-block;
|
|
39
|
+
padding: 0.6em 1.2em;
|
|
40
|
+
border: 1px solid currentColor;
|
|
41
|
+
border-radius: 4px;
|
|
42
|
+
text-decoration: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.site-footer { padding-block: 2rem; color: var(--muted); font-size: 0.875rem; }
|
|
46
|
+
.locales { display: flex; gap: 0.75rem; padding-block: 1rem; font-size: 0.875rem; }
|
|
47
|
+
.locales a[aria-current="page"] { font-weight: 600; }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { f, smoodly, type Props } from "smoodly";
|
|
2
|
+
|
|
3
|
+
// An Element: a small registered block editors insert inside richtext
|
|
4
|
+
// (the `richtext` context). This one is a link styled as a button. Copy
|
|
5
|
+
// this file to add another element, then list it in smoodly/elements.ts
|
|
6
|
+
// and in the toolsets that may embed it (smoodly/toolsets.ts).
|
|
7
|
+
// Relative imports only in this file and in smoodly/toolsets.ts: the
|
|
8
|
+
// template build imports them outside the app, where `@/` has no mapping.
|
|
9
|
+
export const buttonSchema = smoodly.schema.element({
|
|
10
|
+
name: "button",
|
|
11
|
+
title: "Button",
|
|
12
|
+
icon: "→",
|
|
13
|
+
description: "A link styled as a button.",
|
|
14
|
+
contexts: ["richtext"],
|
|
15
|
+
fields: {
|
|
16
|
+
label: f.text().max(60),
|
|
17
|
+
href: f.link(),
|
|
18
|
+
},
|
|
19
|
+
sample: { label: "Read more", href: "/" },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function ButtonView({ label, href }: Props<typeof buttonSchema>) {
|
|
23
|
+
return <a className="button" href={href}>{label}</a>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const Button = smoodly.element(buttonSchema, ButtonView);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { smoodly, z, Zone, type Props } from "smoodly";
|
|
2
|
+
import { Text } from "../sections/Text";
|
|
3
|
+
|
|
4
|
+
// A page shape: zones declared, <Zone /> placed, one file. `slug` makes
|
|
5
|
+
// it fixed — one record editors can neither delete nor duplicate — and
|
|
6
|
+
// the config's homePageSlug maps that record to "/". The body is a
|
|
7
|
+
// freeform zone over the sections listed here.
|
|
8
|
+
export const homeSchema = smoodly.schema.page({
|
|
9
|
+
name: "home",
|
|
10
|
+
title: "Home",
|
|
11
|
+
slug: "home",
|
|
12
|
+
zones: {
|
|
13
|
+
body: z.freeform({ sections: [Text] }),
|
|
14
|
+
},
|
|
15
|
+
meta: { seo: true },
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function HomeView({ zones }: Props<typeof homeSchema>) {
|
|
19
|
+
return <Zone of={zones.body} />;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const HomePage = smoodly.page(homeSchema, HomeView);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { smoodly, z, Zone, type Props } from "smoodly";
|
|
2
|
+
import { Text } from "../sections/Text";
|
|
3
|
+
|
|
4
|
+
// An open page shape: no `slug`, so editors create as many as they like,
|
|
5
|
+
// anywhere in the tree the config allows. `page` is where this one sits
|
|
6
|
+
// (title, path, ancestors, children).
|
|
7
|
+
export const pageSchema = smoodly.schema.page({
|
|
8
|
+
name: "page",
|
|
9
|
+
title: "Page",
|
|
10
|
+
zones: {
|
|
11
|
+
body: z.freeform({ sections: [Text] }),
|
|
12
|
+
},
|
|
13
|
+
meta: { seo: true },
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function PageView({ zones, page }: Props<typeof pageSchema>) {
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<h1 className="container">{page.title}</h1>
|
|
20
|
+
<Zone of={zones.body} />
|
|
21
|
+
</>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const StandardPage = smoodly.page(pageSchema, PageView);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { smoodly, type EntryPageProps } from "smoodly";
|
|
2
|
+
import { RichText } from "@/smoodly/richtext";
|
|
3
|
+
import { posts } from "@/smoodly/collections";
|
|
4
|
+
|
|
5
|
+
// The entry page for `posts`, served at /posts/<slug>. It receives the
|
|
6
|
+
// entry's fields; the body is richtext rendered by the site's bound
|
|
7
|
+
// RichText, which knows the registered elements.
|
|
8
|
+
type PostFields = { title: string; date?: string; body?: unknown };
|
|
9
|
+
|
|
10
|
+
function PostView({ entry }: EntryPageProps) {
|
|
11
|
+
const post = entry.fields as PostFields;
|
|
12
|
+
return (
|
|
13
|
+
<article className="container prose">
|
|
14
|
+
<h1>{post.title}</h1>
|
|
15
|
+
{post.date && <p><time dateTime={post.date}>{post.date.slice(0, 10)}</time></p>}
|
|
16
|
+
<RichText doc={post.body} />
|
|
17
|
+
</article>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const PostPage = smoodly.entryPage(posts, PostView);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { f, smoodly, type Props } from "smoodly";
|
|
2
|
+
import { RichText } from "@/smoodly/richtext";
|
|
3
|
+
import { text } from "@/smoodly/toolsets";
|
|
4
|
+
|
|
5
|
+
// A section: a title and a richtext body. Registered in
|
|
6
|
+
// smoodly/sections.ts and allowed in the page shapes' zones
|
|
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.
|
|
10
|
+
export const textSchema = smoodly.schema.section({
|
|
11
|
+
name: "text",
|
|
12
|
+
title: "Text",
|
|
13
|
+
description: "A title and a body of text.",
|
|
14
|
+
fields: {
|
|
15
|
+
title: f.text(),
|
|
16
|
+
body: f.richtext({ toolset: text }),
|
|
17
|
+
},
|
|
18
|
+
sample: {
|
|
19
|
+
title: "Hello",
|
|
20
|
+
body: { type: "doc", content: [{ type: "paragraph", content: [{ type: "text", text: "Write something here." }] }] },
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Marks tie the rendered elements to their fields, so the editor can
|
|
25
|
+
// select and inline-edit them on the canvas.
|
|
26
|
+
const m = smoodly.marks(textSchema);
|
|
27
|
+
|
|
28
|
+
function TextView({ title, body, styles }: Props<typeof textSchema>) {
|
|
29
|
+
return (
|
|
30
|
+
<smoodly.Section styles={styles}>
|
|
31
|
+
<div className="container prose">
|
|
32
|
+
<h2 {...m.field("title")}>{title}</h2>
|
|
33
|
+
<div {...m.field("body")}><RichText doc={body} /></div>
|
|
34
|
+
</div>
|
|
35
|
+
</smoodly.Section>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const Text = smoodly.section(textSchema, TextView);
|