Skip to content

Commit 0ee5bbc

Browse files
authored
Merge pull request #71 from tokenhost/issue-62/pr-09-cyber-grid-default-theme
[Issue 62 9/9] Make cyber-grid the explicit default theme
2 parents c8ad9c1 + 76117d1 commit 0ee5bbc

8 files changed

Lines changed: 141 additions & 7 deletions

File tree

apps/example/microblog.schema.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
"app": {
55
"name": "Microblog",
66
"slug": "microblog",
7+
"theme": {
8+
"preset": "cyber-grid"
9+
},
710
"features": {
811
"uploads": true,
912
"onChainIndexing": true

packages/cli/src/index.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,9 @@ type SharedThemeTokens = {
335335
motion: { fast: string; base: string };
336336
};
337337

338+
const DEFAULT_THEME_PRESET = 'cyber-grid';
339+
type SharedThemePreset = typeof DEFAULT_THEME_PRESET;
340+
338341
function defaultSharedThemeTokens(): SharedThemeTokens {
339342
return {
340343
colors: {
@@ -362,7 +365,16 @@ function defaultSharedThemeTokens(): SharedThemeTokens {
362365
};
363366
}
364367

365-
function loadSharedThemeTokens(): SharedThemeTokens {
368+
function resolveSharedThemePreset(theme: Record<string, unknown> | undefined | null): SharedThemePreset {
369+
const preset = String(theme?.preset ?? DEFAULT_THEME_PRESET).trim();
370+
if (preset !== DEFAULT_THEME_PRESET) {
371+
throw new Error(`Unsupported theme preset "${preset}". Supported presets: ${DEFAULT_THEME_PRESET}.`);
372+
}
373+
return DEFAULT_THEME_PRESET;
374+
}
375+
376+
function loadSharedThemeTokensForPreset(preset: SharedThemePreset): SharedThemeTokens {
377+
if (preset !== DEFAULT_THEME_PRESET) return defaultSharedThemeTokens();
366378
try {
367379
const templateDir = resolveNextExportUiTemplateDir();
368380
const tokenPath = path.join(templateDir, 'src', 'theme', 'tokens.json');
@@ -373,6 +385,14 @@ function loadSharedThemeTokens(): SharedThemeTokens {
373385
}
374386
}
375387

388+
function materializeUiThemePreset(uiDir: string, schema: ThsSchema) {
389+
const preset = resolveSharedThemePreset((schema.app.theme as Record<string, unknown> | undefined) ?? undefined);
390+
const tokens = loadSharedThemeTokensForPreset(preset);
391+
const tokenPath = path.join(uiDir, 'src', 'theme', 'tokens.json');
392+
ensureDir(path.dirname(tokenPath));
393+
fs.writeFileSync(tokenPath, JSON.stringify(tokens, null, 2) + '\n');
394+
}
395+
376396
function renderStudioThemeCssVars(tokens: SharedThemeTokens): string {
377397
return [
378398
`--th-bg:${tokens.colors.bg}`,
@@ -454,7 +474,7 @@ function loadStudioBackgroundPngBuffer(): Buffer | null {
454474

455475
function renderStudioHtml(): string {
456476
// Keep this local-first and dependency-free for fast startup in any repo clone.
457-
const themeTokens = loadSharedThemeTokens();
477+
const themeTokens = loadSharedThemeTokensForPreset(DEFAULT_THEME_PRESET);
458478
const cssVars = renderStudioThemeCssVars(themeTokens);
459479
const studioWordmarkSvg = loadStudioWordmarkSvg();
460480
return `<!doctype html>
@@ -1214,6 +1234,7 @@ function syncUiOutput(args: {
12141234
const thsTsPath = path.join(uiDir, 'src', 'generated', 'ths.ts');
12151235
ensureDir(path.dirname(thsTsPath));
12161236
fs.writeFileSync(thsTsPath, renderThsTs(args.schema));
1237+
materializeUiThemePreset(uiDir, args.schema);
12171238
materializeCollectionRoutes(uiDir, args.schema);
12181239

12191240
const compiledPublicPath = path.join(uiDir, 'public', 'compiled', 'App.json');
@@ -2962,6 +2983,7 @@ function buildFromSchema(
29622983
const thsTsPath = path.join(uiWorkDir, 'src', 'generated', 'ths.ts');
29632984
ensureDir(path.dirname(thsTsPath));
29642985
fs.writeFileSync(thsTsPath, renderThsTs(schema));
2986+
materializeUiThemePreset(uiWorkDir, schema);
29652987
materializeCollectionRoutes(uiWorkDir, schema);
29662988

29672989
// Ship ABI alongside the UI so it can operate without additional servers.

packages/schema/schemas/tokenhost-ths.schema.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,14 @@
3131
"description": { "type": "string" },
3232
"theme": {
3333
"type": "object",
34-
"description": "Theme tokens (implementation-defined).",
35-
"additionalProperties": true
34+
"description": "Theme preset selection and future theme options.",
35+
"additionalProperties": true,
36+
"properties": {
37+
"preset": {
38+
"type": "string",
39+
"enum": ["cyber-grid"]
40+
}
41+
}
3642
},
3743
"ui": {
3844
"type": "object",

packages/schema/src/lint.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@ function isSafeAutoExpr(expr: string): boolean {
7070

7171
export function lintThs(schema: ThsSchema): Issue[] {
7272
const issues: Issue[] = [];
73+
const themePreset = String(schema.app.theme?.preset ?? '').trim();
74+
75+
if (themePreset && themePreset !== 'cyber-grid') {
76+
issues.push(
77+
err('/app/theme/preset', 'lint.app.theme.unknown_preset', `Unknown theme preset "${themePreset}". Supported presets: cyber-grid.`)
78+
);
79+
}
7380

7481
if (schema.app.ui?.homePage?.mode === 'custom' && !String(schema.app.ui?.extensions?.directory ?? '').trim()) {
7582
issues.push(

packages/schema/src/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,18 @@ export interface ThsAppUi {
2222
extensions?: ThsAppUiExtensions;
2323
}
2424

25+
export type ThsThemePreset = 'cyber-grid';
26+
27+
export interface ThsAppTheme {
28+
preset?: ThsThemePreset;
29+
[key: string]: unknown;
30+
}
31+
2532
export interface ThsApp {
2633
name: string;
2734
slug: string;
2835
description?: string;
29-
theme?: Record<string, unknown>;
36+
theme?: ThsAppTheme;
3037
features?: ThsAppFeatures;
3138
ui?: ThsAppUi;
3239
}

schemas/tokenhost-ths.schema.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,14 @@
3131
"description": { "type": "string" },
3232
"theme": {
3333
"type": "object",
34-
"description": "Theme tokens (implementation-defined).",
35-
"additionalProperties": true
34+
"description": "Theme preset selection and future theme options.",
35+
"additionalProperties": true,
36+
"properties": {
37+
"preset": {
38+
"type": "string",
39+
"enum": ["cyber-grid"]
40+
}
41+
}
3642
},
3743
"features": {
3844
"type": "object",

test/testCliGenerateUi.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ function runCmd(cmd, args, cwd) {
7171
return res;
7272
}
7373

74+
function readTemplateThemeTokens() {
75+
return fs.readFileSync(path.join(process.cwd(), 'packages', 'templates', 'next-export-ui', 'src', 'theme', 'tokens.json'), 'utf-8');
76+
}
77+
7478
function minimalSchema() {
7579
return {
7680
thsVersion: '2025-12',
@@ -95,6 +99,12 @@ function minimalSchema() {
9599
};
96100
}
97101

102+
function minimalSchemaWithThemePreset() {
103+
const schema = minimalSchema();
104+
schema.app.theme = { preset: 'cyber-grid' };
105+
return schema;
106+
}
107+
98108
function schemaWithUiOverrides() {
99109
return {
100110
thsVersion: '2025-12',
@@ -161,6 +171,25 @@ describe('th generate (UI template)', function () {
161171
const layoutSource = fs.readFileSync(path.join(outDir, 'ui', 'app', 'layout.tsx'), 'utf-8');
162172
expect(layoutSource).to.include('NetworkStatus');
163173
expect(layoutSource).to.include('rootStyleVars');
174+
175+
const generatedTokens = fs.readFileSync(path.join(outDir, 'ui', 'src', 'theme', 'tokens.json'), 'utf-8');
176+
expect(generatedTokens).to.equal(readTemplateThemeTokens());
177+
});
178+
179+
it('materializes the explicit cyber-grid theme preset into generated UI output', function () {
180+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'th-ui-theme-preset-'));
181+
const schemaPath = path.join(dir, 'schema.json');
182+
const outDir = path.join(dir, 'out');
183+
writeJson(schemaPath, minimalSchemaWithThemePreset());
184+
185+
const res = runTh(['generate', schemaPath, '--out', outDir], process.cwd());
186+
expect(res.status, res.stderr || res.stdout).to.equal(0);
187+
188+
const generatedThs = fs.readFileSync(path.join(outDir, 'ui', 'src', 'generated', 'ths.ts'), 'utf-8');
189+
expect(generatedThs).to.include('"preset": "cyber-grid"');
190+
191+
const generatedTokens = fs.readFileSync(path.join(outDir, 'ui', 'src', 'theme', 'tokens.json'), 'utf-8');
192+
expect(generatedTokens).to.equal(readTemplateThemeTokens());
164193
});
165194

166195
it('generated UI builds (next export)', function () {
@@ -228,6 +257,8 @@ describe('th generate (UI template)', function () {
228257
const uiDir = path.join(outDir, 'ui');
229258
expect(fs.existsSync(path.join(uiDir, 'app', 'page.tsx'))).to.equal(true);
230259
expect(fs.existsSync(path.join(uiDir, 'app', 'tag', 'page.tsx'))).to.equal(true);
260+
const generatedThs = fs.readFileSync(path.join(uiDir, 'src', 'generated', 'ths.ts'), 'utf-8');
261+
expect(generatedThs).to.include('"preset": "cyber-grid"');
231262

232263
const install = runCmd('pnpm', ['install'], uiDir);
233264
expect(install.status, install.stderr || install.stdout).to.equal(0);
@@ -362,6 +393,29 @@ describe('th ui sync', function () {
362393
expect(fs.existsSync(path.join(outDir, 'ui', 'app', 'run', 'page.tsx'))).to.equal(true);
363394
});
364395

396+
it('preserves lockfiles and existing node_modules when package.json is unchanged', function () {
397+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'th-ui-sync-preserve-'));
398+
const schemaPath = path.join(dir, 'schema.json');
399+
const outDir = path.join(dir, 'out');
400+
const uiDir = path.join(outDir, 'ui');
401+
402+
writeJson(schemaPath, minimalSchema());
403+
writeCompiledArtifact(path.join(outDir, 'compiled', 'App.json'));
404+
writeManifest(path.join(outDir, 'manifest.json'));
405+
406+
const first = runTh(['ui', 'sync', schemaPath, '--out', outDir], process.cwd());
407+
expect(first.status, first.stderr || first.stdout).to.equal(0);
408+
409+
fs.writeFileSync(path.join(uiDir, 'pnpm-lock.yaml'), 'lockfileVersion: 9.0\n');
410+
fs.mkdirSync(path.join(uiDir, 'node_modules', '.keep'), { recursive: true });
411+
fs.writeFileSync(path.join(uiDir, 'node_modules', '.keep', 'marker.txt'), 'present\n');
412+
413+
const second = runTh(['ui', 'sync', schemaPath, '--out', outDir], process.cwd());
414+
expect(second.status, second.stderr || second.stdout).to.equal(0);
415+
expect(fs.readFileSync(path.join(uiDir, 'pnpm-lock.yaml'), 'utf-8')).to.equal('lockfileVersion: 9.0\n');
416+
expect(fs.readFileSync(path.join(uiDir, 'node_modules', '.keep', 'marker.txt'), 'utf-8')).to.equal('present\n');
417+
});
418+
365419
it('fails clearly when compiled artifacts are missing', function () {
366420
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'th-ui-sync-missing-compiled-'));
367421
const schemaPath = path.join(dir, 'schema.json');

test/testThsSchema.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,35 @@ describe('THS schema validation + lint', function () {
133133
expect(res.ok).to.equal(true);
134134
});
135135

136+
it('validateThsStructural accepts app.theme.preset for cyber-grid', function () {
137+
const input = minimalSchema({
138+
app: {
139+
name: 'Test App',
140+
slug: 'test-app',
141+
theme: { preset: 'cyber-grid' },
142+
features: { uploads: false, onChainIndexing: true }
143+
}
144+
});
145+
146+
const res = validateThsStructural(input);
147+
expect(res.ok).to.equal(true);
148+
});
149+
150+
it('validateThsStructural rejects unknown app.theme.preset values', function () {
151+
const input = minimalSchema({
152+
app: {
153+
name: 'Test App',
154+
slug: 'test-app',
155+
theme: { preset: 'not-a-theme' },
156+
features: { uploads: false, onChainIndexing: true }
157+
}
158+
});
159+
160+
const res = validateThsStructural(input);
161+
expect(res.ok).to.equal(false);
162+
expect(res.issues.some((i) => i.path === '/app/theme/preset')).to.equal(true);
163+
});
164+
136165
it('lintThs warns when custom home page is configured without extensions directory', function () {
137166
const input = minimalSchema({
138167
app: {

0 commit comments

Comments
 (0)