@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
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Single point of configuration for the playground.
3
+ *
4
+ * To point the playground at a different design system or a different CDN,
5
+ * edit this file. Nothing else in `src/` hard-codes a package name or a URL.
6
+ */
7
+
8
+ /** An npm package the preview loads from the CDN. */
9
+ export interface PlaygroundPackage {
10
+ /** The npm package name, for example `@nysds/components`. */
11
+ name: string;
12
+ /** The path inside the package, relative to the package root. */
13
+ path: string;
14
+ }
15
+
16
+ /** The shape of {@link PLAYGROUND_CONFIG}. */
17
+ export interface PlaygroundAppConfig {
18
+ /** The application title. It appears in the toolbar and the page title. */
19
+ title: string;
20
+ /** The packages the preview document loads. */
21
+ packages: {
22
+ /** The JavaScript bundle that registers the custom elements. */
23
+ components: PlaygroundPackage;
24
+ /** The stylesheet that provides the reset, typography, and utilities. */
25
+ styles: PlaygroundPackage;
26
+ };
27
+ /** The CDN origin that serves npm packages, with no trailing slash. */
28
+ cdnBase: string;
29
+ /** The versions API endpoint, with a trailing slash. Append a package name. */
30
+ versionsApi: string;
31
+ /** The version to select on first load. Use `latest` for the newest stable. */
32
+ defaultVersion: string;
33
+ /** The versions to offer when the versions API is unreachable. */
34
+ fallbackVersions: string[];
35
+ /**
36
+ * Extra markup to add to the preview `<head>`, such as a fonts stylesheet.
37
+ *
38
+ * Write `href` and `src` values relative to the playground page. The
39
+ * preview runs on another origin, so the app resolves them to absolute
40
+ * URLs before it builds the preview document.
41
+ */
42
+ extraHeadHtml: string;
43
+ /** Whether the version list includes prereleases before you opt in. */
44
+ showPrereleasesByDefault: boolean;
45
+ }
46
+
47
+ export const PLAYGROUND_CONFIG: PlaygroundAppConfig = {
48
+ title: 'NYSDS Playground',
49
+ packages: {
50
+ components: {name: '@nysds/components', path: 'dist/nysds.js'},
51
+ styles: {name: '@nysds/styles', path: 'dist/nysds-full.min.css'},
52
+ },
53
+ cdnBase: 'https://cdn.jsdelivr.net/npm',
54
+ versionsApi: 'https://data.jsdelivr.com/v1/package/npm/',
55
+ defaultVersion: 'latest',
56
+ fallbackVersions: ['1.21.1', '1.21.0', '1.20.1', '1.20.0', '1.19.4'],
57
+ extraHeadHtml: '<link rel="stylesheet" href="./fonts/nysds-fonts.css">',
58
+ showPrereleasesByDefault: false,
59
+ };
60
+
61
+ /** Returns the CDN URL for a package file at a given version. */
62
+ export function cdnUrl(pkg: PlaygroundPackage, version: string): string {
63
+ return `${PLAYGROUND_CONFIG.cdnBase}/${pkg.name}@${version}/${pkg.path}`;
64
+ }
65
+
66
+ /** Returns the stylesheet URL the preview loads at a given version. */
67
+ export function stylesUrl(version: string): string {
68
+ return cdnUrl(PLAYGROUND_CONFIG.packages.styles, version);
69
+ }
70
+
71
+ /** Returns the component bundle URL the preview loads at a given version. */
72
+ export function componentsUrl(version: string): string {
73
+ return cdnUrl(PLAYGROUND_CONFIG.packages.components, version);
74
+ }
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Wires the `playground-elements` project to the playground's own state.
3
+ *
4
+ * The project holds four files. Three are the editable ones: `snippet.html`,
5
+ * `styles.css`, and `script.js`. The fourth, `index.html`, is hidden from the
6
+ * editors; the host regenerates it from the snippet so the preview has a whole
7
+ * document to load. Keeping the wrapper out of the snippet is what makes
8
+ * select-all in the HTML pane copy only the user's markup.
9
+ */
10
+ import 'playground-elements/playground-project.js';
11
+ import 'playground-elements/playground-tab-bar.js';
12
+ import 'playground-elements/playground-file-editor.js';
13
+ import 'playground-elements/playground-preview.js';
14
+
15
+ import type {PlaygroundProject} from 'playground-elements/playground-project.js';
16
+ import type {SampleFile} from 'playground-elements/shared/worker-api.js';
17
+
18
+ import {PLAYGROUND_CONFIG, componentsUrl, stylesUrl} from './playground.config';
19
+ import type {QuietDebounce} from './debounce';
20
+ import {createQuietDebounce} from './debounce';
21
+ import type {UpdateMode} from './settings';
22
+ import {UPDATE_DELAYS} from './settings';
23
+ import type {PlaygroundState} from './state';
24
+ import {resolveHeadUrls, wrapUserHtml} from './wrapper';
25
+
26
+ /** The hidden document the preview loads. */
27
+ const INDEX_FILE = 'index.html';
28
+
29
+ /** The editable files, in the order the tabs and columns show them. */
30
+ const HTML_FILE = 'snippet.html';
31
+ const CSS_FILE = 'styles.css';
32
+ const JS_FILE = 'script.js';
33
+
34
+ /** Drives one `<playground-project>` element. */
35
+ export class PlaygroundHost {
36
+ readonly project: PlaygroundProject;
37
+
38
+ /** The state last written into the project, used when a split fails. */
39
+ private current: PlaygroundState;
40
+
41
+ /** The deck's base CSS, injected into the hidden head of every slide. */
42
+ private baseCss = '';
43
+
44
+ private readonly listeners = new Set<() => void>();
45
+ private readonly pendingListeners = new Set<(pending: boolean) => void>();
46
+
47
+ /** Whether the files have changed since the last build. */
48
+ private pending = false;
49
+
50
+ /**
51
+ * The file contents a load replaced, keyed by file name.
52
+ *
53
+ * Replacing the project's files makes every editor re-render, and an editor
54
+ * that has not caught up writes its old document straight back through
55
+ * `editFile`. Matching that exact text identifies the echo without a timer,
56
+ * so a real keystroke is never dropped, however fast it arrives.
57
+ */
58
+ private echoes = new Map<string, string>();
59
+
60
+ /** Waits for typing to stop before it rebuilds the preview. */
61
+ private readonly build: QuietDebounce = createQuietDebounce(() => {
62
+ this.setPending(false);
63
+ void this.project.save();
64
+ }, UPDATE_DELAYS.typing);
65
+
66
+ constructor(project: PlaygroundProject, initial: PlaygroundState, baseCss = '') {
67
+ this.project = project;
68
+ this.current = {...initial};
69
+ this.baseCss = baseCss;
70
+ this.project.cdnBaseUrl = PLAYGROUND_CONFIG.cdnBase;
71
+ this.interceptEdits();
72
+ this.apply(initial);
73
+ }
74
+
75
+ /**
76
+ * Replaces every file. The editors reset, including the cursor position.
77
+ *
78
+ * `baseCss` is the deck-wide stylesheet. Pass it on every load so switching
79
+ * decks swaps it too.
80
+ */
81
+ load(state: PlaygroundState, baseCss = ''): void {
82
+ this.current = {...state};
83
+ this.baseCss = baseCss;
84
+ // Loading a project builds it right away, whatever the update mode is.
85
+ this.build.cancel();
86
+ this.setPending(false);
87
+ this.echoes = new Map(
88
+ (this.project.files ?? []).map((file) => [file.name, file.content]),
89
+ );
90
+ this.apply(state);
91
+ }
92
+
93
+ /**
94
+ * Switches the design system version, keeping the user's three files.
95
+ *
96
+ * The version only appears in the hidden wrapper, so nothing the editors
97
+ * show has to change. Regenerating that file and rebuilding is enough.
98
+ */
99
+ setVersion(version: string): void {
100
+ this.current = {...this.readFiles(), version};
101
+ this.regenerateIndex();
102
+ this.buildNow();
103
+ }
104
+
105
+ /** Returns the current editor contents and version. */
106
+ getState(): PlaygroundState {
107
+ return this.readFiles();
108
+ }
109
+
110
+ /** Registers a callback that runs after the user edits any file. */
111
+ onEdit(listener: () => void): void {
112
+ this.listeners.add(listener);
113
+ }
114
+
115
+ /** Registers a callback for the "changes not in the preview yet" state. */
116
+ onPendingChange(listener: (pending: boolean) => void): void {
117
+ this.pendingListeners.add(listener);
118
+ }
119
+
120
+ /** Whether edits are waiting to reach the preview. */
121
+ get hasPendingChanges(): boolean {
122
+ return this.pending;
123
+ }
124
+
125
+ /**
126
+ * Sets how soon an edit reaches the preview.
127
+ *
128
+ * `manual` stops automatic rebuilds; {@link buildNow} is then the only way.
129
+ */
130
+ setUpdateMode(mode: UpdateMode): void {
131
+ this.build.setDelay(UPDATE_DELAYS[mode]);
132
+ }
133
+
134
+ /** Rebuilds the preview right away and clears the pending state. */
135
+ buildNow(): void {
136
+ this.build.flush();
137
+ }
138
+
139
+ /**
140
+ * Replaces the project's own build debounce with a quiet-period one.
141
+ *
142
+ * `playground-file-editor` calls `project.editFile` on every keystroke, and
143
+ * `editFile` schedules a build through `saveDebounced`, which the library
144
+ * tunes for "maximal responsiveness". That reloads the preview iframe on
145
+ * almost every character, which reads as a flicker. Wrapping the two methods
146
+ * keeps the library untouched while the playground decides when to build,
147
+ * and gives a reliable edit signal even when builds are switched off.
148
+ */
149
+ private interceptEdits(): void {
150
+ const project = this.project as PlaygroundProject & {
151
+ editFile(file: SampleFile, content: string): void;
152
+ saveDebounced(): Promise<void>;
153
+ };
154
+ const originalEditFile = project.editFile.bind(project);
155
+ const originalSaveDebounced = project.saveDebounced.bind(project);
156
+ // `editFile` calls `saveDebounced` synchronously, so this flag tells the
157
+ // two calls apart: an edit waits for the quiet period, while a library
158
+ // call (such as a project load) builds right away and is never "pending".
159
+ let editing = false;
160
+ project.editFile = (file: SampleFile, content: string): void => {
161
+ if (this.echoes.get(file.name) === content) {
162
+ // An editor that has not caught up with a load, writing back what the
163
+ // load replaced. Drop it once; anything else is a real edit.
164
+ this.echoes.delete(file.name);
165
+ return;
166
+ }
167
+ this.echoes.clear();
168
+ editing = true;
169
+ try {
170
+ originalEditFile(file, content);
171
+ if (file.name === HTML_FILE) {
172
+ // The preview loads the wrapper, so it has to follow the snippet.
173
+ this.regenerateIndex();
174
+ }
175
+ } finally {
176
+ editing = false;
177
+ }
178
+ this.handleEdit();
179
+ };
180
+ project.saveDebounced = (): Promise<void> => {
181
+ if (!editing) {
182
+ return originalSaveDebounced();
183
+ }
184
+ this.setPending(true);
185
+ this.build.schedule();
186
+ return Promise.resolve();
187
+ };
188
+ }
189
+
190
+ private setPending(pending: boolean): void {
191
+ if (this.pending === pending) {
192
+ return;
193
+ }
194
+ this.pending = pending;
195
+ for (const listener of this.pendingListeners) {
196
+ listener(pending);
197
+ }
198
+ }
199
+
200
+ /** Builds the hidden preview document from the snippet on screen. */
201
+ private wrap(state: PlaygroundState): string {
202
+ return wrapUserHtml(state.html, {
203
+ stylesHref: stylesUrl(state.version),
204
+ componentsSrc: componentsUrl(state.version),
205
+ extraHeadHtml: resolveHeadUrls(PLAYGROUND_CONFIG.extraHeadHtml, document.baseURI),
206
+ baseCss: this.baseCss,
207
+ title: PLAYGROUND_CONFIG.title,
208
+ });
209
+ }
210
+
211
+ /**
212
+ * Rewrites the hidden preview document in place.
213
+ *
214
+ * No editor shows this file, so changing its content behind the library's
215
+ * back is safe, and it avoids resetting the project on every keystroke.
216
+ */
217
+ private regenerateIndex(): void {
218
+ const index = (this.project.files ?? []).find((file) => file.name === INDEX_FILE);
219
+ if (index) {
220
+ index.content = this.wrap(this.readFiles());
221
+ }
222
+ }
223
+
224
+ private apply(state: PlaygroundState): void {
225
+ this.project.config = {
226
+ files: {
227
+ [HTML_FILE]: {content: state.html, label: 'HTML', selected: true},
228
+ [CSS_FILE]: {content: state.css, label: 'CSS'},
229
+ [JS_FILE]: {content: state.js, label: 'JS'},
230
+ [INDEX_FILE]: {content: this.wrap(state), hidden: true},
231
+ },
232
+ };
233
+ }
234
+
235
+ private readFiles(): PlaygroundState {
236
+ const files = this.project.files ?? [];
237
+ const find = (name: string): string =>
238
+ files.find((file) => file.name === name)?.content ?? '';
239
+ return {
240
+ version: this.current.version,
241
+ html: files.length ? find(HTML_FILE) : this.current.html,
242
+ css: files.length ? find(CSS_FILE) : this.current.css,
243
+ js: files.length ? find(JS_FILE) : this.current.js,
244
+ };
245
+ }
246
+
247
+ private handleEdit(): void {
248
+ const next = this.readFiles();
249
+ if (
250
+ next.html === this.current.html &&
251
+ next.css === this.current.css &&
252
+ next.js === this.current.js
253
+ ) {
254
+ return;
255
+ }
256
+ this.current = next;
257
+ for (const listener of this.listeners) {
258
+ listener();
259
+ }
260
+ }
261
+ }