create-imagine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -0
  3. package/bin/create-imagine.js +407 -0
  4. package/package.json +19 -0
  5. package/templates/blank/README.md +96 -0
  6. package/templates/blank/gitignore +6 -0
  7. package/templates/blank/index.html +13 -0
  8. package/templates/blank/package.json +42 -0
  9. package/templates/blank/projects/example/figures/hello-world.tsx +45 -0
  10. package/templates/blank/projects/example/manifest.ts +44 -0
  11. package/templates/blank/projects/example/project.ts +16 -0
  12. package/templates/blank/projects/example/props.json +4 -0
  13. package/templates/blank/scripts/list.ts +46 -0
  14. package/templates/blank/scripts/projects.ts +43 -0
  15. package/templates/blank/scripts/render.ts +288 -0
  16. package/templates/blank/scripts/server.ts +117 -0
  17. package/templates/blank/src/core/__tests__/controls.test.ts +14 -0
  18. package/templates/blank/src/core/__tests__/manifest.test.ts +45 -0
  19. package/templates/blank/src/core/__tests__/props.test.ts +9 -0
  20. package/templates/blank/src/core/controls.ts +14 -0
  21. package/templates/blank/src/core/manifest.ts +134 -0
  22. package/templates/blank/src/core/props.ts +7 -0
  23. package/templates/blank/src/framework/Figure.tsx +34 -0
  24. package/templates/blank/src/framework/__tests__/sizing.test.ts +29 -0
  25. package/templates/blank/src/framework/charts/Axes.tsx +103 -0
  26. package/templates/blank/src/framework/charts/GridLines.tsx +59 -0
  27. package/templates/blank/src/framework/charts/Legend.tsx +31 -0
  28. package/templates/blank/src/framework/charts/Series.tsx +50 -0
  29. package/templates/blank/src/framework/charts/scales.ts +19 -0
  30. package/templates/blank/src/framework/diagrams/primitives.tsx +134 -0
  31. package/templates/blank/src/framework/layout/PanelGrid.tsx +60 -0
  32. package/templates/blank/src/framework/math/MathSvg.tsx +35 -0
  33. package/templates/blank/src/framework/math/mathjax.ts +64 -0
  34. package/templates/blank/src/framework/sizing.ts +28 -0
  35. package/templates/blank/src/framework/theme.ts +35 -0
  36. package/templates/blank/src/framework/types.ts +42 -0
  37. package/templates/blank/src/main.tsx +11 -0
  38. package/templates/blank/src/studio/StudioApp.tsx +130 -0
  39. package/templates/blank/src/studio/StudioRoot.tsx +14 -0
  40. package/templates/blank/src/studio/base64url.ts +8 -0
  41. package/templates/blank/src/studio/figureLoader.ts +30 -0
  42. package/templates/blank/src/studio/projectLoader.ts +40 -0
  43. package/templates/blank/src/studio/propsApi.ts +26 -0
  44. package/templates/blank/src/studio/routes/FigureView.tsx +365 -0
  45. package/templates/blank/src/studio/routes/ProjectHome.tsx +107 -0
  46. package/templates/blank/src/studio/routes/ProjectsHome.tsx +63 -0
  47. package/templates/blank/src/studio/routes/RenderView.tsx +123 -0
  48. package/templates/blank/src/studio/studio.css +540 -0
  49. package/templates/blank/src/studio/useProjectProps.ts +129 -0
  50. package/templates/blank/src/vite-env.d.ts +2 -0
  51. package/templates/blank/tsconfig.json +20 -0
  52. package/templates/blank/vite.config.ts +82 -0
  53. package/templates/blank/vitest.config.ts +8 -0
  54. package/templates/example/README.md +96 -0
  55. package/templates/example/gitignore +6 -0
  56. package/templates/example/index.html +13 -0
  57. package/templates/example/package.json +42 -0
  58. package/templates/example/projects/example/figures/ai-agent-architecture.tsx +133 -0
  59. package/templates/example/projects/example/figures/equation.tsx +29 -0
  60. package/templates/example/projects/example/figures/hello-world.tsx +45 -0
  61. package/templates/example/projects/example/figures/line-chart.tsx +80 -0
  62. package/templates/example/projects/example/figures/multi-panel.tsx +51 -0
  63. package/templates/example/projects/example/figures/pipeline-diagram.tsx +51 -0
  64. package/templates/example/projects/example/manifest.ts +161 -0
  65. package/templates/example/projects/example/project.ts +31 -0
  66. package/templates/example/projects/example/props.json +10 -0
  67. package/templates/example/public/projects/example/previews/ai-agent-architecture--default.png +0 -0
  68. package/templates/example/public/projects/example/previews/equation--default.png +0 -0
  69. package/templates/example/public/projects/example/previews/hello-world--default.png +0 -0
  70. package/templates/example/public/projects/example/previews/line-chart--default.png +0 -0
  71. package/templates/example/public/projects/example/previews/multi-panel--default.png +0 -0
  72. package/templates/example/public/projects/example/previews/pipeline-diagram--default.png +0 -0
  73. package/templates/example/scripts/list.ts +46 -0
  74. package/templates/example/scripts/projects.ts +43 -0
  75. package/templates/example/scripts/render.ts +288 -0
  76. package/templates/example/scripts/server.ts +117 -0
  77. package/templates/example/src/core/__tests__/controls.test.ts +14 -0
  78. package/templates/example/src/core/__tests__/manifest.test.ts +45 -0
  79. package/templates/example/src/core/__tests__/props.test.ts +9 -0
  80. package/templates/example/src/core/controls.ts +14 -0
  81. package/templates/example/src/core/manifest.ts +134 -0
  82. package/templates/example/src/core/props.ts +7 -0
  83. package/templates/example/src/framework/Figure.tsx +34 -0
  84. package/templates/example/src/framework/__tests__/sizing.test.ts +29 -0
  85. package/templates/example/src/framework/charts/Axes.tsx +103 -0
  86. package/templates/example/src/framework/charts/GridLines.tsx +59 -0
  87. package/templates/example/src/framework/charts/Legend.tsx +31 -0
  88. package/templates/example/src/framework/charts/Series.tsx +50 -0
  89. package/templates/example/src/framework/charts/scales.ts +19 -0
  90. package/templates/example/src/framework/diagrams/primitives.tsx +134 -0
  91. package/templates/example/src/framework/layout/PanelGrid.tsx +60 -0
  92. package/templates/example/src/framework/math/MathSvg.tsx +35 -0
  93. package/templates/example/src/framework/math/mathjax.ts +64 -0
  94. package/templates/example/src/framework/sizing.ts +28 -0
  95. package/templates/example/src/framework/theme.ts +35 -0
  96. package/templates/example/src/framework/types.ts +42 -0
  97. package/templates/example/src/main.tsx +11 -0
  98. package/templates/example/src/studio/StudioApp.tsx +130 -0
  99. package/templates/example/src/studio/StudioRoot.tsx +14 -0
  100. package/templates/example/src/studio/base64url.ts +8 -0
  101. package/templates/example/src/studio/figureLoader.ts +30 -0
  102. package/templates/example/src/studio/projectLoader.ts +40 -0
  103. package/templates/example/src/studio/propsApi.ts +26 -0
  104. package/templates/example/src/studio/routes/FigureView.tsx +365 -0
  105. package/templates/example/src/studio/routes/ProjectHome.tsx +107 -0
  106. package/templates/example/src/studio/routes/ProjectsHome.tsx +63 -0
  107. package/templates/example/src/studio/routes/RenderView.tsx +123 -0
  108. package/templates/example/src/studio/studio.css +540 -0
  109. package/templates/example/src/studio/useProjectProps.ts +129 -0
  110. package/templates/example/src/vite-env.d.ts +2 -0
  111. package/templates/example/tsconfig.json +20 -0
  112. package/templates/example/vite.config.ts +82 -0
  113. package/templates/example/vitest.config.ts +8 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MX
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # create-imagine
2
+
3
+ Scaffold a new Imagine project (React → scientific figures).
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ # Recommended
9
+ npx create-imagine@latest my-figures
10
+
11
+ # Also works (because the package name is create-imagine)
12
+ npm create imagine@latest my-figures
13
+ ```
14
+
15
+ ## Templates
16
+
17
+ - `blank`: minimal starter project (no preview images)
18
+ - `example`: full starter project (includes example figures + preview PNGs)
19
+
20
+ ## Options
21
+
22
+ ```bash
23
+ create-imagine [targetDir]
24
+
25
+ --template blank|example
26
+ --pm npm|pnpm|yarn|bun
27
+ --install / --no-install
28
+ --skills / --no-skills
29
+ --yes
30
+ --force
31
+ --help
32
+ ```
33
+
34
+ ## Optional: install agent skills
35
+
36
+ This scaffolder can optionally install the `imagine-best-practices` skill into your project:
37
+
38
+ ```bash
39
+ npx skills add https://github.com/midhunxavier/imagine-skills --skill imagine-best-practices
40
+ ```
@@ -0,0 +1,407 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { spawn } from 'node:child_process';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { createInterface } from 'node:readline/promises';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ function usage() {
13
+ const text = `
14
+ create-imagine
15
+
16
+ Usage:
17
+ create-imagine [targetDir] [options]
18
+
19
+ Options:
20
+ --template blank|example
21
+ --pm npm|pnpm|yarn|bun
22
+ --install / --no-install
23
+ --skills / --no-skills
24
+ --yes
25
+ --force
26
+ --help
27
+ `.trim();
28
+ // eslint-disable-next-line no-console
29
+ console.log(text);
30
+ }
31
+
32
+ function parseArgs(argv) {
33
+ /** @type {{ targetDir?: string; template?: string; pm?: string; install?: boolean; skills?: boolean; yes: boolean; force: boolean; help: boolean }} */
34
+ const out = { yes: false, force: false, help: false };
35
+ /** @type {string[]} */
36
+ const positional = [];
37
+ /** @type {string[]} */
38
+ const unknown = [];
39
+
40
+ for (let i = 2; i < argv.length; i++) {
41
+ const a = argv[i];
42
+ if (!a) continue;
43
+
44
+ if (a === '--') {
45
+ positional.push(...argv.slice(i + 1).filter(Boolean));
46
+ break;
47
+ }
48
+
49
+ if (a === '--help' || a === '-h') {
50
+ out.help = true;
51
+ continue;
52
+ }
53
+ if (a === '--yes' || a === '-y') {
54
+ out.yes = true;
55
+ continue;
56
+ }
57
+ if (a === '--force') {
58
+ out.force = true;
59
+ continue;
60
+ }
61
+ if (a === '--install') {
62
+ out.install = true;
63
+ continue;
64
+ }
65
+ if (a === '--no-install') {
66
+ out.install = false;
67
+ continue;
68
+ }
69
+ if (a === '--skills') {
70
+ out.skills = true;
71
+ continue;
72
+ }
73
+ if (a === '--no-skills') {
74
+ out.skills = false;
75
+ continue;
76
+ }
77
+
78
+ if (a === '--template') {
79
+ const v = argv[i + 1];
80
+ if (!v) unknown.push(a);
81
+ else out.template = v;
82
+ i += 1;
83
+ continue;
84
+ }
85
+ if (a.startsWith('--template=')) {
86
+ out.template = a.slice('--template='.length);
87
+ continue;
88
+ }
89
+
90
+ if (a === '--pm') {
91
+ const v = argv[i + 1];
92
+ if (!v) unknown.push(a);
93
+ else out.pm = v;
94
+ i += 1;
95
+ continue;
96
+ }
97
+ if (a.startsWith('--pm=')) {
98
+ out.pm = a.slice('--pm='.length);
99
+ continue;
100
+ }
101
+
102
+ if (a.startsWith('-')) {
103
+ unknown.push(a);
104
+ continue;
105
+ }
106
+
107
+ positional.push(a);
108
+ }
109
+
110
+ if (unknown.length) {
111
+ const msg = `Unknown option(s): ${unknown.join(', ')}`;
112
+ const err = new Error(msg);
113
+ // @ts-ignore
114
+ err.code = 'USAGE';
115
+ throw err;
116
+ }
117
+
118
+ if (positional.length > 1) {
119
+ const err = new Error(`Too many arguments. Expected at most 1 targetDir, got: ${positional.join(' ')}`);
120
+ // @ts-ignore
121
+ err.code = 'USAGE';
122
+ throw err;
123
+ }
124
+
125
+ if (positional.length === 1) out.targetDir = positional[0];
126
+ return out;
127
+ }
128
+
129
+ function isNonEmptyDir(entries) {
130
+ return entries.some((e) => e !== '.' && e !== '..');
131
+ }
132
+
133
+ async function dirExists(p) {
134
+ return fs
135
+ .stat(p)
136
+ .then((s) => s.isDirectory())
137
+ .catch(() => false);
138
+ }
139
+
140
+ async function fileExists(p) {
141
+ return fs
142
+ .stat(p)
143
+ .then((s) => s.isFile())
144
+ .catch(() => false);
145
+ }
146
+
147
+ async function isDirEmpty(p) {
148
+ const entries = await fs.readdir(p).catch(() => null);
149
+ if (!entries) return true;
150
+ return !isNonEmptyDir(entries);
151
+ }
152
+
153
+ function toKebabCase(input) {
154
+ return String(input)
155
+ .trim()
156
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
157
+ .replace(/[^a-zA-Z0-9]+/g, '-')
158
+ .replace(/^-+|-+$/g, '')
159
+ .toLowerCase();
160
+ }
161
+
162
+ function toValidPackageName(baseName) {
163
+ let name = toKebabCase(baseName);
164
+ name = name.replace(/^[_\\.]+/, '');
165
+ name = name.replace(/[^a-z0-9-]+/g, '');
166
+ name = name.replace(/^-+|-+$/g, '');
167
+ if (!name) name = 'imagine-project';
168
+ return name;
169
+ }
170
+
171
+ function toTitle(input) {
172
+ const words = String(input)
173
+ .trim()
174
+ .replace(/[-_]+/g, ' ')
175
+ .replace(/\s+/g, ' ')
176
+ .split(' ')
177
+ .filter(Boolean);
178
+ if (!words.length) return 'Imagine Project';
179
+ return words.map((w) => w.slice(0, 1).toUpperCase() + w.slice(1)).join(' ');
180
+ }
181
+
182
+ function detectPackageManager() {
183
+ const ua = process.env.npm_config_user_agent ?? '';
184
+ const m = ua.match(/^(npm|pnpm|yarn|bun)\//);
185
+ return m?.[1] ?? null;
186
+ }
187
+
188
+ function platformBin(cmd) {
189
+ if (process.platform !== 'win32') return cmd;
190
+ return `${cmd}.cmd`;
191
+ }
192
+
193
+ function run(cmd, args, opts) {
194
+ return new Promise((resolve, reject) => {
195
+ const child = spawn(cmd, args, {
196
+ cwd: opts?.cwd ?? process.cwd(),
197
+ stdio: 'inherit',
198
+ env: process.env
199
+ });
200
+ child.on('error', reject);
201
+ child.on('exit', (code) => {
202
+ if (code === 0) resolve();
203
+ else reject(new Error(`${cmd} ${args.join(' ')} exited with code ${code}`));
204
+ });
205
+ });
206
+ }
207
+
208
+ async function copyDirContents(srcDir, destDir) {
209
+ const entries = await fs.readdir(srcDir, { withFileTypes: true });
210
+ for (const e of entries) {
211
+ const src = path.join(srcDir, e.name);
212
+ const dest = path.join(destDir, e.name);
213
+ await fs.cp(src, dest, { recursive: true });
214
+ }
215
+ }
216
+
217
+ function ensureTemplate(value) {
218
+ if (value !== 'blank' && value !== 'example') {
219
+ const err = new Error(`Invalid --template: ${value}. Expected "blank" or "example".`);
220
+ // @ts-ignore
221
+ err.code = 'USAGE';
222
+ throw err;
223
+ }
224
+ return value;
225
+ }
226
+
227
+ function ensurePm(value) {
228
+ if (value !== 'npm' && value !== 'pnpm' && value !== 'yarn' && value !== 'bun') {
229
+ const err = new Error(`Invalid --pm: ${value}. Expected npm|pnpm|yarn|bun.`);
230
+ // @ts-ignore
231
+ err.code = 'USAGE';
232
+ throw err;
233
+ }
234
+ return value;
235
+ }
236
+
237
+ async function main() {
238
+ const args = parseArgs(process.argv);
239
+ if (args.help) {
240
+ usage();
241
+ return;
242
+ }
243
+
244
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
245
+ const rl = interactive ? createInterface({ input: process.stdin, output: process.stdout }) : null;
246
+
247
+ try {
248
+ const imagineSkillsRepo = 'https://github.com/midhunxavier/imagine-skills';
249
+ const imagineSkillsName = 'imagine-best-practices';
250
+
251
+ const cwdBase = path.basename(process.cwd());
252
+ const defaultTargetDir = cwdBase && cwdBase !== path.parse(process.cwd()).root ? cwdBase : 'imagine-project';
253
+
254
+ let targetDirInput = args.targetDir;
255
+ if (!targetDirInput) {
256
+ if (args.yes || !interactive) {
257
+ targetDirInput = defaultTargetDir;
258
+ } else {
259
+ const answer = await rl.question(`Project directory (${defaultTargetDir}): `);
260
+ targetDirInput = answer.trim() || defaultTargetDir;
261
+ }
262
+ }
263
+
264
+ const targetPath = path.resolve(process.cwd(), targetDirInput);
265
+ const targetBaseName = path.basename(targetPath);
266
+
267
+ const template = ensureTemplate(
268
+ args.template ??
269
+ (args.yes || !interactive
270
+ ? 'blank'
271
+ : ensureTemplate((await rl.question('Template (blank/example) (blank): ')).trim() || 'blank'))
272
+ );
273
+
274
+ const install =
275
+ args.install ??
276
+ (args.yes || !interactive
277
+ ? true
278
+ : (() => {
279
+ return null;
280
+ })());
281
+
282
+ let shouldInstall = install;
283
+ if (shouldInstall === null) {
284
+ const answer = (await rl.question('Install dependencies? (Y/n): ')).trim().toLowerCase();
285
+ shouldInstall = answer === '' || answer === 'y' || answer === 'yes';
286
+ }
287
+
288
+ let pm = args.pm ?? null;
289
+ if (shouldInstall) {
290
+ if (!pm) pm = args.yes || !interactive ? detectPackageManager() ?? 'npm' : null;
291
+ if (!pm && interactive) {
292
+ const answer = (await rl.question('Package manager (npm/pnpm/yarn/bun) (npm): ')).trim().toLowerCase();
293
+ pm = answer || 'npm';
294
+ }
295
+ pm = ensurePm(pm ?? 'npm');
296
+ }
297
+
298
+ const skills =
299
+ args.skills ??
300
+ (args.yes || !interactive
301
+ ? false
302
+ : (() => {
303
+ return null;
304
+ })());
305
+
306
+ let shouldInstallSkills = skills;
307
+ if (shouldInstallSkills === null) {
308
+ const answer = (await rl.question(`Add Imagine agent skill "${imagineSkillsName}"? (y/N): `)).trim().toLowerCase();
309
+ shouldInstallSkills = answer === 'y' || answer === 'yes';
310
+ }
311
+
312
+ const existsDir = await dirExists(targetPath);
313
+ if (existsDir) {
314
+ const empty = await isDirEmpty(targetPath);
315
+ if (!empty) {
316
+ if (!args.force) {
317
+ throw new Error(`Target directory is not empty: ${targetPath}\nUse --force to overwrite.`);
318
+ }
319
+ await fs.rm(targetPath, { recursive: true, force: true });
320
+ }
321
+ } else {
322
+ const existsFile = await fileExists(targetPath);
323
+ if (existsFile) throw new Error(`Target path exists and is a file: ${targetPath}`);
324
+ }
325
+
326
+ await fs.mkdir(targetPath, { recursive: true });
327
+
328
+ const templateDir = path.resolve(__dirname, '..', 'templates', template);
329
+ await copyDirContents(templateDir, targetPath);
330
+
331
+ const gitignoreSrc = path.join(targetPath, 'gitignore');
332
+ const gitignoreDest = path.join(targetPath, '.gitignore');
333
+ if (await fileExists(gitignoreSrc)) {
334
+ await fs.rename(gitignoreSrc, gitignoreDest).catch(async (err) => {
335
+ if (String(err?.code ?? '') === 'EEXIST') {
336
+ await fs.rm(gitignoreDest, { force: true });
337
+ await fs.rename(gitignoreSrc, gitignoreDest);
338
+ return;
339
+ }
340
+ throw err;
341
+ });
342
+ }
343
+
344
+ const pkgPath = path.join(targetPath, 'package.json');
345
+ const pkgRaw = await fs.readFile(pkgPath, 'utf8');
346
+ const pkg = JSON.parse(pkgRaw);
347
+ pkg.name = toValidPackageName(targetBaseName);
348
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
349
+
350
+ const readmePath = path.join(targetPath, 'README.md');
351
+ if (await fileExists(readmePath)) {
352
+ const raw = await fs.readFile(readmePath, 'utf8');
353
+ const lines = raw.split(/\\r?\\n/);
354
+ lines[0] = `# ${toTitle(targetBaseName)}`;
355
+ await fs.writeFile(readmePath, lines.join('\n'), 'utf8');
356
+ }
357
+
358
+ if (shouldInstall) {
359
+ const bin = platformBin(pm);
360
+ await run(bin, ['install'], { cwd: targetPath });
361
+ }
362
+
363
+ if (shouldInstallSkills) {
364
+ const npx = platformBin('npx');
365
+ try {
366
+ await run(
367
+ npx,
368
+ ['--yes', 'skills', 'add', imagineSkillsRepo, '--skill', imagineSkillsName, '-y'],
369
+ { cwd: targetPath }
370
+ );
371
+ } catch (err) {
372
+ // eslint-disable-next-line no-console
373
+ console.error('\nFailed to install skills (optional). You can run this later:\n');
374
+ // eslint-disable-next-line no-console
375
+ console.error(` npx skills add ${imagineSkillsRepo} --skill ${imagineSkillsName}\n`);
376
+ }
377
+ }
378
+
379
+ const cdPath = path.isAbsolute(targetDirInput) ? targetPath : targetDirInput;
380
+
381
+ // eslint-disable-next-line no-console
382
+ console.log('\nDone.\n');
383
+ // eslint-disable-next-line no-console
384
+ console.log('Next steps:');
385
+ // eslint-disable-next-line no-console
386
+ console.log(` cd ${cdPath}`);
387
+ if (!shouldInstall) {
388
+ // eslint-disable-next-line no-console
389
+ console.log(' npm install');
390
+ }
391
+ // eslint-disable-next-line no-console
392
+ console.log(' npm run dev');
393
+ // eslint-disable-next-line no-console
394
+ console.log(' npm run render -- --project example');
395
+ // eslint-disable-next-line no-console
396
+ console.log(`\nOptional: npx skills add ${imagineSkillsRepo} --skill ${imagineSkillsName}`);
397
+ } finally {
398
+ if (rl) rl.close();
399
+ }
400
+ }
401
+
402
+ main().catch((err) => {
403
+ if (String(err?.code ?? '') === 'USAGE') usage();
404
+ // eslint-disable-next-line no-console
405
+ console.error(err instanceof Error ? err.message : err);
406
+ process.exitCode = 1;
407
+ });
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "create-imagine",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a new Imagine project (React → scientific figures).",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "create-imagine": "bin/create-imagine.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "templates",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18"
18
+ }
19
+ }
@@ -0,0 +1,96 @@
1
+ # Imagine
2
+
3
+ React → scientific figures (PNG + SVG) with a live Studio and a Playwright renderer.
4
+
5
+ ## Quickstart
6
+
7
+ ```bash
8
+ npm install
9
+ npm run dev
10
+ ```
11
+
12
+ Open the Studio at http://localhost:5173
13
+
14
+ ## Projects
15
+
16
+ Figures are organized into projects under `projects/<id>/`.
17
+
18
+ The Studio home shows projects. Clicking a project shows:
19
+ - a preview gallery (static images from `public/`)
20
+ - the live React-generated figures for that project
21
+
22
+ ```bash
23
+ npm run list
24
+ ```
25
+
26
+ List one project’s figures:
27
+
28
+ ```bash
29
+ npm run list -- --project example
30
+ ```
31
+
32
+ ## Render exports
33
+
34
+ ```bash
35
+ npm run render -- --project example
36
+ ```
37
+
38
+ By default outputs are written to `out/<projectId>/`:
39
+ - `out/<projectId>/<figure>--<variant>.png`
40
+ - `out/<projectId>/<figure>--<variant>.svg`
41
+ - `out/<projectId>/manifest.json`
42
+
43
+ ### Render options
44
+
45
+ ```bash
46
+ # Only one figure
47
+ npm run render -- --project example --fig line-chart
48
+
49
+ # Only one variant
50
+ npm run render -- --project example --fig hello-world --variant transparent
51
+
52
+ # Dev-mode rendering (requires `npm run dev` already running)
53
+ npm run render:dev -- --project example
54
+ ```
55
+
56
+ Common flags:
57
+ - `--project <id>`
58
+ - `--fig <id>` (repeatable or comma-separated)
59
+ - `--variant <id>` (repeatable or comma-separated)
60
+ - `--formats png,svg`
61
+ - `--out <dir>`
62
+ - `--mode build|dev`
63
+ - `--url http://localhost:5173` (dev mode)
64
+ - `--no-props` (ignore Studio-saved overrides)
65
+
66
+ ## Text editing (Studio Controls)
67
+
68
+ When you edit figure text in Studio, it saves overrides into:
69
+ - `projects/<projectId>/props.json`
70
+
71
+ `npm run render` automatically uses those overrides (unless `--no-props`).
72
+
73
+ ## Create a new project
74
+
75
+ 1. Copy `projects/example/` to `projects/<your-id>/`
76
+ 2. Update `projects/<your-id>/project.ts` and `projects/<your-id>/manifest.ts`
77
+ 3. Restart `npm run dev` (project discovery is build-time)
78
+
79
+ ## Create a new figure (inside a project)
80
+
81
+ 1. Add a new file in `projects/<projectId>/figures/` that default-exports a React component whose root is an `<svg>`.
82
+ 2. Register it in `projects/<projectId>/manifest.ts` with an `id`, `title`, `moduleKey`, and at least one variant.
83
+ 3. Optionally add preview images under `public/projects/<projectId>/previews/` and reference them from `projects/<projectId>/project.ts`.
84
+
85
+ Notes:
86
+ - `moduleKey` must match the figure filename (without `.tsx`). Example: `projects/example/figures/line-chart.tsx` → `moduleKey: "line-chart"`.
87
+ - Sizes can be specified as pixels or paper-friendly `mm + dpi` (converted to px for rendering).
88
+ - MathJax is loaded on-demand from CDN by default. Override with `VITE_MATHJAX_URL` if needed.
89
+
90
+ ## Generate example previews
91
+
92
+ ```bash
93
+ npm run render:previews
94
+ ```
95
+
96
+ This writes PNGs to `public/projects/example/previews/`.
@@ -0,0 +1,6 @@
1
+ node_modules
2
+ dist
3
+ out
4
+ .DS_Store
5
+ *.log
6
+
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Imagine Studio</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
13
+
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "imagine-project",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview",
10
+ "list": "tsx scripts/list.ts",
11
+ "render": "tsx scripts/render.ts",
12
+ "render:dev": "tsx scripts/render.ts --mode dev",
13
+ "render:previews": "tsx scripts/render.ts --project example --variant default --formats png --no-props --no-manifest --out public/projects/example/previews",
14
+ "typecheck": "tsc --noEmit",
15
+ "test": "vitest run",
16
+ "test:watch": "vitest"
17
+ },
18
+ "dependencies": {
19
+ "d3-array": "^3.2.4",
20
+ "d3-format": "^3.1.0",
21
+ "d3-scale": "^4.0.2",
22
+ "d3-shape": "^3.2.0",
23
+ "react": "^18.3.1",
24
+ "react-dom": "^18.3.1",
25
+ "react-router-dom": "^6.28.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/d3-array": "^3.2.2",
29
+ "@types/d3-format": "^3.0.4",
30
+ "@types/d3-scale": "^4.0.9",
31
+ "@types/d3-shape": "^3.1.8",
32
+ "@types/node": "^22.10.7",
33
+ "@types/react": "^18.3.12",
34
+ "@types/react-dom": "^18.3.1",
35
+ "@vitejs/plugin-react": "^4.3.4",
36
+ "playwright": "^1.49.1",
37
+ "tsx": "^4.19.2",
38
+ "typescript": "^5.7.2",
39
+ "vite": "^6.0.6",
40
+ "vitest": "^2.1.8"
41
+ }
42
+ }
@@ -0,0 +1,45 @@
1
+ import { Figure } from '@/framework/Figure';
2
+ import type { FigureComponentBaseProps } from '@/framework/types';
3
+ import { theme } from '@/framework/theme';
4
+
5
+ export default function HelloWorldFigure({
6
+ width,
7
+ height,
8
+ background,
9
+ heading = 'Imagine',
10
+ subtitle = 'React components → scientific figures (PNG + SVG)',
11
+ tipHeading = 'Tips',
12
+ tip1 = 'Edit the figure component and watch this update live.',
13
+ tip2 = 'Use the Controls panel to adjust text, then export via `npm run render`.'
14
+ }: FigureComponentBaseProps & {
15
+ heading?: string;
16
+ subtitle?: string;
17
+ tipHeading?: string;
18
+ tip1?: string;
19
+ tip2?: string;
20
+ }) {
21
+ return (
22
+ <Figure width={width} height={height} background={background} title="Hello world">
23
+ <g>
24
+ <text x={40} y={70} fontSize={34} fontWeight={700} fill={theme.colors.text}>
25
+ {heading}
26
+ </text>
27
+ <text x={40} y={110} fontSize={16} fill={theme.colors.subtleText}>
28
+ {subtitle}
29
+ </text>
30
+
31
+ <rect x={40} y={150} width={width - 80} height={height - 190} rx={theme.radii.md} fill={theme.colors.panel} />
32
+
33
+ <text x={70} y={210} fontSize={16} fill={theme.colors.text} fontWeight={600}>
34
+ {tipHeading}
35
+ </text>
36
+ <text x={70} y={245} fontSize={14} fill={theme.colors.text}>
37
+ • {tip1}
38
+ </text>
39
+ <text x={70} y={270} fontSize={14} fill={theme.colors.text}>
40
+ • {tip2}
41
+ </text>
42
+ </g>
43
+ </Figure>
44
+ );
45
+ }