@jackens/nnn 2026.10.1 → 2026.11.17
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.
- package/README.md +304 -62
- package/nnn.d.ts +29 -17
- package/nnn.js +138 -80
- package/package.json +13 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# nnn
|
|
2
2
|
|
|
3
|
-
A collection of Jackens’ JavaScript helper utilities (version: `2026.
|
|
3
|
+
A collection of Jackens’ JavaScript helper utilities (version: `2026.11.17`).
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -18,9 +18,9 @@ npm i @jackens/nnn
|
|
|
18
18
|
|
|
19
19
|
```js
|
|
20
20
|
import {
|
|
21
|
-
CNode,
|
|
22
|
-
HArgs,
|
|
23
|
-
HArgs1,
|
|
21
|
+
type CNode,
|
|
22
|
+
type HArgs,
|
|
23
|
+
type HArgs1,
|
|
24
24
|
c,
|
|
25
25
|
h,
|
|
26
26
|
rwd,
|
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
|
|
53
53
|
```ts
|
|
54
54
|
type CNode = {
|
|
55
|
-
[attributeOrSelector: string]: string | number | CNode | undefined
|
|
55
|
+
[attributeOrSelector: string]: string | number | CNode | null | undefined
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
@@ -87,14 +87,20 @@ A minimal CSS-in-JS helper that converts a JavaScript object hierarchy into a CS
|
|
|
87
87
|
An object describing CSS rules.
|
|
88
88
|
Keys are selectors or at-rules; values are either CSS property values or nested rule objects.
|
|
89
89
|
Nested selectors are appended to the parent selector as is (e.g. `'.c'` inside `a` → `a.c`), so a leading space is needed for a descendant combinator (e.g. `' .c'` → `a .c`).
|
|
90
|
+
If a nested selector contains `&`, every `&` is replaced with the parent selector instead (e.g. `'&:hover'` → `a:hover`, `'.c &'` → `.c a`).
|
|
90
91
|
A selector key may contain comma-separated alternatives (e.g. `'.b,.c'`); when nested, each alternative is combined with each parent alternative (cartesian product), producing a comma-joined selector list (e.g. `'a.b,a.c'`).
|
|
91
|
-
|
|
92
|
+
Commas inside brackets or quotes (e.g. `':is(.b,.c)'`, `'[title="b,c"]'`) do not separate alternatives.
|
|
93
|
+
Whitespace after a separating comma belongs to the next alternative, so like a leading space of the first one it makes a descendant combinator when nested (e.g. `' .b, .c'` → `a .b,a .c`, `'.b, .c'` → `a.b,a .c`); leading whitespace of a resulting selector is ignored (e.g. `'h1, h2'` → `h1,h2`, `'.b, &:hover'` → `a.b,a:hover`).
|
|
94
|
+
At-rules (e.g. `'@media …'`) wrap their nested rules; an at-rule nested in a selector wraps that selector (e.g. `{ a: { '@media(…)': { color: 'red' } } }` → `@media(…){a{color:red}}`).
|
|
95
|
+
Inside a top-level `:root` rule nested keys are concatenated into property names (e.g. `{ ':root': { '--': { bg: 'red' } } }` → `:root{--bg:red}`).
|
|
96
|
+
Properties without a selector (top-level or directly in a top-level at-rule, e.g. `@font-face`) are emitted as bare declarations.
|
|
92
97
|
`null`/`undefined` values are skipped.
|
|
93
98
|
|
|
94
99
|
#### splitter
|
|
95
100
|
|
|
96
101
|
A delimiter used to create unique keys (default: `'$$'`).
|
|
97
102
|
The substring from `splitter` to the end of a key is ignored (e.g. `src$$1` → `src`).
|
|
103
|
+
An empty `splitter` disables this feature.
|
|
98
104
|
|
|
99
105
|
#### Returns
|
|
100
106
|
|
|
@@ -169,7 +175,7 @@ const actual = c({
|
|
|
169
175
|
'@font-face$$1': {
|
|
170
176
|
'font-family': 'Jackens',
|
|
171
177
|
src$$1: 'url(otf/jackens.otf)',
|
|
172
|
-
src$$2: "url(otf/jackens.otf) format('opentype'),
|
|
178
|
+
src$$2: "url(otf/jackens.otf) format('opentype'),url(svg/jackens.svg) format('svg')",
|
|
173
179
|
'font-weight': 'normal',
|
|
174
180
|
'font-style': 'normal',
|
|
175
181
|
},
|
|
@@ -405,13 +411,156 @@ const expected = `
|
|
|
405
411
|
expect(actual).toBe(expected)
|
|
406
412
|
```
|
|
407
413
|
|
|
414
|
+
`&` is replaced with the parent selector:
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
const actual = c({
|
|
418
|
+
a: {
|
|
419
|
+
color: 'red',
|
|
420
|
+
'&:hover': { color: 'blue' },
|
|
421
|
+
'.dark &': { color: 'white' },
|
|
422
|
+
'.b,& .c': { margin: 1 },
|
|
423
|
+
},
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
const expected = `
|
|
427
|
+
a{
|
|
428
|
+
color:red
|
|
429
|
+
}
|
|
430
|
+
a:hover{
|
|
431
|
+
color:blue
|
|
432
|
+
}
|
|
433
|
+
.dark a{
|
|
434
|
+
color:white
|
|
435
|
+
}
|
|
436
|
+
a.b,a .c{
|
|
437
|
+
margin:1
|
|
438
|
+
}
|
|
439
|
+
`.replace(/\n\s*/g, '')
|
|
440
|
+
|
|
441
|
+
expect(actual).toBe(expected)
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Commas inside brackets or quotes do not split selectors:
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
const actual = c({
|
|
448
|
+
':is(.a,.b)': {
|
|
449
|
+
'.c,.d': { margin: 1 },
|
|
450
|
+
'[title="x,y"]': { margin: 2 },
|
|
451
|
+
},
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
const expected = `
|
|
455
|
+
:is(.a,.b).c,:is(.a,.b).d{
|
|
456
|
+
margin:1
|
|
457
|
+
}
|
|
458
|
+
:is(.a,.b)[title="x,y"]{
|
|
459
|
+
margin:2
|
|
460
|
+
}
|
|
461
|
+
`.replace(/\n\s*/g, '')
|
|
462
|
+
|
|
463
|
+
expect(actual).toBe(expected)
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Whitespace after a comma is kept, but leading whitespace of a resulting selector is ignored:
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
const actual = c({
|
|
470
|
+
'h1, h2': {
|
|
471
|
+
margin: 0,
|
|
472
|
+
' .a, .b': { margin: 1 },
|
|
473
|
+
'.c, .d , &:hover': { margin: 2 },
|
|
474
|
+
},
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
const expected = `
|
|
478
|
+
h1,h2{
|
|
479
|
+
margin:0
|
|
480
|
+
}
|
|
481
|
+
h1 .a,h1 .b,h2 .a,h2 .b{
|
|
482
|
+
margin:1
|
|
483
|
+
}
|
|
484
|
+
h1.c,h1 .d,h1:hover,h2.c,h2 .d,h2:hover{
|
|
485
|
+
margin:2
|
|
486
|
+
}
|
|
487
|
+
`.replace(/\n\s*/g, '')
|
|
488
|
+
|
|
489
|
+
expect(actual).toBe(expected)
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
At-rules nested in selectors wrap these selectors:
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
const actual = c({
|
|
496
|
+
a: {
|
|
497
|
+
color: 'red',
|
|
498
|
+
'@media(min-width:600px)': {
|
|
499
|
+
color: 'blue',
|
|
500
|
+
'.b': { margin: 1 },
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
const expected = `
|
|
506
|
+
a{
|
|
507
|
+
color:red
|
|
508
|
+
}
|
|
509
|
+
@media(min-width:600px){
|
|
510
|
+
a{
|
|
511
|
+
color:blue
|
|
512
|
+
}
|
|
513
|
+
a.b{
|
|
514
|
+
margin:1
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
`.replace(/\n\s*/g, '')
|
|
518
|
+
|
|
519
|
+
expect(actual).toBe(expected)
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
Declarations without a selector are separated from subsequent rules:
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
expect(c({ color: 'red', margin: 0 })).toBe('color:red;margin:0')
|
|
526
|
+
|
|
527
|
+
expect(c({ color: 'red', a: { margin: 0 }, padding: 0 })).toBe('color:red;a{margin:0}padding:0')
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
`null` and `undefined` values are skipped:
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
const actual = c({
|
|
534
|
+
a: {
|
|
535
|
+
color: 'red',
|
|
536
|
+
margin: null,
|
|
537
|
+
padding: undefined,
|
|
538
|
+
'.b': null,
|
|
539
|
+
},
|
|
540
|
+
':root': {
|
|
541
|
+
'--': { bg: null, fg: 'black' },
|
|
542
|
+
},
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
expect(actual).toBe('a{color:red}:root{--fg:black}')
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
Custom splitter (an empty one disables splitting):
|
|
549
|
+
|
|
550
|
+
```ts
|
|
551
|
+
expect(c({ a: { 'src#1': 'url(a.otf)', 'src#2': 'url(a.woff)' } }, '#')).toBe('a{src:url(a.otf);src:url(a.woff)}')
|
|
552
|
+
|
|
553
|
+
expect(c({ a: { b$$c: 1 } }, '')).toBe('a{b$$c:1}')
|
|
554
|
+
```
|
|
555
|
+
|
|
408
556
|
### h
|
|
409
557
|
|
|
410
558
|
```ts
|
|
411
559
|
const h: {
|
|
412
560
|
<T extends keyof HTMLElementTagNameMap>(tag: T, ...args: HArgs1[]): HTMLElementTagNameMap[T]
|
|
413
561
|
<N extends Node>(node: N, ...args: HArgs1[]): N
|
|
414
|
-
(
|
|
562
|
+
(tag: string, ...args: HArgs1[]): HTMLElement
|
|
563
|
+
(node: Node, ...args: HArgs1[]): Node
|
|
415
564
|
}
|
|
416
565
|
```
|
|
417
566
|
|
|
@@ -427,9 +576,9 @@ If a `Node`, that node is modified in place.
|
|
|
427
576
|
Additional arguments processed as follows:
|
|
428
577
|
|
|
429
578
|
- `Object`: maps attributes/properties.
|
|
430
|
-
- Keys starting with `$` set element properties (without the `$` prefix); if the value is a plain object, it is shallow-merged into the existing property instead of replacing it (useful e.g. for `$style`).
|
|
431
|
-
- Other keys set attributes: a key prefixed with `xlink:` sets the attribute via `setAttributeNS` in the
|
|
432
|
-
A value of `true` sets an empty attribute (e.g. `checked=""`), a value of `false` removes the attribute.
|
|
579
|
+
- Keys starting with `$` set element properties (without the `$` prefix); if the value is a plain object (created by `{…}` or `Object.create(null)`), it is shallow-merged into the existing property instead of replacing it (useful e.g. for `$style`), other values (including arrays and class instances) replace the property.
|
|
580
|
+
- Other keys set attributes: a key prefixed with `xlink:`, `xml:` or `xmlns:` sets the attribute via `setAttributeNS` in the corresponding namespace; any other key sets it via `setAttribute`.
|
|
581
|
+
A value of `true` sets an empty attribute (e.g. `checked=""`), a value of `false` removes the attribute, a value of `null`/`undefined` leaves the attribute unchanged.
|
|
433
582
|
- `null`/`undefined`: ignored.
|
|
434
583
|
- `Node`: appended as a child.
|
|
435
584
|
- `string`/`number`: converted to a `Text` node and appended.
|
|
@@ -453,13 +602,11 @@ const i = h('i', 'text')
|
|
|
453
602
|
h(b, i)
|
|
454
603
|
|
|
455
604
|
expect(i.outerHTML).toBe('<i>text</i>')
|
|
456
|
-
|
|
457
605
|
expect(b.outerHTML).toBe('<b><i>text</i></b>')
|
|
458
606
|
|
|
459
607
|
h(i, { $className: 'some class' })
|
|
460
608
|
|
|
461
609
|
expect(i.outerHTML).toBe('<i class="some class">text</i>')
|
|
462
|
-
|
|
463
610
|
expect(b.outerHTML).toBe('<b><i class="some class">text</i></b>')
|
|
464
611
|
```
|
|
465
612
|
|
|
@@ -467,11 +614,9 @@ Text content from strings, numbers and `$innerText`:
|
|
|
467
614
|
|
|
468
615
|
```ts
|
|
469
616
|
expect(h('span', 'text').outerHTML).toBe('<span>text</span>')
|
|
470
|
-
|
|
471
617
|
expect(h('span', { $innerText: 'text' }).outerHTML).toBe('<span>text</span>')
|
|
472
618
|
|
|
473
619
|
expect(h('span', '42').outerHTML).toBe('<span>42</span>')
|
|
474
|
-
|
|
475
620
|
expect(h('span', 42).outerHTML).toBe('<span>42</span>')
|
|
476
621
|
```
|
|
477
622
|
|
|
@@ -479,9 +624,7 @@ expect(h('span', 42).outerHTML).toBe('<span>42</span>')
|
|
|
479
624
|
|
|
480
625
|
```ts
|
|
481
626
|
expect(h('div', { style: 'margin:0;padding:0' }).outerHTML).toBe('<div style="margin:0;padding:0"></div>')
|
|
482
|
-
|
|
483
627
|
expect(h('div', { $style: 'margin:0;padding:0' }).outerHTML).toBe('<div style="margin: 0px; padding: 0px;"></div>')
|
|
484
|
-
|
|
485
628
|
expect(h('div', { $style: { margin: 0, padding: 0 } }).outerHTML).toBe(
|
|
486
629
|
'<div style="margin: 0px; padding: 0px;"></div>'
|
|
487
630
|
)
|
|
@@ -517,14 +660,13 @@ expect(checkbox1.checked).toBe(true)
|
|
|
517
660
|
expect(checkbox2.checked).toBe(true)
|
|
518
661
|
|
|
519
662
|
expect(checkbox1.outerHTML).toBe('<input type="checkbox" checked="">')
|
|
520
|
-
|
|
521
663
|
expect(checkbox2.outerHTML).toBe('<input type="checkbox">')
|
|
522
664
|
```
|
|
523
665
|
|
|
524
666
|
Object values of `$`-prefixed properties are merged:
|
|
525
667
|
|
|
526
668
|
```ts
|
|
527
|
-
const div = h('div')
|
|
669
|
+
const div = h('div') as any
|
|
528
670
|
|
|
529
671
|
expect(div.key).toBeUndefined()
|
|
530
672
|
|
|
@@ -555,6 +697,46 @@ const elemWithNested = h('div', ['span', 'hello'], ['b', 'world'])
|
|
|
555
697
|
expect(elemWithNested.outerHTML).toBe('<div><span>hello</span><b>world</b></div>')
|
|
556
698
|
```
|
|
557
699
|
|
|
700
|
+
`false` removes an attribute, `null` and `undefined` leave it unchanged:
|
|
701
|
+
|
|
702
|
+
```ts
|
|
703
|
+
const input = h('input', { type: 'checkbox', checked: true, title: 'title' })
|
|
704
|
+
|
|
705
|
+
h(input, { checked: false, title: null, type: undefined })
|
|
706
|
+
|
|
707
|
+
expect(input.outerHTML).toBe('<input type="checkbox" title="title">')
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
Only plain objects are merged into `$`-prefixed properties:
|
|
711
|
+
|
|
712
|
+
```ts
|
|
713
|
+
const date = new Date(0)
|
|
714
|
+
const div = h('div', { $date: date, $list: [1, 2] }) as any
|
|
715
|
+
|
|
716
|
+
expect(div.date).toBe(date)
|
|
717
|
+
|
|
718
|
+
h(div, { $list: [3] })
|
|
719
|
+
|
|
720
|
+
expect(div.list).toEqual([3])
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
Namespaced attributes:
|
|
724
|
+
|
|
725
|
+
```ts
|
|
726
|
+
const div = h('div', { 'xml:lang': 'pl', 'ng:x': 1 })
|
|
727
|
+
|
|
728
|
+
expect(div.getAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang')).toBe('pl')
|
|
729
|
+
expect(div.getAttribute('ng:x')).toBe('1')
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
Return types:
|
|
733
|
+
|
|
734
|
+
```ts
|
|
735
|
+
expectTypeOf(h('input')).toEqualTypeOf<HTMLInputElement>()
|
|
736
|
+
expectTypeOf(h('my-element')).toEqualTypeOf<HTMLElement>()
|
|
737
|
+
expectTypeOf(h(document.createTextNode(''))).toEqualTypeOf<Text>()
|
|
738
|
+
```
|
|
739
|
+
|
|
558
740
|
### rwd
|
|
559
741
|
|
|
560
742
|
```ts
|
|
@@ -563,7 +745,7 @@ const rwd: (
|
|
|
563
745
|
selector: string,
|
|
564
746
|
cellWidthPx: number,
|
|
565
747
|
cellHeightPx: number,
|
|
566
|
-
...specs: [number, number
|
|
748
|
+
...specs: [columns: number, width?: number, height?: number][]
|
|
567
749
|
) => void
|
|
568
750
|
```
|
|
569
751
|
|
|
@@ -580,20 +762,24 @@ The CSS selector for the grid item.
|
|
|
580
762
|
|
|
581
763
|
#### cellWidthPx
|
|
582
764
|
|
|
583
|
-
The base cell width in pixels.
|
|
765
|
+
The base cell width in pixels (a positive number).
|
|
584
766
|
|
|
585
767
|
#### cellHeightPx
|
|
586
768
|
|
|
587
|
-
The base cell height in pixels.
|
|
769
|
+
The base cell height in pixels (a positive number).
|
|
588
770
|
|
|
589
771
|
#### specs
|
|
590
772
|
|
|
591
|
-
An array of breakpoint specifications, each a tuple of:
|
|
773
|
+
An array of breakpoint specifications, each a tuple of positive integers:
|
|
592
774
|
|
|
593
|
-
- `
|
|
594
|
-
- `width` (optional, default `1`): number of horizontal cells the element spans.
|
|
775
|
+
- `columns`: number of cells per row; the breakpoint is `min-width: cellWidthPx * columns` (`1` modifies the base rule).
|
|
776
|
+
- `width` (optional, default `1`, at most `columns`): number of horizontal cells the element spans.
|
|
595
777
|
- `height` (optional, default `1`): number of vertical cells the element spans.
|
|
596
778
|
|
|
779
|
+
#### Throws
|
|
780
|
+
|
|
781
|
+
A `RangeError` if any of the numeric arguments is out of range.
|
|
782
|
+
|
|
597
783
|
#### Usage Examples
|
|
598
784
|
|
|
599
785
|
Responsive grid cell widths and heights with media queries:
|
|
@@ -643,13 +829,59 @@ expect(style).toEqual({
|
|
|
643
829
|
})
|
|
644
830
|
```
|
|
645
831
|
|
|
832
|
+
Widths that divide evenly do not use `calc`; existing at-rules are extended:
|
|
833
|
+
|
|
834
|
+
```ts
|
|
835
|
+
const style: CNode = {
|
|
836
|
+
'@media(min-width:400px)': {
|
|
837
|
+
'.r2': { margin: 0 },
|
|
838
|
+
},
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
rwd(style, '.r2', 100, 40, [4, 2], [2, 2, 3])
|
|
842
|
+
|
|
843
|
+
expect(style).toEqual({
|
|
844
|
+
'@media(min-width:400px)': {
|
|
845
|
+
'.r2': {
|
|
846
|
+
margin: 0,
|
|
847
|
+
width: '50%',
|
|
848
|
+
height: '40px',
|
|
849
|
+
},
|
|
850
|
+
},
|
|
851
|
+
'.r2': {
|
|
852
|
+
'box-sizing': 'border-box',
|
|
853
|
+
display: 'block',
|
|
854
|
+
float: 'left',
|
|
855
|
+
width: '100%',
|
|
856
|
+
height: '40px',
|
|
857
|
+
},
|
|
858
|
+
'@media(min-width:200px)': {
|
|
859
|
+
'.r2': {
|
|
860
|
+
width: '100%',
|
|
861
|
+
height: '120px',
|
|
862
|
+
},
|
|
863
|
+
},
|
|
864
|
+
})
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
Invalid arguments throw a `RangeError`:
|
|
868
|
+
|
|
869
|
+
```ts
|
|
870
|
+
expect(() => rwd({}, '.x', 0, 50)).toThrow(RangeError)
|
|
871
|
+
expect(() => rwd({}, '.x', 100, Infinity)).toThrow(RangeError)
|
|
872
|
+
expect(() => rwd({}, '.x', 100, 50, [0])).toThrow(RangeError)
|
|
873
|
+
expect(() => rwd({}, '.x', 100, 50, [2, 3])).toThrow(RangeError)
|
|
874
|
+
expect(() => rwd({}, '.x', 100, 50, [2, 1, 1.5])).toThrow(RangeError)
|
|
875
|
+
```
|
|
876
|
+
|
|
646
877
|
### s
|
|
647
878
|
|
|
648
879
|
```ts
|
|
649
880
|
const s: {
|
|
650
881
|
<T extends keyof SVGElementTagNameMap>(tag: T, ...args: HArgs1[]): SVGElementTagNameMap[T]
|
|
651
882
|
<N extends Node>(node: N, ...args: HArgs1[]): N
|
|
652
|
-
(
|
|
883
|
+
(tag: string, ...args: HArgs1[]): SVGElement
|
|
884
|
+
(node: Node, ...args: HArgs1[]): Node
|
|
653
885
|
}
|
|
654
886
|
```
|
|
655
887
|
|
|
@@ -665,9 +897,9 @@ If a `Node`, that node is modified in place.
|
|
|
665
897
|
Additional arguments processed as follows:
|
|
666
898
|
|
|
667
899
|
- `Object`: maps attributes/properties.
|
|
668
|
-
- Keys starting with `$` set element properties (without the `$` prefix); if the value is a plain object, it is shallow-merged into the existing property instead of replacing it.
|
|
669
|
-
- Other keys set attributes: a key prefixed with `xlink:` sets the attribute via `setAttributeNS` in the
|
|
670
|
-
A value of `true` sets an empty attribute
|
|
900
|
+
- Keys starting with `$` set element properties (without the `$` prefix); if the value is a plain object (created by `{…}` or `Object.create(null)`), it is shallow-merged into the existing property instead of replacing it, other values (including arrays and class instances) replace the property.
|
|
901
|
+
- Other keys set attributes: a key prefixed with `xlink:`, `xml:` or `xmlns:` sets the attribute via `setAttributeNS` in the corresponding namespace (e.g. `xml:lang`); any other key sets it via `setAttribute`.
|
|
902
|
+
A value of `true` sets an empty attribute, a value of `false` removes the attribute, a value of `null`/`undefined` leaves the attribute unchanged.
|
|
671
903
|
- `null`/`undefined`: ignored.
|
|
672
904
|
- `Node`: appended as a child.
|
|
673
905
|
- `string`/`number`: converted to a `Text` node and appended.
|
|
@@ -679,6 +911,16 @@ The created `SVGElement` or the modified node.
|
|
|
679
911
|
|
|
680
912
|
#### Usage Examples
|
|
681
913
|
|
|
914
|
+
Elements (including nested ones) are created in the SVG namespace:
|
|
915
|
+
|
|
916
|
+
```ts
|
|
917
|
+
const svg = s('svg', ['g', ['circle', { r: 1 }]])
|
|
918
|
+
|
|
919
|
+
expect(svg.namespaceURI).toBe('http://www.w3.org/2000/svg')
|
|
920
|
+
expect(svg.querySelector('circle')?.namespaceURI).toBe('http://www.w3.org/2000/svg')
|
|
921
|
+
expect(svg.outerHTML).toBe('<svg><g><circle r="1"></circle></g></svg>')
|
|
922
|
+
```
|
|
923
|
+
|
|
682
924
|
Namespaced `xlink:` attribute set to `true` gets an empty value:
|
|
683
925
|
|
|
684
926
|
```ts
|
|
@@ -687,10 +929,12 @@ const svg = s('svg', { 'xlink:href': true })
|
|
|
687
929
|
expect(svg.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe('')
|
|
688
930
|
```
|
|
689
931
|
|
|
690
|
-
Namespaced `xlink:` attribute set to `false` is
|
|
932
|
+
Namespaced `xlink:` attribute set to `false` is removed:
|
|
691
933
|
|
|
692
934
|
```ts
|
|
693
|
-
const svg = s('svg', { 'xlink:href':
|
|
935
|
+
const svg = s('svg', { 'xlink:href': '#id' })
|
|
936
|
+
|
|
937
|
+
s(svg, { 'xlink:href': false })
|
|
694
938
|
|
|
695
939
|
expect(svg.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBeNull()
|
|
696
940
|
```
|
|
@@ -711,6 +955,22 @@ const svg = s('svg', { 'xlink:title': 42 })
|
|
|
711
955
|
expect(svg.getAttributeNS('http://www.w3.org/1999/xlink', 'title')).toBe('42')
|
|
712
956
|
```
|
|
713
957
|
|
|
958
|
+
Namespaced `xml:` and `xmlns:` attributes:
|
|
959
|
+
|
|
960
|
+
```ts
|
|
961
|
+
const svg = s('svg', { 'xml:lang': 'pl', 'xmlns:xlink': 'http://www.w3.org/1999/xlink' })
|
|
962
|
+
|
|
963
|
+
expect(svg.getAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang')).toBe('pl')
|
|
964
|
+
expect(svg.getAttributeNS('http://www.w3.org/2000/xmlns/', 'xlink')).toBe('http://www.w3.org/1999/xlink')
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
Return types:
|
|
968
|
+
|
|
969
|
+
```ts
|
|
970
|
+
expectTypeOf(s('circle')).toEqualTypeOf<SVGCircleElement>()
|
|
971
|
+
expectTypeOf(s('my-element')).toEqualTypeOf<SVGElement>()
|
|
972
|
+
```
|
|
973
|
+
|
|
714
974
|
### svgUse
|
|
715
975
|
|
|
716
976
|
```ts
|
|
@@ -722,7 +982,7 @@ Shorthand for creating an `<svg>` element with a `<use>` child referencing a sym
|
|
|
722
982
|
Equivalent to:
|
|
723
983
|
|
|
724
984
|
```ts
|
|
725
|
-
s('svg', ['use', {
|
|
985
|
+
s('svg', ['use', { href: '#' + id }], ...args)
|
|
726
986
|
```
|
|
727
987
|
|
|
728
988
|
#### id
|
|
@@ -739,46 +999,28 @@ An `SVGSVGElement` containing a `<use>` element.
|
|
|
739
999
|
|
|
740
1000
|
#### Usage Examples
|
|
741
1001
|
|
|
742
|
-
SVG `use` element referencing a symbol
|
|
1002
|
+
SVG `use` element referencing a symbol:
|
|
743
1003
|
|
|
744
1004
|
```ts
|
|
745
1005
|
const svg = svgUse('icon-home')
|
|
746
1006
|
|
|
747
|
-
expect(svg.
|
|
748
|
-
|
|
749
|
-
const useElement = svg.children[0]
|
|
750
|
-
|
|
751
|
-
expect(useElement.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe('#icon-home')
|
|
752
|
-
|
|
753
|
-
const svgWithViewBox = svgUse('icon-star', { viewBox: '0 0 24 24' })
|
|
754
|
-
|
|
755
|
-
expect(svgWithViewBox.getAttribute('viewBox')).toBe('0 0 24 24')
|
|
756
|
-
|
|
757
|
-
const useViewBox = svgWithViewBox.children[0]
|
|
758
|
-
|
|
759
|
-
expect(useViewBox.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe('#icon-star')
|
|
760
|
-
|
|
761
|
-
const svgWithClass = svgUse('icon-menu', { class: 'icon-btn' })
|
|
762
|
-
|
|
763
|
-
expect(svgWithClass.getAttribute('class')).toBe('icon-btn')
|
|
764
|
-
|
|
765
|
-
expect(svgWithClass.children.length).toBe(1)
|
|
766
|
-
|
|
767
|
-
const useClass = svgWithClass.children[0]
|
|
768
|
-
|
|
769
|
-
expect(useClass.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe('#icon-menu')
|
|
1007
|
+
expect(svg.outerHTML).toBe('<svg><use href="#icon-home"></use></svg>')
|
|
1008
|
+
```
|
|
770
1009
|
|
|
771
|
-
|
|
1010
|
+
Additional attributes are set on the outer `svg` element:
|
|
772
1011
|
|
|
773
|
-
|
|
1012
|
+
```ts
|
|
1013
|
+
const svg = svgUse('icon-settings', { width: 24, height: 24, class: 'icon' })
|
|
774
1014
|
|
|
775
|
-
expect(
|
|
1015
|
+
expect(svg.outerHTML).toBe('<svg width="24" height="24" class="icon"><use href="#icon-settings"></use></svg>')
|
|
1016
|
+
```
|
|
776
1017
|
|
|
777
|
-
|
|
1018
|
+
Additional children are appended to the outer `svg` element:
|
|
778
1019
|
|
|
779
|
-
|
|
1020
|
+
```ts
|
|
1021
|
+
const svg = svgUse('icon-star', ['title', 'Star'])
|
|
780
1022
|
|
|
781
|
-
expect(
|
|
1023
|
+
expect(svg.outerHTML).toBe('<svg><use href="#icon-star"></use><title>Star</title></svg>')
|
|
782
1024
|
```
|
|
783
1025
|
|
|
784
1026
|
## License
|
package/nnn.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Represents a CSS rule node for the {@link c} helper. Keys are CSS properties or nested selectors. */
|
|
2
2
|
export type CNode = {
|
|
3
|
-
[attributeOrSelector: string]: string | number | CNode | undefined
|
|
3
|
+
[attributeOrSelector: string]: string | number | CNode | null | undefined
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
6
|
* A minimal CSS-in-JS helper that converts a JavaScript object hierarchy into a CSS string.
|
|
@@ -10,14 +10,20 @@ export type CNode = {
|
|
|
10
10
|
* An object describing CSS rules.
|
|
11
11
|
* Keys are selectors or at-rules; values are either CSS property values or nested rule objects.
|
|
12
12
|
* Nested selectors are appended to the parent selector as is (e.g. `'.c'` inside `a` → `a.c`), so a leading space is needed for a descendant combinator (e.g. `' .c'` → `a .c`).
|
|
13
|
+
* If a nested selector contains `&`, every `&` is replaced with the parent selector instead (e.g. `'&:hover'` → `a:hover`, `'.c &'` → `.c a`).
|
|
13
14
|
* A selector key may contain comma-separated alternatives (e.g. `'.b,.c'`); when nested, each alternative is combined with each parent alternative (cartesian product), producing a comma-joined selector list (e.g. `'a.b,a.c'`).
|
|
14
|
-
*
|
|
15
|
+
* Commas inside brackets or quotes (e.g. `':is(.b,.c)'`, `'[title="b,c"]'`) do not separate alternatives.
|
|
16
|
+
* Whitespace after a separating comma belongs to the next alternative, so like a leading space of the first one it makes a descendant combinator when nested (e.g. `' .b, .c'` → `a .b,a .c`, `'.b, .c'` → `a.b,a .c`); leading whitespace of a resulting selector is ignored (e.g. `'h1, h2'` → `h1,h2`, `'.b, &:hover'` → `a.b,a:hover`).
|
|
17
|
+
* At-rules (e.g. `'@media …'`) wrap their nested rules; an at-rule nested in a selector wraps that selector (e.g. `{ a: { '@media(…)': { color: 'red' } } }` → `@media(…){a{color:red}}`).
|
|
18
|
+
* Inside a top-level `:root` rule nested keys are concatenated into property names (e.g. `{ ':root': { '--': { bg: 'red' } } }` → `:root{--bg:red}`).
|
|
19
|
+
* Properties without a selector (top-level or directly in a top-level at-rule, e.g. `@font-face`) are emitted as bare declarations.
|
|
15
20
|
* `null`/`undefined` values are skipped.
|
|
16
21
|
*
|
|
17
22
|
* @param splitter
|
|
18
23
|
*
|
|
19
24
|
* A delimiter used to create unique keys (default: `'$$'`).
|
|
20
25
|
* The substring from `splitter` to the end of a key is ignored (e.g. `src$$1` → `src`).
|
|
26
|
+
* An empty `splitter` disables this feature.
|
|
21
27
|
*
|
|
22
28
|
* @returns
|
|
23
29
|
*
|
|
@@ -41,9 +47,9 @@ export type HArgs = [string | Node, ...HArgs1[]]
|
|
|
41
47
|
* Additional arguments processed as follows:
|
|
42
48
|
*
|
|
43
49
|
* - `Object`: maps attributes/properties.
|
|
44
|
-
* - Keys starting with `$` set element properties (without the `$` prefix); if the value is a plain object, it is shallow-merged into the existing property instead of replacing it (useful e.g. for `$style`).
|
|
45
|
-
* - Other keys set attributes: a key prefixed with `xlink:` sets the attribute via `setAttributeNS` in the
|
|
46
|
-
* A value of `true` sets an empty attribute (e.g. `checked=""`), a value of `false` removes the attribute.
|
|
50
|
+
* - Keys starting with `$` set element properties (without the `$` prefix); if the value is a plain object (created by `{…}` or `Object.create(null)`), it is shallow-merged into the existing property instead of replacing it (useful e.g. for `$style`), other values (including arrays and class instances) replace the property.
|
|
51
|
+
* - Other keys set attributes: a key prefixed with `xlink:`, `xml:` or `xmlns:` sets the attribute via `setAttributeNS` in the corresponding namespace; any other key sets it via `setAttribute`.
|
|
52
|
+
* A value of `true` sets an empty attribute (e.g. `checked=""`), a value of `false` removes the attribute, a value of `null`/`undefined` leaves the attribute unchanged.
|
|
47
53
|
* - `null`/`undefined`: ignored.
|
|
48
54
|
* - `Node`: appended as a child.
|
|
49
55
|
* - `string`/`number`: converted to a `Text` node and appended.
|
|
@@ -56,7 +62,8 @@ export type HArgs = [string | Node, ...HArgs1[]]
|
|
|
56
62
|
export declare const h: {
|
|
57
63
|
<T extends keyof HTMLElementTagNameMap>(tag: T, ...args: HArgs1[]): HTMLElementTagNameMap[T]
|
|
58
64
|
<N extends Node>(node: N, ...args: HArgs1[]): N
|
|
59
|
-
(
|
|
65
|
+
(tag: string, ...args: HArgs1[]): HTMLElement
|
|
66
|
+
(node: Node, ...args: HArgs1[]): Node
|
|
60
67
|
}
|
|
61
68
|
/**
|
|
62
69
|
* A lightweight _HyperScript_-style helper for creating and modifying `SVGElement`s (see also {@link h}).
|
|
@@ -71,9 +78,9 @@ export declare const h: {
|
|
|
71
78
|
* Additional arguments processed as follows:
|
|
72
79
|
*
|
|
73
80
|
* - `Object`: maps attributes/properties.
|
|
74
|
-
* - Keys starting with `$` set element properties (without the `$` prefix); if the value is a plain object, it is shallow-merged into the existing property instead of replacing it.
|
|
75
|
-
* - Other keys set attributes: a key prefixed with `xlink:` sets the attribute via `setAttributeNS` in the
|
|
76
|
-
* A value of `true` sets an empty attribute
|
|
81
|
+
* - Keys starting with `$` set element properties (without the `$` prefix); if the value is a plain object (created by `{…}` or `Object.create(null)`), it is shallow-merged into the existing property instead of replacing it, other values (including arrays and class instances) replace the property.
|
|
82
|
+
* - Other keys set attributes: a key prefixed with `xlink:`, `xml:` or `xmlns:` sets the attribute via `setAttributeNS` in the corresponding namespace (e.g. `xml:lang`); any other key sets it via `setAttribute`.
|
|
83
|
+
* A value of `true` sets an empty attribute, a value of `false` removes the attribute, a value of `null`/`undefined` leaves the attribute unchanged.
|
|
77
84
|
* - `null`/`undefined`: ignored.
|
|
78
85
|
* - `Node`: appended as a child.
|
|
79
86
|
* - `string`/`number`: converted to a `Text` node and appended.
|
|
@@ -86,7 +93,8 @@ export declare const h: {
|
|
|
86
93
|
export declare const s: {
|
|
87
94
|
<T extends keyof SVGElementTagNameMap>(tag: T, ...args: HArgs1[]): SVGElementTagNameMap[T]
|
|
88
95
|
<N extends Node>(node: N, ...args: HArgs1[]): N
|
|
89
|
-
(
|
|
96
|
+
(tag: string, ...args: HArgs1[]): SVGElement
|
|
97
|
+
(node: Node, ...args: HArgs1[]): Node
|
|
90
98
|
}
|
|
91
99
|
/**
|
|
92
100
|
* Shorthand for creating an `<svg>` element with a `<use>` child referencing a symbol (e.g. an icon) by ID.
|
|
@@ -94,7 +102,7 @@ export declare const s: {
|
|
|
94
102
|
* Equivalent to:
|
|
95
103
|
*
|
|
96
104
|
* ```ts
|
|
97
|
-
* s('svg', ['use', {
|
|
105
|
+
* s('svg', ['use', { href: '#' + id }], ...args)
|
|
98
106
|
* ```
|
|
99
107
|
*
|
|
100
108
|
* @param id
|
|
@@ -124,24 +132,28 @@ export declare const svgUse: (id: string, ...args: HArgs1[]) => SVGSVGElement
|
|
|
124
132
|
*
|
|
125
133
|
* @param cellWidthPx
|
|
126
134
|
*
|
|
127
|
-
* The base cell width in pixels.
|
|
135
|
+
* The base cell width in pixels (a positive number).
|
|
128
136
|
*
|
|
129
137
|
* @param cellHeightPx
|
|
130
138
|
*
|
|
131
|
-
* The base cell height in pixels.
|
|
139
|
+
* The base cell height in pixels (a positive number).
|
|
132
140
|
*
|
|
133
141
|
* @param specs
|
|
134
142
|
*
|
|
135
|
-
* An array of breakpoint specifications, each a tuple of:
|
|
143
|
+
* An array of breakpoint specifications, each a tuple of positive integers:
|
|
136
144
|
*
|
|
137
|
-
* - `
|
|
138
|
-
* - `width` (optional, default `1`): number of horizontal cells the element spans.
|
|
145
|
+
* - `columns`: number of cells per row; the breakpoint is `min-width: cellWidthPx * columns` (`1` modifies the base rule).
|
|
146
|
+
* - `width` (optional, default `1`, at most `columns`): number of horizontal cells the element spans.
|
|
139
147
|
* - `height` (optional, default `1`): number of vertical cells the element spans.
|
|
148
|
+
*
|
|
149
|
+
* @throws
|
|
150
|
+
*
|
|
151
|
+
* A `RangeError` if any of the numeric arguments is out of range.
|
|
140
152
|
*/
|
|
141
153
|
export declare const rwd: (
|
|
142
154
|
root: CNode,
|
|
143
155
|
selector: string,
|
|
144
156
|
cellWidthPx: number,
|
|
145
157
|
cellHeightPx: number,
|
|
146
|
-
...specs: [number, number
|
|
158
|
+
...specs: [columns: number, width?: number, height?: number][]
|
|
147
159
|
) => void
|
package/nnn.js
CHANGED
|
@@ -1,114 +1,174 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/c.ts
|
|
2
2
|
var DEFAULT_SPLITTER = "$$";
|
|
3
|
-
var
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
var OPENING_BRACKETS = "([";
|
|
4
|
+
var CLOSING_BRACKETS = ")]";
|
|
5
|
+
var scan = (selector, onChar) => {
|
|
6
|
+
let depth = 0;
|
|
7
|
+
let quote = "";
|
|
8
|
+
for (const char of selector) {
|
|
9
|
+
if (quote !== "") {
|
|
10
|
+
if (char === quote) {
|
|
11
|
+
quote = "";
|
|
12
|
+
}
|
|
13
|
+
} else if (char === '"' || char === "'") {
|
|
14
|
+
quote = char;
|
|
15
|
+
} else if (OPENING_BRACKETS.includes(char)) {
|
|
16
|
+
++depth;
|
|
17
|
+
} else if (CLOSING_BRACKETS.includes(char)) {
|
|
18
|
+
--depth;
|
|
19
|
+
}
|
|
20
|
+
onChar(char, quote === "" && depth === 0);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var splitSelectorList = (selectorList) => {
|
|
24
|
+
const selectors = [""];
|
|
25
|
+
scan(selectorList, (char, topLevel) => {
|
|
26
|
+
if (topLevel && char === ",") {
|
|
27
|
+
selectors.push("");
|
|
28
|
+
} else {
|
|
29
|
+
selectors[selectors.length - 1] += char;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
return selectors.map((selector) => selector.trimEnd());
|
|
33
|
+
};
|
|
34
|
+
var nestSelector = (parent, selector) => {
|
|
35
|
+
let nested = "";
|
|
36
|
+
let ampersandFound = false;
|
|
37
|
+
scan(selector, (char, topLevel) => {
|
|
38
|
+
if (char === "&" && topLevel) {
|
|
39
|
+
nested += parent;
|
|
40
|
+
ampersandFound = true;
|
|
41
|
+
} else {
|
|
42
|
+
nested += char;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return (ampersandFound ? nested : parent + selector).trimStart();
|
|
46
|
+
};
|
|
47
|
+
var stripSplitter = (key, splitter) => {
|
|
48
|
+
const splitterIndex = splitter === "" ? -1 : key.indexOf(splitter);
|
|
49
|
+
return splitterIndex < 0 ? key : key.slice(0, splitterIndex);
|
|
50
|
+
};
|
|
51
|
+
var cProperties = (node, splitter, prefix) => {
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const [key, value] of Object.entries(node)) {
|
|
54
|
+
if (value != null) {
|
|
55
|
+
const name = prefix + stripSplitter(key, splitter);
|
|
56
|
+
if (typeof value === "object") {
|
|
57
|
+
out.push(...cProperties(value, splitter, name));
|
|
58
|
+
} else {
|
|
59
|
+
out.push(`${name}:${value}`);
|
|
60
|
+
}
|
|
10
61
|
}
|
|
11
62
|
}
|
|
12
|
-
return
|
|
63
|
+
return out;
|
|
13
64
|
};
|
|
14
|
-
var
|
|
65
|
+
var cRules = (node, splitter, selectors) => {
|
|
15
66
|
const out = [];
|
|
16
|
-
let
|
|
67
|
+
let declarations = [];
|
|
17
68
|
const flush = () => {
|
|
18
|
-
if (
|
|
19
|
-
|
|
69
|
+
if (declarations.length > 0) {
|
|
70
|
+
out.push(selectors.length === 0 ? `${declarations.join(";")};` : `${selectors.join(",")}{${declarations.join(";")}}`);
|
|
71
|
+
declarations = [];
|
|
20
72
|
}
|
|
21
|
-
out.push(prefix === "" ? buffer.join(";") : `${prefix}{${buffer.join(";")}}`);
|
|
22
|
-
buffer = [];
|
|
23
73
|
};
|
|
24
|
-
for (const [
|
|
74
|
+
for (const [key, value] of Object.entries(node)) {
|
|
25
75
|
if (value == null) {
|
|
26
76
|
continue;
|
|
27
77
|
}
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (flatten) {
|
|
32
|
-
out.push(`${prefix + name}:${value}`);
|
|
33
|
-
} else {
|
|
34
|
-
buffer.push(`${name}:${value}`);
|
|
35
|
-
}
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
if (flatten) {
|
|
39
|
-
out.push(_c(value, splitter, prefix + name, true));
|
|
78
|
+
const name = stripSplitter(key, splitter);
|
|
79
|
+
if (typeof value !== "object") {
|
|
80
|
+
declarations.push(`${name}:${value}`);
|
|
40
81
|
continue;
|
|
41
82
|
}
|
|
42
83
|
flush();
|
|
43
|
-
if (
|
|
44
|
-
out.push(`${name}{${
|
|
84
|
+
if (name[0] === "@") {
|
|
85
|
+
out.push(`${name}{${cRules(value, splitter, selectors)}}`);
|
|
86
|
+
} else if (selectors.length === 0 && name === ":root") {
|
|
87
|
+
out.push(`:root{${cProperties(value, splitter, "").join(";")}}`);
|
|
45
88
|
} else {
|
|
46
|
-
|
|
89
|
+
const alternatives = splitSelectorList(name);
|
|
90
|
+
const parents = selectors.length === 0 ? [""] : selectors;
|
|
91
|
+
out.push(cRules(value, splitter, parents.flatMap((parent) => alternatives.map((selector) => nestSelector(parent, selector)))));
|
|
47
92
|
}
|
|
48
93
|
}
|
|
49
94
|
flush();
|
|
50
|
-
|
|
95
|
+
const css = out.join("");
|
|
96
|
+
return css.endsWith(";") ? css.slice(0, -1) : css;
|
|
97
|
+
};
|
|
98
|
+
var c = (node, splitter = DEFAULT_SPLITTER) => cRules(node, splitter, []);
|
|
99
|
+
// src/h.ts
|
|
100
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
101
|
+
var NAMESPACES = {
|
|
102
|
+
xlink: "http://www.w3.org/1999/xlink",
|
|
103
|
+
xml: "http://www.w3.org/XML/1998/namespace",
|
|
104
|
+
xmlns: "http://www.w3.org/2000/xmlns/"
|
|
105
|
+
};
|
|
106
|
+
var isPlainObject = (value) => {
|
|
107
|
+
if (typeof value !== "object" || value == null) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
const prototype = Object.getPrototypeOf(value);
|
|
111
|
+
return prototype === Object.prototype || prototype == null;
|
|
112
|
+
};
|
|
113
|
+
var setAttribute = (element, name, value) => {
|
|
114
|
+
if (value == null) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const indexOfColon = name.indexOf(":");
|
|
118
|
+
const namespace = indexOfColon < 0 ? undefined : NAMESPACES[name.slice(0, indexOfColon)];
|
|
119
|
+
if (namespace == null) {
|
|
120
|
+
if (value === false) {
|
|
121
|
+
element.removeAttribute(name);
|
|
122
|
+
} else {
|
|
123
|
+
element.setAttribute(name, value === true ? "" : String(value));
|
|
124
|
+
}
|
|
125
|
+
} else if (value === false) {
|
|
126
|
+
element.removeAttributeNS(namespace, name.slice(indexOfColon + 1));
|
|
127
|
+
} else {
|
|
128
|
+
element.setAttributeNS(namespace, name, value === true ? "" : String(value));
|
|
129
|
+
}
|
|
51
130
|
};
|
|
52
|
-
var
|
|
53
|
-
// src/nnn/h.ts
|
|
54
|
-
var XLINK_NS = "http://www.w3.org/1999/xlink";
|
|
55
|
-
var _h = (createElement, tagOrNode, ...args) => {
|
|
131
|
+
var hyperscript = (createElement, tagOrNode, ...args) => {
|
|
56
132
|
const node = typeof tagOrNode === "string" ? createElement(tagOrNode) : tagOrNode;
|
|
57
|
-
|
|
58
|
-
let child = null;
|
|
133
|
+
for (const arg of args) {
|
|
59
134
|
if (arg instanceof Node) {
|
|
60
|
-
|
|
135
|
+
node.appendChild(arg);
|
|
61
136
|
} else if (Array.isArray(arg)) {
|
|
62
|
-
|
|
137
|
+
node.appendChild(hyperscript(createElement, ...arg));
|
|
63
138
|
} else if (typeof arg === "string" || typeof arg === "number") {
|
|
64
|
-
|
|
65
|
-
} else if (
|
|
139
|
+
node.appendChild(document.createTextNode(String(arg)));
|
|
140
|
+
} else if (arg != null) {
|
|
66
141
|
for (const name in arg) {
|
|
67
142
|
const value = arg[name];
|
|
68
143
|
if (name[0] === "$") {
|
|
69
144
|
const name1 = name.slice(1);
|
|
70
|
-
if (
|
|
145
|
+
if (isPlainObject(value)) {
|
|
71
146
|
Object.assign(node[name1] ??= {}, value);
|
|
72
147
|
} else {
|
|
73
148
|
node[name1] = value;
|
|
74
149
|
}
|
|
75
150
|
} else if (node instanceof Element) {
|
|
76
|
-
|
|
77
|
-
if (indexOfColon >= 0) {
|
|
78
|
-
const nsKey = name.slice(0, indexOfColon);
|
|
79
|
-
if (nsKey === "xlink") {
|
|
80
|
-
const basename = name.slice(indexOfColon + 1);
|
|
81
|
-
if (value === true) {
|
|
82
|
-
node.setAttributeNS(XLINK_NS, basename, "");
|
|
83
|
-
} else if (value === false) {
|
|
84
|
-
node.removeAttributeNS(XLINK_NS, basename);
|
|
85
|
-
} else {
|
|
86
|
-
node.setAttributeNS(XLINK_NS, basename, "" + value);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
} else {
|
|
90
|
-
if (value === true) {
|
|
91
|
-
node.setAttribute(name, "");
|
|
92
|
-
} else if (value === false) {
|
|
93
|
-
node.removeAttribute(name);
|
|
94
|
-
} else {
|
|
95
|
-
node.setAttribute(name, "" + value);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
151
|
+
setAttribute(node, name, value);
|
|
98
152
|
}
|
|
99
153
|
}
|
|
100
154
|
}
|
|
101
|
-
|
|
102
|
-
node.appendChild(child);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
155
|
+
}
|
|
105
156
|
return node;
|
|
106
157
|
};
|
|
107
|
-
var h =
|
|
108
|
-
var s =
|
|
109
|
-
var svgUse = (id, ...args) => s("svg", ["use", {
|
|
110
|
-
// src/
|
|
158
|
+
var h = hyperscript.bind(null, (tag) => document.createElement(tag));
|
|
159
|
+
var s = hyperscript.bind(null, (tag) => document.createElementNS(SVG_NS, tag));
|
|
160
|
+
var svgUse = (id, ...args) => s("svg", ["use", { href: "#" + id }], ...args);
|
|
161
|
+
// src/rwd.ts
|
|
162
|
+
var isPositiveInteger = (value) => Number.isInteger(value) && value > 0;
|
|
111
163
|
var rwd = (root, selector, cellWidthPx, cellHeightPx, ...specs) => {
|
|
164
|
+
if (!(cellWidthPx > 0 && cellHeightPx > 0 && Number.isFinite(cellWidthPx) && Number.isFinite(cellHeightPx))) {
|
|
165
|
+
throw new RangeError(`rwd: cell size must be positive: ${cellWidthPx}×${cellHeightPx}`);
|
|
166
|
+
}
|
|
167
|
+
for (const [columns, width = 1, height = 1] of specs) {
|
|
168
|
+
if (!isPositiveInteger(columns) || !isPositiveInteger(width) || !isPositiveInteger(height) || width > columns) {
|
|
169
|
+
throw new RangeError(`rwd: invalid spec: [${columns}, ${width}, ${height}]`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
112
172
|
const main = root[selector] ??= {};
|
|
113
173
|
main["box-sizing"] = "border-box";
|
|
114
174
|
main.display = "block";
|
|
@@ -116,18 +176,16 @@ var rwd = (root, selector, cellWidthPx, cellHeightPx, ...specs) => {
|
|
|
116
176
|
main.width = "100%";
|
|
117
177
|
main.height = `${cellHeightPx}px`;
|
|
118
178
|
specs.sort(([a], [b]) => a - b);
|
|
119
|
-
for (
|
|
120
|
-
const atRule = `@media(min-width:${cellWidthPx *
|
|
121
|
-
const node =
|
|
122
|
-
width ??= 1;
|
|
123
|
-
height ??= 1;
|
|
179
|
+
for (const [columns, width = 1, height = 1] of specs) {
|
|
180
|
+
const atRule = `@media(min-width:${cellWidthPx * columns}px)`;
|
|
181
|
+
const node = columns === 1 ? main : (root[atRule] ??= {})[selector] ??= {};
|
|
124
182
|
let gcd = 100 * width;
|
|
125
|
-
let tmp =
|
|
183
|
+
let tmp = columns;
|
|
126
184
|
while (tmp > 0) {
|
|
127
185
|
[gcd, tmp] = [tmp, gcd % tmp];
|
|
128
186
|
}
|
|
129
187
|
const widthOverGcd = 100 * width / gcd;
|
|
130
|
-
node.width =
|
|
188
|
+
node.width = columns === gcd ? `${widthOverGcd}%` : `calc(${widthOverGcd}% / ${columns / gcd})`;
|
|
131
189
|
node.height = `${cellHeightPx * height}px`;
|
|
132
190
|
}
|
|
133
191
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jackens/nnn",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.11.17",
|
|
4
4
|
"description": "Jackens’ JavaScript helpers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"CSS-in-JS",
|
|
@@ -14,5 +14,16 @@
|
|
|
14
14
|
"author": "Jackens",
|
|
15
15
|
"type": "module",
|
|
16
16
|
"main": "nnn.js",
|
|
17
|
-
"types": "nnn.d.ts"
|
|
17
|
+
"types": "nnn.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./nnn.d.ts",
|
|
21
|
+
"default": "./nnn.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"nnn.d.ts",
|
|
26
|
+
"nnn.js"
|
|
27
|
+
],
|
|
28
|
+
"sideEffects": false
|
|
18
29
|
}
|