@zerotal/flow 1.0.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 (64) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/LICENSE +21 -0
  3. package/README.md +148 -0
  4. package/package.json +84 -0
  5. package/src/Component.ts +1498 -0
  6. package/src/Form.ts +189 -0
  7. package/src/Layout.ts +47 -0
  8. package/src/augment.ts +94 -0
  9. package/src/client/FlowClientError.ts +12 -0
  10. package/src/client/FlowComponent.ts +96 -0
  11. package/src/client/alpinejs-morph.d.ts +5 -0
  12. package/src/client/bridge.ts +3463 -0
  13. package/src/client/confirm.ts +338 -0
  14. package/src/client/cspAdapter.ts +79 -0
  15. package/src/client/cspEvaluator.ts +494 -0
  16. package/src/client/errorOverlay.ts +124 -0
  17. package/src/client/headless.ts +948 -0
  18. package/src/client/index.csp.ts +47 -0
  19. package/src/client/index.ts +56 -0
  20. package/src/client/optimistic.ts +52 -0
  21. package/src/client/timeline.ts +160 -0
  22. package/src/client/url.ts +51 -0
  23. package/src/commands/MakeFlowCommand.ts +290 -0
  24. package/src/compiler/cache.ts +101 -0
  25. package/src/compiler/directives.ts +107 -0
  26. package/src/compiler/helpers.ts +26 -0
  27. package/src/compiler/index.ts +172 -0
  28. package/src/compiler/transform.ts +2070 -0
  29. package/src/components.ts +1465 -0
  30. package/src/config.ts +52 -0
  31. package/src/decorators.ts +978 -0
  32. package/src/dehydrate.ts +333 -0
  33. package/src/durable.ts +243 -0
  34. package/src/events.ts +118 -0
  35. package/src/frameworkEvents.ts +40 -0
  36. package/src/headless.ts +1147 -0
  37. package/src/index.ts +267 -0
  38. package/src/jsx-dev-runtime.ts +14 -0
  39. package/src/jsx-runtime.ts +1272 -0
  40. package/src/mixins.ts +199 -0
  41. package/src/observability.ts +167 -0
  42. package/src/pagination.ts +300 -0
  43. package/src/presence.ts +81 -0
  44. package/src/provider/FlowProvider.ts +1401 -0
  45. package/src/registry.ts +118 -0
  46. package/src/renderContext.ts +55 -0
  47. package/src/router.ts +401 -0
  48. package/src/shared.ts +226 -0
  49. package/src/store.ts +116 -0
  50. package/src/synths/CarbonIntervalSynth.ts +22 -0
  51. package/src/synths/CarbonSynth.ts +20 -0
  52. package/src/synths/CollectionSynth.ts +62 -0
  53. package/src/synths/DateSynth.ts +24 -0
  54. package/src/synths/ModelSynth.ts +78 -0
  55. package/src/synths/TemporaryUploadedFileSynth.ts +19 -0
  56. package/src/synths/index.ts +148 -0
  57. package/src/testing.ts +361 -0
  58. package/src/types.ts +315 -0
  59. package/src/uploads/FileUploads.ts +94 -0
  60. package/src/uploads/TemporaryUploadedFile.ts +274 -0
  61. package/src/uploads/sign.ts +18 -0
  62. package/src/urlSafety.ts +0 -0
  63. package/src/utils.ts +216 -0
  64. package/src/validation.ts +55 -0
