@nysds/playground 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 (68) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +364 -0
  3. package/bin/cli.mjs +223 -0
  4. package/bin/cli.test.mjs +36 -0
  5. package/decks/customizing-components.json +97 -0
  6. package/dist/assets/index-LO1tomgR.css +6 -0
  7. package/dist/assets/index-bBtGCGL_.js +5190 -0
  8. package/dist/assets/internal/typescript.js +193739 -0
  9. package/dist/assets/nys-icon.library-Bi_7DKlD-YSs5zqZy-DXISxj9N.js +609 -0
  10. package/dist/assets/nys-icon.library-CwuPZJAc-TryaOS7Z.js +600 -0
  11. package/dist/assets/playground-typescript-worker-BcTrPYfY.js +87 -0
  12. package/dist/assets/playground-typescript-worker.js +87 -0
  13. package/dist/favicon.svg +10 -0
  14. package/dist/index.html +769 -0
  15. package/dist/nysds-logo.svg +21 -0
  16. package/dist/nysds-symbol.svg +7 -0
  17. package/index.html +768 -0
  18. package/package.json +59 -0
  19. package/presets/00-welcome.json +8 -0
  20. package/presets/01-button.json +7 -0
  21. package/presets/02-alert.json +7 -0
  22. package/presets/03-badge-and-avatar.json +7 -0
  23. package/presets/04-text-input.json +7 -0
  24. package/presets/05-select-radio-checkbox.json +7 -0
  25. package/presets/06-form-validation.json +7 -0
  26. package/presets/07-card.json +7 -0
  27. package/presets/08-accordion.json +7 -0
  28. package/presets/09-tabs.json +7 -0
  29. package/presets/10-modal.json +7 -0
  30. package/presets/11-stepper.json +7 -0
  31. package/presets/12-table-and-pagination.json +7 -0
  32. package/presets/13-tooltip-and-dropdown.json +7 -0
  33. package/presets/14-navigation.json +7 -0
  34. package/presets/15-page-structure.json +7 -0
  35. package/presets/16-themes.json +7 -0
  36. package/presets/17-utility-classes.json +7 -0
  37. package/presets/README.md +118 -0
  38. package/public/favicon.svg +10 -0
  39. package/public/nysds-logo.svg +21 -0
  40. package/public/nysds-symbol.svg +7 -0
  41. package/src/app.css +1087 -0
  42. package/src/debounce.ts +86 -0
  43. package/src/deck-model.ts +299 -0
  44. package/src/deck-store.ts +144 -0
  45. package/src/decks.test.ts +288 -0
  46. package/src/editor-panes.ts +190 -0
  47. package/src/editors.ts +92 -0
  48. package/src/home.ts +225 -0
  49. package/src/icon-names.ts +117 -0
  50. package/src/icons.test.ts +58 -0
  51. package/src/keys.ts +162 -0
  52. package/src/main.ts +1456 -0
  53. package/src/playground.config.ts +74 -0
  54. package/src/playground.ts +261 -0
  55. package/src/present.ts +398 -0
  56. package/src/preset-schema.ts +228 -0
  57. package/src/route.test.ts +56 -0
  58. package/src/routing.ts +60 -0
  59. package/src/settings.ts +211 -0
  60. package/src/starters.ts +86 -0
  61. package/src/state.test.ts +544 -0
  62. package/src/state.ts +237 -0
  63. package/src/theme.ts +82 -0
  64. package/src/version-catalog.ts +42 -0
  65. package/src/versions.ts +88 -0
  66. package/src/wrapper.ts +84 -0
  67. package/tsconfig.json +25 -0
  68. package/vite.config.ts +62 -0
