@sudajs/cli 0.10.4 → 0.12.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sudajs/cli",
3
- "version": "0.10.4",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "suda": "./bin/suda.js"
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@modelcontextprotocol/sdk": "^1.29.0",
28
- "@puckeditor/core": "^0.21.3",
28
+ "@puckeditor/core": "^0.22.0",
29
29
  "commander": "^12.1.0",
30
30
  "esbuild": "^0.25.12",
31
31
  "lucide-react": "^1.17.0",
@@ -34,7 +34,7 @@
34
34
  "react": "^19.2.7",
35
35
  "react-dom": "^19.2.7",
36
36
  "zod": "^3.24.1",
37
- "@sudajs/theme-engine": "2.6.2"
37
+ "@sudajs/theme-engine": "4.0.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@tailwindcss/postcss": "^4.3.0",
@@ -4,17 +4,347 @@ This directory is a standalone Vite + React + Tailwind Suda theme generated by `
4
4
 
5
5
  Your job in a theme project is to create a complete, publishable theme with theme-specific components, realistic starter pages, scoped styling, assets, and AI metadata. Do not build pages by assembling generic engine components; those are reserved for end users who need supplemental editing blocks after the theme is installed.
6
6
 
7
+ ## Theme-building workflow
8
+
9
+ When you add or change a section, complete the whole theme authoring loop in
10
+ one pass:
11
+
12
+ 1. Define the props type. Use serializable values only. Use `Slot` only for
13
+ props that are backed by `blockSlots`.
14
+ 2. Export a `SudaComponentConfig<Props>` with `label`, `ai.instructions`,
15
+ `fields`, optional `blockSlots`, `defaultProps`, and `render`.
16
+ 3. Keep `fields` and `defaultProps` aligned. Every normal field should have a
17
+ sensible value in `defaultProps`; do not put block-slot props in
18
+ `defaultProps`. Do not put `id` in component `defaultProps`; `id` belongs
19
+ only to concrete top-level page/layout content items.
20
+ 4. Add every editor-facing label key to `src/locales/en.json` when you add the
21
+ component, field, option, or local block.
22
+ 5. Register the component in `src/config.ts` under `pageConfig.components`.
23
+ Register only shared chrome components such as `Header`, `Footer`, and
24
+ `PageOutlet` under `layoutConfig.components`.
25
+ 6. Use the component in starter pages and CMS templates with public component
26
+ keys from `pageConfig.components`.
27
+ 7. Wrap authored page data with `defineSudaPageData(pageConfig, data)` whenever
28
+ practical so TypeScript checks top-level component types and block-slot
29
+ nested `type` values.
30
+ 8. Run `pnpm typecheck`, `pnpm lint`, `pnpm build`, and `pnpm validate` before
31
+ handoff.
32
+
33
+ Configure ordinary fields like this:
34
+
35
+ ```ts
36
+ type HeroProps = {
37
+ eyebrow?: string;
38
+ title?: string;
39
+ body?: string;
40
+ align?: "left" | "center";
41
+ featured?: boolean;
42
+ };
43
+
44
+ export const Hero: SudaComponentConfig<HeroProps> = {
45
+ label: t("sections.hero.label"),
46
+ ai: {
47
+ instructions:
48
+ "Landing page hero section. Configure concise copy and a layout choice.",
49
+ },
50
+ fields: {
51
+ eyebrow: { type: "text", label: t("sections.hero.fields.eyebrow") },
52
+ title: { type: "text", label: t("sections.hero.fields.title") },
53
+ body: { type: "textarea", label: t("sections.hero.fields.body") },
54
+ align: {
55
+ type: "select",
56
+ label: t("sections.hero.fields.align"),
57
+ options: [
58
+ { label: t("common.options.left"), value: "left" },
59
+ { label: t("common.options.center"), value: "center" },
60
+ ],
61
+ },
62
+ featured: { type: "checkbox", label: t("sections.hero.fields.featured") },
63
+ },
64
+ defaultProps: {
65
+ eyebrow: "New",
66
+ title: "Welcome",
67
+ body: "Describe the main offer in one or two sentences.",
68
+ align: "left",
69
+ featured: true,
70
+ },
71
+ render: ({ eyebrow, title, body, align, featured }) => (
72
+ <section data-align={align} data-featured={featured ? "true" : "false"}>
73
+ {eyebrow ? <p>{eyebrow}</p> : null}
74
+ <h1>{title}</h1>
75
+ {body ? <p>{body}</p> : null}
76
+ </section>
77
+ ),
78
+ };
79
+ ```
80
+
81
+ Use normal fields for scalar section settings such as text, textarea, number,
82
+ range, checkbox, select/radio options, media, URL, icon, color, object, and
83
+ array values. Put those field values in the host component's `defaultProps`.
84
+ Use `blockSlots` only when the section owns nested local blocks that editors
85
+ and AI should add, remove, or reorder independently.
86
+
87
+ Field source of truth:
88
+
89
+ - Use TypeScript, not a hand-written runtime schema. Type props first, then let
90
+ `SudaComponentConfig<Props>`, `SudaFields<Props>`, `SudaField`, and
91
+ `defineSudaPageData(pageConfig, data)` catch mismatches.
92
+ - There is no theme-authored zod schema for fields. Do not invent one in the
93
+ theme. Run `pnpm typecheck` and `pnpm validate` instead.
94
+ - Every field entry needs a `type`. Add a localized `label` for every
95
+ editor-facing field.
96
+ - Put a matching serializable value in `defaultProps` for every normal field.
97
+ Optional props can still have defaults; defaults make editor insertion and AI
98
+ generation reliable.
99
+ - Use `description`, `placeholder`, and `visibleIf` only when they help the
100
+ editor or AI choose the right value.
101
+ - Suda extended field types are `url`, `icon`, `font`, `menu`, `color`,
102
+ `range`, `spacing`, `media`, `image`, `video`, and `posts`. These are valid
103
+ in `fields` and are normalized by `@sudajs/theme-engine`.
104
+
105
+ Use these field patterns:
106
+
107
+ ```ts
108
+ fields: {
109
+ // Plain copy
110
+ title: { type: "text", label: t("common.fields.title") },
111
+ body: { type: "textarea", label: t("common.fields.description") },
112
+ richBody: { type: "richtext", label: t("common.fields.content") },
113
+
114
+ // Numbers and booleans
115
+ columns: { type: "number", label: t("common.fields.columns") },
116
+ featured: { type: "checkbox", label: t("common.fields.featured") },
117
+ opacity: {
118
+ type: "range",
119
+ label: t("common.fields.opacity"),
120
+ min: 0,
121
+ max: 100,
122
+ step: 5,
123
+ unit: "%",
124
+ },
125
+
126
+ // Choices: always provide options with localized labels and serializable values.
127
+ align: {
128
+ type: "radio",
129
+ label: t("common.fields.align"),
130
+ options: [
131
+ { label: t("common.options.left"), value: "left" },
132
+ { label: t("common.options.center"), value: "center" },
133
+ ],
134
+ },
135
+ tone: {
136
+ type: "select",
137
+ label: t("common.fields.tone"),
138
+ options: [
139
+ { label: t("common.options.light"), value: "light" },
140
+ { label: t("common.options.dark"), value: "dark" },
141
+ ],
142
+ },
143
+
144
+ // Suda extended fields
145
+ href: { type: "url", label: t("common.fields.link") },
146
+ image: { type: "image", label: t("common.fields.image") },
147
+ video: { type: "video", label: t("common.fields.video") },
148
+ download: { type: "media", kind: "file", label: t("common.fields.file") },
149
+ icon: { type: "icon", label: t("common.fields.icon") },
150
+ accentColor: { type: "color", label: t("common.fields.color") },
151
+ font: { type: "font", label: t("common.fields.font") },
152
+ links: { type: "menu", label: t("common.fields.menu") },
153
+ spacing: { type: "spacing", label: t("common.fields.spacing") },
154
+
155
+ // Dynamic post lists. Use this only on page sections that intentionally
156
+ // render post resources, not on fixed CMS main sections.
157
+ postList: { type: "posts", label: t("common.fields.posts") },
158
+
159
+ // Objects and arrays need nested typed fields.
160
+ badge: {
161
+ type: "object",
162
+ label: t("common.fields.badge"),
163
+ objectFields: {
164
+ label: { type: "text", label: t("common.fields.label") },
165
+ tone: { type: "text", label: t("common.fields.tone") },
166
+ },
167
+ },
168
+ cards: {
169
+ type: "array",
170
+ label: t("common.fields.cards"),
171
+ getItemSummary: (item) => item.title || "Card",
172
+ defaultItemProps: { title: "Card title", body: "Card body" },
173
+ arrayFields: {
174
+ title: { type: "text", label: t("common.fields.title") },
175
+ body: { type: "textarea", label: t("common.fields.description") },
176
+ href: { type: "url", label: t("common.fields.link") },
177
+ },
178
+ },
179
+ }
180
+ ```
181
+
182
+ Match those fields with defaults:
183
+
184
+ ```ts
185
+ defaultProps: {
186
+ title: "Welcome",
187
+ body: "Describe the offer.",
188
+ richBody: "<p>Long-form content.</p>",
189
+ columns: 3,
190
+ featured: true,
191
+ opacity: 80,
192
+ align: "left",
193
+ tone: "light",
194
+ href: "/contact",
195
+ image: themeAsset("assets/hero.jpg"),
196
+ video: "",
197
+ download: "",
198
+ icon: "sparkles",
199
+ accentColor: "#2563eb",
200
+ font: "system",
201
+ links: [{ label: "Home", url: "/" }],
202
+ spacing: "md",
203
+ postList: { strategy: "featured", limit: 3 },
204
+ badge: { label: "New", tone: "primary" },
205
+ cards: [{ title: "Fast setup", body: "Launch quickly.", href: "/features" }],
206
+ }
207
+ ```
208
+
209
+ Starter page and CMS template data must use this shape:
210
+
211
+ ```ts
212
+ defineSudaPageData(pageConfig, {
213
+ root: { props: {} },
214
+ content: [
215
+ {
216
+ type: "Hero",
217
+ props: {
218
+ id: "Hero-1",
219
+ title: "Welcome",
220
+ actions: [
221
+ {
222
+ type: "button",
223
+ props: { label: "Contact us", href: "/contact" },
224
+ },
225
+ ],
226
+ },
227
+ },
228
+ ],
229
+ });
230
+ ```
231
+
232
+ Top-level `content[].type` must be a key registered in `pageConfig.components`.
233
+ Top-level `content[].props.id` is required in starter pages, CMS templates, and
234
+ layout data. Do not put `id` in component `defaultProps`, local block
235
+ `defaultProps`, `defaultBlocks`, or block-slot nested items.
236
+ Nested block-slot items must use short local block kind names such as
237
+ `"button"`, `"copy"`, or `"cards"`. Never write
238
+ `__suda_local_block__/...` in source templates, starter pages, CMS templates,
239
+ or AI examples.
240
+
7
241
  ## Theme contract
8
242
 
9
243
  - Keep `renderMode: "ssr"` in `src/manifest.ts`.
10
244
  - Keep source entries at `src/index.tsx`, `src/runtime.client.ts`, and `src/styles.css`.
11
- - Export a complete `ThemeModule` from `src/index.tsx`: `manifest`, `pageConfig`, `layoutConfig`, `defaultLayout`, and `starterPages`.
12
- - Starter page slugs preview at site-like root routes in `suda theme dev`: `home` is `/home`, `contact-us` is `/contact-us`. Do not use `/pages/...` as a preview route prefix.
245
+ - Export a complete `ThemeModule` from `src/index.tsx`: `manifest`, `pageConfig`, `layoutConfig`, `defaultLayout`, `starterPages`, and `cmsTemplates`.
246
+ - Starter page slugs preview at site-like root routes in `suda theme dev`: `index` is `/index`, `contact-us` is `/contact-us`. Do not use `/pages/...` as a preview route prefix.
13
247
  - Keep `pageConfig` focused on page content sections. Keep `layoutConfig` focused on shared site chrome such as root, header, page outlet, and footer.
14
248
  - Do not edit generated files under `dist/`; run `pnpm build` to regenerate them.
15
249
  - React, React DOM, Puck, and `@sudajs/theme-engine` are host-provided peers. Do not bundle private copies into the theme runtime.
16
250
  - Keep persisted props JSON-serializable. Do not store functions, React nodes, class instances, database ids for media, or environment-specific absolute filesystem paths.
17
251
 
252
+ ## CMS and starter templates
253
+
254
+ Suda CMS is a built-in fixed post system. Themes must provide exactly these
255
+ four CMS templates through `cmsTemplates`:
256
+
257
+ - `posts`: post list template for `/posts`
258
+ - `post`: post detail template for `/posts/:slug`
259
+ - `tags`: tag/category list template for `/tags`
260
+ - `tag`: posts filtered by one tag for `/tags/:slug`
261
+
262
+ Each `cmsTemplates` value must be an object with `{ title: string; data:
263
+ PageData }`. Do not point a key directly at raw `PageData`, and do not use
264
+ intersection types such as `ThemeModule & { cmsTemplates:
265
+ Record<string, PageData> }` to bypass the current contract.
266
+
267
+ ```ts
268
+ export const cmsTemplates: ThemeCmsTemplates = {
269
+ posts: { title: "Posts", data: postIndexTemplate },
270
+ post: { title: "Post", data: postDetailTemplate },
271
+ tags: { title: "Tags", data: tagIndexTemplate },
272
+ tag: { title: "Tag", data: tagDetailTemplate },
273
+ };
274
+ ```
275
+
276
+ Do not create Shopify-style template variants such as `post.default`,
277
+ `post.modern`, or `posts.magazine`. Do not add arbitrary collection/content
278
+ types or manual data-source binding contracts. Keep CMS-only sections limited
279
+ to the CMS templates where they belong.
280
+
281
+ Create CMS template main sections like this:
282
+
283
+ - `MainPosts`: place only on `posts` and optionally `tag` templates with
284
+ `placement: { cmsTemplates: ["posts", "tag"] }`; render `cms.posts`
285
+ directly and use `cms.pagination` for pagination controls. Use
286
+ `cms.currentTag` when a tag filter is active.
287
+ - `MainPost`: place only on the `post` template; render the current article
288
+ when `cms.type === "post"`.
289
+ - `MainTags`: place only on the `tags` template; render tag/category cards
290
+ when `cms.type === "tags"`.
291
+ - `MainTagPosts` can be a separate tag-page list section, or `MainPosts` can
292
+ handle both `posts` and `tag` contexts. Choose the clearer API for the theme.
293
+
294
+ In CMS template main sections, call `getCmsContent(puck?.metadata)` and render
295
+ the matching CMS data. Do not add `type: "posts"` fields to those sections.
296
+
297
+ For pagination controls, use `cms.pagination.page`,
298
+ `cms.pagination.totalPages`, `cms.pagination.hasPreviousPage`, and
299
+ `cms.pagination.hasNextPage` to decide what to show. Do not hand-build
300
+ pagination query strings. Import `cmsPaginationUrl` from
301
+ `@sudajs/theme-engine/runtime` and call
302
+ `cmsPaginationUrl(cms.pagination, targetPage)` for numbered links so existing
303
+ filter/search parameters stay in the URL. Use `cms.pagination.previousUrl` and
304
+ `cms.pagination.nextUrl` for previous/next buttons when present.
305
+
306
+ Themes must also provide at least one reusable ordinary-page post section, for
307
+ example `FeaturedPosts`, `LatestPosts`, or `TopicPosts`. Use it for homepage
308
+ insights, featured articles, case studies, news teasers, or tag-filtered post
309
+ groups on ordinary pages.
310
+
311
+ Define that section with one or more top-level `type: "posts"` fields and a
312
+ default query on each matching prop:
313
+
314
+ ```tsx
315
+ fields: {
316
+ postList: { type: "posts" },
317
+ },
318
+ defaultProps: {
319
+ postList: { strategy: "featured", limit: 3 },
320
+ },
321
+ ```
322
+
323
+ Supported strategies are `latest`, `featured`, `by_tag`, and `manual`. Manual
324
+ queries store only `items` and must not include `limit`. Runtime post resources
325
+ are keyed as `${props.id}.${fieldKey}` with both parts URL-encoded by the
326
+ platform. In the section render function, call
327
+ `getPostResource(puck?.metadata, { componentId: props.id, fieldKey: "postList" })`
328
+ with the exact posts field key. Never import Prisma, call platform APIs, or
329
+ query the database from theme code.
330
+
331
+ Starter pages must include one home/index page with `isHome: true`. Recommended
332
+ starter pages include `index`, `about-us`, `contact-us`, `services`, and `team`.
333
+ Those recommended pages are not hard requirements, but a publishable theme
334
+ should usually include several realistic pages so AI site generation has a rich
335
+ reference set.
336
+
337
+ Treat repetition as a design and business judgment, not a fixed rule. Let the
338
+ site strategy decide where pages should share patterns and where they should
339
+ diverge. Reusing strong sections across starter pages is often right: CTA,
340
+ contact, booking, newsletter, download, and other conversion sections may appear
341
+ repeatedly when they help visitors move forward. Avoid only mechanical
342
+ copy-paste that makes different pages feel interchangeable. When useful, give
343
+ each page its own purpose, angle, proof points, media, or ordering, while
344
+ keeping repeated business-driving sections where they serve the user journey.
345
+ Richer starter pages make the theme easier for AI to adapt without making users
346
+ learn CMS/template mechanics.
347
+
18
348
  ## Editor i18n and locales
19
349
 
20
350
  Theme editor labels are localized by the host editor. Use the template's
@@ -156,14 +486,20 @@ Good component instructions explain:
156
486
 
157
487
  ## Block slot authoring rules
158
488
 
159
- Use `blockSlots` when a section owns controlled local content that benefits from independent editor selection and configuration, such as hero actions, pricing cards, feature rows, stats, timeline items, or contact methods. Keep ordinary section props in `fields`.
489
+ Use `blockSlots` when a section owns controlled local content such as hero
490
+ actions, pricing cards, feature rows, stats, timeline items, or contact
491
+ methods. Keep ordinary section props in `fields`.
160
492
 
161
- - Type the slot prop as Puck `Slot` and declare `blockSlots` with the same prop key. This is the Suda block-slot API; do not create a native `fields.<key>.type = "slot"` yourself.
162
- - Define local block kinds under `blockSlots.<slotName>.blocks`. Local blocks support normal Suda fields, `label`, `defaultProps`, `render`, `metadata`, `inline`, `permissions`, and optional `ai`.
493
+ - Type the slot prop as Puck `Slot` and declare `blockSlots` with the same prop key.
494
+ - Do not create a native `fields.<key>.type = "slot"` in page components.
495
+ - Define local block kinds under `blockSlots.<slotName>.blocks`.
496
+ - Give every local block `label`, useful fields, `defaultProps`, `render`, and
497
+ optional `ai.instructions` when the block needs generation guidance.
163
498
  - Keep local blocks specific to the owning section. They are not reusable global page components.
164
- - Do not add nested `blockSlots` or `cms` to local blocks. Block slots are one level deep in the theme authoring API.
499
+ - Do not add nested `blockSlots` to local blocks. Block slots are one level deep in the theme authoring API.
165
500
  - Do not put the slot prop in the host component's `defaultProps`; use `defaultBlocks` on the slot instead.
166
501
  - Do not hand-write `id` in local block `defaultProps`, `defaultBlocks[].props`, or starter page nested block props. The editor/runtime owns block ids.
502
+ - In authored page data, starter pages, CMS templates, and AI examples, nested block-slot items must use the short local block kind as `type`, exactly matching a key in `blockSlots.<slotName>.blocks`. Never write internal local block types such as `__suda_local_block__/Hero/actions/button`; `@sudajs/theme-engine` generates those only for Puck internals.
167
503
 
168
504
  Example:
169
505
 
@@ -171,29 +507,35 @@ Example:
171
507
  import type { Slot } from "@puckeditor/core";
172
508
  import type { SudaComponentConfig } from "@sudajs/theme-engine";
173
509
 
510
+ import { t } from "./i18n.js";
511
+
174
512
  type HeroProps = {
175
513
  title?: string;
176
514
  actions?: Slot;
177
515
  };
178
516
 
179
517
  export const Hero: SudaComponentConfig<HeroProps> = {
180
- label: "Hero",
518
+ label: t("sections.hero.label"),
181
519
  ai: {
182
520
  instructions:
183
521
  "Primary page introduction for landing pages. Use once near the top with one or two local action blocks.",
184
522
  },
185
523
  fields: {
186
- title: { type: "text", label: "Title" },
524
+ title: { type: "text", label: t("sections.hero.fields.title") },
187
525
  },
188
526
  blockSlots: {
189
527
  actions: {
190
- label: "Actions",
528
+ label: t("sections.hero.blocks.actions"),
191
529
  blocks: {
192
530
  button: {
193
- label: "Button",
531
+ label: t("sections.hero.blocks.button"),
532
+ ai: {
533
+ instructions:
534
+ "Primary or secondary hero action. Use one or two buttons with clear labels and valid links.",
535
+ },
194
536
  fields: {
195
- label: { type: "text", label: "Label" },
196
- href: { type: "url", label: "Link" },
537
+ label: { type: "text", label: t("sections.hero.blocks.buttonLabel") },
538
+ href: { type: "url", label: t("sections.hero.blocks.buttonHref") },
197
539
  },
198
540
  defaultProps: {
199
541
  label: "Get started",
@@ -217,7 +559,7 @@ export const Hero: SudaComponentConfig<HeroProps> = {
217
559
  };
218
560
  ```
