@gradial/aci 0.1.31 → 0.1.33
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/README.md +4 -4
- package/bin/aci-darwin-arm64 +0 -0
- package/bin/aci.js +29 -31
- package/dist/astro/assets.d.ts +46 -0
- package/dist/astro/assets.js +51 -0
- package/dist/astro/index.d.ts +2 -0
- package/dist/astro/index.js +8 -0
- package/dist/astro/page.d.ts +88 -0
- package/dist/astro/page.js +117 -0
- package/dist/dev/content-watch.d.ts +1 -0
- package/dist/dev/content-watch.js +1 -1
- package/dist/next/config.d.ts +6 -0
- package/dist/next/config.js +20 -13
- package/dist/next/content-watch.d.ts +1 -0
- package/dist/next/content-watch.js +2 -2
- package/dist/next/index.d.ts +1 -0
- package/dist/next/index.js +1 -0
- package/dist/next/middleware.d.ts +3 -2
- package/dist/next/middleware.js +40 -7
- package/dist/next/page.d.ts +13 -5
- package/dist/next/page.js +42 -17
- package/dist/next/server.d.ts +1 -1
- package/dist/next/server.js +7 -8
- package/dist/testing/conformance/checks.d.ts +12 -0
- package/dist/testing/conformance/checks.js +483 -0
- package/dist/testing/conformance/index.d.ts +3 -0
- package/dist/testing/conformance/index.js +2 -0
- package/dist/testing/conformance/preset.d.ts +14 -0
- package/dist/testing/conformance/preset.js +98 -0
- package/dist/testing/conformance/types.d.ts +45 -0
- package/dist/testing/conformance/types.js +7 -0
- package/package.json +17 -9
- package/src/cli/materialize-template.mjs +133 -0
- package/src/cli/validate-content.mjs +0 -0
- package/src/cli/verify-next-cdn-routes.mjs +10 -19
- package/templates/astro/starter-content/src/cms/contracts/components/button.contract.ts +19 -0
- package/templates/astro/starter-content/src/cms/contracts/components/container.contract.ts +19 -0
- package/templates/astro/starter-content/src/cms/contracts/components/featureCard.contract.ts +15 -0
- package/templates/astro/starter-content/src/cms/contracts/components/featureGrid.contract.ts +16 -0
- package/templates/astro/starter-content/src/cms/contracts/components/homeHero.contract.ts +36 -0
- package/templates/astro/starter-content/src/cms/contracts/components/index.ts +21 -0
- package/templates/astro/starter-content/src/cms/contracts/components/statsBar.contract.ts +19 -0
- package/templates/astro/starter-content/src/cms/registry.ts +28 -0
- package/templates/astro/template/{.aci.yaml → .aci.yaml.tmpl} +1 -1
- package/templates/astro/template/README.md.tmpl +53 -0
- package/templates/astro/template/gitignore +12 -0
- package/templates/astro/template/package.json.tmpl +5 -3
- package/templates/astro/template/src/cms/contracts/components/index.ts +3 -0
- package/templates/astro/template/src/cms/registry.ts +3 -26
- package/templates/astro/template/src/pages/[...slug].astro +13 -35
- package/templates/astro/template/tests/conformance.test.ts.tmpl +12 -0
- package/templates/common/.agents/AGENTS.md +3 -5
- package/templates/common/.agents/skills/aci/SKILL.md +72 -23
- package/templates/common/.agents/skills/authoring/SKILL.md +53 -14
- package/templates/common/.agents/skills/authoring/references/block-registry.md +109 -15
- package/templates/common/.agents/skills/authoring/references/contracts.md +23 -7
- package/templates/common/.agents/skills/authoring/references/repo-structure.md +35 -19
- package/templates/common/.agents/skills/authoring/references/sdk-server-functions.md +75 -17
- package/templates/common/.agents/skills/migration/SKILL.md +7 -1
- package/templates/common/.agents/skills/migration/references/canon-migration.md +471 -0
- package/templates/common/.content/config/site.json.tmpl +15 -0
- package/templates/common/.content/pages/home/_index.json +4 -103
- package/templates/common/.nvmrc +1 -0
- package/templates/common/README.md.tmpl +32 -11
- package/templates/common/contracts/components/index.ts +1 -19
- package/templates/common/design-system/tokens/colors.css +34 -10
- package/templates/common/starter-content/.content/config/robots.txt +2 -0
- package/templates/common/starter-content/.content/pages/home/_index.json +112 -0
- package/templates/common/starter-content/.content/schemas/site.schema.json +138 -0
- package/templates/nextjs/starter-content/src/cms/contracts/components/container.contract.ts +19 -0
- package/templates/nextjs/starter-content/src/cms/contracts/components/featureCard.contract.ts +15 -0
- package/templates/nextjs/starter-content/src/cms/contracts/components/featureGrid.contract.ts +16 -0
- package/templates/nextjs/starter-content/src/cms/contracts/components/index.ts +21 -0
- package/templates/nextjs/starter-content/src/cms/contracts/components/siteFooter.contract.ts +45 -0
- package/templates/nextjs/starter-content/src/cms/contracts/components/siteNavigation.contract.ts +30 -0
- package/templates/nextjs/starter-content/src/cms/contracts/components/statsBar.contract.ts +19 -0
- package/templates/nextjs/starter-content/src/cms/registry.ts +28 -0
- package/templates/nextjs/template/{.aci.yaml → .aci.yaml.tmpl} +1 -1
- package/templates/nextjs/template/README.md.tmpl +52 -0
- package/templates/nextjs/template/gitignore +15 -0
- package/templates/nextjs/template/next.config.mjs +1 -6
- package/templates/nextjs/template/package.json.tmpl +5 -3
- package/templates/nextjs/template/src/app/gradial/assets/[...path]/route.ts +1 -4
- package/templates/nextjs/template/src/cms/contracts/components/index.ts +1 -13
- package/templates/nextjs/template/src/cms/registry.ts +3 -26
- package/templates/nextjs/template/src/design-system/styles.css +346 -0
- package/templates/nextjs/template/src/design-system/tokens/colors.css +110 -0
- package/templates/nextjs/template/src/design-system/tokens/components.css +86 -0
- package/templates/nextjs/template/src/design-system/tokens/index.css +6 -0
- package/templates/nextjs/template/src/design-system/tokens/radii.css +5 -0
- package/templates/nextjs/template/src/design-system/tokens/shadows.css +9 -0
- package/templates/nextjs/template/src/design-system/tokens/spacing.css +26 -0
- package/templates/nextjs/template/src/design-system/tokens/typography.css +32 -0
- package/templates/nextjs/template/src/middleware.ts.tmpl +12 -0
- package/templates/nextjs/template/tests/conformance.test.ts.tmpl +12 -0
- package/templates/nextjs/template/src/cms/contracts/components/container.contract.ts +0 -20
- package/templates/nextjs/template/src/middleware.ts +0 -7
- /package/templates/{nextjs/template → astro/starter-content}/src/cms/contracts/components/siteFooter.contract.ts +0 -0
- /package/templates/{nextjs/template → astro/starter-content}/src/cms/contracts/components/siteNavigation.contract.ts +0 -0
- /package/templates/astro/{template → starter-content}/src/components/chrome/Footer.astro +0 -0
- /package/templates/astro/{template → starter-content}/src/components/chrome/Header.astro +0 -0
- /package/templates/astro/{template → starter-content}/src/components/elements/AtomicBlock.astro +0 -0
- /package/templates/astro/{template → starter-content}/src/components/elements/AtomicBlocks.astro +0 -0
- /package/templates/astro/{template → starter-content}/src/components/elements/Button.astro +0 -0
- /package/templates/astro/{template → starter-content}/src/components/elements/ButtonBlock.astro +0 -0
- /package/templates/astro/{template → starter-content}/src/components/elements/Container.astro +0 -0
- /package/templates/astro/{template → starter-content}/src/components/elements/Icon.astro +0 -0
- /package/templates/astro/{template → starter-content}/src/components/elements/Logo.astro +0 -0
- /package/templates/astro/{template → starter-content}/src/components/elements/RemoteImage.astro +0 -0
- /package/templates/astro/{template → starter-content}/src/components/sections/ContainerSection.astro +0 -0
- /package/templates/astro/{template → starter-content}/src/components/sections/FeatureCard.astro +0 -0
- /package/templates/astro/{template → starter-content}/src/components/sections/FeatureGrid.astro +0 -0
- /package/templates/astro/{template → starter-content}/src/components/sections/HomeHero.astro +0 -0
- /package/templates/astro/{template → starter-content}/src/components/sections/StatsBar.astro +0 -0
- /package/templates/astro/{template → starter-content}/src/fixtures/atomicBlocks.ts +0 -0
- /package/templates/astro/{template → starter-content}/src/fixtures/composition.ts +0 -0
- /package/templates/astro/{template → starter-content}/src/fixtures/index.ts +0 -0
- /package/templates/astro/{template → starter-content}/src/fixtures/sections.ts +0 -0
- /package/templates/astro/{template → starter-content}/src/render/RenderPage.astro +0 -0
- /package/templates/astro/{template → starter-content}/src/stories/CompositionStoryFrame.astro +0 -0
- /package/templates/astro/{template → starter-content}/src/stories/HomeHero.stories.ts +0 -0
- /package/templates/astro/{template → starter-content}/src/stories/SectionStoryFrame.astro +0 -0
- /package/templates/astro/{template → starter-content}/src/stories/StoryDocFrame.astro +0 -0
- /package/templates/common/{.content → starter-content/.content}/.gradial-dam/blobs/sha256/7f/bf/7fbfc7fe230ff12688c496f660119e41d9725c4504d82e310c15665c73c09292 +0 -0
- /package/templates/common/{.content → starter-content/.content}/.gradial-dam/blobs/sha256/a0/f5/a0f506689de9b15a7055f70103a3f02cd3ba105771ae2de6754df2ede1210d3a +0 -0
- /package/templates/common/{.content → starter-content/.content}/.gradial-dam/blobs/sha256/dd/b7/ddb7e9642be08317bcd4b2bd26b58ba8d0d34dd7239e03e684a4f3634fa3845b +0 -0
- /package/templates/common/{.content → starter-content/.content}/.gradial-dam/blobs/sha256/fb/32/fb3260e8286ea8231ea1c77b9c0be92d6edd9026565731eb14cfdcfd6b1b75b3 +0 -0
- /package/templates/common/{.content → starter-content/.content}/config/site.json +0 -0
- /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_about_hero/asset.json +0 -0
- /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_about_hero/editorial.json +0 -0
- /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_about_hero/governance.json +0 -0
- /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_about_hero/versions/v0001.json +0 -0
- /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_home_hero/asset.json +0 -0
- /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_home_hero/editorial.json +0 -0
- /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_home_hero/governance.json +0 -0
- /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_home_hero/versions/v0001.json +0 -0
- /package/templates/common/{.content → starter-content/.content}/fragments/navbar/_index.json +0 -0
- /package/templates/common/{.content → starter-content/.content}/fragments/site-footer/_index.json +0 -0
- /package/templates/common/{.content → starter-content/.content}/pages/about/_index.json +0 -0
- /package/templates/common/{public → starter-content/public}/assets/Dwire-model.png +0 -0
- /package/templates/common/{public → starter-content/public}/assets/seattle.avif +0 -0
- /package/templates/nextjs/{template → starter-content}/src/cms/contracts/components/button.contract.ts +0 -0
- /package/templates/nextjs/{template → starter-content}/src/cms/contracts/components/homeHero.contract.ts +0 -0
- /package/templates/nextjs/{template → starter-content}/src/components/chrome/Footer.tsx +0 -0
- /package/templates/nextjs/{template → starter-content}/src/components/chrome/Header.tsx +0 -0
- /package/templates/nextjs/{template → starter-content}/src/components/elements/AtomicBlock.tsx +0 -0
- /package/templates/nextjs/{template → starter-content}/src/components/elements/Button.tsx +0 -0
- /package/templates/nextjs/{template → starter-content}/src/components/elements/ButtonBlock.tsx +0 -0
- /package/templates/nextjs/{template → starter-content}/src/components/elements/Container.tsx +0 -0
- /package/templates/nextjs/{template → starter-content}/src/components/elements/Icon.tsx +0 -0
- /package/templates/nextjs/{template → starter-content}/src/components/elements/Logo.tsx +0 -0
- /package/templates/nextjs/{template → starter-content}/src/components/sections/ContainerSection.tsx +0 -0
- /package/templates/nextjs/{template → starter-content}/src/components/sections/FeatureCard.tsx +0 -0
- /package/templates/nextjs/{template → starter-content}/src/components/sections/FeatureGrid.tsx +0 -0
- /package/templates/nextjs/{template → starter-content}/src/components/sections/HomeHero.tsx +0 -0
- /package/templates/nextjs/{template → starter-content}/src/components/sections/StatsBar.tsx +0 -0
- /package/templates/nextjs/{template → starter-content}/src/stories/Button.stories.tsx +0 -0
- /package/templates/nextjs/{template → starter-content}/src/stories/Header.stories.tsx +0 -0
- /package/templates/nextjs/{template → starter-content}/src/stories/HomeHero.stories.tsx +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gradial/aci",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -83,8 +83,20 @@
|
|
|
83
83
|
"types": "./dist/astro/index.d.ts",
|
|
84
84
|
"import": "./dist/astro/index.js",
|
|
85
85
|
"default": "./dist/astro/index.js"
|
|
86
|
+
},
|
|
87
|
+
"./testing": {
|
|
88
|
+
"types": "./dist/testing/conformance/index.d.ts",
|
|
89
|
+
"import": "./dist/testing/conformance/index.js"
|
|
86
90
|
}
|
|
87
91
|
},
|
|
92
|
+
"scripts": {
|
|
93
|
+
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
|
|
94
|
+
"test": "node --import tsx --test \"src/**/*.test.ts\"",
|
|
95
|
+
"build:cli": "cd ../.. && ./scripts/build-cli.sh",
|
|
96
|
+
"generate-starters": "cd ../.. && tsx scripts/generate-starters.ts",
|
|
97
|
+
"compile-registry": "tsx src/cli/compile-registry.mjs",
|
|
98
|
+
"prepack": "npm run generate-starters && npm run build && npm run build:cli && node scripts/ensure-bin-modes.mjs"
|
|
99
|
+
},
|
|
88
100
|
"dependencies": {
|
|
89
101
|
"markdown-it": "^14.1.0",
|
|
90
102
|
"sanitize-html": "^2.13.1",
|
|
@@ -112,19 +124,15 @@
|
|
|
112
124
|
}
|
|
113
125
|
},
|
|
114
126
|
"devDependencies": {
|
|
127
|
+
"@types/markdown-it": "^14.1.2",
|
|
115
128
|
"@types/node": "^24.10.1",
|
|
116
129
|
"@types/react": "^19.0.0",
|
|
117
|
-
"@types/markdown-it": "^14.1.2",
|
|
118
130
|
"@types/sanitize-html": "^2.16.0",
|
|
119
131
|
"@types/ws": "^8.18.0",
|
|
132
|
+
"jsdom": "^29.1.1",
|
|
133
|
+
"next": "^15.5.19",
|
|
120
134
|
"tsx": "^4.20.0",
|
|
121
135
|
"typescript": "^5.9.3",
|
|
122
136
|
"zod": "^4.0.0"
|
|
123
|
-
},
|
|
124
|
-
"scripts": {
|
|
125
|
-
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
|
|
126
|
-
"build:cli": "cd ../.. && ./scripts/build-cli.sh",
|
|
127
|
-
"generate-starters": "cd ../.. && tsx scripts/generate-starters.ts",
|
|
128
|
-
"compile-registry": "tsx src/cli/compile-registry.mjs"
|
|
129
137
|
}
|
|
130
|
-
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Single assembly path for scaffolding a site from the SDK templates.
|
|
2
|
+
//
|
|
3
|
+
// Both `scripts/generate-starters.ts` (repo codegen for starters/*) and
|
|
4
|
+
// `aci init` (bin/aci.js, customer scaffolding from the published package)
|
|
5
|
+
// call materializeTemplate so the two outputs can never drift.
|
|
6
|
+
// Spec: specs/features/starter-fresh-clone-dx (D-1).
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
|
|
10
|
+
const EXCLUDE_PATTERNS = [
|
|
11
|
+
/node_modules/,
|
|
12
|
+
/\.DS_Store$/,
|
|
13
|
+
/package-lock\.json$/,
|
|
14
|
+
/\.git$/,
|
|
15
|
+
/storybook-static/,
|
|
16
|
+
/starter-content/,
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const COMMON_DIRS = ['.aci', '.agents', '.content', 'eslint-rules'];
|
|
20
|
+
const COMMON_ROOT_FILES = ['.nvmrc'];
|
|
21
|
+
|
|
22
|
+
// Per-framework values rendered into shared templates (e.g. README.md.tmpl).
|
|
23
|
+
// Derived here so `aci init` and `generate-starters` can never disagree.
|
|
24
|
+
const FRAMEWORK_VARS = {
|
|
25
|
+
nextjs: { devUrl: 'http://localhost:3000', frameworkName: 'Next.js' },
|
|
26
|
+
astro: { devUrl: 'http://localhost:4321', frameworkName: 'Astro' },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Exclusions match paths RELATIVE to the copy root: the SDK itself is often
|
|
30
|
+
// installed under a node_modules/ directory (npm install, the npx cache), so
|
|
31
|
+
// matching absolute paths would exclude every template file.
|
|
32
|
+
function shouldExclude(relPath) {
|
|
33
|
+
return EXCLUDE_PATTERNS.some((pattern) => pattern.test(relPath));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function copyDirSync(src, dest, root = src) {
|
|
37
|
+
if (!fs.existsSync(src)) return;
|
|
38
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
const srcPath = path.join(src, entry.name);
|
|
41
|
+
const destPath = path.join(dest, entry.name);
|
|
42
|
+
if (shouldExclude(path.relative(root, srcPath))) continue;
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
45
|
+
copyDirSync(srcPath, destPath, root);
|
|
46
|
+
} else {
|
|
47
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
48
|
+
fs.copyFileSync(srcPath, destPath);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function renderTmplFiles(dir, vars, root = dir) {
|
|
54
|
+
if (!fs.existsSync(dir)) return;
|
|
55
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const fullPath = path.join(dir, entry.name);
|
|
58
|
+
if (shouldExclude(path.relative(root, fullPath))) continue;
|
|
59
|
+
if (entry.isDirectory()) {
|
|
60
|
+
renderTmplFiles(fullPath, vars, root);
|
|
61
|
+
} else if (entry.name.endsWith('.tmpl')) {
|
|
62
|
+
let content = fs.readFileSync(fullPath, 'utf8');
|
|
63
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
64
|
+
// Function replacement so `$` sequences in values (site names are
|
|
65
|
+
// user-controlled) are inserted literally, not as $-patterns.
|
|
66
|
+
content = content.replaceAll(`{{${key}}}`, () => value);
|
|
67
|
+
}
|
|
68
|
+
fs.writeFileSync(fullPath.replace(/\.tmpl$/, ''), content);
|
|
69
|
+
fs.unlinkSync(fullPath);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Assemble a complete site tree from the SDK templates.
|
|
76
|
+
*
|
|
77
|
+
* @param {object} opts
|
|
78
|
+
* @param {string} opts.templatesDir absolute path to the templates root
|
|
79
|
+
* @param {string} opts.framework 'nextjs' | 'astro'
|
|
80
|
+
* @param {string} opts.destDir absolute destination (created if missing)
|
|
81
|
+
* @param {{siteName: string, version: string, aciVersion: string}} opts.vars
|
|
82
|
+
*/
|
|
83
|
+
export function materializeTemplate({ templatesDir, framework, destDir, vars }) {
|
|
84
|
+
const commonDir = path.join(templatesDir, 'common');
|
|
85
|
+
const templateDir = path.join(templatesDir, framework, 'template');
|
|
86
|
+
if (!fs.existsSync(templateDir)) {
|
|
87
|
+
throw new Error(`unknown framework template: ${templateDir}`);
|
|
88
|
+
}
|
|
89
|
+
for (const key of ['siteName', 'version', 'aciVersion']) {
|
|
90
|
+
if (!vars[key]) throw new Error(`materializeTemplate: vars.${key} is required`);
|
|
91
|
+
}
|
|
92
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
93
|
+
|
|
94
|
+
for (const dir of COMMON_DIRS) {
|
|
95
|
+
copyDirSync(path.join(commonDir, dir), path.join(destDir, dir));
|
|
96
|
+
}
|
|
97
|
+
for (const file of COMMON_ROOT_FILES) {
|
|
98
|
+
const src = path.join(commonDir, file);
|
|
99
|
+
if (fs.existsSync(src)) fs.copyFileSync(src, path.join(destDir, file));
|
|
100
|
+
}
|
|
101
|
+
copyDirSync(path.join(commonDir, 'public', 'assets'), path.join(destDir, 'public', 'assets'));
|
|
102
|
+
const readmeTmpl = path.join(commonDir, 'README.md.tmpl');
|
|
103
|
+
if (fs.existsSync(readmeTmpl)) {
|
|
104
|
+
fs.copyFileSync(readmeTmpl, path.join(destDir, 'README.md.tmpl'));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
copyDirSync(templateDir, destDir);
|
|
108
|
+
|
|
109
|
+
// Shared sources overwrite any framework-template copies so contracts and
|
|
110
|
+
// design tokens can only come from templates/common.
|
|
111
|
+
copyDirSync(
|
|
112
|
+
path.join(commonDir, 'contracts', 'components'),
|
|
113
|
+
path.join(destDir, 'src', 'cms', 'contracts', 'components'),
|
|
114
|
+
);
|
|
115
|
+
copyDirSync(
|
|
116
|
+
path.join(commonDir, 'design-system', 'tokens'),
|
|
117
|
+
path.join(destDir, 'src', 'design-system', 'tokens'),
|
|
118
|
+
);
|
|
119
|
+
const stylesSrc = path.join(commonDir, 'design-system', 'styles.css');
|
|
120
|
+
if (fs.existsSync(stylesSrc)) {
|
|
121
|
+
fs.mkdirSync(path.join(destDir, 'src', 'design-system'), { recursive: true });
|
|
122
|
+
fs.copyFileSync(stylesSrc, path.join(destDir, 'src', 'design-system', 'styles.css'));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// npm pack drops `.gitignore` files from the published templates, so the
|
|
126
|
+
// framework templates ship `gitignore` and it is renamed on materialize.
|
|
127
|
+
const gitignore = path.join(destDir, 'gitignore');
|
|
128
|
+
if (fs.existsSync(gitignore)) {
|
|
129
|
+
fs.renameSync(gitignore, path.join(destDir, '.gitignore'));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
renderTmplFiles(destDir, { ...FRAMEWORK_VARS[framework], ...vars });
|
|
133
|
+
}
|
|
File without changes
|
|
@@ -31,20 +31,6 @@ function checkMissing(label, relativePath, message) {
|
|
|
31
31
|
checks.push(`✓ ${label}`);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function checkOptionalFile(label, relativePath, validate, message) {
|
|
35
|
-
const fullPath = path.join(projectRoot, relativePath);
|
|
36
|
-
if (!fs.existsSync(fullPath)) {
|
|
37
|
-
checks.push(`- ${label}: ${relativePath} not found (optional)`);
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
41
|
-
if (!validate(content)) {
|
|
42
|
-
checks.push(`✗ ${label}: ${message}`);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
checks.push(`✓ ${label}`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
34
|
checkFile(
|
|
49
35
|
'middleware is configured',
|
|
50
36
|
'src/middleware.ts',
|
|
@@ -60,17 +46,22 @@ checkFile(
|
|
|
60
46
|
'Expected catch-all page to use createPage() from the SDK.',
|
|
61
47
|
);
|
|
62
48
|
|
|
63
|
-
|
|
64
|
-
'
|
|
49
|
+
checkMissing(
|
|
50
|
+
'legacy required catch-all is removed',
|
|
51
|
+
'src/app/[...slug]/page.tsx',
|
|
52
|
+
'Use src/app/[[...slug]]/page.tsx (optional catch-all). The required [...slug] form breaks file-provider local dev where "/" arrives with an empty slug.',
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
checkMissing(
|
|
56
|
+
'legacy app-level asset route is removed',
|
|
65
57
|
'src/app/gradial/assets/[...path]/route.ts',
|
|
66
|
-
(
|
|
67
|
-
'Expected public asset route to re-export @gradial/aci/next/asset-route.',
|
|
58
|
+
'Assets are served via URL rewrites in withAci(), not an app-level route.',
|
|
68
59
|
);
|
|
69
60
|
|
|
70
61
|
checkMissing(
|
|
71
62
|
'legacy release-scoped asset route is removed',
|
|
72
63
|
'src/app/gradial/assets/[releaseId]/[...path]/route.ts',
|
|
73
|
-
'
|
|
64
|
+
'Assets are served via URL rewrites in withAci(), not an app-level route.',
|
|
74
65
|
);
|
|
75
66
|
|
|
76
67
|
checkMissing(
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineComponentContract } from '@gradial/aci';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
export const schema = z.object({
|
|
5
|
+
label: z.string().min(1),
|
|
6
|
+
href: z.string().min(1),
|
|
7
|
+
variant: z.enum(['primary', 'secondary', 'outline', 'tertiary']).optional(),
|
|
8
|
+
newWindow: z.boolean().optional(),
|
|
9
|
+
icon: z.string().optional(),
|
|
10
|
+
iconPosition: z.enum(['before', 'after']).optional(),
|
|
11
|
+
className: z.string().optional(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export type ContentProps = z.infer<typeof schema>;
|
|
15
|
+
|
|
16
|
+
export const buttonContract = defineComponentContract({
|
|
17
|
+
id: 'button',
|
|
18
|
+
props: schema,
|
|
19
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineComponentContract, blockRefArray } from '@gradial/aci';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { buttonContract } from './button.contract';
|
|
4
|
+
import { featureGridContract } from './featureGrid.contract';
|
|
5
|
+
import { featureCardContract } from './featureCard.contract';
|
|
6
|
+
import { statsBarContract } from './statsBar.contract';
|
|
7
|
+
|
|
8
|
+
export const schema = z.object({
|
|
9
|
+
theme: z.enum(['auto', 'light', 'dark']).optional(),
|
|
10
|
+
className: z.string().optional(),
|
|
11
|
+
content: blockRefArray([buttonContract, featureGridContract, featureCardContract, statsBarContract]).optional(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export type ContentProps = z.infer<typeof schema>;
|
|
15
|
+
|
|
16
|
+
export const containerContract = defineComponentContract({
|
|
17
|
+
id: 'container',
|
|
18
|
+
props: schema,
|
|
19
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineComponentContract } from '@gradial/aci';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
export const schema = z.object({
|
|
5
|
+
icon: z.string().optional(),
|
|
6
|
+
title: z.string().min(1),
|
|
7
|
+
description: z.string().optional(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export type ContentProps = z.infer<typeof schema>;
|
|
11
|
+
|
|
12
|
+
export const featureCardContract = defineComponentContract({
|
|
13
|
+
id: 'feature_card',
|
|
14
|
+
props: schema,
|
|
15
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineComponentContract, blockRefArray } from '@gradial/aci';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { featureCardContract } from './featureCard.contract';
|
|
4
|
+
|
|
5
|
+
export const schema = z.object({
|
|
6
|
+
title: z.string().optional(),
|
|
7
|
+
subtitle: z.string().optional(),
|
|
8
|
+
features: blockRefArray([featureCardContract]).optional(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export type ContentProps = z.infer<typeof schema>;
|
|
12
|
+
|
|
13
|
+
export const featureGridContract = defineComponentContract({
|
|
14
|
+
id: 'feature_grid',
|
|
15
|
+
props: schema,
|
|
16
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { defineComponentContract, ImageSchema } from '@gradial/aci';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
export const schema = z.object({
|
|
5
|
+
eyebrow: z.string().optional(),
|
|
6
|
+
headline: z.string().min(1),
|
|
7
|
+
description: z.string().optional(),
|
|
8
|
+
ctaLabel: z.string().min(1),
|
|
9
|
+
ctaHref: z.string().min(1),
|
|
10
|
+
image: ImageSchema.optional(),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export type ContentProps = z.infer<typeof schema>;
|
|
14
|
+
|
|
15
|
+
export const homeHeroContract = defineComponentContract({
|
|
16
|
+
id: 'home_hero',
|
|
17
|
+
props: schema,
|
|
18
|
+
imageSlots: {
|
|
19
|
+
image: {
|
|
20
|
+
formats: ['image/avif', 'image/webp', 'image/jpeg'],
|
|
21
|
+
sizes: '100vw',
|
|
22
|
+
outputs: [
|
|
23
|
+
{
|
|
24
|
+
aspectRatio: '16:9',
|
|
25
|
+
widths: [768, 1280, 1920],
|
|
26
|
+
media: '(min-width: 768px)',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
aspectRatio: '3:4',
|
|
30
|
+
widths: [480, 720, 960],
|
|
31
|
+
media: '(max-width: 767px)',
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { buttonContract } from './button.contract';
|
|
2
|
+
import { containerContract } from './container.contract';
|
|
3
|
+
import { featureCardContract } from './featureCard.contract';
|
|
4
|
+
import { featureGridContract } from './featureGrid.contract';
|
|
5
|
+
import { homeHeroContract } from './homeHero.contract';
|
|
6
|
+
import { siteNavigationContract } from './siteNavigation.contract';
|
|
7
|
+
import { siteFooterContract } from './siteFooter.contract';
|
|
8
|
+
import { statsBarContract } from './statsBar.contract';
|
|
9
|
+
|
|
10
|
+
const componentContracts = [
|
|
11
|
+
homeHeroContract,
|
|
12
|
+
containerContract,
|
|
13
|
+
buttonContract,
|
|
14
|
+
siteNavigationContract,
|
|
15
|
+
siteFooterContract,
|
|
16
|
+
featureGridContract,
|
|
17
|
+
featureCardContract,
|
|
18
|
+
statsBarContract,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export default componentContracts;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineComponentContract } from '@gradial/aci';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const statSchema = z.object({
|
|
5
|
+
label: z.string().min(1),
|
|
6
|
+
value: z.string().min(1),
|
|
7
|
+
suffix: z.string().optional(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const schema = z.object({
|
|
11
|
+
stats: z.array(statSchema).min(1),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export type ContentProps = z.infer<typeof schema>;
|
|
15
|
+
|
|
16
|
+
export const statsBarContract = defineComponentContract({
|
|
17
|
+
id: 'stats_bar',
|
|
18
|
+
props: schema,
|
|
19
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createRegistry } from '@gradial/aci';
|
|
2
|
+
import { homeHeroContract } from './contracts/components/homeHero.contract';
|
|
3
|
+
import { containerContract } from './contracts/components/container.contract';
|
|
4
|
+
import { buttonContract } from './contracts/components/button.contract';
|
|
5
|
+
import { siteNavigationContract } from './contracts/components/siteNavigation.contract';
|
|
6
|
+
import { siteFooterContract } from './contracts/components/siteFooter.contract';
|
|
7
|
+
import { featureGridContract } from './contracts/components/featureGrid.contract';
|
|
8
|
+
import { featureCardContract } from './contracts/components/featureCard.contract';
|
|
9
|
+
import { statsBarContract } from './contracts/components/statsBar.contract';
|
|
10
|
+
import HomeHero from '../components/sections/HomeHero.astro';
|
|
11
|
+
import ContainerSection from '../components/sections/ContainerSection.astro';
|
|
12
|
+
import ButtonBlock from '../components/elements/ButtonBlock.astro';
|
|
13
|
+
import Header from '../components/chrome/Header.astro';
|
|
14
|
+
import Footer from '../components/chrome/Footer.astro';
|
|
15
|
+
import FeatureGrid from '../components/sections/FeatureGrid.astro';
|
|
16
|
+
import FeatureCard from '../components/sections/FeatureCard.astro';
|
|
17
|
+
import StatsBar from '../components/sections/StatsBar.astro';
|
|
18
|
+
|
|
19
|
+
export const registry = createRegistry([
|
|
20
|
+
[homeHeroContract, HomeHero],
|
|
21
|
+
[containerContract, ContainerSection],
|
|
22
|
+
[buttonContract, ButtonBlock],
|
|
23
|
+
[siteNavigationContract, Header],
|
|
24
|
+
[siteFooterContract, Footer],
|
|
25
|
+
[featureGridContract, FeatureGrid],
|
|
26
|
+
[featureCardContract, FeatureCard],
|
|
27
|
+
[statsBarContract, StatsBar],
|
|
28
|
+
]);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# {{siteName}}
|
|
2
|
+
|
|
3
|
+
An Astro site powered by [ACI](https://docs.gradial.com/aci) (Agentic Content Infrastructure).
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The dev server starts at `http://localhost:4321`. Content changes in `.content/`
|
|
13
|
+
are picked up automatically via the ACI integration in `astro.config.mjs`.
|
|
14
|
+
|
|
15
|
+
## Adding Your First Component
|
|
16
|
+
|
|
17
|
+
See `.agents/skills/authoring/SKILL.md` for a step-by-step guide to:
|
|
18
|
+
1. Define a component contract in `src/cms/contracts/components/`
|
|
19
|
+
2. Build the Astro component in `src/components/`
|
|
20
|
+
3. Register it in `src/cms/registry.ts`
|
|
21
|
+
4. Add content to `.content/pages/`
|
|
22
|
+
|
|
23
|
+
## Skills
|
|
24
|
+
|
|
25
|
+
| Skill | When to read |
|
|
26
|
+
|-------|-------------|
|
|
27
|
+
| [aci](.agents/skills/aci/SKILL.md) | Architecture, commands, testing, troubleshooting |
|
|
28
|
+
| [authoring](.agents/skills/authoring/SKILL.md) | Creating content, building components, design tokens |
|
|
29
|
+
| [migration](.agents/skills/migration/SKILL.md) | Migrating an existing site into ACI |
|
|
30
|
+
|
|
31
|
+
## Commands
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm run dev # Start Astro dev server (auto-compiles content)
|
|
35
|
+
npm run build # Production build
|
|
36
|
+
npm run preview # Preview production build locally
|
|
37
|
+
npm run typecheck # TypeScript type checking
|
|
38
|
+
npm test # Run conformance tests
|
|
39
|
+
npm run content:compile # Compile content into .aci/compiled/
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Project Structure
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
.aci.yaml ACI config (siteId, framework, routes)
|
|
46
|
+
.content/ Content source (JSON pages, config, fragments)
|
|
47
|
+
src/cms/ Contracts and component registry
|
|
48
|
+
src/components/ Runtime Astro components
|
|
49
|
+
src/design-system/ Design tokens and global styles
|
|
50
|
+
src/pages/ Astro file-based routing
|
|
51
|
+
src/layouts/ Page layout wrappers
|
|
52
|
+
.agents/skills/ Skills for humans and agents
|
|
53
|
+
```
|
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
"build": "astro build",
|
|
9
9
|
"preview": "ACI_CONTENT_ROOT=./.aci/compiled astro preview --host 0.0.0.0",
|
|
10
10
|
"typecheck": "ACI_CONTENT_ROOT=./.aci/compiled astro check --tsconfig ./tsconfig.json",
|
|
11
|
-
"
|
|
11
|
+
"test": "node --import tsx --test tests/**/*.test.ts",
|
|
12
12
|
"content:compile": "node ./node_modules/@gradial/aci/bin/aci.js content compile --content ./.content --out ./.aci/compiled",
|
|
13
|
-
"aci:doctor": "node ./node_modules/@gradial/aci/bin/aci.js doctor",
|
|
14
|
-
"aci:validate": "node ./node_modules/@gradial/aci/bin/aci.js doctor",
|
|
15
13
|
"storybook": "storybook dev -p 6006",
|
|
16
14
|
"build:storybook": "storybook build"
|
|
17
15
|
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22",
|
|
18
|
+
"npm": ">=10"
|
|
19
|
+
},
|
|
18
20
|
"dependencies": {
|
|
19
21
|
"@astrojs/node": "^10.1.4",
|
|
20
22
|
"@gradial/aci": "{{aciVersion}}",
|
|
@@ -1,28 +1,5 @@
|
|
|
1
1
|
import { createRegistry } from '@gradial/aci';
|
|
2
|
-
import { homeHeroContract } from './contracts/components/homeHero.contract';
|
|
3
|
-
import { containerContract } from './contracts/components/container.contract';
|
|
4
|
-
import { buttonContract } from './contracts/components/button.contract';
|
|
5
|
-
import { siteNavigationContract } from './contracts/components/siteNavigation.contract';
|
|
6
|
-
import { siteFooterContract } from './contracts/components/siteFooter.contract';
|
|
7
|
-
import { featureGridContract } from './contracts/components/featureGrid.contract';
|
|
8
|
-
import { featureCardContract } from './contracts/components/featureCard.contract';
|
|
9
|
-
import { statsBarContract } from './contracts/components/statsBar.contract';
|
|
10
|
-
import HomeHero from '../components/sections/HomeHero.astro';
|
|
11
|
-
import ContainerSection from '../components/sections/ContainerSection.astro';
|
|
12
|
-
import ButtonBlock from '../components/elements/ButtonBlock.astro';
|
|
13
|
-
import Header from '../components/chrome/Header.astro';
|
|
14
|
-
import Footer from '../components/chrome/Footer.astro';
|
|
15
|
-
import FeatureGrid from '../components/sections/FeatureGrid.astro';
|
|
16
|
-
import FeatureCard from '../components/sections/FeatureCard.astro';
|
|
17
|
-
import StatsBar from '../components/sections/StatsBar.astro';
|
|
18
2
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
[buttonContract, ButtonBlock],
|
|
23
|
-
[siteNavigationContract, Header],
|
|
24
|
-
[siteFooterContract, Footer],
|
|
25
|
-
[featureGridContract, FeatureGrid],
|
|
26
|
-
[featureCardContract, FeatureCard],
|
|
27
|
-
[statsBarContract, StatsBar],
|
|
28
|
-
]);
|
|
3
|
+
// Register your [contract, component] pairs here.
|
|
4
|
+
// See .agents/skills/authoring/SKILL.md for how to add components.
|
|
5
|
+
export const registry = createRegistry([]);
|
|
@@ -1,47 +1,25 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { getGradialStaticPaths,
|
|
3
|
-
import { normalizeRoute, routeMetadataForContent, resolveSlots } from '@gradial/aci/content';
|
|
4
|
-
import { FileContentProvider } from '@gradial/aci/providers/file';
|
|
5
|
-
import type { KernelPage, KernelSiteConfig, LayoutEntry, FragmentData } from '@gradial/aci/content';
|
|
2
|
+
import { getGradialStaticPaths, getPageData, createRenderChildren } from '@gradial/aci/astro';
|
|
6
3
|
import { registry } from '../cms/registry';
|
|
7
4
|
import SiteLayout from '../layouts/SiteLayout.astro';
|
|
8
5
|
|
|
9
6
|
export const getStaticPaths = getGradialStaticPaths;
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
let page: KernelPage;
|
|
15
|
-
let provider: FileContentProvider;
|
|
16
|
-
if (runtimeInput) {
|
|
17
|
-
siteConfig = runtimeInput.siteConfig as KernelSiteConfig;
|
|
18
|
-
page = runtimeInput.page as KernelPage;
|
|
19
|
-
} else {
|
|
20
|
-
provider = new FileContentProvider();
|
|
21
|
-
[siteConfig, page] = await Promise.all([
|
|
22
|
-
provider.getSiteConfig<KernelSiteConfig>(),
|
|
23
|
-
provider.getPage<KernelPage>(route),
|
|
24
|
-
]);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const meta = routeMetadataForContent(siteConfig, page);
|
|
28
|
-
const layout = (runtimeInput?.layouts?.[page.layout] ?? await (provider ?? new FileContentProvider()).getLayout<LayoutEntry>(page.layout)) as LayoutEntry;
|
|
29
|
-
const resolvedSlots = await resolveSlots(
|
|
30
|
-
layout,
|
|
31
|
-
page?.regions || {},
|
|
32
|
-
async (id) => {
|
|
33
|
-
if (runtimeInput?.fragments?.[id]) return runtimeInput.fragments[id] as FragmentData;
|
|
34
|
-
return await (provider ?? new FileContentProvider()).getFragment<FragmentData>(id);
|
|
35
|
-
}
|
|
36
|
-
);
|
|
37
|
-
|
|
8
|
+
// SDK owns route normalization, content loading, metadata, layout/slot
|
|
9
|
+
// resolution, and collect-tag emission. No FileContentProvider imports.
|
|
10
|
+
const data = await getPageData(Astro);
|
|
38
11
|
const renderChildren = createRenderChildren(registry);
|
|
39
|
-
const collectTag = collectScriptTagHTML(siteConfig);
|
|
40
12
|
---
|
|
41
13
|
|
|
42
|
-
<SiteLayout
|
|
43
|
-
{
|
|
44
|
-
|
|
14
|
+
<SiteLayout
|
|
15
|
+
title={data.meta.title}
|
|
16
|
+
description={data.meta.description}
|
|
17
|
+
canonical={data.meta.canonical}
|
|
18
|
+
siteName={data.meta.siteName}
|
|
19
|
+
collectTag={data.collectTag}
|
|
20
|
+
>
|
|
21
|
+
{data.layout.slots.map((slot) => {
|
|
22
|
+
const blocks = data.slots[slot.name];
|
|
45
23
|
if (!blocks?.length) return null;
|
|
46
24
|
const rendered = renderChildren(blocks);
|
|
47
25
|
if (slot.name === 'main') {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACI Conformance Tests
|
|
3
|
+
*
|
|
4
|
+
* Validates that the site conforms to canonical ACI integration patterns.
|
|
5
|
+
* See https://docs.gradial.com/aci/testing for details.
|
|
6
|
+
*/
|
|
7
|
+
import { conformancePreset } from '@gradial/aci/testing';
|
|
8
|
+
|
|
9
|
+
conformancePreset({
|
|
10
|
+
framework: 'astro',
|
|
11
|
+
siteId: '{{siteName}}',
|
|
12
|
+
});
|
|
@@ -33,10 +33,8 @@ Skills live in `.agents/skills/`. Each has a `SKILL.md` entry point and
|
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
35
|
npm install
|
|
36
|
-
npm run dev # Start dev server
|
|
37
|
-
npm run
|
|
38
|
-
npm run aci:compile # Compile contracts
|
|
39
|
-
npm run content:compile # Compile content
|
|
36
|
+
npm run dev # Start dev server (compiles content on startup)
|
|
37
|
+
npm run content:compile # Compile content from .content/ into .aci/
|
|
40
38
|
npm run build # Framework build
|
|
41
|
-
npm
|
|
39
|
+
npm test # Conformance tests
|
|
42
40
|
```
|