@koehler8/cms 1.0.0-beta.13 → 1.0.0-beta.14
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/cms-create-extension.js +7 -0
- package/bin/cms-create-theme.js +7 -0
- package/package.json +4 -2
- package/scripts/create-extension.mjs +11 -0
- package/scripts/create-theme.mjs +11 -0
- package/scripts/scaffold.mjs +206 -0
- package/templates/scaffolds/extension/README.md +53 -0
- package/templates/scaffolds/extension/extension.config.json +10 -0
- package/templates/scaffolds/extension/index.js +37 -0
- package/templates/scaffolds/extension/package.json +22 -0
- package/templates/scaffolds/theme/README.md +50 -0
- package/templates/scaffolds/theme/index.js +8 -0
- package/templates/scaffolds/theme/package.json +21 -0
- package/templates/scaffolds/theme/theme.config.js +228 -0
- package/templates/scaffolds/theme/theme.css +14 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const scriptPath = path.resolve(__dirname, '..', 'scripts', 'create-extension.mjs');
|
|
7
|
+
await import(pathToFileURL(scriptPath).href);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const scriptPath = path.resolve(__dirname, '..', 'scripts', 'create-theme.mjs');
|
|
7
|
+
await import(pathToFileURL(scriptPath).href);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koehler8/cms",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.14",
|
|
4
4
|
"description": "Vertex CMS — lightweight, config-driven Vue 3 framework with theming, extensions, and SSG support",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -47,7 +47,9 @@
|
|
|
47
47
|
"bin": {
|
|
48
48
|
"cms-generate-public-assets": "./bin/cms-generate-public-assets.js",
|
|
49
49
|
"cms-validate-themes": "./bin/cms-validate-themes.js",
|
|
50
|
-
"cms-validate-extensions": "./bin/cms-validate-extensions.js"
|
|
50
|
+
"cms-validate-extensions": "./bin/cms-validate-extensions.js",
|
|
51
|
+
"cms-create-theme": "./bin/cms-create-theme.js",
|
|
52
|
+
"cms-create-extension": "./bin/cms-create-extension.js"
|
|
51
53
|
},
|
|
52
54
|
"files": [
|
|
53
55
|
"src",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { runScaffold } from './scaffold.mjs';
|
|
2
|
+
|
|
3
|
+
const result = await runScaffold({
|
|
4
|
+
kind: 'extension',
|
|
5
|
+
argv: process.argv.slice(2),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
if (result.message) {
|
|
9
|
+
process[result.ok ? 'stdout' : 'stderr'].write(result.message + '\n');
|
|
10
|
+
}
|
|
11
|
+
process.exit(result.ok ? 0 : 1);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { runScaffold } from './scaffold.mjs';
|
|
2
|
+
|
|
3
|
+
const result = await runScaffold({
|
|
4
|
+
kind: 'theme',
|
|
5
|
+
argv: process.argv.slice(2),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
if (result.message) {
|
|
9
|
+
process[result.ok ? 'stdout' : 'stderr'].write(result.message + '\n');
|
|
10
|
+
}
|
|
11
|
+
process.exit(result.ok ? 0 : 1);
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// Shared core for the cms-create-{theme,extension} CLIs. Reads a template
|
|
2
|
+
// directory, substitutes __SLUG__ / __DISPLAY_NAME__ placeholders, writes
|
|
3
|
+
// the result to <cwd>/{themes,extensions}/<slug>/.
|
|
4
|
+
//
|
|
5
|
+
// Intentionally side-effect-free except for the file writes — wiring into
|
|
6
|
+
// the consuming site's package.json / vite.config.js is left to the user
|
|
7
|
+
// via the printed next-steps message. Modifying those files automatically
|
|
8
|
+
// would risk corrupting custom formatting and is harder to reverse than a
|
|
9
|
+
// copy/paste from the terminal.
|
|
10
|
+
|
|
11
|
+
import { mkdir, readdir, readFile, writeFile, access } from 'node:fs/promises';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
// Don't use `new URL('..', import.meta.url)` — vitest's happy-dom env can
|
|
16
|
+
// patch the URL constructor in ways that make fileURLToPath reject the
|
|
17
|
+
// result. Compute the path directly.
|
|
18
|
+
function defaultFrameworkRoot() {
|
|
19
|
+
try {
|
|
20
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const SLUG_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
26
|
+
|
|
27
|
+
function toDisplayName(slug) {
|
|
28
|
+
return slug
|
|
29
|
+
.split('-')
|
|
30
|
+
.map((part) => (part ? part[0].toUpperCase() + part.slice(1) : part))
|
|
31
|
+
.join(' ');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseArgs(argv) {
|
|
35
|
+
const args = { slug: null, out: null, force: false, help: false };
|
|
36
|
+
for (let i = 0; i < argv.length; i++) {
|
|
37
|
+
const a = argv[i];
|
|
38
|
+
if (a === '--help' || a === '-h') args.help = true;
|
|
39
|
+
else if (a === '--force' || a === '-f') args.force = true;
|
|
40
|
+
else if (a === '--out') args.out = argv[++i];
|
|
41
|
+
else if (a.startsWith('--out=')) args.out = a.slice('--out='.length);
|
|
42
|
+
else if (!args.slug && !a.startsWith('-')) args.slug = a;
|
|
43
|
+
}
|
|
44
|
+
return args;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function pathExists(p) {
|
|
48
|
+
try {
|
|
49
|
+
await access(p);
|
|
50
|
+
return true;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function collectTemplateFiles(dir) {
|
|
57
|
+
const out = [];
|
|
58
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (entry.name.startsWith('.')) continue;
|
|
61
|
+
const full = path.join(dir, entry.name);
|
|
62
|
+
if (entry.isDirectory()) {
|
|
63
|
+
out.push(...(await collectTemplateFiles(full)));
|
|
64
|
+
} else {
|
|
65
|
+
out.push(full);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function applySubstitutions(content, subs) {
|
|
72
|
+
let result = content;
|
|
73
|
+
for (const [from, to] of Object.entries(subs)) {
|
|
74
|
+
result = result.split(from).join(to);
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Run the scaffold for one of {theme, extension}.
|
|
81
|
+
*
|
|
82
|
+
* @param {object} opts
|
|
83
|
+
* @param {'theme'|'extension'} opts.kind
|
|
84
|
+
* @param {string[]} opts.argv argv slice excluding `node` + script path
|
|
85
|
+
* @param {string} opts.cwd working directory (defaults to process.cwd())
|
|
86
|
+
* @returns {Promise<{ok: boolean, slug?: string, targetDir?: string, message?: string}>}
|
|
87
|
+
*/
|
|
88
|
+
export async function runScaffold({ kind, argv, cwd = process.cwd(), frameworkRoot } = {}) {
|
|
89
|
+
if (kind !== 'theme' && kind !== 'extension') {
|
|
90
|
+
return { ok: false, message: `unknown kind: ${kind}` };
|
|
91
|
+
}
|
|
92
|
+
const root = frameworkRoot || defaultFrameworkRoot();
|
|
93
|
+
if (!root) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
message: 'error: could not resolve @koehler8/cms framework root; pass `frameworkRoot` explicitly',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const args = parseArgs(argv);
|
|
100
|
+
|
|
101
|
+
if (args.help) {
|
|
102
|
+
return { ok: true, message: usage(kind) };
|
|
103
|
+
}
|
|
104
|
+
if (!args.slug) {
|
|
105
|
+
return { ok: false, message: `error: missing <slug>\n${usage(kind)}` };
|
|
106
|
+
}
|
|
107
|
+
if (!SLUG_PATTERN.test(args.slug)) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
message: `error: slug "${args.slug}" must be lowercase kebab-case (e.g. "coastal", "lush-canopy")`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const slug = args.slug;
|
|
115
|
+
const displayName = toDisplayName(slug);
|
|
116
|
+
const subs = { __SLUG__: slug, __DISPLAY_NAME__: displayName };
|
|
117
|
+
const templateDir = path.join(root, 'templates', 'scaffolds', kind);
|
|
118
|
+
const defaultDirName = kind === 'theme' ? 'themes' : 'extensions';
|
|
119
|
+
const targetDir = args.out
|
|
120
|
+
? path.resolve(cwd, args.out)
|
|
121
|
+
: path.join(cwd, defaultDirName, slug);
|
|
122
|
+
|
|
123
|
+
if (!(await pathExists(templateDir))) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
message: `error: scaffold templates missing at ${templateDir}. Reinstall @koehler8/cms?`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if ((await pathExists(targetDir)) && !args.force) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
message: `error: target directory already exists: ${targetDir}\n(use --force to overwrite)`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const files = await collectTemplateFiles(templateDir);
|
|
137
|
+
await mkdir(targetDir, { recursive: true });
|
|
138
|
+
for (const file of files) {
|
|
139
|
+
const rel = path.relative(templateDir, file);
|
|
140
|
+
const outPath = path.join(targetDir, rel);
|
|
141
|
+
await mkdir(path.dirname(outPath), { recursive: true });
|
|
142
|
+
const raw = await readFile(file, 'utf8');
|
|
143
|
+
await writeFile(outPath, applySubstitutions(raw, subs));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const targetRel = path.relative(cwd, targetDir) || targetDir;
|
|
147
|
+
return {
|
|
148
|
+
ok: true,
|
|
149
|
+
slug,
|
|
150
|
+
targetDir,
|
|
151
|
+
message: `${successHeader(kind, slug, targetRel)}\n${nextSteps(kind, slug, targetRel)}`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function successHeader(kind, slug, targetRel) {
|
|
156
|
+
const fileCount = kind === 'theme' ? 5 : 4;
|
|
157
|
+
return `✓ Scaffolded ${kind} "${slug}" → ${targetRel}/ (${fileCount} files)`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function nextSteps(kind, slug, targetRel) {
|
|
161
|
+
if (kind === 'theme') {
|
|
162
|
+
return [
|
|
163
|
+
'',
|
|
164
|
+
'Next steps:',
|
|
165
|
+
' 1. Add to your site\'s package.json dependencies:',
|
|
166
|
+
` "cms-theme-${slug}": "file:./${targetRel}"`,
|
|
167
|
+
' 2. Run `npm install` to symlink it into node_modules.',
|
|
168
|
+
' 3. Add to vite.config.js\'s themes array:',
|
|
169
|
+
` themes: ['cms-theme-${slug}'],`,
|
|
170
|
+
' 4. Activate it in site/content/{baseLocale}/site.json:',
|
|
171
|
+
` "theme": "${slug}"`,
|
|
172
|
+
` 5. Edit ${targetRel}/theme.config.js to define your palette.`,
|
|
173
|
+
'',
|
|
174
|
+
].join('\n');
|
|
175
|
+
}
|
|
176
|
+
return [
|
|
177
|
+
'',
|
|
178
|
+
'Next steps:',
|
|
179
|
+
' 1. Add to your site\'s package.json dependencies:',
|
|
180
|
+
` "cms-ext-${slug}": "file:./${targetRel}"`,
|
|
181
|
+
' 2. Run `npm install` to symlink it into node_modules.',
|
|
182
|
+
' 3. Add to vite.config.js\'s extensions array:',
|
|
183
|
+
` extensions: ['cms-ext-${slug}'],`,
|
|
184
|
+
` 4. Add components to ${targetRel}/components/ as needed (see ${targetRel}/README.md).`,
|
|
185
|
+
` Or, for one-off site-only components, prefer site/components/ instead.`,
|
|
186
|
+
'',
|
|
187
|
+
].join('\n');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function usage(kind) {
|
|
191
|
+
const cmd = kind === 'theme' ? 'cms-create-theme' : 'cms-create-extension';
|
|
192
|
+
const dir = kind === 'theme' ? 'themes' : 'extensions';
|
|
193
|
+
return `
|
|
194
|
+
Usage: npx ${cmd} <slug> [--out <directory>] [--force]
|
|
195
|
+
|
|
196
|
+
Scaffolds a new @koehler8/cms ${kind} into the current site.
|
|
197
|
+
|
|
198
|
+
Arguments:
|
|
199
|
+
<slug> Lowercase kebab-case slug (e.g. "coastal", "lush-canopy").
|
|
200
|
+
|
|
201
|
+
Options:
|
|
202
|
+
--out <dir> Where to create the ${kind}. Defaults to ./${dir}/<slug>/.
|
|
203
|
+
--force Overwrite an existing target directory.
|
|
204
|
+
-h, --help Show this message.
|
|
205
|
+
`;
|
|
206
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# cms-ext-__SLUG__
|
|
2
|
+
|
|
3
|
+
__DISPLAY_NAME__ extension for [`@koehler8/cms`](https://github.com/koehler8/cms).
|
|
4
|
+
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
Starter scaffold. Currently empty (`components: []`). Add components as the design takes shape.
|
|
8
|
+
|
|
9
|
+
## Activate
|
|
10
|
+
|
|
11
|
+
After scaffolding (`npx cms-create-extension __SLUG__`), wire it into your site:
|
|
12
|
+
|
|
13
|
+
```jsonc
|
|
14
|
+
// package.json
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"cms-ext-__SLUG__": "file:./extensions/__SLUG__"
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
// vite.config.js
|
|
22
|
+
plugins: cms({
|
|
23
|
+
siteDir: './site',
|
|
24
|
+
extensions: ['cms-ext-__SLUG__'],
|
|
25
|
+
}),
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Run `npm install`, then build/preview.
|
|
29
|
+
|
|
30
|
+
## Add a component
|
|
31
|
+
|
|
32
|
+
1. Create `components/Foo.vue`. Follow the standard CMS pattern:
|
|
33
|
+
- Read content via `inject('pageContent')`
|
|
34
|
+
- Use `<style scoped>`
|
|
35
|
+
- Consume theme tokens via `var(--brand-*)`
|
|
36
|
+
2. Add an entry to [extension.config.json](extension.config.json) `components[]` — `name`, `label`, `description`, `module`, `configKey`.
|
|
37
|
+
3. Add a lazy dynamic import to [index.js](index.js)'s `components` map keyed by the same module path.
|
|
38
|
+
4. Reference the component by name in any page's `components[]` array (e.g. `"components[N]": "Foo"`).
|
|
39
|
+
5. Put translations under `content.{configKey}.*` in `pages/{pageId}.json`.
|
|
40
|
+
|
|
41
|
+
## When to put a component here vs `site/components/`
|
|
42
|
+
|
|
43
|
+
- **`site/components/`** — one-off components specific to a single site. Auto-globbed; no extension wrapper needed. **Default.**
|
|
44
|
+
- **This extension** — components destined to be shared across multiple sites (e.g. eventually published to npm).
|
|
45
|
+
|
|
46
|
+
## Extracting to a standalone repo
|
|
47
|
+
|
|
48
|
+
Once the extension carries enough to be worth sharing:
|
|
49
|
+
|
|
50
|
+
1. Copy `extensions/__SLUG__/` to a new repo named `cms-ext-__SLUG__/`
|
|
51
|
+
2. Rename `name` in `package.json` to `@koehler8/cms-ext-__SLUG__` and add the standard repo metadata (homepage, bugs, repository) following the pattern of `@koehler8/cms-ext-compliance`
|
|
52
|
+
3. Publish to GitHub Packages / npm
|
|
53
|
+
4. Replace the `file:` dependency in each consuming site with the published version
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import manifest from './extension.config.json' with { type: 'json' };
|
|
2
|
+
|
|
3
|
+
// Components contributed by this extension (use lazy dynamic imports so
|
|
4
|
+
// Vite splits each into its own chunk). Key by the same module path
|
|
5
|
+
// declared in extension.config.json's components[].module.
|
|
6
|
+
//
|
|
7
|
+
// To add a new component:
|
|
8
|
+
// 1. Create components/Foo.vue.
|
|
9
|
+
// 2. Add a {name, label, description, module, configKey} entry to
|
|
10
|
+
// extension.config.json's components[] array.
|
|
11
|
+
// 3. Add a `'./components/Foo.vue': () => import('./components/Foo.vue')`
|
|
12
|
+
// entry to the map below.
|
|
13
|
+
const components = {};
|
|
14
|
+
|
|
15
|
+
// Optional setup() hook — runs once during app initialization with
|
|
16
|
+
// { app, router, pinia, siteData, isClient }. Use it for runtime
|
|
17
|
+
// concerns the manifest doesn't cover (e.g. mounting a banner Vue app
|
|
18
|
+
// to document.body, registering a Pinia store, etc.). Remove if unused.
|
|
19
|
+
//
|
|
20
|
+
// async function setup({ app, router, pinia, siteData, isClient }) {
|
|
21
|
+
// // ...
|
|
22
|
+
// }
|
|
23
|
+
|
|
24
|
+
// Optional contentDefaults — a frozen object keyed by `configKey` that
|
|
25
|
+
// the resolver merges underneath each component's content. Use it to
|
|
26
|
+
// ship sensible defaults so consumer sites only override what they
|
|
27
|
+
// want to change. Remove if unused.
|
|
28
|
+
//
|
|
29
|
+
// import contentDefaults from './content.defaults.json' with { type: 'json' };
|
|
30
|
+
|
|
31
|
+
export default {
|
|
32
|
+
manifest,
|
|
33
|
+
components,
|
|
34
|
+
// setup,
|
|
35
|
+
// contentDefaults,
|
|
36
|
+
};
|
|
37
|
+
export { manifest };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cms-ext-__SLUG__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "__DISPLAY_NAME__ extension for @koehler8/cms.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.js",
|
|
10
|
+
"./config": "./extension.config.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"index.js",
|
|
14
|
+
"extension.config.json",
|
|
15
|
+
"components",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@koehler8/cms": "^1.0.0-beta.1"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# cms-theme-__SLUG__
|
|
2
|
+
|
|
3
|
+
__DISPLAY_NAME__ theme for [`@koehler8/cms`](https://github.com/koehler8/cms).
|
|
4
|
+
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
Starter scaffold. Edit [theme.config.js](theme.config.js) to define your palette, typography, surfaces, etc. The bland slate-and-sand defaults are placeholders.
|
|
8
|
+
|
|
9
|
+
## Activate
|
|
10
|
+
|
|
11
|
+
After scaffolding (`npx cms-create-theme __SLUG__`), wire it into your site:
|
|
12
|
+
|
|
13
|
+
```jsonc
|
|
14
|
+
// package.json
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"cms-theme-__SLUG__": "file:./themes/__SLUG__"
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
// vite.config.js
|
|
22
|
+
plugins: cms({
|
|
23
|
+
siteDir: './site',
|
|
24
|
+
themes: ['cms-theme-__SLUG__'],
|
|
25
|
+
}),
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```jsonc
|
|
29
|
+
// site/content/{baseLocale}/site.json
|
|
30
|
+
{
|
|
31
|
+
"theme": "__SLUG__"
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Run `npm install`, then build/preview to see your theme applied.
|
|
36
|
+
|
|
37
|
+
## Edit
|
|
38
|
+
|
|
39
|
+
All theming happens in [theme.config.js](theme.config.js). The CMS turns each `tokens.*` block into `--brand-*` / `--theme-*` / `--ui-*` CSS variables under `:root[data-site-theme="__SLUG__"]`, bundled into a sync stylesheet so they apply during the initial paint.
|
|
40
|
+
|
|
41
|
+
[theme.css](theme.css) is for supplemental rules the manifest doesn't cover (font `@import`s, brand-color aliases, etc.).
|
|
42
|
+
|
|
43
|
+
## Extracting to a standalone repo
|
|
44
|
+
|
|
45
|
+
Once the theme stabilises:
|
|
46
|
+
|
|
47
|
+
1. Copy `themes/__SLUG__/` to a new repo named `cms-theme-__SLUG__/`
|
|
48
|
+
2. Rename the `name` in `package.json` to `@koehler8/cms-theme-__SLUG__` and add the standard repo metadata (homepage, bugs, repository) following the pattern of `@koehler8/cms-theme-neon`
|
|
49
|
+
3. Publish to GitHub Packages / npm
|
|
50
|
+
4. Replace the `file:` dependency in the consuming site with the published version
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import manifest from './theme.config.js';
|
|
2
|
+
// Side-effect import (not `?inline`): @koehler8/cms >= 1.0.0-beta.8 bundles
|
|
3
|
+
// theme.css as a sync <link rel="stylesheet">, applied during the initial
|
|
4
|
+
// paint instead of injected via JS after Vue hydrates.
|
|
5
|
+
import './theme.css';
|
|
6
|
+
|
|
7
|
+
export default { manifest };
|
|
8
|
+
export { manifest };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cms-theme-__SLUG__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "__DISPLAY_NAME__ theme for @koehler8/cms.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./config": "./theme.config.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"index.js",
|
|
13
|
+
"theme.config.js",
|
|
14
|
+
"theme.css",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@koehler8/cms": "^1.0.0-beta.1"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// __DISPLAY_NAME__ theme manifest. Defines the design tokens the CMS turns
|
|
2
|
+
// into `--brand-*`, `--theme-*`, and `--ui-*` CSS variables under
|
|
3
|
+
// `:root[data-site-theme="__SLUG__"]`. CMS components consume these via
|
|
4
|
+
// `var(--brand-*)`, so editing the values here cascades automatically.
|
|
5
|
+
//
|
|
6
|
+
// All token blocks below are required by the validator. The default palette
|
|
7
|
+
// is intentionally bland (cool slate + warm sand accent) so it's obvious
|
|
8
|
+
// what to change first. See @koehler8/cms-theme-neon, -frog, -aurora,
|
|
9
|
+
// -southpark, -swamp for production-quality references.
|
|
10
|
+
|
|
11
|
+
const slate900 = '#0f172a';
|
|
12
|
+
const slate800 = '#1e293b';
|
|
13
|
+
const slate700 = '#334155';
|
|
14
|
+
const slate200 = '#e2e8f0';
|
|
15
|
+
const slate100 = '#f1f5f9';
|
|
16
|
+
const sand = '#f4a261';
|
|
17
|
+
const sandDeep = '#d97e3a';
|
|
18
|
+
const ink = '#0b1220';
|
|
19
|
+
const cream = '#f7f3ec';
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
slug: '__SLUG__',
|
|
23
|
+
meta: {
|
|
24
|
+
name: '__DISPLAY_NAME__',
|
|
25
|
+
version: '0.1.0',
|
|
26
|
+
},
|
|
27
|
+
tokens: {
|
|
28
|
+
palette: {
|
|
29
|
+
primary: slate800,
|
|
30
|
+
primaryAccent: slate900,
|
|
31
|
+
secondary: slate700,
|
|
32
|
+
accent: sand,
|
|
33
|
+
accentSoft: 'rgba(244, 162, 97, 0.18)',
|
|
34
|
+
neutral: ink,
|
|
35
|
+
neutralStrong: cream,
|
|
36
|
+
neutralSoft: slate200,
|
|
37
|
+
inverse: '#ffffff',
|
|
38
|
+
success: '#2a9d8f',
|
|
39
|
+
info: slate700,
|
|
40
|
+
warning: sand,
|
|
41
|
+
critical: '#d62828',
|
|
42
|
+
criticalSoft: 'rgba(214, 40, 40, 0.18)',
|
|
43
|
+
},
|
|
44
|
+
text: {
|
|
45
|
+
primary: cream,
|
|
46
|
+
muted: slate200,
|
|
47
|
+
mutedStrong: slate100,
|
|
48
|
+
inverse: ink,
|
|
49
|
+
accent: sand,
|
|
50
|
+
onAccent: '#ffffff',
|
|
51
|
+
},
|
|
52
|
+
surfaces: {
|
|
53
|
+
base: ink,
|
|
54
|
+
baseAlt: slate900,
|
|
55
|
+
raised: slate800,
|
|
56
|
+
sunken: '#070b14',
|
|
57
|
+
callout: 'rgba(244, 162, 97, 0.12)',
|
|
58
|
+
card: 'rgba(30, 41, 59, 0.92)',
|
|
59
|
+
cardAlt: 'rgba(51, 65, 85, 0.92)',
|
|
60
|
+
overlay: 'rgba(11, 18, 32, 0.96)',
|
|
61
|
+
backdrop: 'rgba(11, 18, 32, 0.78)',
|
|
62
|
+
border: 'rgba(226, 232, 240, 0.18)',
|
|
63
|
+
divider: 'rgba(226, 232, 240, 0.12)',
|
|
64
|
+
input: 'rgba(30, 41, 59, 0.96)',
|
|
65
|
+
chip: 'rgba(244, 162, 97, 0.22)',
|
|
66
|
+
chipAccent: sand,
|
|
67
|
+
helper: {
|
|
68
|
+
background: 'rgba(226, 232, 240, 0.08)',
|
|
69
|
+
hover: 'rgba(226, 232, 240, 0.16)',
|
|
70
|
+
text: cream,
|
|
71
|
+
border: 'rgba(226, 232, 240, 0.2)',
|
|
72
|
+
heading: cream,
|
|
73
|
+
body: 'rgba(247, 243, 236, 0.85)',
|
|
74
|
+
},
|
|
75
|
+
tabs: {
|
|
76
|
+
background: 'rgba(30, 41, 59, 0.92)',
|
|
77
|
+
border: 'rgba(226, 232, 240, 0.16)',
|
|
78
|
+
shadow: '0 18px 34px rgba(11, 18, 32, 0.45)',
|
|
79
|
+
tabColor: 'rgba(247, 243, 236, 0.88)',
|
|
80
|
+
activeBackground: 'rgba(244, 162, 97, 0.18)',
|
|
81
|
+
activeColor: '#ffffff',
|
|
82
|
+
activeShadow: '0 18px 40px rgba(11, 18, 32, 0.55), inset 0 1px 0 rgba(255,255,255,0.16)',
|
|
83
|
+
stepBackground: 'rgba(255, 255, 255, 0.06)',
|
|
84
|
+
stepBorder: 'rgba(255, 255, 255, 0.18)',
|
|
85
|
+
stepColor: 'rgba(255, 255, 255, 0.72)',
|
|
86
|
+
activeStepBackground: 'rgba(244, 162, 97, 0.24)',
|
|
87
|
+
activeStepBorder: 'rgba(244, 162, 97, 0.5)',
|
|
88
|
+
activeStepColor: sand,
|
|
89
|
+
},
|
|
90
|
+
field: {
|
|
91
|
+
background: 'rgba(30, 41, 59, 0.96)',
|
|
92
|
+
border: 'rgba(226, 232, 240, 0.16)',
|
|
93
|
+
shadow: '0 8px 24px rgba(11, 18, 32, 0.32)',
|
|
94
|
+
addonBackground: 'rgba(11, 18, 32, 0.92)',
|
|
95
|
+
addonBorder: 'rgba(226, 232, 240, 0.16)',
|
|
96
|
+
addonColor: cream,
|
|
97
|
+
inputColor: cream,
|
|
98
|
+
inputPlaceholder: 'rgba(241, 245, 249, 0.55)',
|
|
99
|
+
},
|
|
100
|
+
strip: {
|
|
101
|
+
background: 'rgba(11, 18, 32, 0.9)',
|
|
102
|
+
border: 'rgba(226, 232, 240, 0.16)',
|
|
103
|
+
text: cream,
|
|
104
|
+
},
|
|
105
|
+
chrome: {
|
|
106
|
+
background: 'rgba(11, 18, 32, 0.94)',
|
|
107
|
+
text: cream,
|
|
108
|
+
shadow: '0 18px 45px rgba(2, 5, 12, 0.55)',
|
|
109
|
+
compactShadow: '0 12px 32px rgba(2, 5, 12, 0.65)',
|
|
110
|
+
},
|
|
111
|
+
backdropPrimary: {
|
|
112
|
+
background: 'linear-gradient(150deg, rgba(11,18,32,0.98), rgba(15,23,42,0.92))',
|
|
113
|
+
before: 'radial-gradient(circle, rgba(226,232,240,0.3), rgba(0,0,0,0))',
|
|
114
|
+
after: 'radial-gradient(circle, rgba(244,162,97,0.5), rgba(0,0,0,0))',
|
|
115
|
+
},
|
|
116
|
+
backdropSecondary: {
|
|
117
|
+
background: 'linear-gradient(140deg, rgba(11,18,32,0.97), rgba(15,23,42,0.92))',
|
|
118
|
+
before: 'radial-gradient(circle, rgba(244,162,97,0.45), rgba(0,0,0,0))',
|
|
119
|
+
after: 'radial-gradient(circle, rgba(226,232,240,0.4), rgba(0,0,0,0))',
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
typography: {
|
|
123
|
+
bodyFamily: '"Inter", "Helvetica Neue", -apple-system, BlinkMacSystemFont, system-ui, sans-serif',
|
|
124
|
+
headingFamily: '"Inter", "Helvetica Neue", -apple-system, BlinkMacSystemFont, system-ui, sans-serif',
|
|
125
|
+
monoFamily: '"JetBrains Mono", "SF Mono", Menlo, Consolas, monospace',
|
|
126
|
+
baseSize: '16px',
|
|
127
|
+
scale: 1.125,
|
|
128
|
+
weightRegular: 400,
|
|
129
|
+
weightMedium: 500,
|
|
130
|
+
weightBold: 600,
|
|
131
|
+
letterSpacingTight: '-0.015em',
|
|
132
|
+
letterSpacingWide: '0.08em',
|
|
133
|
+
},
|
|
134
|
+
ctas: {
|
|
135
|
+
primary: {
|
|
136
|
+
bg: `linear-gradient(135deg, ${sand} 0%, ${sandDeep} 100%)`,
|
|
137
|
+
text: '#ffffff',
|
|
138
|
+
border: '1px solid rgba(255,255,255,0.18)',
|
|
139
|
+
hoverBg: `linear-gradient(135deg, ${sandDeep} 0%, ${sand} 100%)`,
|
|
140
|
+
hoverBorder: '1px solid rgba(255,255,255,0.24)',
|
|
141
|
+
shadow: '0 18px 35px rgba(11, 18, 32, 0.45)',
|
|
142
|
+
},
|
|
143
|
+
secondary: {
|
|
144
|
+
bg: 'rgba(30, 41, 59, 0.92)',
|
|
145
|
+
text: cream,
|
|
146
|
+
border: 'rgba(226, 232, 240, 0.2)',
|
|
147
|
+
hoverBg: 'rgba(226, 232, 240, 0.08)',
|
|
148
|
+
hoverBorder: 'rgba(226, 232, 240, 0.32)',
|
|
149
|
+
shadow: '0 8px 24px rgba(11, 18, 32, 0.32)',
|
|
150
|
+
},
|
|
151
|
+
ghost: {
|
|
152
|
+
bg: 'transparent',
|
|
153
|
+
text: cream,
|
|
154
|
+
border: 'rgba(226, 232, 240, 0.2)',
|
|
155
|
+
hoverBg: 'rgba(226, 232, 240, 0.08)',
|
|
156
|
+
hoverBorder: 'rgba(226, 232, 240, 0.32)',
|
|
157
|
+
shadow: 'none',
|
|
158
|
+
},
|
|
159
|
+
link: {
|
|
160
|
+
text: sand,
|
|
161
|
+
hoverText: sandDeep,
|
|
162
|
+
underline: 'rgba(244, 162, 97, 0.45)',
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
chips: {
|
|
166
|
+
neutral: {
|
|
167
|
+
bg: 'rgba(226, 232, 240, 0.12)',
|
|
168
|
+
text: cream,
|
|
169
|
+
border: 'rgba(226, 232, 240, 0.2)',
|
|
170
|
+
},
|
|
171
|
+
accent: {
|
|
172
|
+
bg: 'rgba(244, 162, 97, 0.22)',
|
|
173
|
+
text: '#ffffff',
|
|
174
|
+
border: 'rgba(244, 162, 97, 0.45)',
|
|
175
|
+
},
|
|
176
|
+
outline: {
|
|
177
|
+
bg: 'transparent',
|
|
178
|
+
text: sand,
|
|
179
|
+
border: 'rgba(244, 162, 97, 0.45)',
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
focus: {
|
|
183
|
+
ring: '0 0 0 2px rgba(226, 232, 240, 0.45)',
|
|
184
|
+
ringMuted: '0 0 0 1px rgba(226, 232, 240, 0.32)',
|
|
185
|
+
outline: 'rgba(226, 232, 240, 0.65)',
|
|
186
|
+
shadowInset: '0 0 22px rgba(226, 232, 240, 0.32)',
|
|
187
|
+
},
|
|
188
|
+
radii: {
|
|
189
|
+
sm: '6px',
|
|
190
|
+
md: '12px',
|
|
191
|
+
lg: '20px',
|
|
192
|
+
pill: '999px',
|
|
193
|
+
full: '50%',
|
|
194
|
+
},
|
|
195
|
+
elevation: {
|
|
196
|
+
flat: '0 8px 24px rgba(11, 18, 32, 0.18)',
|
|
197
|
+
raised: '0 18px 38px rgba(11, 18, 32, 0.32)',
|
|
198
|
+
overlay: '0 24px 56px rgba(11, 18, 32, 0.5)',
|
|
199
|
+
},
|
|
200
|
+
utility: {
|
|
201
|
+
divider: 'rgba(226, 232, 240, 0.16)',
|
|
202
|
+
inputBorder: 'rgba(226, 232, 240, 0.16)',
|
|
203
|
+
inputText: cream,
|
|
204
|
+
inputPlaceholder: 'rgba(241, 245, 249, 0.55)',
|
|
205
|
+
selectionBg: 'rgba(244, 162, 97, 0.32)',
|
|
206
|
+
selectionText: '#ffffff',
|
|
207
|
+
gradientHero: 'linear-gradient(150deg, rgba(11,18,32,0.98), rgba(15,23,42,0.92))',
|
|
208
|
+
gradientPromo: 'linear-gradient(140deg, rgba(11,18,32,0.97), rgba(15,23,42,0.92))',
|
|
209
|
+
bodyBackground: [
|
|
210
|
+
'linear-gradient(155deg, rgba(11,18,32,0.98), rgba(15,23,42,0.92))',
|
|
211
|
+
],
|
|
212
|
+
modalSurface: 'rgba(30, 41, 59, 0.96)',
|
|
213
|
+
modalBorder: 'rgba(226, 232, 240, 0.16)',
|
|
214
|
+
modalShadow: '0 24px 56px rgba(11, 18, 32, 0.5)',
|
|
215
|
+
modalRadius: '20px',
|
|
216
|
+
chartTrack: 'rgba(30, 41, 59, 0.85)',
|
|
217
|
+
chartCenterText: cream,
|
|
218
|
+
statusHeadline: {
|
|
219
|
+
background: 'linear-gradient(120deg, rgba(226,232,240,0.12), rgba(244,162,97,0.18))',
|
|
220
|
+
color: sand,
|
|
221
|
+
shadow: '0 12px 24px rgba(11, 18, 32, 0.32), inset 0 1px 0 rgba(255,255,255,0.18)',
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
assets: {
|
|
226
|
+
css: './theme.css',
|
|
227
|
+
},
|
|
228
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* __DISPLAY_NAME__ theme — supplemental rules and brand-specific aliases.
|
|
2
|
+
*
|
|
3
|
+
* Most theming happens via the tokens object in theme.config.js — the CMS
|
|
4
|
+
* derives `:root[data-site-theme="__SLUG__"] { --brand-*, --theme-* }` from
|
|
5
|
+
* the manifest and bundles it into `virtual:cms-theme-vars.css` (loaded as
|
|
6
|
+
* a sync <link rel="stylesheet">). This file is for things the manifest
|
|
7
|
+
* doesn't cover: font @imports, brand-color aliases, transitions, etc.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
:root[data-site-theme="__SLUG__"] {
|
|
11
|
+
/* Brand color aliases (also exposed by the manifest, mirrored here for
|
|
12
|
+
parity with the existing @koehler8/cms-theme-* packages). */
|
|
13
|
+
/* --brand-foo: #XXXXXX; */
|
|
14
|
+
}
|