@svgrid/ui 0.2.0 → 0.3.1
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 +29 -10
- package/index.mjs +282 -41
- package/package.json +2 -2
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
|
|
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),
|
|
15
|
-
package
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
#
|
|
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`
|
|
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
|
|
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)
|
|
11
|
-
//
|
|
12
|
-
//
|
|
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 {
|
|
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
|
-
|
|
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, '<').replace(/>/g, '>')
|
|
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">← 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')}
|
|
153
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
243
|
-
|
|
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,15 +364,140 @@ async function cmdAdd(registry, tokens, args) {
|
|
|
251
364
|
stdout.write(`\n${color('dim', `${[...deps].join(', ')} already in package.json.`)}\n`)
|
|
252
365
|
}
|
|
253
366
|
|
|
254
|
-
//
|
|
255
|
-
|
|
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
|
+
|
|
381
|
+
// Next steps: how to USE it (code) and how to SEE it (try / --preview).
|
|
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
|
-
stdout.write(` ${color('dim', 'Use it:')}
|
|
385
|
+
stdout.write(` ${color('dim', 'Use it:')} import { ${exportName(first.id)} } from '@svgrid/grid'\n`)
|
|
386
|
+
// "See it" - skip when we already wrote preview routes just above.
|
|
387
|
+
if (!(args.preview && isSvelteKit(projectRoot))) {
|
|
388
|
+
const ids = items.map((it) => it.id).join(' ')
|
|
389
|
+
stdout.write(` ${color('dim', 'See it:')} ${color('cyan', `npx @svgrid/ui try ${ids}`)} ${color('dim', '(opens in your browser)')}\n`)
|
|
390
|
+
if (isSvelteKit(projectRoot)) {
|
|
391
|
+
stdout.write(` ${color('dim', 'In app:')} re-run with ${color('cyan', '--preview')} to add a /preview/${first.id} route\n`)
|
|
392
|
+
}
|
|
393
|
+
}
|
|
259
394
|
}
|
|
260
395
|
stdout.write(`\n${color('dim', 'Docs:')} https://www.svgrid.com/docs/help/ui-components\n\n`)
|
|
261
396
|
}
|
|
262
397
|
|
|
398
|
+
/** `try` - render the component(s) in a throwaway Vite + Svelte sandbox and open
|
|
399
|
+
* the browser. No project needed. The sandbox is cached under the OS temp dir so
|
|
400
|
+
* repeat runs skip the install. */
|
|
401
|
+
async function cmdTry(registry, tokens) {
|
|
402
|
+
if (!tokens.length) {
|
|
403
|
+
stdout.write(`${color('red', '✖')} Nothing to try. Try: ${color('cyan', 'npx @svgrid/ui try button')}\n`)
|
|
404
|
+
process.exit(1)
|
|
405
|
+
}
|
|
406
|
+
const items = collectItems(registry, tokens)
|
|
407
|
+
const sandbox = join(tmpdir(), 'svgrid-ui-try')
|
|
408
|
+
const src = join(sandbox, 'src')
|
|
409
|
+
await mkdir(src, { recursive: true })
|
|
410
|
+
|
|
411
|
+
// Base app (mirrors the known-good minimal Vite + Svelte 5 setup).
|
|
412
|
+
await writeFile(
|
|
413
|
+
join(sandbox, 'package.json'),
|
|
414
|
+
JSON.stringify(
|
|
415
|
+
{
|
|
416
|
+
name: 'svgrid-ui-try',
|
|
417
|
+
private: true,
|
|
418
|
+
version: '0.0.0',
|
|
419
|
+
type: 'module',
|
|
420
|
+
scripts: { dev: 'vite' },
|
|
421
|
+
dependencies: { '@svgrid/grid': 'latest' },
|
|
422
|
+
devDependencies: { '@sveltejs/vite-plugin-svelte': '^7.0.0', svelte: '^5.55.5', vite: '^8.0.10' },
|
|
423
|
+
},
|
|
424
|
+
null,
|
|
425
|
+
2,
|
|
426
|
+
) + '\n',
|
|
427
|
+
)
|
|
428
|
+
await writeFile(
|
|
429
|
+
join(sandbox, 'vite.config.js'),
|
|
430
|
+
`import { svelte } from '@sveltejs/vite-plugin-svelte'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({ plugins: [svelte()] })\n`,
|
|
431
|
+
)
|
|
432
|
+
await writeFile(
|
|
433
|
+
join(sandbox, 'svelte.config.js'),
|
|
434
|
+
`import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'\n\nexport default { preprocess: vitePreprocess() }\n`,
|
|
435
|
+
)
|
|
436
|
+
await writeFile(
|
|
437
|
+
join(sandbox, 'index.html'),
|
|
438
|
+
`<!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`,
|
|
439
|
+
)
|
|
440
|
+
await writeFile(
|
|
441
|
+
join(src, 'main.js'),
|
|
442
|
+
`import { mount } from 'svelte'\nimport App from './App.svelte'\n\nexport default mount(App, { target: document.getElementById('app') })\n`,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
// Copy each recipe (all its files) and render the primary one.
|
|
446
|
+
const imports = []
|
|
447
|
+
const sections = []
|
|
448
|
+
for (const it of items) {
|
|
449
|
+
let primary
|
|
450
|
+
for (const file of it.files ?? []) {
|
|
451
|
+
await cp(join(RECIPES_DIR, file.from), join(src, file.write))
|
|
452
|
+
primary ??= file.write
|
|
453
|
+
}
|
|
454
|
+
if (!primary) continue
|
|
455
|
+
const ident = toIdent(it.id)
|
|
456
|
+
imports.push(` import ${ident} from './${primary}'`)
|
|
457
|
+
sections.push(` <section class="svui-try__item">\n <h2>${esc(it.title ?? it.id)}</h2>\n <${ident} />\n </section>`)
|
|
458
|
+
}
|
|
459
|
+
await writeFile(
|
|
460
|
+
join(src, 'App.svelte'),
|
|
461
|
+
`<script lang="ts">
|
|
462
|
+
${imports.join('\n')}
|
|
463
|
+
</script>
|
|
464
|
+
|
|
465
|
+
<main class="svui-try">
|
|
466
|
+
<header class="svui-try__head">
|
|
467
|
+
<strong>@svgrid/ui preview</strong>
|
|
468
|
+
<span>${esc(items.map((i) => i.id).join(', '))}</span>
|
|
469
|
+
</header>
|
|
470
|
+
${sections.join('\n')}
|
|
471
|
+
</main>
|
|
472
|
+
|
|
473
|
+
<style>
|
|
474
|
+
:global(body) { margin: 0; background: #f8fafc; }
|
|
475
|
+
.svui-try { max-width: 960px; margin: 0 auto; padding: 32px 24px; font-family: system-ui, sans-serif; }
|
|
476
|
+
.svui-try__head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 24px; color: #64748b; }
|
|
477
|
+
.svui-try__item { padding: 28px; border: 1px solid #e2e8f0; border-radius: 12px; background: #fff; margin-bottom: 16px; }
|
|
478
|
+
.svui-try__item h2 { margin: 0 0 16px; font-size: 15px; color: #334155; }
|
|
479
|
+
</style>
|
|
480
|
+
`,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
// Install only when the sandbox isn't already provisioned.
|
|
484
|
+
const provisioned =
|
|
485
|
+
existsSync(join(sandbox, 'node_modules', '@svgrid', 'grid')) && existsSync(join(sandbox, 'node_modules', 'vite'))
|
|
486
|
+
if (!provisioned) {
|
|
487
|
+
stdout.write(`\n${color('dim', 'Setting up preview sandbox (first run installs deps)...')}\n`)
|
|
488
|
+
const res = spawnSync('npm install', { cwd: sandbox, stdio: 'inherit', shell: true })
|
|
489
|
+
if (res.status !== 0) {
|
|
490
|
+
stdout.write(`${color('red', '✖')} Sandbox install failed.\n`)
|
|
491
|
+
process.exit(1)
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
stdout.write(
|
|
496
|
+
`\n${color('green', '▶')} Opening ${color('cyan', items.map((i) => i.id).join(', '))} ${color('dim', '(Ctrl+C to stop)')}\n`,
|
|
497
|
+
)
|
|
498
|
+
spawnSync('npx vite --open', { cwd: sandbox, stdio: 'inherit', shell: true })
|
|
499
|
+
}
|
|
500
|
+
|
|
263
501
|
/** Component export name from its id (calendar -> SvCalendar, time-picker ->
|
|
264
502
|
* SvTimePicker). */
|
|
265
503
|
function exportName(id) {
|
|
@@ -287,12 +525,15 @@ async function main() {
|
|
|
287
525
|
return cmdList(registry)
|
|
288
526
|
case 'add':
|
|
289
527
|
return cmdAdd(registry, rest, args)
|
|
528
|
+
case 'try':
|
|
529
|
+
case 'preview':
|
|
530
|
+
return cmdTry(registry, rest)
|
|
290
531
|
default:
|
|
291
532
|
// Treat a bare component id as `add <id>` for convenience.
|
|
292
533
|
if (resolveItem(registry, command) || registry.groups?.[command]) {
|
|
293
534
|
return cmdAdd(registry, [command, ...rest], args)
|
|
294
535
|
}
|
|
295
|
-
stdout.write(`${color('red', '✖')} Unknown command "${command}". Try ${color('cyan', 'add')} or ${color('cyan', 'list')}.\n`)
|
|
536
|
+
stdout.write(`${color('red', '✖')} Unknown command "${command}". Try ${color('cyan', 'add')}, ${color('cyan', 'try')} or ${color('cyan', 'list')}.\n`)
|
|
296
537
|
process.exit(1)
|
|
297
538
|
}
|
|
298
539
|
}
|
package/package.json
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
"type": "commercial",
|
|
5
5
|
"url": "https://svgrid.com/pricing"
|
|
6
6
|
},
|
|
7
|
-
"version": "0.
|
|
8
|
-
"description": "Add SvGrid UI components
|
|
7
|
+
"version": "0.3.1",
|
|
8
|
+
"description": "Add and preview SvGrid UI components in one command: npx @svgrid/ui try calendar",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"author": "jQWidgets Ltd",
|