@sudajs/cli 0.18.9 → 0.18.11
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 +8 -2
- package/dist/index.js +259 -80
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/templates/theme/AGENTS.md +183 -1182
- package/templates/theme/docs/agent-guides/component-authoring.md +270 -0
- package/templates/theme/docs/agent-guides/design-and-runtime.md +170 -0
- package/templates/theme/docs/agent-guides/editor-compatibility.md +231 -0
- package/templates/theme/docs/agent-guides/templates-and-locales.md +206 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# Component Authoring
|
|
2
|
+
|
|
3
|
+
Read this guide before changing sections, fields, defaults, navigation data,
|
|
4
|
+
icons, AI metadata, or block slots. The root `AGENTS.md` rules still apply.
|
|
5
|
+
|
|
6
|
+
## Naming and organization
|
|
7
|
+
|
|
8
|
+
- Group editor components by page role when using `pageConfig.categories`.
|
|
9
|
+
Category titles, component labels, and field labels must use `t(...)`.
|
|
10
|
+
- Use short, concrete PascalCase public keys that describe visitor-facing
|
|
11
|
+
patterns: `HeroImage`, `FeatureCards`, `PricingTable`, `ContactUs`.
|
|
12
|
+
- Avoid theme names, implementation details, and filler suffixes such as
|
|
13
|
+
`Section`, `Component`, `Block`, `New`, or `Custom`.
|
|
14
|
+
- Prefer semantic variants such as `FeatureCards` and `FeatureList`. Numbered
|
|
15
|
+
variants are a fallback and need distinct `ai.instructions` explaining their
|
|
16
|
+
visual or business difference.
|
|
17
|
+
- A top-level `content[].type` is a public component key. A concrete instance's
|
|
18
|
+
`content[].props.id` is separate and must be stable.
|
|
19
|
+
- Keep one page section per kebab-case `.tsx` file under `src/sections/`, with
|
|
20
|
+
a small `src/sections/index.ts` registry/export module.
|
|
21
|
+
|
|
22
|
+
## Field source of truth
|
|
23
|
+
|
|
24
|
+
Type props first. Use `SudaComponentConfig<Props>`, `SudaFields<Props>`, and
|
|
25
|
+
`defineSudaPageData(pageConfig, data)` to catch shape mismatches. Do not invent a
|
|
26
|
+
parallel Zod schema for theme fields.
|
|
27
|
+
|
|
28
|
+
Keep these four structures aligned:
|
|
29
|
+
|
|
30
|
+
1. TypeScript props.
|
|
31
|
+
2. `fields` or local-block fields.
|
|
32
|
+
3. `defaultProps`.
|
|
33
|
+
4. `render`.
|
|
34
|
+
|
|
35
|
+
Every normal field needs a matching serializable value in `defaultProps`.
|
|
36
|
+
Block-slot props are the exception: their initial composition belongs in the
|
|
37
|
+
slot's `defaultBlocks`, not the host component's `defaultProps`.
|
|
38
|
+
|
|
39
|
+
All normal prop defaults belong only in `defaultProps`. Starter/CMS data is
|
|
40
|
+
authored instance content, not component defaults. Render props directly; do
|
|
41
|
+
not use `||`, `??`, ternaries, default parameters, or destructuring defaults to
|
|
42
|
+
invent display values. Conditional rendering may remove intentionally optional
|
|
43
|
+
UI, but must not substitute content.
|
|
44
|
+
|
|
45
|
+
## Field selection
|
|
46
|
+
|
|
47
|
+
Use the most specific supported field:
|
|
48
|
+
|
|
49
|
+
- `text`: short labels, headings, slugs, and names.
|
|
50
|
+
- `textarea`: multi-sentence plain copy.
|
|
51
|
+
- `richtext`: authored rich HTML content.
|
|
52
|
+
- `url`: routes, anchors, `mailto:`, and external links.
|
|
53
|
+
- `image`, `video`, `media`: media according to the accepted kind.
|
|
54
|
+
- `icon`: canonical Suda icon names.
|
|
55
|
+
- `color`, `range`, `spacing`: editor-facing design values.
|
|
56
|
+
- `number`, `checkbox`: numeric and boolean values.
|
|
57
|
+
- `select`, `radio`: stable serializable choices.
|
|
58
|
+
- `object`, `array`: grouped and repeatable structured values.
|
|
59
|
+
- `posts`: one dynamic post query on an ordinary page section.
|
|
60
|
+
|
|
61
|
+
Rules:
|
|
62
|
+
|
|
63
|
+
- Never use `text` for URLs, media, colors, icons, or structured navigation.
|
|
64
|
+
- Use localized option labels and short stable option values.
|
|
65
|
+
- Add localized labels to nested `arrayFields`, `objectFields`, and local blocks.
|
|
66
|
+
- Add `description` and `placeholder` only when they help editor decisions.
|
|
67
|
+
- Use `visibleIf` for pure synchronous sibling-dependent visibility. Reserve
|
|
68
|
+
`resolveFields` for heavier changes that `visibleIf` cannot express.
|
|
69
|
+
- Add `ai.instructions`, `ai.required`, `ai.exclude`, `ai.stream`, or `ai.bind`
|
|
70
|
+
when a field needs generation guidance. Set `ai.stream: false` for atomic
|
|
71
|
+
values such as URLs.
|
|
72
|
+
- Multi-column grids, lists, pricing tables, galleries, and logo walls expose a
|
|
73
|
+
`columns` prop with a `range` or `select` field and a sensible default.
|
|
74
|
+
|
|
75
|
+
Representative field configuration:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
fields: {
|
|
79
|
+
title: { type: "text", label: t("common.fields.title") },
|
|
80
|
+
body: { type: "textarea", label: t("common.fields.description") },
|
|
81
|
+
href: { type: "url", label: t("common.fields.link") },
|
|
82
|
+
image: { type: "image", label: t("common.fields.image") },
|
|
83
|
+
icon: { type: "icon", label: t("common.fields.icon") },
|
|
84
|
+
columns: {
|
|
85
|
+
type: "range",
|
|
86
|
+
label: t("common.fields.columns"),
|
|
87
|
+
min: 2,
|
|
88
|
+
max: 4,
|
|
89
|
+
step: 1,
|
|
90
|
+
},
|
|
91
|
+
align: {
|
|
92
|
+
type: "radio",
|
|
93
|
+
label: t("common.fields.align"),
|
|
94
|
+
options: [
|
|
95
|
+
{ label: t("common.options.left"), value: "left" },
|
|
96
|
+
{ label: t("common.options.center"), value: "center" },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
cards: {
|
|
100
|
+
type: "array",
|
|
101
|
+
label: t("common.fields.cards"),
|
|
102
|
+
defaultItemProps: { title: "Card title", href: "/" },
|
|
103
|
+
arrayFields: {
|
|
104
|
+
title: { type: "text", label: t("common.fields.title") },
|
|
105
|
+
href: { type: "url", label: t("common.fields.link") },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
defaultProps: {
|
|
110
|
+
title: "Welcome",
|
|
111
|
+
body: "Describe the offer.",
|
|
112
|
+
href: "/contact",
|
|
113
|
+
image: themeAsset("assets/hero.jpg"),
|
|
114
|
+
icon: "sparkles",
|
|
115
|
+
columns: 3,
|
|
116
|
+
align: "left",
|
|
117
|
+
cards: [{ title: "Fast setup", href: "/features" }],
|
|
118
|
+
},
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Navigation and footer menus
|
|
122
|
+
|
|
123
|
+
Navigation and footer menus are typed arrays. Do not use a legacy menu field,
|
|
124
|
+
textarea, or newline-delimited string. Every destination uses a `url` field.
|
|
125
|
+
TypeScript props, fields, `defaultItemProps`, and component `defaultProps` must
|
|
126
|
+
have exactly the same structure.
|
|
127
|
+
|
|
128
|
+
Navigation supports at most two levels:
|
|
129
|
+
|
|
130
|
+
- `navItems[]`
|
|
131
|
+
- `navItems[].submenu[]`
|
|
132
|
+
|
|
133
|
+
Submenu items contain only label/link fields. Their schema must make a third
|
|
134
|
+
level impossible. An empty `submenu` means an ordinary top-level link.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
type NavItem = {
|
|
138
|
+
label: string;
|
|
139
|
+
href: string;
|
|
140
|
+
submenu: { label: string; href: string }[];
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const navItemsField = {
|
|
144
|
+
type: "array",
|
|
145
|
+
label: t("common.fields.navigationItems"),
|
|
146
|
+
defaultItemProps: { label: "Home", href: "/", submenu: [] },
|
|
147
|
+
arrayFields: {
|
|
148
|
+
label: { type: "text", label: t("common.fields.label") },
|
|
149
|
+
href: { type: "url", label: t("common.fields.link") },
|
|
150
|
+
submenu: {
|
|
151
|
+
type: "array",
|
|
152
|
+
label: t("common.fields.submenu"),
|
|
153
|
+
defaultItemProps: { label: "About", href: "/about" },
|
|
154
|
+
arrayFields: {
|
|
155
|
+
label: { type: "text", label: t("common.fields.label") },
|
|
156
|
+
href: { type: "url", label: t("common.fields.link") },
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Footer grouped menus use exactly two array levels: `columns[]` and
|
|
164
|
+
`columns[].links[]`. A column has a title and links; a link has text and URL.
|
|
165
|
+
Do not add another nested group.
|
|
166
|
+
|
|
167
|
+
## Icons
|
|
168
|
+
|
|
169
|
+
Interaction and UI-state icons may be fixed when their identity is intrinsic to
|
|
170
|
+
the control: menu, close, previous/next, expand/collapse, loading.
|
|
171
|
+
|
|
172
|
+
Icons that communicate authored content are props:
|
|
173
|
+
|
|
174
|
+
- Declare `{ type: "icon" }` and type values as `SudaIconName`.
|
|
175
|
+
- Put initial icon selections in `defaultProps`, including nested array/object
|
|
176
|
+
values.
|
|
177
|
+
- Render the value with `SudaIcon` from `@sudajs/theme-engine/icons`.
|
|
178
|
+
- Do not hardcode content icon names or icon components inside `render`.
|
|
179
|
+
|
|
180
|
+
Use canonical Lucide names such as `"rocket"` or `"mouse-pointer-click"`, not
|
|
181
|
+
`"lucide-rocket"`. Use namespaced Simple Icons brand names such as
|
|
182
|
+
`"simple-icons:github"`. The engine supports Lucide Icons and Simple Icons; do
|
|
183
|
+
not add another icon library unless neither can represent a required icon. Do
|
|
184
|
+
not create theme-local icon maps, emoji switches, SVG registries, or custom icon
|
|
185
|
+
systems unless the theme genuinely needs a bespoke graphic.
|
|
186
|
+
|
|
187
|
+
## AI metadata
|
|
188
|
+
|
|
189
|
+
Every page and layout component declares useful `ai.instructions`. Missing
|
|
190
|
+
component-level instructions fail `suda theme check` and block publishing.
|
|
191
|
+
Instructions should cover:
|
|
192
|
+
|
|
193
|
+
- purpose and business role;
|
|
194
|
+
- use/avoid conditions;
|
|
195
|
+
- normal placement;
|
|
196
|
+
- expected frequency;
|
|
197
|
+
- composition or neighboring sections when relevant.
|
|
198
|
+
|
|
199
|
+
Use `ai.exclude: true` only for structural/editor-only components, and explain
|
|
200
|
+
why AI must not generate them. `PageOutlet` is the standard example.
|
|
201
|
+
|
|
202
|
+
## Block slots
|
|
203
|
+
|
|
204
|
+
Use `blockSlots` when a section owns controlled local content such as actions,
|
|
205
|
+
pricing cards, feature rows, stats, timeline items, or contact methods.
|
|
206
|
+
|
|
207
|
+
- Type the slot prop as Puck `Slot` and use the same prop key in `blockSlots`.
|
|
208
|
+
- Do not create native `fields.<key>.type = "slot"` entries.
|
|
209
|
+
- Define allowed local kinds under `blockSlots.<slotName>.blocks`.
|
|
210
|
+
- Every local block needs a localized `label`, typed fields, `defaultProps`,
|
|
211
|
+
`render`, and useful AI instructions when applicable.
|
|
212
|
+
- Local blocks belong only to their owning section and cannot contain nested
|
|
213
|
+
block slots.
|
|
214
|
+
- Initial composition belongs in `defaultBlocks`; do not put the slot prop in
|
|
215
|
+
host `defaultProps`.
|
|
216
|
+
- Never put `id` in local block `defaultProps`, `defaultBlocks[].props`, or
|
|
217
|
+
authored nested items. The editor/runtime owns local ids.
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
type HeroProps = { title?: string; actions?: Slot };
|
|
221
|
+
|
|
222
|
+
export const Hero: SudaComponentConfig<HeroProps> = {
|
|
223
|
+
label: t("sections.hero.label"),
|
|
224
|
+
ai: { instructions: "Primary landing-page introduction. Use once near the top." },
|
|
225
|
+
fields: {
|
|
226
|
+
title: { type: "text", label: t("sections.hero.fields.title") },
|
|
227
|
+
},
|
|
228
|
+
blockSlots: {
|
|
229
|
+
actions: {
|
|
230
|
+
label: t("sections.hero.blocks.actions"),
|
|
231
|
+
blocks: {
|
|
232
|
+
button: {
|
|
233
|
+
label: t("sections.hero.blocks.button"),
|
|
234
|
+
fields: {
|
|
235
|
+
label: { type: "text", label: t("sections.hero.blocks.buttonLabel") },
|
|
236
|
+
href: { type: "url", label: t("sections.hero.blocks.buttonHref") },
|
|
237
|
+
},
|
|
238
|
+
defaultProps: { label: "Get started", href: "/contact" },
|
|
239
|
+
render: ({ label, href }) => <a href={href}>{label}</a>,
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
defaultBlocks: [{ kind: "button" }],
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
defaultProps: { title: "Build with SudaCloud" },
|
|
246
|
+
render: ({ title, actions: Actions }) => (
|
|
247
|
+
<section>
|
|
248
|
+
<h1>{title}</h1>
|
|
249
|
+
<Actions />
|
|
250
|
+
</section>
|
|
251
|
+
),
|
|
252
|
+
};
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Authored page data uses the public host type and short local kind:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
{
|
|
259
|
+
type: "Hero",
|
|
260
|
+
props: {
|
|
261
|
+
id: "Hero-1",
|
|
262
|
+
title: "Welcome",
|
|
263
|
+
actions: [{ type: "button", props: { label: "Contact us", href: "/contact" } }],
|
|
264
|
+
},
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Never write internal types such as
|
|
269
|
+
`__suda_local_block__/Hero/actions/button`; those are generated only for Puck
|
|
270
|
+
internals.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Design and Runtime
|
|
2
|
+
|
|
3
|
+
Read this guide before changing the theme contract, manifest, layout, design
|
|
4
|
+
tokens, CSS, assets, contact forms, or runtime metadata. The root `AGENTS.md`
|
|
5
|
+
rules still apply.
|
|
6
|
+
|
|
7
|
+
## Theme contract
|
|
8
|
+
|
|
9
|
+
- Keep `renderMode: "ssr"` in `src/manifest.ts`.
|
|
10
|
+
- Keep source entries at `src/index.tsx` and `src/styles.css`.
|
|
11
|
+
- Export a complete `ThemeModule`: `manifest`, `pageConfig`, `layoutConfig`,
|
|
12
|
+
`defaultLayout`, `starterPages`, and `cmsTemplates`.
|
|
13
|
+
- Keep page sections in `pageConfig`; keep the root, Header, PageOutlet, Footer,
|
|
14
|
+
and necessary global chrome in `layoutConfig`.
|
|
15
|
+
- If browser-only setup is required, export `clientHooks` from `src/client.ts`.
|
|
16
|
+
Do not export theme config there and do not add `src/runtime.client.ts(x)`;
|
|
17
|
+
the CLI generates `dist/runtime.client.js`.
|
|
18
|
+
- React, React DOM, Puck, and `@sudajs/theme-engine` are host-provided peers.
|
|
19
|
+
Do not bundle private copies into the runtime.
|
|
20
|
+
- Keep persisted props JSON-serializable. Never persist functions, React nodes,
|
|
21
|
+
class instances, media database ids, or absolute local paths.
|
|
22
|
+
- Do not edit generated files under `dist/`.
|
|
23
|
+
|
|
24
|
+
Starter routes in `suda theme dev` are site-like root routes such as `/index`
|
|
25
|
+
and `/contact-us`, never `/pages/...`.
|
|
26
|
+
|
|
27
|
+
## Layout
|
|
28
|
+
|
|
29
|
+
- Render Header and Footer exactly once from `defaultLayout`, with PageOutlet
|
|
30
|
+
between them and stable top-level ids.
|
|
31
|
+
- Do not include layout chrome in starter pages or CMS templates.
|
|
32
|
+
- Root config renders layout children. PageOutlet reads the page slot through
|
|
33
|
+
`getPageSlot(puck?.metadata)`.
|
|
34
|
+
- Native Puck slots are limited to engine layout/container primitives and theme
|
|
35
|
+
layout components. Page sections use Suda `blockSlots` for controlled nested
|
|
36
|
+
content.
|
|
37
|
+
- Read platform-owned values such as white-label and ICP/public-security
|
|
38
|
+
records from runtime metadata helpers. Do not copy them into static props.
|
|
39
|
+
- Read public language links from `getSiteLocale(puck?.metadata)` and render the
|
|
40
|
+
provided label/href instead of constructing locale URLs.
|
|
41
|
+
|
|
42
|
+
Read `editor-compatibility.md` as well whenever layout positioning or client
|
|
43
|
+
behavior changes.
|
|
44
|
+
|
|
45
|
+
## Design system
|
|
46
|
+
|
|
47
|
+
Build the entire theme from one editable design system. Sections, CMS views,
|
|
48
|
+
block slots, and starter pages must not invent independent palettes, type
|
|
49
|
+
scales, radii, shadows, or spacing.
|
|
50
|
+
|
|
51
|
+
- Define `sourceManifest.designSystem` in `src/manifest.ts` with `version`,
|
|
52
|
+
`defaultPresetId`, and complete named presets.
|
|
53
|
+
- Use stable engine token keys. At minimum define every color token the theme
|
|
54
|
+
consumes (`background`, `foreground`, `primary`, `primaryForeground`,
|
|
55
|
+
`secondary`, `secondaryForeground`, `accent`, `accentForeground`, `muted`,
|
|
56
|
+
`mutedForeground`) and consumed radius tokens (`card`, `button`, `input`).
|
|
57
|
+
- Do not invent platform-facing token names without checking generated types.
|
|
58
|
+
- Expose `designSystemField(...)` only at the layout root and initialize it with
|
|
59
|
+
`createThemeDesignDefault(sourceManifest.designSystem)`.
|
|
60
|
+
- Resolve tokens once in the root with
|
|
61
|
+
`resolveThemeDesignTokens(sourceManifest.designSystem, props.designSystem)`
|
|
62
|
+
and apply `createThemeDesignCssVariables(tokens)` there. Sections consume CSS
|
|
63
|
+
variables; they do not resolve presets independently.
|
|
64
|
+
|
|
65
|
+
For Tailwind, map Suda tokens through `@theme inline` and consume semantic
|
|
66
|
+
utilities such as `bg-primary`, `text-primary-foreground`, and `rounded-card`.
|
|
67
|
+
The `inline` keyword is required because Suda injects `--suda-*` variables on
|
|
68
|
+
the rendered theme root.
|
|
69
|
+
|
|
70
|
+
Hand-written CSS consumes `--suda-*` directly. Do not use Suda-backed aliases
|
|
71
|
+
such as `var(--color-primary)`, `var(--color-background)`, or
|
|
72
|
+
`var(--radius-card)` in hand-written rules. For derived values, declare a
|
|
73
|
+
theme-prefixed variable on the root:
|
|
74
|
+
|
|
75
|
+
```css
|
|
76
|
+
.my-theme-root {
|
|
77
|
+
--my-theme-primary-hover: color-mix(in srgb, var(--suda-color-primary) 84%, black);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.my-theme-button:hover {
|
|
81
|
+
background: var(--my-theme-primary-hover);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Build/check/publish reject hand-written consumption of Suda-backed Tailwind
|
|
86
|
+
aliases.
|
|
87
|
+
|
|
88
|
+
## Visual consistency
|
|
89
|
+
|
|
90
|
+
- Centralize typography and spacing scales in `src/styles.css`.
|
|
91
|
+
- Reuse container, section padding, heading, eyebrow, body, card, button, input,
|
|
92
|
+
and media-frame patterns.
|
|
93
|
+
- Section props expose semantic decisions such as `tone`, `variant`, `columns`,
|
|
94
|
+
`mediaPosition`, `showImage`, or `spacing`, then map them to shared tokens and
|
|
95
|
+
classes.
|
|
96
|
+
- Do not expose raw color, typography, spacing, radius, or shadow fields merely
|
|
97
|
+
because CSS contains a value. Expose a design control only when editors should
|
|
98
|
+
intentionally customize it per instance.
|
|
99
|
+
- Use `color` and `spacing` fields only for real editor-facing controls; use
|
|
100
|
+
`select`/`radio` for named design-system variants.
|
|
101
|
+
- Local blocks inherit their host section's design system and should not define
|
|
102
|
+
independent palettes, type scales, radii, shadows, or spacing.
|
|
103
|
+
- CMS sections and ordinary post sections use the same containers, typography,
|
|
104
|
+
cards, media ratios, pagination, and states as the rest of the theme.
|
|
105
|
+
- Starter pages demonstrate one coherent design system. Vary content, order,
|
|
106
|
+
media, and semantic variants rather than hardcoding unrelated styles.
|
|
107
|
+
- Add a shared utility/token when multiple sections need it or the theme needs
|
|
108
|
+
a named pattern, not for one-off styling.
|
|
109
|
+
|
|
110
|
+
## Styling and assets
|
|
111
|
+
|
|
112
|
+
- Tailwind v4 starts in `src/styles.css` with `@import "tailwindcss";` and
|
|
113
|
+
`@source "./**/*.{ts,tsx}";`.
|
|
114
|
+
- Scope theme CSS with generated theme-key classes. Avoid global resets that
|
|
115
|
+
affect the host editor or other themes.
|
|
116
|
+
- Theme-local assets live in top-level `assets/` and are copied to
|
|
117
|
+
`dist/assets/` during build.
|
|
118
|
+
- Use `themeAsset("assets/...")` for bundled assets in `defaultProps`, starter
|
|
119
|
+
pages, CMS templates, and AI examples.
|
|
120
|
+
- Do not import theme-local images from React code or hand-write
|
|
121
|
+
`themes/<themeKey>/<version>/...` paths. Full external CDN URLs are valid.
|
|
122
|
+
- Resolve user-selected media with `resolveAsset(puck?.metadata, value)` before
|
|
123
|
+
rendering.
|
|
124
|
+
- Required preview screenshots are `assets/preview/desktop.png`,
|
|
125
|
+
`assets/preview/tablet.png`, and `assets/preview/mobile.png`; generate them
|
|
126
|
+
with `suda theme capture`.
|
|
127
|
+
- Customize Vite, PostCSS, and Tailwind configuration only when necessary.
|
|
128
|
+
- Do not modify minified vendor CSS. Use precise scoped overrides unless an
|
|
129
|
+
explicitly maintained vendor patch is unavoidable.
|
|
130
|
+
|
|
131
|
+
## Contact forms
|
|
132
|
+
|
|
133
|
+
When a theme offers a contact section, render the host-provided form from
|
|
134
|
+
`metadata.contactForm`. The platform owns field configuration, validation,
|
|
135
|
+
submissions, notifications, webhooks, permissions, and limits; the theme owns
|
|
136
|
+
public presentation only.
|
|
137
|
+
|
|
138
|
+
- Read the form through `getContactForm(puck?.metadata)` from
|
|
139
|
+
`@sudajs/theme-engine/runtime`.
|
|
140
|
+
- `suda theme dev` passes a default form through `metadata.contactForm` for
|
|
141
|
+
local preview.
|
|
142
|
+
- If no valid enabled form exists, render nothing for the form area. Do not
|
|
143
|
+
show a public unavailable/error fallback panel.
|
|
144
|
+
- Support and style `text`, `textarea`, `checkbox`, `radio`, and `select` fields.
|
|
145
|
+
- Render labels, keyboard focus, submit state, success state, validation errors,
|
|
146
|
+
and mobile layout accessibly.
|
|
147
|
+
- Use only placeholders supplied by form settings. Do not invent placeholders,
|
|
148
|
+
including an empty select option.
|
|
149
|
+
- Submit field values to `contactForm.endpoint`.
|
|
150
|
+
- Never hardcode notification channels, recipients, webhook URLs, ICP records,
|
|
151
|
+
or white-label branding in the section.
|
|
152
|
+
- Render hidden platform fields, including
|
|
153
|
+
`name={contactForm.honeypotField}`, as bare `<input type="hidden" />` nodes.
|
|
154
|
+
Do not wrap hidden inputs in labels, layout rows, grids, or visual field
|
|
155
|
+
components.
|
|
156
|
+
|
|
157
|
+
## Verification
|
|
158
|
+
|
|
159
|
+
Before handoff:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
pnpm lint
|
|
163
|
+
pnpm typecheck
|
|
164
|
+
pnpm build
|
|
165
|
+
pnpm validate
|
|
166
|
+
suda theme check
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Run `suda theme capture` after visual changes and compare desktop, tablet, and
|
|
170
|
+
mobile output in editor preview and on the public site.
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Editor Compatibility
|
|
2
|
+
|
|
3
|
+
Read this guide before changing Header, Footer, PageOutlet, sticky/fixed UI,
|
|
4
|
+
transparent navigation, carousels, dropdowns, editor CSS, or client behavior.
|
|
5
|
+
Every public component must render consistently on the public site, in the Puck
|
|
6
|
+
Editor iframe, and in editor preview mode.
|
|
7
|
+
|
|
8
|
+
## Stable roots and Puck mutation
|
|
9
|
+
|
|
10
|
+
Puck may attach selection/drag attributes and inline positioning directly to
|
|
11
|
+
the first real element returned by a component:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
el.setAttribute("data-puck-component", id);
|
|
15
|
+
el.setAttribute("data-puck-dnd", id);
|
|
16
|
+
el.style.position = "relative";
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- Never assume `[data-puck-component]` is an external wrapper. It may be the
|
|
20
|
+
component's own `<nav>`, `<header>`, `<section>`, or `<footer>`.
|
|
21
|
+
- Every public component should return one stable semantic real DOM root when
|
|
22
|
+
it controls spacing, background, sizing, stacking, positioning, or selection.
|
|
23
|
+
- Do not use a Fragment or `display: contents` for roots with those jobs.
|
|
24
|
+
- `PageOutlet` needs a real element when it owns overlap, negative margin,
|
|
25
|
+
positioning, or sizing.
|
|
26
|
+
- `:has(.component-root)` does not match an element that is itself the root.
|
|
27
|
+
Cover both shapes when either can occur:
|
|
28
|
+
|
|
29
|
+
```css
|
|
30
|
+
.component-root[data-puck-component],
|
|
31
|
+
[data-puck-component]:has(.component-root) {
|
|
32
|
+
/* compatibility rule */
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Puck's inline `position: relative` beats ordinary CSS. If the actual root must
|
|
37
|
+
remain `sticky`, `fixed`, or `absolute`, use a precise editor-scoped rule with
|
|
38
|
+
`!important`. Never apply `position: relative !important` to every editor
|
|
39
|
+
component; it breaks navigation, overlays, and floating controls.
|
|
40
|
+
|
|
41
|
+
## Editor scope and iframe height
|
|
42
|
+
|
|
43
|
+
Derive editor state from `puck?.metadata?.isEditor === true` and expose a stable
|
|
44
|
+
theme-root marker:
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
<div className="theme-root" data-suda-editor={isEditor ? "true" : undefined}>
|
|
48
|
+
{children}
|
|
49
|
+
</div>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Keep editor-only fixes below `.theme-root[data-suda-editor="true"]`. Do not
|
|
53
|
+
globally override `.navbar`, `.section`, `[data-puck-component]`, or public-site
|
|
54
|
+
layout to repair the editor.
|
|
55
|
+
|
|
56
|
+
The editable page scrolls inside a same-origin iframe. The outer canvas/frame
|
|
57
|
+
may be only one viewport tall, so the iframe document must remain content-height
|
|
58
|
+
driven:
|
|
59
|
+
|
|
60
|
+
- Use `height: auto`, `min-height: 100vh`, and `overflow: visible` on the editor
|
|
61
|
+
theme root unless the theme requires a different proven arrangement.
|
|
62
|
+
- Ensure Puck's `#frame-root` and root `[data-puck-dropzone]` can grow with
|
|
63
|
+
content. Avoid `height: 100%` when it collapses the page to one viewport.
|
|
64
|
+
- Be careful with global `html`, `body`, `iframe`, `.hidden`, `img`, `button`,
|
|
65
|
+
`input`, and Tailwind preflight rules.
|
|
66
|
+
- Put third-party compatibility overrides in scoped `src/styles.css`; do not
|
|
67
|
+
change public behavior globally or patch minified vendor CSS.
|
|
68
|
+
- Scroll through every starter page in edit mode. All components must be
|
|
69
|
+
reachable without selecting them from the outline first.
|
|
70
|
+
|
|
71
|
+
## Sticky and fixed positioning
|
|
72
|
+
|
|
73
|
+
Before implementing Header, announcement bars, floating CTAs, or any
|
|
74
|
+
sticky/fixed element, identify:
|
|
75
|
+
|
|
76
|
+
1. The real positioned root and whether Puck mutates it.
|
|
77
|
+
2. The actual scroll element in the public site and editor iframe.
|
|
78
|
+
3. The intended sticky containing block.
|
|
79
|
+
4. Every ancestor with `overflow`, `transform`, `filter`, `perspective`, or
|
|
80
|
+
`contain` that may change positioning.
|
|
81
|
+
5. Whether the sticky parent is taller than the sticky element.
|
|
82
|
+
6. Explicit `top` and `z-index` values.
|
|
83
|
+
7. Whether iframe and outer-canvas coordinate systems differ.
|
|
84
|
+
|
|
85
|
+
For a layout Header, put sticky behavior on its real root. If that node receives
|
|
86
|
+
`data-puck-component`, correct Puck's inline position on that same node. Site and
|
|
87
|
+
editor may use different mechanics only when their visual result remains
|
|
88
|
+
equivalent.
|
|
89
|
+
|
|
90
|
+
Example covering both possible DOM shapes:
|
|
91
|
+
|
|
92
|
+
```css
|
|
93
|
+
.theme-root[data-suda-editor="true"] .theme-navigation[data-puck-component],
|
|
94
|
+
.theme-root[data-suda-editor="true"]
|
|
95
|
+
[data-puck-component]:has(.theme-navigation)
|
|
96
|
+
.theme-navigation {
|
|
97
|
+
position: sticky !important;
|
|
98
|
+
top: 0;
|
|
99
|
+
z-index: 30;
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Transparent navigation
|
|
104
|
+
|
|
105
|
+
Transparent navigation is coordinated state and layout, not only a transparent
|
|
106
|
+
background. Handle all of these together:
|
|
107
|
+
|
|
108
|
+
- background and shadow;
|
|
109
|
+
- logo, menu text, and button colors;
|
|
110
|
+
- Hero/PageOutlet overlap beneath Header;
|
|
111
|
+
- transition to the solid state after a scroll threshold;
|
|
112
|
+
- Puck root-position mutation;
|
|
113
|
+
- identical SSR-first-frame and hydrated state.
|
|
114
|
+
|
|
115
|
+
Never render white logo/text above an accidentally white Header. Prefer React
|
|
116
|
+
Context for a page's transparency request, an SSR-readable DOM marker for the
|
|
117
|
+
first frame, and a real PageOutlet element for editor overlap. The public site
|
|
118
|
+
may use a spacer or equivalent layout strategy. Keep shared measurements in a
|
|
119
|
+
theme CSS variable such as `--theme-navigation-height`.
|
|
120
|
+
|
|
121
|
+
## Scroll detection
|
|
122
|
+
|
|
123
|
+
Do not rely only on `window.scrollY` or a window scroll listener. The source may
|
|
124
|
+
be `document.scrollingElement`, the iframe document, or a nested
|
|
125
|
+
`overflow: auto/scroll` element, and scroll does not bubble.
|
|
126
|
+
|
|
127
|
+
- Find the nearest scrollable ancestor for each navigation instance and read
|
|
128
|
+
that element's `scrollTop`.
|
|
129
|
+
- Listen on the document with capture and passive mode:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
document.addEventListener("scroll", handler, { capture: true, passive: true });
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
- Keep a window listener for public-site compatibility.
|
|
136
|
+
- Never assume site and editor use the same scroll element.
|
|
137
|
+
|
|
138
|
+
## Client lifecycle
|
|
139
|
+
|
|
140
|
+
Runtime code must tolerate editor remounts, live prop updates, HMR, React Strict
|
|
141
|
+
Mode effect replay, frequent `MutationObserver` callbacks, and scripts loading
|
|
142
|
+
after `DOMContentLoaded`.
|
|
143
|
+
|
|
144
|
+
- Make initialization and event binding idempotent.
|
|
145
|
+
- Observe components inserted later and only relevant attribute changes.
|
|
146
|
+
- Do not use a permanent `window.__xxxBound` flag that blocks HMR reinitialization.
|
|
147
|
+
- Clean up effects, observers, timers, and listeners.
|
|
148
|
+
- Keep SSR markers and client state synchronized.
|
|
149
|
+
- Put optional browser setup in `clientHooks` from `src/client.ts`; never add
|
|
150
|
+
`src/runtime.client.ts(x)`.
|
|
151
|
+
|
|
152
|
+
## Editor interactions
|
|
153
|
+
|
|
154
|
+
Edit mode may effectively disable descendant interaction:
|
|
155
|
+
|
|
156
|
+
```css
|
|
157
|
+
[data-puck-component] * {
|
|
158
|
+
pointer-events: none;
|
|
159
|
+
user-select: none;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
[data-puck-component] {
|
|
163
|
+
pointer-events: auto !important;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
An unclickable link, dropdown, carousel, or button in edit mode is not by itself
|
|
168
|
+
a theme bug. Never globally restore descendant pointer events because that
|
|
169
|
+
breaks selection and drag behavior. Test visitor interaction in editor preview
|
|
170
|
+
and on the public site.
|
|
171
|
+
|
|
172
|
+
## Layout ownership
|
|
173
|
+
|
|
174
|
+
- Keep only Header, PageOutlet, Footer, and necessary global chrome such as an
|
|
175
|
+
announcement bar in `layoutConfig`.
|
|
176
|
+
- Render Header and Footer exactly once from the layout; do not repeat them in
|
|
177
|
+
starter pages or CMS templates.
|
|
178
|
+
- When moving chrome into the layout, remove duplicates from starter pages,
|
|
179
|
+
CMS templates, page categories, and AI examples.
|
|
180
|
+
- Give Header, PageOutlet, and Footer stable ids, with PageOutlet between Header
|
|
181
|
+
and Footer.
|
|
182
|
+
- Read white-label, ICP, and other footer metadata from runtime metadata; do not
|
|
183
|
+
persist duplicated platform values in component data.
|
|
184
|
+
|
|
185
|
+
## Vendor CSS
|
|
186
|
+
|
|
187
|
+
Do not modify original third-party minified CSS unless the task explicitly
|
|
188
|
+
requires a maintained vendor patch. Prefer precise selectors in scoped
|
|
189
|
+
`src/styles.css`. If a patch is unavoidable, document the reproducible problem
|
|
190
|
+
and why an override cannot solve it.
|
|
191
|
+
|
|
192
|
+
## Completion checklist
|
|
193
|
+
|
|
194
|
+
For a new theme, verify:
|
|
195
|
+
|
|
196
|
+
- stable real roots and an editor root marker;
|
|
197
|
+
- layout-owned Header, PageOutlet, and Footer with stable ids;
|
|
198
|
+
- Header sticky behavior in editor and site modes;
|
|
199
|
+
- transparent navigation first frame and scrolled state;
|
|
200
|
+
- correct scrollable-ancestor detection;
|
|
201
|
+
- idempotent runtime setup and cleanup;
|
|
202
|
+
- real-element PageOutlet overlap behavior;
|
|
203
|
+
- responsive behavior; and
|
|
204
|
+
- editor preview versus public-site parity.
|
|
205
|
+
|
|
206
|
+
Whenever Header, Footer, PageOutlet, transparent navigation, sticky/fixed UI,
|
|
207
|
+
carousel, dropdown/mobile menu, or a `100vh` Hero changes, test:
|
|
208
|
+
|
|
209
|
+
- public SSR first frame and post-hydration state;
|
|
210
|
+
- editor desktop, tablet, and mobile;
|
|
211
|
+
- edit and preview modes;
|
|
212
|
+
- top state and state after crossing scroll thresholds;
|
|
213
|
+
- HMR and live prop updates;
|
|
214
|
+
- exactly one Header and one Footer; and
|
|
215
|
+
- intact Puck selection and dragging.
|
|
216
|
+
|
|
217
|
+
## Debugging order
|
|
218
|
+
|
|
219
|
+
When editor and site differ, inspect in this order before changing CSS:
|
|
220
|
+
|
|
221
|
+
1. The component's actual root.
|
|
222
|
+
2. The node receiving `data-puck-component`.
|
|
223
|
+
3. Inline styles on that node.
|
|
224
|
+
4. Computed `position`, `background`, `z-index`, and ancestor `overflow`.
|
|
225
|
+
5. The actual scroll element and its `scrollTop`.
|
|
226
|
+
6. Iframe versus outer-canvas coordinates.
|
|
227
|
+
7. Whether client runtime initialized.
|
|
228
|
+
8. Transparent-state context and SSR DOM markers.
|
|
229
|
+
9. Stylesheet loading and cascade order.
|
|
230
|
+
|
|
231
|
+
Do not guess selectors repeatedly before confirming DOM and computed styles.
|