@sudajs/cli 0.9.4 → 0.10.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.9.4",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "suda": "./bin/suda.js"
@@ -30,10 +30,11 @@
30
30
  "esbuild": "^0.25.12",
31
31
  "lucide-react": "^1.17.0",
32
32
  "open": "^10.1.0",
33
+ "picocolors": "^1.1.1",
33
34
  "react": "^19.2.7",
34
35
  "react-dom": "^19.2.7",
35
36
  "zod": "^3.24.1",
36
- "@sudajs/theme-engine": "2.4.1"
37
+ "@sudajs/theme-engine": "2.5.0"
37
38
  },
38
39
  "devDependencies": {
39
40
  "@tailwindcss/postcss": "^4.3.0",
@@ -14,6 +14,53 @@ Your job in a theme project is to create a complete, publishable theme with them
14
14
  - React, React DOM, Puck, and `@sudajs/theme-engine` are host-provided peers. Do not bundle private copies into the theme runtime.
15
15
  - Keep persisted props JSON-serializable. Do not store functions, React nodes, class instances, database ids for media, or environment-specific absolute filesystem paths.
16
16
 
17
+ ## Editor i18n and locales
18
+
19
+ Theme editor labels are localized by the host editor. Use the template's
20
+ `src/i18n.ts` helper for every component label, field label, option label, and
21
+ other editor-facing configuration string.
22
+
23
+ - Import `t` from `./i18n.js` in files that define `pageConfig`, `layoutConfig`,
24
+ fields, options, or local blocks.
25
+ - Write labels as stable dot-path keys, for example
26
+ `label: t("sections.hero.fields.title")`, not hardcoded strings.
27
+ - Add every key used with `t(...)` to `src/locales/en.json`. This file is the
28
+ required reference locale for theme validation.
29
+ - Keep locale values as nested JSON objects whose leaves are strings. Arrays,
30
+ numbers, booleans, and null are invalid locale values.
31
+ - When adding another locale such as `src/locales/zh-CN.json`, keep its leaf key
32
+ set exactly aligned with `src/locales/en.json`; missing or extra keys fail
33
+ `suda theme validate`, `suda theme check`, `pnpm build`, and publish.
34
+ - The CLI copies `src/locales/*.json` into `dist/locales/` during build. Do not
35
+ edit `dist/locales/` directly.
36
+ - If an installed legacy theme is missing locale files, the editor falls back to
37
+ showing translation keys instead of crashing. New themes must still pass
38
+ locale validation before publishing.
39
+ - Do not put actual page content, starter page copy, default prop values, or
40
+ public-site text through `t(...)`. Those belong in component props and starter
41
+ page data so users can edit them.
42
+
43
+ Example:
44
+
45
+ ```tsx
46
+ import { t } from "./i18n.js";
47
+
48
+ export const Hero: SudaComponentConfig<HeroProps> = {
49
+ label: t("sections.hero.label"),
50
+ fields: {
51
+ title: { type: "text", label: t("sections.hero.fields.title") },
52
+ tone: {
53
+ type: "radio",
54
+ label: t("sections.hero.fields.tone"),
55
+ options: [
56
+ { label: t("sections.hero.fields.toneWarm"), value: "warm" },
57
+ { label: t("sections.hero.fields.toneSharp"), value: "sharp" },
58
+ ],
59
+ },
60
+ },
61
+ };
62
+ ```
63
+
17
64
  ## Component strategy
18
65
 
19
66
  - Create theme-specific components and sections first. Every page component should express this theme's brand, industry, visual rhythm, and content model.
@@ -51,6 +98,9 @@ Use the most specific field type available. A generic `text` field may pass type
51
98
  - Use `select` or `radio` for fixed choices. Keep option values stable, short, and serializable.
52
99
  - Use `array` for repeatable items and `object` for grouped settings. Keep nested field names aligned with the prop shape and `defaultProps`.
53
100
  - Use `textarea` for multi-sentence copy. Use `text` only for short labels, headings, slugs, names, or plain strings that are not URLs, media, colors, icons, fonts, or menus.
101
+ - Use `t(...)` for every editor-facing `label` in components, fields, nested
102
+ `arrayFields` / `object.fields`, local blocks, and option lists. Add matching
103
+ keys to `src/locales/en.json` at the same time.
54
104
  - For card grids, feature lists, pricing tables, logo walls, gallery grids, and any multi-column component, expose a `columns` prop with a `range` or `select` field and a sensible default.
55
105
  - Use `visibleIf: ({ props }, { fields }) => boolean` for synchronous field visibility that depends on sibling props or resolved fields. Keep it pure and fast.
56
106
  - Use Puck `resolveFields` only for heavier dynamic field changes that cannot be expressed with `visibleIf`.
@@ -68,17 +118,9 @@ Good component instructions explain:
68
118
  - Frequency: whether it should appear once, multiple times, or only near another section.
69
119
  - Composition: required neighboring content or local blocks, when relevant.
70
120
 
71
- - Prefer Suda field helpers over generic text fields: use `url` for links and routes, `image` for images, `video` for videos, `media` for mixed media, `color` for colors, `icon` for icon names, `font` for font selections, `range` for bounded numbers, `spacing` for spacing tokens, and `menu` for navigation items.
72
121
  - For icons, expose props with `{ type: "icon" }`, type values as `SudaLucideIconName`, render them with `SudaLucideIcon` from `@sudajs/theme-engine/runtime`, and store canonical Lucide names such as `"rocket"` or `"mouse-pointer-click"` (not `"lucide-rocket"`). Do not keep theme-local icon maps, emoji/icon switch statements, or custom SVG icon registries unless the theme truly needs a bespoke non-Lucide graphic.
73
- - Use `select` or `radio` when authors must choose from a fixed set of values. Keep option values stable and serializable.
74
- - Use `array` for repeatable items and `object` for grouped settings. Keep nested field names aligned with the prop shape and `defaultProps`.
75
- - For card grids, feature lists, pricing tables, logo walls, gallery grids, and any multi-column component, expose a `columns` prop with a `range` or `select` field and a sensible default.
76
- - Use `textarea` for multi-sentence copy and `text` only for short labels, headings, slugs, or plain strings that are not URLs/media/color/icon/font values.
77
- - Keep `fields`, `defaultProps`, and `render` in sync: every editor field should have a sensible default and render should tolerate omitted optional values.
78
- - Put editor-only controls in props when they change rendering, for example `showLogo`; pair dependent controls with `visibleIf` rather than hiding logic only in JSX.
79
- - Keep persisted props JSON-serializable. Do not store functions, React nodes, class instances, database ids for media, or environment-specific absolute filesystem paths.
80
122
  - Add optional field-level `ai.instructions`, `ai.required`, or `ai.exclude` when a prop needs generation guidance beyond its label and type.
81
- Use `ai.exclude: true` only for structural or editor-only components, and still provide instructions explaining why AI must not generate them. `PageOutlet` is the standard example.
123
+ - Use `ai.exclude: true` only for structural or editor-only components, and still provide instructions explaining why AI must not generate them. `PageOutlet` is the standard example.
82
124
 
83
125
  ## Block slot authoring rules
84
126
 
@@ -23,6 +23,8 @@ pnpm validate
23
23
 
24
24
  Theme-local assets live in top-level `assets/`. The starter hero exposes a Logo field that defaults to `assets/brand/suda-logo.svg`, so you can replace it with your own brand image or choose a different image in the editor.
25
25
 
26
+ Editor labels are localized through `src/i18n.ts` and `src/locales/*.json`. Use `t("...")` for component labels, field labels, and option labels, then add matching string leaves to `src/locales/en.json`. `en.json` is required, and any additional locale file must have the same leaf key set. `pnpm validate`, `suda theme check`, and `pnpm build` verify the locale contract.
27
+
26
28
  Preview screenshots are required before publishing:
27
29
 
28
30
  ```bash
@@ -0,0 +1,13 @@
1
+ import { createThemeTranslate } from "@sudajs/theme-engine/runtime";
2
+
3
+ import type en from "./locales/en.json" with { type: "json" };
4
+
5
+ type DotPath<T, Prefix extends string = ""> = T extends string
6
+ ? Prefix
7
+ : {
8
+ [Key in keyof T & string]: DotPath<T[Key], Prefix extends "" ? Key : `${Prefix}.${Key}`>;
9
+ }[keyof T & string];
10
+
11
+ export type MessageKeys = DotPath<typeof en>;
12
+
13
+ export const t = createThemeTranslate<MessageKeys>();
@@ -2,6 +2,8 @@ import type { SudaComponentConfig, SudaRootConfig } from "@sudajs/theme-engine";
2
2
  import { getIcpRecord, getPageSlot, getWhiteLabel } from "@sudajs/theme-engine/runtime";
3
3
  import type { ReactElement, ReactNode } from "react";
4
4
 
5
+ import { t } from "./i18n.js";
6
+
5
7
  type PuckExtras = { puck?: { metadata?: Record<string, unknown> } };
6
8
  type RootProps = { children?: ReactNode; title?: string };
7
9
  type HeaderProps = { siteName?: string };
@@ -10,7 +12,7 @@ type FooterProps = { text?: string } & PuckExtras;
10
12
 
11
13
  export const rootConfig: SudaRootConfig<RootProps> = {
12
14
  fields: {
13
- title: { type: "text", label: "Site title" },
15
+ title: { type: "text", label: t("layout.root.fields.title") },
14
16
  },
15
17
  defaultProps: { title: "__SUDA_THEME_KEY__" },
16
18
  render: ({ children, title }) => (
@@ -21,20 +23,20 @@ export const rootConfig: SudaRootConfig<RootProps> = {
21
23
  };
22
24
 
23
25
  export const Header: SudaComponentConfig<HeaderProps> = {
24
- label: "Header",
26
+ label: t("layout.header.label"),
25
27
  ai: {
26
28
  instructions:
27
29
  "Site-wide header rendered at the top of every page. " +
28
30
  "Place once at the top of the layout (not inside page content). " +
29
31
  "Used for branding and primary navigation, not for promotional content.",
30
32
  },
31
- fields: { siteName: { type: "text", label: "Site name" } },
33
+ fields: { siteName: { type: "text", label: t("layout.header.fields.siteName") } },
32
34
  defaultProps: { siteName: "__SUDA_THEME_KEY__" },
33
35
  render: ({ siteName }) => <header className="__SUDA_THEME_KEY__-header">{siteName}</header>,
34
36
  };
35
37
 
36
38
  export const PageOutlet: SudaComponentConfig<PageOutletProps> = {
37
- label: "Page outlet",
39
+ label: t("layout.pageOutlet.label"),
38
40
  ai: {
39
41
  exclude: true,
40
42
  instructions:
@@ -47,14 +49,14 @@ export const PageOutlet: SudaComponentConfig<PageOutletProps> = {
47
49
  };
48
50
 
49
51
  export const Footer: SudaComponentConfig<FooterProps> = {
50
- label: "Footer",
52
+ label: t("layout.footer.label"),
51
53
  ai: {
52
54
  instructions:
53
55
  "Site-wide footer rendered at the bottom of every page. " +
54
56
  "Place once at the end of the layout. " +
55
57
  "Used for legal text, copyright, and secondary links — not for primary CTAs.",
56
58
  },
57
- fields: { text: { type: "text", label: "Text" } },
59
+ fields: { text: { type: "text", label: t("layout.footer.fields.text") } },
58
60
  defaultProps: { text: "© __SUDA_THEME_KEY__" },
59
61
  render: ({ text, puck }) => {
60
62
  const whiteLabel = getWhiteLabel(puck?.metadata);
@@ -0,0 +1,69 @@
1
+ {
2
+ "layout": {
3
+ "root": {
4
+ "fields": {
5
+ "title": "Site title"
6
+ }
7
+ },
8
+ "header": {
9
+ "label": "Header",
10
+ "fields": {
11
+ "siteName": "Site name"
12
+ }
13
+ },
14
+ "pageOutlet": {
15
+ "label": "Page outlet"
16
+ },
17
+ "footer": {
18
+ "label": "Footer",
19
+ "fields": {
20
+ "text": "Text"
21
+ }
22
+ }
23
+ },
24
+ "sections": {
25
+ "hero": {
26
+ "label": "Hero",
27
+ "fields": {
28
+ "showLogo": "Show logo",
29
+ "show": "Show",
30
+ "hide": "Hide",
31
+ "logo": "Logo",
32
+ "eyebrow": "Eyebrow",
33
+ "title": "Title",
34
+ "description": "Description",
35
+ "primaryLabel": "Primary button label",
36
+ "primaryHref": "Primary button link"
37
+ }
38
+ },
39
+ "featureGrid": {
40
+ "label": "Feature grid",
41
+ "fields": {
42
+ "title": "Title",
43
+ "description": "Description",
44
+ "columns": "Columns",
45
+ "features": "Features",
46
+ "featureTitle": "Title",
47
+ "featureIcon": "Icon",
48
+ "featureDescription": "Description"
49
+ }
50
+ },
51
+ "testimonial": {
52
+ "label": "Testimonial",
53
+ "fields": {
54
+ "quote": "Quote",
55
+ "author": "Author",
56
+ "role": "Role"
57
+ }
58
+ },
59
+ "callToAction": {
60
+ "label": "Call to action",
61
+ "fields": {
62
+ "title": "Title",
63
+ "description": "Description",
64
+ "buttonLabel": "Button label",
65
+ "buttonHref": "Button link"
66
+ }
67
+ }
68
+ }
69
+ }
@@ -1,6 +1,7 @@
1
1
  import type { SudaComponentConfig, SudaLucideIconName } from "@sudajs/theme-engine";
2
2
  import { resolveAsset, SudaLucideIcon, type ThemeRenderMetadata } from "@sudajs/theme-engine/runtime";
3
3
 
4
+ import { t } from "./i18n.js";
4
5
  import { themeAsset } from "./theme-asset.js";
5
6
 
6
7
  type FeatureItem = { icon?: SudaLucideIconName; title?: string; description?: string };
@@ -33,7 +34,7 @@ type CallToActionProps = {
33
34
  };
34
35
 
35
36
  export const Hero: SudaComponentConfig<HeroProps> = {
36
- label: "Hero",
37
+ label: t("sections.hero.label"),
37
38
  ai: {
38
39
  instructions:
39
40
  "Primary page introduction used to communicate the main value proposition. " +
@@ -44,22 +45,22 @@ export const Hero: SudaComponentConfig<HeroProps> = {
44
45
  fields: {
45
46
  showLogo: {
46
47
  type: "radio",
47
- label: "Show logo",
48
+ label: t("sections.hero.fields.showLogo"),
48
49
  options: [
49
- { label: "Show", value: true },
50
- { label: "Hide", value: false },
50
+ { label: t("sections.hero.fields.show"), value: true },
51
+ { label: t("sections.hero.fields.hide"), value: false },
51
52
  ],
52
53
  },
53
54
  logo: {
54
55
  type: "image",
55
- label: "Logo",
56
+ label: t("sections.hero.fields.logo"),
56
57
  visibleIf: ({ props }) => props.showLogo !== false,
57
58
  },
58
- eyebrow: { type: "text", label: "Eyebrow" },
59
- title: { type: "text", label: "Title" },
60
- description: { type: "textarea", label: "Description" },
61
- primaryLabel: { type: "text", label: "Primary button label" },
62
- primaryHref: { type: "url", label: "Primary button link" },
59
+ eyebrow: { type: "text", label: t("sections.hero.fields.eyebrow") },
60
+ title: { type: "text", label: t("sections.hero.fields.title") },
61
+ description: { type: "textarea", label: t("sections.hero.fields.description") },
62
+ primaryLabel: { type: "text", label: t("sections.hero.fields.primaryLabel") },
63
+ primaryHref: { type: "url", label: t("sections.hero.fields.primaryHref") },
63
64
  },
64
65
  defaultProps: {
65
66
  showLogo: true,
@@ -98,7 +99,7 @@ export const Hero: SudaComponentConfig<HeroProps> = {
98
99
  };
99
100
 
100
101
  export const FeatureGrid: SudaComponentConfig<FeatureGridProps> = {
101
- label: "Feature grid",
102
+ label: t("sections.featureGrid.label"),
102
103
  ai: {
103
104
  instructions:
104
105
  "Section that lists 3–6 short feature or benefit cards explaining what the product or service offers. " +
@@ -106,16 +107,16 @@ export const FeatureGrid: SudaComponentConfig<FeatureGridProps> = {
106
107
  "Use when the page needs to communicate multiple distinct value points; do not use for testimonials, FAQs, or step-by-step processes.",
107
108
  },
108
109
  fields: {
109
- title: { type: "text", label: "Title" },
110
- description: { type: "textarea", label: "Description" },
111
- columns: { type: "range", label: "Columns", min: 2, max: 4, step: 1 },
110
+ title: { type: "text", label: t("sections.featureGrid.fields.title") },
111
+ description: { type: "textarea", label: t("sections.featureGrid.fields.description") },
112
+ columns: { type: "range", label: t("sections.featureGrid.fields.columns"), min: 2, max: 4, step: 1 },
112
113
  features: {
113
114
  type: "array",
114
- label: "Features",
115
+ label: t("sections.featureGrid.fields.features"),
115
116
  arrayFields: {
116
- title: { type: "text", label: "Title" },
117
- icon: { type: "icon", label: "Icon" },
118
- description: { type: "textarea", label: "Description" },
117
+ title: { type: "text", label: t("sections.featureGrid.fields.featureTitle") },
118
+ icon: { type: "icon", label: t("sections.featureGrid.fields.featureIcon") },
119
+ description: { type: "textarea", label: t("sections.featureGrid.fields.featureDescription") },
119
120
  },
120
121
  },
121
122
  },
@@ -150,7 +151,7 @@ export const FeatureGrid: SudaComponentConfig<FeatureGridProps> = {
150
151
  };
151
152
 
152
153
  export const Testimonial: SudaComponentConfig<TestimonialProps> = {
153
- label: "Testimonial",
154
+ label: t("sections.testimonial.label"),
154
155
  ai: {
155
156
  instructions:
156
157
  "Section that quotes a single customer or expert as social proof. " +
@@ -158,9 +159,9 @@ export const Testimonial: SudaComponentConfig<TestimonialProps> = {
158
159
  "Do not invent quotes, names, or roles; only use content the user provides.",
159
160
  },
160
161
  fields: {
161
- quote: { type: "textarea", label: "Quote" },
162
- author: { type: "text", label: "Author" },
163
- role: { type: "text", label: "Role" },
162
+ quote: { type: "textarea", label: t("sections.testimonial.fields.quote") },
163
+ author: { type: "text", label: t("sections.testimonial.fields.author") },
164
+ role: { type: "text", label: t("sections.testimonial.fields.role") },
164
165
  },
165
166
  defaultProps: {
166
167
  quote: "SudaCloud gives our team a practical editing workflow without giving up theme control.",
@@ -178,7 +179,7 @@ export const Testimonial: SudaComponentConfig<TestimonialProps> = {
178
179
  };
179
180
 
180
181
  export const CallToAction: SudaComponentConfig<CallToActionProps> = {
181
- label: "Call to action",
182
+ label: t("sections.callToAction.label"),
182
183
  ai: {
183
184
  instructions:
184
185
  "Closing conversion section that invites the visitor to take a specific action (sign up, contact, buy, etc.). " +
@@ -186,10 +187,10 @@ export const CallToAction: SudaComponentConfig<CallToActionProps> = {
186
187
  "Use at most once per page. Do not use as the page's first introduction — the Hero component fills that role.",
187
188
  },
188
189
  fields: {
189
- title: { type: "text", label: "Title" },
190
- description: { type: "textarea", label: "Description" },
191
- buttonLabel: { type: "text", label: "Button label" },
192
- buttonHref: { type: "url", label: "Button link" },
190
+ title: { type: "text", label: t("sections.callToAction.fields.title") },
191
+ description: { type: "textarea", label: t("sections.callToAction.fields.description") },
192
+ buttonLabel: { type: "text", label: t("sections.callToAction.fields.buttonLabel") },
193
+ buttonHref: { type: "url", label: t("sections.callToAction.fields.buttonHref") },
193
194
  },
194
195
  defaultProps: {
195
196
  title: "Ready to build your next page?",