219
561
 
220
- Starter page data should use the public host component type and a standard array on the block-slot prop. Use local block `kind` values as nested `type` values.
562
+ Starter page data should use the public host component type and a standard array on the block-slot prop. Use local block `kind` values as nested `type` values; for the example above, `actions[].type` can only be `"button"`.
221
563
 
222
564
  ```ts
223
565
  {
@@ -244,17 +586,12 @@ Starter page data should use the public host component type and a standard array
244
586
  - Every starter page is Puck `Data` with `root: { props: {} }` and `content: [...]`.
245
587
  - Each top-level component instance in starter page `content` must include a stable `props.id`.
246
588
  - Use only components registered in `pageConfig.components` for top-level page content.
589
+ - For any `blockSlots` prop, use only short local block kinds as nested `type` values, never `__suda_local_block__/...` internal types.
590
+ - Do not put local block `id` values in starter pages. Only top-level page components need `props.id`.
591
+ - Where practical, wrap authored data with `defineSudaPageData(pageConfig, data)` so TypeScript checks top-level component keys and block-slot nested `type` values.
247
592
  - Match starter page content to the theme's intended audience and category. The home page should show the theme's best composition, not just every component in order.
248
593
  - Use `themeAsset("assets/...")` for bundled starter media.
249
594
 
250
- ## CMS-aware themes
251
-
252
- - If the theme supports posts or tags, declare `content.views` on the exported `ThemeModule`.
253
- - Add one starter page with matching `cmsView` for every declared CMS view.
254
- - Declare component-level `cms` capabilities only on components that can render that CMS source.
255
- - Use `cmsBinding` in starter page props when a component should receive CMS data at render time.
256
- - Keep static fallback props for CMS components so the page still renders meaningfully when CMS payloads are absent.
257
-
258
595
  ## Commands
259
596
 
260
597
  ```bash
