@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/README.md +55 -0
- package/dist/types/component/index.d.ts +80 -0
- package/dist/types/component/view.d.ts +49 -0
- package/dist/types/component/viewmodel.d.ts +7 -0
- package/dist/types/engines.d.ts +10 -0
- package/dist/types/examples.d.ts +9 -0
- package/dist/types/format.d.ts +15 -0
- package/dist/types/index.d.ts +272 -0
- package/docs/PLAY-FORMAT.md +156 -0
- package/package.json +64 -0
- package/src/component/index.js +53 -0
- package/src/component/view.js +318 -0
- package/src/component/viewmodel.js +181 -0
- package/src/engines.js +516 -0
- package/src/examples.js +546 -0
- package/src/format.js +20 -0
- package/src/index.js +157 -0
- package/styles/play.css +176 -0
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
|
+
}
|
package/styles/play.css
ADDED
|
@@ -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
|
+
}
|