@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/bin/suda-dev.js +2 -1
- package/bin/suda.js +4 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +172 -45
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/templates/theme/AGENTS.md +51 -9
- package/templates/theme/README.md +2 -0
- package/templates/theme/src/i18n.ts +13 -0
- package/templates/theme/src/layout.tsx +8 -6
- package/templates/theme/src/locales/en.json +69 -0
- package/templates/theme/src/sections.tsx +28 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sudajs/cli",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
48
|
+
label: t("sections.hero.fields.showLogo"),
|
|
48
49
|
options: [
|
|
49
|
-
{ label: "
|
|
50
|
-
{ label: "
|
|
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: "
|
|
56
|
+
label: t("sections.hero.fields.logo"),
|
|
56
57
|
visibleIf: ({ props }) => props.showLogo !== false,
|
|
57
58
|
},
|
|
58
|
-
eyebrow: { type: "text", label: "
|
|
59
|
-
title: { type: "text", label: "
|
|
60
|
-
description: { type: "textarea", label: "
|
|
61
|
-
primaryLabel: { type: "text", label: "
|
|
62
|
-
primaryHref: { type: "url", label: "
|
|
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: "
|
|
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: "
|
|
110
|
-
description: { type: "textarea", label: "
|
|
111
|
-
columns: { type: "range", label: "
|
|
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: "
|
|
115
|
+
label: t("sections.featureGrid.fields.features"),
|
|
115
116
|
arrayFields: {
|
|
116
|
-
title: { type: "text", label: "
|
|
117
|
-
icon: { type: "icon", label: "
|
|
118
|
-
description: { type: "textarea", label: "
|
|
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: "
|
|
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: "
|
|
162
|
-
author: { type: "text", label: "
|
|
163
|
-
role: { type: "text", label: "
|
|
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: "
|
|
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: "
|
|
190
|
-
description: { type: "textarea", label: "
|
|
191
|
-
buttonLabel: { type: "text", label: "
|
|
192
|
-
buttonHref: { type: "url", label: "
|
|
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?",
|