@@ -0,0 +1,494 @@
1
+ // ── CSP-safe expression evaluator ──────────────────────────────────────────────
2
+ //
3
+ // A tiny tokenizer + recursive-descent parser + tree-walking evaluator for the
4
+ // subset of JavaScript that Flow/Alpine expressions use — WITHOUT `eval` or
5
+ // `new Function`, so it runs under a strict Content-Security-Policy that omits
6
+ // `'unsafe-eval'`.
7
+ //
8
+ // Supported: identifiers, member access (a.b.c), calls f(a, b), literals
9
+ // (number/string/true/false/null/undefined), arrays, object literals, unary
10
+ // (! - +), prefix/postfix ++/--, binary (+ - * / % === !== == != < > <= >=),
11
+ // logical (&& ||), ternary (?:), grouping, and assignment (= += -= *= /=).
12
+ //
13
+ // NOT supported (throws): arrow functions (=>), template literals (`…`), computed
14
+ // member access (a[b]), spread (...), comma sequences, statements. Move that logic
15
+ // into an @expose action or a named method instead.
16
+
17
+ import { FlowClientError } from "./FlowClientError.ts";
18
+
19
+ export class CspSyntaxError extends FlowClientError {}
20
+
21
+ // ── Tokenizer ───────────────────────────────────────────────────────────────
22
+
23
+ type Tok =
24
+ | { t: "num"; v: number }
25
+ | { t: "str"; v: string }
26
+ | { t: "name"; v: string }
27
+ | { t: "punc"; v: string };
28
+
29
+ const PUNCT3 = ["===", "!==", "**="];
30
+ const PUNCT2 = ["==", "!=", "<=", ">=", "&&", "||", "++", "--", "+=", "-=", "*=", "/=", "%=", "?."];
31
+ const PUNCT1 = [
32
+ "+",
33
+ "-",
34
+ "*",
35
+ "/",
36
+ "%",
37
+ "!",
38
+ "<",
39
+ ">",
40
+ "=",
41
+ "(",
42
+ ")",
43
+ "[",
44
+ "]",
45
+ "{",
46
+ "}",
47
+ ".",
48
+ ",",
49
+ ":",
50
+ "?",
51
+ ];
52
+
53
+ function tokenize(src: string): Tok[] {
54
+ const toks: Tok[] = [];
55
+ let i = 0;
56
+ const n = src.length;
57
+ while (i < n) {
58
+ const c = src[i]!;
59
+ if (c === " " || c === "\t" || c === "\n" || c === "\r") {
60
+ i++;
61
+ continue;
62
+ }
63
+
64
+ // Reject the constructs we deliberately don't support, with a clear message.
65
+ if (c === "`") throw new CspSyntaxError("Template literals are not allowed in CSP-safe mode.");
66
+
67
+ // String literal
68
+ if (c === '"' || c === "'") {
69
+ const quote = c;
70
+ let s = "";
71
+ i++;
72
+ while (i < n && src[i] !== quote) {
73
+ if (src[i] === "\\") {
74
+ s += src[i + 1] ?? "";
75
+ i += 2;
76
+ } else {
77
+ s += src[i];
78
+ i++;
79
+ }
80
+ }
81
+ if (i >= n) throw new CspSyntaxError("Unterminated string literal.");
82
+ i++; // closing quote
83
+ toks.push({ t: "str", v: s });
84
+ continue;
85
+ }
86
+
87
+ // Number
88
+ if (c >= "0" && c <= "9") {
89
+ let s = "";
90
+ while (i < n && /[0-9.]/.test(src[i]!)) {
91
+ s += src[i];
92
+ i++;
93
+ }
94
+ toks.push({ t: "num", v: Number(s) });
95
+ continue;
96
+ }
97
+
98
+ // Identifier / keyword ($ and _ allowed)
99
+ if (/[A-Za-z_$]/.test(c)) {
100
+ let s = "";
101
+ while (i < n && /[A-Za-z0-9_$]/.test(src[i]!)) {
102
+ s += src[i];
103
+ i++;
104
+ }
105
+ toks.push({ t: "name", v: s });
106
+ continue;
107
+ }
108
+
109
+ // Punctuators (longest match first)
110
+ const three = src.slice(i, i + 3);
111
+ const two = src.slice(i, i + 2);
112
+ if (two === "=>") throw new CspSyntaxError("Arrow functions are not allowed in CSP-safe mode.");
113
+ if (two === "...") {
114
+ /* not reachable (3-char) */
115
+ }
116
+ if (src.slice(i, i + 3) === "...")
117
+ throw new CspSyntaxError("Spread (...) is not allowed in CSP-safe mode.");
118
+ if (PUNCT3.includes(three)) {
119
+ toks.push({ t: "punc", v: three });
120
+ i += 3;
121
+ continue;
122
+ }
123
+ if (PUNCT2.includes(two)) {
124
+ toks.push({ t: "punc", v: two });
125
+ i += 2;
126
+ continue;
127
+ }
128
+ if (PUNCT1.includes(c)) {
129
+ toks.push({ t: "punc", v: c });
130
+ i += 1;
131
+ continue;
132
+ }
133
+
134
+ throw new CspSyntaxError(`Unexpected character '${c}' in expression.`);
135
+ }
136
+ return toks;
137
+ }
138
+
139
+ // ── AST ─────────────────────────────────────────────────────────────────────
140
+
141
+ type Node =
142
+ | { k: "lit"; v: unknown }
143
+ | { k: "id"; name: string }
144
+ | { k: "member"; obj: Node; prop: string }
145
+ | { k: "call"; callee: Node; args: Node[] }
146
+ | { k: "array"; els: Node[] }
147
+ | { k: "object"; props: Array<{ key: string; value: Node }> }
148
+ | { k: "unary"; op: string; arg: Node }
149
+ | { k: "update"; op: string; prefix: boolean; arg: Node }
150
+ | { k: "binary"; op: string; left: Node; right: Node }
151
+ | { k: "logical"; op: string; left: Node; right: Node }
152
+ | { k: "cond"; test: Node; cons: Node; alt: Node }
153
+ | { k: "assign"; op: string; target: Node; value: Node };
154
+
155
+ // ── Parser (recursive descent) ────────────────────────────────────────────────
156
+
157
+ class Parser {
158
+ private p = 0;
159
+ constructor(private toks: Tok[]) {}
160
+
161
+ private peek(): Tok | undefined {
162
+ return this.toks[this.p];
163
+ }
164
+ private next(): Tok | undefined {
165
+ return this.toks[this.p++];
166
+ }
167
+ private isPunc(v: string): boolean {
168
+ const t = this.peek();
169
+ return !!t && t.t === "punc" && t.v === v;
170
+ }
171
+ private eat(v: string): void {
172
+ if (!this.isPunc(v)) throw new CspSyntaxError(`Expected '${v}'.`);
173
+ this.p++;
174
+ }
175
+
176
+ parse(): Node {
177
+ const node = this.assignment();
178
+ if (this.p < this.toks.length)
179
+ throw new CspSyntaxError("Unexpected trailing tokens in expression.");
180
+ return node;
181
+ }
182
+
183
+ private assignment(): Node {
184
+ const left = this.conditional();
185
+ const t = this.peek();
186
+ if (t && t.t === "punc" && ["=", "+=", "-=", "*=", "/=", "%="].includes(t.v)) {
187
+ this.p++;
188
+ const value = this.assignment();
189
+ return { k: "assign", op: t.v, target: left, value };
190
+ }
191
+ return left;
192
+ }
193
+
194
+ private conditional(): Node {
195
+ const test = this.logicalOr();
196
+ if (this.isPunc("?")) {
197
+ this.p++;
198
+ const cons = this.assignment();
199
+ this.eat(":");
200
+ const alt = this.assignment();
201
+ return { k: "cond", test, cons, alt };
202
+ }
203
+ return test;
204
+ }
205
+
206
+ private logicalOr(): Node {
207
+ let left = this.logicalAnd();
208
+ while (this.isPunc("||")) {
209
+ this.p++;
210
+ left = { k: "logical", op: "||", left, right: this.logicalAnd() };
211
+ }
212
+ return left;
213
+ }
214
+ private logicalAnd(): Node {
215
+ let left = this.equality();
216
+ while (this.isPunc("&&")) {
217
+ this.p++;
218
+ left = { k: "logical", op: "&&", left, right: this.equality() };
219
+ }
220
+ return left;
221
+ }
222
+ private equality(): Node {
223
+ let left = this.relational();
224
+ while (this.peekPuncIn(["===", "!==", "==", "!="])) {
225
+ const op = (this.next() as Tok).v as string;
226
+ left = { k: "binary", op, left, right: this.relational() };
227
+ }
228
+ return left;
229
+ }
230
+ private relational(): Node {
231
+ let left = this.additive();
232
+ while (this.peekPuncIn(["<", ">", "<=", ">="])) {
233
+ const op = (this.next() as Tok).v as string;
234
+ left = { k: "binary", op, left, right: this.additive() };
235
+ }
236
+ return left;
237
+ }
238
+ private additive(): Node {
239
+ let left = this.multiplicative();
240
+ while (this.peekPuncIn(["+", "-"])) {
241
+ const op = (this.next() as Tok).v as string;
242
+ left = { k: "binary", op, left, right: this.multiplicative() };
243
+ }
244
+ return left;
245
+ }
246
+ private multiplicative(): Node {
247
+ let left = this.unary();
248
+ while (this.peekPuncIn(["*", "/", "%"])) {
249
+ const op = (this.next() as Tok).v as string;
250
+ left = { k: "binary", op, left, right: this.unary() };
251
+ }
252
+ return left;
253
+ }
254
+
255
+ private peekPuncIn(ops: string[]): boolean {
256
+ const t = this.peek();
257
+ return !!t && t.t === "punc" && ops.includes(t.v);
258
+ }
259
+
260
+ private unary(): Node {
261
+ if (this.peekPuncIn(["!", "-", "+"])) {
262
+ const op = (this.next() as Tok).v as string;
263
+ return { k: "unary", op, arg: this.unary() };
264
+ }
265
+ if (this.peekPuncIn(["++", "--"])) {
266
+ const op = (this.next() as Tok).v as string;
267
+ return { k: "update", op, prefix: true, arg: this.unary() };
268
+ }
269
+ return this.postfix();
270
+ }
271
+
272
+ private postfix(): Node {
273
+ let node = this.callMember();
274
+ if (this.peekPuncIn(["++", "--"])) {
275
+ const op = (this.next() as Tok).v as string;
276
+ node = { k: "update", op, prefix: false, arg: node };
277
+ }
278
+ return node;
279
+ }
280
+
281
+ private callMember(): Node {
282
+ let node = this.primary();
283
+ for (;;) {
284
+ if (this.isPunc(".")) {
285
+ this.p++;
286
+ const t = this.next();
287
+ if (!t || t.t !== "name") throw new CspSyntaxError('Expected property name after ".".');
288
+ node = { k: "member", obj: node, prop: t.v };
289
+ } else if (this.isPunc("(")) {
290
+ this.p++;
291
+ const args: Node[] = [];
292
+ if (!this.isPunc(")")) {
293
+ args.push(this.assignment());
294
+ while (this.isPunc(",")) {
295
+ this.p++;
296
+ args.push(this.assignment());
297
+ }
298
+ }
299
+ this.eat(")");
300
+ node = { k: "call", callee: node, args };
301
+ } else if (this.isPunc("[")) {
302
+ throw new CspSyntaxError(
303
+ "Computed member access (obj[expr]) is not allowed in CSP-safe mode.",
304
+ );
305
+ } else {
306
+ return node;
307
+ }
308
+ }
309
+ }
310
+
311
+ private primary(): Node {
312
+ const t = this.next();
313
+ if (!t) throw new CspSyntaxError("Unexpected end of expression.");
314
+ if (t.t === "num") return { k: "lit", v: t.v };
315
+ if (t.t === "str") return { k: "lit", v: t.v };
316
+ if (t.t === "name") {
317
+ if (t.v === "true") return { k: "lit", v: true };
318
+ if (t.v === "false") return { k: "lit", v: false };
319
+ if (t.v === "null") return { k: "lit", v: null };
320
+ if (t.v === "undefined") return { k: "lit", v: undefined };
321
+ return { k: "id", name: t.v };
322
+ }
323
+ if (t.t === "punc") {
324
+ if (t.v === "(") {
325
+ const e = this.assignment();
326
+ this.eat(")");
327
+ return e;
328
+ }
329
+ if (t.v === "[") {
330
+ const els: Node[] = [];
331
+ if (!this.isPunc("]")) {
332
+ els.push(this.assignment());
333
+ while (this.isPunc(",")) {
334
+ this.p++;
335
+ els.push(this.assignment());
336
+ }
337
+ }
338
+ this.eat("]");
339
+ return { k: "array", els };
340
+ }
341
+ if (t.v === "{") {
342
+ const props: Array<{ key: string; value: Node }> = [];
343
+ if (!this.isPunc("}")) {
344
+ do {
345
+ const kt = this.next();
346
+ let key: string;
347
+ if (kt && (kt.t === "name" || kt.t === "str")) key = String(kt.v);
348
+ else throw new CspSyntaxError("Expected object key.");
349
+ this.eat(":");
350
+ props.push({ key, value: this.assignment() });
351
+ } while (this.isPunc(",") && (this.p++, true));
352
+ }
353
+ this.eat("}");
354
+ return { k: "object", props };
355
+ }
356
+ }
357
+ throw new CspSyntaxError(`Unexpected token '${(t as { v?: unknown }).v}'.`);
358
+ }
359
+ }
360
+
361
+ // ── Evaluator ───────────────────────────────────────────────────────────────
362
+
363
+ type Scope = Record<string, unknown>;
364
+
365
+ /** Resolve an assignment/update target to its container + key (for write-back). */
366
+ function resolveRef(node: Node, scope: Scope): { obj: Record<string, unknown>; key: string } {
367
+ if (node.k === "id") return { obj: scope, key: node.name };
368
+ if (node.k === "member") {
369
+ const obj = evalNode(node.obj, scope) as Record<string, unknown>;
370
+ if (obj == null) throw new CspSyntaxError(`Cannot assign to property "${node.prop}" of null.`);
371
+ return { obj, key: node.prop };
372
+ }
373
+ throw new CspSyntaxError("Invalid assignment target.");
374
+ }
375
+
376
+ function evalNode(node: Node, scope: Scope): unknown {
377
+ switch (node.k) {
378
+ case "lit":
379
+ return node.v;
380
+ case "id":
381
+ return scope[node.name];
382
+ case "member": {
383
+ const obj = evalNode(node.obj, scope);
384
+ return obj == null ? undefined : (obj as Record<string, unknown>)[node.prop];
385
+ }
386
+ case "array":
387
+ return node.els.map((e) => evalNode(e, scope));
388
+ case "object": {
389
+ const o: Record<string, unknown> = {};
390
+ for (const p of node.props) o[p.key] = evalNode(p.value, scope);
391
+ return o;
392
+ }
393
+ case "call": {
394
+ let thisArg: unknown = undefined;
395
+ let fn: unknown;
396
+ if (node.callee.k === "member") {
397
+ thisArg = evalNode(node.callee.obj, scope);
398
+ fn = thisArg == null ? undefined : (thisArg as Record<string, unknown>)[node.callee.prop];
399
+ } else {
400
+ fn = evalNode(node.callee, scope);
401
+ }
402
+ if (typeof fn !== "function") throw new CspSyntaxError("Attempted to call a non-function.");
403
+ const args = node.args.map((a) => evalNode(a, scope));
404
+ return (fn as (...a: unknown[]) => unknown).apply(thisArg, args);
405
+ }
406
+ case "unary": {
407
+ const v = evalNode(node.arg, scope);
408
+ if (node.op === "!") return !v;
409
+ if (node.op === "-") return -(v as number);
410
+ return +(v as number);
411
+ }
412
+ case "update": {
413
+ const ref = resolveRef(node.arg, scope);
414
+ const old = Number(ref.obj[ref.key]);
415
+ const next = node.op === "++" ? old + 1 : old - 1;
416
+ ref.obj[ref.key] = next;
417
+ return node.prefix ? next : old;
418
+ }
419
+ case "logical": {
420
+ const l = evalNode(node.left, scope);
421
+ if (node.op === "&&") return l ? evalNode(node.right, scope) : l;
422
+ return l ? l : evalNode(node.right, scope);
423
+ }
424
+ case "cond":
425
+ return evalNode(node.test, scope) ? evalNode(node.cons, scope) : evalNode(node.alt, scope);
426
+ case "binary": {
427
+ const l = evalNode(node.left, scope) as never;
428
+ const r = evalNode(node.right, scope) as never;
429
+ switch (node.op) {
430
+ case "===":
431
+ return l === r;
432
+ case "!==":
433
+ return l !== r;
434
+ case "==":
435
+ return l == r; // eslint-disable-line eqeqeq
436
+ case "!=":
437
+ return l != r; // eslint-disable-line eqeqeq
438
+ case "<":
439
+ return l < r;
440
+ case ">":
441
+ return l > r;
442
+ case "<=":
443
+ return l <= r;
444
+ case ">=":
445
+ return l >= r;
446
+ case "+":
447
+ return (l as number) + (r as number);
448
+ case "-":
449
+ return (l as number) - (r as number);
450
+ case "*":
451
+ return (l as number) * (r as number);
452
+ case "/":
453
+ return (l as number) / (r as number);
454
+ case "%":
455
+ return (l as number) % (r as number);
456
+ }
457
+ throw new CspSyntaxError(`Unknown operator '${node.op}'.`);
458
+ }
459
+ case "assign": {
460
+ const ref = resolveRef(node.target, scope);
461
+ const rhs = evalNode(node.value, scope);
462
+ if (node.op === "=") {
463
+ ref.obj[ref.key] = rhs;
464
+ return rhs;
465
+ }
466
+ const cur = ref.obj[ref.key] as number;
467
+ const next =
468
+ node.op === "+="
469
+ ? (cur as unknown as number) + (rhs as number)
470
+ : node.op === "-="
471
+ ? cur - (rhs as number)
472
+ : node.op === "*="
473
+ ? cur * (rhs as number)
474
+ : node.op === "/="
475
+ ? cur / (rhs as number)
476
+ : cur % (rhs as number);
477
+ ref.obj[ref.key] = next;
478
+ return next;
479
+ }
480
+ }
481
+ }
482
+
483
+ // Small AST cache so repeated evaluations of the same expression skip re-parsing.
484
+ const _astCache = new Map<string, Node>();
485
+
486
+ /** Parse + evaluate `expr` against `scope`, with no eval/new Function. */
487
+ export function evaluateCsp(expr: string, scope: Scope): unknown {
488
+ let ast = _astCache.get(expr);
489
+ if (!ast) {
490
+ ast = new Parser(tokenize(expr)).parse();
491
+ _astCache.set(expr, ast);
492
+ }
493
+ return evalNode(ast, scope);
494
+ }
@@ -0,0 +1,124 @@
1
+ // ── Dev-mode error overlay ─────────────────────────────────────────────────────
2
+ //
3
+ // A full-screen, Vite/Next-style overlay for unexpected errors thrown by a Flow action. The
4
+ // server attaches the error's message + stack + action to the error frame ONLY under the dev
5
+ // worker (never in production), so the mere presence of a stack means "show the overlay". Purely
6
+ // presentational + dismissible; the component's patch still applies underneath, so dismissing
7
+ // returns you to a live page.
8
+
9
+ export interface OverlayError {
10
+ message: string;
11
+ name?: string | undefined;
12
+ stack?: string | undefined;
13
+ action?: string | undefined;
14
+ component?: string | undefined;
15
+ }
16
+
17
+ const OVERLAY_ID = "flow-error-overlay";
18
+
19
+ function _esc(s: unknown): string {
20
+ return String(s ?? "").replace(
21
+ /[&<>"']/g,
22
+ (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]!,
23
+ );
24
+ }
25
+
26
+ /** Render each stack line, dimming framework/node_modules frames so app frames stand out. @internal */
27
+ export function _formatStack(stack: string): string {
28
+ return stack
29
+ .split("\n")
30
+ .slice(1) // first line is "Name: message", already shown as the heading
31
+ .map((line) => {
32
+ const dim = /node_modules|node:internal|\/flow\/src\//.test(line);
33
+ return `<div class="flow-eo-frame${dim ? " dim" : ""}">${_esc(line.trim())}</div>`;
34
+ })
35
+ .join("");
36
+ }
37
+
38
+ /** Show (or replace) the dev error overlay for `err`. Idempotent — re-showing replaces content. */
39
+ export function showErrorOverlay(err: OverlayError): void {
40
+ if (typeof document === "undefined") return;
41
+ hideErrorOverlay();
42
+
43
+ const root = document.createElement("div");
44
+ root.id = OVERLAY_ID;
45
+ const shadow = root.attachShadow({ mode: "open" });
46
+
47
+ const where = [
48
+ err.action ? `action <b>${_esc(err.action)}</b>` : "",
49
+ err.component ? `in <b>${_esc(err.component)}</b>` : "",
50
+ ]
51
+ .filter(Boolean)
52
+ .join(" ");
53
+
54
+ shadow.innerHTML = `
55
+ <style>
56
+ :host { all: initial; }
57
+ * { box-sizing: border-box; }
58
+ .flow-eo-back {
59
+ position: fixed; inset: 0; z-index: 2147483647;
60
+ background: rgba(10, 12, 20, 0.72); backdrop-filter: blur(3px);
61
+ display: flex; align-items: flex-start; justify-content: center; padding: 6vh 16px;
62
+ font: 13px/1.55 ui-monospace, 'JetBrains Mono', 'SF Mono', Menlo, monospace; color: #e5e7eb;
63
+ overflow: auto;
64
+ }
65
+ .flow-eo-card {
66
+ width: min(920px, 100%); background: #14161f;
67
+ border: 1px solid #3b1d24; border-radius: 12px; overflow: hidden;
68
+ box-shadow: 0 20px 60px rgba(0,0,0,.5);
69
+ }
70
+ .flow-eo-head { padding: 16px 20px; background: #1b1013; border-bottom: 1px solid #3b1d24; }
71
+ .flow-eo-badge {
72
+ display: inline-block; font-size: 11px; font-weight: 700; letter-spacing: .4px;
73
+ color: #fecaca; background: #7f1d1d; padding: 2px 8px; border-radius: 999px; text-transform: uppercase;
74
+ }
75
+ .flow-eo-where { margin-top: 8px; font-size: 12px; color: #9aa0b4; }
76
+ .flow-eo-where b { color: #cbd5e1; font-weight: 700; }
77
+ .flow-eo-msg { margin-top: 8px; font-size: 16px; font-weight: 600; color: #fca5a5; white-space: pre-wrap; word-break: break-word; }
78
+ .flow-eo-stack { padding: 12px 20px 18px; max-height: 46vh; overflow: auto; }
79
+ .flow-eo-frame { white-space: pre-wrap; word-break: break-all; color: #cbd5e1; padding: 1px 0; }
80
+ .flow-eo-frame.dim { color: #565f89; }
81
+ .flow-eo-foot { display: flex; align-items: center; gap: 10px; padding: 10px 20px; border-top: 1px solid #262b3f; background: #0f1119; }
82
+ .flow-eo-hint { flex: 1; font-size: 11px; color: #565f89; }
83
+ .flow-eo-btn {
84
+ font: inherit; font-size: 12px; cursor: pointer; padding: 5px 12px; border-radius: 6px;
85
+ background: #262b3f; color: #e5e7eb; border: 1px solid #3b4261;
86
+ }
87
+ .flow-eo-btn:hover { background: #303650; }
88
+ </style>
89
+ <div class="flow-eo-back" part="back">
90
+ <div class="flow-eo-card" role="alertdialog" aria-modal="true">
91
+ <div class="flow-eo-head">
92
+ <span class="flow-eo-badge">${_esc(err.name || "Error")}</span>
93
+ ${where ? `<div class="flow-eo-where">${where}</div>` : ""}
94
+ <div class="flow-eo-msg">${_esc(err.message)}</div>
95
+ </div>
96
+ ${err.stack ? `<div class="flow-eo-stack">${_formatStack(err.stack)}</div>` : ""}
97
+ <div class="flow-eo-foot">
98
+ <span class="flow-eo-hint">Flow dev error · fix the action and save, or dismiss (Esc)</span>
99
+ <button class="flow-eo-btn" data-eo-dismiss type="button">Dismiss</button>
100
+ </div>
101
+ </div>
102
+ </div>`;
103
+
104
+ document.body.appendChild(root);
105
+
106
+ const back = shadow.querySelector<HTMLElement>(".flow-eo-back")!;
107
+ back.addEventListener("click", (e) => {
108
+ // Dismiss on backdrop click or the Dismiss button; keep clicks inside the card.
109
+ if (e.target === back || (e.target as HTMLElement).closest("[data-eo-dismiss]"))
110
+ hideErrorOverlay();
111
+ });
112
+ document.addEventListener("keydown", _onKey);
113
+ }
114
+
115
+ function _onKey(e: KeyboardEvent): void {
116
+ if (e.key === "Escape") hideErrorOverlay();
117
+ }
118
+
119
+ /** Remove the overlay if present. */
120
+ export function hideErrorOverlay(): void {
121
+ if (typeof document === "undefined") return;
122
+ document.getElementById(OVERLAY_ID)?.remove();
123
+ document.removeEventListener("keydown", _onKey);
124
+ }