@ponchia/ui 0.3.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1063 @@
1
+ /**
2
+ * @ponchia/ui — display glyphs.
3
+ *
4
+ * Small, frozen bitmap registry rendered on the existing `.ui-dotmatrix`
5
+ * primitive (see css/dots.css). CSS-first to the core: a glyph is just a
6
+ * grid of dot cells, three tones — `.` off (the dim panel dot), `#` hot,
7
+ * `*` accent — so it re-skins with the same `--field-dot*` tokens as every
8
+ * other dot surface.
9
+ *
10
+ * Framework-agnostic, dependency-free, side-effect-free on import, SSR-safe
11
+ * (`renderGlyph` returns a string; nothing touches the DOM here — the
12
+ * optional `initDotGlyph` behavior lives in behaviors/index.js).
13
+ *
14
+ * import { renderGlyph } from '@ponchia/ui/glyphs';
15
+ * el.innerHTML = renderGlyph('check', { label: 'Done' });
16
+ *
17
+ * The `GlyphName` union, helper signatures, and drift gate are generated +
18
+ * checked from this file — see scripts/gen-glyphs.mjs / check-glyphs.mjs.
19
+ */
20
+
21
+ /** The grid edge length: every glyph is GLYPH_SIZE rows of GLYPH_SIZE chars. */
22
+ export const GLYPH_SIZE = 16;
23
+
24
+ // Raw bitmaps. Each is GLYPH_SIZE rows of GLYPH_SIZE chars over [.#*]:
25
+ // `.` off · `#` hot · `*` accent. Only `spark` uses accent dots — it is the
26
+ // canonical two-tone demo; the gate in check-glyphs.mjs enforces the shape.
27
+ const RAW = {
28
+ circle: [
29
+ '................',
30
+ '.....######.....',
31
+ '....########....',
32
+ '...###....###...',
33
+ '..##........##..',
34
+ '.###........###.',
35
+ '.##..........##.',
36
+ '.##..........##.',
37
+ '.##..........##.',
38
+ '.##..........##.',
39
+ '.###........###.',
40
+ '..##........##..',
41
+ '...###....###...',
42
+ '....########....',
43
+ '.....######.....',
44
+ '................',
45
+ ],
46
+ 'check-circle': [
47
+ '................',
48
+ '.....######.....',
49
+ '....########....',
50
+ '...###....###...',
51
+ '..##........##..',
52
+ '.###........###.',
53
+ '.##........####.',
54
+ '.##..##...##.##.',
55
+ '.##...##.##..##.',
56
+ '.##....###...##.',
57
+ '.###...##...###.',
58
+ '..##........##..',
59
+ '...###....###...',
60
+ '....########....',
61
+ '.....######.....',
62
+ '................',
63
+ ],
64
+ 'x-circle': [
65
+ '................',
66
+ '.....######.....',
67
+ '....########....',
68
+ '...###....###...',
69
+ '..##........##..',
70
+ '.###.##..##.###.',
71
+ '.##...####...##.',
72
+ '.##....##....##.',
73
+ '.##...####...##.',
74
+ '.##..##..##..##.',
75
+ '.#####....#####.',
76
+ '..##........##..',
77
+ '...###....###...',
78
+ '....########....',
79
+ '.....######.....',
80
+ '................',
81
+ ],
82
+ 'plus-circle': [
83
+ '................',
84
+ '.....######.....',
85
+ '....########....',
86
+ '...###....###...',
87
+ '..##........##..',
88
+ '.###...##...###.',
89
+ '.##....##....##.',
90
+ '.##..######..##.',
91
+ '.##..######..##.',
92
+ '.##....##....##.',
93
+ '.###...##...###.',
94
+ '..##........##..',
95
+ '...###....###...',
96
+ '....########....',
97
+ '.....######.....',
98
+ '................',
99
+ ],
100
+ 'minus-circle': [
101
+ '................',
102
+ '.....######.....',
103
+ '....########....',
104
+ '...###....###...',
105
+ '..##........##..',
106
+ '.###........###.',
107
+ '.##..........##.',
108
+ '.##..######..##.',
109
+ '.##..######..##.',
110
+ '.##..........##.',
111
+ '.###........###.',
112
+ '..##........##..',
113
+ '...###....###...',
114
+ '....########....',
115
+ '.....######.....',
116
+ '................',
117
+ ],
118
+ 'arrow-down': [
119
+ '................',
120
+ '......####......',
121
+ '......####......',
122
+ '......####......',
123
+ '......####......',
124
+ '......####......',
125
+ '......####......',
126
+ '.##############.',
127
+ '..############..',
128
+ '...##########...',
129
+ '....########....',
130
+ '.....######.....',
131
+ '......####......',
132
+ '.......##.......',
133
+ '.......##.......',
134
+ '................',
135
+ ],
136
+ 'arrow-left': [
137
+ '................',
138
+ '........#.......',
139
+ '.......##.......',
140
+ '......###.......',
141
+ '.....####.......',
142
+ '....#####.......',
143
+ '...############.',
144
+ '.##############.',
145
+ '.##############.',
146
+ '...############.',
147
+ '....#####.......',
148
+ '.....####.......',
149
+ '......###.......',
150
+ '.......##.......',
151
+ '........#.......',
152
+ '................',
153
+ ],
154
+ 'arrow-right': [
155
+ '................',
156
+ '.......#........',
157
+ '.......##.......',
158
+ '.......###......',
159
+ '.......####.....',
160
+ '.......#####....',
161
+ '.############...',
162
+ '.##############.',
163
+ '.##############.',
164
+ '.############...',
165
+ '.......#####....',
166
+ '.......####.....',
167
+ '.......###......',
168
+ '.......##.......',
169
+ '.......#........',
170
+ '................',
171
+ ],
172
+ 'arrow-up': [
173
+ '................',
174
+ '.......##.......',
175
+ '.......##.......',
176
+ '......####......',
177
+ '.....######.....',
178
+ '....########....',
179
+ '...##########...',
180
+ '..############..',
181
+ '.##############.',
182
+ '......####......',
183
+ '......####......',
184
+ '......####......',
185
+ '......####......',
186
+ '......####......',
187
+ '......####......',
188
+ '................',
189
+ ],
190
+ bell: [
191
+ '................',
192
+ '.......##.......',
193
+ '......####......',
194
+ '.....######.....',
195
+ '....########....',
196
+ '....########....',
197
+ '....########....',
198
+ '...##########...',
199
+ '...##########...',
200
+ '..############..',
201
+ '.##############.',
202
+ '################',
203
+ '................',
204
+ '......####......',
205
+ '......####......',
206
+ '................',
207
+ ],
208
+ check: [
209
+ '................',
210
+ '................',
211
+ '.............###',
212
+ '............###.',
213
+ '...........###..',
214
+ '..........###...',
215
+ '.........###....',
216
+ '.##.....###.....',
217
+ '.##....###......',
218
+ '.###..###.......',
219
+ '..######........',
220
+ '...####.........',
221
+ '...###..........',
222
+ '................',
223
+ '................',
224
+ '................',
225
+ ],
226
+ 'chevron-down': [
227
+ '................',
228
+ '................',
229
+ '................',
230
+ '................',
231
+ '.##..........##.',
232
+ '.###........###.',
233
+ '..###......###..',
234
+ '...###....###...',
235
+ '....###..###....',
236
+ '.....######.....',
237
+ '......####......',
238
+ '.......##.......',
239
+ '................',
240
+ '................',
241
+ '................',
242
+ '................',
243
+ ],
244
+ 'chevron-left': [
245
+ '................',
246
+ '..........##....',
247
+ '.........###....',
248
+ '........###.....',
249
+ '.......###......',
250
+ '......###.......',
251
+ '.....###........',
252
+ '....###.........',
253
+ '....###.........',
254
+ '.....###........',
255
+ '......###.......',
256
+ '.......###......',
257
+ '........###.....',
258
+ '.........###....',
259
+ '..........##....',
260
+ '................',
261
+ ],
262
+ 'chevron-right': [
263
+ '................',
264
+ '....##..........',
265
+ '....###.........',
266
+ '.....###........',
267
+ '......###.......',
268
+ '.......###......',
269
+ '........###.....',
270
+ '.........###....',
271
+ '.........###....',
272
+ '........###.....',
273
+ '.......###......',
274
+ '......###.......',
275
+ '.....###........',
276
+ '....###.........',
277
+ '....##..........',
278
+ '................',
279
+ ],
280
+ 'chevron-up': [
281
+ '................',
282
+ '................',
283
+ '................',
284
+ '................',
285
+ '.......##.......',
286
+ '......####......',
287
+ '.....######.....',
288
+ '....###..###....',
289
+ '...###....###...',
290
+ '..###......###..',
291
+ '.###........###.',
292
+ '.##..........##.',
293
+ '................',
294
+ '................',
295
+ '................',
296
+ '................',
297
+ ],
298
+ clock: [
299
+ '................',
300
+ '.......##.......',
301
+ '....########....',
302
+ '...##......##...',
303
+ '..##...##...##..',
304
+ '..#....##....#..',
305
+ '..#....##....#..',
306
+ '.##....####..##.',
307
+ '.##....####..##.',
308
+ '..#..........#..',
309
+ '..#..........#..',
310
+ '..##........##..',
311
+ '...##......##...',
312
+ '....########....',
313
+ '.......##.......',
314
+ '................',
315
+ ],
316
+ close: [
317
+ '................',
318
+ '................',
319
+ '.###........###.',
320
+ '..###......###..',
321
+ '...###....###...',
322
+ '....###..###....',
323
+ '.....######.....',
324
+ '......####......',
325
+ '......####......',
326
+ '.....######.....',
327
+ '....###..###....',
328
+ '...###....###...',
329
+ '..###......###..',
330
+ '.###........###.',
331
+ '................',
332
+ '................',
333
+ ],
334
+ download: [
335
+ '................',
336
+ '.......##.......',
337
+ '.......##.......',
338
+ '.......##.......',
339
+ '.......##.......',
340
+ '.......##.......',
341
+ '...#########....',
342
+ '....#######.....',
343
+ '.....#####......',
344
+ '......###.......',
345
+ '.......#........',
346
+ '................',
347
+ '.##############.',
348
+ '.##############.',
349
+ '................',
350
+ '................',
351
+ ],
352
+ edit: [
353
+ '................',
354
+ '...........####.',
355
+ '..........#####.',
356
+ '.........#####..',
357
+ '........#####...',
358
+ '.......#####....',
359
+ '......#####.....',
360
+ '.....#####......',
361
+ '....#####.......',
362
+ '...#####........',
363
+ '..#####.........',
364
+ '.#####..........',
365
+ '.####...........',
366
+ '.###............',
367
+ '.#..............',
368
+ '................',
369
+ ],
370
+ eye: [
371
+ '................',
372
+ '................',
373
+ '................',
374
+ '....########....',
375
+ '..####....####..',
376
+ '.###..####..###.',
377
+ '.##..######..##.',
378
+ '##..########..##',
379
+ '.##..######..##.',
380
+ '.###..####..###.',
381
+ '..####....####..',
382
+ '....########....',
383
+ '................',
384
+ '................',
385
+ '................',
386
+ '................',
387
+ ],
388
+ 'eye-off': [
389
+ '................',
390
+ '................',
391
+ '#...............',
392
+ '.#..########....',
393
+ '..####....####..',
394
+ '.###.#####..###.',
395
+ '.##..###.##..##.',
396
+ '##..####.###..##',
397
+ '.##..######.#.#.',
398
+ '.###..####..###.',
399
+ '..####....####.#',
400
+ '....########.#..',
401
+ '............#...',
402
+ '...........#....',
403
+ '................',
404
+ '................',
405
+ ],
406
+ file: [
407
+ '................',
408
+ '..##########....',
409
+ '..##......###...',
410
+ '..##......####..',
411
+ '..##......#.##..',
412
+ '..##......####..',
413
+ '..##........##..',
414
+ '..##........##..',
415
+ '..##........##..',
416
+ '..##........##..',
417
+ '..##........##..',
418
+ '..##........##..',
419
+ '..############..',
420
+ '................',
421
+ '................',
422
+ '................',
423
+ ],
424
+ folder: [
425
+ '................',
426
+ '................',
427
+ '..#####.........',
428
+ '.#######........',
429
+ '.##############.',
430
+ '.##############.',
431
+ '.##..........##.',
432
+ '.##..........##.',
433
+ '.##..........##.',
434
+ '.##..........##.',
435
+ '.##..........##.',
436
+ '.##############.',
437
+ '................',
438
+ '................',
439
+ '................',
440
+ '................',
441
+ ],
442
+ gear: [
443
+ '.......##.......',
444
+ '.......##.......',
445
+ '......####......',
446
+ '....########....',
447
+ '...##########...',
448
+ '...###....###...',
449
+ '..###......###..',
450
+ '#####......#####',
451
+ '#####......#####',
452
+ '..###......###..',
453
+ '...###....###...',
454
+ '...##########...',
455
+ '....########....',
456
+ '......####......',
457
+ '.......##.......',
458
+ '.......##.......',
459
+ ],
460
+ grid: [
461
+ '................',
462
+ '..#####..#####..',
463
+ '..#####..#####..',
464
+ '..#####..#####..',
465
+ '..#####..#####..',
466
+ '..#####..#####..',
467
+ '................',
468
+ '................',
469
+ '..#####..#####..',
470
+ '..#####..#####..',
471
+ '..#####..#####..',
472
+ '..#####..#####..',
473
+ '..#####..#####..',
474
+ '................',
475
+ '................',
476
+ '................',
477
+ ],
478
+ heart: [
479
+ '................',
480
+ '...###....###...',
481
+ '..#####..#####..',
482
+ '.######..######.',
483
+ '.##############.',
484
+ '.##############.',
485
+ '..############..',
486
+ '..############..',
487
+ '...##########...',
488
+ '....########....',
489
+ '.....######.....',
490
+ '......####......',
491
+ '.......##.......',
492
+ '................',
493
+ '................',
494
+ '................',
495
+ ],
496
+ home: [
497
+ '................',
498
+ '.......##.......',
499
+ '......####......',
500
+ '.....######.....',
501
+ '....########....',
502
+ '...##########...',
503
+ '..############..',
504
+ '.##############.',
505
+ '..############..',
506
+ '..##........##..',
507
+ '..##..####..##..',
508
+ '..##..####..##..',
509
+ '..##..####..##..',
510
+ '..############..',
511
+ '................',
512
+ '................',
513
+ ],
514
+ info: [
515
+ '................',
516
+ '......####......',
517
+ '.....######.....',
518
+ '.....######.....',
519
+ '......####......',
520
+ '................',
521
+ '....######......',
522
+ '......####......',
523
+ '......####......',
524
+ '......####......',
525
+ '......####......',
526
+ '....##########..',
527
+ '....##########..',
528
+ '................',
529
+ '................',
530
+ '................',
531
+ ],
532
+ link: [
533
+ '................',
534
+ '................',
535
+ '.....######.....',
536
+ '....########....',
537
+ '...###....###...',
538
+ '..###......###..',
539
+ '..##........##..',
540
+ '....######......',
541
+ '......######....',
542
+ '..##........##..',
543
+ '..###......###..',
544
+ '...###....###...',
545
+ '....########....',
546
+ '.....######.....',
547
+ '................',
548
+ '................',
549
+ ],
550
+ lock: [
551
+ '................',
552
+ '.....######.....',
553
+ '....##....##....',
554
+ '....##....##....',
555
+ '...##......##...',
556
+ '...##......##...',
557
+ '..############..',
558
+ '..############..',
559
+ '..####....####..',
560
+ '..###......###..',
561
+ '..###......###..',
562
+ '..####....####..',
563
+ '..############..',
564
+ '..############..',
565
+ '................',
566
+ '................',
567
+ ],
568
+ mail: [
569
+ '................',
570
+ '................',
571
+ '.##############.',
572
+ '.##..........##.',
573
+ '.#.##......##.#.',
574
+ '.#..##....##..#.',
575
+ '.#...##..##...#.',
576
+ '.#....####....#.',
577
+ '.#.....##.....#.',
578
+ '.#............#.',
579
+ '.#............#.',
580
+ '.##############.',
581
+ '................',
582
+ '................',
583
+ '................',
584
+ '................',
585
+ ],
586
+ menu: [
587
+ '................',
588
+ '................',
589
+ '.##############.',
590
+ '.##############.',
591
+ '................',
592
+ '................',
593
+ '.##############.',
594
+ '.##############.',
595
+ '................',
596
+ '................',
597
+ '.##############.',
598
+ '.##############.',
599
+ '................',
600
+ '................',
601
+ '................',
602
+ '................',
603
+ ],
604
+ minus: [
605
+ '................',
606
+ '................',
607
+ '................',
608
+ '................',
609
+ '................',
610
+ '................',
611
+ '..############..',
612
+ '..############..',
613
+ '..############..',
614
+ '..############..',
615
+ '................',
616
+ '................',
617
+ '................',
618
+ '................',
619
+ '................',
620
+ '................',
621
+ ],
622
+ moon: [
623
+ '................',
624
+ '......##........',
625
+ '....###.........',
626
+ '...###..........',
627
+ '..####..........',
628
+ '..####..........',
629
+ '.#####..........',
630
+ '.#####..........',
631
+ '.#####..........',
632
+ '.######.........',
633
+ '..######........',
634
+ '..#######.......',
635
+ '...##########...',
636
+ '....########....',
637
+ '......####......',
638
+ '................',
639
+ ],
640
+ 'more-horizontal': [
641
+ '................',
642
+ '................',
643
+ '................',
644
+ '................',
645
+ '................',
646
+ '................',
647
+ '................',
648
+ '.##...##...##...',
649
+ '.##...##...##...',
650
+ '................',
651
+ '................',
652
+ '................',
653
+ '................',
654
+ '................',
655
+ '................',
656
+ '................',
657
+ ],
658
+ 'more-vertical': [
659
+ '................',
660
+ '.......##.......',
661
+ '.......##.......',
662
+ '................',
663
+ '................',
664
+ '.......##.......',
665
+ '.......##.......',
666
+ '................',
667
+ '................',
668
+ '.......##.......',
669
+ '.......##.......',
670
+ '................',
671
+ '................',
672
+ '................',
673
+ '................',
674
+ '................',
675
+ ],
676
+ pause: [
677
+ '................',
678
+ '...###....###...',
679
+ '...###....###...',
680
+ '...###....###...',
681
+ '...###....###...',
682
+ '...###....###...',
683
+ '...###....###...',
684
+ '...###....###...',
685
+ '...###....###...',
686
+ '...###....###...',
687
+ '...###....###...',
688
+ '...###....###...',
689
+ '...###....###...',
690
+ '...###....###...',
691
+ '................',
692
+ '................',
693
+ ],
694
+ play: [
695
+ '................',
696
+ '....#...........',
697
+ '....##..........',
698
+ '....###.........',
699
+ '....####........',
700
+ '....#####.......',
701
+ '....######......',
702
+ '....#######.....',
703
+ '....#######.....',
704
+ '....######......',
705
+ '....#####.......',
706
+ '....####........',
707
+ '....###.........',
708
+ '....##..........',
709
+ '....#...........',
710
+ '................',
711
+ ],
712
+ plus: [
713
+ '................',
714
+ '................',
715
+ '.......##.......',
716
+ '.......##.......',
717
+ '.......##.......',
718
+ '.......##.......',
719
+ '..############..',
720
+ '..############..',
721
+ '..############..',
722
+ '..############..',
723
+ '.......##.......',
724
+ '.......##.......',
725
+ '.......##.......',
726
+ '.......##.......',
727
+ '................',
728
+ '................',
729
+ ],
730
+ refresh: [
731
+ '.....##.........',
732
+ '.....####.......',
733
+ '.....###........',
734
+ '.....##.........',
735
+ '.....#....#.....',
736
+ '....##....##....',
737
+ '...##......##...',
738
+ '...#........#...',
739
+ '...#........#...',
740
+ '...#........#...',
741
+ '...##......##...',
742
+ '....##....##....',
743
+ '.....######.....',
744
+ '......####......',
745
+ '................',
746
+ '................',
747
+ ],
748
+ search: [
749
+ '................',
750
+ '...######.......',
751
+ '..########......',
752
+ '.###....###.....',
753
+ '.##......##.....',
754
+ '.##......##.....',
755
+ '.##......##.....',
756
+ '.###....###.....',
757
+ '..########......',
758
+ '...#######......',
759
+ '......#####.....',
760
+ '.......#####....',
761
+ '........#####...',
762
+ '.........###....',
763
+ '................',
764
+ '................',
765
+ ],
766
+ spark: [
767
+ '................',
768
+ '.......**.......',
769
+ '.......**.......',
770
+ '.......**.......',
771
+ '.......**.......',
772
+ '.......**.......',
773
+ '......****......',
774
+ '.******##******.',
775
+ '.******##******.',
776
+ '......****......',
777
+ '.......**.......',
778
+ '.......**.......',
779
+ '.......**.......',
780
+ '.......**.......',
781
+ '.......**.......',
782
+ '................',
783
+ ],
784
+ star: [
785
+ '................',
786
+ '.......##.......',
787
+ '.......##.......',
788
+ '......####......',
789
+ '......####......',
790
+ '.##############.',
791
+ '..############..',
792
+ '...##########...',
793
+ '....########....',
794
+ '....########....',
795
+ '...####..####...',
796
+ '..####....####..',
797
+ '..###......###..',
798
+ '................',
799
+ '................',
800
+ '................',
801
+ ],
802
+ sun: [
803
+ '.......##.......',
804
+ '.......##.......',
805
+ '..#..........#..',
806
+ '...#........#...',
807
+ '................',
808
+ '......####......',
809
+ '.....######.....',
810
+ '##...######...##',
811
+ '##...######...##',
812
+ '.....######.....',
813
+ '......####......',
814
+ '................',
815
+ '...#........#...',
816
+ '..#..........#..',
817
+ '.......##.......',
818
+ '.......##.......',
819
+ ],
820
+ trash: [
821
+ '................',
822
+ '......####......',
823
+ '....########....',
824
+ '.##############.',
825
+ '................',
826
+ '..############..',
827
+ '..###.##.##.##..',
828
+ '..###.##.##.##..',
829
+ '..###.##.##.##..',
830
+ '..###.##.##.##..',
831
+ '..###.##.##.##..',
832
+ '..###.##.##.##..',
833
+ '..###.##.##.##..',
834
+ '..############..',
835
+ '................',
836
+ '................',
837
+ ],
838
+ upload: [
839
+ '................',
840
+ '.......#........',
841
+ '......###.......',
842
+ '.....#####......',
843
+ '....#######.....',
844
+ '...#########....',
845
+ '.......##.......',
846
+ '.......##.......',
847
+ '.......##.......',
848
+ '.......##.......',
849
+ '.......##.......',
850
+ '................',
851
+ '.##############.',
852
+ '.##############.',
853
+ '................',
854
+ '................',
855
+ ],
856
+ user: [
857
+ '................',
858
+ '.....######.....',
859
+ '....########....',
860
+ '....########....',
861
+ '....########....',
862
+ '.....######.....',
863
+ '................',
864
+ '..############..',
865
+ '.##############.',
866
+ '###..........###',
867
+ '###..........###',
868
+ '###..........###',
869
+ '##............##',
870
+ '................',
871
+ '................',
872
+ '................',
873
+ ],
874
+ warning: [
875
+ '................',
876
+ '.......##.......',
877
+ '.......##.......',
878
+ '......####......',
879
+ '......####......',
880
+ '.....##..##.....',
881
+ '.....##..##.....',
882
+ '....###..###....',
883
+ '....##....##....',
884
+ '...###....###...',
885
+ '...##.####.##...',
886
+ '..###.####.###..',
887
+ '..##........##..',
888
+ '.###############',
889
+ '.###############',
890
+ '................',
891
+ ],
892
+ };
893
+
894
+ /** The frozen name→bitmap registry (keys sorted, every row frozen). */
895
+ export const GLYPHS = Object.freeze(
896
+ Object.fromEntries(
897
+ Object.keys(RAW)
898
+ .sort()
899
+ .map((name) => [name, Object.freeze(RAW[name])]),
900
+ ),
901
+ );
902
+
903
+ /** Every glyph name, frozen and sorted (mirrors GLYPHS keys). */
904
+ export const GLYPH_NAMES = Object.freeze(Object.keys(GLYPHS));
905
+
906
+ const TONE = { '#': 'hot', '*': 'accent' };
907
+
908
+ /** The raw bitmap rows for a glyph, or `undefined` if the name is unknown. */
909
+ export function glyph(name) {
910
+ return GLYPHS[name];
911
+ }
912
+
913
+ /** GLYPH_SIZE² cell descriptors (row-major), or `[]` if the name is unknown. */
914
+ export function glyphCells(name) {
915
+ const rows = GLYPHS[name];
916
+ if (!rows) return [];
917
+ const cells = [];
918
+ for (const row of rows) {
919
+ for (const ch of row) {
920
+ if (ch === '.') cells.push({ on: false });
921
+ else cells.push({ on: true, tone: TONE[ch] });
922
+ }
923
+ }
924
+ return cells;
925
+ }
926
+
927
+ const CELL = 'ui-dotmatrix__cell';
928
+
929
+ function cellClass(cell) {
930
+ if (!cell.on) return CELL;
931
+ if (cell.tone === 'hot') return `${CELL} ${CELL}--hot`;
932
+ if (cell.tone === 'accent') return `${CELL} ${CELL}--accent`;
933
+ return CELL;
934
+ }
935
+
936
+ function esc(s) {
937
+ return String(s)
938
+ .replace(/&/g, '&')
939
+ .replace(/</g, '&lt;')
940
+ .replace(/>/g, '&gt;')
941
+ .replace(/"/g, '&quot;');
942
+ }
943
+
944
+ // `dot`/`gap` land in an inline-CSS context (`style="--dotmatrix-dot:VALUE"`),
945
+ // where HTML-escaping a `"` stops attribute breakout but a `;` would still open
946
+ // a second CSS declaration (overlay/clickjacking, selector exfil). So restrict
947
+ // them to length/calc syntax — digits, units, %, whitespace and `()+-*/.,` for
948
+ // calc()/clamp()/var() — and drop anything else rather than emit it.
949
+ function cssLen(v) {
950
+ return /^[\w.%+\-*/()\s,]+$/.test(v) ? v : '';
951
+ }
952
+
953
+ /**
954
+ * Build a CSS `mask-image` `url()` of an inline SVG for a glyph's lit cells —
955
+ * the basis of the one-node `render: 'mask'` path. Horizontal runs are merged
956
+ * into single `<rect>`s (fewer nodes, no seams). The SVG is alpha-masked, so
957
+ * the element's `currentColor` background shows through only the lit cells —
958
+ * one DOM node instead of GLYPH_SIZE² (256), for an icon at any size/colour.
959
+ */
960
+ function maskUrl(rows) {
961
+ let rects = '';
962
+ rows.forEach((row, y) => {
963
+ let run = 0;
964
+ for (let x = 0; x <= GLYPH_SIZE; x++) {
965
+ const on = x < GLYPH_SIZE && row[x] !== '.';
966
+ if (on) run++;
967
+ else if (run) {
968
+ rects += `<rect x='${x - run}' y='${y}' width='${run}' height='1'/>`;
969
+ run = 0;
970
+ }
971
+ }
972
+ });
973
+ // The url() lands unquoted inside an HTML `style="…"` attribute, so it must
974
+ // carry no spaces or quotes of its own — encode them (and `<>#`) so it stays
975
+ // a single CSS url-token and can't break out of the attribute.
976
+ const svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 ${GLYPH_SIZE} ${GLYPH_SIZE}'>${rects}</svg>`;
977
+ const enc = svg
978
+ .replace(/%/g, '%25')
979
+ .replace(/</g, '%3C')
980
+ .replace(/>/g, '%3E')
981
+ .replace(/#/g, '%23')
982
+ .replace(/'/g, '%27')
983
+ .replace(/ /g, '%20');
984
+ return `url(data:image/svg+xml,${enc})`;
985
+ }
986
+
987
+ /**
988
+ * A full `.ui-dotmatrix` HTML string for a glyph (`''` if the name is
989
+ * unknown). Decorative by default (`aria-hidden`); pass `label` to expose it
990
+ * as `role="img"`. Pins `--dotmatrix-cols` to GLYPH_SIZE so the square layout
991
+ * holds regardless of the 12-col default; `dot`/`gap` set `--dotmatrix-dot` /
992
+ * `--dotmatrix-gap` (sanitized to a CSS length/calc allowlist; a malformed
993
+ * value is dropped). `grid: false` drops the unlit panel dots (glyph-only).
994
+ *
995
+ * `solid: true` renders the lit cells as square, gapless pixels (a filled
996
+ * silhouette) instead of separated dots — the legible mode at small/inline
997
+ * sizes (~16–24px) where the dot look fragments. It implies glyph-only.
998
+ *
999
+ * `anim` opts into a decorative animation (disabled under reduced-motion;
1000
+ * the static frame + `label` still carry the meaning): `'reveal'` powers the
1001
+ * cells on in a scan, `'pulse'` makes the glyph breathe.
1002
+ *
1003
+ * `render: 'mask'` returns a single `.ui-icon` element masked by the glyph
1004
+ * bitmap (one DOM node, not GLYPH_SIZE²) — the icon-at-scale path: it sizes to
1005
+ * `size` (or `--icon-size` / `1em`) and inherits `currentColor`. The
1006
+ * cell-mode options (grid/solid/anim/dot/gap) don't apply; `label` does.
1007
+ * Needs `@ponchia/ui/css` (the `.ui-icon` rule).
1008
+ */
1009
+ export function renderGlyph(name, options = {}) {
1010
+ const rows = GLYPHS[name];
1011
+ if (!rows) return '';
1012
+ const { grid = true, solid = false, anim, label, dot, gap, render, size } = options;
1013
+
1014
+ // One-node icon: a single `.ui-icon` span masked by the glyph's bitmap, so
1015
+ // it scales to any font-size and inherits `currentColor` — for icon-at-scale
1016
+ // (e.g. one in every table row) where the GLYPH_SIZE²-cell path is too heavy.
1017
+ if (render === 'mask') {
1018
+ const a11yM = label ? `role="img" aria-label="${esc(label)}"` : 'aria-hidden="true"';
1019
+ const sz = size && cssLen(size) ? `--icon-size:${cssLen(size)};` : '';
1020
+ return `<span class="ui-icon" style="${sz}--icon-mask:${maskUrl(rows)}" ${a11yM}></span>`;
1021
+ }
1022
+
1023
+ const cells = glyphCells(name);
1024
+
1025
+ const style = [`--dotmatrix-cols:${GLYPH_SIZE}`];
1026
+ const dotLen = dot && cssLen(dot);
1027
+ const gapLen = gap && cssLen(gap);
1028
+ if (dotLen) style.push(`--dotmatrix-dot:${dotLen}`);
1029
+ // Solid mode fuses the dots into a crisp pixel glyph: square cells, no gap.
1030
+ if (solid) style.push('--dotmatrix-dot-radius:0', '--dotmatrix-gap:0');
1031
+ else if (gapLen) style.push(`--dotmatrix-gap:${gapLen}`);
1032
+
1033
+ const cls =
1034
+ anim === 'reveal'
1035
+ ? 'ui-dotmatrix ui-dotmatrix--reveal'
1036
+ : anim === 'pulse'
1037
+ ? 'ui-dotmatrix ui-dotmatrix--pulse'
1038
+ : 'ui-dotmatrix';
1039
+ // `reveal` staggers each cell by its row-major index via `--i`.
1040
+ const stagger = anim === 'reveal';
1041
+
1042
+ const a11y = label ? `role="img" aria-label="${esc(label)}"` : 'aria-hidden="true"';
1043
+
1044
+ // Off cells keep the cell class (so they hold their grid track and 1:1
1045
+ // aspect-ratio); `grid: false` (implied by `solid`) only drops their lit
1046
+ // background, for the glyph-only look, without collapsing all-off rows.
1047
+ const showPanel = grid && !solid;
1048
+ const inner = cells
1049
+ .map((c, i) => {
1050
+ const cl = c.on ? cellClass(c) : 'ui-dotmatrix__cell';
1051
+ const cellStyle = [];
1052
+ if (!c.on && !showPanel) cellStyle.push('background:transparent');
1053
+ if (stagger) cellStyle.push(`--i:${i}`);
1054
+ const s = cellStyle.length ? ` style="${cellStyle.join(';')}"` : '';
1055
+ return `<span class="${cl}"${s}></span>`;
1056
+ })
1057
+ .join('');
1058
+
1059
+ // A `<span>` (not `<div>`): it's phrasing content, so the glyph is valid
1060
+ // inline and inside a `<button>` (its content model is phrasing-only) — the
1061
+ // inline-icon use. `.ui-dotmatrix`'s `display: grid` works on a span.
1062
+ return `<span class="${cls}" style="${style.join(';')}" ${a11y}>${inner}</span>`;
1063
+ }