@jarenjs/studio 0.83.3 → 0.85.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 (90) hide show
  1. package/README.md +21 -6
  2. package/contracts/data.contract.json +243 -0
  3. package/dist/types/component/data/actions.d.ts +234 -0
  4. package/dist/types/component/data/index.d.ts +9 -0
  5. package/dist/types/component/data/mount.d.ts +87 -0
  6. package/dist/types/component/data/project-widget.d.ts +27 -0
  7. package/dist/types/component/data/viewmodel.d.ts +85 -0
  8. package/dist/types/component/data/views.d.ts +148 -0
  9. package/dist/types/component/document.d.ts +82 -0
  10. package/dist/types/component/flow/actions.d.ts +708 -0
  11. package/dist/types/component/flow/index.d.ts +7 -0
  12. package/dist/types/component/flow/mount.d.ts +66 -0
  13. package/dist/types/component/flow/project-widget.d.ts +8 -0
  14. package/dist/types/component/flow/runtime.d.ts +57 -0
  15. package/dist/types/component/flow/views.d.ts +242 -0
  16. package/dist/types/component/host.d.ts +2 -2
  17. package/dist/types/component/index.d.ts +21 -15
  18. package/dist/types/component/mount.d.ts +51 -0
  19. package/dist/types/component/project-actions.d.ts +302 -0
  20. package/dist/types/component/project-controller.d.ts +81 -0
  21. package/dist/types/component/project-state.d.ts +1 -0
  22. package/dist/types/component/project.d.ts +327 -0
  23. package/dist/types/component/shared/host-widget.d.ts +24 -0
  24. package/dist/types/component/shared/memo.d.ts +12 -0
  25. package/dist/types/component/shared/nodes.d.ts +92 -0
  26. package/dist/types/component/shared/schema-options.d.ts +28 -0
  27. package/dist/types/component/shared/studio-kit.d.ts +78 -0
  28. package/dist/types/component/shared/ui.d.ts +136 -0
  29. package/dist/types/component/view.d.ts +6 -6
  30. package/dist/types/data/boot-stages.d.ts +97 -0
  31. package/dist/types/data/browser-worker.d.ts +22 -0
  32. package/dist/types/data/contract.d.ts +9 -0
  33. package/dist/types/data/editor.d.ts +81 -0
  34. package/dist/types/data/handlers.d.ts +130 -0
  35. package/dist/types/data/host.d.ts +8 -0
  36. package/dist/types/data/project-worker.d.ts +8 -0
  37. package/dist/types/data/runtime.d.ts +46 -0
  38. package/dist/types/data/state.d.ts +33 -0
  39. package/dist/types/data/storage.d.ts +26 -0
  40. package/dist/types/data/transport.d.ts +69 -0
  41. package/dist/types/flow-document.d.ts +19 -0
  42. package/dist/types/flow-editor.d.ts +88 -0
  43. package/docs/EDITORS.md +158 -0
  44. package/docs/PROJECT-FORMAT.md +1 -11
  45. package/package.json +32 -15
  46. package/src/component/data/actions.js +138 -0
  47. package/src/component/data/index.js +10 -0
  48. package/src/component/data/mount.js +63 -0
  49. package/src/component/data/project-widget.js +164 -0
  50. package/src/component/data/viewmodel.js +185 -0
  51. package/src/component/data/views.js +241 -0
  52. package/src/component/document.js +344 -0
  53. package/src/component/flow/actions.js +331 -0
  54. package/src/component/flow/index.js +8 -0
  55. package/src/component/flow/mount.js +53 -0
  56. package/src/component/flow/project-widget.js +44 -0
  57. package/src/component/flow/runtime.js +481 -0
  58. package/src/component/flow/views.js +196 -0
  59. package/src/component/host.js +2 -2
  60. package/src/component/index.js +19 -9
  61. package/src/component/mount.js +43 -0
  62. package/src/component/project-actions.js +189 -0
  63. package/src/component/project-controller.js +248 -0
  64. package/src/component/project-state.js +30 -0
  65. package/src/component/project.js +308 -0
  66. package/src/component/shared/host-widget.js +35 -0
  67. package/src/component/shared/memo.js +28 -0
  68. package/src/component/shared/nodes.js +94 -0
  69. package/src/component/shared/schema-options.js +30 -0
  70. package/src/component/shared/studio-kit.js +59 -0
  71. package/src/component/shared/ui.js +134 -0
  72. package/src/data/boot-stages.js +202 -0
  73. package/src/data/browser-worker.js +247 -0
  74. package/src/data/contract.js +7 -0
  75. package/src/data/editor.js +95 -0
  76. package/src/data/handlers.js +349 -0
  77. package/src/data/host.js +8 -0
  78. package/src/data/project-worker.js +26 -0
  79. package/src/data/runtime.js +462 -0
  80. package/src/data/state.js +46 -0
  81. package/src/data/storage.js +61 -0
  82. package/src/data/transport.js +215 -0
  83. package/src/flow-document.js +24 -0
  84. package/src/flow-editor.js +98 -0
  85. package/styles/data.css +53 -0
  86. package/styles/editor.css +150 -0
  87. package/styles/flow.css +99 -0
  88. package/styles/studio.css +1 -0
  89. package/dist/types/author.d.ts +0 -27
  90. package/src/author.js +0 -55
