@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,338 @@
1
+ // ── Styled confirmation dialog ──────────────────────────────────────────────
2
+ //
3
+ // Replaces the browser-native window.confirm()/prompt() with a Tailwind-Plus-style
4
+ // modal (centered panel, status icon, title + description, Confirm/Cancel buttons,
5
+ // optional "type-to-confirm" input). Built with INLINE styles on purpose: the
6
+ // dialog is created at runtime from the bridge, so an app's Tailwind `@source`
7
+ // never scans it and utility classes would be purged. Inline = self-contained and
8
+ // correct in every app, and it auto-adapts to dark mode like the toast.
9
+ //
10
+ // `confirmDialog(opts)` returns a Promise<boolean> — true on Confirm, false on
11
+ // Cancel / Escape / backdrop. For the prompt variant, Confirm is enabled only once
12
+ // the typed value matches `opts.prompt`.
13
+
14
+ export interface ConfirmDialogOptions {
15
+ /** Body text (the description, or the heading when no `title` is given). */
16
+ message: string;
17
+ /** Bold heading above the message. */
18
+ title?: string;
19
+ /** Confirm button label (default: "Confirm", or "Delete" for danger). */
20
+ confirmLabel?: string;
21
+ /** Cancel button label (default: "Cancel"). */
22
+ cancelLabel?: string;
23
+ /** `danger` = destructive (red); `primary` = neutral/indigo (default). */
24
+ variant?: "danger" | "primary";
25
+ /** Custom icon (emoji/text/SVG markup), or `false` to hide the icon entirely. */
26
+ icon?: string | false;
27
+ /** Show a type-to-confirm input; Confirm unlocks only when the value matches this. */
28
+ prompt?: string;
29
+ }
30
+
31
+ const ICONS = {
32
+ // Heroicons solid — exclamation-triangle (danger) / question-mark-circle (primary).
33
+ danger:
34
+ '<svg viewBox="0 0 20 20" fill="currentColor" width="24" height="24"><path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd"/></svg>',
35
+ primary:
36
+ '<svg viewBox="0 0 20 20" fill="currentColor" width="24" height="24"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM8.94 6.94a.75.75 0 11-1.06-1.061 3.5 3.5 0 114.95 4.95l-1.757 1.757a.75.75 0 01-1.06-1.06l1.757-1.757a2 2 0 10-2.829-2.83zM10 14a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd"/></svg>',
37
+ };
38
+
39
+ interface Palette {
40
+ bg: string;
41
+ ring: string;
42
+ title: string;
43
+ body: string;
44
+ // Status icon circle.
45
+ iconBg: string;
46
+ iconFg: string;
47
+ // Confirm (primary) button.
48
+ btnBg: string;
49
+ btnBgHover: string;
50
+ btnFg: string;
51
+ // Cancel (secondary) button.
52
+ cancelBg: string;
53
+ cancelBgHover: string;
54
+ cancelFg: string;
55
+ cancelRing: string;
56
+ // Type-to-confirm input.
57
+ inputBg: string;
58
+ inputRing: string;
59
+ inputFg: string;
60
+ }
61
+
62
+ function palette(variant: "danger" | "primary"): Palette {
63
+ const el = document.documentElement;
64
+ const dark =
65
+ el.classList.contains("dark") ||
66
+ (!el.classList.contains("light") &&
67
+ typeof matchMedia === "function" &&
68
+ matchMedia("(prefers-color-scheme: dark)").matches);
69
+
70
+ const accent =
71
+ variant === "danger"
72
+ ? { bg: "#dc2626", hover: "#b91c1c", tintLight: "#fee2e2", fgLight: "#dc2626" }
73
+ : { bg: "#4f46e5", hover: "#4338ca", tintLight: "#e0e7ff", fgLight: "#4f46e5" };
74
+
75
+ return dark
76
+ ? {
77
+ bg: "#1f2937",
78
+ ring: "rgba(255,255,255,.1)",
79
+ title: "#f9fafb",
80
+ body: "#9ca3af",
81
+ iconBg: variant === "danger" ? "rgba(220,38,38,.15)" : "rgba(99,102,241,.15)",
82
+ iconFg: variant === "danger" ? "#f87171" : "#a5b4fc",
83
+ btnBg: accent.bg,
84
+ btnBgHover: accent.hover,
85
+ btnFg: "#ffffff",
86
+ cancelBg: "#374151",
87
+ cancelBgHover: "#4b5563",
88
+ cancelFg: "#e5e7eb",
89
+ cancelRing: "rgba(255,255,255,.1)",
90
+ inputBg: "#111827",
91
+ inputRing: "#4b5563",
92
+ inputFg: "#f9fafb",
93
+ }
94
+ : {
95
+ bg: "#ffffff",
96
+ ring: "rgba(0,0,0,.05)",
97
+ title: "#111827",
98
+ body: "#6b7280",
99
+ iconBg: accent.tintLight,
100
+ iconFg: accent.fgLight,
101
+ btnBg: accent.bg,
102
+ btnBgHover: accent.hover,
103
+ btnFg: "#ffffff",
104
+ cancelBg: "#ffffff",
105
+ cancelBgHover: "#f9fafb",
106
+ cancelFg: "#111827",
107
+ cancelRing: "#d1d5db",
108
+ inputBg: "#ffffff",
109
+ inputRing: "#d1d5db",
110
+ inputFg: "#111827",
111
+ };
112
+ }
113
+
114
+ function esc(s: string): string {
115
+ return String(s).replace(
116
+ /[&<>"]/g,
117
+ (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c] as string,
118
+ );
119
+ }
120
+
121
+ const FONT = "system-ui,-apple-system,Segoe UI,Roboto,sans-serif";
122
+
123
+ /**
124
+ * Show a styled confirmation dialog. Resolves `true` if the user confirms,
125
+ * `false` on cancel / Escape / backdrop click.
126
+ */
127
+ export function confirmDialog(opts: ConfirmDialogOptions): Promise<boolean> {
128
+ const variant = opts.variant === "danger" ? "danger" : "primary";
129
+ const c = palette(variant);
130
+ const heading = opts.title ? opts.title : opts.message;
131
+ const description = opts.title ? opts.message : "";
132
+ const confirmLabel = opts.confirmLabel ?? (variant === "danger" ? "Delete" : "Confirm");
133
+ const cancelLabel = opts.cancelLabel ?? "Cancel";
134
+ const showIcon = opts.icon !== false;
135
+ const iconHtml = showIcon
136
+ ? typeof opts.icon === "string" && opts.icon
137
+ ? opts.icon
138
+ : ICONS[variant]
139
+ : "";
140
+
141
+ return new Promise<boolean>((resolve) => {
142
+ const prevOverflow = document.body.style.overflow;
143
+ const lastFocused = document.activeElement as HTMLElement | null;
144
+
145
+ const overlay = document.createElement("div");
146
+ overlay.setAttribute("role", "dialog");
147
+ overlay.setAttribute("aria-modal", "true");
148
+ overlay.style.cssText =
149
+ "position:fixed;inset:0;z-index:1000;display:flex;align-items:flex-end;justify-content:center;" +
150
+ "padding:16px;font-family:" +
151
+ FONT +
152
+ ";";
153
+ // Center on >= 640px, bottom-sheet on small screens (matches the Tailwind example).
154
+ if (typeof matchMedia === "function" && matchMedia("(min-width: 640px)").matches) {
155
+ overlay.style.alignItems = "center";
156
+ }
157
+
158
+ const backdrop = document.createElement("div");
159
+ backdrop.style.cssText =
160
+ "position:absolute;inset:0;background:rgba(17,24,39,.6);opacity:0;transition:opacity .2s ease;";
161
+
162
+ const panel = document.createElement("div");
163
+ panel.style.cssText =
164
+ "position:relative;width:100%;max-width:32rem;background:" +
165
+ c.bg +
166
+ ";border-radius:14px;" +
167
+ "box-shadow:0 20px 50px rgba(0,0,0,.25),0 0 0 1px " +
168
+ c.ring +
169
+ ";padding:24px;" +
170
+ "opacity:0;transform:translateY(12px) scale(.98);transition:opacity .2s ease, transform .2s ease;";
171
+
172
+ const inputId = "flow-confirm-input";
173
+ panel.innerHTML =
174
+ '<div style="display:flex;gap:16px;align-items:flex-start">' +
175
+ (showIcon
176
+ ? '<div style="flex-shrink:0;display:flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:9999px;background:' +
177
+ c.iconBg +
178
+ ";color:" +
179
+ c.iconFg +
180
+ '">' +
181
+ iconHtml +
182
+ "</div>"
183
+ : "") +
184
+ '<div style="flex:1;min-width:0;text-align:left">' +
185
+ '<h2 style="margin:0;font:600 16px ' +
186
+ FONT +
187
+ ";color:" +
188
+ c.title +
189
+ '">' +
190
+ esc(heading) +
191
+ "</h2>" +
192
+ (description
193
+ ? '<p style="margin:6px 0 0;font:400 14px ' +
194
+ FONT +
195
+ ";color:" +
196
+ c.body +
197
+ ';line-height:1.55">' +
198
+ esc(description) +
199
+ "</p>"
200
+ : "") +
201
+ (opts.prompt
202
+ ? '<input id="' +
203
+ inputId +
204
+ '" type="text" autocomplete="off" placeholder="' +
205
+ esc(opts.prompt) +
206
+ '" style="margin-top:14px;width:100%;box-sizing:border-box;padding:8px 12px;border-radius:8px;' +
207
+ "font:400 14px " +
208
+ FONT +
209
+ ";background:" +
210
+ c.inputBg +
211
+ ";color:" +
212
+ c.inputFg +
213
+ ";border:0;box-shadow:0 0 0 1px " +
214
+ c.inputRing +
215
+ ' inset;outline:none" />'
216
+ : "") +
217
+ "</div></div>" +
218
+ // Buttons: row-reverse on >= 640px (Confirm right), stacked on small screens.
219
+ '<div data-flow-confirm-actions style="margin-top:24px;display:flex;gap:10px;flex-direction:column-reverse">' +
220
+ '<button type="button" data-flow-cancel style="padding:9px 16px;border-radius:8px;border:0;cursor:pointer;' +
221
+ "font:600 14px " +
222
+ FONT +
223
+ ";background:" +
224
+ c.cancelBg +
225
+ ";color:" +
226
+ c.cancelFg +
227
+ ";box-shadow:0 0 0 1px " +
228
+ c.cancelRing +
229
+ ' inset">' +
230
+ esc(cancelLabel) +
231
+ "</button>" +
232
+ '<button type="button" data-flow-ok style="padding:9px 16px;border-radius:8px;border:0;cursor:pointer;' +
233
+ "font:600 14px " +
234
+ FONT +
235
+ ";background:" +
236
+ c.btnBg +
237
+ ";color:" +
238
+ c.btnFg +
239
+ ";box-shadow:0 1px 2px rgba(0,0,0,.1)" +
240
+ (opts.prompt ? ";opacity:.5;pointer-events:none" : "") +
241
+ '">' +
242
+ esc(confirmLabel) +
243
+ "</button>" +
244
+ "</div>";
245
+
246
+ overlay.appendChild(backdrop);
247
+ overlay.appendChild(panel);
248
+ document.body.appendChild(overlay);
249
+ document.body.style.overflow = "hidden";
250
+
251
+ // On >= 640px, lay the buttons out as a right-aligned row (Confirm rightmost).
252
+ const actions = panel.querySelector("[data-flow-confirm-actions]") as HTMLElement | null;
253
+ if (actions && typeof matchMedia === "function" && matchMedia("(min-width: 640px)").matches) {
254
+ actions.style.flexDirection = "row-reverse";
255
+ actions.style.justifyContent = "flex-start";
256
+ }
257
+
258
+ const okBtn = panel.querySelector("[data-flow-ok]") as HTMLButtonElement;
259
+ const cancelBtn = panel.querySelector("[data-flow-cancel]") as HTMLButtonElement;
260
+ const input = panel.querySelector("#" + inputId) as HTMLInputElement | null;
261
+
262
+ // Hover polish (inline styles can't carry :hover).
263
+ const hover = (btn: HTMLElement, on: string, off: string) => {
264
+ btn.addEventListener("mouseenter", () => (btn.style.background = on));
265
+ btn.addEventListener("mouseleave", () => (btn.style.background = off));
266
+ };
267
+ hover(okBtn, c.btnBgHover, c.btnBg);
268
+ hover(cancelBtn, c.cancelBgHover, c.cancelBg);
269
+
270
+ let okEnabled = !opts.prompt;
271
+ const setOk = (on: boolean) => {
272
+ okEnabled = on;
273
+ okBtn.style.opacity = on ? "1" : ".5";
274
+ okBtn.style.pointerEvents = on ? "auto" : "none";
275
+ };
276
+ if (input) {
277
+ input.addEventListener("input", () => setOk(input.value.trim() === opts.prompt));
278
+ }
279
+
280
+ requestAnimationFrame(() => {
281
+ backdrop.style.opacity = "1";
282
+ panel.style.opacity = "1";
283
+ panel.style.transform = "none";
284
+ });
285
+
286
+ let settled = false;
287
+ const close = (result: boolean) => {
288
+ if (settled) return;
289
+ settled = true;
290
+ document.removeEventListener("keydown", onKey, true);
291
+ backdrop.style.opacity = "0";
292
+ panel.style.opacity = "0";
293
+ panel.style.transform = "translateY(12px) scale(.98)";
294
+ document.body.style.overflow = prevOverflow;
295
+ setTimeout(() => {
296
+ overlay.remove();
297
+ if (lastFocused && typeof lastFocused.focus === "function") lastFocused.focus();
298
+ }, 200);
299
+ resolve(result);
300
+ };
301
+
302
+ const confirm = () => {
303
+ if (okEnabled) close(true);
304
+ };
305
+
306
+ okBtn.addEventListener("click", confirm);
307
+ cancelBtn.addEventListener("click", () => close(false));
308
+ backdrop.addEventListener("click", () => close(false));
309
+
310
+ // Keyboard: Escape cancels; Enter confirms; Tab is trapped within the panel.
311
+ const onKey = (e: KeyboardEvent) => {
312
+ if (e.key === "Escape") {
313
+ e.preventDefault();
314
+ close(false);
315
+ } else if (e.key === "Enter") {
316
+ e.preventDefault();
317
+ confirm();
318
+ } else if (e.key === "Tab") {
319
+ const focusables = [input, cancelBtn, okBtn].filter(Boolean) as HTMLElement[];
320
+ const first = focusables[0];
321
+ const last = focusables[focusables.length - 1];
322
+ if (!first || !last) return;
323
+ const activeEl = document.activeElement;
324
+ if (e.shiftKey && activeEl === first) {
325
+ e.preventDefault();
326
+ last.focus();
327
+ } else if (!e.shiftKey && activeEl === last) {
328
+ e.preventDefault();
329
+ first.focus();
330
+ }
331
+ }
332
+ };
333
+ document.addEventListener("keydown", onKey, true);
334
+
335
+ // Focus the input (type-to-confirm) or Cancel (safer default for destructive actions).
336
+ (input ?? cancelBtn).focus();
337
+ });
338
+ }
@@ -0,0 +1,79 @@
1
+ // ── CSP evaluator adapter ──────────────────────────────────────────────────────
2
+ //
3
+ // Replaces Alpine's default (`new Function`) evaluator with our eval-free
4
+ // `evaluateCsp` interpreter, so EVERY Alpine expression (x-data="{}", x-text,
5
+ // :class, flow:click client expressions, …) runs under a strict CSP without
6
+ // `'unsafe-eval'`.
7
+ //
8
+ // ⚠️ BROWSER-VERIFICATION REQUIRED (CSP_PLAN.md · C5). The Alpine evaluator
9
+ // contract and `$data`/magic resolution below are implemented to Alpine 3's
10
+ // documented surface but MUST be confirmed against a real `unsafe-eval`-free CSP
11
+ // in a browser — that's the one piece unit tests can't cover. The pure evaluator
12
+ // (cspEvaluator.ts) is independently verified; this file is only the wiring.
13
+
14
+ import { evaluateCsp } from "./cspEvaluator.ts";
15
+ import { _resolveGel } from "./bridge.ts";
16
+
17
+ type AlpineLike = {
18
+ setEvaluator(fn: (el: Element, expression: string | (() => unknown)) => unknown): void;
19
+ $data?(el: Element): Record<string, unknown>;
20
+ store?(name: string, init?: unknown): unknown;
21
+ };
22
+
23
+ type Receiver = (value: unknown) => void;
24
+ type EvalOpts = { scope?: Record<string, unknown>; params?: unknown[] };
25
+
26
+ export function installCspEvaluator(Alpine: AlpineLike): void {
27
+ Alpine.setEvaluator((el, expression) => {
28
+ // Alpine sometimes passes a function (handlers bound as functions) — already
29
+ // CSP-safe (no string is compiled), so just call it.
30
+ if (typeof expression === "function") {
31
+ return (receiver: Receiver = () => {}, opts: EvalOpts = {}) =>
32
+ receiver((expression as (...a: unknown[]) => unknown)(...(opts.params ?? [])));
33
+ }
34
+
35
+ const expr = expression;
36
+ return (receiver: Receiver = () => {}, opts: EvalOpts = {}): void => {
37
+ const data = (Alpine.$data ? Alpine.$data(el) : {}) as Record<string, unknown>;
38
+ const extra = opts.scope ?? {};
39
+
40
+ // Scope proxy: resolve Flow/Alpine magics we control, then x-for-style
41
+ // call scope, then the element's reactive x-data. Reads/writes pass through
42
+ // to the reactive `data` so dependency tracking and mutation still work.
43
+ const scope = new Proxy({} as Record<string, unknown>, {
44
+ get(_t, k: string | symbol): unknown {
45
+ if (typeof k !== "string") return undefined;
46
+ if (k === "$flow") return _resolveGel(el);
47
+ if (k === "$el") return el;
48
+ if (k === "$store") return Alpine.store;
49
+ if (k in extra) return extra[k];
50
+ return data[k];
51
+ },
52
+ set(_t, k: string | symbol, v: unknown): boolean {
53
+ if (typeof k !== "string") return false;
54
+ if (k in extra) {
55
+ extra[k] = v;
56
+ return true;
57
+ }
58
+ data[k] = v; // reactive write
59
+ return true;
60
+ },
61
+ has(): boolean {
62
+ return true;
63
+ },
64
+ });
65
+
66
+ let result: unknown;
67
+ try {
68
+ result = evaluateCsp(expr, scope);
69
+ } catch (e) {
70
+ console.error("[Flow CSP] expression error:", e, "\nExpression:", expr);
71
+ return;
72
+ }
73
+ if (typeof result === "function") {
74
+ result = (result as (...a: unknown[]) => unknown)(...(opts.params ?? []));
75
+ }
76
+ receiver(result);
77
+ };
78
+ });
79
+ }