@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/src/routing.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which view a URL asks for, and when a history move needs a fresh boot.
|
|
3
|
+
*
|
|
4
|
+
* The router runs once at start-up, so going back or forward between views has
|
|
5
|
+
* to be turned into a reload. These helpers are pure so `src/route.test.ts`
|
|
6
|
+
* can cover the rule without a browser.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The three views the playground can show. */
|
|
10
|
+
export type Route =
|
|
11
|
+
| {kind: 'home'}
|
|
12
|
+
| {kind: 'deck'; id: string}
|
|
13
|
+
| {kind: 'scratch'};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Decides between the home page and the editor.
|
|
17
|
+
*
|
|
18
|
+
* A deck id opens that deck. A `#code=` or `#preset=` hash without a deck id
|
|
19
|
+
* opens the scratch pad, which is what a shared link and the CLI produce.
|
|
20
|
+
* Anything else lands on the home page.
|
|
21
|
+
*/
|
|
22
|
+
export function routeFor(search: string, hash: string): Route {
|
|
23
|
+
const deck = new URLSearchParams(search).get('deck');
|
|
24
|
+
if (deck) {
|
|
25
|
+
return {kind: 'deck', id: deck};
|
|
26
|
+
}
|
|
27
|
+
const value = hash.startsWith('#') ? hash.slice(1) : hash;
|
|
28
|
+
if (value.startsWith('code=') || value.startsWith('preset=')) {
|
|
29
|
+
return {kind: 'scratch'};
|
|
30
|
+
}
|
|
31
|
+
return {kind: 'home'};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Reports whether two routes show the same view of the same deck. */
|
|
35
|
+
export function sameRoute(a: Route, b: Route): boolean {
|
|
36
|
+
if (a.kind !== b.kind) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return a.kind === 'deck' && b.kind === 'deck' ? a.id === b.id : true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Reports whether a history move has to reload the page.
|
|
44
|
+
*
|
|
45
|
+
* Moving within one deck only changes the slide, which the app handles in
|
|
46
|
+
* place. Everything else changes which view is mounted.
|
|
47
|
+
*/
|
|
48
|
+
export function needsReboot(from: Route, to: Route): boolean {
|
|
49
|
+
return !sameRoute(from, to);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Returns the slide id a URL names, or `null`. */
|
|
53
|
+
export function slideIdFromHash(hash: string): string | null {
|
|
54
|
+
const value = hash.startsWith('#') ? hash.slice(1) : hash;
|
|
55
|
+
if (!value.startsWith('preset=')) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const id = decodeURIComponent(value.slice('preset='.length));
|
|
59
|
+
return id || null;
|
|
60
|
+
}
|
package/src/settings.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every playground setting and the one place that touches `localStorage`.
|
|
3
|
+
*
|
|
4
|
+
* Each setting can also come from the URL, so a link can open the playground
|
|
5
|
+
* configured a particular way. The URL wins over the remembered value, and the
|
|
6
|
+
* remembered value wins over the default.
|
|
7
|
+
*
|
|
8
|
+
* The parsing and clamping here are pure, so `src/state.test.ts` exercises them
|
|
9
|
+
* without a DOM. Only the `read*`/`write*` helpers touch storage, and every one
|
|
10
|
+
* of them tolerates storage being blocked.
|
|
11
|
+
*/
|
|
12
|
+
import type {EditorLayout} from './editors.ts';
|
|
13
|
+
import type {PaneId} from './preset-schema.ts';
|
|
14
|
+
import {PANE_IDS} from './preset-schema.ts';
|
|
15
|
+
import type {EditorTheme} from './theme.ts';
|
|
16
|
+
|
|
17
|
+
/** The code font sizes the settings modal offers. */
|
|
18
|
+
export type FontSize = 'small' | 'medium' | 'large';
|
|
19
|
+
|
|
20
|
+
/** How soon an edit reaches the preview. */
|
|
21
|
+
export type UpdateMode = 'typing' | 'pause' | 'manual';
|
|
22
|
+
|
|
23
|
+
/** The pixel size each font choice maps to. */
|
|
24
|
+
export const FONT_SIZES: Record<FontSize, string> = {
|
|
25
|
+
small: '13px',
|
|
26
|
+
medium: '15px',
|
|
27
|
+
large: '18px',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* How long the playground waits for typing to stop before it rebuilds, in
|
|
32
|
+
* milliseconds. `manual` never rebuilds on its own.
|
|
33
|
+
*/
|
|
34
|
+
export const UPDATE_DELAYS: Record<UpdateMode, number | null> = {
|
|
35
|
+
typing: 800,
|
|
36
|
+
pause: 2000,
|
|
37
|
+
manual: null,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** The defaults a fresh visit, and "Reset settings", start from. */
|
|
41
|
+
export const DEFAULTS = {
|
|
42
|
+
theme: 'dark' as EditorTheme,
|
|
43
|
+
layout: 'columns' as EditorLayout,
|
|
44
|
+
fontSize: 'medium' as FontSize,
|
|
45
|
+
updateMode: 'typing' as UpdateMode,
|
|
46
|
+
prereleases: false,
|
|
47
|
+
drawerSize: 32,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Every key the playground writes, so "Reset settings" can clear them all. */
|
|
51
|
+
export const STORAGE_KEYS = {
|
|
52
|
+
theme: 'nysds-playground:editor-theme',
|
|
53
|
+
layout: 'nysds-playground:editor-layout',
|
|
54
|
+
fontSize: 'nysds-playground:font-size',
|
|
55
|
+
updateMode: 'nysds-playground:update-mode',
|
|
56
|
+
prereleases: 'nysds-playground:prereleases',
|
|
57
|
+
collapsedPanes: 'nysds-playground:collapsed-panes',
|
|
58
|
+
drawerSize: 'nysds-playground:present-editor-size',
|
|
59
|
+
codeCollapsed: 'nysds-playground:present-code-collapsed',
|
|
60
|
+
} as const;
|
|
61
|
+
|
|
62
|
+
/* URL parsing ----------------------------------------------------------- */
|
|
63
|
+
|
|
64
|
+
/** Reads `?font=`. Returns `null` when it is absent or unrecognised. */
|
|
65
|
+
export function readFontParam(search: string): FontSize | null {
|
|
66
|
+
const value = new URLSearchParams(search).get('font');
|
|
67
|
+
return value === 'small' || value === 'medium' || value === 'large' ? value : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Reads `?update=`. Returns `null` when it is absent or unrecognised. */
|
|
71
|
+
export function readUpdateParam(search: string): UpdateMode | null {
|
|
72
|
+
const value = new URLSearchParams(search).get('update');
|
|
73
|
+
return value === 'typing' || value === 'pause' || value === 'manual' ? value : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Keeps a pane ratio inside the range the layout can actually render. */
|
|
77
|
+
export function clampRatio(value: number, min: number, max: number): number {
|
|
78
|
+
if (!Number.isFinite(value)) {
|
|
79
|
+
return min;
|
|
80
|
+
}
|
|
81
|
+
return Math.min(Math.max(value, min), max);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Storage --------------------------------------------------------------- */
|
|
85
|
+
|
|
86
|
+
/** Reads one key, returning `null` when storage is unavailable or empty. */
|
|
87
|
+
export function readKey(key: string): string | null {
|
|
88
|
+
try {
|
|
89
|
+
return window.localStorage.getItem(key);
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Writes one key, ignoring storage that refuses to take it. */
|
|
96
|
+
export function writeKey(key: string, value: string): void {
|
|
97
|
+
try {
|
|
98
|
+
window.localStorage.setItem(key, value);
|
|
99
|
+
} catch {
|
|
100
|
+
// Private browsing can block storage. The setting still applies this visit.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Picks a setting: the URL first, then storage, then the default. */
|
|
105
|
+
function choose<T extends string>(
|
|
106
|
+
fromUrl: T | null,
|
|
107
|
+
key: string,
|
|
108
|
+
valid: readonly T[],
|
|
109
|
+
fallback: T,
|
|
110
|
+
): T {
|
|
111
|
+
if (fromUrl) {
|
|
112
|
+
return fromUrl;
|
|
113
|
+
}
|
|
114
|
+
const stored = readKey(key);
|
|
115
|
+
return valid.includes(stored as T) ? (stored as T) : fallback;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The code font size for this visit. */
|
|
119
|
+
export function initialFontSize(search: string = window.location.search): FontSize {
|
|
120
|
+
return choose(
|
|
121
|
+
readFontParam(search),
|
|
122
|
+
STORAGE_KEYS.fontSize,
|
|
123
|
+
['small', 'medium', 'large'],
|
|
124
|
+
DEFAULTS.fontSize,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The preview update mode for this visit. */
|
|
129
|
+
export function initialUpdateMode(search: string = window.location.search): UpdateMode {
|
|
130
|
+
return choose(
|
|
131
|
+
readUpdateParam(search),
|
|
132
|
+
STORAGE_KEYS.updateMode,
|
|
133
|
+
['typing', 'pause', 'manual'],
|
|
134
|
+
DEFAULTS.updateMode,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Whether the version menu lists prereleases. */
|
|
139
|
+
export function readPrereleases(): boolean {
|
|
140
|
+
const stored = readKey(STORAGE_KEYS.prereleases);
|
|
141
|
+
return stored === null ? DEFAULTS.prereleases : stored === '1';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Remembers whether the version menu lists prereleases. */
|
|
145
|
+
export function writePrereleases(on: boolean): void {
|
|
146
|
+
writeKey(STORAGE_KEYS.prereleases, on ? '1' : '0');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Reads a remembered pane ratio, clamped to the range the layout allows. */
|
|
150
|
+
export function readRatio(key: string, fallback: number, min: number, max: number): number {
|
|
151
|
+
const raw = readKey(key);
|
|
152
|
+
if (raw === null) {
|
|
153
|
+
return fallback;
|
|
154
|
+
}
|
|
155
|
+
const parsed = Number.parseFloat(raw);
|
|
156
|
+
return Number.isFinite(parsed) ? clampRatio(parsed, min, max) : fallback;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Remembers a pane ratio. */
|
|
160
|
+
export function writeRatio(key: string, ratio: number): void {
|
|
161
|
+
writeKey(key, ratio.toFixed(2));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Reads the collapsed editor columns. */
|
|
165
|
+
export function readCollapsedPanes(): Set<PaneId> {
|
|
166
|
+
const raw = readKey(STORAGE_KEYS.collapsedPanes);
|
|
167
|
+
if (!raw) {
|
|
168
|
+
return new Set();
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const parsed: unknown = JSON.parse(raw);
|
|
172
|
+
if (!Array.isArray(parsed)) {
|
|
173
|
+
return new Set();
|
|
174
|
+
}
|
|
175
|
+
return new Set(
|
|
176
|
+
parsed.filter((pane): pane is PaneId => (PANE_IDS as readonly unknown[]).includes(pane)),
|
|
177
|
+
);
|
|
178
|
+
} catch {
|
|
179
|
+
return new Set();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Remembers the collapsed editor columns. */
|
|
184
|
+
export function writeCollapsedPanes(collapsed: ReadonlySet<PaneId>): void {
|
|
185
|
+
writeKey(STORAGE_KEYS.collapsedPanes, JSON.stringify([...collapsed]));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Clears every playground key, so the next read falls back to the defaults. */
|
|
189
|
+
export function clearAllSettings(): void {
|
|
190
|
+
for (const key of Object.values(STORAGE_KEYS)) {
|
|
191
|
+
try {
|
|
192
|
+
window.localStorage.removeItem(key);
|
|
193
|
+
} catch {
|
|
194
|
+
// Nothing to do when storage refuses.
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Applies a code font size to the editors. */
|
|
200
|
+
export function applyFontSize(size: FontSize): void {
|
|
201
|
+
// The dark theme block also sets this property on `body`, so an inline style
|
|
202
|
+
// on `body` is what reliably wins.
|
|
203
|
+
document.body.style.setProperty('--playground-code-font-size', FONT_SIZES[size]);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Returns the URL with a setting's query parameter set. */
|
|
207
|
+
export function settingUrl(name: string, value: string, href: string): string {
|
|
208
|
+
const url = new URL(href);
|
|
209
|
+
url.searchParams.set(name, value);
|
|
210
|
+
return url.toString();
|
|
211
|
+
}
|
package/src/starters.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The decks and examples bundled with the build.
|
|
3
|
+
*
|
|
4
|
+
* These are starter content. They seed the browser store the first time the
|
|
5
|
+
* playground runs; after that the app reads only from the store. Every JSON
|
|
6
|
+
* file in `decks/` becomes a starter deck, and all of `presets/` becomes one
|
|
7
|
+
* starter deck called the component library.
|
|
8
|
+
*/
|
|
9
|
+
import type {RetiredStarter, StoredDeck} from './deck-model.ts';
|
|
10
|
+
import {makeSlide} from './deck-model.ts';
|
|
11
|
+
import type {Preset} from './preset-schema.ts';
|
|
12
|
+
import {idFromPresetPath, parseDeck, parsePreset} from './preset-schema.ts';
|
|
13
|
+
|
|
14
|
+
/** The starter id of the deck built from `presets/*.json`. */
|
|
15
|
+
export const LIBRARY_STARTER_ID = 'library';
|
|
16
|
+
|
|
17
|
+
const presetModules = import.meta.glob<unknown>('../presets/*.json', {
|
|
18
|
+
eager: true,
|
|
19
|
+
import: 'default',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const deckModules = import.meta.glob<unknown>('../decks/*.json', {
|
|
23
|
+
eager: true,
|
|
24
|
+
import: 'default',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/** A timestamp every starter shares, so seeding is deterministic. */
|
|
28
|
+
const SEEDED_AT = new Date(0).toISOString();
|
|
29
|
+
|
|
30
|
+
const libraryPresets: Preset[] = Object.keys(presetModules)
|
|
31
|
+
.sort()
|
|
32
|
+
.map((path) => parsePreset(path, presetModules[path], idFromPresetPath(path)))
|
|
33
|
+
.filter((preset): preset is Preset => preset !== null)
|
|
34
|
+
.map((preset) => makeSlide(preset));
|
|
35
|
+
|
|
36
|
+
const libraryDeck: StoredDeck = {
|
|
37
|
+
id: LIBRARY_STARTER_ID,
|
|
38
|
+
title: 'Component library',
|
|
39
|
+
description: 'One example for every part of the design system.',
|
|
40
|
+
baseCss: '',
|
|
41
|
+
slides: libraryPresets,
|
|
42
|
+
createdAt: SEEDED_AT,
|
|
43
|
+
updatedAt: SEEDED_AT,
|
|
44
|
+
starter: LIBRARY_STARTER_ID,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const bundledDecks: StoredDeck[] = Object.keys(deckModules)
|
|
48
|
+
.sort()
|
|
49
|
+
.map((path): StoredDeck | null => {
|
|
50
|
+
const deck = parseDeck(path, deckModules[path]);
|
|
51
|
+
if (!deck) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
id: deck.id,
|
|
56
|
+
title: deck.title,
|
|
57
|
+
description: deck.description,
|
|
58
|
+
baseCss: deck.baseCss,
|
|
59
|
+
slides: deck.presets.map((preset) => makeSlide(preset)),
|
|
60
|
+
createdAt: SEEDED_AT,
|
|
61
|
+
updatedAt: SEEDED_AT,
|
|
62
|
+
starter: deck.id,
|
|
63
|
+
};
|
|
64
|
+
})
|
|
65
|
+
.filter((deck): deck is StoredDeck => deck !== null);
|
|
66
|
+
|
|
67
|
+
/** Every bundled deck, with the component library first. */
|
|
68
|
+
export const STARTER_DECKS: StoredDeck[] = [libraryDeck, ...bundledDecks].filter(
|
|
69
|
+
(deck) => deck.slides.length > 0,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Starter keys that earlier builds seeded under a different file name.
|
|
74
|
+
*
|
|
75
|
+
* Renaming a file in `decks/` changes the deck's id and starter key, so a
|
|
76
|
+
* browser that seeded the old name keeps that copy and seeds the new name
|
|
77
|
+
* beside it. Listing the old name here lets `seedStarters` drop the stale copy
|
|
78
|
+
* when nothing in it has changed.
|
|
79
|
+
*/
|
|
80
|
+
export const RETIRED_STARTERS: readonly RetiredStarter[] = [
|
|
81
|
+
{
|
|
82
|
+
starter: 'styling-levels',
|
|
83
|
+
replacedBy: 'customizing-components',
|
|
84
|
+
title: 'Three levels of strictness',
|
|
85
|
+
},
|
|
86
|
+
];
|