@@ -19,7 +19,7 @@
19
19
  "devDependencies": {
20
20
  "@eslint/js": "^9.39.4",
21
21
  "@tailwindcss/postcss": "^4.3.0",
22
- "@puckeditor/core": "^0.21.3",
22
+ "@puckeditor/core": "^0.22.0",
23
23
  "@sudajs/cli": "*",
24
24
  "@sudajs/theme-engine": "*",
25
25
  "@types/node": "^24.13.2",
@@ -42,7 +42,7 @@
42
42
  "vite": "^8.1.0"
43
43
  },
44
44
  "peerDependencies": {
45
- "@puckeditor/core": "^0.21.3",
45
+ "@puckeditor/core": "^0.22.0",
46
46
  "@sudajs/theme-engine": "*",
47
47
  "lucide-react": "^1.17.0",
48
48
  "react": "^19.2.7",
@@ -2,7 +2,7 @@ import type { ThemeManifest, ThemeModule } from "@sudajs/theme-engine";
2
2
 
3
3
  import { defaultLayout, layoutConfig, pageConfig } from "./config.js";
4
4
  import { sourceManifest } from "./manifest.js";
5
- import { starterPages } from "./templates.js";
5
+ import { cmsTemplates, starterPages } from "./templates.js";
6
6
 
7
7
  // Single source of truth: theme version comes from package.json#version,
8
8
  // injected by Vite as a compile-time constant so the published bundle stays
@@ -15,7 +15,8 @@ const theme: ThemeModule = {
15
15
  layoutConfig,
16
16
  defaultLayout,
17
17
  starterPages,
18
+ cmsTemplates,
18
19
  };
19
20
 
20
21
  export default theme;
21
- export { pageConfig, layoutConfig, defaultLayout, starterPages };
22
+ export { pageConfig, layoutConfig, defaultLayout, starterPages, cmsTemplates };
@@ -1,11 +1,21 @@
1
1
  import type { SudaComponentConfig, SudaRootConfig } from "@sudajs/theme-engine";
2
- import { getIcpRecord, getPageSlot, getWhiteLabel } from "@sudajs/theme-engine/runtime";
2
+ import {
3
+ createThemeDesignCssVariables,
4
+ createThemeDesignDefault,
5
+ designSystemField,
6
+ getIcpRecord,
7
+ getPageSlot,
8
+ getWhiteLabel,
9
+ resolveThemeDesignTokens,
10
+ type ThemeDesignValue,
11
+ } from "@sudajs/theme-engine/runtime";
3
12
  import type { ReactElement, ReactNode } from "react";
4
13
 
5
14
  import { t } from "./i18n.js";
15
+ import { sourceManifest } from "./manifest.js";
6
16
 
7
17
  type PuckExtras = { puck?: { metadata?: Record<string, unknown> } };
8
- type RootProps = { children?: ReactNode; title?: string };
18
+ type RootProps = { children?: ReactNode; title?: string; designSystem?: ThemeDesignValue };
9
19
  type HeaderProps = { siteName?: string };
10
20
  type PageOutletProps = PuckExtras;
11
21
  type FooterProps = { text?: string } & PuckExtras;
@@ -13,13 +23,21 @@ type FooterProps = { text?: string } & PuckExtras;
13
23
  export const rootConfig: SudaRootConfig<RootProps> = {
14
24
  fields: {
15
25
  title: { type: "text", label: t("layout.root.fields.title") },
26
+ designSystem: designSystemField(t("layout.root.fields.designSystem"), sourceManifest.designSystem),
27
+ },
28
+ defaultProps: {
29
+ title: "__SUDA_THEME_KEY__",
30
+ designSystem: createThemeDesignDefault(sourceManifest.designSystem),
31
+ },
32
+ render: ({ children, designSystem, title }) => {
33
+ const tokens = resolveThemeDesignTokens(sourceManifest.designSystem, designSystem);
34
+ const style = createThemeDesignCssVariables(tokens);
35
+ return (
36
+ <div className="__SUDA_THEME_KEY__-root" data-site-name={title} style={style}>
37
+ {children}
38
+ </div>
39
+ );
16
40
  },
17
- defaultProps: { title: "__SUDA_THEME_KEY__" },
18
- render: ({ children, title }) => (
19
- <div className="__SUDA_THEME_KEY__-root" data-site-name={title}>
20
- {children}
21
- </div>
22
- ),
23
41
  };
24
42
 
25
43
  export const Header: SudaComponentConfig<HeaderProps> = {
@@ -2,7 +2,8 @@
2
2
  "layout": {
3
3
  "root": {
4
4
  "fields": {
5
- "title": "Site title"
5
+ "title": "Site title",
6
+ "designSystem": "Design system"
6
7
  }
7
8
  },
8
9
  "header": {
@@ -48,6 +49,14 @@
48
49
  "featureDescription": "Description"
49
50
  }
50
51
  },
52
+ "featuredPosts": {
53
+ "label": "Featured posts",
54
+ "fields": {
55
+ "title": "Title",
56
+ "description": "Description",
57
+ "postList": "Post list"
58
+ }
59
+ },
51
60
  "testimonial": {
52
61
  "label": "Testimonial",
53
62
  "fields": {
@@ -8,7 +8,55 @@ export const sourceManifest: ThemeSourceManifest = {
8
8
  key: "__SUDA_THEME_KEY__",
9
9
  name: "__SUDA_THEME_KEY__",
10
10
  categories: ["other"],
11
- minEngineVersion: "0.0.0",
11
+ minEngineVersion: "4.0.0",
12
+ designSystem: {
13
+ version: 1,
14
+ defaultPresetId: "default",
15
+ presets: [
16
+ {
17
+ id: "default",
18
+ label: "Default",
19
+ tokens: {
20
+ colors: {
21
+ background: "#ffffff",
22
+ foreground: "#111827",
23
+ primary: "#111827",
24
+ primaryForeground: "#ffffff",
25
+ accent: "#f8fafc",
26
+ accentForeground: "#111827",
27
+ muted: "#f4f5f7",
28
+ mutedForeground: "#4b5563",
29
+ },
30
+ radius: {
31
+ card: "8px",
32
+ button: "8px",
33
+ input: "8px",
34
+ },
35
+ },
36
+ },
37
+ {
38
+ id: "ocean",
39
+ label: "Ocean",
40
+ tokens: {
41
+ colors: {
42
+ background: "#f8fbff",
43
+ foreground: "#0e1b36",
44
+ primary: "#2563eb",
45
+ primaryForeground: "#ffffff",
46
+ accent: "#dbeafe",
47
+ accentForeground: "#0e1b36",
48
+ muted: "#eff6ff",
49
+ mutedForeground: "#42526e",
50
+ },
51
+ radius: {
52
+ card: "14px",
53
+ button: "9999px",
54
+ input: "10px",
55
+ },
56
+ },
57
+ },
58
+ ],
59
+ },
12
60
  entry: "index.js",
13
61
  clientEntry: "runtime.client.js",
14
62
  // Required. SudaCloud currently hosts SSR themes only; CSR/Hybrid postures