package/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 New York State Office of Information Technology Services
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.
22
+
23
+ The New York State Design System fonts (Proxima Nova and D Sari) in
24
+ public/fonts/ are not covered by this license. They are licensed for New York
25
+ State use only and are not included in the npm package.
package/README.md ADDED
@@ -0,0 +1,364 @@
1
+ # NYSDS Playground
2
+
3
+ NYSDS Playground is a web app for building and presenting examples of New
4
+ York State Design System (NYSDS) components in the browser. You edit
5
+ HTML, CSS, and JavaScript for each slide of a deck, the app saves your
6
+ decks in that browser, and you share what you build as a URL.
7
+
8
+ Live site: https://its-hcd.github.io/nysds-playground/
9
+
10
+ Source: https://github.com/ITS-HCD/nysds-playground
11
+
12
+ ## Quick start
13
+
14
+ Prerequisites: Node.js 20 or newer.
15
+
16
+ 1. Clone the repository and change into it.
17
+ 2. Run `npm install`.
18
+ 3. Run `npm start`.
19
+
20
+ `npm start` runs the CLI, which starts the dev server on port 5173 and
21
+ opens a browser tab. See Use the CLI for other ways to run it.
22
+
23
+ To run the playground without cloning, run `npx @nysds/playground`. The
24
+ npm package doesn't include the NYSDS fonts. See Requirements and
25
+ limitations.
26
+
27
+ ## Home page
28
+
29
+ The bare URL opens the home page: a card for every deck saved in this
30
+ browser, each with **Open**, **Present**, **Duplicate**, **Export**, and
31
+ **Delete**. Above the list:
32
+
33
+ - **New deck** creates a deck named "Untitled" and opens it at once —
34
+ there's no name prompt. The title opens selected for editing, so
35
+ typing replaces "Untitled" right away.
36
+ - **Import deck** adds a deck from a JSON file, through a file picker or
37
+ by dragging a file anywhere onto the page. It accepts a deck export or
38
+ a single preset, which becomes a one-slide deck. If the imported id is
39
+ already taken, the copy gets a `-2` suffix.
40
+ - **Scratch pad** opens the editor with no deck attached, for trying
41
+ something out before you commit to a deck. See Share, below, for how
42
+ to turn it into one.
43
+ - **Restore starter decks** adds back any bundled starter deck that is
44
+ missing from this browser.
45
+ - **Settings** opens the same settings modal as the editor.
46
+
47
+ ## Decks live in your browser
48
+
49
+ Decks are stored in IndexedDB, in the browser you're using, not as files
50
+ in this repository. The JSON files in `presets/` and `decks/` are
51
+ starter content: the first time the playground runs in a browser, it
52
+ copies them into that browser's store as starter decks (the presets
53
+ become one deck called **Component library**), and after that the app
54
+ reads only from the store. Editing or adding a file in `presets/` or
55
+ `decks/` and rebuilding has no effect on a browser that already seeded —
56
+ select **Restore starter decks** on the home page to add back anything
57
+ that changed or is missing.
58
+
59
+ Decks do not sync between browsers or machines. Use **Export** on a deck
60
+ card, or **Export deck** in the editor toolbar, to download a deck as
61
+ JSON, and **Import deck** on the home page to bring it into another
62
+ browser.
63
+
64
+ ## Editing a deck
65
+
66
+ Opening or creating a deck puts you in the editor. The preview fills the
67
+ stage, and its own "Result" bar stays hidden — press Cmd+Enter or
68
+ Ctrl+Enter, or Cmd+S or Ctrl+S, or use the manual-mode **Update preview**
69
+ button, to force a rebuild. The HTML, CSS, and JS editors float in a
70
+ drawer over the bottom of the stage — drag the divider to resize it, or
71
+ press `c` to collapse it. The slide bar along the bottom shows a slide
72
+ picker in place of a title, grouped the way your slides are grouped,
73
+ with **Previous** and **Next**, and, while editing, **Add slide**,
74
+ **Slide settings**, **Move earlier**, **Move later**, **Duplicate
75
+ slide**, and **Delete slide**.
76
+
77
+ The toolbar is one row: the NYSDS mark (a link to Home), the deck title,
78
+ **Deck settings**, and the **Saved** indicator on the left; the version
79
+ selector, **Share**, **Export deck**, **Settings**, and **Present** on
80
+ the right. Share, Export deck, and Settings are icon buttons with
81
+ tooltips.
82
+
83
+ Click the deck title to rename it in place, the way a document title
84
+ behaves: type the new name, then press Enter or click away to save, or
85
+ Escape to cancel. **Deck settings**, the circle button next to the
86
+ title, opens a modal for the title, description, and base CSS injected
87
+ into every slide's hidden head.
88
+
89
+ **Slide settings** opens the inspector for the current slide: title,
90
+ group, description, presenter notes, which of the HTML, CSS, and JS
91
+ columns start expanded in the side-by-side layout, and a design system
92
+ version to pin the slide to.
93
+
94
+ Edits save automatically, 500 milliseconds after you stop typing, with a
95
+ brief **Saved** indicator to confirm it. Leaving the editor — selecting
96
+ **Home**, closing the tab, or entering presentation mode — flushes
97
+ anything still pending first, so nothing is lost.
98
+
99
+ ## Settings
100
+
101
+ The **Settings** button opens a modal with these controls. Changes apply
102
+ immediately, and **Done** closes the modal.
103
+
104
+ | Setting | What it does |
105
+ | --- | --- |
106
+ | Dark editor | Switches the code editors between a dark theme, the default, and a light one. The preview always shows the design system as it is. |
107
+ | Side by side editors | Switches between the three-column layout, the default, and tabs, which show one file at a time. |
108
+ | Editor font size | Small (13px), medium (15px), or large (18px). |
109
+ | Update preview | How soon an edit reaches the preview: on every pause in typing, only after a longer pause, or only when you ask. See Preview updates, below. |
110
+ | Show prerelease versions | Includes prerelease versions in the version selector. |
111
+ | Reset settings | Clears every remembered setting, back to the defaults. |
112
+
113
+ Each setting also has a URL query parameter, so a link can open the
114
+ playground configured a particular way. A parameter in the URL wins over
115
+ a remembered setting.
116
+
117
+ | Parameter | Values |
118
+ | --- | --- |
119
+ | `?theme=` | `light`, `dark` |
120
+ | `?editors=` | `tabs`, `columns` |
121
+ | `?font=` | `small`, `medium`, `large` |
122
+ | `?update=` | `typing`, `pause`, `manual` |
123
+
124
+ ## Preview updates
125
+
126
+ By default, the preview rebuilds 800 milliseconds after you stop typing.
127
+ Set **Update preview** to a longer pause (2 seconds) when you want to
128
+ keep typing through small mistakes, or to manual to control every
129
+ rebuild yourself. Pressing Cmd+Enter or Ctrl+Enter, or Cmd+S or Ctrl+S,
130
+ rebuilds immediately in any mode, even in the middle of an edit. In
131
+ manual mode, an **Update preview** button appears in the corner of the
132
+ preview and reads "Update preview (changes pending)" once you have
133
+ unsaved edits.
134
+
135
+ ## Presentation mode
136
+
137
+ Select **Present** in the editor toolbar, or **Present** on a deck card,
138
+ to present a deck. Presenting reuses the same layout: it hides the
139
+ toolbar and the slide-editing controls, requests fullscreen, and shows a
140
+ decorative NYSDS mark centered in the slide bar. In place of the
141
+ edit-only controls, the slide bar shows, in order, **Edit** (returns to
142
+ editing), presenter **Notes**, **Reset slide** (discards this session's
143
+ edits to the current slide and reloads what's saved), and **Settings**
144
+ (opens the settings modal without leaving the presentation). Opening a
145
+ deck with `?present=1` presents it directly. Escape exits presentation
146
+ mode, and leaves fullscreen with it.
147
+
148
+ Editing while presenting works the same as editing normally, but those
149
+ edits are ephemeral: they last only for the browser session, and leaving
150
+ presentation mode discards them and restores the saved slide, so a demo
151
+ can never damage the deck.
152
+
153
+ Because editing and presenting share one layout, most of these keyboard
154
+ shortcuts work throughout the app, not only while presenting — as long
155
+ as focus isn't inside an editor.
156
+
157
+ | Key | Action |
158
+ | --- | --- |
159
+ | Right arrow, down arrow, page down, space | Go to the next slide |
160
+ | Left arrow, up arrow, page up | Go to the previous slide |
161
+ | Home | Go to the first slide |
162
+ | End | Go to the last slide |
163
+ | Alt+arrow | Change slides even while the cursor is in an editor |
164
+ | `c` | Collapse or expand the editor drawer |
165
+ | `e` | Switch between the tabs and side-by-side editor layouts |
166
+ | `1`, `2`, `3` | Collapse or expand the HTML, CSS, or JS column (side-by-side layout only, and only when focus is outside the editors) |
167
+ | `t` | Switch the editor theme between light and dark |
168
+ | `n` | Show or hide presenter notes, when the slide has any |
169
+ | Escape | Close presenter notes, then exit presentation mode |
170
+
171
+ Escape needs two presses while you're mid-edit: the first is caught by
172
+ the code editor, and the second reaches presentation mode.
173
+
174
+ ## Share
175
+
176
+ **Share** on a deck offers two links.
177
+
178
+ | Link | What it does |
179
+ | --- | --- |
180
+ | Copy link to this slide | A `?deck=<id>#preset=<slideId>` URL. Opens the same slide, but only in a browser that already holds this deck. |
181
+ | Copy standalone link | A `#code=<compressed JSON>` URL that carries the current HTML, CSS, JS, and version. Opens anywhere, in the **Scratch pad**. |
182
+
183
+ On the scratch pad, Share always copies a standalone link, and a
184
+ **Save as deck** button turns the current code into a new deck named
185
+ "Untitled" — the same no-prompt, select-to-rename behavior as **New
186
+ deck**. Opening a standalone link, or a bare `#preset=` link with no
187
+ `?deck=`, lands on the scratch pad; a bare URL with neither opens Home.
188
+
189
+ ## Use the CLI
190
+
191
+ Installing dependencies also installs a `nysds-playground` command,
192
+ runnable through `npx nysds-playground` or the `npm start` and
193
+ `npm run present` scripts. Outside a clone, `npx @nysds/playground`
194
+ runs the same command from the published package.
195
+
196
+ | Command | What it does |
197
+ | --- | --- |
198
+ | `nysds-playground` | Start the dev server and open Home. |
199
+ | `nysds-playground --deck library --preset button` | Open a specific slide in a deck already saved in this browser (a starter deck is seeded on first run). |
200
+ | `nysds-playground --deck library --present` | Present a deck. |
201
+ | `nysds-playground --html demo.html --css demo.css` | Open the scratch pad with local files loaded into the editors. |
202
+ | `nysds-playground link --html demo.html` | Print a shareable URL for a local file without starting a server. |
203
+ | `nysds-playground link --html demo.html --base https://its-hcd.github.io/nysds-playground/` | Print a link to the published site instead of a local server. |
204
+ | `nysds-playground --built` | Serve the production build in `dist/` with `vite preview`, instead of the dev server. |
205
+ | `nysds-playground --help` | List every command and flag. |
206
+
207
+ Other flags: `--js <file>`, `--version <v>` (used with `--html`,
208
+ `--css`, or `--js`; defaults to `latest`), `--dark`/`--light` and
209
+ `--columns`/`--tabs` (dark and columns are the app's own defaults, so
210
+ these are most useful to override a remembered setting), `--font
211
+ small|medium|large`, `--update typing|pause|manual`, `--port <n>`,
212
+ `--no-open`, and `--base <url>` (the origin `link` builds the URL
213
+ against). `bin/cli.mjs` is the source of truth for every flag; run
214
+ `nysds-playground --help` to see it.
215
+
216
+ `--deck <id>` and `--preset <id>` only work for a deck already in the
217
+ browser that opens the link — a bundled starter (seeded automatically on
218
+ first run), or one you created or imported earlier.
219
+
220
+ Run `npm link` from the repository to make `nysds-playground` available
221
+ globally on your machine.
222
+
223
+ ### For Claude and other agents
224
+
225
+ To see a snippet render, write it to a file and run
226
+ `nysds-playground --html snippet.html`. To hand someone a URL instead of
227
+ opening a browser, run `nysds-playground link --html snippet.html` and
228
+ share the printed link. Either one opens the scratch pad, so it never
229
+ touches a saved deck.
230
+
231
+ ## Starter decks and presets
232
+
233
+ `presets/*.json` and `decks/*.json` are starter content, bundled at
234
+ build time and used only to seed a browser's deck store — see Decks live
235
+ in your browser, above. `presets/` becomes the **Component library**
236
+ starter deck, one slide per file. Each file in `decks/` becomes its own
237
+ starter deck.
238
+
239
+ | Field | Type | Required | Description |
240
+ | --- | --- | --- | --- |
241
+ | `title` | string | yes | Short name shown in the slide list. |
242
+ | `description` | string | yes | One sentence shown as the presentation caption. |
243
+ | `html` | string | yes | Body markup only. |
244
+ | `css` | string | yes | Custom styles, or an empty string. |
245
+ | `js` | string | yes | An ES module script, or an empty string. |
246
+ | `group` | string | no | A section label shown in the caption. |
247
+ | `notes` | string | no | Presenter notes, shown when you press `n`. |
248
+ | `version` | string | no | An NYSDS version to pin this slide to. |
249
+ | `editors` | string array | no | Which columns to expand in the side-by-side layout, for example `["html", "css"]`. Columns left out start collapsed. Ignored in the tabs layout. |
250
+
251
+ Name preset files `NN-slug.json`, for example `03-alert.json`. The
252
+ numeric prefix sets the display order. Name deck files `<id>.json`, for
253
+ example `customizing-components.json`; the filename becomes the starter deck's
254
+ id, and each slide inside the deck's `presets` array needs its own `id`.
255
+
256
+ The fastest way to build either is to shape it in the running app and
257
+ use **Export deck**, since there's no separate build step for starter
258
+ content.
259
+
260
+ - For a deck, create one in the app, add and edit its slides, then
261
+ **Export deck**. The download already matches the `decks/<id>.json`
262
+ shape — move it into `decks/` and rebuild.
263
+ - For a single preset, build a one-slide deck (the **Scratch pad**'s
264
+ **Save as deck** button is the fastest way in), **Export deck**, then
265
+ take the one object out of the downloaded file's `presets` array,
266
+ drop its `id`, and save the rest as `presets/NN-slug.json`.
267
+
268
+ Either way, the file only reaches this browser's own store, and any
269
+ other browser, once you rebuild and it seeds fresh, or you select
270
+ **Restore starter decks** here. Check every icon name you use against
271
+ the list at https://designsystem.ny.gov/components/icon/, mirrored in
272
+ `src/icon-names.ts` and enforced by `npm test`. See `presets/README.md`
273
+ for the full authoring guide, including the deck format and a worked
274
+ example.
275
+
276
+ ## Configure for another design system
277
+
278
+ All of the design-system-specific configuration lives in
279
+ `src/playground.config.ts`: the package names and dist paths to load, the
280
+ CDN base, the versions API, the default version, fallback versions, and
281
+ optional extra head HTML for fonts. To point the playground at a
282
+ different design system, edit that file and replace the contents of
283
+ `presets/` and `decks/`, the starter content new browsers seed from.
284
+
285
+ ## Deploy
286
+
287
+ ### GitHub Pages
288
+
289
+ A push to `main` runs `.github/workflows/deploy.yml`, which builds the
290
+ app and deploys `dist/` to GitHub Pages.
291
+
292
+ Before the first run can succeed, enable Pages with source "GitHub
293
+ Actions" on the repository:
294
+
295
+ ```
296
+ gh api -X POST repos/ITS-HCD/nysds-playground/pages -f build_type=workflow
297
+ ```
298
+
299
+ Because Vite's `base` is set to `./`, the build works whether Pages
300
+ serves it from a project path such as `/nysds-playground/` or from a
301
+ custom domain at the root.
302
+
303
+ ### Google Cloud Run
304
+
305
+ Deploy the included `Dockerfile` directly from source:
306
+
307
+ ```
308
+ gcloud run deploy nysds-playground --source . --region us-east4 --allow-unauthenticated
309
+ ```
310
+
311
+ For a team-only deployment, replace `--allow-unauthenticated` with
312
+ `--no-allow-unauthenticated` and put the service behind Identity-Aware
313
+ Proxy or an internal load balancer.
314
+
315
+ ### npm
316
+
317
+ `npm publish` publishes `@nysds/playground`. The `prepublishOnly` script
318
+ runs the tests and the build first, and the `files` list keeps the
319
+ fonts out of the package. Bump `version` in `package.json` before you
320
+ publish.
321
+
322
+ ### Any static host
323
+
324
+ Run `npm run build` and upload the contents of `dist/` to any static
325
+ file host or CDN.
326
+
327
+ ## Architecture
328
+
329
+ The live preview runs on
330
+ [`playground-elements`](https://github.com/google/playground-elements),
331
+ which loads your code into a sandboxed iframe served from a separate
332
+ origin (unpkg.com hosts the playground service worker). Because the
333
+ preview never shares an origin with the playground itself, it's safe to
334
+ load arbitrary code from a share link without exposing the rest of the
335
+ page. The app has no backend: your decks live in IndexedDB in your own
336
+ browser, and all NYSDS assets load from jsDelivr at request time.
337
+
338
+ ## Requirements and limitations
339
+
340
+ - The browser needs network access to `cdn.jsdelivr.net`,
341
+ `data.jsdelivr.com`, and `unpkg.com`. Without it, the preview and
342
+ version selector don't work.
343
+ - Decks live in IndexedDB and don't sync between browsers, devices, or
344
+ private-browsing sessions. Export a deck before you clear site data or
345
+ switch machines, and Import it wherever you need it next.
346
+ - The NYSDS fonts (Proxima Nova and D Sari) ship in `public/fonts/` in
347
+ this repository and on the live site, and load in both the app and the
348
+ preview there. They are licensed for New York State use only. Don't
349
+ reuse them outside NYS projects. The npm package leaves them out. The
350
+ app still asks for the same font families, so when you run the
351
+ package on a computer that has Proxima Nova and D Sari installed, the
352
+ browser uses those. Otherwise it falls back to system fonts. To serve
353
+ the fonts from a package install, copy the bundle into
354
+ `node_modules/@nysds/playground/public/fonts/`.
355
+ - The preview runs on a different origin, so any host that serves the
356
+ playground has to send permissive CORS headers for `fonts/`. GitHub
357
+ Pages does, and `nginx.conf` does for the container. In local
358
+ development, Chrome blocks the preview from fetching `localhost`,
359
+ because the preview is a public origin asking for a local network
360
+ resource, so the preview falls back to system fonts until you deploy.
361
+ The app itself still shows the fonts.
362
+ - The preview relies on service workers, and the deck store relies on
363
+ IndexedDB, so the playground needs a browser that supports both. Every
364
+ current major browser does.
package/bin/cli.mjs ADDED
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Command-line entry point for the NYSDS Playground.
4
+ *
5
+ * Starts the playground and opens it in your browser, or prints a share link
6
+ * for local HTML, CSS, and JS files. Run `nysds-playground --help` for usage.
7
+ *
8
+ * The hash format matches `src/state.ts`: `#code=` carries an lz-string
9
+ * compressed JSON object with the keys `v`, `h`, `c`, and `j`.
10
+ */
11
+ import {realpathSync} from 'node:fs';
12
+ import {readFile} from 'node:fs/promises';
13
+ import {resolve, dirname} from 'node:path';
14
+ import {fileURLToPath} from 'node:url';
15
+ import {parseArgs} from 'node:util';
16
+ import LZString from 'lz-string';
17
+
18
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
19
+ const DEFAULT_PORT = 5173;
20
+ const PREVIEW_PORT = 4173;
21
+
22
+ const HELP = `NYSDS Playground
23
+
24
+ Usage
25
+ nysds-playground [open] [options] Start the playground and open a browser tab
26
+ nysds-playground link [options] Print a playground URL without starting a server
27
+ nysds-playground help Show this help
28
+
29
+ Options
30
+ --preset <id> Open a preset by id (for example: button)
31
+ --deck <id> Open a deck by id (for example: customizing-components)
32
+ --present Open in presentation mode
33
+ --dark Use the dark editor theme (default)
34
+ --light Use the light editor theme
35
+ --columns Show the HTML, CSS, and JS editors side by side (default)
36
+ --tabs Show the editors as tabs instead of columns
37
+ --font <size> Editor font size: small, medium, or large
38
+ --update <mode> When the preview rebuilds: typing, pause, or manual
39
+ --html <file> Load an HTML file into the HTML tab
40
+ --css <file> Load a CSS file into the CSS tab
41
+ --js <file> Load a JavaScript file into the JS tab
42
+ --version <v> Design system version for --html/--css/--js links (default: latest)
43
+ --port <n> Port for the local server (default: ${DEFAULT_PORT})
44
+ --built Serve the production build in dist/ instead of the dev server
45
+ --no-open Start the server without opening a browser
46
+ --base <url> Base URL for the link command (default: http://localhost:${DEFAULT_PORT}/).
47
+ A path such as https://its-hcd.github.io/nysds-playground/ is kept.
48
+
49
+ Examples
50
+ nysds-playground Open the library deck
51
+ nysds-playground --preset modal Open the modal preset
52
+ nysds-playground --deck customizing-components --present --dark
53
+ nysds-playground --html demo.html --css demo.css
54
+ nysds-playground link --html demo.html Print a shareable URL for demo.html
55
+ nysds-playground link --html demo.html --base https://its-hcd.github.io/nysds-playground/
56
+ `;
57
+
58
+ /** Parses argv into a command name and typed options. */
59
+ function parseCli(argv) {
60
+ const {values, positionals} = parseArgs({
61
+ args: argv,
62
+ allowPositionals: true,
63
+ allowNegative: true,
64
+ options: {
65
+ preset: {type: 'string'},
66
+ deck: {type: 'string'},
67
+ present: {type: 'boolean', default: false},
68
+ dark: {type: 'boolean', default: false},
69
+ light: {type: 'boolean', default: false},
70
+ columns: {type: 'boolean', default: false},
71
+ tabs: {type: 'boolean', default: false},
72
+ font: {type: 'string'},
73
+ update: {type: 'string'},
74
+ html: {type: 'string'},
75
+ css: {type: 'string'},
76
+ js: {type: 'string'},
77
+ version: {type: 'string', default: 'latest'},
78
+ port: {type: 'string'},
79
+ built: {type: 'boolean', default: false},
80
+ open: {type: 'boolean', default: true},
81
+ base: {type: 'string'},
82
+ help: {type: 'boolean', short: 'h', default: false},
83
+ },
84
+ });
85
+ const command = positionals[0] ?? 'open';
86
+ return {command, options: values};
87
+ }
88
+
89
+ /** Reads a file argument, or returns an empty string when it isn't given. */
90
+ async function readOptional(path) {
91
+ if (!path) {
92
+ return '';
93
+ }
94
+ return readFile(resolve(process.cwd(), path), 'utf8');
95
+ }
96
+
97
+ /**
98
+ * Builds the path, query, and hash that select what the playground shows.
99
+ *
100
+ * File options win over `--preset` because they carry explicit content.
101
+ */
102
+ async function buildLocation(options) {
103
+ const query = new URLSearchParams();
104
+ if (options.deck) {
105
+ query.set('deck', options.deck);
106
+ }
107
+ if (options.present) {
108
+ query.set('present', '1');
109
+ }
110
+ if (options.light) {
111
+ query.set('theme', 'light');
112
+ } else if (options.dark) {
113
+ query.set('theme', 'dark');
114
+ }
115
+ if (options.tabs) {
116
+ query.set('editors', 'tabs');
117
+ } else if (options.columns) {
118
+ query.set('editors', 'columns');
119
+ }
120
+ if (options.font) {
121
+ if (!['small', 'medium', 'large'].includes(options.font)) {
122
+ throw new Error(`--font must be small, medium, or large (got "${options.font}").`);
123
+ }
124
+ query.set('font', options.font);
125
+ }
126
+ if (options.update) {
127
+ if (!['typing', 'pause', 'manual'].includes(options.update)) {
128
+ throw new Error(`--update must be typing, pause, or manual (got "${options.update}").`);
129
+ }
130
+ query.set('update', options.update);
131
+ }
132
+ let hash = '';
133
+ if (options.html || options.css || options.js) {
134
+ const [html, css, js] = await Promise.all([
135
+ readOptional(options.html),
136
+ readOptional(options.css),
137
+ readOptional(options.js),
138
+ ]);
139
+ const payload = JSON.stringify({v: options.version, h: html, c: css, j: js});
140
+ hash = `#code=${LZString.compressToEncodedURIComponent(payload)}`;
141
+ } else if (options.preset) {
142
+ hash = `#preset=${encodeURIComponent(options.preset)}`;
143
+ }
144
+ const search = query.size > 0 ? `?${query.toString()}` : '';
145
+ return `/${search}${hash}`;
146
+ }
147
+
148
+ /**
149
+ * Joins a playground location onto a base URL, keeping the base's path.
150
+ *
151
+ * `buildLocation` returns a root-relative location such as `/?deck=x#code=y`,
152
+ * which the dev server needs. Resolving that directly against a base with a
153
+ * path, such as a GitHub Pages project site, would drop the path, so the
154
+ * location is made relative and the base is treated as a directory.
155
+ */
156
+ export function linkUrl(base, location) {
157
+ const directory = base.endsWith('/') ? base : `${base}/`;
158
+ return new URL(location.replace(/^\//, ''), directory).toString();
159
+ }
160
+
161
+ /** Prints a URL for the given options without starting a server. */
162
+ async function link(options) {
163
+ const port = options.port ? Number(options.port) : DEFAULT_PORT;
164
+ const base = options.base ?? `http://localhost:${port}/`;
165
+ const location = await buildLocation(options);
166
+ process.stdout.write(`${linkUrl(base, location)}\n`);
167
+ }
168
+
169
+ /** Starts the dev server or the production preview and opens the browser. */
170
+ async function open(options) {
171
+ const location = await buildLocation(options);
172
+ const vite = await import('vite');
173
+ const port = options.port ? Number(options.port) : options.built ? PREVIEW_PORT : DEFAULT_PORT;
174
+ const openTarget = options.open ? location : false;
175
+
176
+ if (options.built) {
177
+ const server = await vite.preview({
178
+ root: ROOT,
179
+ configFile: resolve(ROOT, 'vite.config.ts'),
180
+ preview: {port, strictPort: false, open: openTarget},
181
+ });
182
+ server.printUrls();
183
+ return;
184
+ }
185
+
186
+ const server = await vite.createServer({
187
+ root: ROOT,
188
+ configFile: resolve(ROOT, 'vite.config.ts'),
189
+ server: {port, strictPort: false, open: openTarget},
190
+ });
191
+ await server.listen();
192
+ server.printUrls();
193
+ server.bindCLIShortcuts({print: true});
194
+ }
195
+
196
+ async function main() {
197
+ const {command, options} = parseCli(process.argv.slice(2));
198
+ if (options.help || command === 'help') {
199
+ process.stdout.write(HELP);
200
+ return;
201
+ }
202
+ switch (command) {
203
+ case 'open':
204
+ await open(options);
205
+ break;
206
+ case 'link':
207
+ await link(options);
208
+ break;
209
+ default:
210
+ process.stderr.write(`Unknown command "${command}".\n\n${HELP}`);
211
+ process.exitCode = 1;
212
+ }
213
+ }
214
+
215
+ const isMain =
216
+ process.argv[1] !== undefined && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
217
+
218
+ if (isMain) {
219
+ main().catch((error) => {
220
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
221
+ process.exitCode = 1;
222
+ });
223
+ }
@@ -0,0 +1,36 @@
1
+ /** Checks that link URLs keep the base's path, such as a GitHub Pages project site. */
2
+ import assert from 'node:assert/strict';
3
+ import test from 'node:test';
4
+
5
+ import {linkUrl} from './cli.mjs';
6
+
7
+ const LOCATION = '/?deck=library#preset=button';
8
+
9
+ test('a base with a project path keeps that path', () => {
10
+ assert.equal(
11
+ linkUrl('https://its-hcd.github.io/nysds-playground/', LOCATION),
12
+ 'https://its-hcd.github.io/nysds-playground/?deck=library#preset=button',
13
+ );
14
+ });
15
+
16
+ test('a base without a trailing slash is treated as a directory', () => {
17
+ assert.equal(
18
+ linkUrl('https://its-hcd.github.io/nysds-playground', LOCATION),
19
+ 'https://its-hcd.github.io/nysds-playground/?deck=library#preset=button',
20
+ );
21
+ });
22
+
23
+ test('the default local base still works', () => {
24
+ assert.equal(linkUrl('http://localhost:5173/', LOCATION), 'http://localhost:5173/?deck=library#preset=button');
25
+ });
26
+
27
+ test('a code hash on a project site keeps the path', () => {
28
+ assert.equal(
29
+ linkUrl('https://its-hcd.github.io/nysds-playground/', '/#code=abc'),
30
+ 'https://its-hcd.github.io/nysds-playground/#code=abc',
31
+ );
32
+ });
33
+
34
+ test('an empty location returns the base', () => {
35
+ assert.equal(linkUrl('https://its-hcd.github.io/nysds-playground/', '/'), 'https://its-hcd.github.io/nysds-playground/');
36
+ });