@sudajs/cli 0.7.1 → 0.8.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 +142 -1
- package/dist/index.js +365 -80
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/templates/theme/.prettierrc +7 -0
- package/templates/theme/AGENTS.md +17 -0
- package/templates/theme/README.md +8 -0
- package/templates/theme/eslint.config.js +48 -0
- package/templates/theme/package.json +24 -13
- package/templates/theme/src/assets/suda-logo.svg +17 -0
- package/templates/theme/src/config.ts +5 -5
- package/templates/theme/src/layout.tsx +17 -12
- package/templates/theme/src/sections.tsx +69 -21
- package/templates/theme/src/styles.css +12 -1
- package/templates/theme/src/templates.ts +3 -0
- package/templates/theme/src/vite-env.d.ts +3 -0
- package/templates/theme/tsconfig.json +18 -13
- package/templates/theme/vite.config.ts +18 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sudajs/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"suda": "./bin/suda.js"
|
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
28
|
-
"@puckeditor/core": "^0.21.
|
|
28
|
+
"@puckeditor/core": "^0.21.3",
|
|
29
29
|
"commander": "^12.1.0",
|
|
30
30
|
"esbuild": "^0.25.12",
|
|
31
31
|
"open": "^10.1.0",
|
|
32
|
-
"react": "^19.
|
|
33
|
-
"react-dom": "^19.
|
|
32
|
+
"react": "^19.2.7",
|
|
33
|
+
"react-dom": "^19.2.7",
|
|
34
34
|
"zod": "^3.24.1",
|
|
35
|
-
"@sudajs/theme-engine": "2.0
|
|
35
|
+
"@sudajs/theme-engine": "2.1.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@tailwindcss/postcss": "^4.3.0",
|
|
@@ -12,13 +12,16 @@ This directory is a standalone Vite + React + Tailwind Suda theme generated by `
|
|
|
12
12
|
## Styling
|
|
13
13
|
|
|
14
14
|
- Tailwind v4 starts in `src/styles.css` with `@import "tailwindcss";` and `@source "./**/*.{ts,tsx}";`.
|
|
15
|
+
- Theme-local assets live in `src/assets/`; the CLI copies them to `dist/assets/` during build.
|
|
15
16
|
- Customize `vite.config.ts`, `postcss.config.mjs`, and `src/styles.css` as needed.
|
|
16
17
|
|
|
17
18
|
## Commands
|
|
18
19
|
|
|
19
20
|
```bash
|
|
20
21
|
pnpm install
|
|
22
|
+
pnpm lint
|
|
21
23
|
pnpm typecheck
|
|
24
|
+
pnpm format
|
|
22
25
|
pnpm dev # Vite-powered local preview
|
|
23
26
|
pnpm build # Vite build + Suda artifact finalize + AI metadata check
|
|
24
27
|
pnpm validate
|
|
@@ -28,3 +31,17 @@ suda theme check
|
|
|
28
31
|
## AI metadata
|
|
29
32
|
|
|
30
33
|
Every component and layout component should declare useful `ai.instructions`. Missing component-level instructions fail `suda theme check` and block publish.
|
|
34
|
+
|
|
35
|
+
Use `visibleIf: ({ props }, { fields }) => boolean` for synchronous field visibility that depends on sibling props or resolved fields. Keep it pure and fast; use Puck `resolveFields` directly for heavier dynamic field changes. AI and MCP schemas expose `visibleIf` fields as dynamic visibility so agents know the editor surface can change with props.
|
|
36
|
+
|
|
37
|
+
## Field authoring rules
|
|
38
|
+
|
|
39
|
+
- 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.
|
|
40
|
+
- Use `select` or `radio` when authors must choose from a fixed set of values. Keep option values stable and serializable.
|
|
41
|
+
- Use `array` for repeatable items and `object` for grouped settings. Keep nested field names aligned with the prop shape and `defaultProps`.
|
|
42
|
+
- 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.
|
|
43
|
+
- 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.
|
|
44
|
+
- Keep `fields`, `defaultProps`, and `render` in sync: every editor field should have a sensible default and render should tolerate omitted optional values.
|
|
45
|
+
- 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.
|
|
46
|
+
- Keep persisted props JSON-serializable. Do not store functions, React nodes, class instances, database ids for media, or environment-specific absolute filesystem paths.
|
|
47
|
+
- Add optional field-level `ai.instructions`, `ai.required`, or `ai.exclude` when a prop needs generation guidance beyond its label and type.
|
|
@@ -12,13 +12,21 @@ pnpm install
|
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
pnpm dev
|
|
15
|
+
pnpm lint
|
|
15
16
|
pnpm typecheck
|
|
17
|
+
pnpm format
|
|
16
18
|
pnpm build
|
|
17
19
|
pnpm validate
|
|
18
20
|
```
|
|
19
21
|
|
|
20
22
|
`pnpm dev` starts the Vite-powered preview. `pnpm build` produces the Suda artifact contract in `dist/`, including `dist/index.js`, `dist/runtime.client.js`, `dist/manifest.json`, and `dist/styles.css`.
|
|
21
23
|
|
|
24
|
+
Theme-local assets live in `src/assets/`. The starter hero exposes a Logo field that defaults to `src/assets/suda-logo.svg`, so you can replace it with your own brand image or choose a different image in the editor.
|
|
25
|
+
|
|
26
|
+
Fields can use `visibleIf: ({ props }, { fields }) => boolean` for synchronous dynamic visibility. The starter hero shows this by hiding the Logo picker when `showLogo` is false. Suda converts `visibleIf` to Puck `resolveFields` during normalization, and the generated AI/MCP section schema marks those fields as dynamically visible.
|
|
27
|
+
|
|
28
|
+
When adding fields, prefer purpose-built Suda field types over generic text inputs: use `url` for links, `image` / `video` / `media` for media, `color` for colors, `icon` for icon names, `range` for bounded numbers, `spacing` for spacing tokens, `menu` for navigation, `select` / `radio` for fixed choices, and `array` / `object` for structured values. Multi-column sections such as card grids should expose a `columns` prop.
|
|
29
|
+
|
|
22
30
|
## Publish
|
|
23
31
|
|
|
24
32
|
```bash
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
import reactHooks from "eslint-plugin-react-hooks";
|
|
3
|
+
import reactRefresh from "eslint-plugin-react-refresh";
|
|
4
|
+
import globals from "globals";
|
|
5
|
+
import prettier from "eslint-config-prettier";
|
|
6
|
+
import tseslint from "typescript-eslint";
|
|
7
|
+
|
|
8
|
+
export default tseslint.config(
|
|
9
|
+
{
|
|
10
|
+
ignores: ["dist/**", ".suda-build/**", "node_modules/**", "*.tsbuildinfo"],
|
|
11
|
+
},
|
|
12
|
+
js.configs.recommended,
|
|
13
|
+
{
|
|
14
|
+
files: ["**/*.{js,mjs,cjs}"],
|
|
15
|
+
languageOptions: {
|
|
16
|
+
ecmaVersion: 2022,
|
|
17
|
+
globals: globals.node,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
...tseslint.configs.recommendedTypeChecked.map((config) => ({
|
|
21
|
+
...config,
|
|
22
|
+
files: ["**/*.{ts,tsx}"],
|
|
23
|
+
})),
|
|
24
|
+
{
|
|
25
|
+
files: ["**/*.{ts,tsx}"],
|
|
26
|
+
languageOptions: {
|
|
27
|
+
ecmaVersion: 2022,
|
|
28
|
+
globals: globals.browser,
|
|
29
|
+
parserOptions: {
|
|
30
|
+
projectService: true,
|
|
31
|
+
tsconfigRootDir: import.meta.dirname,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
plugins: {
|
|
35
|
+
"react-hooks": reactHooks,
|
|
36
|
+
"react-refresh": reactRefresh,
|
|
37
|
+
},
|
|
38
|
+
rules: {
|
|
39
|
+
...reactHooks.configs.recommended.rules,
|
|
40
|
+
"@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
|
|
41
|
+
"react-refresh/only-export-components": [
|
|
42
|
+
"warn",
|
|
43
|
+
{ allowConstantExport: true },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
prettier,
|
|
48
|
+
);
|
|
@@ -10,29 +10,40 @@
|
|
|
10
10
|
"build": "suda theme build --theme-root .",
|
|
11
11
|
"dev": "suda theme dev --theme-root .",
|
|
12
12
|
"finalize": "suda theme finalize --theme-root .",
|
|
13
|
-
"
|
|
13
|
+
"format": "prettier --write .",
|
|
14
|
+
"lint": "eslint .",
|
|
15
|
+
"lint:fix": "eslint . --fix",
|
|
16
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
14
17
|
"validate": "suda theme validate --theme-root ."
|
|
15
18
|
},
|
|
16
19
|
"devDependencies": {
|
|
20
|
+
"@eslint/js": "^9.39.4",
|
|
17
21
|
"@tailwindcss/postcss": "^4.3.0",
|
|
18
|
-
"@puckeditor/core": "^0.21.
|
|
22
|
+
"@puckeditor/core": "^0.21.3",
|
|
19
23
|
"@sudajs/cli": "*",
|
|
20
24
|
"@sudajs/theme-engine": "*",
|
|
21
|
-
"@types/node": "^
|
|
22
|
-
"@types/react": "^19.
|
|
23
|
-
"@types/react-dom": "^19.
|
|
24
|
-
"@vitejs/plugin-react": "^
|
|
25
|
+
"@types/node": "^24.13.2",
|
|
26
|
+
"@types/react": "^19.2.17",
|
|
27
|
+
"@types/react-dom": "^19.2.3",
|
|
28
|
+
"@vitejs/plugin-react": "^6.0.2",
|
|
29
|
+
"eslint": "^9.39.4",
|
|
30
|
+
"eslint-config-prettier": "^9.1.2",
|
|
31
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
32
|
+
"eslint-plugin-react-refresh": "^0.4.26",
|
|
33
|
+
"globals": "^16.5.0",
|
|
25
34
|
"postcss": "^8.5.15",
|
|
26
|
-
"
|
|
27
|
-
"react
|
|
35
|
+
"prettier": "^3.8.4",
|
|
36
|
+
"react": "^19.2.7",
|
|
37
|
+
"react-dom": "^19.2.7",
|
|
28
38
|
"tailwindcss": "^4.3.0",
|
|
29
|
-
"typescript": "
|
|
30
|
-
"
|
|
39
|
+
"typescript": "~6.0.2",
|
|
40
|
+
"typescript-eslint": "^8.62.0",
|
|
41
|
+
"vite": "^8.1.0"
|
|
31
42
|
},
|
|
32
43
|
"peerDependencies": {
|
|
33
|
-
"@puckeditor/core": "^0.21.
|
|
44
|
+
"@puckeditor/core": "^0.21.3",
|
|
34
45
|
"@sudajs/theme-engine": "*",
|
|
35
|
-
"react": "^19.
|
|
36
|
-
"react-dom": "^19.
|
|
46
|
+
"react": "^19.2.7",
|
|
47
|
+
"react-dom": "^19.2.7"
|
|
37
48
|
}
|
|
38
49
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" viewBox="0 0 2048 2048">
|
|
3
|
+
<defs>
|
|
4
|
+
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
5
|
+
<stop offset="0%" stop-color="#00B6F6" />
|
|
6
|
+
<stop offset="100%" stop-color="#0538A1" />
|
|
7
|
+
</linearGradient>
|
|
8
|
+
</defs>
|
|
9
|
+
|
|
10
|
+
<rect x="0" y="0" width="2048" height="2048" fill="url(#bg)" />
|
|
11
|
+
|
|
12
|
+
<polygon fill="#FFFFFF" points="1508,503 989,503 934,710 867,764 398,764 328,1022 847,1022 902,816 969,761 1439,761" />
|
|
13
|
+
|
|
14
|
+
<polygon fill="#FFFFFF" points="929,894 860,1153 1118,1153 1187,894" />
|
|
15
|
+
|
|
16
|
+
<polygon fill="#FFFFFF" points="1719,1025 1200,1024 1144,1233 1079,1285 608,1285 539,1544 1058,1544 1114,1335 1179,1283 1650,1283" />
|
|
17
|
+
</svg>
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { LayoutData, SudaConfig } from "@sudajs/theme-engine";
|
|
2
2
|
|
|
3
3
|
import { LAYOUT_COMPONENTS, rootConfig } from "./layout.js";
|
|
4
4
|
import { SECTION_COMPONENTS } from "./sections.js";
|
|
5
5
|
|
|
6
|
-
export const pageConfig:
|
|
6
|
+
export const pageConfig: SudaConfig = {
|
|
7
7
|
components: SECTION_COMPONENTS,
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
export const layoutConfig:
|
|
10
|
+
export const layoutConfig: SudaConfig = {
|
|
11
11
|
root: rootConfig,
|
|
12
12
|
components: LAYOUT_COMPONENTS,
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
export const defaultLayout:
|
|
16
|
-
root: { props: {
|
|
15
|
+
export const defaultLayout: LayoutData = {
|
|
16
|
+
root: { props: { title: "__SUDA_THEME_KEY__" } },
|
|
17
17
|
content: [
|
|
18
18
|
{ type: "Header", props: { id: "Header-1", siteName: "__SUDA_THEME_KEY__" } },
|
|
19
19
|
{ type: "PageOutlet", props: { id: "PageOutlet-1" } },
|
|
@@ -1,21 +1,26 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { SudaComponentConfig } from "@sudajs/theme-engine";
|
|
1
|
+
import type { SudaComponentConfig, SudaRootConfig } from "@sudajs/theme-engine";
|
|
3
2
|
import { getIcpRecord, getPageSlot, getWhiteLabel } from "@sudajs/theme-engine/runtime";
|
|
4
3
|
import type { ReactElement, ReactNode } from "react";
|
|
5
4
|
|
|
6
5
|
type PuckExtras = { puck?: { metadata?: Record<string, unknown> } };
|
|
6
|
+
type RootProps = { children?: ReactNode; title?: string };
|
|
7
|
+
type HeaderProps = { siteName?: string };
|
|
8
|
+
type PageOutletProps = PuckExtras;
|
|
9
|
+
type FooterProps = { text?: string } & PuckExtras;
|
|
7
10
|
|
|
8
|
-
export const rootConfig:
|
|
11
|
+
export const rootConfig: SudaRootConfig<RootProps> = {
|
|
9
12
|
fields: {
|
|
10
|
-
|
|
13
|
+
title: { type: "text", label: "Site title" },
|
|
11
14
|
},
|
|
12
|
-
defaultProps: {
|
|
13
|
-
render: ({ children,
|
|
14
|
-
<div className="__SUDA_THEME_KEY__-root" data-site-name={
|
|
15
|
+
defaultProps: { title: "__SUDA_THEME_KEY__" },
|
|
16
|
+
render: ({ children, title }) => (
|
|
17
|
+
<div className="__SUDA_THEME_KEY__-root" data-site-name={title}>
|
|
18
|
+
{children}
|
|
19
|
+
</div>
|
|
15
20
|
),
|
|
16
21
|
};
|
|
17
22
|
|
|
18
|
-
export const Header: SudaComponentConfig = {
|
|
23
|
+
export const Header: SudaComponentConfig<HeaderProps> = {
|
|
19
24
|
label: "Header",
|
|
20
25
|
ai: {
|
|
21
26
|
instructions:
|
|
@@ -28,7 +33,7 @@ export const Header: SudaComponentConfig = {
|
|
|
28
33
|
render: ({ siteName }) => <header className="__SUDA_THEME_KEY__-header">{siteName}</header>,
|
|
29
34
|
};
|
|
30
35
|
|
|
31
|
-
export const PageOutlet: SudaComponentConfig = {
|
|
36
|
+
export const PageOutlet: SudaComponentConfig<PageOutletProps> = {
|
|
32
37
|
label: "Page outlet",
|
|
33
38
|
ai: {
|
|
34
39
|
exclude: true,
|
|
@@ -38,10 +43,10 @@ export const PageOutlet: SudaComponentConfig = {
|
|
|
38
43
|
},
|
|
39
44
|
fields: {},
|
|
40
45
|
defaultProps: {},
|
|
41
|
-
render: (props
|
|
46
|
+
render: (props): ReactElement => <>{getPageSlot(props.puck?.metadata)}</>,
|
|
42
47
|
};
|
|
43
48
|
|
|
44
|
-
export const Footer: SudaComponentConfig = {
|
|
49
|
+
export const Footer: SudaComponentConfig<FooterProps> = {
|
|
45
50
|
label: "Footer",
|
|
46
51
|
ai: {
|
|
47
52
|
instructions:
|
|
@@ -51,7 +56,7 @@ export const Footer: SudaComponentConfig = {
|
|
|
51
56
|
},
|
|
52
57
|
fields: { text: { type: "text", label: "Text" } },
|
|
53
58
|
defaultProps: { text: "© __SUDA_THEME_KEY__" },
|
|
54
|
-
render: ({ text, puck }
|
|
59
|
+
render: ({ text, puck }) => {
|
|
55
60
|
const whiteLabel = getWhiteLabel(puck?.metadata);
|
|
56
61
|
const icp = getIcpRecord(puck?.metadata);
|
|
57
62
|
return (
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import type { SudaComponentConfig } from "@sudajs/theme-engine";
|
|
2
|
+
import { resolveAsset, type ThemeRenderMetadata } from "@sudajs/theme-engine/runtime";
|
|
3
|
+
|
|
4
|
+
import { themeAsset } from "./theme-asset.js";
|
|
2
5
|
|
|
3
6
|
type FeatureItem = { title?: string; description?: string };
|
|
7
|
+
type PuckExtras = { puck?: { metadata?: ThemeRenderMetadata } };
|
|
4
8
|
type HeroProps = {
|
|
9
|
+
showLogo?: boolean;
|
|
10
|
+
logo?: string;
|
|
5
11
|
eyebrow?: string;
|
|
6
12
|
title?: string;
|
|
7
13
|
description?: string;
|
|
8
14
|
primaryLabel?: string;
|
|
9
15
|
primaryHref?: string;
|
|
10
|
-
};
|
|
16
|
+
} & PuckExtras;
|
|
11
17
|
type FeatureGridProps = {
|
|
12
18
|
title?: string;
|
|
13
19
|
description?: string;
|
|
20
|
+
columns?: number;
|
|
14
21
|
features?: FeatureItem[];
|
|
15
22
|
};
|
|
16
23
|
type TestimonialProps = {
|
|
@@ -25,7 +32,7 @@ type CallToActionProps = {
|
|
|
25
32
|
buttonHref?: string;
|
|
26
33
|
};
|
|
27
34
|
|
|
28
|
-
export const Hero: SudaComponentConfig = {
|
|
35
|
+
export const Hero: SudaComponentConfig<HeroProps> = {
|
|
29
36
|
label: "Hero",
|
|
30
37
|
ai: {
|
|
31
38
|
instructions:
|
|
@@ -35,30 +42,62 @@ export const Hero: SudaComponentConfig = {
|
|
|
35
42
|
"Use at most once per page. Do not use for ordinary section headings or article content.",
|
|
36
43
|
},
|
|
37
44
|
fields: {
|
|
45
|
+
showLogo: {
|
|
46
|
+
type: "radio",
|
|
47
|
+
label: "Show logo",
|
|
48
|
+
options: [
|
|
49
|
+
{ label: "Show", value: true },
|
|
50
|
+
{ label: "Hide", value: false },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
logo: {
|
|
54
|
+
type: "image",
|
|
55
|
+
label: "Logo",
|
|
56
|
+
visibleIf: ({ props }) => props.showLogo !== false,
|
|
57
|
+
},
|
|
38
58
|
eyebrow: { type: "text", label: "Eyebrow" },
|
|
39
59
|
title: { type: "text", label: "Title" },
|
|
40
60
|
description: { type: "textarea", label: "Description" },
|
|
41
61
|
primaryLabel: { type: "text", label: "Primary button label" },
|
|
42
|
-
primaryHref: { type: "
|
|
62
|
+
primaryHref: { type: "url", label: "Primary button link" },
|
|
43
63
|
},
|
|
44
64
|
defaultProps: {
|
|
65
|
+
showLogo: true,
|
|
66
|
+
logo: themeAsset("assets/suda-logo.svg"),
|
|
45
67
|
eyebrow: "New theme",
|
|
46
68
|
title: "Build with SudaCloud",
|
|
47
69
|
description: "Edit this starter section in the visual editor.",
|
|
48
70
|
primaryLabel: "Get started",
|
|
49
71
|
primaryHref: "#contact",
|
|
50
72
|
},
|
|
51
|
-
render: ({
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
render: ({
|
|
74
|
+
showLogo = true,
|
|
75
|
+
logo,
|
|
76
|
+
eyebrow,
|
|
77
|
+
title,
|
|
78
|
+
description,
|
|
79
|
+
primaryLabel,
|
|
80
|
+
primaryHref,
|
|
81
|
+
puck,
|
|
82
|
+
}) => {
|
|
83
|
+
const logoSrc = resolveAsset(puck?.metadata, logo);
|
|
84
|
+
return (
|
|
85
|
+
<section className="__SUDA_THEME_KEY__-section __SUDA_THEME_KEY__-hero">
|
|
86
|
+
{showLogo && logoSrc ? (
|
|
87
|
+
<img className="__SUDA_THEME_KEY__-hero-logo" src={logoSrc} alt="" />
|
|
88
|
+
) : null}
|
|
89
|
+
<p className="__SUDA_THEME_KEY__-eyebrow">{eyebrow}</p>
|
|
90
|
+
<h1>{title}</h1>
|
|
91
|
+
<p>{description}</p>
|
|
92
|
+
<a className="__SUDA_THEME_KEY__-button" href={primaryHref}>
|
|
93
|
+
{primaryLabel}
|
|
94
|
+
</a>
|
|
95
|
+
</section>
|
|
96
|
+
);
|
|
97
|
+
},
|
|
59
98
|
};
|
|
60
99
|
|
|
61
|
-
export const FeatureGrid: SudaComponentConfig = {
|
|
100
|
+
export const FeatureGrid: SudaComponentConfig<FeatureGridProps> = {
|
|
62
101
|
label: "Feature grid",
|
|
63
102
|
ai: {
|
|
64
103
|
instructions:
|
|
@@ -69,6 +108,7 @@ export const FeatureGrid: SudaComponentConfig = {
|
|
|
69
108
|
fields: {
|
|
70
109
|
title: { type: "text", label: "Title" },
|
|
71
110
|
description: { type: "textarea", label: "Description" },
|
|
111
|
+
columns: { type: "range", label: "Columns", min: 2, max: 4, step: 1 },
|
|
72
112
|
features: {
|
|
73
113
|
type: "array",
|
|
74
114
|
label: "Features",
|
|
@@ -81,17 +121,21 @@ export const FeatureGrid: SudaComponentConfig = {
|
|
|
81
121
|
defaultProps: {
|
|
82
122
|
title: "Everything you need to launch",
|
|
83
123
|
description: "Use this section to explain the core value of the project.",
|
|
124
|
+
columns: 3,
|
|
84
125
|
features: [
|
|
85
126
|
{ title: "Fast setup", description: "Start from a clean theme contract." },
|
|
86
127
|
{ title: "Visual editing", description: "Expose content fields through Puck." },
|
|
87
128
|
{ title: "Publish ready", description: "Build and publish with the Suda CLI." },
|
|
88
129
|
],
|
|
89
130
|
},
|
|
90
|
-
render: ({ title, description, features = [] }
|
|
131
|
+
render: ({ title, description, columns = 3, features = [] }) => (
|
|
91
132
|
<section className="__SUDA_THEME_KEY__-section">
|
|
92
133
|
<h2>{title}</h2>
|
|
93
134
|
<p>{description}</p>
|
|
94
|
-
<div
|
|
135
|
+
<div
|
|
136
|
+
className="__SUDA_THEME_KEY__-grid"
|
|
137
|
+
style={{ "--__SUDA_THEME_KEY__-columns": columns }}
|
|
138
|
+
>
|
|
95
139
|
{features.map((feature: FeatureItem, index: number) => (
|
|
96
140
|
<article className="__SUDA_THEME_KEY__-card" key={index}>
|
|
97
141
|
<h3>{feature.title}</h3>
|
|
@@ -103,7 +147,7 @@ export const FeatureGrid: SudaComponentConfig = {
|
|
|
103
147
|
),
|
|
104
148
|
};
|
|
105
149
|
|
|
106
|
-
export const Testimonial: SudaComponentConfig = {
|
|
150
|
+
export const Testimonial: SudaComponentConfig<TestimonialProps> = {
|
|
107
151
|
label: "Testimonial",
|
|
108
152
|
ai: {
|
|
109
153
|
instructions:
|
|
@@ -121,15 +165,17 @@ export const Testimonial: SudaComponentConfig = {
|
|
|
121
165
|
author: "Alex Chen",
|
|
122
166
|
role: "Founder",
|
|
123
167
|
},
|
|
124
|
-
render: ({ quote, author, role }
|
|
168
|
+
render: ({ quote, author, role }) => (
|
|
125
169
|
<section className="__SUDA_THEME_KEY__-section __SUDA_THEME_KEY__-quote">
|
|
126
170
|
<blockquote>{quote}</blockquote>
|
|
127
|
-
<p>
|
|
171
|
+
<p>
|
|
172
|
+
{author} · {role}
|
|
173
|
+
</p>
|
|
128
174
|
</section>
|
|
129
175
|
),
|
|
130
176
|
};
|
|
131
177
|
|
|
132
|
-
export const CallToAction: SudaComponentConfig = {
|
|
178
|
+
export const CallToAction: SudaComponentConfig<CallToActionProps> = {
|
|
133
179
|
label: "Call to action",
|
|
134
180
|
ai: {
|
|
135
181
|
instructions:
|
|
@@ -141,7 +187,7 @@ export const CallToAction: SudaComponentConfig = {
|
|
|
141
187
|
title: { type: "text", label: "Title" },
|
|
142
188
|
description: { type: "textarea", label: "Description" },
|
|
143
189
|
buttonLabel: { type: "text", label: "Button label" },
|
|
144
|
-
buttonHref: { type: "
|
|
190
|
+
buttonHref: { type: "url", label: "Button link" },
|
|
145
191
|
},
|
|
146
192
|
defaultProps: {
|
|
147
193
|
title: "Ready to build your next page?",
|
|
@@ -149,11 +195,13 @@ export const CallToAction: SudaComponentConfig = {
|
|
|
149
195
|
buttonLabel: "Contact us",
|
|
150
196
|
buttonHref: "#contact",
|
|
151
197
|
},
|
|
152
|
-
render: ({ title, description, buttonLabel, buttonHref }
|
|
198
|
+
render: ({ title, description, buttonLabel, buttonHref }) => (
|
|
153
199
|
<section className="__SUDA_THEME_KEY__-section __SUDA_THEME_KEY__-cta" id="contact">
|
|
154
200
|
<h2>{title}</h2>
|
|
155
201
|
<p>{description}</p>
|
|
156
|
-
<a className="__SUDA_THEME_KEY__-button" href={buttonHref}>
|
|
202
|
+
<a className="__SUDA_THEME_KEY__-button" href={buttonHref}>
|
|
203
|
+
{buttonLabel}
|
|
204
|
+
</a>
|
|
157
205
|
</section>
|
|
158
206
|
),
|
|
159
207
|
};
|
|
@@ -37,6 +37,12 @@
|
|
|
37
37
|
.__SUDA_THEME_KEY__-hero {
|
|
38
38
|
background: #f8fafc;
|
|
39
39
|
}
|
|
40
|
+
.__SUDA_THEME_KEY__-hero-logo {
|
|
41
|
+
width: 64px;
|
|
42
|
+
height: 64px;
|
|
43
|
+
border-radius: 14px;
|
|
44
|
+
margin-bottom: 24px;
|
|
45
|
+
}
|
|
40
46
|
.__SUDA_THEME_KEY__-eyebrow {
|
|
41
47
|
text-transform: uppercase;
|
|
42
48
|
letter-spacing: 0.08em;
|
|
@@ -74,7 +80,7 @@
|
|
|
74
80
|
}
|
|
75
81
|
.__SUDA_THEME_KEY__-grid {
|
|
76
82
|
display: grid;
|
|
77
|
-
grid-template-columns: repeat(
|
|
83
|
+
grid-template-columns: repeat(var(--__SUDA_THEME_KEY__-columns, 3), minmax(0, 1fr));
|
|
78
84
|
gap: 16px;
|
|
79
85
|
margin-top: 28px;
|
|
80
86
|
}
|
|
@@ -100,3 +106,8 @@
|
|
|
100
106
|
background: #ffffff;
|
|
101
107
|
color: #111827;
|
|
102
108
|
}
|
|
109
|
+
@media (max-width: 760px) {
|
|
110
|
+
.__SUDA_THEME_KEY__-grid {
|
|
111
|
+
grid-template-columns: 1fr;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ThemeStarterPage } from "@sudajs/theme-engine";
|
|
2
2
|
|
|
3
|
+
import { themeAsset } from "./theme-asset.js";
|
|
4
|
+
|
|
3
5
|
export const starterPages: ThemeStarterPage[] = [
|
|
4
6
|
{
|
|
5
7
|
slug: "home",
|
|
@@ -12,6 +14,7 @@ export const starterPages: ThemeStarterPage[] = [
|
|
|
12
14
|
type: "Hero",
|
|
13
15
|
props: {
|
|
14
16
|
id: "Hero-1",
|
|
17
|
+
logo: themeAsset("assets/suda-logo.svg"),
|
|
15
18
|
eyebrow: "Starter page",
|
|
16
19
|
title: "Welcome to __SUDA_THEME_KEY__",
|
|
17
20
|
description: "This page was generated by suda theme init.",
|
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
-
"
|
|
4
|
-
"
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
|
5
6
|
"module": "NodeNext",
|
|
6
7
|
"moduleResolution": "NodeNext",
|
|
8
|
+
"types": ["node", "vite/client"],
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"verbatimModuleSyntax": true,
|
|
13
|
+
"moduleDetection": "force",
|
|
14
|
+
"noEmit": true,
|
|
7
15
|
"jsx": "react-jsx",
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
"declaration": true,
|
|
11
|
-
"declarationMap": true,
|
|
12
|
-
"sourceMap": true,
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
|
|
13
18
|
"strict": true,
|
|
14
19
|
"noUncheckedIndexedAccess": true,
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
+
"exactOptionalPropertyTypes": true,
|
|
21
|
+
"noUnusedLocals": true,
|
|
22
|
+
"noUnusedParameters": true,
|
|
23
|
+
"erasableSyntaxOnly": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true
|
|
20
25
|
},
|
|
21
|
-
"include": ["src", "
|
|
26
|
+
"include": ["src", "vite.config.ts", "postcss.config.mjs", "eslint.config.js"]
|
|
22
27
|
}
|
|
@@ -2,9 +2,27 @@ import react from "@vitejs/plugin-react";
|
|
|
2
2
|
import { defineConfig } from "vite";
|
|
3
3
|
import pkg from "./package.json" with { type: "json" };
|
|
4
4
|
|
|
5
|
+
function assetFileNames(assetInfo: { names?: string[] | undefined }): string {
|
|
6
|
+
const names = assetInfo.names ?? [];
|
|
7
|
+
const isCss = names.some((name) => name.endsWith(".css"));
|
|
8
|
+
if (isCss) {
|
|
9
|
+
return "styles.css";
|
|
10
|
+
}
|
|
11
|
+
return "assets/[name]-[hash][extname]";
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
export default defineConfig({
|
|
6
15
|
define: {
|
|
7
16
|
__SUDA_THEME_VERSION__: JSON.stringify(pkg.version),
|
|
8
17
|
},
|
|
9
18
|
plugins: [react()],
|
|
19
|
+
build: {
|
|
20
|
+
assetsInlineLimit: 0,
|
|
21
|
+
cssCodeSplit: false,
|
|
22
|
+
rollupOptions: {
|
|
23
|
+
output: {
|
|
24
|
+
assetFileNames,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
10
28
|
});
|