@deckspec/cli 0.1.0 → 0.1.2
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/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +125 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/approve.d.ts +2 -0
- package/dist/commands/approve.d.ts.map +1 -0
- package/dist/commands/approve.js +56 -0
- package/dist/commands/approve.js.map +1 -0
- package/dist/commands/dev.d.ts +4 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +333 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +249 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/lock.d.ts +2 -0
- package/dist/commands/lock.d.ts.map +1 -0
- package/dist/commands/lock.js +155 -0
- package/dist/commands/lock.js.map +1 -0
- package/dist/commands/patterns.d.ts +6 -0
- package/dist/commands/patterns.d.ts.map +1 -0
- package/dist/commands/patterns.js +97 -0
- package/dist/commands/patterns.js.map +1 -0
- package/dist/commands/render.d.ts +6 -0
- package/dist/commands/render.d.ts.map +1 -0
- package/dist/commands/render.js +50 -0
- package/dist/commands/render.js.map +1 -0
- package/dist/commands/validate.d.ts +6 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +37 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/package.json +9 -4
- package/src/cli.ts +19 -2
- package/src/commands/dev.ts +3 -2
- package/src/commands/init.ts +276 -0
- package/src/commands/render.ts +13 -4
- package/templates/noir-display/components/card/index.tsx +31 -0
- package/templates/noir-display/components/index.ts +2 -0
- package/templates/noir-display/components/slide-header/index.tsx +47 -0
- package/templates/noir-display/design.md +289 -0
- package/templates/noir-display/globals.css +263 -0
- package/templates/noir-display/package.json +39 -0
- package/templates/noir-display/patterns/_lib/chart-colors.ts +12 -0
- package/templates/noir-display/patterns/_lib/icon.tsx +50 -0
- package/templates/noir-display/patterns/big-number/index.tsx +87 -0
- package/templates/noir-display/patterns/body-message/index.tsx +295 -0
- package/templates/noir-display/patterns/bullet-list/index.tsx +132 -0
- package/templates/noir-display/patterns/challenge-cards/index.tsx +112 -0
- package/templates/noir-display/patterns/chart-bar/index.tsx +107 -0
- package/templates/noir-display/patterns/comparison-columns/index.tsx +115 -0
- package/templates/noir-display/patterns/feature-metrics/index.tsx +94 -0
- package/templates/noir-display/patterns/flow-diagram/index.tsx +151 -0
- package/templates/noir-display/patterns/hero-statement/index.tsx +72 -0
- package/templates/noir-display/patterns/icon-grid/index.tsx +126 -0
- package/templates/noir-display/patterns/index.ts +17 -0
- package/templates/noir-display/patterns/phased-roadmap/index.tsx +179 -0
- package/templates/noir-display/patterns/photo-split/index.tsx +110 -0
- package/templates/noir-display/patterns/pricing-tiers/index.tsx +127 -0
- package/templates/noir-display/patterns/showcase-grid/index.tsx +99 -0
- package/templates/noir-display/patterns/thank-you/index.tsx +86 -0
- package/templates/noir-display/patterns/three-pillars/index.tsx +112 -0
- package/templates/noir-display/patterns/title-center/index.tsx +46 -0
- package/templates/noir-display/tokens.json +30 -0
- package/templates/noir-display/tsconfig.json +13 -0
- package/tsconfig.json +0 -14
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { resolve, dirname } from "node:path";
|
|
2
|
+
import { loadDeckFile, validateDeck } from "@deckspec/dsl";
|
|
3
|
+
import { extractThemeName, resolveThemePatternsDir, compileTsxCached } from "@deckspec/renderer";
|
|
4
|
+
/**
|
|
5
|
+
* Validates a deck YAML file.
|
|
6
|
+
* Prints per-slide results to stdout and exits with code 1 if any errors.
|
|
7
|
+
*/
|
|
8
|
+
export async function validateCommand(filePath) {
|
|
9
|
+
const raw = await loadDeckFile(filePath);
|
|
10
|
+
const basePath = dirname(resolve(filePath));
|
|
11
|
+
const themeName = extractThemeName(raw);
|
|
12
|
+
const patternsDir = resolveThemePatternsDir(themeName);
|
|
13
|
+
const result = await validateDeck(raw, { basePath, patternsDir, compileTsx: compileTsxCached });
|
|
14
|
+
if (result.deckError) {
|
|
15
|
+
console.error("\u2717 Deck structure is invalid:");
|
|
16
|
+
for (const issue of result.deckError.issues) {
|
|
17
|
+
console.error(` ${issue.path.join(".")}: ${issue.message}`);
|
|
18
|
+
}
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
for (const slideResult of result.results) {
|
|
22
|
+
if (slideResult.valid) {
|
|
23
|
+
console.log(`\u2713 slides[${slideResult.index}]: valid`);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
for (const issue of slideResult.errors.issues) {
|
|
27
|
+
const detail = issue.message;
|
|
28
|
+
console.error(`\u2717 slides[${slideResult.index}]: ${detail}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!result.valid) {
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
console.log(`\nAll ${result.results.length} slide(s) passed validation.`);
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEjG;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,QAAgB;IACpD,MAAM,GAAG,GAAG,MAAM,YAAY,CAAC,QAAQ,CAAC,CAAC;IACzC,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC5C,MAAM,SAAS,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;IACxC,MAAM,WAAW,GAAG,uBAAuB,CAAC,SAAS,CAAC,CAAC;IAEvD,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAEhG,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACrB,OAAO,CAAC,KAAK,CAAC,mCAAmC,CAAC,CAAC;QACnD,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;YAC5C,OAAO,CAAC,KAAK,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QAC/D,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,KAAK,MAAM,WAAW,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACzC,IAAI,WAAW,CAAC,KAAK,EAAE,CAAC;YACtB,OAAO,CAAC,GAAG,CAAC,iBAAiB,WAAW,CAAC,KAAK,UAAU,CAAC,CAAC;QAC5D,CAAC;aAAM,CAAC;YACN,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,MAAO,CAAC,MAAM,EAAE,CAAC;gBAC/C,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC;gBAC7B,OAAO,CAAC,KAAK,CAAC,iBAAiB,WAAW,CAAC,KAAK,MAAM,MAAM,EAAE,CAAC,CAAC;YAClE,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAClB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,SAAS,MAAM,CAAC,OAAO,CAAC,MAAM,8BAA8B,CAAC,CAAC;AAC5E,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deckspec/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"files": [
|
|
5
|
+
"dist",
|
|
6
|
+
"src",
|
|
7
|
+
"templates"
|
|
8
|
+
],
|
|
4
9
|
"type": "module",
|
|
5
10
|
"main": "./dist/index.js",
|
|
6
11
|
"types": "./dist/index.d.ts",
|
|
@@ -13,9 +18,9 @@
|
|
|
13
18
|
"react": "^19.0.0",
|
|
14
19
|
"react-dom": "^19.0.0",
|
|
15
20
|
"zod": "^3.23.0",
|
|
16
|
-
"@deckspec/dsl": "0.1.
|
|
17
|
-
"@deckspec/
|
|
18
|
-
"@deckspec/
|
|
21
|
+
"@deckspec/dsl": "0.1.2",
|
|
22
|
+
"@deckspec/renderer": "0.1.2",
|
|
23
|
+
"@deckspec/schema": "0.1.2"
|
|
19
24
|
},
|
|
20
25
|
"license": "Apache-2.0",
|
|
21
26
|
"repository": {
|
package/src/cli.ts
CHANGED
|
@@ -6,11 +6,13 @@ import { approveCommand } from "./commands/approve.js";
|
|
|
6
6
|
import { lockCommand } from "./commands/lock.js";
|
|
7
7
|
import { patternsCommand } from "./commands/patterns.js";
|
|
8
8
|
import { devCommand } from "./commands/dev.js";
|
|
9
|
+
import { initCommand } from "./commands/init.js";
|
|
9
10
|
|
|
10
11
|
const USAGE = `
|
|
11
12
|
Usage: deckspec <command> [options]
|
|
12
13
|
|
|
13
14
|
Commands:
|
|
15
|
+
init [dir] [--theme <name>] Scaffold a new DeckSpec project
|
|
14
16
|
validate <file> Validate a deck YAML file
|
|
15
17
|
render <file> -o <output> Render a deck YAML file to HTML
|
|
16
18
|
approve <file> [options] Approve/reject slides or archive/activate decks
|
|
@@ -103,8 +105,23 @@ async function main(): Promise<void> {
|
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
case "dev": {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
let devDir = process.cwd();
|
|
109
|
+
let devPort: number | undefined;
|
|
110
|
+
const devArgs = args.slice(1);
|
|
111
|
+
for (let i = 0; i < devArgs.length; i++) {
|
|
112
|
+
if (devArgs[i] === "--port" && devArgs[i + 1]) {
|
|
113
|
+
devPort = parseInt(devArgs[i + 1], 10);
|
|
114
|
+
i++;
|
|
115
|
+
} else if (!devArgs[i].startsWith("-")) {
|
|
116
|
+
devDir = devArgs[i];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
await devCommand(devDir, { port: devPort });
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
case "init": {
|
|
124
|
+
await initCommand(args.slice(1));
|
|
108
125
|
break;
|
|
109
126
|
}
|
|
110
127
|
|
package/src/commands/dev.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Deck, Slide } from "@deckspec/schema";
|
|
|
5
5
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
6
6
|
import { renderDeck, renderDashboard, renderSlide, renderThemeDetail, loadThemeCSS, loadThemeTokens, extractThemeName, resolveThemePatternsDir, resolveThemePatternsSrcDir, compileTsxCached, clearCompileCache, type DeckWithPreviews, type ThemeSummary } from "@deckspec/renderer";
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
let PORT = 3002;
|
|
9
9
|
|
|
10
10
|
/** SSE clients for live reload */
|
|
11
11
|
const sseClients = new Set<ServerResponse>();
|
|
@@ -308,8 +308,9 @@ async function handleRequest(
|
|
|
308
308
|
}
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
export async function devCommand(dir: string): Promise<void> {
|
|
311
|
+
export async function devCommand(dir: string, options?: { port?: number }): Promise<void> {
|
|
312
312
|
const baseDir = resolve(dir);
|
|
313
|
+
if (options?.port != null) PORT = options.port;
|
|
313
314
|
|
|
314
315
|
// Try dynamic import of chokidar for file watching
|
|
315
316
|
let watcherActive = false;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { resolve, join, dirname, basename, relative } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import {
|
|
4
|
+
mkdir,
|
|
5
|
+
readdir,
|
|
6
|
+
copyFile,
|
|
7
|
+
writeFile,
|
|
8
|
+
stat,
|
|
9
|
+
} from "node:fs/promises";
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
|
|
14
|
+
/** Directories to skip when copying a theme. */
|
|
15
|
+
const SKIP_DIRS = new Set(["dist", "node_modules", ".turbo"]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Recursively copy a directory, skipping entries in SKIP_DIRS.
|
|
19
|
+
*/
|
|
20
|
+
async function copyDirRecursive(src: string, dest: string): Promise<void> {
|
|
21
|
+
await mkdir(dest, { recursive: true });
|
|
22
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
23
|
+
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
const srcPath = join(src, entry.name);
|
|
26
|
+
const destPath = join(dest, entry.name);
|
|
27
|
+
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
30
|
+
await copyDirRecursive(srcPath, destPath);
|
|
31
|
+
} else {
|
|
32
|
+
await copyFile(srcPath, destPath);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* List pattern directory names under a theme's patterns/ folder.
|
|
39
|
+
* Excludes _lib and index.ts; returns only directories.
|
|
40
|
+
*/
|
|
41
|
+
async function listPatternNames(patternsDir: string): Promise<string[]> {
|
|
42
|
+
let entries;
|
|
43
|
+
try {
|
|
44
|
+
entries = await readdir(patternsDir, { withFileTypes: true });
|
|
45
|
+
} catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
return entries
|
|
49
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith("_"))
|
|
50
|
+
.map((e) => e.name)
|
|
51
|
+
.sort();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the templates directory bundled with the CLI package.
|
|
56
|
+
* Works both in dev (src/commands/) and installed (dist/commands/).
|
|
57
|
+
*
|
|
58
|
+
* __dirname is packages/cli/dist/commands or packages/cli/src/commands
|
|
59
|
+
* Go up 2 levels to packages/cli/, then into templates/
|
|
60
|
+
*/
|
|
61
|
+
function resolveTemplatesDir(): string {
|
|
62
|
+
return resolve(__dirname, "..", "..", "templates");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function generateSampleDeck(theme: string): string {
|
|
66
|
+
return `meta:
|
|
67
|
+
title: "My Presentation"
|
|
68
|
+
theme: ${theme}
|
|
69
|
+
state: active
|
|
70
|
+
slides:
|
|
71
|
+
- file: title-center
|
|
72
|
+
vars:
|
|
73
|
+
title: "Welcome"
|
|
74
|
+
subtitle: "A presentation built with DeckSpec"
|
|
75
|
+
|
|
76
|
+
- file: feature-metrics
|
|
77
|
+
vars:
|
|
78
|
+
headline: "Key Numbers"
|
|
79
|
+
metrics:
|
|
80
|
+
- label: "Slides"
|
|
81
|
+
value: "3"
|
|
82
|
+
- label: "Patterns"
|
|
83
|
+
value: "17+"
|
|
84
|
+
- label: "Build Steps"
|
|
85
|
+
value: "0"
|
|
86
|
+
|
|
87
|
+
- file: three-pillars
|
|
88
|
+
vars:
|
|
89
|
+
label: "How It Works"
|
|
90
|
+
heading: "Three Simple Steps"
|
|
91
|
+
pillars:
|
|
92
|
+
- title: "Write"
|
|
93
|
+
value: "YAML"
|
|
94
|
+
description: "Define your content in a simple, structured format."
|
|
95
|
+
- title: "Validate"
|
|
96
|
+
value: "Zod"
|
|
97
|
+
description: "Schemas catch mistakes before you ever see the output."
|
|
98
|
+
- title: "Render"
|
|
99
|
+
value: "HTML"
|
|
100
|
+
description: "Patterns produce standalone HTML — no dependencies."
|
|
101
|
+
`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function generatePackageJson(): string {
|
|
105
|
+
return JSON.stringify(
|
|
106
|
+
{
|
|
107
|
+
private: true,
|
|
108
|
+
type: "module",
|
|
109
|
+
dependencies: {
|
|
110
|
+
"@deckspec/cli": "^0.1.0",
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
null,
|
|
114
|
+
2,
|
|
115
|
+
) + "\n";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function generateGitignore(): string {
|
|
119
|
+
return `node_modules/
|
|
120
|
+
dist/
|
|
121
|
+
output/
|
|
122
|
+
tmp/
|
|
123
|
+
*.tsbuildinfo
|
|
124
|
+
`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function generateClaudeMd(theme: string, patterns: string[]): string {
|
|
128
|
+
const patternList = patterns.map((p) => `- \`${p}\``).join("\n");
|
|
129
|
+
|
|
130
|
+
return `# DeckSpec Project
|
|
131
|
+
|
|
132
|
+
Programmable presentations. YAML で書く。Zod で守る。React で描く。
|
|
133
|
+
|
|
134
|
+
## YAML DSL Spec
|
|
135
|
+
|
|
136
|
+
A deck is defined by a single \`deck.yaml\` file:
|
|
137
|
+
|
|
138
|
+
\`\`\`yaml
|
|
139
|
+
meta:
|
|
140
|
+
title: "Presentation Title"
|
|
141
|
+
theme: ${theme}
|
|
142
|
+
state: active # active | archived
|
|
143
|
+
slides:
|
|
144
|
+
- file: pattern-name # pattern from themes/${theme}/patterns/
|
|
145
|
+
vars:
|
|
146
|
+
key: "value" # content injected into the pattern
|
|
147
|
+
\`\`\`
|
|
148
|
+
|
|
149
|
+
### Slide Fields
|
|
150
|
+
|
|
151
|
+
| Field | Required | Description |
|
|
152
|
+
|-------|----------|-------------|
|
|
153
|
+
| \`file\` | yes | Pattern name (resolved from theme) or relative path |
|
|
154
|
+
| \`vars\` | yes | Content variables — validated by the pattern's Zod schema |
|
|
155
|
+
| \`state\` | no | \`generated\` (default) / \`approved\` / \`locked\` |
|
|
156
|
+
|
|
157
|
+
## Available Patterns (theme: ${theme})
|
|
158
|
+
|
|
159
|
+
${patternList}
|
|
160
|
+
|
|
161
|
+
Each pattern lives in \`themes/${theme}/patterns/<name>/index.tsx\` and exports:
|
|
162
|
+
- \`export const schema\` — Zod schema defining accepted \`vars\`
|
|
163
|
+
- \`export default Component\` — React component for SSR
|
|
164
|
+
|
|
165
|
+
Check each pattern's \`examples.yaml\` (if present) for usage examples.
|
|
166
|
+
|
|
167
|
+
## Commands
|
|
168
|
+
|
|
169
|
+
\`\`\`bash
|
|
170
|
+
npx deckspec validate decks/sample/deck.yaml # Validate YAML against Zod schemas
|
|
171
|
+
npx deckspec render decks/sample/deck.yaml -o out # Render to standalone HTML
|
|
172
|
+
npx deckspec dev # Live preview at http://localhost:3002
|
|
173
|
+
npx deckspec patterns # List all patterns with schemas
|
|
174
|
+
\`\`\`
|
|
175
|
+
|
|
176
|
+
## Creating a New Slide
|
|
177
|
+
|
|
178
|
+
1. Pick a pattern from the list above
|
|
179
|
+
2. Add a slide entry to \`deck.yaml\` with \`file:\` and \`vars:\`
|
|
180
|
+
3. Run \`npx deckspec validate\` to check your YAML
|
|
181
|
+
4. Run \`npx deckspec dev\` to preview
|
|
182
|
+
|
|
183
|
+
## Creating a Deck-Local Pattern
|
|
184
|
+
|
|
185
|
+
If no existing pattern fits, create a custom one:
|
|
186
|
+
|
|
187
|
+
1. Create \`decks/<deck>/patterns/<name>/index.tsx\`
|
|
188
|
+
2. Export \`schema\` (Zod) and \`default\` (React component)
|
|
189
|
+
3. Reference it in \`deck.yaml\` with \`file: <name>\`
|
|
190
|
+
4. Deck-local patterns are compiled on-the-fly with esbuild — no build step needed
|
|
191
|
+
|
|
192
|
+
## Theme Design Reference
|
|
193
|
+
|
|
194
|
+
See \`themes/${theme}/design.md\` for the theme's design principles, color palette, and typography rules.
|
|
195
|
+
`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* deckspec init [dir] --theme <name>
|
|
200
|
+
*/
|
|
201
|
+
export async function initCommand(args: string[]): Promise<void> {
|
|
202
|
+
// Parse arguments
|
|
203
|
+
let targetDir = ".";
|
|
204
|
+
let themeName = "noir-display";
|
|
205
|
+
|
|
206
|
+
const themeIdx = args.indexOf("--theme");
|
|
207
|
+
if (themeIdx !== -1) {
|
|
208
|
+
const val = args[themeIdx + 1];
|
|
209
|
+
if (!val) {
|
|
210
|
+
console.error("Error: --theme requires a value.");
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
themeName = val;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// First positional arg (not a flag) is the target directory
|
|
217
|
+
for (const arg of args) {
|
|
218
|
+
if (arg === "--theme") break;
|
|
219
|
+
if (!arg.startsWith("-")) {
|
|
220
|
+
targetDir = arg;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const root = resolve(targetDir);
|
|
226
|
+
const templatesDir = resolveTemplatesDir();
|
|
227
|
+
const themeSrc = join(templatesDir, themeName);
|
|
228
|
+
|
|
229
|
+
// Verify theme exists
|
|
230
|
+
try {
|
|
231
|
+
const s = await stat(themeSrc);
|
|
232
|
+
if (!s.isDirectory()) throw new Error();
|
|
233
|
+
} catch {
|
|
234
|
+
console.error(`Error: Theme "${themeName}" not found at ${themeSrc}`);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.log(`Initializing DeckSpec project in ${root}`);
|
|
239
|
+
console.log(` Theme: ${themeName}`);
|
|
240
|
+
|
|
241
|
+
// 1. Copy theme
|
|
242
|
+
const themeDest = join(root, "themes", themeName);
|
|
243
|
+
console.log(` Copying theme to themes/${themeName}/`);
|
|
244
|
+
await copyDirRecursive(themeSrc, themeDest);
|
|
245
|
+
|
|
246
|
+
// 2. Create sample deck
|
|
247
|
+
const deckDir = join(root, "decks", "sample");
|
|
248
|
+
await mkdir(deckDir, { recursive: true });
|
|
249
|
+
const deckPath = join(deckDir, "deck.yaml");
|
|
250
|
+
await writeFile(deckPath, generateSampleDeck(themeName));
|
|
251
|
+
console.log(" Created decks/sample/deck.yaml");
|
|
252
|
+
|
|
253
|
+
// 3. Create package.json
|
|
254
|
+
const pkgPath = join(root, "package.json");
|
|
255
|
+
await writeFile(pkgPath, generatePackageJson());
|
|
256
|
+
console.log(" Created package.json");
|
|
257
|
+
|
|
258
|
+
// 4. Create CLAUDE.md
|
|
259
|
+
const patterns = await listPatternNames(join(themeDest, "patterns"));
|
|
260
|
+
const claudePath = join(root, "CLAUDE.md");
|
|
261
|
+
await writeFile(claudePath, generateClaudeMd(themeName, patterns));
|
|
262
|
+
console.log(" Created CLAUDE.md");
|
|
263
|
+
|
|
264
|
+
// 5. Create .gitignore
|
|
265
|
+
const gitignorePath = join(root, ".gitignore");
|
|
266
|
+
await writeFile(gitignorePath, generateGitignore());
|
|
267
|
+
console.log(" Created .gitignore");
|
|
268
|
+
|
|
269
|
+
console.log("");
|
|
270
|
+
console.log("Done! Next steps:");
|
|
271
|
+
console.log("");
|
|
272
|
+
console.log(` cd ${relative(process.cwd(), root) || "."}`);
|
|
273
|
+
console.log(" npm install");
|
|
274
|
+
console.log(" npx deckspec dev");
|
|
275
|
+
console.log("");
|
|
276
|
+
}
|
package/src/commands/render.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { writeFile } from "node:fs/promises";
|
|
2
|
-
import { resolve, dirname } from "node:path";
|
|
1
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { resolve, dirname, extname, join } from "node:path";
|
|
3
3
|
import { loadDeckFile, validateDeck } from "@deckspec/dsl";
|
|
4
4
|
import type { Deck } from "@deckspec/schema";
|
|
5
5
|
import { renderDeck, loadThemeCSS, extractThemeName, resolveThemePatternsDir, resolveThemePatternsSrcDir, compileTsxCached } from "@deckspec/renderer";
|
|
@@ -45,6 +45,15 @@ export async function renderCommand(
|
|
|
45
45
|
const patternsSrcDir = resolveThemePatternsSrcDir(themeName);
|
|
46
46
|
const html = await renderDeck(deck, themeCSS, { basePath, patternsDir, patternsSrcDir });
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
// If outputPath has no .html extension, treat it as a directory
|
|
49
|
+
let finalPath = outputPath;
|
|
50
|
+
if (extname(outputPath) !== ".html") {
|
|
51
|
+
finalPath = join(outputPath, "index.html");
|
|
52
|
+
await mkdir(outputPath, { recursive: true });
|
|
53
|
+
} else {
|
|
54
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await writeFile(finalPath, html, "utf-8");
|
|
58
|
+
console.log(`\u2713 Rendered ${result.results.length} slide(s) to ${finalPath}`);
|
|
50
59
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode, CSSProperties } from "react";
|
|
2
|
+
|
|
3
|
+
interface CardProps {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
highlight?: boolean;
|
|
6
|
+
style?: CSSProperties;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Dark card with subtle border and rounded corners.
|
|
11
|
+
*/
|
|
12
|
+
export function Card({ children, highlight, style }: CardProps): ReactElement {
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
style={{
|
|
16
|
+
backgroundColor: "var(--color-card-background)",
|
|
17
|
+
borderRadius: "var(--radius)",
|
|
18
|
+
border: highlight
|
|
19
|
+
? "1px solid var(--color-primary)"
|
|
20
|
+
: "1px solid var(--color-border)",
|
|
21
|
+
padding: "32px",
|
|
22
|
+
display: "flex",
|
|
23
|
+
flexDirection: "column",
|
|
24
|
+
gap: 12,
|
|
25
|
+
...style,
|
|
26
|
+
}}
|
|
27
|
+
>
|
|
28
|
+
{children}
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
|
|
3
|
+
interface SlideHeaderProps {
|
|
4
|
+
label?: string;
|
|
5
|
+
heading: string;
|
|
6
|
+
headingSize?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Minimal slide header — optional muted label + display heading.
|
|
11
|
+
* Apple-style: label is small muted text, heading is large semibold.
|
|
12
|
+
*/
|
|
13
|
+
export function SlideHeader({
|
|
14
|
+
label,
|
|
15
|
+
heading,
|
|
16
|
+
headingSize = 40,
|
|
17
|
+
}: SlideHeaderProps): ReactElement {
|
|
18
|
+
return (
|
|
19
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
20
|
+
{label && (
|
|
21
|
+
<span
|
|
22
|
+
style={{
|
|
23
|
+
fontSize: 14,
|
|
24
|
+
fontWeight: 600,
|
|
25
|
+
letterSpacing: "-0.016em",
|
|
26
|
+
color: "var(--color-accent)",
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
{label}
|
|
30
|
+
</span>
|
|
31
|
+
)}
|
|
32
|
+
<h2
|
|
33
|
+
style={{
|
|
34
|
+
fontFamily: "var(--font-heading)",
|
|
35
|
+
fontSize: headingSize,
|
|
36
|
+
fontWeight: 600,
|
|
37
|
+
lineHeight: 1.1,
|
|
38
|
+
letterSpacing: headingSize >= 48 ? "-0.015em" : "-0.009em",
|
|
39
|
+
color: "var(--color-foreground)",
|
|
40
|
+
margin: 0,
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
{heading}
|
|
44
|
+
</h2>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|