@pfern/elements 0.1.11 → 0.2.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,696 @@
1
+ import { htmlTagNames, svgTagNames } from './tags.js'
2
+
3
+ /**
4
+ * Primitive values that can be serialized as HTML/SVG attribute values.
5
+ *
6
+ * Note: the runtime assigns most props via `setAttribute`, so strings are the
7
+ * canonical representation. Numbers/booleans are accepted for convenience and
8
+ * are coerced by the DOM.
9
+ *
10
+ * @typedef {string | number | boolean | null | undefined}
11
+ * ElementsAttributeValue
12
+ */
13
+
14
+ /**
15
+ * Inline style object applied via `Object.assign(el.style, style)`.
16
+ *
17
+ * @typedef {Partial<CSSStyleDeclaration> & Record<string, string | number>}
18
+ * ElementsStyleObject
19
+ */
20
+
21
+ /**
22
+ * A vnode is a declarative array of the form:
23
+ *
24
+ * ```js
25
+ * [tag, props, ...children]
26
+ * ```
27
+ *
28
+ * @typedef {[tag: string, props: ElementsProps, ...children: ElementsChild[]]}
29
+ * ElementsVNode
30
+ */
31
+
32
+ /**
33
+ * Child nodes are plain values, nested arrays, or vnodes.
34
+ *
35
+ * Note: nested arrays are treated as children values (they are not
36
+ * automatically flattened).
37
+ *
38
+ * @typedef {ElementsVNode | string | number | boolean | null | undefined
39
+ * | any[]} ElementsChild
40
+ */
41
+
42
+ /**
43
+ * Props for the `ontick` animation hook.
44
+ *
45
+ * `ontick` is not a DOM event. It runs once per animation frame and can thread
46
+ * context across ticks:
47
+ *
48
+ * ```js
49
+ * ontick: (el, ctx = { t: 0 }, dt) => ({ ...ctx, t: ctx.t + dt })
50
+ * ```
51
+ *
52
+ * The handler must be synchronous; thrown errors stop ticking.
53
+ *
54
+ * @callback ElementsOnTick
55
+ * @param {Element} el
56
+ * @param {any} ctx
57
+ * @param {number} dtMs
58
+ * @returns {any | void}
59
+ */
60
+
61
+ /**
62
+ * If an event handler returns a vnode array, the UI updates by replacing the
63
+ * nearest component boundary. Otherwise the event is treated as passive.
64
+ *
65
+ * @typedef {ElementsVNode | void | null | false | '' | 0} ElementsEventResult
66
+ * @typedef {ElementsEventResult | Promise<ElementsEventResult>}
67
+ * ElementsMaybeAsyncEventResult
68
+ */
69
+
70
+ /**
71
+ * @template {Event} Evt
72
+ * @callback ElementsEventHandler
73
+ * @param {Evt} event
74
+ * @returns {ElementsMaybeAsyncEventResult}
75
+ */
76
+
77
+ /**
78
+ * Form event handlers receive `(elements, event)`.
79
+ *
80
+ * @template {Event} FormEvt
81
+ * @callback ElementsFormEventHandler
82
+ * @param {any} elements
83
+ * @param {FormEvt} event
84
+ * @returns {ElementsMaybeAsyncEventResult}
85
+ */
86
+
87
+ /**
88
+ * Common global HTML attributes.
89
+ *
90
+ * This is a permissive shared set: some attributes only apply to certain
91
+ * elements in the HTML spec, but Elements.js accepts them everywhere because
92
+ * the underlying DOM will ignore irrelevant attributes.
93
+ *
94
+ * This is not exhaustive; `data-*` and `aria-*` are supported via template
95
+ * keys below.
96
+ *
97
+ * @typedef {{
98
+ * id?: string,
99
+ * class?: string,
100
+ * title?: string,
101
+ * role?: string,
102
+ * name?: string,
103
+ * part?: string,
104
+ * slot?: string,
105
+ * exportparts?: string,
106
+ * nonce?: string,
107
+ * elementtiming?: string,
108
+ * contentsecuritypolicy?: string,
109
+ * popovertarget?: string,
110
+ * popovertargetaction?: 'toggle' | 'show' | 'hide',
111
+ * autocapitalize?:
112
+ * | 'none'
113
+ * | 'sentences'
114
+ * | 'words'
115
+ * | 'characters'
116
+ * | 'on'
117
+ * | 'off',
118
+ * enterkeyhint?:
119
+ * | 'enter'
120
+ * | 'done'
121
+ * | 'go'
122
+ * | 'next'
123
+ * | 'previous'
124
+ * | 'search'
125
+ * | 'send',
126
+ * inputmode?:
127
+ * | 'none'
128
+ * | 'text'
129
+ * | 'tel'
130
+ * | 'url'
131
+ * | 'email'
132
+ * | 'numeric'
133
+ * | 'decimal'
134
+ * | 'search',
135
+ * virtualkeyboardpolicy?: 'auto' | 'manual',
136
+ * draggable?: boolean,
137
+ * hidden?: boolean,
138
+ * inert?: boolean,
139
+ * spellcheck?: boolean,
140
+ * tabindex?: number | string,
141
+ * accesskey?: string,
142
+ * translate?: 'yes' | 'no',
143
+ * autocorrect?: 'on' | 'off',
144
+ * autocomplete?: string,
145
+ * lang?: string,
146
+ * dir?: 'ltr' | 'rtl' | 'auto',
147
+ * is?: string,
148
+ * writingsuggestions?: 'true' | 'false' | boolean,
149
+ * contenteditable?: 'true' | 'false' | 'plaintext-only' | boolean,
150
+ * autofocus?: boolean,
151
+ * popover?: 'auto' | 'manual',
152
+ * label?: string,
153
+ * itemscope?: boolean,
154
+ * itemtype?: string,
155
+ * itemid?: string,
156
+ * itemprop?: string,
157
+ * itemref?: string,
158
+ * vocab?: string,
159
+ * typeof?: string,
160
+ * property?: string,
161
+ * resource?: string,
162
+ * prefix?: string,
163
+ * href?: string,
164
+ * hreflang?: string,
165
+ * referrerpolicy?: string,
166
+ * target?: string,
167
+ * rel?: string,
168
+ * download?: string,
169
+ * ping?: string,
170
+ * src?: string,
171
+ * srcset?: string,
172
+ * sizes?: string,
173
+ * imagesrcset?: string,
174
+ * imagesizes?: string,
175
+ * alt?: string,
176
+ * width?: number | string,
177
+ * height?: number | string,
178
+ * type?: string,
179
+ * content?: string,
180
+ * placeholder?: string,
181
+ * required?: boolean,
182
+ * readonly?: boolean,
183
+ * minlength?: number | string,
184
+ * maxlength?: number | string,
185
+ * min?: number | string,
186
+ * max?: number | string,
187
+ * step?: number | string,
188
+ * pattern?: string,
189
+ * accept?: string,
190
+ * for?: string,
191
+ * action?: string,
192
+ * method?: string,
193
+ * enctype?: string,
194
+ * novalidate?: boolean,
195
+ * formaction?: string,
196
+ * formenctype?: string,
197
+ * formmethod?: string,
198
+ * formnovalidate?: boolean,
199
+ * formtarget?: string,
200
+ * rows?: number | string,
201
+ * cols?: number | string,
202
+ * wrap?: string,
203
+ * list?: string,
204
+ * async?: boolean,
205
+ * defer?: boolean,
206
+ * nomodule?: boolean,
207
+ * crossorigin?: '' | 'anonymous' | 'use-credentials',
208
+ * integrity?: string,
209
+ * fetchpriority?: 'auto' | 'high' | 'low',
210
+ * loading?: 'lazy' | 'eager',
211
+ * decoding?: 'sync' | 'async' | 'auto',
212
+ * media?: string,
213
+ * as?: string,
214
+ * controls?: boolean,
215
+ * controlslist?: string,
216
+ * autoplay?: boolean,
217
+ * loop?: boolean,
218
+ * playsinline?: boolean,
219
+ * preload?: 'none' | 'metadata' | 'auto',
220
+ * poster?: string
221
+ * }} ElementsGlobalAttributes
222
+ */
223
+
224
+ /**
225
+ * @typedef {{ [key: `data-${string}`]: ElementsAttributeValue }}
226
+ * ElementsDataAttributes
227
+ */
228
+
229
+ /**
230
+ * @typedef {{ [key: `aria-${string}`]: ElementsAttributeValue }}
231
+ * ElementsAriaAttributes
232
+ */
233
+
234
+ /**
235
+ * Dash-cased attributes commonly used by custom elements and utilities.
236
+ *
237
+ * @typedef {{ [key: `${string}-${string}`]: ElementsAttributeValue }}
238
+ * ElementsDashedAttributes
239
+ */
240
+
241
+ /**
242
+ * DOM event handler props.
243
+ *
244
+ * @typedef {Omit<{
245
+ * [K in keyof GlobalEventHandlersEventMap as `on${K}`]?:
246
+ * ElementsEventHandler<GlobalEventHandlersEventMap[K]>
247
+ * }, 'oninput' | 'onsubmit' | 'onchange'> & {
248
+ * oninput?: ElementsFormEventHandler<InputEvent | Event>,
249
+ * onsubmit?: ElementsFormEventHandler<SubmitEvent | Event>,
250
+ * onchange?: ElementsFormEventHandler<Event>
251
+ * }} ElementsEventProps
252
+ */
253
+
254
+ /**
255
+ * Special (non-attribute) props supported by Elements.js.
256
+ *
257
+ * Note: some props are assigned as DOM properties (when present on the target
258
+ * element) rather than attributes: `value`, `checked`, `selected`, `disabled`,
259
+ * `multiple`, `muted`, `volume`, `currentTime`, `playbackRate`, `open`,
260
+ * `indeterminate`.
261
+ *
262
+ * @typedef {{
263
+ * style?: ElementsStyleObject,
264
+ * innerHTML?: string,
265
+ * ontick?: ElementsOnTick,
266
+ * value?: string | number,
267
+ * checked?: boolean,
268
+ * selected?: boolean,
269
+ * disabled?: boolean,
270
+ * multiple?: boolean,
271
+ * muted?: boolean,
272
+ * volume?: number,
273
+ * currentTime?: number,
274
+ * playbackRate?: number,
275
+ * open?: boolean,
276
+ * indeterminate?: boolean
277
+ * }} ElementsSpecialProps
278
+ */
279
+
280
+ /**
281
+ * Props accepted by all Elements.js tag helpers.
282
+ *
283
+ * @typedef {ElementsGlobalAttributes
284
+ * & ElementsDataAttributes
285
+ * & ElementsAriaAttributes
286
+ * & ElementsDashedAttributes
287
+ * & ElementsEventProps
288
+ * & ElementsSpecialProps
289
+ * } ElementsProps
290
+ */
291
+
292
+ /**
293
+ * @template {ElementsProps} P
294
+ * @callback ElementsElementHelper
295
+ * @param {P | ElementsChild} [propsOrChild]
296
+ * @param {...ElementsChild} children
297
+ * @returns {ElementsVNode}
298
+ */
299
+
300
+ /**
301
+ * @typedef {typeof htmlTagNames[number]} ElementsHtmlTagName
302
+ */
303
+
304
+ /**
305
+ * @typedef {typeof svgTagNames[number]} ElementsSvgTagName
306
+ */
307
+
308
+ /**
309
+ * @typedef {ElementsHtmlTagName | ElementsSvgTagName} ElementsTagName
310
+ */
311
+
312
+ /**
313
+ * Props for `<a>`.
314
+ *
315
+ * @typedef {ElementsProps & {
316
+ * href?: string,
317
+ * target?: string,
318
+ * rel?: string,
319
+ * download?: string
320
+ * }} ElementsAnchorProps
321
+ */
322
+
323
+ /**
324
+ * Props for `<img>`.
325
+ *
326
+ * @typedef {ElementsProps & {
327
+ * src?: string,
328
+ * alt?: string,
329
+ * loading?: 'lazy' | 'eager',
330
+ * decoding?: 'sync' | 'async' | 'auto',
331
+ * referrerpolicy?: string
332
+ * }} ElementsImgProps
333
+ */
334
+
335
+ /**
336
+ * Props for `<input>`.
337
+ *
338
+ * @typedef {ElementsProps & {
339
+ * type?: string,
340
+ * name?: string,
341
+ * placeholder?: string,
342
+ * required?: boolean,
343
+ * readonly?: boolean,
344
+ * minlength?: number | string,
345
+ * maxlength?: number | string,
346
+ * min?: number | string,
347
+ * max?: number | string,
348
+ * step?: number | string,
349
+ * pattern?: string,
350
+ * accept?: string,
351
+ * autocomplete?: string
352
+ * }} ElementsInputProps
353
+ */
354
+
355
+ /**
356
+ * Props for `<form>`.
357
+ *
358
+ * @typedef {ElementsProps & {
359
+ * action?: string,
360
+ * method?: string,
361
+ * enctype?: string,
362
+ * novalidate?: boolean
363
+ * }} ElementsFormProps
364
+ */
365
+
366
+ /**
367
+ * Props for `<button>`.
368
+ *
369
+ * @typedef {ElementsProps & {
370
+ * type?: 'button' | 'submit' | 'reset' | string,
371
+ * name?: string,
372
+ * value?: string | number,
373
+ * disabled?: boolean
374
+ * }} ElementsButtonProps
375
+ */
376
+
377
+ /**
378
+ * Props for `<textarea>`.
379
+ *
380
+ * @typedef {ElementsProps & {
381
+ * name?: string,
382
+ * rows?: number | string,
383
+ * cols?: number | string,
384
+ * wrap?: string,
385
+ * placeholder?: string,
386
+ * required?: boolean,
387
+ * readonly?: boolean,
388
+ * minlength?: number | string,
389
+ * maxlength?: number | string
390
+ * }} ElementsTextareaProps
391
+ */
392
+
393
+ /**
394
+ * Props for `<select>`.
395
+ *
396
+ * @typedef {ElementsProps & {
397
+ * name?: string,
398
+ * required?: boolean,
399
+ * multiple?: boolean
400
+ * }} ElementsSelectProps
401
+ */
402
+
403
+ /**
404
+ * Props for `<option>`.
405
+ *
406
+ * @typedef {ElementsProps & {
407
+ * value?: string | number,
408
+ * selected?: boolean,
409
+ * disabled?: boolean
410
+ * }} ElementsOptionProps
411
+ */
412
+
413
+ /**
414
+ * Props for `<script>`.
415
+ *
416
+ * @typedef {ElementsProps & {
417
+ * src?: string,
418
+ * type?: string,
419
+ * async?: boolean,
420
+ * defer?: boolean,
421
+ * nomodule?: boolean,
422
+ * crossorigin?: '' | 'anonymous' | 'use-credentials',
423
+ * integrity?: string
424
+ * }} ElementsScriptProps
425
+ */
426
+
427
+ /**
428
+ * Props for `<link>`.
429
+ *
430
+ * @typedef {ElementsProps & {
431
+ * href?: string,
432
+ * rel?: string,
433
+ * as?: string,
434
+ * type?: string,
435
+ * media?: string,
436
+ * crossorigin?: '' | 'anonymous' | 'use-credentials',
437
+ * integrity?: string,
438
+ * referrerpolicy?: string,
439
+ * fetchpriority?: 'auto' | 'high' | 'low'
440
+ * }} ElementsLinkProps
441
+ */
442
+
443
+ /**
444
+ * Props for `<meta>`.
445
+ *
446
+ * @typedef {ElementsProps & {
447
+ * name?: string,
448
+ * content?: string,
449
+ * charset?: string,
450
+ * 'http-equiv'?: string
451
+ * }} ElementsMetaProps
452
+ */
453
+
454
+ /**
455
+ * Props for `<video>` and `<audio>`.
456
+ *
457
+ * @typedef {ElementsProps & {
458
+ * src?: string,
459
+ * controls?: boolean,
460
+ * controlslist?: string,
461
+ * autoplay?: boolean,
462
+ * loop?: boolean,
463
+ * muted?: boolean,
464
+ * playsinline?: boolean,
465
+ * preload?: 'none' | 'metadata' | 'auto'
466
+ * }} ElementsMediaProps
467
+ */
468
+
469
+ /**
470
+ * Props for `<source>`.
471
+ *
472
+ * @typedef {ElementsProps & {
473
+ * src?: string,
474
+ * type?: string,
475
+ * srcset?: string,
476
+ * sizes?: string,
477
+ * media?: string
478
+ * }} ElementsSourceProps
479
+ */
480
+
481
+ /**
482
+ * Props for `<svg>`.
483
+ *
484
+ * @typedef {ElementsProps & {
485
+ * viewBox?: string,
486
+ * xmlns?: string,
487
+ * fill?: string,
488
+ * stroke?: string,
489
+ * 'stroke-width'?: number | string
490
+ * }} ElementsSvgProps
491
+ */
492
+
493
+ /**
494
+ * Props for `<path>`.
495
+ *
496
+ * @typedef {ElementsProps & {
497
+ * d?: string,
498
+ * fill?: string,
499
+ * stroke?: string,
500
+ * 'stroke-width'?: number | string
501
+ * }} ElementsPathProps
502
+ */
503
+
504
+ /**
505
+ * Props for `<circle>`.
506
+ *
507
+ * @typedef {ElementsProps & {
508
+ * cx?: number | string,
509
+ * cy?: number | string,
510
+ * r?: number | string,
511
+ * fill?: string,
512
+ * stroke?: string,
513
+ * 'stroke-width'?: number | string
514
+ * }} ElementsCircleProps
515
+ */
516
+
517
+ /**
518
+ * Props for `<rect>`.
519
+ *
520
+ * @typedef {ElementsProps & {
521
+ * x?: number | string,
522
+ * y?: number | string,
523
+ * rx?: number | string,
524
+ * ry?: number | string,
525
+ * width?: number | string,
526
+ * height?: number | string,
527
+ * fill?: string,
528
+ * stroke?: string,
529
+ * 'stroke-width'?: number | string
530
+ * }} ElementsRectProps
531
+ */
532
+
533
+ /**
534
+ * Props for `<line>`.
535
+ *
536
+ * @typedef {ElementsProps & {
537
+ * x1?: number | string,
538
+ * y1?: number | string,
539
+ * x2?: number | string,
540
+ * y2?: number | string,
541
+ * stroke?: string,
542
+ * 'stroke-width'?: number | string
543
+ * }} ElementsLineProps
544
+ */
545
+
546
+ /**
547
+ * Props for `<text>` (SVG).
548
+ *
549
+ * Note: X3D also has a `text` node; it is exported as `x3dtext` in `/3d`.
550
+ *
551
+ * @typedef {ElementsProps & {
552
+ * x?: number | string,
553
+ * y?: number | string,
554
+ * dx?: number | string,
555
+ * dy?: number | string,
556
+ * rotate?: number | string,
557
+ * 'text-anchor'?: string
558
+ * }} ElementsSvgTextProps
559
+ */
560
+
561
+ /**
562
+ * Props for `<label>`.
563
+ *
564
+ * @typedef {ElementsProps & {
565
+ * for?: string,
566
+ * form?: string
567
+ * }} ElementsLabelProps
568
+ */
569
+
570
+ /**
571
+ * Props for `<dialog>`.
572
+ *
573
+ * @typedef {ElementsProps & {
574
+ * open?: boolean
575
+ * }} ElementsDialogProps
576
+ */
577
+
578
+ /**
579
+ * Props for `<details>`.
580
+ *
581
+ * @typedef {ElementsProps & {
582
+ * open?: boolean
583
+ * }} ElementsDetailsProps
584
+ */
585
+
586
+ /**
587
+ * Props for `<progress>`.
588
+ *
589
+ * @typedef {ElementsProps & {
590
+ * value?: number | string,
591
+ * max?: number | string
592
+ * }} ElementsProgressProps
593
+ */
594
+
595
+ /**
596
+ * Props for `<meter>`.
597
+ *
598
+ * @typedef {ElementsProps & {
599
+ * value?: number | string,
600
+ * min?: number | string,
601
+ * max?: number | string,
602
+ * low?: number | string,
603
+ * high?: number | string,
604
+ * optimum?: number | string
605
+ * }} ElementsMeterProps
606
+ */
607
+
608
+ /**
609
+ * Props for `<iframe>`.
610
+ *
611
+ * @typedef {ElementsProps & {
612
+ * src?: string,
613
+ * srcdoc?: string,
614
+ * name?: string,
615
+ * allow?: string,
616
+ * sandbox?: string,
617
+ * referrerpolicy?: string,
618
+ * loading?: 'lazy' | 'eager',
619
+ * allowfullscreen?: boolean
620
+ * }} ElementsIframeProps
621
+ */
622
+
623
+ /**
624
+ * Props for `<canvas>`.
625
+ *
626
+ * @typedef {ElementsProps & {
627
+ * width?: number | string,
628
+ * height?: number | string
629
+ * }} ElementsCanvasProps
630
+ */
631
+
632
+ /**
633
+ * @template {ElementsTagName} Tag
634
+ * @typedef {Tag extends 'a' ? ElementsAnchorProps
635
+ * : Tag extends 'img' ? ElementsImgProps
636
+ * : Tag extends 'input' ? ElementsInputProps
637
+ * : Tag extends 'form' ? ElementsFormProps
638
+ * : Tag extends 'button' ? ElementsButtonProps
639
+ * : Tag extends 'textarea' ? ElementsTextareaProps
640
+ * : Tag extends 'select' ? ElementsSelectProps
641
+ * : Tag extends 'option' ? ElementsOptionProps
642
+ * : Tag extends 'script' ? ElementsScriptProps
643
+ * : Tag extends 'link' ? ElementsLinkProps
644
+ * : Tag extends 'meta' ? ElementsMetaProps
645
+ * : Tag extends 'video' ? ElementsMediaProps
646
+ * : Tag extends 'audio' ? ElementsMediaProps
647
+ * : Tag extends 'source' ? ElementsSourceProps
648
+ * : Tag extends 'svg' ? ElementsSvgProps
649
+ * : Tag extends 'path' ? ElementsPathProps
650
+ * : Tag extends 'circle' ? ElementsCircleProps
651
+ * : Tag extends 'rect' ? ElementsRectProps
652
+ * : Tag extends 'line' ? ElementsLineProps
653
+ * : Tag extends 'text'
654
+ * ? ElementsSvgTextProps
655
+ * : Tag extends 'label'
656
+ * ? ElementsLabelProps
657
+ * : Tag extends 'dialog'
658
+ * ? ElementsDialogProps
659
+ * : Tag extends 'details'
660
+ * ? ElementsDetailsProps
661
+ * : Tag extends 'progress'
662
+ * ? ElementsProgressProps
663
+ * : Tag extends 'meter'
664
+ * ? ElementsMeterProps
665
+ * : Tag extends 'iframe'
666
+ * ? ElementsIframeProps
667
+ * : Tag extends 'canvas'
668
+ * ? ElementsCanvasProps
669
+ * : ElementsProps} ElementsPropsForTag
670
+ */
671
+
672
+ /**
673
+ * All tag helpers, with per-tag prop typing when known.
674
+ *
675
+ * @typedef {{ fragment: ElementsElementHelper<ElementsProps> } & {
676
+ * [K in ElementsTagName]: ElementsElementHelper<ElementsPropsForTag<K>>
677
+ * }} ElementsElementMap
678
+ */
679
+
680
+ /**
681
+ * Aliases (short names) for Types-as-Docs.
682
+ *
683
+ * These are convenience names for editor DX. The `Elements*` names remain the
684
+ * canonical internal names.
685
+ *
686
+ * @typedef {ElementsProps} ElementProps
687
+ */
688
+
689
+ /**
690
+ * @template {ElementProps} P
691
+ * @typedef {ElementsElementHelper<P>} ElementHelper
692
+ */
693
+
694
+ /**
695
+ * @typedef {ElementsElementMap} ElementMap
696
+ */