@jarenjs/play 0.34.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/src/index.js ADDED
@@ -0,0 +1,157 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `@jarenjs/play` — a JSON-engine playground. Pick an engine, feed
4
+ * it a source input and one or more datasets, and watch it run — a way to
5
+ * understand an engine standalone before composing it in the studio.
6
+ *
7
+ * The model (PLAY-FORMAT.md): each engine is a DESCRIPTOR — the panes it
8
+ * consumes (`sourcePanes` = the input; `dataPanes` = the JSON it runs
9
+ * against, possibly none) plus a pure `run`. Each EXAMPLE presets those
10
+ * source panes plus a LIST of datasets: 0 = the engine takes no data
11
+ * (markdown/mermaid), 1 = one dataset, N = a dataset switcher (the same
12
+ * source over several shapes). The engine registry and the example library
13
+ * fill in over the next orders; this is the headless core the component
14
+ * renders.
15
+ */
16
+
17
+ import { ENGINE_LIST } from './engines.js';
18
+ import { EXAMPLE_LIST } from './examples.js';
19
+
20
+ /**
21
+ * @typedef {Object} EnginePane
22
+ * @property {string} key - the pane's id (e.g. 'selector', 'data')
23
+ * @property {string} label
24
+ * @property {'code' | 'text'} [control]
25
+ */
26
+
27
+ /**
28
+ * @typedef {Object} OptionPane
29
+ * A live select above the editors — a mode an engine runs in (JOSL vs TOML,
30
+ * CSV strict vs repair). Its value lives in the host's `config` slice.
31
+ * @property {string} key
32
+ * @property {string} label
33
+ * @property {Array<{ value: string, label: string }>} choices
34
+ * @property {string} default - the value used until the user picks another
35
+ */
36
+
37
+ /**
38
+ * @typedef {Object} Panel
39
+ * A single result SCREEN. Every ok run yields at least one; a multi-screen
40
+ * engine (CSV: a summary note, the parsed table, the CSV round-trip) yields
41
+ * several, which the component shows behind a tab strip.
42
+ * @property {string} id - unique within the result (the tab key)
43
+ * @property {string} [label] - the tab label (defaults to `id`)
44
+ * @property {'code' | 'view' | 'table' | 'note' | 'cards'} kind
45
+ * @property {'simple' | 'deep'} [depth] - `deep` panels are the engine's rich
46
+ * explainers, hidden until the student drills in via the depth toggle;
47
+ * defaults to `simple` (the calm default view)
48
+ * @property {string} [text] - `code` / `note`: the text body
49
+ * @property {any} [vnode] - `view`: a host-rendered vnode, spliced verbatim
50
+ * @property {string[]} [columns] - `table`: the header labels
51
+ * @property {Array<Array<any>>} [rows] - `table`: cells, row-major
52
+ * @property {'ok' | 'warn' | 'info'} [tone] - `note`: the callout tone
53
+ * @property {Array<{ title: string, value: string, note?: string }>} [items]
54
+ * `cards`: a row of stat cards (matches, compile/run timings, …)
55
+ */
56
+
57
+ /**
58
+ * @typedef {Object} PlayResult
59
+ * @property {boolean} ok
60
+ * @property {{ compileMs: number, runMs: number } | null} timing
61
+ * @property {{ message: string, code?: string, path?: string } | null} error
62
+ * @property {Panel[]} panels - the result screens (`[]` on error); a single
63
+ * `code` panel for most engines, several for the richer ones
64
+ */
65
+
66
+ /**
67
+ * @typedef {Object} EngineDescriptor
68
+ * @property {string} id
69
+ * @property {string} label
70
+ * @property {string} [lead] - one line describing the engine
71
+ * @property {EnginePane[]} sourcePanes - the engine INPUT pane(s)
72
+ * @property {EnginePane[]} dataPanes - the JSON it runs against (may be [])
73
+ * @property {OptionPane[]} [optionPanes] - live mode selects (may be absent)
74
+ * @property {(source: Record<string, string>, data: Record<string, string>, options?: RunOptions) => PlayResult} run
75
+ */
76
+
77
+ /**
78
+ * @typedef {Object} RunOptions
79
+ * @property {any} [operators] - a host operator registry ({ toOptions() }) for query/jslt/jtlt
80
+ * @property {Record<string, string>} [config] - the current option-pane values
81
+ * @property {Record<string, (source: string, config?: any) => any>} [renderers]
82
+ * host-injected vnode renderers keyed by engine id — the visual engines
83
+ * (markdown/mermaid/charts) delegate their rendering here (the hybrid seam),
84
+ * so the package owns the descriptors + examples but stays dependency-light.
85
+ * A renderer receives the source text plus its second argument — the
86
+ * option-pane config for the visual engines, the PARSED data document for
87
+ * `mdx` — and returns the preview vnode, or `{ vnode, deep }` where `deep`
88
+ * is extra `Panel`s (AST, canonical round-trip) the host derives — shown
89
+ * only behind the depth toggle
90
+ * @property {(schemaText: string, data: any, locale: string) => { schemaError: string|null, draft: string, compileMs: number|null, validateMs: number|null, valid: boolean|null, errors: any[] }} [validate]
91
+ * host-injected JSON Schema validator (the same seam) — the `validate`
92
+ * engine delegates here so @jarenjs/validate + the locale packs stay in the host
93
+ */
94
+
95
+ /**
96
+ * @typedef {Object} PlayExample
97
+ * @property {string} id
98
+ * @property {string} label
99
+ * @property {string} engine - an engine id
100
+ * @property {Record<string, string>} source - presets the source pane(s)
101
+ * @property {Array<{ label: string, data: Record<string, string> }>} datasets
102
+ * the JSON to run against — `[]` for a source-only engine (josl/csv/md),
103
+ * one for a single run, several for a switcher
104
+ * @property {Record<string, string>} [config] - presets option-pane values
105
+ */
106
+
107
+ /** The registered engines, by id. */
108
+ export const ENGINES = Object.freeze(/** @type {Record<string, EngineDescriptor>} */ (
109
+ Object.fromEntries(ENGINE_LIST.map((e) => [e.id, e]))));
110
+
111
+ /** The ids of the registered engines, in registration order. */
112
+ export function engineIds() {
113
+ return ENGINE_LIST.map((e) => e.id);
114
+ }
115
+
116
+ /** The curated example library (the canonical home for the suite's engine
117
+ * examples). */
118
+ export const EXAMPLES = Object.freeze(/** @type {PlayExample[]} */ (EXAMPLE_LIST));
119
+
120
+ /**
121
+ * Fill an engine's option-pane defaults so its runner always sees a
122
+ * complete `config` (the host `config` slice may hold only user overrides).
123
+ * @param {EngineDescriptor} engine
124
+ * @param {{ operators?: any, config?: Record<string, string> }} options
125
+ */
126
+ function withConfig(engine, options) {
127
+ const panes = engine.optionPanes ?? [];
128
+ if (panes.length === 0) return options;
129
+ const given = options.config ?? {};
130
+ /** @type {Record<string, string>} */
131
+ const config = {};
132
+ for (const p of panes) config[p.key] = given[p.key] ?? p.default;
133
+ return { ...options, config };
134
+ }
135
+
136
+ /**
137
+ * Run one engine over a source + data. An unknown engine (or a throwing
138
+ * runner) yields an error Result — this never throws.
139
+ * @param {string} engineId
140
+ * @param {Record<string, string>} source
141
+ * @param {Record<string, string>} data
142
+ * @param {RunOptions} [options] - operators (query/jslt), the option-pane
143
+ * config, and host renderers (markdown/mermaid/charts)
144
+ * @returns {PlayResult}
145
+ */
146
+ export function runExample(engineId, source, data, options = {}) {
147
+ const engine = ENGINES[engineId];
148
+ if (engine === undefined) {
149
+ return { ok: false, timing: null, error: { message: `unknown engine: ${engineId}` }, panels: [] };
150
+ }
151
+ try {
152
+ return engine.run(source ?? {}, data ?? {}, withConfig(engine, options));
153
+ }
154
+ catch (err) {
155
+ return { ok: false, timing: null, error: { message: String(/** @type {any} */ (err)?.message ?? err) }, panels: [] };
156
+ }
157
+ }
@@ -0,0 +1,176 @@
1
+ /* @jarenjs/play — the playground stylesheet.
2
+ *
3
+ * DESIGN.md-conformant: a rail | editors | stage grid, minmax(0,1fr) tracks
4
+ * with min-width:0 on every scrollable child, the established 1024
5
+ * breakpoint and theme tokens. Base controls (.editor, .seg / .seg-btn,
6
+ * .error-line, .code-block, .muted) are the host's shared styles — the
7
+ * playground only adds its own .jplay-* rules, no new hue family.
8
+ */
9
+
10
+ .jplay {
11
+ display: grid;
12
+ --jplay-ratio: 0.5;
13
+ gap: var(--space-3, 0.75rem);
14
+ /* rail | editors (ratio-driven) | split | result (1fr absorbs the rest);
15
+ the play-splitter widget drives --jplay-ratio live during a drag */
16
+ grid-template-columns: 12rem minmax(0, calc((100% - 12rem) * var(--jplay-ratio))) auto minmax(0, 1fr);
17
+ grid-template-areas:
18
+ "bar bar bar bar"
19
+ "rail editors split stage";
20
+ grid-template-rows: auto minmax(0, 1fr);
21
+ min-height: 24rem;
22
+ }
23
+ /* the phone pane switcher exists only below the breakpoint */
24
+ .jplay-mobilebar { display: none; }
25
+ /* ——— the IDE bar (New/Save/Load/Share + the session name) ——— */
26
+ .jplay-bar {
27
+ grid-area: bar; display: flex; align-items: center; gap: var(--space-2, 0.5rem);
28
+ flex-wrap: wrap; padding-bottom: var(--space-2, 0.5rem); border-bottom: 1px solid var(--border, #e2e8f0);
29
+ }
30
+ .jplay-title { color: var(--fg, #0f172a); }
31
+ .jplay-name {
32
+ max-width: 12rem; min-width: 0;
33
+ border: 1px solid var(--border, #e2e8f0); border-radius: var(--radius-sm, 8px);
34
+ background: var(--bg, #fff); color: var(--fg, #0f172a);
35
+ padding: var(--space-1, 0.25rem) var(--space-3, 0.75rem); font: inherit; font-size: 0.85rem;
36
+ }
37
+ .jplay-actions { display: flex; gap: var(--space-1, 0.25rem); flex-wrap: wrap; align-items: center; }
38
+ .jplay-load { max-width: 9rem; }
39
+ .jplay-shared { margin-left: auto; }
40
+ /* ——— the editors|result drag splitter (host = the grab bar) ——— */
41
+ .jplay-split {
42
+ grid-area: split; cursor: col-resize; width: 10px; align-self: stretch; touch-action: none;
43
+ display: flex; align-items: center; justify-content: center;
44
+ }
45
+ .jplay-split::before { content: ''; width: 2px; height: 100%; background: var(--border, #e2e8f0); border-radius: 1px; }
46
+ .jplay-split:hover::before, .jplay-split:focus-visible::before { background: var(--accent, #2563eb); }
47
+ .jplay-split:focus-visible { outline: none; }
48
+ .jplay-rail { grid-area: rail; min-width: 0; display: flex; flex-direction: column; overflow-y: auto; }
49
+ .jplay-editors { grid-area: editors; min-width: 0; display: flex; flex-direction: column; gap: var(--space-2, 0.5rem); }
50
+ .jplay-stage { grid-area: stage; min-width: 0; display: flex; flex-direction: column; }
51
+
52
+ /* ——— the example rail ——— */
53
+ .jplay-group-head {
54
+ font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em;
55
+ margin-block: var(--space-2, 0.5rem) 2px;
56
+ }
57
+ .jplay-ex {
58
+ display: block; width: 100%; text-align: left; border: none; background: transparent;
59
+ cursor: pointer; padding: 3px var(--space-2, 0.5rem);
60
+ border-radius: var(--radius-sm, 6px); border-left: 2px solid transparent;
61
+ color: var(--muted, #64748b); font-size: 0.85rem;
62
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
63
+ }
64
+ .jplay-ex:hover { color: var(--fg, #0f172a); }
65
+ .jplay-ex.active { color: var(--fg, #0f172a); background: var(--accent-soft, #eff6ff); border-left-color: var(--accent, #2563eb); }
66
+
67
+ /* ——— the editors ——— */
68
+ .jplay-head { display: flex; align-items: baseline; gap: var(--space-2, 0.5rem); flex-wrap: wrap; }
69
+ .jplay-engine { color: var(--fg, #0f172a); }
70
+ .jplay-lead { font-size: 0.8rem; }
71
+ .jplay-pane { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
72
+ .jplay-pane-label { font-size: 0.7rem; font-family: var(--mono, monospace); }
73
+ .jplay-datasets { align-self: flex-start; }
74
+ /* the validate engine's JSON ↔ form toggle + the generated-form container */
75
+ .jplay-dataview { align-self: flex-start; }
76
+ .jplay-form { min-width: 0; }
77
+
78
+ /* ——— option selects (josl dialect, csv strict/repair) ——— */
79
+ .jplay-options { display: flex; flex-wrap: wrap; gap: var(--space-2, 0.5rem); }
80
+ .jplay-options:empty { display: none; }
81
+ .jplay-option { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
82
+ .jplay-option select { min-width: 0; max-width: 100%; }
83
+
84
+ /* ——— the stage ——— */
85
+ .jplay-stage-head { font-family: var(--mono, monospace); font-size: 0.75rem; padding-bottom: var(--space-2, 0.5rem); }
86
+ .jplay-result { flex: 1; min-height: 0; overflow: auto; overscroll-behavior: contain; }
87
+ /* a rendered visual result (markdown HTML, mermaid/charts SVG) */
88
+ .jplay-view { min-width: 0; }
89
+ .jplay-view svg { max-width: 100%; height: auto; }
90
+ .jplay-timing { font-size: 0.75rem; margin: 0 0 var(--space-2, 0.5rem); }
91
+ .jplay-hint { padding: var(--space-3, 0.75rem); }
92
+
93
+ /* ——— multi-panel results: a tab strip (host .seg/.seg-btn) + panel bodies ——— */
94
+ .jplay-tabs { align-self: flex-start; flex-wrap: wrap; margin-bottom: var(--space-2, 0.5rem); }
95
+ /* a `note` panel — a callout, tone-coloured, repair lines on their own line */
96
+ .jplay-note {
97
+ margin: 0; padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
98
+ border-left: 3px solid var(--border, #e2e8f0); border-radius: var(--radius-sm, 8px);
99
+ background: var(--surface, #f8fafc); font-size: 0.85rem; white-space: pre-line;
100
+ }
101
+ .jplay-note.ok { border-left-color: var(--ok, #16a34a); background: var(--ok-soft, #f0fdf4); }
102
+ .jplay-note.warn { border-left-color: var(--warn, #d97706); background: var(--warn-soft, #fffbeb); }
103
+ .jplay-note.info { border-left-color: var(--accent, #2563eb); background: var(--accent-soft, #eff6ff); }
104
+ /* a `table` panel — a scrollable bordered grid with a sticky header */
105
+ .jplay-table-wrap { overflow-x: auto; }
106
+ .jplay-table { border-collapse: collapse; font-size: 0.8rem; font-family: var(--mono, monospace); }
107
+ .jplay-table th, .jplay-table td {
108
+ border: 1px solid var(--border, #e2e8f0); padding: 2px var(--space-2, 0.5rem);
109
+ text-align: left; white-space: nowrap;
110
+ }
111
+ .jplay-table th { position: sticky; top: 0; background: var(--surface, #f8fafc); color: var(--fg, #0f172a); }
112
+
113
+ /* ——— the drill-deeper depth toggle: calm by default, deep on demand ———
114
+ * Desktop: the simple answer stays visible and the deep panels open BENEATH
115
+ * it as an additional tab row — the learner sees the answer and the
116
+ * machinery side by side. Mobile swaps panes instead (see the breakpoint). */
117
+ .jplay-deep { margin-top: var(--space-3, 0.75rem); border-top: 1px dashed var(--border, #e2e8f0); padding-top: var(--space-2, 0.5rem); }
118
+ .jplay-deep-toggle {
119
+ border: none; background: transparent; cursor: pointer; padding: var(--space-1, 0.25rem) 0;
120
+ color: var(--accent, #2563eb); font-size: 0.8rem; font-family: var(--mono, monospace);
121
+ }
122
+ .jplay-deep-toggle:hover { text-decoration: underline; }
123
+ .jplay-deep-body { display: flex; flex-direction: column; gap: var(--space-2, 0.5rem); margin-top: var(--space-1, 0.25rem); }
124
+ .jplay-deep-tabs { align-self: flex-start; flex-wrap: wrap; }
125
+ /* the ← back affordance exists for the mobile full-pane swap only */
126
+ .jplay-deep-back { display: none; }
127
+ /* a `cards` panel — a row of stat cards (matches, timings, identity) */
128
+ .jplay-cards { display: flex; flex-wrap: wrap; gap: var(--space-2, 0.5rem); }
129
+ .jplay-card {
130
+ display: flex; flex-direction: column; gap: 2px; min-width: 7rem;
131
+ border: 1px solid var(--border, #e2e8f0); border-radius: var(--radius-sm, 6px);
132
+ padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem); background: var(--surface, #f8fafc);
133
+ }
134
+ .jplay-card-title { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; }
135
+ .jplay-card-value { color: var(--fg, #0f172a); font-size: 1.1rem; }
136
+ .jplay-card-note { font-size: 0.7rem; }
137
+
138
+ @media (max-width: 1024px) {
139
+ /* ONE pane at a time behind the segmented switcher — a phone screen
140
+ never stacks rail + editors + result into a tall scroll */
141
+ .jplay {
142
+ grid-template-columns: minmax(0, 1fr);
143
+ grid-template-areas: "bar" "switch" "rail" "editors" "stage";
144
+ grid-template-rows: auto;
145
+ }
146
+ .jplay-mobilebar { display: flex; grid-area: switch; align-self: start; }
147
+ .jplay-mobilebar .seg-btn { flex: 1; }
148
+ .jplay[data-pane='examples'] .jplay-editors,
149
+ .jplay[data-pane='examples'] .jplay-stage,
150
+ .jplay[data-pane='editor'] .jplay-rail,
151
+ .jplay[data-pane='editor'] .jplay-stage,
152
+ .jplay[data-pane='result'] .jplay-rail,
153
+ .jplay[data-pane='result'] .jplay-editors { display: none; }
154
+ /* no thin drag handle on touch — the switcher owns the pane choice */
155
+ .jplay-split { display: none; }
156
+ /* the keyboard seam: the editor pane reserves the published inset as
157
+ scroll room, so the caret clears the on-screen keyboard */
158
+ .jplay-editors { padding-bottom: var(--kb-inset, 0px); }
159
+ /* touch targets: rows, tabs, segments and bar buttons are finger-sized */
160
+ .jplay-ex, .jplay .seg-btn, .jplay-bar .btn { min-height: 44px; }
161
+ .jplay-ex { display: flex; align-items: center; }
162
+ /* the drill-down is a full-pane SWAP on a phone: there is no room to
163
+ * stack the machinery under the answer (and never under the keyboard),
164
+ * so the open deep view replaces the simple result, one deep panel at a
165
+ * time, with a ← back to the answer */
166
+ .jplay-result.deep-on .jplay-simple { display: none; }
167
+ .jplay-result.deep-on .jplay-deep { margin-top: 0; border-top: none; padding-top: 0; }
168
+ .jplay-result.deep-on .jplay-deep-toggle { display: none; }
169
+ .jplay-deep-back {
170
+ display: inline-flex; align-items: center; align-self: flex-start;
171
+ border: none; background: transparent; cursor: pointer;
172
+ color: var(--accent, #2563eb); font-size: 0.85rem; padding: 0;
173
+ }
174
+ /* touch targets: the deep affordances are finger-sized on a phone */
175
+ .jplay-deep-toggle, .jplay-deep-back, .jplay-deep-tabs .seg-btn { min-height: 44px; }
176
+ }