@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/dist/index.d.ts +712 -0
- package/dist/index.js +199 -31
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/templates/theme/AGENTS.md +358 -21
- package/templates/theme/package.json +2 -2
- package/templates/theme/src/index.tsx +3 -2
- package/templates/theme/src/layout.tsx +26 -8
- package/templates/theme/src/locales/en.json +10 -1
- package/templates/theme/src/manifest.ts +49 -1
- package/templates/theme/src/sections.tsx +73 -2
- package/templates/theme/src/styles.css +20 -20
- package/templates/theme/src/templates.ts +151 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sudajs/cli",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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": "
|
|
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 `
|
|
12
|
-
- Starter page slugs preview at site-like root routes in `suda theme dev`: `
|
|
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
|
|
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.
|
|
162
|
-
-
|
|
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`
|
|
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: "
|
|
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: "
|
|
524
|
+
title: { type: "text", label: t("sections.hero.fields.title") },
|
|
187
525
|
},
|
|
188
526
|
blockSlots: {
|
|
189
527
|
actions: {
|
|
190
|
-
label: "
|
|
528
|
+
label: t("sections.hero.blocks.actions"),
|
|
191
529
|
blocks: {
|
|
192
530
|
button: {
|
|
193
|
-
label: "
|
|
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: "
|
|
196
|
-
href: { type: "url", label: "
|
|
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.
|
|
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.
|
|
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 {
|
|
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: "
|
|
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
|