xote 4.3.0 → 4.4.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.
@@ -0,0 +1,677 @@
1
+ open Signals
2
+ module Component = Xote__Component
3
+
4
+ /* ReScript JSX transform type aliases */
5
+ type element = Component.node
6
+
7
+ type component<'props> = 'props => element
8
+
9
+ type componentLike<'props, 'return> = 'props => 'return
10
+
11
+ /* JSX functions for component creation */
12
+ let jsx = (component: component<'props>, props: 'props): element => component(props)
13
+
14
+ let jsxs = (component: component<'props>, props: 'props): element => component(props)
15
+
16
+ let jsxKeyed = (
17
+ component: component<'props>,
18
+ props: 'props,
19
+ ~key: option<string>=?,
20
+ _: unit,
21
+ ): element => {
22
+ let _ = key /* TODO: Implement key support for list reconciliation */
23
+ component(props)
24
+ }
25
+
26
+ let jsxsKeyed = (
27
+ component: component<'props>,
28
+ props: 'props,
29
+ ~key: option<string>=?,
30
+ _: unit,
31
+ ): element => {
32
+ let _ = key
33
+ component(props)
34
+ }
35
+
36
+ /* Fragment support */
37
+ type fragmentProps = {children?: element}
38
+
39
+ let jsxFragment = (props: fragmentProps): element => {
40
+ switch props.children {
41
+ | Some(child) => child
42
+ | None => Component.fragment([])
43
+ }
44
+ }
45
+
46
+ /* Element converters for JSX expressions */
47
+ let array = (children: array<element>): element => Component.fragment(children)
48
+
49
+ let null = (): element => Component.text("")
50
+
51
+ /* Elements module for lowercase HTML tags */
52
+ module Elements = {
53
+ /* Attribute value type that can be static, signal, or computed */
54
+ @unboxed
55
+ type rec attributeValue = Any('a): attributeValue
56
+
57
+ /* Automatic conversion from string to attributeValue */
58
+ external fromString: string => attributeValue = "%identity"
59
+
60
+ /* Helper to convert a signal to an attributeValue */
61
+ let signal = (s: Signals.Signal.t<string>): attributeValue => Any(s)
62
+
63
+ /* Helper to convert a computed function to an attributeValue */
64
+ let computed = (f: unit => string): attributeValue => Any(f)
65
+
66
+ /* Props type for HTML elements - supports common attributes and events
67
+ * String-like attributes use polymorphic types to accept strings, signals, or computed functions
68
+ */
69
+ type props<
70
+ 'id,
71
+ 'class,
72
+ 'style,
73
+ 'typ,
74
+ 'name,
75
+ 'value,
76
+ 'placeholder,
77
+ 'min,
78
+ 'max,
79
+ 'step,
80
+ 'pattern,
81
+ 'autoComplete,
82
+ 'accept,
83
+ 'forAttr,
84
+ 'href,
85
+ 'target,
86
+ 'src,
87
+ 'alt,
88
+ 'width,
89
+ 'height,
90
+ 'role,
91
+ 'ariaLabel,
92
+ > = {
93
+ /* Standard attributes - can be static strings, signals, or computed values */
94
+ id?: 'id,
95
+ class?: 'class,
96
+ style?: 'style,
97
+ /* Form/Input attributes */
98
+ @as("type") type_?: 'typ,
99
+ name?: 'name,
100
+ value?: 'value,
101
+ placeholder?: 'placeholder,
102
+ disabled?: bool,
103
+ checked?: bool,
104
+ required?: bool,
105
+ readOnly?: bool,
106
+ maxLength?: int,
107
+ minLength?: int,
108
+ min?: 'min,
109
+ max?: 'max,
110
+ step?: 'step,
111
+ pattern?: 'pattern,
112
+ autoComplete?: 'autoComplete,
113
+ multiple?: bool,
114
+ accept?: 'accept,
115
+ rows?: int,
116
+ cols?: int,
117
+ /* Label attributes */
118
+ @as("for") for_?: 'forAttr,
119
+ /* Link attributes */
120
+ href?: 'href,
121
+ target?: 'target,
122
+ /* Image attributes */
123
+ src?: 'src,
124
+ alt?: 'alt,
125
+ width?: 'width,
126
+ height?: 'height,
127
+ /* Accessibility attributes */
128
+ role?: 'role,
129
+ tabIndex?: int,
130
+ @as("aria-label") ariaLabel?: 'ariaLabel,
131
+ @as("aria-hidden") ariaHidden?: bool,
132
+ @as("aria-expanded") ariaExpanded?: bool,
133
+ @as("aria-selected") ariaSelected?: bool,
134
+ /* Data attributes */
135
+ data?: Obj.t,
136
+ /* Event handlers */
137
+ onClick?: Dom.event => unit,
138
+ onInput?: Dom.event => unit,
139
+ onChange?: Dom.event => unit,
140
+ onSubmit?: Dom.event => unit,
141
+ onFocus?: Dom.event => unit,
142
+ onBlur?: Dom.event => unit,
143
+ onKeyDown?: Dom.event => unit,
144
+ onKeyUp?: Dom.event => unit,
145
+ onMouseEnter?: Dom.event => unit,
146
+ onMouseLeave?: Dom.event => unit,
147
+ /* Children */
148
+ children?: element,
149
+ }
150
+
151
+ /* Helper to detect if a value is a signal (has an id property) */
152
+ @get external hasId: 'a => option<int> = "id"
153
+
154
+ /* Helper to convert any value to Component.attrValue */
155
+ let convertAttrValue = (key: string, value: 'a): (string, Component.attrValue) => {
156
+ // Check if it's a function (computed)
157
+ if typeof(value) == #function {
158
+ // It's a computed function
159
+ let f: unit => string = Obj.magic(value)
160
+ Component.computedAttr(key, f)
161
+ } else if typeof(value) == #object && hasId(value)->Option.isSome {
162
+ // It's a signal (has an id property)
163
+ let sig: Signal.t<string> = Obj.magic(value)
164
+ Component.signalAttr(key, sig)
165
+ } else {
166
+ // It's a static string
167
+ let s: string = Obj.magic(value)
168
+ Component.attr(key, s)
169
+ }
170
+ }
171
+
172
+ /* Convert props to attrs array */
173
+ let propsToAttrs = (
174
+ props: props<
175
+ 'id,
176
+ 'class,
177
+ 'style,
178
+ 'typ,
179
+ 'name,
180
+ 'value,
181
+ 'placeholder,
182
+ 'min,
183
+ 'max,
184
+ 'step,
185
+ 'pattern,
186
+ 'autoComplete,
187
+ 'accept,
188
+ 'forAttr,
189
+ 'href,
190
+ 'target,
191
+ 'src,
192
+ 'alt,
193
+ 'width,
194
+ 'height,
195
+ 'role,
196
+ 'ariaLabel,
197
+ >,
198
+ ): array<(string, Component.attrValue)> => {
199
+ let attrs = []
200
+
201
+ /* Standard attributes */
202
+ switch props.id {
203
+ | Some(v) => attrs->Array.push(convertAttrValue("id", v))
204
+ | None => ()
205
+ }
206
+
207
+ switch props.class {
208
+ | Some(v) => attrs->Array.push(convertAttrValue("class", v))
209
+ | None => ()
210
+ }
211
+
212
+ switch props.style {
213
+ | Some(v) => attrs->Array.push(convertAttrValue("style", v))
214
+ | None => ()
215
+ }
216
+
217
+ /* Form/Input attributes */
218
+ switch props.type_ {
219
+ | Some(v) => attrs->Array.push(convertAttrValue("type", v))
220
+ | None => ()
221
+ }
222
+
223
+ switch props.name {
224
+ | Some(v) => attrs->Array.push(convertAttrValue("name", v))
225
+ | None => ()
226
+ }
227
+
228
+ switch props.value {
229
+ | Some(v) => attrs->Array.push(convertAttrValue("value", v))
230
+ | None => ()
231
+ }
232
+
233
+ switch props.placeholder {
234
+ | Some(v) => attrs->Array.push(convertAttrValue("placeholder", v))
235
+ | None => ()
236
+ }
237
+
238
+ switch props.disabled {
239
+ | Some(true) => attrs->Array.push(Component.attr("disabled", "true"))
240
+ | _ => ()
241
+ }
242
+
243
+ switch props.checked {
244
+ | Some(true) => attrs->Array.push(Component.attr("checked", "true"))
245
+ | _ => ()
246
+ }
247
+
248
+ switch props.required {
249
+ | Some(true) => attrs->Array.push(Component.attr("required", "true"))
250
+ | _ => ()
251
+ }
252
+
253
+ switch props.readOnly {
254
+ | Some(true) => attrs->Array.push(Component.attr("readonly", "true"))
255
+ | _ => ()
256
+ }
257
+
258
+ switch props.maxLength {
259
+ | Some(v) => attrs->Array.push(Component.attr("maxlength", Int.toString(v)))
260
+ | None => ()
261
+ }
262
+
263
+ switch props.minLength {
264
+ | Some(v) => attrs->Array.push(Component.attr("minlength", Int.toString(v)))
265
+ | None => ()
266
+ }
267
+
268
+ switch props.min {
269
+ | Some(v) => attrs->Array.push(convertAttrValue("min", v))
270
+ | None => ()
271
+ }
272
+
273
+ switch props.max {
274
+ | Some(v) => attrs->Array.push(convertAttrValue("max", v))
275
+ | None => ()
276
+ }
277
+
278
+ switch props.step {
279
+ | Some(v) => attrs->Array.push(convertAttrValue("step", v))
280
+ | None => ()
281
+ }
282
+
283
+ switch props.pattern {
284
+ | Some(v) => attrs->Array.push(convertAttrValue("pattern", v))
285
+ | None => ()
286
+ }
287
+
288
+ switch props.autoComplete {
289
+ | Some(v) => attrs->Array.push(convertAttrValue("autocomplete", v))
290
+ | None => ()
291
+ }
292
+
293
+ switch props.multiple {
294
+ | Some(true) => attrs->Array.push(Component.attr("multiple", "true"))
295
+ | _ => ()
296
+ }
297
+
298
+ switch props.accept {
299
+ | Some(v) => attrs->Array.push(convertAttrValue("accept", v))
300
+ | None => ()
301
+ }
302
+
303
+ switch props.rows {
304
+ | Some(v) => attrs->Array.push(Component.attr("rows", Int.toString(v)))
305
+ | None => ()
306
+ }
307
+
308
+ switch props.cols {
309
+ | Some(v) => attrs->Array.push(Component.attr("cols", Int.toString(v)))
310
+ | None => ()
311
+ }
312
+
313
+ /* Label attributes */
314
+ switch props.for_ {
315
+ | Some(v) => attrs->Array.push(convertAttrValue("for", v))
316
+ | None => ()
317
+ }
318
+
319
+ /* Link attributes */
320
+ switch props.href {
321
+ | Some(v) => attrs->Array.push(convertAttrValue("href", v))
322
+ | None => ()
323
+ }
324
+
325
+ switch props.target {
326
+ | Some(v) => attrs->Array.push(convertAttrValue("target", v))
327
+ | None => ()
328
+ }
329
+
330
+ /* Image attributes */
331
+ switch props.src {
332
+ | Some(v) => attrs->Array.push(convertAttrValue("src", v))
333
+ | None => ()
334
+ }
335
+
336
+ switch props.alt {
337
+ | Some(v) => attrs->Array.push(convertAttrValue("alt", v))
338
+ | None => ()
339
+ }
340
+
341
+ switch props.width {
342
+ | Some(v) => attrs->Array.push(convertAttrValue("width", v))
343
+ | None => ()
344
+ }
345
+
346
+ switch props.height {
347
+ | Some(v) => attrs->Array.push(convertAttrValue("height", v))
348
+ | None => ()
349
+ }
350
+
351
+ /* Accessibility attributes */
352
+ switch props.role {
353
+ | Some(v) => attrs->Array.push(convertAttrValue("role", v))
354
+ | None => ()
355
+ }
356
+
357
+ switch props.tabIndex {
358
+ | Some(v) => attrs->Array.push(Component.attr("tabindex", Int.toString(v)))
359
+ | None => ()
360
+ }
361
+
362
+ switch props.ariaLabel {
363
+ | Some(v) => attrs->Array.push(convertAttrValue("aria-label", v))
364
+ | None => ()
365
+ }
366
+
367
+ switch props.ariaHidden {
368
+ | Some(true) => attrs->Array.push(Component.attr("aria-hidden", "true"))
369
+ | Some(false) => attrs->Array.push(Component.attr("aria-hidden", "false"))
370
+ | None => ()
371
+ }
372
+
373
+ switch props.ariaExpanded {
374
+ | Some(true) => attrs->Array.push(Component.attr("aria-expanded", "true"))
375
+ | Some(false) => attrs->Array.push(Component.attr("aria-expanded", "false"))
376
+ | None => ()
377
+ }
378
+
379
+ switch props.ariaSelected {
380
+ | Some(true) => attrs->Array.push(Component.attr("aria-selected", "true"))
381
+ | Some(false) => attrs->Array.push(Component.attr("aria-selected", "false"))
382
+ | None => ()
383
+ }
384
+
385
+ /* Data attributes */
386
+ switch props.data {
387
+ | Some(_dataObj) => {
388
+ let _ = %raw(`
389
+ Object.entries(_dataObj).forEach(([key, value]) => {
390
+ attrs.push(convertAttrValue("data-" + key, value))
391
+ })
392
+ `)
393
+ }
394
+ | None => ()
395
+ }
396
+
397
+ attrs
398
+ }
399
+
400
+ /* Convert props to events array */
401
+ let propsToEvents = (
402
+ props: props<
403
+ 'id,
404
+ 'class,
405
+ 'style,
406
+ 'typ,
407
+ 'name,
408
+ 'value,
409
+ 'placeholder,
410
+ 'min,
411
+ 'max,
412
+ 'step,
413
+ 'pattern,
414
+ 'autoComplete,
415
+ 'accept,
416
+ 'forAttr,
417
+ 'href,
418
+ 'target,
419
+ 'src,
420
+ 'alt,
421
+ 'width,
422
+ 'height,
423
+ 'role,
424
+ 'ariaLabel,
425
+ >,
426
+ ): array<(string, Dom.event => unit)> => {
427
+ let events = []
428
+
429
+ switch props.onClick {
430
+ | Some(handler) => events->Array.push(("click", handler))
431
+ | None => ()
432
+ }
433
+
434
+ switch props.onInput {
435
+ | Some(handler) => events->Array.push(("input", handler))
436
+ | None => ()
437
+ }
438
+
439
+ switch props.onChange {
440
+ | Some(handler) => events->Array.push(("change", handler))
441
+ | None => ()
442
+ }
443
+
444
+ switch props.onSubmit {
445
+ | Some(handler) => events->Array.push(("submit", handler))
446
+ | None => ()
447
+ }
448
+
449
+ switch props.onFocus {
450
+ | Some(handler) => events->Array.push(("focus", handler))
451
+ | None => ()
452
+ }
453
+
454
+ switch props.onBlur {
455
+ | Some(handler) => events->Array.push(("blur", handler))
456
+ | None => ()
457
+ }
458
+
459
+ switch props.onKeyDown {
460
+ | Some(handler) => events->Array.push(("keydown", handler))
461
+ | None => ()
462
+ }
463
+
464
+ switch props.onKeyUp {
465
+ | Some(handler) => events->Array.push(("keyup", handler))
466
+ | None => ()
467
+ }
468
+
469
+ switch props.onMouseEnter {
470
+ | Some(handler) => events->Array.push(("mouseenter", handler))
471
+ | None => ()
472
+ }
473
+
474
+ switch props.onMouseLeave {
475
+ | Some(handler) => events->Array.push(("mouseleave", handler))
476
+ | None => ()
477
+ }
478
+
479
+ events
480
+ }
481
+
482
+ /* Extract children from props */
483
+ let getChildren = (
484
+ props: props<
485
+ 'id,
486
+ 'class,
487
+ 'style,
488
+ 'typ,
489
+ 'name,
490
+ 'value,
491
+ 'placeholder,
492
+ 'min,
493
+ 'max,
494
+ 'step,
495
+ 'pattern,
496
+ 'autoComplete,
497
+ 'accept,
498
+ 'forAttr,
499
+ 'href,
500
+ 'target,
501
+ 'src,
502
+ 'alt,
503
+ 'width,
504
+ 'height,
505
+ 'role,
506
+ 'ariaLabel,
507
+ >,
508
+ ): array<element> => {
509
+ switch props.children {
510
+ | Some(Fragment(children)) => children
511
+ | Some(child) => [child]
512
+ | None => []
513
+ }
514
+ }
515
+
516
+ /* Create an element from a tag string and props */
517
+ let createElement = (
518
+ tag: string,
519
+ props: props<
520
+ 'id,
521
+ 'class,
522
+ 'style,
523
+ 'typ,
524
+ 'name,
525
+ 'value,
526
+ 'placeholder,
527
+ 'min,
528
+ 'max,
529
+ 'step,
530
+ 'pattern,
531
+ 'autoComplete,
532
+ 'accept,
533
+ 'forAttr,
534
+ 'href,
535
+ 'target,
536
+ 'src,
537
+ 'alt,
538
+ 'width,
539
+ 'height,
540
+ 'role,
541
+ 'ariaLabel,
542
+ >,
543
+ ): element => {
544
+ Component.Element({
545
+ tag,
546
+ attrs: propsToAttrs(props),
547
+ events: propsToEvents(props),
548
+ children: getChildren(props),
549
+ })
550
+ }
551
+
552
+ /* JSX functions for HTML elements */
553
+ let jsx = (
554
+ tag: string,
555
+ props: props<
556
+ 'id,
557
+ 'class,
558
+ 'style,
559
+ 'typ,
560
+ 'name,
561
+ 'value,
562
+ 'placeholder,
563
+ 'min,
564
+ 'max,
565
+ 'step,
566
+ 'pattern,
567
+ 'autoComplete,
568
+ 'accept,
569
+ 'forAttr,
570
+ 'href,
571
+ 'target,
572
+ 'src,
573
+ 'alt,
574
+ 'width,
575
+ 'height,
576
+ 'role,
577
+ 'ariaLabel,
578
+ >,
579
+ ): element => createElement(tag, props)
580
+
581
+ let jsxs = (
582
+ tag: string,
583
+ props: props<
584
+ 'id,
585
+ 'class,
586
+ 'style,
587
+ 'typ,
588
+ 'name,
589
+ 'value,
590
+ 'placeholder,
591
+ 'min,
592
+ 'max,
593
+ 'step,
594
+ 'pattern,
595
+ 'autoComplete,
596
+ 'accept,
597
+ 'forAttr,
598
+ 'href,
599
+ 'target,
600
+ 'src,
601
+ 'alt,
602
+ 'width,
603
+ 'height,
604
+ 'role,
605
+ 'ariaLabel,
606
+ >,
607
+ ): element => createElement(tag, props)
608
+
609
+ let jsxKeyed = (
610
+ tag: string,
611
+ props: props<
612
+ 'id,
613
+ 'class,
614
+ 'style,
615
+ 'typ,
616
+ 'name,
617
+ 'value,
618
+ 'placeholder,
619
+ 'min,
620
+ 'max,
621
+ 'step,
622
+ 'pattern,
623
+ 'autoComplete,
624
+ 'accept,
625
+ 'forAttr,
626
+ 'href,
627
+ 'target,
628
+ 'src,
629
+ 'alt,
630
+ 'width,
631
+ 'height,
632
+ 'role,
633
+ 'ariaLabel,
634
+ >,
635
+ ~key: option<string>=?,
636
+ _: unit,
637
+ ): element => {
638
+ let _ = key
639
+ createElement(tag, props)
640
+ }
641
+
642
+ let jsxsKeyed = (
643
+ tag: string,
644
+ props: props<
645
+ 'id,
646
+ 'class,
647
+ 'style,
648
+ 'typ,
649
+ 'name,
650
+ 'value,
651
+ 'placeholder,
652
+ 'min,
653
+ 'max,
654
+ 'step,
655
+ 'pattern,
656
+ 'autoComplete,
657
+ 'accept,
658
+ 'forAttr,
659
+ 'href,
660
+ 'target,
661
+ 'src,
662
+ 'alt,
663
+ 'width,
664
+ 'height,
665
+ 'role,
666
+ 'ariaLabel,
667
+ >,
668
+ ~key: option<string>=?,
669
+ _: unit,
670
+ ): element => {
671
+ let _ = key
672
+ createElement(tag, props)
673
+ }
674
+
675
+ /* Element helper for ReScript JSX type checking */
676
+ external someElement: element => option<element> = "%identity"
677
+ }