@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,1147 @@
1
+ // ── Headless components (@zerotal/flow) ──────────────────────────────────
2
+ //
3
+ // Unstyled, fully-accessible interactive primitives — the Headless UI philosophy
4
+ // adapted to Flow. They ship NO visual classes: state is exposed via `data-*`
5
+ // attributes (`data-open`, `data-checked`, `data-active`) so you Tailwind-style
6
+ // them yourself, e.g. `class="data-[open]:rotate-180"`.
7
+ //
8
+ // Behaviour is driven by Alpine (open/close, keyboard, focus) and ARIA roles are
9
+ // wired in. Components that hold a value (Switch) bind to an @expose prop and sync
10
+ // to the server via $flow.$set — so the value persists and submits like any field.
11
+ //
12
+ // <Switch bind={this.enabled} />
13
+ // <Disclosure label="Details">…</Disclosure>
14
+ // <Accordion items={[{ label, content }]} />
15
+ // <Popover trigger={<button>Menu</button>}>…</Popover>
16
+
17
+ import { jsx, _resolveReactiveName, _injectedBindKey } from "./jsx-runtime.ts";
18
+ import type { HtmlNode } from "./jsx-runtime.ts";
19
+
20
+ // Append extra classes without clobbering a structural base.
21
+ function cx(base: string, extra?: string): string {
22
+ return extra ? `${base} ${extra}`.trim() : base;
23
+ }
24
+
25
+ // ── <Switch> ──────────────────────────────────────────────────────────────────
26
+ // An accessible on/off toggle (role="switch"). Bind it to an @expose boolean:
27
+ // clicking (or Space/Enter — native on a <button>) flips it and syncs to the
28
+ // server. Style the on-state with `data-[checked]:…`.
29
+ //
30
+ // <Switch bind={this.notify} class="… data-[checked]:bg-indigo-600" />
31
+
32
+ export interface SwitchProps {
33
+ /** Bound @expose boolean — `this.enabled`. */
34
+ bind: unknown;
35
+ class?: string;
36
+ children?: unknown;
37
+ [key: string]: unknown;
38
+ }
39
+
40
+ /**
41
+ * An accessible on/off toggle (`role="switch"`) bound to an `@expose` boolean: clicking
42
+ * (or Space/Enter) flips it and syncs to the server via `$flow.$set`. Unstyled — target the
43
+ * on-state with `data-[checked]:…`.
44
+ * @category Headless components
45
+ *
46
+ * @example
47
+ * ```tsx
48
+ * <Switch bind={this.notify} class="… data-[checked]:bg-indigo-600" />
49
+ * ```
50
+ */
51
+ export function Switch(props: SwitchProps): HtmlNode {
52
+ const { bind, class: cls, children, ...rest } = props;
53
+ const name = _injectedBindKey(props, "bind") ?? _resolveReactiveName(bind);
54
+ const on = Boolean(bind);
55
+
56
+ const out: Record<string, unknown> = {
57
+ ...rest,
58
+ type: "button",
59
+ role: "switch",
60
+ tabindex: 0,
61
+ "aria-checked": on ? "true" : "false", // ARIA wants the string, not a bare attr
62
+ // `group` so a child knob can react to the on-state with `group-data-[checked]:…`
63
+ // (the data-checked attribute lives on this button, not on the child).
64
+ class: cx("flow-switch group", cls),
65
+ children,
66
+ };
67
+ if (on) out["data-checked"] = "";
68
+
69
+ if (name) {
70
+ // flow:bind:attr is imperatively synced by the bridge on every state change,
71
+ // avoiding the Firefox SpiderMonkey issue where reading $flow through a nested
72
+ // Proxy doesn't establish Alpine reactive subscriptions for :attr bindings.
73
+ out["flow:bind:attr"] = JSON.stringify({ "aria-checked": name, "data-checked": name });
74
+ out["x-on:click"] = `$flow.$set('${name}', !$flow.${name})`;
75
+ }
76
+ return jsx("button", out);
77
+ }
78
+
79
+ // ── <Disclosure> ────────────────────────────────────────────────────────────────
80
+ // A single collapsible section (button + panel) with correct aria-expanded /
81
+ // aria-controls wiring. Client-side only. `data-open` is exposed on both the
82
+ // trigger and the panel for styling.
83
+ //
84
+ // <Disclosure label="What is your refund policy?">Full refund within 30 days.</Disclosure>
85
+
86
+ export interface DisclosureProps {
87
+ label?: unknown;
88
+ /** Custom trigger (overrides `label`). */
89
+ trigger?: unknown;
90
+ defaultOpen?: boolean;
91
+ class?: string;
92
+ /** Classes for the trigger button. */
93
+ buttonClass?: string;
94
+ /** Classes for the panel. */
95
+ panelClass?: string;
96
+ children?: unknown;
97
+ [key: string]: unknown;
98
+ }
99
+
100
+ /**
101
+ * A single collapsible section (trigger button + panel) with correct `aria-expanded` /
102
+ * `aria-controls` wiring. Client-side only; `data-open` is exposed on both parts for styling.
103
+ * @category Headless components
104
+ */
105
+ export function Disclosure(props: DisclosureProps): HtmlNode {
106
+ const {
107
+ label,
108
+ trigger,
109
+ defaultOpen,
110
+ class: cls,
111
+ buttonClass,
112
+ panelClass,
113
+ children,
114
+ ...rest
115
+ } = props;
116
+
117
+ const button = trigger
118
+ ? jsx("span", {
119
+ "x-on:click": "open = !open",
120
+ ":aria-expanded": "open",
121
+ ":data-open": "open ? '' : null",
122
+ "aria-controls": "",
123
+ ":aria-controls": "$id('flow-disclosure')",
124
+ role: "button",
125
+ tabindex: 0,
126
+ class: cx("flow-disclosure-button", buttonClass),
127
+ children: trigger,
128
+ })
129
+ : jsx("button", {
130
+ type: "button",
131
+ "x-on:click": "open = !open",
132
+ ":aria-expanded": "open",
133
+ ":data-open": "open ? '' : null",
134
+ ":aria-controls": "$id('flow-disclosure')",
135
+ class: cx("flow-disclosure-button", buttonClass),
136
+ children: label ?? "Details",
137
+ });
138
+
139
+ const panel = jsx("div", {
140
+ ":id": "$id('flow-disclosure')",
141
+ "x-show": "open",
142
+ "x-cloak": true,
143
+ ":data-open": "open ? '' : null",
144
+ class: cx("flow-disclosure-panel", panelClass),
145
+ children,
146
+ });
147
+
148
+ return jsx("div", {
149
+ ...rest,
150
+ "x-data": `{ open: ${defaultOpen ? "true" : "false"} }`,
151
+ "x-id": "['flow-disclosure']",
152
+ class: cx("flow-disclosure", cls),
153
+ children: [button, panel],
154
+ });
155
+ }
156
+
157
+ // ── <Accordion> ───────────────────────────────────────────────────────────────
158
+ // A group of disclosures. Single-open by default (an "exclusive" accordion); pass
159
+ // `multiple` to allow several open at once. Each item gets aria-expanded/controls.
160
+ //
161
+ // <Accordion items={[{ label: 'A', content: <p/> }, …]} />
162
+
163
+ export interface AccordionItem {
164
+ label: unknown;
165
+ content: unknown;
166
+ }
167
+
168
+ export interface AccordionProps {
169
+ items: AccordionItem[];
170
+ /** Allow multiple panels open simultaneously (default false → exclusive). */
171
+ multiple?: boolean;
172
+ /** Index open initially (single mode), or -1 for none. */
173
+ defaultIndex?: number;
174
+ class?: string;
175
+ itemClass?: string;
176
+ buttonClass?: string;
177
+ panelClass?: string;
178
+ [key: string]: unknown;
179
+ }
180
+
181
+ /**
182
+ * A group of disclosures. Single-open (exclusive) by default; pass `multiple` to allow
183
+ * several open at once. Each item gets `aria-expanded`/`aria-controls` wiring.
184
+ * @category Headless components
185
+ */
186
+ export function Accordion(props: AccordionProps): HtmlNode {
187
+ const {
188
+ items,
189
+ multiple,
190
+ defaultIndex = -1,
191
+ class: cls,
192
+ itemClass,
193
+ buttonClass,
194
+ panelClass,
195
+ ...rest
196
+ } = props;
197
+
198
+ // Single mode tracks one active index; multiple mode tracks an open-map.
199
+ const data = multiple ? `{ open: {} }` : `{ active: ${defaultIndex} }`;
200
+ const isOpen = (i: number) => (multiple ? `open[${i}]` : `active === ${i}`);
201
+ const toggle = (i: number) =>
202
+ multiple ? `open[${i}] = !open[${i}]` : `active = active === ${i} ? -1 : ${i}`;
203
+
204
+ const rows = items.map((it, i) =>
205
+ jsx("div", {
206
+ class: cx("flow-accordion-item", itemClass),
207
+ "x-id": "['flow-accordion']",
208
+ children: [
209
+ jsx("button", {
210
+ type: "button",
211
+ "x-on:click": toggle(i),
212
+ ":aria-expanded": isOpen(i),
213
+ ":data-open": `${isOpen(i)} ? '' : null`,
214
+ ":aria-controls": "$id('flow-accordion')",
215
+ class: cx("flow-accordion-button", buttonClass),
216
+ children: it.label,
217
+ }),
218
+ jsx("div", {
219
+ ":id": "$id('flow-accordion')",
220
+ "x-show": isOpen(i),
221
+ "x-cloak": true,
222
+ ":data-open": `${isOpen(i)} ? '' : null`,
223
+ class: cx("flow-accordion-panel", panelClass),
224
+ children: it.content,
225
+ }),
226
+ ],
227
+ }),
228
+ );
229
+
230
+ return jsx("div", {
231
+ ...rest,
232
+ "x-data": data,
233
+ class: cx("flow-accordion", cls),
234
+ children: rows,
235
+ });
236
+ }
237
+
238
+ // ── <Popover> ─────────────────────────────────────────────────────────────────
239
+ // A generic anchored panel: click to open, click-outside or Escape to close. The
240
+ // rigorous, unstyled cousin of <Dropdown>. `data-open` is exposed for styling.
241
+ //
242
+ // <Popover trigger={<button>Solutions ▾</button>}>… arbitrary content …</Popover>
243
+
244
+ export interface PopoverProps {
245
+ label?: unknown;
246
+ trigger?: unknown;
247
+ class?: string;
248
+ buttonClass?: string;
249
+ panelClass?: string;
250
+ children?: unknown;
251
+ [key: string]: unknown;
252
+ }
253
+
254
+ /**
255
+ * A generic anchored panel: click to open, click-outside or Escape to close — the rigorous,
256
+ * unstyled cousin of `<Dropdown>`. `data-open` is exposed for styling.
257
+ * @category Headless components
258
+ */
259
+ export function Popover(props: PopoverProps): HtmlNode {
260
+ const { label, trigger, class: cls, buttonClass, panelClass, children, ...rest } = props;
261
+
262
+ const button = trigger
263
+ ? jsx("span", {
264
+ "x-on:click": "open = !open",
265
+ ":aria-expanded": "open",
266
+ ":data-open": "open ? '' : null",
267
+ role: "button",
268
+ tabindex: 0,
269
+ class: cx("flow-popover-button", buttonClass),
270
+ children: trigger,
271
+ })
272
+ : jsx("button", {
273
+ type: "button",
274
+ "x-on:click": "open = !open",
275
+ ":aria-expanded": "open",
276
+ ":data-open": "open ? '' : null",
277
+ class: cx("flow-popover-button", buttonClass),
278
+ children: label ?? "Open",
279
+ });
280
+
281
+ const panel = jsx("div", {
282
+ "x-show": "open",
283
+ "x-cloak": true,
284
+ "x-transition": true,
285
+ "x-on:click.outside": "open = false",
286
+ "x-on:keydown.escape.window": "open = false",
287
+ ":data-open": "open ? '' : null",
288
+ class: cx("flow-popover-panel", panelClass),
289
+ children,
290
+ });
291
+
292
+ return jsx("div", {
293
+ ...rest,
294
+ "x-data": "{ open: false }",
295
+ class: cx("flow-popover", cls),
296
+ children: [button, panel],
297
+ });
298
+ }
299
+
300
+ // ── <Checkbox> ──────────────────────────────────────────────────────────────
301
+ // An accessible checkbox (role="checkbox") bound to an @expose boolean. Like
302
+ // <Switch> but checkbox-semantic. Style the checked state with `data-[checked]:…`.
303
+ //
304
+ // <Checkbox bind={this.agree} class="… data-[checked]:bg-indigo-600" />
305
+
306
+ export interface CheckboxProps {
307
+ bind: unknown;
308
+ class?: string;
309
+ children?: unknown;
310
+ [key: string]: unknown;
311
+ }
312
+
313
+ /**
314
+ * An accessible checkbox (`role="checkbox"`) bound to an `@expose` boolean — like
315
+ * {@link Switch} but checkbox-semantic. Style the checked state with `data-[checked]:…`.
316
+ * @category Headless components
317
+ */
318
+ export function Checkbox(props: CheckboxProps): HtmlNode {
319
+ const { bind, class: cls, children, ...rest } = props;
320
+ const name = _injectedBindKey(props, "bind") ?? _resolveReactiveName(bind);
321
+ const on = Boolean(bind);
322
+
323
+ const out: Record<string, unknown> = {
324
+ ...rest,
325
+ type: "button",
326
+ role: "checkbox",
327
+ tabindex: 0,
328
+ "aria-checked": on ? "true" : "false",
329
+ class: cx("flow-checkbox", cls),
330
+ children,
331
+ };
332
+ if (on) out["data-checked"] = "";
333
+ if (name) {
334
+ out["flow:bind:attr"] = JSON.stringify({ "aria-checked": name, "data-checked": name });
335
+ out["x-on:click"] = `$flow.$set('${name}', !$flow.${name})`;
336
+ }
337
+ return jsx("button", out);
338
+ }
339
+
340
+ // ── <Select> ──────────────────────────────────────────────────────────────────
341
+ // A styled native <select> bound to an @expose value (via flow:model). Reliable
342
+ // and fully accessible for free — reach for <Listbox> only when you need custom
343
+ // option markup.
344
+ //
345
+ // <Select bind={this.country} options={[{ label: 'Canada', value: 'ca' }]} />
346
+
347
+ export interface SelectOption {
348
+ label: unknown;
349
+ value: string | number;
350
+ }
351
+ export interface SelectProps {
352
+ bind: unknown;
353
+ options: SelectOption[];
354
+ /** Optional empty/placeholder first option. */
355
+ placeholder?: string;
356
+ class?: string;
357
+ [key: string]: unknown;
358
+ }
359
+
360
+ /**
361
+ * A styled native `<select>` bound to an `@expose` value via `flow:model` — reliable and
362
+ * fully accessible for free. Reach for {@link Listbox} only when you need custom option markup.
363
+ * @category Headless components
364
+ *
365
+ * @example
366
+ * ```tsx
367
+ * <Select bind={this.country} options={[{ label: 'Canada', value: 'ca' }]} />
368
+ * ```
369
+ */
370
+ export function Select(props: SelectProps): HtmlNode {
371
+ const {
372
+ bind,
373
+ options,
374
+ placeholder,
375
+ class: cls,
376
+ children: _ignore,
377
+ ...rest
378
+ } = props as SelectProps & { children?: unknown };
379
+ const name = _injectedBindKey(props, "bind") ?? _resolveReactiveName(bind);
380
+ const current = bind as string | number | undefined;
381
+
382
+ const opts = options.map((o) =>
383
+ jsx("option", {
384
+ value: o.value,
385
+ ...(o.value === current ? { selected: true } : {}),
386
+ children: o.label,
387
+ }),
388
+ );
389
+ const all = placeholder
390
+ ? [
391
+ jsx("option", {
392
+ value: "",
393
+ disabled: true,
394
+ ...(current == null || current === "" ? { selected: true } : {}),
395
+ children: placeholder,
396
+ }),
397
+ ...opts,
398
+ ]
399
+ : opts;
400
+
401
+ const out: Record<string, unknown> = { ...rest, class: cx("flow-select", cls), children: all };
402
+ if (name) out["flow:model"] = name;
403
+ return jsx("select", out);
404
+ }
405
+
406
+ // ── <RadioGroup> ──────────────────────────────────────────────────────────────
407
+ // An accessible set of radios (role="radiogroup"/"radio") bound to an @expose
408
+ // value, with arrow-key roving (flowRadioGroup runtime). Style the selected option
409
+ // with `data-[checked]:…`.
410
+ //
411
+ // <RadioGroup bind={this.plan} options={[{ label: 'Pro', value: 'pro' }]} />
412
+
413
+ export interface RadioOption {
414
+ label: unknown;
415
+ value: string | number;
416
+ }
417
+ export interface RadioGroupProps {
418
+ bind: unknown;
419
+ options: RadioOption[];
420
+ class?: string;
421
+ optionClass?: string;
422
+ [key: string]: unknown;
423
+ }
424
+
425
+ /**
426
+ * An accessible set of radios (`role="radiogroup"`/`"radio"`) bound to an `@expose` value,
427
+ * with arrow-key roving. Style the selected option with `data-[checked]:…`.
428
+ * @category Headless components
429
+ */
430
+ export function RadioGroup(props: RadioGroupProps): HtmlNode {
431
+ const {
432
+ bind,
433
+ options,
434
+ class: cls,
435
+ optionClass,
436
+ children: _ignore,
437
+ ...rest
438
+ } = props as RadioGroupProps & { children?: unknown };
439
+ const name = _injectedBindKey(props, "bind") ?? _resolveReactiveName(bind);
440
+ const current = bind;
441
+
442
+ const radios = options.map((o, i) => {
443
+ const v = JSON.stringify(o.value);
444
+ const selected = o.value === current;
445
+ const out: Record<string, unknown> = {
446
+ role: "radio",
447
+ "aria-checked": selected ? "true" : "false",
448
+ class: cx("flow-radio", optionClass),
449
+ children: o.label,
450
+ };
451
+ if (selected) out["data-checked"] = "";
452
+ // first option is tab-reachable when nothing is selected (roving tabindex)
453
+ out["tabindex"] = selected || (current == null && i === 0) ? 0 : -1;
454
+ if (name) {
455
+ out[":aria-checked"] = `$flow.${name} === ${v}`;
456
+ out[":data-checked"] = `$flow.${name} === ${v} ? '' : null`;
457
+ out[":tabindex"] = `$flow.${name} === ${v} || ($flow.${name} == null && ${i === 0}) ? 0 : -1`;
458
+ out["x-on:click"] = `select(${v})`;
459
+ out["x-on:keydown"] = "onKey($event)";
460
+ }
461
+ return jsx("div", out);
462
+ });
463
+
464
+ return jsx("div", {
465
+ ...rest,
466
+ role: "radiogroup",
467
+ "x-data": name ? `flowRadioGroup({ name: '${name}' })` : "{}",
468
+ class: cx("flow-radiogroup", cls),
469
+ children: radios,
470
+ });
471
+ }
472
+
473
+ // ── <Listbox> ─────────────────────────────────────────────────────────────────
474
+ // A custom, fully-keyboard-navigable select (role="listbox") bound to an @expose
475
+ // value — arrow keys, Home/End, Enter/Escape, aria-activedescendant. Use custom
476
+ // option markup; the committed value round-trips through the server. Pass
477
+ // `multiple` for multi-select (value is an array). States: data-[selected],
478
+ // data-[active], data-[open].
479
+ //
480
+ // <Listbox bind={this.assignee} options={[{ label: 'Jo', value: 1 }]} />
481
+
482
+ export interface ListboxOption {
483
+ label: unknown;
484
+ value: string | number;
485
+ }
486
+ export interface ListboxProps {
487
+ bind: unknown;
488
+ options: ListboxOption[];
489
+ multiple?: boolean;
490
+ placeholder?: string;
491
+ class?: string;
492
+ buttonClass?: string;
493
+ optionsClass?: string;
494
+ optionClass?: string;
495
+ [key: string]: unknown;
496
+ }
497
+
498
+ /**
499
+ * A custom, fully keyboard-navigable select (`role="listbox"`) bound to an `@expose` value —
500
+ * arrow keys, Home/End, Enter/Escape, `aria-activedescendant`. Pass `multiple` for
501
+ * multi-select (value is an array). States: `data-[selected]`, `data-[active]`, `data-[open]`.
502
+ * @category Headless components
503
+ */
504
+ export function Listbox(props: ListboxProps): HtmlNode {
505
+ const {
506
+ bind,
507
+ options,
508
+ multiple,
509
+ placeholder,
510
+ class: cls,
511
+ buttonClass,
512
+ optionsClass,
513
+ optionClass,
514
+ children: _ignore,
515
+ ...rest
516
+ } = props as ListboxProps & { children?: unknown };
517
+ const name = _injectedBindKey(props, "bind") ?? _resolveReactiveName(bind);
518
+ const current = bind;
519
+
520
+ // Server-rendered button label (updates on the round-trip after select).
521
+ const selectedLabel = (() => {
522
+ if (multiple) {
523
+ const arr = Array.isArray(current) ? current : [];
524
+ const labels = options.filter((o) => arr.includes(o.value)).map((o) => o.label);
525
+ return labels.length ? labels.join(", ") : (placeholder ?? "Select…");
526
+ }
527
+ const hit = options.find((o) => o.value === current);
528
+ return hit ? hit.label : (placeholder ?? "Select…");
529
+ })();
530
+
531
+ const button = jsx("button", {
532
+ type: "button",
533
+ "x-on:click": "toggle()",
534
+ "x-on:keydown": "onButtonKey($event)",
535
+ ":aria-expanded": "open",
536
+ "aria-haspopup": "listbox",
537
+ ":data-open": "open ? '' : null",
538
+ class: cx("flow-listbox-button", buttonClass),
539
+ children: selectedLabel,
540
+ });
541
+
542
+ const optionEls = options.map((o, i) => {
543
+ const v = JSON.stringify(o.value);
544
+ const selected = multiple
545
+ ? Array.isArray(current) && current.includes(o.value)
546
+ : o.value === current;
547
+ const out: Record<string, unknown> = {
548
+ id: name ? `${name}-opt-${i}` : undefined,
549
+ role: "option",
550
+ "data-value": v,
551
+ "aria-selected": selected ? "true" : "false",
552
+ ":aria-selected": `isSelected(${v})`,
553
+ ":data-selected": `isSelected(${v}) ? '' : null`,
554
+ ":data-active": `active === ${i} ? '' : null`,
555
+ "x-on:click": `select(${v})`,
556
+ "x-on:mousemove": `active = ${i}`,
557
+ class: cx("flow-listbox-option", optionClass),
558
+ children: o.label,
559
+ };
560
+ if (selected) out["data-selected"] = "";
561
+ return jsx("li", out);
562
+ });
563
+
564
+ const list = jsx("ul", {
565
+ role: "listbox",
566
+ tabindex: -1,
567
+ "x-show": "open",
568
+ "x-cloak": true,
569
+ "x-on:keydown": "onKey($event)",
570
+ "x-on:click.outside": "close()",
571
+ ":aria-activedescendant": name ? `active >= 0 ? '${name}-opt-' + active : null` : "null",
572
+ ...(multiple ? { "aria-multiselectable": "true" } : {}),
573
+ class: cx("flow-listbox-options", optionsClass),
574
+ children: optionEls,
575
+ });
576
+
577
+ return jsx("div", {
578
+ ...rest,
579
+ "x-data": name
580
+ ? `flowListbox({ name: '${name}', multiple: ${!!multiple} })`
581
+ : "{ open: false, active: -1 }",
582
+ class: cx("flow-listbox", cls),
583
+ children: [button, list],
584
+ });
585
+ }
586
+
587
+ // ── <Combobox> ────────────────────────────────────────────────────────────────
588
+ // An autocomplete: a text input + a filtered option list, bound to an @expose
589
+ // value. Two modes:
590
+ // • client (default) — options are rendered once and filtered locally as you type.
591
+ // • server — pass `query={this.search}`: the input uses flow:model.live, so the
592
+ // SERVER re-renders the (already-filtered) options every keystroke. Render
593
+ // `options` from your filtered server list. This is server-side autocomplete
594
+ // with full headless UX.
595
+ //
596
+ // <Combobox bind={this.assignee} options={people} /> // client filter
597
+ // <Combobox bind={this.cityId} query={this.search} options={hits} /> // server filter
598
+ //
599
+ // For an unambiguous binding you can pass explicit prop names (`name` / `queryName`
600
+ // strings) instead of `bind` / `query`. States: data-[active], data-[selected].
601
+ // ARIA: role=combobox/listbox/option.
602
+
603
+ export interface ComboboxOption {
604
+ label: string;
605
+ value: string | number;
606
+ }
607
+ export interface ComboboxProps {
608
+ bind?: unknown;
609
+ /** Present → server-filter mode: the bound query @expose prop. */
610
+ query?: unknown;
611
+ /** Explicit value prop-name (escape hatch instead of `bind`). */
612
+ name?: string;
613
+ /** Explicit query prop-name (escape hatch instead of `query`). */
614
+ queryName?: string;
615
+ options: ComboboxOption[];
616
+ placeholder?: string;
617
+ class?: string;
618
+ inputClass?: string;
619
+ optionsClass?: string;
620
+ optionClass?: string;
621
+ [key: string]: unknown;
622
+ }
623
+
624
+ /**
625
+ * An autocomplete: a text input plus a filtered option list bound to an `@expose` value.
626
+ * Client mode (default) filters locally; passing `query={this.search}` switches to server-side
627
+ * filtering, where the input uses `flow:model.live` and the server re-renders matches per
628
+ * keystroke. States: `data-[active]`, `data-[selected]`.
629
+ * @category Headless components
630
+ *
631
+ * @example
632
+ * ```tsx
633
+ * <Combobox bind={this.assignee} options={people} /> // client filter
634
+ * <Combobox bind={this.cityId} query={this.search} options={hits} /> // server filter
635
+ * ```
636
+ */
637
+ export function Combobox(props: ComboboxProps): HtmlNode {
638
+ const {
639
+ bind,
640
+ query,
641
+ name: nameProp,
642
+ queryName: queryNameProp,
643
+ options,
644
+ placeholder,
645
+ class: cls,
646
+ inputClass,
647
+ optionsClass,
648
+ optionClass,
649
+ children: _ignore,
650
+ ...rest
651
+ } = props as ComboboxProps & { children?: unknown };
652
+
653
+ // Resolve the QUERY binding first (its capture is freshest — it's evaluated last
654
+ // in the props), then the value binding (identity fallback). `name`/`queryName`
655
+ // strings are explicit overrides — note `bind`/`query` are reactive *values*, so
656
+ // we never treat a string value as a name.
657
+ const queryName =
658
+ queryNameProp ??
659
+ _injectedBindKey(props, "query") ??
660
+ (query === undefined ? null : _resolveReactiveName(query));
661
+ const name = nameProp ?? _injectedBindKey(props, "bind") ?? _resolveReactiveName(bind);
662
+ const server = queryName != null;
663
+ const current = bind;
664
+
665
+ const selectedLabel = (() => {
666
+ const hit = options.find((o) => o.value === current);
667
+ return hit ? hit.label : "";
668
+ })();
669
+
670
+ const listId = name ? `${name}-list` : "flow-combobox-list";
671
+
672
+ const input = jsx("input", {
673
+ type: "text",
674
+ role: "combobox",
675
+ autocomplete: "off",
676
+ "aria-autocomplete": "list",
677
+ "aria-controls": listId,
678
+ ":aria-expanded": "open",
679
+ ...(placeholder ? { placeholder } : {}),
680
+ ...(server ? (queryName ? { "flow:model.live": queryName } : {}) : { ":value": "query" }),
681
+ "x-on:input": "onInput($event)",
682
+ "x-on:focus": "openList()",
683
+ "x-on:click": "openList()",
684
+ "x-on:keydown": "onKey($event)",
685
+ class: cx("flow-combobox-input", inputClass),
686
+ });
687
+
688
+ const optionEls = options.map((o, i) => {
689
+ const v = JSON.stringify(o.value);
690
+ const labelLower = JSON.stringify(o.label.toLowerCase());
691
+ const selected = o.value === current;
692
+ const out: Record<string, unknown> = {
693
+ id: name ? `${name}-opt-${i}` : undefined,
694
+ role: "option",
695
+ "data-value": v,
696
+ "data-label": o.label,
697
+ "aria-selected": selected ? "true" : "false",
698
+ ":aria-selected": `isSelected(${v})`,
699
+ ":data-selected": `isSelected(${v}) ? '' : null`,
700
+ ":data-active": `active === ${i} ? '' : null`,
701
+ "x-on:click": "selectEl($event.currentTarget)",
702
+ "x-on:mousemove": `active = ${i}`,
703
+ class: cx("flow-combobox-option", optionClass),
704
+ children: o.label,
705
+ };
706
+ if (selected) out["data-selected"] = "";
707
+ // Client mode filters locally; server mode renders only matches already.
708
+ if (!server) out["x-show"] = `!query || ${labelLower}.includes(query.toLowerCase())`;
709
+ return jsx("li", out);
710
+ });
711
+
712
+ const list = jsx("ul", {
713
+ id: listId,
714
+ role: "listbox",
715
+ "x-show": "open",
716
+ "x-cloak": true,
717
+ "x-on:click.outside": "close()",
718
+ ":aria-activedescendant": name ? `active >= 0 ? '${name}-opt-' + active : null` : "null",
719
+ class: cx("flow-combobox-options", optionsClass),
720
+ children: optionEls,
721
+ });
722
+
723
+ const config = name
724
+ ? `flowCombobox({ name: '${name}', queryName: ${queryName ? `'${queryName}'` : "null"}, query: ${JSON.stringify(server ? "" : selectedLabel)} })`
725
+ : "{ open: false, active: -1, query: '' }";
726
+
727
+ return jsx("div", {
728
+ ...rest,
729
+ "x-data": config,
730
+ class: cx("flow-combobox", cls),
731
+ children: [input, list],
732
+ });
733
+ }
734
+
735
+ // ── <Field> / <Label> / <Description> ─────────────────────────────────────────
736
+ // Accessibility glue around a single control. <Field> wraps a label, the control,
737
+ // an optional description, and an error, and (via the flowField runtime) wires
738
+ // `for`/`id`/`aria-describedby` and keeps `aria-invalid` in sync with the error.
739
+ //
740
+ // <Field label="Email" description="We never share it." error={this.errors.email}>
741
+ // <input value={this.form.email} />
742
+ // </Field>
743
+ //
744
+ // You can also compose <Label>/<Description> manually inside <Field>.
745
+
746
+ export interface LabelProps {
747
+ class?: string;
748
+ children?: unknown;
749
+ [key: string]: unknown;
750
+ }
751
+ /**
752
+ * A styled `<label>`. Compose inside {@link Field} (which wires `for`/`id`) or use standalone.
753
+ * @category Headless components
754
+ */
755
+ export function Label(props: LabelProps): HtmlNode {
756
+ const { class: cls, children, ...rest } = props;
757
+ return jsx("label", { ...rest, class: cx("flow-label", cls), children });
758
+ }
759
+
760
+ export interface DescriptionProps {
761
+ class?: string;
762
+ children?: unknown;
763
+ [key: string]: unknown;
764
+ }
765
+ /**
766
+ * Help/description text for a control; wired as `aria-describedby` when composed in {@link Field}.
767
+ * @category Headless components
768
+ */
769
+ export function Description(props: DescriptionProps): HtmlNode {
770
+ const { class: cls, children, ...rest } = props;
771
+ return jsx("p", {
772
+ ...rest,
773
+ "data-flow-description": true,
774
+ class: cx("flow-description", cls),
775
+ children,
776
+ });
777
+ }
778
+
779
+ export interface FieldProps {
780
+ label?: unknown;
781
+ description?: unknown;
782
+ /** The field accessor — `this.errors.<field>`; renders a self-hiding message. */
783
+ error?: unknown;
784
+ class?: string;
785
+ children?: unknown;
786
+ [key: string]: unknown;
787
+ }
788
+
789
+ /**
790
+ * Accessibility glue around a single control: wraps a label, the control, an optional
791
+ * description, and an error, wiring `for`/`id`/`aria-describedby` and keeping `aria-invalid`
792
+ * in sync with the error. Compose `<Label>`/`<Description>` manually or pass them as props.
793
+ * @category Headless components
794
+ *
795
+ * @example
796
+ * ```tsx
797
+ * <Field label="Email" description="We never share it." error={this.errors.email}>
798
+ * <input value={this.form.email} />
799
+ * </Field>
800
+ * ```
801
+ */
802
+ export function Field(props: FieldProps): HtmlNode {
803
+ const { label, description, error, class: cls, children, ...rest } = props;
804
+ const kids: unknown[] = [];
805
+ if (label !== undefined && label !== null) kids.push(Label({ children: label }));
806
+ kids.push(children);
807
+ if (description !== undefined && description !== null)
808
+ kids.push(Description({ children: description }));
809
+ if (error !== undefined && error !== null) {
810
+ // Same as <Error>: the `error` directive emits flow:error + flow:show + SSR text.
811
+ kids.push(jsx("span", { error, class: "flow-field-error text-red-400 text-xs" }));
812
+ }
813
+ return jsx("div", {
814
+ ...rest,
815
+ "x-data": "flowField()",
816
+ class: cx("flow-field", cls),
817
+ children: kids,
818
+ });
819
+ }
820
+
821
+ // ── <Fieldset> / <Legend> ───────────────────────────────────────────────────────
822
+ // Group related fields. A native <fieldset disabled> cascades the disabled state
823
+ // to every control inside it.
824
+ //
825
+ // <Fieldset legend="Shipping" disabled={this.saving}>…</Fieldset>
826
+
827
+ export interface LegendProps {
828
+ class?: string;
829
+ children?: unknown;
830
+ [key: string]: unknown;
831
+ }
832
+ /**
833
+ * A styled `<legend>` for a {@link Fieldset}.
834
+ * @category Headless components
835
+ */
836
+ export function Legend(props: LegendProps): HtmlNode {
837
+ const { class: cls, children, ...rest } = props;
838
+ return jsx("legend", { ...rest, class: cx("flow-legend", cls), children });
839
+ }
840
+
841
+ export interface FieldsetProps {
842
+ legend?: unknown;
843
+ disabled?: boolean;
844
+ class?: string;
845
+ children?: unknown;
846
+ [key: string]: unknown;
847
+ }
848
+
849
+ /**
850
+ * Groups related fields under an optional legend. A native `<fieldset disabled>` cascades the
851
+ * disabled state to every control inside it.
852
+ * @category Headless components
853
+ *
854
+ * @example
855
+ * ```tsx
856
+ * <Fieldset legend="Shipping" disabled={this.saving}>…</Fieldset>
857
+ * ```
858
+ */
859
+ export function Fieldset(props: FieldsetProps): HtmlNode {
860
+ const { legend, disabled, class: cls, children, ...rest } = props;
861
+ const kids: unknown[] = [];
862
+ if (legend !== undefined && legend !== null) kids.push(Legend({ children: legend }));
863
+ kids.push(children);
864
+ const out: Record<string, unknown> = { ...rest, class: cx("flow-fieldset", cls), children: kids };
865
+ if (disabled) out["disabled"] = true;
866
+ return jsx("fieldset", out);
867
+ }
868
+
869
+ // ── <Slider> ──────────────────────────────────────────────────────────────────
870
+ // A value chosen from a range, over a native `<input type="range">` so keyboard,
871
+ // touch and screen-reader support come from the platform rather than being
872
+ // rebuilt. Alpine owns the live value: the fill and any readout follow the thumb
873
+ // at frame rate, and the bound @expose prop is written once the drag settles.
874
+ //
875
+ // `data-dragging` is exposed while the pointer is down, so a readout can be
876
+ // shown only during the drag if you'd rather it not sit there permanently.
877
+ //
878
+ // <Slider bind={this.volume} min={0} max={100} />
879
+
880
+ export interface SliderProps {
881
+ /** Bound @expose number — `this.volume`. */
882
+ bind?: unknown;
883
+ name?: string;
884
+ min?: number;
885
+ max?: number;
886
+ step?: number;
887
+ class?: string;
888
+ inputClass?: string;
889
+ /**
890
+ * Initial inline style for the input — a track gradient, typically.
891
+ *
892
+ * Server-rendered so the track is filled on first paint, then replaced by
893
+ * {@link SliderProps.inputStyleExpression} once Alpine takes over.
894
+ */
895
+ inputStyle?: string;
896
+ /**
897
+ * Alpine expression producing the input's style while dragging. `value` and
898
+ * `percent()` are in scope.
899
+ */
900
+ inputStyleExpression?: string;
901
+ /** Rendered inside the wrapper, after the input — a readout, a tick strip. */
902
+ children?: unknown;
903
+ [key: string]: unknown;
904
+ }
905
+
906
+ export function Slider(props: SliderProps): HtmlNode {
907
+ const {
908
+ bind,
909
+ name: nameProp,
910
+ min = 0,
911
+ max = 100,
912
+ step = 1,
913
+ class: cls,
914
+ inputClass,
915
+ inputStyle,
916
+ inputStyleExpression,
917
+ children,
918
+ ...rest
919
+ } = props;
920
+
921
+ const name = nameProp ?? _injectedBindKey(props, "bind") ?? _resolveReactiveName(bind);
922
+ const value = typeof bind === "number" ? bind : min;
923
+
924
+ const config = JSON.stringify({ name, min, max, step });
925
+
926
+ return jsx("div", {
927
+ ...rest,
928
+ "x-data": `flowSlider(${config})`,
929
+ ":data-dragging": "dragging ? '' : null",
930
+ class: cx("flow-slider", cls),
931
+ children: [
932
+ jsx("input", {
933
+ type: "range",
934
+ min,
935
+ max,
936
+ step,
937
+ value,
938
+ ...(inputStyle ? { style: inputStyle } : {}),
939
+ ...(inputStyleExpression ? { ":style": inputStyleExpression } : {}),
940
+ "x-model.number": "value",
941
+ // Local while moving, synced on release: the two events are what keep a
942
+ // drag smooth and the server eventually correct.
943
+ "x-on:input": "onInput($event)",
944
+ "x-on:change": "commit()",
945
+ "x-on:keyup.debounce.200ms": "commit()",
946
+ class: cx("flow-slider-input", inputClass),
947
+ }),
948
+ children,
949
+ ],
950
+ });
951
+ }
952
+
953
+ // ── <Toggle> / <ToggleGroup> ──────────────────────────────────────────────────
954
+ // A button that stays pressed, and a set of them. Distinct from Switch: a switch
955
+ // is a setting that applies immediately, a toggle is a mode you are in — which
956
+ // is why this reports `aria-pressed` rather than `role="switch"`.
957
+ //
958
+ // The pressed state flips locally the instant it is clicked and syncs to the
959
+ // bound prop after, so the button never waits on the network to look pressed.
960
+ //
961
+ // <ToggleGroup bind={this.view} options={[{ value: "grid", label: "Grid" }]} />
962
+
963
+ export interface ToggleOption {
964
+ value: string;
965
+ label?: unknown;
966
+ disabled?: boolean;
967
+ }
968
+
969
+ export interface ToggleGroupProps {
970
+ /** Bound @expose value — a string, or an array when `multiple`. */
971
+ bind?: unknown;
972
+ name?: string;
973
+ options: ToggleOption[];
974
+ /** Allow several pressed at once; the bound value becomes an array. */
975
+ multiple?: boolean;
976
+ class?: string;
977
+ optionClass?: string;
978
+ [key: string]: unknown;
979
+ }
980
+
981
+ export function ToggleGroup(props: ToggleGroupProps): HtmlNode {
982
+ const {
983
+ bind,
984
+ name: nameProp,
985
+ options,
986
+ multiple,
987
+ class: cls,
988
+ optionClass,
989
+ children: _ignore,
990
+ ...rest
991
+ } = props as ToggleGroupProps & { children?: unknown };
992
+
993
+ const name = nameProp ?? _injectedBindKey(props, "bind") ?? _resolveReactiveName(bind);
994
+ const current = new Set(Array.isArray(bind) ? bind.map(String) : bind ? [String(bind)] : []);
995
+
996
+ const buttons = options.map((option) => {
997
+ const v = JSON.stringify(option.value);
998
+ const on = current.has(option.value);
999
+ const out: Record<string, unknown> = {
1000
+ type: "button",
1001
+ // Server-rendered pressed state, so the first paint is correct before
1002
+ // Alpine has initialised.
1003
+ "aria-pressed": on ? "true" : "false",
1004
+ ":aria-pressed": `isOn(${v})`,
1005
+ ":data-pressed": `isOn(${v}) ? '' : null`,
1006
+ "x-on:click": `toggle(${v})`,
1007
+ class: cx("flow-toggle", optionClass),
1008
+ children: option.label ?? option.value,
1009
+ };
1010
+ if (on) out["data-pressed"] = "";
1011
+ if (option.disabled) out["disabled"] = true;
1012
+ return jsx("button", out);
1013
+ });
1014
+
1015
+ return jsx("div", {
1016
+ ...rest,
1017
+ // Single-choice reads as a radiogroup; multiple is a plain group of buttons.
1018
+ role: multiple ? "group" : "radiogroup",
1019
+ "x-data": `flowToggleGroup(${JSON.stringify({ name, multiple: Boolean(multiple) })})`,
1020
+ class: cx("flow-toggle-group", cls),
1021
+ children: buttons,
1022
+ });
1023
+ }
1024
+
1025
+ // ── <Calendar> ────────────────────────────────────────────────────────────────
1026
+ // A month grid for picking a date. Paging between months and selecting a day are
1027
+ // entirely client-side — the grid is arithmetic, so a round-trip would be asking
1028
+ // the server a question that needs no data. Only the chosen day is synced.
1029
+ //
1030
+ // Every cell is a real `<button>` inside a grid with `role="grid"`, so arrow-key
1031
+ // and screen-reader behaviour come from the semantics rather than from script.
1032
+ //
1033
+ // <Calendar bind={this.due} min={today} />
1034
+
1035
+ export interface CalendarProps {
1036
+ /** Bound @expose `YYYY-MM-DD` string. */
1037
+ bind?: unknown;
1038
+ name?: string;
1039
+ /** Month first shown, `YYYY-MM`. Defaults to the bound value's month, else now. */
1040
+ month?: string;
1041
+ /** Start weeks on Sunday instead of Monday. */
1042
+ sundayFirst?: boolean;
1043
+ min?: string;
1044
+ max?: string;
1045
+ class?: string;
1046
+ headerClass?: string;
1047
+ navClass?: string;
1048
+ weekdayClass?: string;
1049
+ gridClass?: string;
1050
+ dayClass?: string;
1051
+ [key: string]: unknown;
1052
+ }
1053
+
1054
+ const FLOW_WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
1055
+
1056
+ export function Calendar(props: CalendarProps): HtmlNode {
1057
+ const {
1058
+ bind,
1059
+ name: nameProp,
1060
+ month: monthProp,
1061
+ sundayFirst,
1062
+ min,
1063
+ max,
1064
+ class: cls,
1065
+ headerClass,
1066
+ navClass,
1067
+ weekdayClass,
1068
+ gridClass,
1069
+ dayClass,
1070
+ children: _ignore,
1071
+ ...rest
1072
+ } = props as CalendarProps & { children?: unknown };
1073
+
1074
+ const name = nameProp ?? _injectedBindKey(props, "bind") ?? _resolveReactiveName(bind);
1075
+ const selected = typeof bind === "string" ? bind : "";
1076
+ const now = new Date();
1077
+ const month =
1078
+ monthProp ??
1079
+ (selected
1080
+ ? selected.slice(0, 7)
1081
+ : `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`);
1082
+
1083
+ const weekdays = sundayFirst ? ["Sun", ...FLOW_WEEKDAYS.slice(0, 6)] : FLOW_WEEKDAYS;
1084
+ const config = JSON.stringify({ name, month, sundayFirst: Boolean(sundayFirst), min, max });
1085
+
1086
+ return jsx("div", {
1087
+ ...rest,
1088
+ "x-data": `flowCalendar(${config})`,
1089
+ class: cx("flow-calendar", cls),
1090
+ children: [
1091
+ jsx("div", {
1092
+ class: cx("flow-calendar-header", headerClass),
1093
+ children: [
1094
+ jsx("button", {
1095
+ type: "button",
1096
+ "aria-label": "Previous month",
1097
+ "x-on:click": "shift(-1)",
1098
+ class: cx("flow-calendar-nav", navClass),
1099
+ children: "‹",
1100
+ }),
1101
+ jsx("span", { "x-text": "label()", class: "flow-calendar-label", children: "" }),
1102
+ jsx("button", {
1103
+ type: "button",
1104
+ "aria-label": "Next month",
1105
+ "x-on:click": "shift(1)",
1106
+ class: cx("flow-calendar-nav", navClass),
1107
+ children: "›",
1108
+ }),
1109
+ ],
1110
+ }),
1111
+ jsx("div", {
1112
+ role: "grid",
1113
+ class: cx("flow-calendar-grid", gridClass),
1114
+ children: [
1115
+ ...weekdays.map((d) =>
1116
+ jsx("span", {
1117
+ role: "columnheader",
1118
+ class: cx("flow-calendar-weekday", weekdayClass),
1119
+ children: d,
1120
+ }),
1121
+ ),
1122
+ // One template for all 42 cells: the markup ships once and Alpine
1123
+ // repeats it, rather than the server emitting six weeks of buttons
1124
+ // that are replaced the moment somebody pages the month.
1125
+ jsx("template", {
1126
+ "x-for": "cell in cells()",
1127
+ ":key": "cell.day",
1128
+ children: jsx("button", {
1129
+ type: "button",
1130
+ role: "gridcell",
1131
+ ":aria-label": "cell.day",
1132
+ ":aria-selected": "value === cell.day",
1133
+ ":data-selected": "value === cell.day ? '' : null",
1134
+ ":data-today": "today === cell.day ? '' : null",
1135
+ ":data-outside": "cell.inMonth ? null : ''",
1136
+ ":disabled": "isDisabled(cell.day)",
1137
+ "x-on:click": "select(cell.day)",
1138
+ "x-text": "dayNumber(cell.day)",
1139
+ class: cx("flow-calendar-day", dayClass),
1140
+ children: "",
1141
+ }),
1142
+ }),
1143
+ ],
1144
+ }),
1145
+ ],
1146
+ });
1147
+ }