@jackens/nnn 2026.10.1 → 2026.11.16
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 +274 -52
- package/nnn.d.ts +26 -15
- 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.16`).
|
|
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,19 @@ 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; whitespace after a separating comma is ignored (use `&` for a descendant combinator in a non-first alternative, e.g. `'.b,& .c'` → `a.b,a .c`).
|
|
93
|
+
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}}`).
|
|
94
|
+
Inside a top-level `:root` rule nested keys are concatenated into property names (e.g. `{ ':root': { '--': { bg: 'red' } } }` → `:root{--bg:red}`).
|
|
95
|
+
Properties without a selector (top-level or directly in a top-level at-rule, e.g. `@font-face`) are emitted as bare declarations.
|
|
92
96
|
`null`/`undefined` values are skipped.
|
|
93
97
|
|
|
94
98
|
#### splitter
|
|
95
99
|
|
|
96
100
|
A delimiter used to create unique keys (default: `'$$'`).
|
|
97
101
|
The substring from `splitter` to the end of a key is ignored (e.g. `src$$1` → `src`).
|
|
102
|
+
An empty `splitter` disables this feature.
|
|
98
103
|
|
|
99
104
|
#### Returns
|
|
100
105
|
|
|
@@ -169,7 +174,7 @@ const actual = c({
|
|
|
169
174
|
'@font-face$$1': {
|
|
170
175
|
'font-family': 'Jackens',
|
|
171
176
|
src$$1: 'url(otf/jackens.otf)',
|
|
172
|
-
src$$2: "url(otf/jackens.otf) format('opentype'),
|
|
177
|
+
src$$2: "url(otf/jackens.otf) format('opentype'),url(svg/jackens.svg) format('svg')",
|
|
173
178
|
'font-weight': 'normal',
|
|
174
179
|
'font-style': 'normal',
|
|
175
180
|
},
|
|
@@ -405,11 +410,128 @@ const expected = `
|
|
|
405
410
|
expect(actual).toBe(expected)
|
|
406
411
|
```
|
|
407
412
|
|
|
413
|
+
`&` is replaced with the parent selector:
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
const actual = c({
|
|
417
|
+
a: {
|
|
418
|
+
color: 'red',
|
|
419
|
+
'&:hover': { color: 'blue' },
|
|
420
|
+
'.dark &': { color: 'white' },
|
|
421
|
+
'.b,& .c': { margin: 1 },
|
|
422
|
+
},
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
const expected = `
|
|
426
|
+
a{
|
|
427
|
+
color:red
|
|
428
|
+
}
|
|
429
|
+
a:hover{
|
|
430
|
+
color:blue
|
|
431
|
+
}
|
|
432
|
+
.dark a{
|
|
433
|
+
color:white
|
|
434
|
+
}
|
|
435
|
+
a.b,a .c{
|
|
436
|
+
margin:1
|
|
437
|
+
}
|
|
438
|
+
`.replace(/\n\s*/g, '')
|
|
439
|
+
|
|
440
|
+
expect(actual).toBe(expected)
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Commas inside brackets or quotes do not split selectors and whitespace after a comma is ignored:
|
|
444
|
+
|
|
445
|
+
```ts
|
|
446
|
+
const actual = c({
|
|
447
|
+
':is(.a,.b)': {
|
|
448
|
+
'.c, .d': { margin: 1 },
|
|
449
|
+
'[title="x,y"]': { margin: 2 },
|
|
450
|
+
},
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
const expected = `
|
|
454
|
+
:is(.a,.b).c,:is(.a,.b).d{
|
|
455
|
+
margin:1
|
|
456
|
+
}
|
|
457
|
+
:is(.a,.b)[title="x,y"]{
|
|
458
|
+
margin:2
|
|
459
|
+
}
|
|
460
|
+
`.replace(/\n\s*/g, '')
|
|
461
|
+
|
|
462
|
+
expect(actual).toBe(expected)
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
At-rules nested in selectors wrap these selectors:
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
const actual = c({
|
|
469
|
+
a: {
|
|
470
|
+
color: 'red',
|
|
471
|
+
'@media(min-width:600px)': {
|
|
472
|
+
color: 'blue',
|
|
473
|
+
'.b': { margin: 1 },
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
const expected = `
|
|
479
|
+
a{
|
|
480
|
+
color:red
|
|
481
|
+
}
|
|
482
|
+
@media(min-width:600px){
|
|
483
|
+
a{
|
|
484
|
+
color:blue
|
|
485
|
+
}
|
|
486
|
+
a.b{
|
|
487
|
+
margin:1
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
`.replace(/\n\s*/g, '')
|
|
491
|
+
|
|
492
|
+
expect(actual).toBe(expected)
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Declarations without a selector are separated from subsequent rules:
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
expect(c({ color: 'red', margin: 0 })).toBe('color:red;margin:0')
|
|
499
|
+
|
|
500
|
+
expect(c({ color: 'red', a: { margin: 0 }, padding: 0 })).toBe('color:red;a{margin:0}padding:0')
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
`null` and `undefined` values are skipped:
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
const actual = c({
|
|
507
|
+
a: {
|
|
508
|
+
color: 'red',
|
|
509
|
+
margin: null,
|
|
510
|
+
padding: undefined,
|
|
511
|
+
'.b': null,
|
|
512
|
+
},
|
|
513
|
+
':root': {
|
|
514
|
+
'--': { bg: null, fg: 'black' },
|
|
515
|
+
},
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
expect(actual).toBe('a{color:red}:root{--fg:black}')
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
Custom splitter (an empty one disables splitting):
|
|
522
|
+
|
|
523
|
+
```ts
|
|
524
|
+
expect(c({ a: { 'src#1': 'url(a.otf)', 'src#2': 'url(a.woff)' } }, '#')).toBe('a{src:url(a.otf);src:url(a.woff)}')
|
|
525
|
+
|
|
526
|
+
expect(c({ a: { b$$c: 1 } }, '')).toBe('a{b$$c:1}')
|
|
527
|
+
```
|
|
528
|
+
|
|
408
529
|
### h
|
|
409
530
|
|
|
410
531
|
```ts
|
|
411
532
|
const h: {
|
|
412
533
|
<T extends keyof HTMLElementTagNameMap>(tag: T, ...args: HArgs1[]): HTMLElementTagNameMap[T]
|
|
534
|
+
(tag: string, ...args: HArgs1[]): HTMLElement
|
|
413
535
|
<N extends Node>(node: N, ...args: HArgs1[]): N
|
|
414
536
|
(tagOrNode: string | Node, ...args: HArgs1[]): Node
|
|
415
537
|
}
|
|
@@ -427,9 +549,9 @@ If a `Node`, that node is modified in place.
|
|
|
427
549
|
Additional arguments processed as follows:
|
|
428
550
|
|
|
429
551
|
- `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.
|
|
552
|
+
- 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.
|
|
553
|
+
- 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`.
|
|
554
|
+
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
555
|
- `null`/`undefined`: ignored.
|
|
434
556
|
- `Node`: appended as a child.
|
|
435
557
|
- `string`/`number`: converted to a `Text` node and appended.
|
|
@@ -555,6 +677,46 @@ const elemWithNested = h('div', ['span', 'hello'], ['b', 'world'])
|
|
|
555
677
|
expect(elemWithNested.outerHTML).toBe('<div><span>hello</span><b>world</b></div>')
|
|
556
678
|
```
|
|
557
679
|
|
|
680
|
+
`false` removes an attribute, `null` and `undefined` leave it unchanged:
|
|
681
|
+
|
|
682
|
+
```ts
|
|
683
|
+
const input = h('input', { type: 'checkbox', checked: true, title: 'title' })
|
|
684
|
+
|
|
685
|
+
h(input, { checked: false, title: null, type: undefined })
|
|
686
|
+
|
|
687
|
+
expect(input.outerHTML).toBe('<input type="checkbox" title="title">')
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
Only plain objects are merged into `$`-prefixed properties:
|
|
691
|
+
|
|
692
|
+
```ts
|
|
693
|
+
const date = new Date(0)
|
|
694
|
+
const div = h('div', { $date: date, $list: [1, 2] })
|
|
695
|
+
|
|
696
|
+
expect(div.date).toBe(date)
|
|
697
|
+
|
|
698
|
+
h(div, { $list: [3] })
|
|
699
|
+
|
|
700
|
+
expect(div.list).toEqual([3])
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
Namespaced attributes:
|
|
704
|
+
|
|
705
|
+
```ts
|
|
706
|
+
const div = h('div', { 'xml:lang': 'pl', 'ng:x': 1 })
|
|
707
|
+
|
|
708
|
+
expect(div.getAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang')).toBe('pl')
|
|
709
|
+
expect(div.getAttribute('ng:x')).toBe('1')
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
Return types:
|
|
713
|
+
|
|
714
|
+
```ts
|
|
715
|
+
expectTypeOf(h('input')).toEqualTypeOf<HTMLInputElement>()
|
|
716
|
+
expectTypeOf(h('my-element')).toEqualTypeOf<HTMLElement>()
|
|
717
|
+
expectTypeOf(h(document.createTextNode(''))).toEqualTypeOf<Text>()
|
|
718
|
+
```
|
|
719
|
+
|
|
558
720
|
### rwd
|
|
559
721
|
|
|
560
722
|
```ts
|
|
@@ -563,7 +725,7 @@ const rwd: (
|
|
|
563
725
|
selector: string,
|
|
564
726
|
cellWidthPx: number,
|
|
565
727
|
cellHeightPx: number,
|
|
566
|
-
...specs: [number, number
|
|
728
|
+
...specs: [columns: number, width?: number, height?: number][]
|
|
567
729
|
) => void
|
|
568
730
|
```
|
|
569
731
|
|
|
@@ -580,20 +742,24 @@ The CSS selector for the grid item.
|
|
|
580
742
|
|
|
581
743
|
#### cellWidthPx
|
|
582
744
|
|
|
583
|
-
The base cell width in pixels.
|
|
745
|
+
The base cell width in pixels (a positive number).
|
|
584
746
|
|
|
585
747
|
#### cellHeightPx
|
|
586
748
|
|
|
587
|
-
The base cell height in pixels.
|
|
749
|
+
The base cell height in pixels (a positive number).
|
|
588
750
|
|
|
589
751
|
#### specs
|
|
590
752
|
|
|
591
|
-
An array of breakpoint specifications, each a tuple of:
|
|
753
|
+
An array of breakpoint specifications, each a tuple of positive integers:
|
|
592
754
|
|
|
593
|
-
- `
|
|
594
|
-
- `width` (optional, default `1`): number of horizontal cells the element spans.
|
|
755
|
+
- `columns`: number of cells per row; the breakpoint is `min-width: cellWidthPx * columns` (`1` modifies the base rule).
|
|
756
|
+
- `width` (optional, default `1`, at most `columns`): number of horizontal cells the element spans.
|
|
595
757
|
- `height` (optional, default `1`): number of vertical cells the element spans.
|
|
596
758
|
|
|
759
|
+
#### Throws
|
|
760
|
+
|
|
761
|
+
A `RangeError` if any of the numeric arguments is out of range.
|
|
762
|
+
|
|
597
763
|
#### Usage Examples
|
|
598
764
|
|
|
599
765
|
Responsive grid cell widths and heights with media queries:
|
|
@@ -643,11 +809,57 @@ expect(style).toEqual({
|
|
|
643
809
|
})
|
|
644
810
|
```
|
|
645
811
|
|
|
812
|
+
Widths that divide evenly do not use `calc`; existing at-rules are extended:
|
|
813
|
+
|
|
814
|
+
```ts
|
|
815
|
+
const style: CNode = {
|
|
816
|
+
'@media(min-width:400px)': {
|
|
817
|
+
'.r2': { margin: 0 },
|
|
818
|
+
},
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
rwd(style, '.r2', 100, 40, [4, 2], [2, 2, 3])
|
|
822
|
+
|
|
823
|
+
expect(style).toEqual({
|
|
824
|
+
'@media(min-width:400px)': {
|
|
825
|
+
'.r2': {
|
|
826
|
+
margin: 0,
|
|
827
|
+
width: '50%',
|
|
828
|
+
height: '40px',
|
|
829
|
+
},
|
|
830
|
+
},
|
|
831
|
+
'.r2': {
|
|
832
|
+
'box-sizing': 'border-box',
|
|
833
|
+
display: 'block',
|
|
834
|
+
float: 'left',
|
|
835
|
+
width: '100%',
|
|
836
|
+
height: '40px',
|
|
837
|
+
},
|
|
838
|
+
'@media(min-width:200px)': {
|
|
839
|
+
'.r2': {
|
|
840
|
+
width: '100%',
|
|
841
|
+
height: '120px',
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
})
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
Invalid arguments throw a `RangeError`:
|
|
848
|
+
|
|
849
|
+
```ts
|
|
850
|
+
expect(() => rwd({}, '.x', 0, 50)).toThrow(RangeError)
|
|
851
|
+
expect(() => rwd({}, '.x', 100, Infinity)).toThrow(RangeError)
|
|
852
|
+
expect(() => rwd({}, '.x', 100, 50, [0])).toThrow(RangeError)
|
|
853
|
+
expect(() => rwd({}, '.x', 100, 50, [2, 3])).toThrow(RangeError)
|
|
854
|
+
expect(() => rwd({}, '.x', 100, 50, [2, 1, 1.5])).toThrow(RangeError)
|
|
855
|
+
```
|
|
856
|
+
|
|
646
857
|
### s
|
|
647
858
|
|
|
648
859
|
```ts
|
|
649
860
|
const s: {
|
|
650
861
|
<T extends keyof SVGElementTagNameMap>(tag: T, ...args: HArgs1[]): SVGElementTagNameMap[T]
|
|
862
|
+
(tag: string, ...args: HArgs1[]): SVGElement
|
|
651
863
|
<N extends Node>(node: N, ...args: HArgs1[]): N
|
|
652
864
|
(tagOrNode: string | Node, ...args: HArgs1[]): Node
|
|
653
865
|
}
|
|
@@ -665,9 +877,9 @@ If a `Node`, that node is modified in place.
|
|
|
665
877
|
Additional arguments processed as follows:
|
|
666
878
|
|
|
667
879
|
- `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
|
|
880
|
+
- 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.
|
|
881
|
+
- 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`.
|
|
882
|
+
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
883
|
- `null`/`undefined`: ignored.
|
|
672
884
|
- `Node`: appended as a child.
|
|
673
885
|
- `string`/`number`: converted to a `Text` node and appended.
|
|
@@ -679,6 +891,16 @@ The created `SVGElement` or the modified node.
|
|
|
679
891
|
|
|
680
892
|
#### Usage Examples
|
|
681
893
|
|
|
894
|
+
Elements (including nested ones) are created in the SVG namespace:
|
|
895
|
+
|
|
896
|
+
```ts
|
|
897
|
+
const svg = s('svg', ['g', ['circle', { r: 1 }]])
|
|
898
|
+
|
|
899
|
+
expect(svg.namespaceURI).toBe('http://www.w3.org/2000/svg')
|
|
900
|
+
expect(svg.querySelector('circle')?.namespaceURI).toBe('http://www.w3.org/2000/svg')
|
|
901
|
+
expect(svg.outerHTML).toBe('<svg><g><circle r="1"></circle></g></svg>')
|
|
902
|
+
```
|
|
903
|
+
|
|
682
904
|
Namespaced `xlink:` attribute set to `true` gets an empty value:
|
|
683
905
|
|
|
684
906
|
```ts
|
|
@@ -687,10 +909,12 @@ const svg = s('svg', { 'xlink:href': true })
|
|
|
687
909
|
expect(svg.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe('')
|
|
688
910
|
```
|
|
689
911
|
|
|
690
|
-
Namespaced `xlink:` attribute set to `false` is
|
|
912
|
+
Namespaced `xlink:` attribute set to `false` is removed:
|
|
691
913
|
|
|
692
914
|
```ts
|
|
693
|
-
const svg = s('svg', { 'xlink:href':
|
|
915
|
+
const svg = s('svg', { 'xlink:href': '#id' })
|
|
916
|
+
|
|
917
|
+
s(svg, { 'xlink:href': false })
|
|
694
918
|
|
|
695
919
|
expect(svg.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBeNull()
|
|
696
920
|
```
|
|
@@ -711,6 +935,22 @@ const svg = s('svg', { 'xlink:title': 42 })
|
|
|
711
935
|
expect(svg.getAttributeNS('http://www.w3.org/1999/xlink', 'title')).toBe('42')
|
|
712
936
|
```
|
|
713
937
|
|
|
938
|
+
Namespaced `xml:` and `xmlns:` attributes:
|
|
939
|
+
|
|
940
|
+
```ts
|
|
941
|
+
const svg = s('svg', { 'xml:lang': 'pl', 'xmlns:xlink': 'http://www.w3.org/1999/xlink' })
|
|
942
|
+
|
|
943
|
+
expect(svg.getAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang')).toBe('pl')
|
|
944
|
+
expect(svg.getAttributeNS('http://www.w3.org/2000/xmlns/', 'xlink')).toBe('http://www.w3.org/1999/xlink')
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
Return types:
|
|
948
|
+
|
|
949
|
+
```ts
|
|
950
|
+
expectTypeOf(s('circle')).toEqualTypeOf<SVGCircleElement>()
|
|
951
|
+
expectTypeOf(s('my-element')).toEqualTypeOf<SVGElement>()
|
|
952
|
+
```
|
|
953
|
+
|
|
714
954
|
### svgUse
|
|
715
955
|
|
|
716
956
|
```ts
|
|
@@ -722,7 +962,7 @@ Shorthand for creating an `<svg>` element with a `<use>` child referencing a sym
|
|
|
722
962
|
Equivalent to:
|
|
723
963
|
|
|
724
964
|
```ts
|
|
725
|
-
s('svg', ['use', {
|
|
965
|
+
s('svg', ['use', { href: '#' + id }], ...args)
|
|
726
966
|
```
|
|
727
967
|
|
|
728
968
|
#### id
|
|
@@ -739,46 +979,28 @@ An `SVGSVGElement` containing a `<use>` element.
|
|
|
739
979
|
|
|
740
980
|
#### Usage Examples
|
|
741
981
|
|
|
742
|
-
SVG `use` element referencing a symbol
|
|
982
|
+
SVG `use` element referencing a symbol:
|
|
743
983
|
|
|
744
984
|
```ts
|
|
745
985
|
const svg = svgUse('icon-home')
|
|
746
986
|
|
|
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')
|
|
987
|
+
expect(svg.outerHTML).toBe('<svg><use href="#icon-home"></use></svg>')
|
|
988
|
+
```
|
|
770
989
|
|
|
771
|
-
|
|
990
|
+
Additional attributes are set on the outer `svg` element:
|
|
772
991
|
|
|
773
|
-
|
|
992
|
+
```ts
|
|
993
|
+
const svg = svgUse('icon-settings', { width: 24, height: 24, class: 'icon' })
|
|
774
994
|
|
|
775
|
-
expect(
|
|
995
|
+
expect(svg.outerHTML).toBe('<svg width="24" height="24" class="icon"><use href="#icon-settings"></use></svg>')
|
|
996
|
+
```
|
|
776
997
|
|
|
777
|
-
|
|
998
|
+
Additional children are appended to the outer `svg` element:
|
|
778
999
|
|
|
779
|
-
|
|
1000
|
+
```ts
|
|
1001
|
+
const svg = svgUse('icon-star', ['title', 'Star'])
|
|
780
1002
|
|
|
781
|
-
expect(
|
|
1003
|
+
expect(svg.outerHTML).toBe('<svg><use href="#icon-star"></use><title>Star</title></svg>')
|
|
782
1004
|
```
|
|
783
1005
|
|
|
784
1006
|
## 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,19 @@ 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; whitespace after a separating comma is ignored (use `&` for a descendant combinator in a non-first alternative, e.g. `'.b,& .c'` → `a.b,a .c`).
|
|
16
|
+
* 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}}`).
|
|
17
|
+
* Inside a top-level `:root` rule nested keys are concatenated into property names (e.g. `{ ':root': { '--': { bg: 'red' } } }` → `:root{--bg:red}`).
|
|
18
|
+
* Properties without a selector (top-level or directly in a top-level at-rule, e.g. `@font-face`) are emitted as bare declarations.
|
|
15
19
|
* `null`/`undefined` values are skipped.
|
|
16
20
|
*
|
|
17
21
|
* @param splitter
|
|
18
22
|
*
|
|
19
23
|
* A delimiter used to create unique keys (default: `'$$'`).
|
|
20
24
|
* The substring from `splitter` to the end of a key is ignored (e.g. `src$$1` → `src`).
|
|
25
|
+
* An empty `splitter` disables this feature.
|
|
21
26
|
*
|
|
22
27
|
* @returns
|
|
23
28
|
*
|
|
@@ -41,9 +46,9 @@ export type HArgs = [string | Node, ...HArgs1[]]
|
|
|
41
46
|
* Additional arguments processed as follows:
|
|
42
47
|
*
|
|
43
48
|
* - `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.
|
|
49
|
+
* - 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.
|
|
50
|
+
* - 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`.
|
|
51
|
+
* 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
52
|
* - `null`/`undefined`: ignored.
|
|
48
53
|
* - `Node`: appended as a child.
|
|
49
54
|
* - `string`/`number`: converted to a `Text` node and appended.
|
|
@@ -55,6 +60,7 @@ export type HArgs = [string | Node, ...HArgs1[]]
|
|
|
55
60
|
*/
|
|
56
61
|
export declare const h: {
|
|
57
62
|
<T extends keyof HTMLElementTagNameMap>(tag: T, ...args: HArgs1[]): HTMLElementTagNameMap[T]
|
|
63
|
+
(tag: string, ...args: HArgs1[]): HTMLElement
|
|
58
64
|
<N extends Node>(node: N, ...args: HArgs1[]): N
|
|
59
65
|
(tagOrNode: string | Node, ...args: HArgs1[]): Node
|
|
60
66
|
}
|
|
@@ -71,9 +77,9 @@ export declare const h: {
|
|
|
71
77
|
* Additional arguments processed as follows:
|
|
72
78
|
*
|
|
73
79
|
* - `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
|
|
80
|
+
* - 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.
|
|
81
|
+
* - 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`.
|
|
82
|
+
* 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
83
|
* - `null`/`undefined`: ignored.
|
|
78
84
|
* - `Node`: appended as a child.
|
|
79
85
|
* - `string`/`number`: converted to a `Text` node and appended.
|
|
@@ -85,6 +91,7 @@ export declare const h: {
|
|
|
85
91
|
*/
|
|
86
92
|
export declare const s: {
|
|
87
93
|
<T extends keyof SVGElementTagNameMap>(tag: T, ...args: HArgs1[]): SVGElementTagNameMap[T]
|
|
94
|
+
(tag: string, ...args: HArgs1[]): SVGElement
|
|
88
95
|
<N extends Node>(node: N, ...args: HArgs1[]): N
|
|
89
96
|
(tagOrNode: string | Node, ...args: HArgs1[]): Node
|
|
90
97
|
}
|
|
@@ -94,7 +101,7 @@ export declare const s: {
|
|
|
94
101
|
* Equivalent to:
|
|
95
102
|
*
|
|
96
103
|
* ```ts
|
|
97
|
-
* s('svg', ['use', {
|
|
104
|
+
* s('svg', ['use', { href: '#' + id }], ...args)
|
|
98
105
|
* ```
|
|
99
106
|
*
|
|
100
107
|
* @param id
|
|
@@ -124,24 +131,28 @@ export declare const svgUse: (id: string, ...args: HArgs1[]) => SVGSVGElement
|
|
|
124
131
|
*
|
|
125
132
|
* @param cellWidthPx
|
|
126
133
|
*
|
|
127
|
-
* The base cell width in pixels.
|
|
134
|
+
* The base cell width in pixels (a positive number).
|
|
128
135
|
*
|
|
129
136
|
* @param cellHeightPx
|
|
130
137
|
*
|
|
131
|
-
* The base cell height in pixels.
|
|
138
|
+
* The base cell height in pixels (a positive number).
|
|
132
139
|
*
|
|
133
140
|
* @param specs
|
|
134
141
|
*
|
|
135
|
-
* An array of breakpoint specifications, each a tuple of:
|
|
142
|
+
* An array of breakpoint specifications, each a tuple of positive integers:
|
|
136
143
|
*
|
|
137
|
-
* - `
|
|
138
|
-
* - `width` (optional, default `1`): number of horizontal cells the element spans.
|
|
144
|
+
* - `columns`: number of cells per row; the breakpoint is `min-width: cellWidthPx * columns` (`1` modifies the base rule).
|
|
145
|
+
* - `width` (optional, default `1`, at most `columns`): number of horizontal cells the element spans.
|
|
139
146
|
* - `height` (optional, default `1`): number of vertical cells the element spans.
|
|
147
|
+
*
|
|
148
|
+
* @throws
|
|
149
|
+
*
|
|
150
|
+
* A `RangeError` if any of the numeric arguments is out of range.
|
|
140
151
|
*/
|
|
141
152
|
export declare const rwd: (
|
|
142
153
|
root: CNode,
|
|
143
154
|
selector: string,
|
|
144
155
|
cellWidthPx: number,
|
|
145
156
|
cellHeightPx: number,
|
|
146
|
-
...specs: [number, number
|
|
157
|
+
...specs: [columns: number, width?: number, height?: number][]
|
|
147
158
|
) => 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, index) => (index === 0 ? selector : selector.trimStart()).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;
|
|
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.16",
|
|
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
|
}
|