@svgrid/create 2.5.0 → 2.7.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.
- package/README.md +16 -7
- package/index.mjs +482 -403
- package/package.json +2 -2
- package/templates/admin-dashboard/src/app.css +184 -184
- package/templates/headless/README.md +65 -0
- package/templates/headless/_gitignore +4 -0
- package/templates/headless/_package.json +22 -0
- package/templates/headless/index.html +12 -0
- package/templates/headless/src/App.svelte +159 -0
- package/templates/headless/src/app.css +153 -0
- package/templates/headless/src/main.ts +7 -0
- package/templates/headless/src/vite-env.d.ts +2 -0
- package/templates/headless/svelte.config.js +5 -0
- package/templates/headless/tsconfig.json +14 -0
- package/templates/headless/vite.config.js +6 -0
- package/templates/minimal/src/app.css +1 -1
- package/templates/sveltekit/README.md +44 -0
- package/templates/sveltekit/_gitignore +23 -0
- package/templates/sveltekit/_package.json +26 -0
- package/templates/sveltekit/src/app.css +15 -0
- package/templates/sveltekit/src/app.d.ts +13 -0
- package/templates/sveltekit/src/app.html +22 -0
- package/templates/sveltekit/src/lib/people.ts +22 -0
- package/templates/sveltekit/src/lib/theme.svelte.ts +87 -0
- package/templates/sveltekit/src/routes/+layout.svelte +78 -0
- package/templates/sveltekit/src/routes/+page.svelte +7 -0
- package/templates/sveltekit/src/routes/people/+page.server.ts +16 -0
- package/templates/sveltekit/src/routes/people/+page.svelte +51 -0
- package/templates/sveltekit/tsconfig.json +20 -0
- package/templates/sveltekit/vite.config.ts +20 -0
package/index.mjs
CHANGED
|
@@ -1,403 +1,482 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// @svgrid/create - scaffold a Svelte app powered by SvGrid.
|
|
3
|
-
//
|
|
4
|
-
// npm create @svgrid@latest # interactive
|
|
5
|
-
// pnpm create @svgrid # interactive
|
|
6
|
-
// npm create @svgrid@latest my-app -- --template admin-dashboard
|
|
7
|
-
// npm create @svgrid@latest my-app -- -t minimal
|
|
8
|
-
//
|
|
9
|
-
// Zero runtime dependencies - Node built-ins only. Copies a bundled template,
|
|
10
|
-
// renames `_`-prefixed dotfiles, and rewrites the project name.
|
|
11
|
-
|
|
12
|
-
import { cp, mkdir, readdir, rename, stat, readFile, writeFile } from 'node:fs/promises'
|
|
13
|
-
import { existsSync } from 'node:fs'
|
|
14
|
-
import { dirname, join, basename, resolve, isAbsolute } from 'node:path'
|
|
15
|
-
import { fileURLToPath } from 'node:url'
|
|
16
|
-
import { createInterface } from 'node:readline/promises'
|
|
17
|
-
import { stdin, stdout } from 'node:process'
|
|
18
|
-
|
|
19
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
20
|
-
|
|
21
|
-
const TEMPLATES = {
|
|
22
|
-
minimal: {
|
|
23
|
-
label: 'Minimal - Vite + Svelte 5 + SvGrid, one page',
|
|
24
|
-
bundled: join(__dirname, 'templates', 'minimal'),
|
|
25
|
-
},
|
|
26
|
-
|
|
27
|
-
label: '
|
|
28
|
-
bundled: join(__dirname, 'templates', '
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
let
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
:
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
})
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @svgrid/create - scaffold a Svelte app powered by SvGrid.
|
|
3
|
+
//
|
|
4
|
+
// npm create @svgrid@latest # interactive
|
|
5
|
+
// pnpm create @svgrid # interactive
|
|
6
|
+
// npm create @svgrid@latest my-app -- --template admin-dashboard
|
|
7
|
+
// npm create @svgrid@latest my-app -- -t minimal
|
|
8
|
+
//
|
|
9
|
+
// Zero runtime dependencies - Node built-ins only. Copies a bundled template,
|
|
10
|
+
// renames `_`-prefixed dotfiles, and rewrites the project name.
|
|
11
|
+
|
|
12
|
+
import { cp, mkdir, readdir, rename, stat, readFile, writeFile } from 'node:fs/promises'
|
|
13
|
+
import { existsSync } from 'node:fs'
|
|
14
|
+
import { dirname, join, basename, resolve, isAbsolute } from 'node:path'
|
|
15
|
+
import { fileURLToPath } from 'node:url'
|
|
16
|
+
import { createInterface } from 'node:readline/promises'
|
|
17
|
+
import { stdin, stdout } from 'node:process'
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
20
|
+
|
|
21
|
+
const TEMPLATES = {
|
|
22
|
+
minimal: {
|
|
23
|
+
label: 'Minimal - Vite + Svelte 5 + SvGrid, one page',
|
|
24
|
+
bundled: join(__dirname, 'templates', 'minimal'),
|
|
25
|
+
},
|
|
26
|
+
sveltekit: {
|
|
27
|
+
label: 'SvelteKit - server load, URL-driven sort, form-action edits, theme picker',
|
|
28
|
+
bundled: join(__dirname, 'templates', 'sveltekit'),
|
|
29
|
+
},
|
|
30
|
+
'admin-dashboard': {
|
|
31
|
+
label: 'Admin dashboard - SvelteKit shell, multiple grids, deploy to Vercel',
|
|
32
|
+
bundled: join(__dirname, 'templates', 'admin-dashboard'),
|
|
33
|
+
// When running from the monorepo before `prepack` has synced the bundled
|
|
34
|
+
// copy, fall back to the canonical source.
|
|
35
|
+
fallback: join(__dirname, '..', '..', 'templates', 'sveltekit-admin-dashboard'),
|
|
36
|
+
},
|
|
37
|
+
headless: {
|
|
38
|
+
label: 'Headless - createSvGrid engine, your own <table> and CSS',
|
|
39
|
+
bundled: join(__dirname, 'templates', 'headless'),
|
|
40
|
+
// No <SvGrid>, so no preset stylesheet and nothing for --theme to write to.
|
|
41
|
+
// Asking the theme questions here would collect two answers and apply
|
|
42
|
+
// neither.
|
|
43
|
+
theme: false,
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const RENAME_BACK = new Map([
|
|
48
|
+
['_gitignore', '.gitignore'],
|
|
49
|
+
['_npmrc', '.npmrc'],
|
|
50
|
+
['_env.example', '.env.example'],
|
|
51
|
+
['_package.json', 'package.json'],
|
|
52
|
+
])
|
|
53
|
+
|
|
54
|
+
const c = {
|
|
55
|
+
reset: '\x1b[0m',
|
|
56
|
+
bold: '\x1b[1m',
|
|
57
|
+
dim: '\x1b[2m',
|
|
58
|
+
green: '\x1b[32m',
|
|
59
|
+
cyan: '\x1b[36m',
|
|
60
|
+
red: '\x1b[31m',
|
|
61
|
+
yellow: '\x1b[33m',
|
|
62
|
+
}
|
|
63
|
+
const color = stdout.isTTY ? (k, s) => `${c[k]}${s}${c.reset}` : (_k, s) => s
|
|
64
|
+
|
|
65
|
+
function parseArgs(argv) {
|
|
66
|
+
const args = { _: [], template: null, force: false, help: false, theme: null, mode: null }
|
|
67
|
+
for (let i = 0; i < argv.length; i++) {
|
|
68
|
+
const a = argv[i]
|
|
69
|
+
if (a === '--help' || a === '-h') args.help = true
|
|
70
|
+
else if (a === '--force' || a === '-f') args.force = true
|
|
71
|
+
else if (a === '--template' || a === '-t') args.template = argv[++i]
|
|
72
|
+
else if (a.startsWith('--template=')) args.template = a.slice('--template='.length)
|
|
73
|
+
else if (a === '--theme') args.theme = argv[++i]
|
|
74
|
+
else if (a.startsWith('--theme=')) args.theme = a.slice('--theme='.length)
|
|
75
|
+
else if (a === '--dark') args.mode = 'dark'
|
|
76
|
+
else if (a === '--light') args.mode = 'light'
|
|
77
|
+
else if (!a.startsWith('-')) args._.push(a)
|
|
78
|
+
}
|
|
79
|
+
return args
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function printHelp() {
|
|
83
|
+
stdout.write(`
|
|
84
|
+
${color('bold', '@svgrid/create')} - scaffold a Svelte app powered by SvGrid
|
|
85
|
+
|
|
86
|
+
${color('bold', 'Usage')}
|
|
87
|
+
npm create @svgrid@latest [dir] -- [--template <name>] [--force]
|
|
88
|
+
|
|
89
|
+
${color('bold', 'Templates')}
|
|
90
|
+
${Object.entries(TEMPLATES)
|
|
91
|
+
.map(([k, t]) => ` ${color('cyan', k.padEnd(16))} ${t.label.replace(/^\S+\s+-\s+/, '')}`)
|
|
92
|
+
.join('\n')}
|
|
93
|
+
|
|
94
|
+
${color('bold', 'Options')}
|
|
95
|
+
--theme <id> One of: ${THEME_IDS.join(', ')} (default: ember).
|
|
96
|
+
Ignored by headless, which ships no stylesheet to theme.
|
|
97
|
+
--dark / --light Pin the starting mode. Left out, minimal and sveltekit
|
|
98
|
+
follow the visitor's OS; admin-dashboard starts dark.
|
|
99
|
+
--force Scaffold into a non-empty directory.
|
|
100
|
+
|
|
101
|
+
${color('bold', 'Examples')}
|
|
102
|
+
npm create @svgrid@latest
|
|
103
|
+
npm create @svgrid@latest my-app -- --template admin-dashboard
|
|
104
|
+
npm create @svgrid@latest my-app -- -t admin-dashboard --theme material --light
|
|
105
|
+
pnpm create @svgrid my-app -t minimal
|
|
106
|
+
`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const THEME_IDS = [
|
|
110
|
+
'ember', 'shadcn', 'tailwind', 'material', 'excel', 'fluent', 'carbon', 'sap',
|
|
111
|
+
'salesforce', 'atlassian', 'github', 'antd', 'ag-alpine', 'bootstrap',
|
|
112
|
+
'vercel', 'linear', 'notion', 'nord', 'dracula', 'catppuccin',
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Resolve the theme + mode to apply: from `--theme` / `--dark` / `--light`
|
|
117
|
+
* flags, or (in a TTY) an interactive prompt; otherwise left untouched (the
|
|
118
|
+
* template's own default look stands). Reuses `@svgrid/grid/themes`'s own
|
|
119
|
+
* preset list, so the picker can never drift from the canonical set.
|
|
120
|
+
*/
|
|
121
|
+
async function promptTheme(args, ask, interactive) {
|
|
122
|
+
let themes
|
|
123
|
+
try {
|
|
124
|
+
themes = await import('@svgrid/grid/themes')
|
|
125
|
+
} catch {
|
|
126
|
+
return null // theming is best-effort; the template's own default stands in.
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let themeId = args.theme ? args.theme.trim().toLowerCase() : null
|
|
130
|
+
if (themeId && !themes.getThemePreset(themeId)) {
|
|
131
|
+
stdout.write(`${color('yellow', '!')} Unknown theme "${themeId}" - using ember. (${THEME_IDS.join(', ')})\n`)
|
|
132
|
+
themeId = null
|
|
133
|
+
}
|
|
134
|
+
if (!themeId && interactive) {
|
|
135
|
+
const list = themes.themePresets.map((t, i) => ` ${i + 1}. ${t.name} ${color('dim', `(${t.id})`)}`).join('\n')
|
|
136
|
+
stdout.write(`\n${color('bold', 'Theme')}\n${list}\n`)
|
|
137
|
+
const choice = await ask('Pick a number or id:', 'ember')
|
|
138
|
+
const byIndex = themes.themePresets[Number(choice) - 1]
|
|
139
|
+
themeId = byIndex ? byIndex.id : (themes.getThemePreset(choice.trim().toLowerCase())?.id ?? 'ember')
|
|
140
|
+
}
|
|
141
|
+
themeId ??= 'ember'
|
|
142
|
+
|
|
143
|
+
let mode = args.mode
|
|
144
|
+
if (!mode && interactive) {
|
|
145
|
+
const a = await ask('Light or dark mode? (light/dark)', 'dark')
|
|
146
|
+
mode = /^l(ight)?$/i.test(a) ? 'light' : 'dark'
|
|
147
|
+
}
|
|
148
|
+
// Whether the mode was actually chosen, as opposed to falling through to the
|
|
149
|
+
// default below. minimal follows the OS when nobody said, which is only
|
|
150
|
+
// distinguishable from an explicit --dark by keeping this flag.
|
|
151
|
+
const explicitMode = mode != null
|
|
152
|
+
mode ??= 'dark' // matches the template's existing default - only patched when 'light' is chosen.
|
|
153
|
+
|
|
154
|
+
return { themeId, mode, explicitMode, name: themes.getThemePreset(themeId)?.name ?? themeId, themes }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Rewrite `app.css` between the `svgrid-theme` markers.
|
|
158
|
+
*
|
|
159
|
+
* The two templates want different things there. admin-dashboard inlines the
|
|
160
|
+
* resolved `--sg-*` tokens, because its Tailwind layer aliases them to
|
|
161
|
+
* `--app-*` and having the values on the page makes them editable. minimal
|
|
162
|
+
* just points at the preset stylesheet the package already ships, which keeps
|
|
163
|
+
* its `app.css` short enough to read in one go. Which one we are looking at
|
|
164
|
+
* is decided by what the marked block currently holds, not by the template
|
|
165
|
+
* name, so a hand-edited project still round-trips. */
|
|
166
|
+
async function applyTheme(destDir, choice) {
|
|
167
|
+
if (!choice) return
|
|
168
|
+
const { themeId, themes } = choice
|
|
169
|
+
const preset = themes.getThemePreset(themeId) ?? themes.defaultThemePreset
|
|
170
|
+
const light = themes.resolveThemeTokens(preset, 'light')
|
|
171
|
+
const dark = themes.resolveThemeTokens(preset, 'dark')
|
|
172
|
+
const block = (selector, tokens, scheme) => {
|
|
173
|
+
const lines = Object.entries(tokens).map(([k, v]) => ` ${k}: ${v};`).join('\n')
|
|
174
|
+
return `${selector} {\n${lines}\n color-scheme: ${scheme};\n}`
|
|
175
|
+
}
|
|
176
|
+
const tokenBlocks = `${block(':root', light, 'light')}\n${block(":root[data-theme='dark']", dark, 'dark')}`
|
|
177
|
+
|
|
178
|
+
const cssPath = join(destDir, 'src', 'app.css')
|
|
179
|
+
const cssText = await readFile(cssPath, 'utf8').catch(() => null)
|
|
180
|
+
if (cssText == null) return
|
|
181
|
+
const marked = /\/\* svgrid-theme:start \*\/([\s\S]*?)\/\* svgrid-theme:end \*\//
|
|
182
|
+
// A CSS @import has to stay at the top of the file, so where the block holds
|
|
183
|
+
// one we swap the preset id in place instead of replacing it with token
|
|
184
|
+
// declarations that would then sit above the import.
|
|
185
|
+
const held = cssText.match(marked)
|
|
186
|
+
const css = held && /@import\s/.test(held[1])
|
|
187
|
+
? `@import '@svgrid/grid/themes/${preset.id}.css';`
|
|
188
|
+
: tokenBlocks
|
|
189
|
+
if (marked.test(cssText)) {
|
|
190
|
+
await writeFile(cssPath, cssText.replace(marked, `/* svgrid-theme:start */\n${css}\n/* svgrid-theme:end */`))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Templates with a RUNTIME theme picker keep the starting selection in TS, so
|
|
194
|
+
// the picker opens on the theme that was scaffolded rather than disagreeing
|
|
195
|
+
// with the stylesheet above. Absent in templates without a picker, where the
|
|
196
|
+
// missing file makes this a no-op.
|
|
197
|
+
const tsPath = join(destDir, 'src', 'lib', 'theme.svelte.ts')
|
|
198
|
+
const tsText = await readFile(tsPath, 'utf8').catch(() => null)
|
|
199
|
+
if (tsText == null) return
|
|
200
|
+
const tsMarked = /\/\* svgrid-initial-theme:start \*\/([\s\S]*?)\/\* svgrid-initial-theme:end \*\//
|
|
201
|
+
if (!tsMarked.test(tsText)) return
|
|
202
|
+
// Only the preset here. The mode is left to applyModeSvelteKit, which pins it
|
|
203
|
+
// only when one was actually asked for.
|
|
204
|
+
const body = tsText.match(tsMarked)[1].replace(
|
|
205
|
+
/export const INITIAL_THEME = '[^']*'/,
|
|
206
|
+
`export const INITIAL_THEME = '${preset.id}'`,
|
|
207
|
+
)
|
|
208
|
+
await writeFile(
|
|
209
|
+
tsPath,
|
|
210
|
+
tsText.replace(
|
|
211
|
+
tsMarked,
|
|
212
|
+
`/* svgrid-initial-theme:start */${body}/* svgrid-initial-theme:end */`,
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** sveltekit settles its start mode in an inline script in `src/app.html`,
|
|
218
|
+
* falling back to the OS preference, exactly as minimal does. Pin it only when
|
|
219
|
+
* light or dark was actually asked for. The picker in the layout reads the
|
|
220
|
+
* attribute that script sets, so both halves have to agree. */
|
|
221
|
+
async function applyModeSvelteKit(destDir, choice) {
|
|
222
|
+
if (!choice || !choice.explicitMode) return
|
|
223
|
+
|
|
224
|
+
const htmlPath = join(destDir, 'src', 'app.html')
|
|
225
|
+
const html = await readFile(htmlPath, 'utf8').catch(() => null)
|
|
226
|
+
if (html != null) {
|
|
227
|
+
await writeFile(
|
|
228
|
+
htmlPath,
|
|
229
|
+
html
|
|
230
|
+
.replace(`<html lang="en" data-theme="light">`, `<html lang="en" data-theme="${choice.mode}">`)
|
|
231
|
+
.replace(
|
|
232
|
+
`var fallback = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'`,
|
|
233
|
+
`var fallback = '${choice.mode}'`,
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const tsPath = join(destDir, 'src', 'lib', 'theme.svelte.ts')
|
|
239
|
+
const ts = await readFile(tsPath, 'utf8').catch(() => null)
|
|
240
|
+
if (ts == null) return
|
|
241
|
+
await writeFile(
|
|
242
|
+
tsPath,
|
|
243
|
+
ts.replace(
|
|
244
|
+
/export const INITIAL_MODE: ThemeMode = '[^']*'/,
|
|
245
|
+
`export const INITIAL_MODE: ThemeMode = '${choice.mode}'`,
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** admin-dashboard defaults to dark (see app.html / src/lib/theme.ts /
|
|
251
|
+
* +layout.svelte). Flip all three to light-by-default when 'light' is chosen
|
|
252
|
+
* - a no-op when 'dark' is chosen, since that already matches the template. */
|
|
253
|
+
async function applyMode(destDir, choice) {
|
|
254
|
+
if (!choice || choice.mode !== 'light') return
|
|
255
|
+
|
|
256
|
+
const htmlPath = join(destDir, 'src', 'app.html')
|
|
257
|
+
const html = await readFile(htmlPath, 'utf8').catch(() => null)
|
|
258
|
+
if (html != null) {
|
|
259
|
+
await writeFile(
|
|
260
|
+
htmlPath,
|
|
261
|
+
html
|
|
262
|
+
.replace(`data-theme="dark"`, `data-theme="light"`)
|
|
263
|
+
.replace(`t === 'light' ? 'light' : 'dark'`, `t === 'dark' ? 'dark' : 'light'`),
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const themeTsPath = join(destDir, 'src', 'lib', 'theme.ts')
|
|
268
|
+
const themeTs = await readFile(themeTsPath, 'utf8').catch(() => null)
|
|
269
|
+
if (themeTs != null) {
|
|
270
|
+
await writeFile(
|
|
271
|
+
themeTsPath,
|
|
272
|
+
themeTs
|
|
273
|
+
.replace('Defaults to dark (matches app.html).', 'Defaults to light (matches app.html).')
|
|
274
|
+
.replace(`if (!browser) return 'dark'`, `if (!browser) return 'light'`)
|
|
275
|
+
.replace(
|
|
276
|
+
`return localStorage.getItem('theme') === 'light' ? 'light' : 'dark'`,
|
|
277
|
+
`return localStorage.getItem('theme') === 'dark' ? 'dark' : 'light'`,
|
|
278
|
+
),
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const layoutPath = join(destDir, 'src', 'routes', '+layout.svelte')
|
|
283
|
+
const layout = await readFile(layoutPath, 'utf8').catch(() => null)
|
|
284
|
+
if (layout != null) {
|
|
285
|
+
await writeFile(
|
|
286
|
+
layoutPath,
|
|
287
|
+
layout
|
|
288
|
+
.replace('// Theme: dark by default (see app.html), toggleable + persisted.', '// Theme: light by default (see app.html), toggleable + persisted.')
|
|
289
|
+
.replace(`let theme = $state<Theme>('dark')`, `let theme = $state<Theme>('light')`),
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** minimal picks its start mode in an inline script in `index.html`, falling
|
|
295
|
+
* back to the OS preference. Pin it only when light or dark was actually asked
|
|
296
|
+
* for - following the OS is the better default for anyone who did not say. */
|
|
297
|
+
async function applyModeMinimal(destDir, choice) {
|
|
298
|
+
if (!choice || !choice.explicitMode) return
|
|
299
|
+
|
|
300
|
+
const htmlPath = join(destDir, 'index.html')
|
|
301
|
+
const html = await readFile(htmlPath, 'utf8').catch(() => null)
|
|
302
|
+
if (html == null) return
|
|
303
|
+
|
|
304
|
+
await writeFile(
|
|
305
|
+
htmlPath,
|
|
306
|
+
html
|
|
307
|
+
.replace(`<html lang="en" data-theme="light">`, `<html lang="en" data-theme="${choice.mode}">`)
|
|
308
|
+
.replace(
|
|
309
|
+
`var fallback = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'`,
|
|
310
|
+
`var fallback = '${choice.mode}'`,
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function sanitizeName(name) {
|
|
316
|
+
return (
|
|
317
|
+
name
|
|
318
|
+
.trim()
|
|
319
|
+
.toLowerCase()
|
|
320
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
321
|
+
.replace(/^[-_.]+|[-_.]+$/g, '') || 'sv-grid-app'
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function resolveTemplateDir(key) {
|
|
326
|
+
const t = TEMPLATES[key]
|
|
327
|
+
if (existsSync(t.bundled)) return t.bundled
|
|
328
|
+
if (t.fallback && existsSync(t.fallback)) return t.fallback
|
|
329
|
+
return null
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function copyTemplate(srcDir, destDir) {
|
|
333
|
+
// Match only on the path RELATIVE to the template root. The absolute `src`
|
|
334
|
+
// includes the install location (e.g. `.../node_modules/@svgrid/create/...`),
|
|
335
|
+
// so testing it directly would match `node_modules` and skip the whole copy.
|
|
336
|
+
const skip = /(^|[\\/])(node_modules|\.svelte-kit|\.vercel|build|dist)([\\/]|$)/
|
|
337
|
+
await cp(srcDir, destDir, {
|
|
338
|
+
recursive: true,
|
|
339
|
+
filter: (src) => !skip.test(src.slice(srcDir.length)),
|
|
340
|
+
})
|
|
341
|
+
// Rename `_`-prefixed files back to their real dotfile names.
|
|
342
|
+
await renameBack(destDir)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function renameBack(dir) {
|
|
346
|
+
for (const entry of await readdir(dir)) {
|
|
347
|
+
const full = join(dir, entry)
|
|
348
|
+
if ((await stat(full)).isDirectory()) {
|
|
349
|
+
await renameBack(full)
|
|
350
|
+
} else if (RENAME_BACK.has(entry)) {
|
|
351
|
+
await rename(full, join(dir, RENAME_BACK.get(entry)))
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function setProjectName(destDir, name) {
|
|
357
|
+
const pkgPath = join(destDir, 'package.json')
|
|
358
|
+
if (!existsSync(pkgPath)) return
|
|
359
|
+
try {
|
|
360
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
361
|
+
pkg.name = name
|
|
362
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
363
|
+
} catch {
|
|
364
|
+
// leave the template's name if it isn't valid JSON for some reason
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function isEmptyDir(dir) {
|
|
369
|
+
if (!existsSync(dir)) return true
|
|
370
|
+
const entries = await readdir(dir)
|
|
371
|
+
return entries.filter((e) => e !== '.git').length === 0
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function main() {
|
|
375
|
+
const args = parseArgs(process.argv.slice(2))
|
|
376
|
+
if (args.help) return printHelp()
|
|
377
|
+
|
|
378
|
+
const interactive = stdin.isTTY && stdout.isTTY
|
|
379
|
+
let rl = null
|
|
380
|
+
const ask = async (q, def) => {
|
|
381
|
+
if (!interactive) return def
|
|
382
|
+
rl ??= createInterface({ input: stdin, output: stdout })
|
|
383
|
+
const a = (await rl.question(`${q} ${color('dim', `(${def})`)} `)).trim()
|
|
384
|
+
return a || def
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
stdout.write(`\n${color('bold', '◆ @svgrid/create')} ${color('dim', 'Svelte + SvGrid')}\n\n`)
|
|
388
|
+
|
|
389
|
+
// 1. Target directory.
|
|
390
|
+
let target = args._[0]
|
|
391
|
+
if (!target) target = await ask('Project directory:', 'my-sv-grid-app')
|
|
392
|
+
const destDir = resolve(process.cwd(), target)
|
|
393
|
+
const projectName = sanitizeName(basename(destDir))
|
|
394
|
+
|
|
395
|
+
// 2. Template.
|
|
396
|
+
let template = args.template
|
|
397
|
+
if (!template) {
|
|
398
|
+
if (interactive) {
|
|
399
|
+
stdout.write(`\n Templates:\n`)
|
|
400
|
+
Object.entries(TEMPLATES).forEach(([, t], i) => {
|
|
401
|
+
stdout.write(` ${color('cyan', String(i + 1))}. ${t.label}\n`)
|
|
402
|
+
})
|
|
403
|
+
const pick = await ask(`\nChoose a template (1-${Object.keys(TEMPLATES).length}):`, '1')
|
|
404
|
+
template = Object.keys(TEMPLATES)[Number(pick) - 1] ?? 'minimal'
|
|
405
|
+
} else {
|
|
406
|
+
template = 'minimal'
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (!TEMPLATES[template]) {
|
|
410
|
+
if (rl) rl.close()
|
|
411
|
+
stdout.write(
|
|
412
|
+
`\n${color('red', '✖')} Unknown template "${template}". Choose: ${Object.keys(TEMPLATES).join(', ')}\n`,
|
|
413
|
+
)
|
|
414
|
+
process.exit(1)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 3. Safety: don't clobber a non-empty directory.
|
|
418
|
+
if (!(await isEmptyDir(destDir)) && !args.force) {
|
|
419
|
+
const ok = await ask(
|
|
420
|
+
`\n${color('yellow', '!')} ${target} is not empty. Continue and overwrite files? (y/N)`,
|
|
421
|
+
'N',
|
|
422
|
+
)
|
|
423
|
+
if (!/^y(es)?$/i.test(ok)) {
|
|
424
|
+
if (rl) rl.close()
|
|
425
|
+
stdout.write(`${color('red', '✖')} Aborted.\n`)
|
|
426
|
+
process.exit(1)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const srcDir = resolveTemplateDir(template)
|
|
431
|
+
if (!srcDir) {
|
|
432
|
+
if (rl) rl.close()
|
|
433
|
+
stdout.write(
|
|
434
|
+
`\n${color('red', '✖')} Template "${template}" is not available in this build.\n`,
|
|
435
|
+
)
|
|
436
|
+
process.exit(1)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// 3b. Theme + light/dark mode - only for templates that render <SvGrid> and
|
|
440
|
+
// therefore carry a --sg-* palette. headless has no stylesheet to write to,
|
|
441
|
+
// so it is neither asked nor told a theme; say so if flags implied otherwise.
|
|
442
|
+
const themed = TEMPLATES[template].theme !== false
|
|
443
|
+
if (!themed && (args.theme || args.mode)) {
|
|
444
|
+
stdout.write(
|
|
445
|
+
`${color('yellow', '!')} ${template} has no stylesheet to theme - ignoring --theme/--dark/--light.\n`,
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
const themeChoice = themed ? await promptTheme(args, ask, interactive) : null
|
|
449
|
+
if (rl) rl.close()
|
|
450
|
+
|
|
451
|
+
// 4. Scaffold.
|
|
452
|
+
await mkdir(destDir, { recursive: true })
|
|
453
|
+
await copyTemplate(srcDir, destDir)
|
|
454
|
+
await setProjectName(destDir, projectName)
|
|
455
|
+
await applyTheme(destDir, themeChoice)
|
|
456
|
+
await applyMode(destDir, themeChoice)
|
|
457
|
+
await applyModeMinimal(destDir, themeChoice)
|
|
458
|
+
await applyModeSvelteKit(destDir, themeChoice)
|
|
459
|
+
|
|
460
|
+
// 5. Next steps.
|
|
461
|
+
const rel = isAbsolute(target) || target.startsWith('.') ? target : `./${target}`
|
|
462
|
+
stdout.write(`\n${color('green', '✔')} Scaffolded ${color('bold', projectName)} (${template}) into ${rel}\n`)
|
|
463
|
+
if (themeChoice) {
|
|
464
|
+
// minimal and sveltekit only pin a mode when one was asked for; otherwise
|
|
465
|
+
// they read the OS preference at load, so reporting "dark" here would be a
|
|
466
|
+
// lie. admin-dashboard is dark by default either way.
|
|
467
|
+
const pinned = themeChoice.explicitMode || template === 'admin-dashboard'
|
|
468
|
+
const mode = pinned ? themeChoice.mode : 'follows your OS'
|
|
469
|
+
stdout.write(` ${color('dim', 'theme')} ${themeChoice.name} (${mode})\n`)
|
|
470
|
+
}
|
|
471
|
+
stdout.write(`\n`)
|
|
472
|
+
stdout.write(`${color('bold', 'Next steps')}\n`)
|
|
473
|
+
stdout.write(` cd ${target}\n`)
|
|
474
|
+
stdout.write(` npm install\n`)
|
|
475
|
+
stdout.write(` npm run dev\n\n`)
|
|
476
|
+
stdout.write(`${color('dim', 'Docs:')} https://svgrid.com/docs ${color('dim', 'Pro:')} https://svgrid.com/pricing\n\n`)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
main().catch((err) => {
|
|
480
|
+
console.error(err)
|
|
481
|
+
process.exit(1)
|
|
482
|
+
})
|