@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.
- package/LICENSE +25 -0
- package/README.md +364 -0
- package/bin/cli.mjs +223 -0
- package/bin/cli.test.mjs +36 -0
- package/decks/customizing-components.json +97 -0
- package/dist/assets/index-LO1tomgR.css +6 -0
- package/dist/assets/index-bBtGCGL_.js +5190 -0
- package/dist/assets/internal/typescript.js +193739 -0
- package/dist/assets/nys-icon.library-Bi_7DKlD-YSs5zqZy-DXISxj9N.js +609 -0
- package/dist/assets/nys-icon.library-CwuPZJAc-TryaOS7Z.js +600 -0
- package/dist/assets/playground-typescript-worker-BcTrPYfY.js +87 -0
- package/dist/assets/playground-typescript-worker.js +87 -0
- package/dist/favicon.svg +10 -0
- package/dist/index.html +769 -0
- package/dist/nysds-logo.svg +21 -0
- package/dist/nysds-symbol.svg +7 -0
- package/index.html +768 -0
- package/package.json +59 -0
- package/presets/00-welcome.json +8 -0
- package/presets/01-button.json +7 -0
- package/presets/02-alert.json +7 -0
- package/presets/03-badge-and-avatar.json +7 -0
- package/presets/04-text-input.json +7 -0
- package/presets/05-select-radio-checkbox.json +7 -0
- package/presets/06-form-validation.json +7 -0
- package/presets/07-card.json +7 -0
- package/presets/08-accordion.json +7 -0
- package/presets/09-tabs.json +7 -0
- package/presets/10-modal.json +7 -0
- package/presets/11-stepper.json +7 -0
- package/presets/12-table-and-pagination.json +7 -0
- package/presets/13-tooltip-and-dropdown.json +7 -0
- package/presets/14-navigation.json +7 -0
- package/presets/15-page-structure.json +7 -0
- package/presets/16-themes.json +7 -0
- package/presets/17-utility-classes.json +7 -0
- package/presets/README.md +118 -0
- package/public/favicon.svg +10 -0
- package/public/nysds-logo.svg +21 -0
- package/public/nysds-symbol.svg +7 -0
- package/src/app.css +1087 -0
- package/src/debounce.ts +86 -0
- package/src/deck-model.ts +299 -0
- package/src/deck-store.ts +144 -0
- package/src/decks.test.ts +288 -0
- package/src/editor-panes.ts +190 -0
- package/src/editors.ts +92 -0
- package/src/home.ts +225 -0
- package/src/icon-names.ts +117 -0
- package/src/icons.test.ts +58 -0
- package/src/keys.ts +162 -0
- package/src/main.ts +1456 -0
- package/src/playground.config.ts +74 -0
- package/src/playground.ts +261 -0
- package/src/present.ts +398 -0
- package/src/preset-schema.ts +228 -0
- package/src/route.test.ts +56 -0
- package/src/routing.ts +60 -0
- package/src/settings.ts +211 -0
- package/src/starters.ts +86 -0
- package/src/state.test.ts +544 -0
- package/src/state.ts +237 -0
- package/src/theme.ts +82 -0
- package/src/version-catalog.ts +42 -0
- package/src/versions.ts +88 -0
- package/src/wrapper.ts +84 -0
- package/tsconfig.json +25 -0
- 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
|
+
}
|
package/bin/cli.test.mjs
ADDED
|
@@ -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
|
+
});
|