@svgrid/ui 0.2.0 → 0.3.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 (3) hide show
  1. package/README.md +29 -10
  2. package/index.mjs +272 -39
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,25 +1,43 @@
1
1
  # @svgrid/ui
2
2
 
3
3
  Add [SvGrid UI](https://www.svgrid.com/docs/help/ui-components) components to your
4
- app in one command:
4
+ app - and see them - in one command:
5
5
 
6
6
  ```sh
7
- npx @svgrid/ui add calendar
7
+ npx @svgrid/ui try calendar # open it in a throwaway sandbox, no project needed
8
+ npx @svgrid/ui add calendar # write the recipe into your app + install the dep
8
9
  ```
9
10
 
10
11
  ## How it works
11
12
 
12
13
  `@svgrid/ui` is a **recipe scaffolder**, not a component library. `add` writes a
13
14
  minimal, ready-to-edit `.svelte` starter into your project that imports from
14
- [`@svgrid/grid`](https://www.npmjs.com/package/@svgrid/grid), then makes sure the
15
- package is a dependency and prints the install command. The components themselves
16
- live in `@svgrid/grid` - you get bug fixes and new features by bumping the package,
17
- while the file `add` drops in is yours to style and wire however you like.
15
+ [`@svgrid/grid`](https://www.npmjs.com/package/@svgrid/grid), and installs the
16
+ package for you. The components themselves live in `@svgrid/grid` - you get bug
17
+ fixes and new features by bumping the package, while the file `add` drops in is
18
+ yours to style and wire however you like. Each recipe is a self-contained demo, so
19
+ `try` and `--preview` can render it immediately.
20
+
21
+ ## See it immediately
22
+
23
+ ```sh
24
+ # zero setup: spins up a sandbox and opens the component in your browser
25
+ npx @svgrid/ui try button
26
+
27
+ # in your own SvelteKit app: also writes a /preview/button route
28
+ npx @svgrid/ui add button --preview
29
+ # -> start your dev server, open http://localhost:5173/preview/button
30
+ ```
31
+
32
+ `try` needs no project - it caches a tiny Vite + Svelte sandbox under your temp
33
+ dir (so repeat runs are instant) and opens the browser. `--preview` drops a
34
+ `src/routes/preview/<id>` page (plus a `/preview` index) into an existing
35
+ SvelteKit app so it renders in your running dev server.
18
36
 
19
37
  ## Usage
20
38
 
21
39
  ```sh
22
- # add one component
40
+ # add one component (installs @svgrid/grid)
23
41
  npx @svgrid/ui add calendar
24
42
 
25
43
  # add several, into a custom folder
@@ -28,8 +46,8 @@ npx @svgrid/ui add calendar time-picker --dir src/lib/ui
28
46
  # add a whole family
29
47
  npx @svgrid/ui add date-time
30
48
 
31
- # install the dependency automatically (default: just prints the command)
32
- npx @svgrid/ui add calendar --install
49
+ # just write files, don't run the package manager
50
+ npx @svgrid/ui add calendar --no-install
33
51
 
34
52
  # see what you can add
35
53
  npx @svgrid/ui list
@@ -39,9 +57,10 @@ npx @svgrid/ui list
39
57
 
40
58
  | Flag | Description |
41
59
  | ---------------- | ------------------------------------------------------------------------ |
60
+ | `--preview`, `-p`| (with `add`) Also write a `src/routes/preview/<id>` route so you can see it in your dev server. SvelteKit apps only. |
42
61
  | `--dir <path>` | Where to write files. Default: `src/lib/components/ui` (or `componentsDir` in a project `svgrid.json`). |
43
62
  | `--force` | Overwrite files that already exist. |
44
- | `--install` | Run your package manager to install deps instead of just printing it. |
63
+ | `--no-install` | Skip installing the dependency; just print the install command. |
45
64
 
46
65
  ## Components
47
66
 
package/index.mjs CHANGED
@@ -1,23 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  // @svgrid/ui - add SvGrid UI components to your app, one command at a time.
3
3
  //
4
- // npx @svgrid/ui add calendar
4
+ // npx @svgrid/ui add calendar # add a component (installs the dep)
5
+ // npx @svgrid/ui add button --preview # + a /preview/button route to see it
6
+ // npx @svgrid/ui try button # zero-setup: open it in a sandbox
5
7
  // npx @svgrid/ui add calendar time-picker --dir src/lib/ui
6
- // npx @svgrid/ui add date-time # the whole date/time family
8
+ // npx @svgrid/ui add date-time # the whole date/time family
7
9
  // npx @svgrid/ui list
8
10
  //
9
11
  // Recipe-scaffolder model: `add` writes a minimal, ready-to-EDIT .svelte starter
10
- // that imports from `@svgrid/grid` (which you own and can change), then makes
11
- // sure the package is a dependency and prints the install command. It does NOT
12
- // vendor library source - the components live in `@svgrid/grid`.
12
+ // that imports from `@svgrid/grid` (which you own and can change). Each recipe is
13
+ // a self-contained demo, so `--preview` (in your app) and `try` (in a throwaway
14
+ // sandbox) can render it immediately - "one command and see it".
13
15
  //
14
16
  // Zero runtime dependencies - Node built-ins only.
15
17
 
16
- import { cp, mkdir, readFile, writeFile } from 'node:fs/promises'
18
+ import { cp, mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
17
19
  import { existsSync } from 'node:fs'
18
- import { dirname, join, resolve } from 'node:path'
20
+ import { dirname, join, relative, resolve } from 'node:path'
19
21
  import { fileURLToPath } from 'node:url'
20
- import { stdin, stdout } from 'node:process'
22
+ import { tmpdir } from 'node:os'
23
+ import { stdout } from 'node:process'
21
24
  import { spawnSync } from 'node:child_process'
22
25
 
23
26
  const __dirname = dirname(fileURLToPath(import.meta.url))
@@ -43,12 +46,16 @@ const PMS = [
43
46
  ]
44
47
 
45
48
  function parseArgs(argv) {
46
- const args = { _: [], dir: null, force: false, install: false, help: false, js: false }
49
+ // install defaults ON: an added component imports @svgrid/grid, so it should
50
+ // work right away. `--no-install` opts out (just prints the command).
51
+ const args = { _: [], dir: null, force: false, install: true, preview: false, help: false, js: false }
47
52
  for (let i = 0; i < argv.length; i++) {
48
53
  const a = argv[i]
49
54
  if (a === '--help' || a === '-h') args.help = true
50
55
  else if (a === '--force' || a === '-f') args.force = true
51
56
  else if (a === '--install') args.install = true
57
+ else if (a === '--no-install') args.install = false
58
+ else if (a === '--preview' || a === '-p') args.preview = true
52
59
  else if (a === '--js') args.js = true
53
60
  else if (a === '--ts') args.js = false
54
61
  else if (a === '--dir' || a === '-d') args.dir = argv[++i]
@@ -82,6 +89,25 @@ function expand(registry, token) {
82
89
  return item ? [item] : []
83
90
  }
84
91
 
92
+ /** Resolve tokens -> unique items in order; exits with a helpful error on unknowns. */
93
+ function collectItems(registry, tokens) {
94
+ const items = new Map()
95
+ const unknown = []
96
+ for (const tok of tokens) {
97
+ const matched = expand(registry, tok)
98
+ if (!matched.length) unknown.push(tok)
99
+ for (const it of matched) items.set(it.id, it)
100
+ }
101
+ if (unknown.length) {
102
+ stdout.write(
103
+ `${color('red', '✖')} Unknown component(s): ${unknown.join(', ')}\n` +
104
+ ` See ${color('cyan', 'npx @svgrid/ui list')} for the available set.\n`,
105
+ )
106
+ process.exit(1)
107
+ }
108
+ return [...items.values()]
109
+ }
110
+
85
111
  /** Walk up from `start` to the nearest directory containing a package.json. */
86
112
  function findProjectRoot(start) {
87
113
  let dir = start
@@ -93,6 +119,11 @@ function findProjectRoot(start) {
93
119
  }
94
120
  }
95
121
 
122
+ /** A SvelteKit app has file-based routes, so we can drop in a /preview route. */
123
+ function isSvelteKit(root) {
124
+ return !!root && (existsSync(join(root, 'svelte.config.js')) || existsSync(join(root, 'src', 'routes')))
125
+ }
126
+
96
127
  function detectPm(root) {
97
128
  if (!root) return PMS.find((p) => p.id === 'npm')
98
129
  return PMS.find((p) => existsSync(join(root, p.lock))) ?? PMS.find((p) => p.id === 'npm')
@@ -140,28 +171,123 @@ async function ensureDeps(projectRoot, deps) {
140
171
  return added
141
172
  }
142
173
 
174
+ /** A valid JS identifier for a component id (time-picker -> C_time_picker). */
175
+ function toIdent(id) {
176
+ return 'C_' + id.replace(/[^a-zA-Z0-9]/g, '_')
177
+ }
178
+
179
+ function esc(s) {
180
+ return String(s ?? '').replace(/</g, '&lt;').replace(/>/g, '&gt;')
181
+ }
182
+
183
+ /** A SvelteKit `/preview/<id>` page that renders the recipe you just added, so
184
+ * you can open it in your dev server. Import is relative to the recipe on disk,
185
+ * so it works whether or not the recipe lives under $lib. */
186
+ async function writePreviewRoutes(projectRoot, dest, items, force) {
187
+ const routesRoot = join(projectRoot, 'src', 'routes', 'preview')
188
+ const urls = []
189
+ for (const it of items) {
190
+ const file = it.files?.[0]
191
+ if (!file) continue
192
+ const routeDir = join(routesRoot, it.id)
193
+ await mkdir(routeDir, { recursive: true })
194
+ const routeFile = join(routeDir, '+page.svelte')
195
+ urls.push('/preview/' + it.id)
196
+ if (existsSync(routeFile) && !force) continue
197
+ let rel = relative(routeDir, join(dest, file.write)).split('\\').join('/')
198
+ if (!rel.startsWith('.')) rel = './' + rel
199
+ await writeFile(
200
+ routeFile,
201
+ `<script lang="ts">
202
+ // Auto-generated by @svgrid/ui to preview the ${it.id} recipe. Yours to edit.
203
+ import Demo from '${rel}'
204
+ </script>
205
+
206
+ <div class="svui-preview">
207
+ <a class="svui-preview__back" href="/preview">&larr; all components</a>
208
+ <h1 class="svui-preview__title">${esc(it.title ?? it.id)}</h1>
209
+ <p class="svui-preview__desc">${esc(it.description ?? '')}</p>
210
+ <div class="svui-preview__stage">
211
+ <Demo />
212
+ </div>
213
+ </div>
214
+
215
+ <style>
216
+ .svui-preview { max-width: 880px; margin: 0 auto; padding: 32px 24px; font-family: system-ui, sans-serif; }
217
+ .svui-preview__back { font-size: 13px; color: #6366f1; text-decoration: none; }
218
+ .svui-preview__title { margin: 12px 0 4px; font-size: 24px; }
219
+ .svui-preview__desc { margin: 0 0 20px; color: #64748b; font-size: 14px; }
220
+ .svui-preview__stage { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-start; padding: 28px; border: 1px solid #e2e8f0; border-radius: 12px; background: #fff; }
221
+ </style>
222
+ `,
223
+ )
224
+ }
225
+ await writePreviewIndex(routesRoot)
226
+ return urls
227
+ }
228
+
229
+ /** (Re)generate /preview - an index of every component preview present on disk. */
230
+ async function writePreviewIndex(routesRoot) {
231
+ let entries = []
232
+ try {
233
+ entries = await readdir(routesRoot, { withFileTypes: true })
234
+ } catch {
235
+ return
236
+ }
237
+ const ids = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort()
238
+ const links = ids.map((id) => ` <a class="svui-index__link" href="/preview/${id}">${esc(id)}</a>`).join('\n')
239
+ await writeFile(
240
+ join(routesRoot, '+page.svelte'),
241
+ `<script lang="ts">
242
+ // Auto-generated by @svgrid/ui. Lists the component previews you've added.
243
+ </script>
244
+
245
+ <div class="svui-index">
246
+ <h1>Component previews</h1>
247
+ <div class="svui-index__grid">
248
+ ${links}
249
+ </div>
250
+ </div>
251
+
252
+ <style>
253
+ .svui-index { max-width: 880px; margin: 0 auto; padding: 32px 24px; font-family: system-ui, sans-serif; }
254
+ .svui-index h1 { font-size: 22px; }
255
+ .svui-index__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px; margin-top: 16px; }
256
+ .svui-index__link { padding: 12px 14px; border: 1px solid #e2e8f0; border-radius: 10px; text-decoration: none; color: #0f172a; font-size: 14px; }
257
+ .svui-index__link:hover { border-color: #6366f1; color: #6366f1; }
258
+ </style>
259
+ `,
260
+ )
261
+ }
262
+
143
263
  function printHelp() {
144
264
  stdout.write(`
145
265
  ${color('bold', '@svgrid/ui')} - add SvGrid UI components to your app
146
266
 
147
267
  ${color('bold', 'Usage')}
148
- npx @svgrid/ui add <component...> [--dir <path>] [--force] [--install]
268
+ npx @svgrid/ui add <component...> [--dir <path>] [--preview] [--force] [--no-install]
269
+ npx @svgrid/ui try <component...>
149
270
  npx @svgrid/ui list
150
271
 
151
272
  ${color('bold', 'Commands')}
152
- ${color('cyan', 'add')} Write a ready-to-edit recipe for each component into your project.
153
- ${color('cyan', 'list')} Show the components you can add.
273
+ ${color('cyan', 'add')} Write a ready-to-edit recipe for each component into your project
274
+ (and install @svgrid/grid). Add ${color('cyan', '--preview')} to also drop a /preview route.
275
+ ${color('cyan', 'try')} Open the component(s) in a throwaway sandbox - no project needed.
276
+ ${color('cyan', 'list')} Show the components you can add.
154
277
 
155
278
  ${color('bold', 'Options')}
279
+ --preview, -p (add) Also write a src/routes/preview/<id> route so you can see it
280
+ in your running dev server. SvelteKit apps only.
156
281
  --dir <path> Where to write files (default: src/lib/components/ui, or the
157
282
  "componentsDir" in a project svgrid.json).
158
283
  --force Overwrite files that already exist.
159
- --install Run the package manager to install deps (default: just print it).
284
+ --no-install Do not run the package manager; just print the install command.
160
285
 
161
286
  ${color('bold', 'Examples')}
162
- npx @svgrid/ui add calendar
287
+ npx @svgrid/ui try button
288
+ npx @svgrid/ui add button --preview
163
289
  npx @svgrid/ui add calendar time-picker --dir src/lib/ui
164
- npx @svgrid/ui add date-time --install
290
+ npx @svgrid/ui add date-time
165
291
  `)
166
292
  }
167
293
 
@@ -177,7 +303,8 @@ async function cmdList(registry) {
177
303
  stdout.write(` ${color('cyan', id.padEnd(18))} ${color('dim', g.items.join(', '))}\n`)
178
304
  }
179
305
  }
180
- stdout.write(`\n${color('dim', 'Add one with:')} npx @svgrid/ui add ${registry.items[0]?.id ?? 'calendar'}\n\n`)
306
+ stdout.write(`\n${color('dim', 'Add one with:')} npx @svgrid/ui add ${registry.items[0]?.id ?? 'calendar'}\n`)
307
+ stdout.write(`${color('dim', 'Or just see it:')} npx @svgrid/ui try ${registry.items[0]?.id ?? 'calendar'}\n\n`)
181
308
  }
182
309
 
183
310
  async function cmdAdd(registry, tokens, args) {
@@ -185,22 +312,7 @@ async function cmdAdd(registry, tokens, args) {
185
312
  stdout.write(`${color('red', '✖')} Nothing to add. Try: ${color('cyan', 'npx @svgrid/ui list')}\n`)
186
313
  process.exit(1)
187
314
  }
188
-
189
- // Resolve tokens -> unique items.
190
- const items = new Map()
191
- const unknown = []
192
- for (const tok of tokens) {
193
- const matched = expand(registry, tok)
194
- if (!matched.length) unknown.push(tok)
195
- for (const it of matched) items.set(it.id, it)
196
- }
197
- if (unknown.length) {
198
- stdout.write(
199
- `${color('red', '✖')} Unknown component(s): ${unknown.join(', ')}\n` +
200
- ` See ${color('cyan', 'npx @svgrid/ui list')} for the available set.\n`,
201
- )
202
- process.exit(1)
203
- }
315
+ const items = collectItems(registry, tokens)
204
316
 
205
317
  const cwd = process.cwd()
206
318
  const projectRoot = findProjectRoot(cwd)
@@ -210,7 +322,7 @@ async function cmdAdd(registry, tokens, args) {
210
322
  const written = []
211
323
  const skipped = []
212
324
  const deps = new Set()
213
- for (const it of items.values()) {
325
+ for (const it of items) {
214
326
  for (const d of it.deps ?? []) deps.add(d)
215
327
  for (const file of it.files ?? []) {
216
328
  const outPath = join(dest, file.write)
@@ -228,19 +340,20 @@ async function cmdAdd(registry, tokens, args) {
228
340
  for (const f of written) stdout.write(` ${color('green', '+')} ${relFromCwd(cwd, join(dest, f))}\n`)
229
341
  for (const f of skipped)
230
342
  stdout.write(` ${color('yellow', '•')} ${f} ${color('dim', 'already exists (use --force to overwrite)')}\n`)
231
- if (!written.length) {
343
+ if (!written.length && !args.preview) {
232
344
  stdout.write(`\n${color('yellow', '!')} No files written.\n\n`)
233
345
  return
234
346
  }
235
347
 
236
- // Ensure deps + install.
348
+ // Ensure deps + install (on by default; --no-install just prints the command).
237
349
  const added = await ensureDeps(projectRoot, [...deps])
238
350
  const pm = detectPm(projectRoot)
239
351
  if (added.length) {
240
352
  if (args.install && projectRoot) {
241
353
  stdout.write(`\n${color('dim', `Installing with ${pm.id}...`)}\n`)
242
- const [bin, ...rest] = pm.add.split(' ')
243
- const res = spawnSync(bin, [...rest, ...added], { cwd: projectRoot, stdio: 'inherit', shell: true })
354
+ // Single shell string (not bin + args[]) so Node doesn't warn DEP0190
355
+ // under shell:true; the tokens here are fixed pm commands + npm package ids.
356
+ const res = spawnSync(`${pm.add} ${added.join(' ')}`, { cwd: projectRoot, stdio: 'inherit', shell: true })
244
357
  if (res.status !== 0) {
245
358
  stdout.write(`${color('yellow', '!')} Install failed - run it yourself: ${color('cyan', `${pm.add} ${added.join(' ')}`)}\n`)
246
359
  }
@@ -251,8 +364,22 @@ async function cmdAdd(registry, tokens, args) {
251
364
  stdout.write(`\n${color('dim', `${[...deps].join(', ')} already in package.json.`)}\n`)
252
365
  }
253
366
 
367
+ // Optional preview route(s).
368
+ if (args.preview) {
369
+ if (isSvelteKit(projectRoot)) {
370
+ const urls = await writePreviewRoutes(projectRoot, dest, items, args.force)
371
+ stdout.write(`\n${color('green', '✔')} Preview route(s) written. Start your dev server and open:\n`)
372
+ for (const u of urls) stdout.write(` ${color('cyan', u)}\n`)
373
+ } else {
374
+ stdout.write(
375
+ `\n${color('yellow', '!')} --preview needs a SvelteKit app (src/routes). ` +
376
+ `To see it with zero setup: ${color('cyan', `npx @svgrid/ui try ${items[0].id}`)}\n`,
377
+ )
378
+ }
379
+ }
380
+
254
381
  // Usage hint.
255
- const first = [...items.values()][0]
382
+ const first = items[0]
256
383
  stdout.write(`\n${color('green', '✔')} Added ${written.length} file(s). They're yours - edit away.\n`)
257
384
  if (first) {
258
385
  stdout.write(` ${color('dim', 'Use it:')} import { ${exportName(first.id)} } from '@svgrid/grid'\n`)
@@ -260,6 +387,109 @@ async function cmdAdd(registry, tokens, args) {
260
387
  stdout.write(`\n${color('dim', 'Docs:')} https://www.svgrid.com/docs/help/ui-components\n\n`)
261
388
  }
262
389
 
390
+ /** `try` - render the component(s) in a throwaway Vite + Svelte sandbox and open
391
+ * the browser. No project needed. The sandbox is cached under the OS temp dir so
392
+ * repeat runs skip the install. */
393
+ async function cmdTry(registry, tokens) {
394
+ if (!tokens.length) {
395
+ stdout.write(`${color('red', '✖')} Nothing to try. Try: ${color('cyan', 'npx @svgrid/ui try button')}\n`)
396
+ process.exit(1)
397
+ }
398
+ const items = collectItems(registry, tokens)
399
+ const sandbox = join(tmpdir(), 'svgrid-ui-try')
400
+ const src = join(sandbox, 'src')
401
+ await mkdir(src, { recursive: true })
402
+
403
+ // Base app (mirrors the known-good minimal Vite + Svelte 5 setup).
404
+ await writeFile(
405
+ join(sandbox, 'package.json'),
406
+ JSON.stringify(
407
+ {
408
+ name: 'svgrid-ui-try',
409
+ private: true,
410
+ version: '0.0.0',
411
+ type: 'module',
412
+ scripts: { dev: 'vite' },
413
+ dependencies: { '@svgrid/grid': 'latest' },
414
+ devDependencies: { '@sveltejs/vite-plugin-svelte': '^7.0.0', svelte: '^5.55.5', vite: '^8.0.10' },
415
+ },
416
+ null,
417
+ 2,
418
+ ) + '\n',
419
+ )
420
+ await writeFile(
421
+ join(sandbox, 'vite.config.js'),
422
+ `import { svelte } from '@sveltejs/vite-plugin-svelte'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({ plugins: [svelte()] })\n`,
423
+ )
424
+ await writeFile(
425
+ join(sandbox, 'svelte.config.js'),
426
+ `import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'\n\nexport default { preprocess: vitePreprocess() }\n`,
427
+ )
428
+ await writeFile(
429
+ join(sandbox, 'index.html'),
430
+ `<!doctype html>\n<html lang="en">\n <head>\n <meta charset="UTF-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1.0" />\n <title>@svgrid/ui preview</title>\n </head>\n <body>\n <div id="app"></div>\n <script type="module" src="/src/main.js"></script>\n </body>\n</html>\n`,
431
+ )
432
+ await writeFile(
433
+ join(src, 'main.js'),
434
+ `import { mount } from 'svelte'\nimport App from './App.svelte'\n\nexport default mount(App, { target: document.getElementById('app') })\n`,
435
+ )
436
+
437
+ // Copy each recipe (all its files) and render the primary one.
438
+ const imports = []
439
+ const sections = []
440
+ for (const it of items) {
441
+ let primary
442
+ for (const file of it.files ?? []) {
443
+ await cp(join(RECIPES_DIR, file.from), join(src, file.write))
444
+ primary ??= file.write
445
+ }
446
+ if (!primary) continue
447
+ const ident = toIdent(it.id)
448
+ imports.push(` import ${ident} from './${primary}'`)
449
+ sections.push(` <section class="svui-try__item">\n <h2>${esc(it.title ?? it.id)}</h2>\n <${ident} />\n </section>`)
450
+ }
451
+ await writeFile(
452
+ join(src, 'App.svelte'),
453
+ `<script lang="ts">
454
+ ${imports.join('\n')}
455
+ </script>
456
+
457
+ <main class="svui-try">
458
+ <header class="svui-try__head">
459
+ <strong>@svgrid/ui preview</strong>
460
+ <span>${esc(items.map((i) => i.id).join(', '))}</span>
461
+ </header>
462
+ ${sections.join('\n')}
463
+ </main>
464
+
465
+ <style>
466
+ :global(body) { margin: 0; background: #f8fafc; }
467
+ .svui-try { max-width: 960px; margin: 0 auto; padding: 32px 24px; font-family: system-ui, sans-serif; }
468
+ .svui-try__head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 24px; color: #64748b; }
469
+ .svui-try__item { padding: 28px; border: 1px solid #e2e8f0; border-radius: 12px; background: #fff; margin-bottom: 16px; }
470
+ .svui-try__item h2 { margin: 0 0 16px; font-size: 15px; color: #334155; }
471
+ </style>
472
+ `,
473
+ )
474
+
475
+ // Install only when the sandbox isn't already provisioned.
476
+ const provisioned =
477
+ existsSync(join(sandbox, 'node_modules', '@svgrid', 'grid')) && existsSync(join(sandbox, 'node_modules', 'vite'))
478
+ if (!provisioned) {
479
+ stdout.write(`\n${color('dim', 'Setting up preview sandbox (first run installs deps)...')}\n`)
480
+ const res = spawnSync('npm install', { cwd: sandbox, stdio: 'inherit', shell: true })
481
+ if (res.status !== 0) {
482
+ stdout.write(`${color('red', '✖')} Sandbox install failed.\n`)
483
+ process.exit(1)
484
+ }
485
+ }
486
+
487
+ stdout.write(
488
+ `\n${color('green', '▶')} Opening ${color('cyan', items.map((i) => i.id).join(', '))} ${color('dim', '(Ctrl+C to stop)')}\n`,
489
+ )
490
+ spawnSync('npx vite --open', { cwd: sandbox, stdio: 'inherit', shell: true })
491
+ }
492
+
263
493
  /** Component export name from its id (calendar -> SvCalendar, time-picker ->
264
494
  * SvTimePicker). */
265
495
  function exportName(id) {
@@ -287,12 +517,15 @@ async function main() {
287
517
  return cmdList(registry)
288
518
  case 'add':
289
519
  return cmdAdd(registry, rest, args)
520
+ case 'try':
521
+ case 'preview':
522
+ return cmdTry(registry, rest)
290
523
  default:
291
524
  // Treat a bare component id as `add <id>` for convenience.
292
525
  if (resolveItem(registry, command) || registry.groups?.[command]) {
293
526
  return cmdAdd(registry, [command, ...rest], args)
294
527
  }
295
- stdout.write(`${color('red', '✖')} Unknown command "${command}". Try ${color('cyan', 'add')} or ${color('cyan', 'list')}.\n`)
528
+ stdout.write(`${color('red', '✖')} Unknown command "${command}". Try ${color('cyan', 'add')}, ${color('cyan', 'try')} or ${color('cyan', 'list')}.\n`)
296
529
  process.exit(1)
297
530
  }
298
531
  }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "type": "commercial",
5
5
  "url": "https://svgrid.com/pricing"
6
6
  },
7
- "version": "0.2.0",
7
+ "version": "0.3.0",
8
8
  "description": "Add SvGrid UI components to your app in one command: npx @svgrid/ui add calendar",
9
9
  "type": "module",
10
10
  "license": "MIT",