@@ -0,0 +1,215 @@
1
+ //@ts-check
2
+ import { openPortClient } from '@jarenjs/contract/port';
3
+ import { dataContract } from './contract.js';
4
+ import { createStageRunner, bootFailure, BOOT_STAGES } from './boot-stages.js';
5
+
6
+ /**
7
+ * The browser-shaped dependencies of a transport: how a worker is
8
+ * spawned, how a port client is opened over a channel, how the shared
9
+ * channel is opened, and the stage budgets. The page supplies the
10
+ * platform's; a Node test supplies fakes and millisecond budgets.
11
+ * @typedef {Object} TransportDeps
12
+ * @property {() => any} spawnWorker - a fresh owner worker
13
+ * @property {(channel: any) => any} [openClient] - a port client over a channel
14
+ * @property {() => any} openChannel - the shared tab channel
15
+ * @property {Readonly<Record<string, number>>} [budgets] - per-stage budgets
16
+ * @property {(fn: () => void, ms: number) => any} [setTimer]
17
+ * @property {(handle: any) => void} [clearTimer]
18
+ */
19
+
20
+ /**
21
+ * The transport: a contract PORT client over the own worker (owner) or
22
+ * the shared channel (client). `request` unwraps the binding's D6
23
+ * outcome into the value-or-throw shape the effects consume — a
24
+ * declared `db` failure surfaces the store's own code and message from
25
+ * its details; `subscribe` is the client's stream half, passed through.
26
+ *
27
+ * `boot()` is the closed protocol of `lib/boot-stages.js`: the worker
28
+ * starting (its `ready` frame, or its `error` event), then the worker's
29
+ * own `data.init` under the three stages it announces as it passes them
30
+ * (`sqlite-init`, `vfs-acquire`, `topology`) — each bounded from here,
31
+ * so a stage the worker never finishes fails under its own name and
32
+ * never masquerades as an OPFS absence. `bounded('store-open', …)` is
33
+ * the fifth stage, run by the boot effect around the first open.
34
+ * `close()` releases everything this transport created — the client,
35
+ * the worker, the shared channel, the listeners — exactly once, so a
36
+ * failed boot leaves nothing behind and a retry starts clean.
37
+ * @param {TransportDeps} [deps]
38
+ */
39
+ export function createTransport(deps) {
40
+ const openClient = deps.openClient ?? (channel => openPortClient(dataContract, { channel, timeoutMs: 30_000 }));
41
+ const runner = createStageRunner({ budgets: deps.budgets, setTimer: deps.setTimer, clearTimer: deps.clearTimer });
42
+ /** @type {any} */
43
+ let client = null;
44
+ /** @type {any} */
45
+ let worker = null;
46
+ /** @type {any} */
47
+ let shared = null;
48
+ /** @type {{ target: any, type: string, fn: any }[]} */
49
+ const listeners = [];
50
+ let closed = false;
51
+ /** The stage a boot is in, for a failure that arrives outside a run. */
52
+ let stage = BOOT_STAGES[0];
53
+
54
+ /** @param {any} outcome */
55
+ const unwrap = (outcome) => {
56
+ if (outcome.ok) return outcome.value;
57
+ const details = outcome.error.details;
58
+ const dbError = details !== null && typeof details === 'object' && typeof details.message === 'string';
59
+ const error = new Error(dbError ? details.message : outcome.error.message);
60
+ /** @type {any} */ (error).code = dbError ? details.code : outcome.error.code;
61
+ throw error;
62
+ };
63
+
64
+ const request = async (op, args) => {
65
+ if (client === null) throw new Error('the data transport is closed');
66
+ return unwrap(await client.invoke(op, args));
67
+ };
68
+
69
+ /** @param {any} input @param {any} callbacks */
70
+ const subscribe = (input, callbacks) => client.subscribe('data.live', input, callbacks);
71
+
72
+ /** @type {((notice: any) => void) | null} */
73
+ let onNotice = null;
74
+ /** @type {((stage: string) => void) | null} */
75
+ let onStage = null;
76
+ /** @type {((fault: { message: string }) => void) | null} */
77
+ let onFault = null;
78
+ /** Whether the boot has settled: a worker `error` after that is a fault
79
+ * of a running store, not of a boot stage, and is reported as one. */
80
+ let booted = false;
81
+ /** Attach a listener this transport will detach on close. */
82
+ const on = (/** @type {any} */ target, /** @type {string} */ type, /** @type {any} */ fn) => {
83
+ target.addEventListener(type, fn);
84
+ listeners.push({ target, type, fn });
85
+ };
86
+ /** The owner's store-changed notices and the worker's boot-stage
87
+ * announcements travel beside the contract frames on whichever
88
+ * transport this tab ended up on, and are told apart by shape — the
89
+ * same arrangement the owner-discovery frames use. */
90
+ const listen = (/** @type {any} */ target) => {
91
+ on(target, 'message', (/** @type {any} */ event) => {
92
+ const message = event.data;
93
+ if (message === null || typeof message !== 'object') return;
94
+ if (typeof message.boot === 'string') {
95
+ // only the five stages are stages: a foreign frame on the shared
96
+ // channel cannot rename the failure this tab reports
97
+ if (BOOT_STAGES.includes(message.boot)) {
98
+ stage = message.boot;
99
+ onStage?.(message.boot);
100
+ }
101
+ return;
102
+ }
103
+ if (message.store !== undefined) onNotice?.(message);
104
+ });
105
+ };
106
+
107
+ /** Release everything created here, once; a second close is a no-op. */
108
+ const close = () => {
109
+ if (closed) return;
110
+ closed = true;
111
+ runner.dispose();
112
+ for (const { target, type, fn } of listeners) {
113
+ try {
114
+ target.removeEventListener(type, fn);
115
+ }
116
+ catch { /* a terminated worker may refuse; nothing to release then */ }
117
+ }
118
+ listeners.length = 0;
119
+ try {
120
+ client?.close();
121
+ }
122
+ catch { /* already closed */ }
123
+ client = null;
124
+ try {
125
+ worker?.terminate();
126
+ }
127
+ catch { /* already gone */ }
128
+ worker = null;
129
+ try {
130
+ shared?.close();
131
+ }
132
+ catch { /* already closed */ }
133
+ shared = null;
134
+ onNotice = null;
135
+ onStage = null;
136
+ onFault = null;
137
+ };
138
+
139
+ /**
140
+ * One bounded stage. A failure inside it — the work's, or its budget
141
+ * running out — is the named boot failure, and never something else.
142
+ * @param {string} name @param {(advance: (next: string) => void) => any} work
143
+ */
144
+ const bounded = (name, work) => {
145
+ stage = name;
146
+ return runner.run(name, work);
147
+ };
148
+
149
+ /** The worker has started when its module posts `{ ready: true }`; a
150
+ * module that fails to load fires `error` instead. */
151
+ const workerStart = () => new Promise((resolve, reject) => {
152
+ on(worker, 'message', (/** @type {any} */ event) => {
153
+ if (event.data !== null && typeof event.data === 'object' && event.data.ready === true) resolve(true);
154
+ });
155
+ on(worker, 'error', (/** @type {any} */ event) => {
156
+ const message = typeof event?.message === 'string' && event.message.length > 0
157
+ ? event.message : 'the data worker failed';
158
+ if (booted) {
159
+ // the store was up: the page must not go on reading `ready`
160
+ onFault?.({ message: `the data worker stopped: ${message}` });
161
+ return;
162
+ }
163
+ reject(bootFailure('worker-start', new Error(message)));
164
+ });
165
+ });
166
+
167
+ const boot = async () => {
168
+ if (closed) throw bootFailure('worker-start', new Error('the data transport is closed'));
169
+ worker = deps.spawnWorker();
170
+ client = openClient(worker);
171
+ listen(worker);
172
+ await bounded('worker-start', () => workerStart());
173
+ // the worker's init announces its own stages as it passes them; each
174
+ // announcement moves the budget to the next stage
175
+ const status = await bounded('sqlite-init', (advance) => {
176
+ onStage = advance;
177
+ return request('data.init', null);
178
+ });
179
+ onStage = null;
180
+ if (status.topology !== 'client') {
181
+ // 'owner' (holds the OPFS pool) or 'memory' (OPFS absent — a
182
+ // standalone in-memory store): either way this worker IS the
183
+ // connection; an owner also serves the channel for client tabs.
184
+ return status;
185
+ }
186
+ // another context owns the pool: downgrade to a CLIENT over the
187
+ // channel; this direct worker has nothing to hold, so it dies.
188
+ client.close();
189
+ worker.terminate();
190
+ client = null;
191
+ worker = null;
192
+ shared = deps.openChannel();
193
+ listen(shared);
194
+ client = openClient(shared);
195
+ return status;
196
+ };
197
+ /** The boot settled: later worker faults are the running store's. */
198
+ const settled = () => { booted = true; };
199
+
200
+ return {
201
+ boot,
202
+ bounded,
203
+ request,
204
+ subscribe,
205
+ close,
206
+ /** The stage the last boot reached (for a failure outside a run). */
207
+ stage: () => stage,
208
+ /** @param {(notice: any) => void} cb */
209
+ notices: (cb) => { onNotice = cb; },
210
+ /** @param {(fault: { message: string }) => void} cb - a worker that
211
+ * fails after the boot settled */
212
+ faults: (cb) => { onFault = cb; },
213
+ settled,
214
+ };
215
+ }
@@ -0,0 +1,24 @@
1
+ //@ts-check
2
+ /** Validation shared by text commits and revision-checked publication. */
3
+ import { validateFile } from './validate.js';
4
+ import { JarenValidator } from '@jarenjs/validate';
5
+ import { jsonFormats } from '@jarenjs/formats';
6
+ import { OUR_SCHEMA_OPTIONS } from './component/shared/schema-options.js';
7
+ import dagSchema from '@jarenjs/flow/schemas/jaren-dag.schema.json' with { type: 'json' };
8
+ import querySchema from '@jarenjs/json/schemas/jaren-query.schema.json' with { type: 'json' };
9
+ import jsltSchema from '@jarenjs/json/schemas/jaren-jslt.schema.json' with { type: 'json' };
10
+ export const validateDagDoc = new JarenValidator(OUR_SCHEMA_OPTIONS)
11
+ .addFormats(jsonFormats)
12
+ .addSchema(querySchema)
13
+ .addSchema(jsltSchema)
14
+ .compile(dagSchema);
15
+
16
+
17
+ /** @param {'fsm'|'dag'} kind @param {any} document */
18
+ export function validateFlowDocument(kind, document) {
19
+ try {
20
+ if (kind !== 'fsm' && kind !== 'dag') throw new TypeError('The Flow kind must be fsm or dag.');
21
+ return validateFile({ kind, text: JSON.stringify(document) });
22
+ }
23
+ catch (error) { return { valid: false, errors: [{ code: error.code ?? 'JF0001', message: error.message, docPath: error.docPath ?? '' }] }; }
24
+ }
@@ -0,0 +1,98 @@
1
+ //@ts-check
2
+ /** @typedef {{ kind: 'fsm'|'dag'|null, document: any, revision: string, result: any }} FlowSnapshot */
3
+ /** @typedef {{ ok: boolean, document?: any, revision?: string, conflict?: boolean, error?: string,
4
+ * errors?: any[], valid?: boolean, total?: number, code?: string, output?: any, started?: boolean }} EditorReceipt */
5
+ /** Revision-checked Flow publication over the editor's own queued actions. */
6
+ import { semanticKey } from '@jarenjs/core/object';
7
+ import { applyJSONPatch } from '@jarenjs/json/patch';
8
+ import { validateFlowDocument } from './flow-document.js';
9
+
10
+ /** @param {{ kind?: 'fsm'|'dag'|null, document?: any, input?: any }} [options] */
11
+ export function createFlowState(options = {}) {
12
+ return {
13
+ kind: options.kind ?? null, doc: options.document == null ? null : structuredClone(options.document),
14
+ selection: null, connect: null, tab: 'diagram', parseError: null, history: { past: [], future: [] },
15
+ run: null, runContext: structuredClone(options.input ?? null),
16
+ dagInput: options.input === undefined ? '' : JSON.stringify(options.input), revision: 0, mobilePane: 'diagram',
17
+ };
18
+ }
19
+
20
+ /** @param {{ getApp: () => any, runtime: any }} env */
21
+ export function createFlowController(env) {
22
+ let stopped = false, detach = null, sequence = 0;
23
+ const requests = new Map(), listeners = new Set();
24
+ const copy = value => structuredClone(value);
25
+ /** @returns {FlowSnapshot} */
26
+ function read() {
27
+ const { kind, doc, run } = env.getApp().getState().flow;
28
+ return { kind, document: copy(doc), revision: semanticKey({ kind, doc }), result: copy(run) };
29
+ }
30
+ /** @param {any} candidate @param {'fsm'|'dag'} [kind] */
31
+ function validate(candidate, kind = env.getApp().getState().flow.kind) { return validateFlowDocument(kind, candidate); }
32
+ function settle(id, result) { const resolve = requests.get(id); requests.delete(id); resolve?.({ ...read(), ...result }); }
33
+ /** @param {any} candidate @param {{ expectedRevision?: string, kind?: 'fsm'|'dag' }} [options]
34
+ * @returns {Promise<EditorReceipt>}
35
+ */
36
+ function replace(candidate, options = {}) {
37
+ if (stopped) return Promise.resolve({ ok: false, error: 'The Flow editor is disposed.' });
38
+ const before = read(), kind = options.kind ?? before.kind;
39
+ if (before.kind !== null && kind !== before.kind) return Promise.resolve({ ok: false, error: 'A replacement must keep the current Flow kind; open a new document to change it.' });
40
+ if (options.expectedRevision !== before.revision) return Promise.resolve({ ok: false, conflict: true, ...before });
41
+ const checked = validate(candidate, kind);
42
+ if (!checked.valid) return Promise.resolve({ ok: false, ...checked });
43
+ return new Promise(resolve => {
44
+ const id = ++sequence; requests.set(id, resolve);
45
+ env.getApp().dispatch('flow/replace', { requestId: id, expected: { kind: before.kind, doc: before.document }, kind, doc: copy(candidate) });
46
+ });
47
+ }
48
+ /** @param {Parameters<typeof applyJSONPatch>[1]} patch @param {{ expectedRevision?: string, kind?: 'fsm'|'dag' }} [options]
49
+ * @returns {Promise<EditorReceipt>}
50
+ */
51
+ function apply(patch, options) {
52
+ try { return replace(applyJSONPatch(read().document, patch), options); }
53
+ catch (error) { return Promise.resolve({ ok: false, error: error.message, code: error.code }); }
54
+ }
55
+ /** Execute with the host's task registry and explicit input; return the settled DAG result or machine start receipt.
56
+ * @param {{ input?: any, event?: string }} [options]
57
+ * @returns {Promise<EditorReceipt>}
58
+ */
59
+ function run(options = {}) {
60
+ if (stopped) return Promise.resolve({ ok: false, error: 'The Flow editor is disposed.' });
61
+ const state = env.getApp().getState().flow;
62
+ if (state.doc === null) return Promise.resolve({ ok: false, error: 'No Flow document is open.' });
63
+ return new Promise(resolve => {
64
+ const id = ++sequence; requests.set(id, resolve);
65
+ env.getApp().dispatch('flow/execute', { requestId: id, input: options.input ?? state.runContext,
66
+ inputText: options.input === undefined ? state.dagInput : JSON.stringify(options.input), event: options.event ?? null });
67
+ });
68
+ }
69
+ /** @param {(snapshot: FlowSnapshot) => void} listener */
70
+ function subscribe(listener) { if (stopped) return () => {}; listeners.add(listener); return () => listeners.delete(listener); }
71
+ function attach() {
72
+ if (stopped || detach) return;
73
+ let revision = read().revision;
74
+ detach = env.getApp().subscribe(() => {
75
+ const current = read();
76
+ if (current.revision !== revision) { revision = current.revision; for (const listener of listeners) listener(current); }
77
+ });
78
+ }
79
+ function dispose() {
80
+ if (stopped) return;
81
+ stopped = true; detach?.(); detach = null; listeners.clear(); env.runtime.dispose();
82
+ for (const resolve of requests.values()) resolve({ ok: false, error: 'The Flow editor is disposed.' });
83
+ requests.clear();
84
+ }
85
+ const effects = {
86
+ 'flow-accepted': id => { env.runtime.effects['flow-dag-abort'](); settle(id, { ok: true }); },
87
+ 'flow-refused': id => settle(id, { ok: false, conflict: true }),
88
+ 'flow-editor-run': Object.assign(async (props, dispatch) => {
89
+ if (stopped) { settle(props.requestId, { ok: false, error: 'The Flow editor is disposed.' }); return; }
90
+ if (props.kind === 'fsm') {
91
+ dispatch(props.event === null ? 'flow/run' : 'flow/send', props.event);
92
+ settle(props.requestId, { ok: true, started: props.event === null });
93
+ }
94
+ else settle(props.requestId, await env.runtime.effects['flow-dag-run'](props, dispatch));
95
+ }, { dispose }),
96
+ };
97
+ return { read, validate, replace, apply, run, subscribe, attach, effects, dispose };
98
+ }
@@ -0,0 +1,53 @@
1
+ @import './editor.css';
2
+ /* Complete Data editor layout and responsive panes, scoped to each mount. */
3
+
4
+ :where(.jaren-data-editor) .data-panebar { display: none; }
5
+ @media (max-width: 1024px) {
6
+ :where(.jaren-data-editor) .data-grid { padding-bottom: var(--kb-inset, 0px); }
7
+ }
8
+ @media (max-width: 1024px) {
9
+ :where(.jaren-data-editor) .data-panebar { display: flex; }
10
+ }
11
+ @media (max-width: 1024px) {
12
+ :where(.jaren-data-editor) .data-panebar .seg-btn { flex: 1; }
13
+ }
14
+ @media (max-width: 1024px) {
15
+ :where(.jaren-data-editor) .data-panebar,
16
+ :where(.jaren-data-editor) .data-grid > .card { grid-column: 1 / -1; }
17
+ }
18
+ @media (max-width: 1024px) {
19
+
20
+ :where(.jaren-data-editor) .data-grid[data-pane='store'] .data-query,
21
+ :where(.jaren-data-editor) .data-grid[data-pane='store'] .data-live,
22
+ :where(.jaren-data-editor) .data-grid[data-pane='store'] .data-trip,
23
+ :where(.jaren-data-editor) .data-grid[data-pane='query'] .data-status,
24
+ :where(.jaren-data-editor) .data-grid[data-pane='query'] .data-live,
25
+ :where(.jaren-data-editor) .data-grid[data-pane='query'] .data-trip,
26
+ :where(.jaren-data-editor) .data-grid[data-pane='live'] .data-status,
27
+ :where(.jaren-data-editor) .data-grid[data-pane='live'] .data-query,
28
+ :where(.jaren-data-editor) .data-grid[data-pane='live'] .data-trip,
29
+ :where(.jaren-data-editor) .data-grid[data-pane='trip'] .data-status,
30
+ :where(.jaren-data-editor) .data-grid[data-pane='trip'] .data-query,
31
+ :where(.jaren-data-editor) .data-grid[data-pane='trip'] .data-live { display: none; }
32
+ }
33
+ @media (max-width: 1024px) {
34
+ :where(.jaren-data-editor) .data-grid .btn,
35
+ :where(.jaren-data-editor) .data-grid .seg-btn { min-height: 44px; }
36
+ }
37
+ :where(.jaren-data-editor) .data-grid {
38
+ display: grid;
39
+ grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr));
40
+ gap: var(--space-4, 1rem);
41
+ align-items: start;
42
+ }
43
+ :where(.jaren-data-editor) .data-rows { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2, 0.5rem); }
44
+ :where(.jaren-data-editor) .data-row {
45
+ display: flex; align-items: center; gap: var(--space-2, 0.5rem);
46
+ border: 1px solid var(--border, hsl(220 14% 88%)); border-radius: var(--radius-sm, 8px);
47
+ background: var(--bg, hsl(0 0% 100%)); padding: var(--space-1, 0.25rem) var(--space-2, 0.5rem);
48
+ }
49
+ :where(.jaren-data-editor) .data-row code { flex: 1; min-width: 0; overflow-x: auto; white-space: nowrap; font-size: 0.8rem; }
50
+ :where(.jaren-data-editor) .data-row-delete { flex: none; color: var(--muted, hsl(220 12% 45%)); }
51
+ :where(.jaren-data-editor) .data-row-delete:hover { color: var(--fail, hsl(0 68% 46%)); border-color: var(--fail, hsl(0 68% 46%)); }
52
+ :where(.jaren-data-editor) .data-trip-map { margin-block: var(--space-3, 0.75rem); padding: 0; border: 0; background: transparent; }
53
+ :where(.jaren-data-editor) .data-trip-narrative { word-break: break-word; }
@@ -0,0 +1,150 @@
1
+ /* Shared editor primitives inherit host design tokens, with usable standalone defaults. */
2
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) { min-width: 0; color: var(--fg, hsl(224 20% 14%)); font-family: var(--font, 'Inter', ui-sans-serif, system-ui, sans-serif); }
3
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio), :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) * { box-sizing: border-box; }
4
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) button, :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) input, :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) textarea, :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) select { font: inherit; }
5
+
6
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .editor { font-family: var(--mono, 'JetBrains Mono', ui-monospace, 'Cascadia Code', monospace); font-size: 0.86em; }
7
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .muted { color: var(--muted, hsl(220 12% 45%)); font-size: 0.85rem; }
8
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .card p,
9
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .doc-p,
10
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) td,
11
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) th { overflow-wrap: anywhere; }
12
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .btn {
13
+ display: inline-block; border: 1px solid var(--border, hsl(220 14% 88%)); border-radius: var(--radius-sm, 8px);
14
+ padding: var(--space-2, 0.5rem) var(--space-4, 1rem); color: var(--fg, hsl(224 20% 14%)); font-size: 0.92rem;
15
+ background: var(--surface, hsl(220 20% 98%)); cursor: pointer;
16
+ }
17
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .btn:hover { text-decoration: none; border-color: var(--accent, hsl(221 83% 53%)); }
18
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .btn.primary { background: var(--accent, hsl(221 83% 53%)); border-color: var(--accent, hsl(221 83% 53%)); color: var(--accent-fg, hsl(0 0% 100%)); }
19
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .btn.primary:hover { background: var(--accent-hover, hsl(221 83% 47%)); border-color: var(--accent-hover, hsl(221 83% 47%)); }
20
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) :where(.seg) { display: flex; flex-wrap: wrap; gap: var(--space-1, 0.25rem); }
21
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .seg-btn {
22
+ border: 1px solid var(--border, hsl(220 14% 88%)); background: transparent; color: var(--muted, hsl(220 12% 45%));
23
+ padding: var(--space-1, 0.25rem) var(--space-3, 0.75rem); border-radius: var(--radius-sm, 8px); font-size: 0.8rem;
24
+ cursor: pointer;
25
+ }
26
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .seg-btn.active { background: var(--accent-soft, hsl(214 95% 93%)); color: var(--accent, hsl(221 83% 53%)); border-color: var(--accent, hsl(221 83% 53%)); }
27
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .card {
28
+ background: var(--surface, hsl(220 20% 98%)); border: 1px solid var(--border, hsl(220 14% 88%));
29
+ border-radius: var(--radius, 10px); padding: var(--space-4, 1rem) var(--space-5, 1.25rem);
30
+ }
31
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .card-head {
32
+ display: flex; align-items: center; justify-content: space-between; gap: var(--space-4, 1rem);
33
+ flex-wrap: wrap; margin-bottom: var(--space-3, 0.75rem);
34
+ }
35
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .card-head h3 { margin: 0; min-width: 0; flex: 1 1 auto; overflow-wrap: anywhere; }
36
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .card-head > :last-child { margin-left: auto; }
37
+
38
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .callout {
39
+ background: var(--surface, hsl(220 20% 98%)); border: 1px solid var(--border, hsl(220 14% 88%));
40
+ border-left: 4px solid var(--accent, hsl(221 83% 53%)); border-radius: var(--radius, 10px);
41
+ padding: var(--space-4, 1rem) var(--space-5, 1.25rem); margin: var(--space-4, 1rem) 0;
42
+ }
43
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .callout.accent { background: var(--accent-soft, hsl(214 95% 93%)); }
44
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .callout.wide { max-width: 60rem; }
45
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .cards {
46
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
47
+ gap: var(--space-4, 1rem); margin: var(--space-4, 1rem) 0;
48
+ }
49
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .stat-card {
50
+ background: var(--surface, hsl(220 20% 98%)); border: 1px solid var(--border, hsl(220 14% 88%));
51
+ border-radius: var(--radius, 10px); padding: var(--space-4, 1rem) var(--space-5, 1.25rem);
52
+ }
53
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .stat-title { margin: 0; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted, hsl(220 12% 45%)); }
54
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .stat-value { margin: 0.2rem 0 0; font-weight: 700; font-size: 1.05rem; }
55
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .stat-note { margin: 0.1rem 0 0; font-size: 0.8rem; color: var(--muted, hsl(220 12% 45%)); }
56
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .table-scroll { overflow-x: auto; min-width: 0; border: 1px solid var(--border, hsl(220 14% 88%)); border-radius: var(--radius, 10px); }
57
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) table { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
58
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .table-scroll th,
59
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .table-scroll td {
60
+ overflow-wrap: normal;
61
+ word-break: normal;
62
+ }
63
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) th,
64
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) td { text-align: left; padding: var(--space-2, 0.5rem) var(--space-4, 1rem); border-bottom: 1px solid var(--border, hsl(220 14% 88%)); }
65
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) th { background: var(--surface, hsl(220 20% 98%)); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted, hsl(220 12% 45%)); }
66
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) tbody tr:last-child td { border-bottom: none; }
67
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) tr.strong td { font-weight: 700; color: var(--accent, hsl(221 83% 53%)); }
68
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .table-note { color: var(--muted, hsl(220 12% 45%)); font-size: 0.82rem; }
69
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .editor {
70
+ width: 100%; resize: vertical; border: 1px solid var(--border, hsl(220 14% 88%)); border-radius: var(--radius-sm, 8px);
71
+ background: var(--code-bg, hsl(224 25% 12%)); color: var(--code-fg, hsl(210 30% 90%)); padding: var(--space-3, 0.75rem); line-height: 1.5;
72
+ }
73
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .badge {
74
+ display: inline-block; border-radius: 999px; padding: var(--space-1, 0.25rem) var(--space-3, 0.75rem);
75
+ font-size: 0.82rem; font-weight: 600;
76
+ }
77
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .badge.ok { background: var(--ok-soft, hsl(150 60% 94%)); color: var(--ok, hsl(150 65% 34%)); }
78
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .badge.fail { background: var(--fail-soft, hsl(0 80% 96%)); color: var(--fail, hsl(0 68% 46%)); }
79
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .error-line { color: var(--fail, hsl(0 68% 46%)); font-size: 0.85rem; font-family: var(--mono, 'JetBrains Mono', ui-monospace, 'Cascadia Code', monospace); }
80
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .code-block {
81
+ background: var(--code-bg, hsl(224 25% 12%)); color: var(--code-fg, hsl(210 30% 90%)); border-radius: var(--radius-sm, 8px);
82
+ padding: var(--space-3, 0.75rem) var(--space-4, 1rem); overflow-x: auto; margin: 0;
83
+ line-height: 1.5; white-space: pre-wrap; word-break: break-word;
84
+ }
85
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .code-block.clamp { max-height: 14rem; overflow-y: auto; }
86
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .details-card {
87
+ border: 1px solid var(--border, hsl(220 14% 88%)); border-radius: var(--radius, 10px);
88
+ padding: var(--space-2, 0.5rem) var(--space-4, 1rem); margin: var(--space-3, 0.75rem) 0; background: var(--surface, hsl(220 20% 98%));
89
+ }
90
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .details-card summary { cursor: pointer; font-weight: 600; font-size: 0.9rem; color: var(--muted, hsl(220 12% 45%)); }
91
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .details-card[open] summary { margin-bottom: 0.4rem; color: var(--fg, hsl(224 20% 14%)); }
92
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .btn.small { padding: var(--space-1, 0.25rem) var(--space-3, 0.75rem); font-size: 0.82rem; }
93
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .field { display: block; margin-bottom: var(--space-3, 0.75rem); }
94
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .field-title { display: block; font-size: 0.8rem; font-weight: 700; color: var(--muted, hsl(220 12% 45%)); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: var(--space-1, 0.25rem); }
95
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .editor.line { min-height: auto; }
96
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .doc-p { color: var(--fg, hsl(224 20% 14%)); max-width: 44rem; }
97
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .example-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr)); gap: var(--space-4, 1rem); }
98
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .example-card { display: flex; flex-direction: column; }
99
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) .example-card .code-block { flex: 1; font-size: 0.75rem; }
100
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) :focus-visible { outline: 2px solid var(--accent, hsl(221 83% 53%)); outline-offset: 2px; }
101
+ :where(.jaren-flow-editor, .jaren-data-editor, .project-data, .studio-root, .jstudio) button:disabled { cursor: default; opacity: 0.6; }
102
+
103
+ /* Standard generated forms and document-result chrome. */
104
+
105
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form { border: none; padding: 0; margin: 0; }
106
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-title { display: none; }
107
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-field { margin-bottom: var(--space-3, 0.75rem); }
108
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-field label { display: block; font-size: 0.85rem; font-weight: 600; }
109
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-field input[type="text"],
110
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-field input[type="email"],
111
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-field input[type="url"],
112
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-field input[type="password"],
113
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-field input[type="number"],
114
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-field input[type="date"],
115
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-field select,
116
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-field textarea {
117
+ display: block; width: 100%; margin-top: var(--space-1, 0.25rem);
118
+ border: 1px solid var(--border, hsl(220 14% 88%)); border-radius: var(--radius-sm, 8px); background: var(--bg, hsl(0 0% 100%)); color: var(--fg, hsl(224 20% 14%));
119
+ padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem); font-size: 0.9rem; font-family: var(--font, 'Inter', ui-sans-serif, system-ui, sans-serif);
120
+ }
121
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-field input[type="checkbox"] { margin-left: var(--space-2, 0.5rem); }
122
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-required { color: var(--fail, hsl(0 68% 46%)); }
123
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-description { color: var(--muted, hsl(220 12% 45%)); font-size: 0.78rem; margin: 0.15rem 0 0; }
124
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-error { color: var(--fail, hsl(0 68% 46%)); font-size: 0.78rem; margin: 0.2rem 0 0; }
125
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-group,
126
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-array {
127
+ border: 1px solid var(--border, hsl(220 14% 88%)); border-radius: var(--radius-sm, 8px);
128
+ padding: var(--space-3, 0.75rem) var(--space-4, 1rem); margin: var(--space-3, 0.75rem) 0;
129
+ }
130
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-group legend,
131
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-array legend {
132
+ font-size: 0.8rem; font-weight: 700; color: var(--muted, hsl(220 12% 45%)); padding: 0 0.3rem;
133
+ }
134
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-add,
135
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-remove {
136
+ border: 1px solid var(--border, hsl(220 14% 88%)); background: var(--surface, hsl(220 20% 98%)); color: var(--muted, hsl(220 12% 45%));
137
+ border-radius: 6px; cursor: pointer; font-size: 0.8rem; padding: 0.1rem 0.55rem;
138
+ margin-top: 0.3rem;
139
+ }
140
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-add:hover,
141
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .jaren-form-remove:hover { color: var(--accent, hsl(221 83% 53%)); border-color: var(--accent, hsl(221 83% 53%)); }
142
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .code-card { margin: var(--space-3, 0.75rem) 0; }
143
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .code-head { display: flex; align-items: center; gap: var(--space-2, 0.5rem); margin-bottom: var(--space-1, 0.25rem); }
144
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .code-title { font-size: 0.8rem; font-weight: 700; color: var(--muted, hsl(220 12% 45%)); text-transform: uppercase; letter-spacing: 0.04em; }
145
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .details-card {
146
+ border: 1px solid var(--border, hsl(220 14% 88%)); border-radius: var(--radius, 10px);
147
+ padding: var(--space-2, 0.5rem) var(--space-4, 1rem); margin: var(--space-3, 0.75rem) 0; background: var(--surface, hsl(220 20% 98%));
148
+ }
149
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .details-card summary { cursor: pointer; font-weight: 600; font-size: 0.9rem; color: var(--muted, hsl(220 12% 45%)); }
150
+ :where(.jstudio, .jaren-flow-editor, .jaren-data-editor) .details-card[open] summary { margin-bottom: 0.4rem; color: var(--fg, hsl(224 20% 14%)); }
@@ -0,0 +1,99 @@
1
+ @import './editor.css';
2
+ /* Complete Flow editor: scoped layout, interaction and reduced-motion support. */
3
+
4
+ :where(.jaren-flow-editor) .flow-panebar { display: none; }
5
+ @media (max-width: 1024px) {
6
+ :where(.jaren-flow-editor) .flow-grid { padding-bottom: var(--kb-inset, 0px); }
7
+ }
8
+ @media (max-width: 1024px) {
9
+ :where(.jaren-flow-editor) .flow-panebar { display: flex; }
10
+ }
11
+ @media (max-width: 1024px) {
12
+ :where(.jaren-flow-editor) .flow-panebar .seg-btn { flex: 1; }
13
+ }
14
+ @media (max-width: 1024px) {
15
+ :where(.jaren-flow-editor) .flow-panebar,
16
+ :where(.jaren-flow-editor) .flow-grid > .card,
17
+ :where(.jaren-flow-editor) .flow-side { grid-column: 1 / -1; }
18
+ }
19
+ @media (max-width: 1024px) {
20
+
21
+ :where(.jaren-flow-editor) .flow-grid[data-pane='diagram'] .flow-side,
22
+ :where(.jaren-flow-editor) .flow-grid[data-pane='inspector'] .flow-canvas-card,
23
+ :where(.jaren-flow-editor) .flow-grid[data-pane='inspector'] .flow-run,
24
+ :where(.jaren-flow-editor) .flow-grid[data-pane='run'] .flow-canvas-card,
25
+ :where(.jaren-flow-editor) .flow-grid[data-pane='run'] .flow-inspector { display: none; }
26
+ }
27
+ @media (max-width: 1024px) {
28
+ :where(.jaren-flow-editor) .flow-grid .btn,
29
+ :where(.jaren-flow-editor) .flow-grid .seg-btn { min-height: 44px; }
30
+ }
31
+
32
+ :where(.jaren-flow-editor) .flow-grid {
33
+ display: grid;
34
+ grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
35
+ gap: var(--space-4, 1rem);
36
+ align-items: start;
37
+ }
38
+ :where(.jaren-flow-editor) .flow-side { display: grid; gap: var(--space-4, 1rem); }
39
+ :where(.jaren-flow-editor) .flow-palette { display: flex; flex-wrap: wrap; gap: var(--space-2, 0.5rem); }
40
+ :where(.jaren-flow-editor) .flow-tabs { display: flex; gap: var(--space-2, 0.5rem); margin-block: var(--space-2, 0.5rem); }
41
+ :where(.jaren-flow-editor) .flow-canvas { overflow: auto; }
42
+ :where(.jaren-flow-editor) .flow-canvas svg { max-width: 100%; height: auto; }
43
+ :where(.jaren-flow-editor) .flow-text { width: 100%; }
44
+ :where(.jaren-flow-editor) .flow-dag-input { width: 100%; }
45
+ :where(.jaren-flow-editor) .flow-log {
46
+ list-style: none;
47
+ margin: var(--space-2, 0.5rem) 0 0;
48
+ padding: 0;
49
+ max-height: 12rem;
50
+ overflow: auto;
51
+ }
52
+ :where(.jaren-flow-editor) .flow-log-line {
53
+ font-family: var(--font-mono, 'JetBrains Mono', ui-monospace, 'Cascadia Code', monospace);
54
+ font-size: 0.78rem;
55
+ padding-block: 1px;
56
+ border-block-end: 1px solid var(--border, hsl(220 14% 88%));
57
+ overflow-wrap: anywhere;
58
+ }
59
+ :where(.jaren-flow-editor) .flow-run-head { display: flex; flex-wrap: wrap; gap: var(--space-2, 0.5rem); align-items: center; margin-block: var(--space-2, 0.5rem); }
60
+ :where(.jaren-flow-editor) .flow-run-buttons { display: flex; flex-wrap: wrap; gap: var(--space-2, 0.5rem); }
61
+ :where(.jaren-flow-editor) .flow-run-mount { border: 1px dashed var(--border, hsl(220 14% 88%)); border-radius: 8px; padding: var(--space-2, 0.5rem); }
62
+ :where(.jaren-flow-editor) .flow-output { max-height: 12rem; overflow: auto; }
63
+ :where(.jaren-flow-editor) .btn.flow-armed { outline: 2px solid var(--accent, hsl(221 83% 53%)); }
64
+ :where(.jaren-flow-editor) .mm-pick { cursor: pointer; }
65
+ :where(.jaren-flow-editor) .mm-selected .mm-node-shape,
66
+ :where(.jaren-flow-editor) .mm-selected .mm-edge-line { stroke-width: 2.5; }
67
+ :where(.jaren-flow-editor) .mm-armed .mm-node-shape { stroke-dasharray: 4 3; stroke-width: 2.5; }
68
+ :where(.jaren-flow-editor) .mm-active .mm-node-shape {
69
+ stroke: var(--accent, hsl(221 83% 53%));
70
+ stroke-width: 2.5;
71
+ transform-box: fill-box;
72
+ transform-origin: center;
73
+ filter: drop-shadow(0 0 6px var(--accent, hsl(221 83% 53%)));
74
+ }
75
+ :where(.jaren-flow-editor) .mm-fired .mm-edge-line {
76
+ stroke: var(--accent, hsl(221 83% 53%));
77
+ stroke-width: 2.5;
78
+ stroke-dasharray: 6 4;
79
+ }
80
+ :where(.jaren-flow-editor) .mm-fired .mm-edge-label { fill: var(--accent, hsl(221 83% 53%)); font-weight: 600; }
81
+ :where(.jaren-flow-editor) .mm-run-ok .mm-node-shape { stroke: var(--ok, #16a34a); stroke-width: 2.5; }
82
+ :where(.jaren-flow-editor) .mm-run-error .mm-node-shape { stroke: var(--danger, #dc2626); stroke-width: 2.5; }
83
+ :where(.jaren-flow-editor) .mm-run-aborted .mm-node-shape { opacity: 0.5; }
84
+ @media (prefers-reduced-motion: no-preference) {
85
+ :where(.jaren-flow-editor) .mm-active .mm-node-shape { animation: mm-pulse 1.5s var(--motion-ease-loop, ease-in-out) infinite; }
86
+ }
87
+ @media (prefers-reduced-motion: no-preference) {
88
+ :where(.jaren-flow-editor) .mm-fired .mm-edge-line { animation: mm-flow 0.7s linear infinite; }
89
+ }
90
+ @media (prefers-reduced-motion: no-preference) {
91
+ :where(.jaren-flow-editor) .mm-run-error .mm-node-shape { animation: mm-pulse 1s var(--motion-ease-loop, ease-in-out) infinite; }
92
+ }
93
+ @keyframes mm-pulse {
94
+ 0%, 100% { filter: drop-shadow(0 0 2px var(--accent, hsl(221 83% 53%))); }
95
+ 50% { filter: drop-shadow(0 0 9px var(--accent, hsl(221 83% 53%))); }
96
+ }
97
+ @keyframes mm-flow {
98
+ to { stroke-dashoffset: -20; }
99
+ }
package/styles/studio.css CHANGED
@@ -1,3 +1,4 @@
1
+ @import './flow.css';
1
2
  /* @jarenjs/studio — the IDE shell stylesheet.
2
3
  *
3
4
  * DESIGN.md-conformant: minmax(0,1fr) tracks with min-width:0 on every