@apliteni/apliteni-ui 0.7.2 → 0.8.1

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.
@@ -9,93 +9,106 @@
9
9
  // iconCategories drives the Storybook grid. Add a glyph to the right group.
10
10
 
11
11
  const NAV = {
12
- chevronDown: '<path d="m6 9 6 6 6-6"/>',
13
- chevronUp: '<path d="m18 15-6-6-6 6"/>',
14
- chevronLeft: '<path d="m15 6-6 6 6 6"/>',
15
- chevronRight: '<path d="m9 6 6 6-6 6"/>',
16
- arrowRight: '<path d="M5 12h14M13 6l6 6-6 6"/>',
17
- arrowLeft: '<path d="M19 12H5M11 6l-6 6 6 6"/>',
18
- arrowUp: '<path d="M12 19V5M6 11l6-6 6 6"/>',
19
- arrowDown: '<path d="M12 5v14M6 13l6 6 6-6"/>',
20
- menu: '<path d="M3 6h18M3 12h18M3 18h18"/>',
21
- x: '<path d="M18 6 6 18M6 6l12 12"/>',
22
- externalLink: '<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>',
23
- home: '<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/>',
24
- search: '<circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/>',
25
- compass: '<circle cx="12" cy="12" r="9"/><path d="m15 9-2 6-4 2 2-6z"/>',
26
- globe: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a15 15 0 0 1 0 18 15 15 0 0 1 0-18z"/>',
12
+ chevronDown: '<path d="M6 9l6 6 6-6" stroke-linecap="round" stroke-linejoin="round"/>',
13
+ chevronUp: '<polyline points="18 15 12 9 6 15"/>',
14
+ chevronLeft: '<polyline points="15 18 9 12 15 6"/>',
15
+ chevronRight: '<path d="M9 18l6-6-6-6" stroke-linecap="round" stroke-linejoin="round"/>',
16
+ arrowRight: '<path d="M5 12h14M12 5l7 7-7 7" stroke-linecap="round" stroke-linejoin="round"/>',
17
+ arrowLeft: '<path d="M19 12H5M12 19l-7-7 7-7" stroke-linecap="round" stroke-linejoin="round"/>',
18
+ arrowUp: '<line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/>',
19
+ arrowDown: '<line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/>',
20
+ menu: '<path d="M3 12h18M3 6h18M3 18h18" stroke-linecap="round"/>',
21
+ x: '<path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/>',
22
+ externalLink: '<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/>',
23
+ home: '<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/>',
24
+ search: '<path d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z" stroke-linecap="round"/>',
25
+ compass: '<circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/>',
26
+ globe: '<circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>',
27
27
  };
28
28
 
29
29
  const ACTIONS = {
30
- plus: '<path d="M12 5v14M5 12h14"/>',
31
- minus: '<path d="M5 12h14"/>',
32
- check: '<path d="M20 6 9 17l-5-5"/>',
33
- edit: '<path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/>',
34
- copy: '<rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/>',
35
- trash: '<path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M6 6v14a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V6"/><path d="M10 11v6M14 11v6"/>',
36
- download: '<path d="M12 3v12"/><path d="m7 10 5 5 5-5"/><path d="M5 21h14"/>',
37
- upload: '<path d="M12 21V9"/><path d="m7 14 5-5 5 5"/><path d="M5 3h14"/>',
38
- share: '<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.6 13.5 6.8 4M15.4 6.5l-6.8 4"/>',
39
- refresh: '<path d="M21 12a9 9 0 1 1-2.6-6.3L21 8"/><path d="M21 3v5h-5"/>',
40
- filter: '<path d="M3 4h18l-7 8v6l-4 2v-8z"/>',
41
- gear: '<circle cx="12" cy="12" r="3"/><path d="M19.4 13a7.9 7.9 0 0 0 0-2l1.6-1.3-1.6-2.7-1.9.8a7.6 7.6 0 0 0-1.7-1L15.5 5h-3l-.3 1.8a7.6 7.6 0 0 0-1.7 1l-1.9-.8-1.6 2.7L8.6 11a7.9 7.9 0 0 0 0 2l-1.6 1.3 1.6 2.7 1.9-.8a7.6 7.6 0 0 0 1.7 1l.3 1.8h3l.3-1.8a7.6 7.6 0 0 0 1.7-1l1.9.8 1.6-2.7z"/>',
42
- link: '<path d="M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1"/><path d="M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1"/>',
43
- moreHorizontal: '<circle cx="5" cy="12" r="1.3"/><circle cx="12" cy="12" r="1.3"/><circle cx="19" cy="12" r="1.3"/>',
44
- moreVertical: '<circle cx="12" cy="5" r="1.3"/><circle cx="12" cy="12" r="1.3"/><circle cx="12" cy="19" r="1.3"/>',
30
+ plus: '<path d="M12 5v14M5 12h14" stroke-linecap="round"/>',
31
+ minus: '<path d="M5 12h14" stroke-linecap="round"/>',
32
+ check: '<path d="M20 6L9 17l-5-5" stroke-linecap="round" stroke-linejoin="round"/>',
33
+ edit: '<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>',
34
+ copy: '<rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>',
35
+ trash: '<path d="M3 6h18M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>',
36
+ download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/>',
37
+ upload: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M17 8l-5-5-5 5"/><path d="M12 3v12"/>',
38
+ share: '<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>',
39
+ refresh: '<path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>',
40
+ filter: '<path d="M22 3H2l8 9.46V19l4 2v-8.54L22 3z"/>',
41
+ gear: '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>',
42
+ link: '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>',
43
+ moreHorizontal: '<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>',
44
+ moreVertical: '<circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/>',
45
+ code: '<path d="M16 18l6-6-6-6M8 6l-6 6 6 6"/>',
46
+ droplet: '<path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z"/>',
47
+ type: '<polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/>',
45
48
  };
46
49
 
47
50
  const STATUS = {
48
- circleCheck: '<circle cx="12" cy="12" r="9"/><path d="m8.5 12 2.5 2.5 5-5"/>',
49
- circleAlert: '<circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/>',
50
- alert: '<path d="M12 3 2 20h20z"/><path d="M12 9v5M12 17h.01"/>',
51
- info: '<circle cx="12" cy="12" r="9"/><path d="M12 8h.01M11 12h1v4h1"/>',
52
- bell: '<path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/>',
53
- clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
54
- eye: '<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/>',
55
- eyeOff: '<path d="M9.9 4.2A9.6 9.6 0 0 1 12 4c6.5 0 10 8 10 8a13.2 13.2 0 0 1-2.2 2.9M6.6 6.6A13.2 13.2 0 0 0 2 12s3.5 7 10 7a9.6 9.6 0 0 0 4-.9"/><path d="M9.9 9.9a3 3 0 0 0 4.2 4.2"/><path d="m2 2 20 20"/>',
56
- lock: '<rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>',
57
- unlock: '<rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 7.5-2"/>',
58
- shield: '<path d="M12 3 4 6v6c0 5 3.5 8 8 9 4.5-1 8-4 8-9V6z"/>',
59
- key: '<rect x="4" y="10" width="16" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/>',
60
- star: '<path d="m12 3 2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 18l-5.8 3 1.1-6.5L2.6 9.8l6.5-.9z"/>',
61
- heart: '<path d="M12 21s-7-4.5-9.4-9A5 5 0 0 1 12 6a5 5 0 0 1 9.4 6c-2.4 4.5-9.4 9-9.4 9z"/>',
51
+ circleCheck: '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="M22 4L12 14.01l-3-3"/>',
52
+ circleAlert: '<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>',
53
+ alert: '<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path stroke-linecap="round" d="M12 9v4"/><path stroke-linecap="round" d="M12 17h.01"/>',
54
+ info: '<circle cx="12" cy="12" r="10"/><path stroke-linecap="round" d="M12 16v-4"/><path stroke-linecap="round" d="M12 8h.01"/>',
55
+ bell: '<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/>',
56
+ clock: '<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>',
57
+ eye: '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>',
58
+ eyeOff: '<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/>',
59
+ lock: '<rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
60
+ unlock: '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/>',
61
+ shield: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>',
62
+ key: '<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>',
63
+ star: '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>',
64
+ heart: '<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>',
65
+ circleX: '<circle cx="12" cy="12" r="10"/><path d="M15 9l-6 6M9 9l6 6"/>',
66
+ target: '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>',
67
+ sun: '<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>',
68
+ moon: '<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>',
62
69
  };
63
70
 
64
71
  const DATA = {
65
- card: '<rect x="2" y="5" width="20" height="14" rx="2"/><path d="M2 10h20M6 15h4"/>',
66
- wallet: '<path d="M4 6a2 2 0 0 1 2-2h11a1 1 0 0 1 1 1v3"/><path d="M4 6v12a2 2 0 0 0 2 2h13a1 1 0 0 0 1-1v-3"/><path d="M21 11h-4a2 2 0 0 0 0 4h4a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1z"/>',
67
- chart: '<path d="M3 3v18h18"/><path d="M8 17v-5M13 17V8M18 17v-8"/>',
68
- trendingUp: '<path d="m3 17 7-7 4 4 7-7"/><path d="M17 7h4v4"/>',
69
- trendingDown: '<path d="m3 7 7 7 4-4 7 7"/><path d="M17 17h4v-4"/>',
70
- table: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 10h18M3 15h18M9 4v16M15 4v16"/>',
71
- database: '<ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v6c0 1.7 3.6 3 8 3s8-1.3 8-3V5"/><path d="M4 11v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6"/>',
72
- layers: '<path d="m12 2 9 5-9 5-9-5z"/><path d="m3 12 9 5 9-5M3 17l9 5 9-5"/>',
73
- cube: '<path d="M12 2 3 7v10l9 5 9-5V7z"/><path d="M3 7l9 5 9-5M12 12v10"/>',
74
- bolt: '<path d="M13 2 3 14h8l-1 8 10-12h-8z"/>',
72
+ card: '<rect x="1" y="4" width="22" height="16" rx="2" ry="2"/><line x1="1" y1="10" x2="23" y2="10"/>',
73
+ wallet: '<path d="M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1"/><path d="M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4"/>',
74
+ chart: '<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>',
75
+ trendingUp: '<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/>',
76
+ trendingDown: '<polyline points="23 18 13.5 8.5 8.5 13.5 1 6"/><polyline points="17 18 23 18 23 12"/>',
77
+ table: '<path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"/>',
78
+ database: '<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>',
79
+ layers: '<polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/>',
80
+ cube: '<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>',
81
+ bolt: '<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>',
82
+ grid: '<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>',
83
+ layout: '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/>',
84
+ monitor: '<rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>',
75
85
  };
76
86
 
77
87
  const FILES = {
78
- doc: '<path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6M8 13h8M8 17h5"/>',
79
- folder: '<path d="M4 5h5l2 2h9a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z"/>',
80
- image: '<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/>',
81
- play: '<path d="M7 4v16l13-8z"/>',
82
- pause: '<rect x="7" y="4" width="3.5" height="16" rx="1"/><rect x="13.5" y="4" width="3.5" height="16" rx="1"/>',
83
- calendar: '<rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 9h18M8 3v4M16 3v4"/>',
88
+ doc: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>',
89
+ folder: '<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>',
90
+ image: '<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/>',
91
+ play: '<polygon points="5 3 19 12 5 21 5 3"/>',
92
+ pause: '<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>',
93
+ calendar: '<rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/>',
84
94
  };
85
95
 
86
96
  const COMMS = {
87
- mail: '<rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/>',
88
- chat: '<path d="M21 15a2 2 0 0 1-2 2H8l-4 4V5a2 2 0 0 1 2-2h13a2 2 0 0 1 2 2z"/>',
89
- phone: '<path d="M6 3h3l2 5-2.5 1.5a11 11 0 0 0 5 5L16 13l5 2v3a2 2 0 0 1-2 2A16 16 0 0 1 3 5a2 2 0 0 1 2-2z"/>',
90
- user: '<circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/>',
91
- logout: '<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="M16 17l5-5-5-5"/><path d="M21 12H9"/>',
92
- plug: '<path d="M9 2v6M15 2v6M7 8h10v3a5 5 0 0 1-10 0z"/><path d="M12 16v6"/>',
93
- sparkle: '<path d="M12 3v4M12 17v4M3 12h4M17 12h4M6 6l2 2M16 16l2 2M18 6l-2 2M8 16l-2 2"/>',
97
+ mail: '<path d="M4 4h16c1.1 0 2 .9 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2z"/><path d="M22 6l-10 7L2 6"/>',
98
+ chat: '<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>',
99
+ phone: '<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>',
100
+ user: '<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
101
+ logout: '<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/>',
102
+ plug: '<path d="M12 22v-5"/><path d="M15 8V2"/><path d="M17 8a1 1 0 0 1 1 1v4a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1z"/><path d="M9 8V2"/>',
103
+ sparkle: '<path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/><path d="M20 2v4"/><path d="M22 4h-4"/><circle cx="4" cy="20" r="2"/>',
104
+ card: '<rect x="1" y="4" width="22" height="16" rx="2" ry="2"/><line x1="1" y1="10" x2="23" y2="10"/>',
105
+ doc: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>',
106
+ chart: '<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>',
94
107
  };
95
108
 
96
109
  const BRAND = {
97
- github: '<path d="M9 19c-4 1.4-4-2.2-6-2.6m12 5v-3.4a3 3 0 0 0-.8-2.3c2.7-.3 5.5-1.3 5.5-6a4.6 4.6 0 0 0-1.3-3.2 4.3 4.3 0 0 0-.1-3.2s-1-.3-3.4 1.3a11.6 11.6 0 0 0-6 0C6.9 2.7 5.9 3 5.9 3a4.3 4.3 0 0 0-.1 3.2A4.6 4.6 0 0 0 4.5 9.4c0 4.6 2.8 5.6 5.5 6a3 3 0 0 0-.8 2.3V21"/>',
98
- linkedin: '<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-4 0v7h-4v-7a6 6 0 0 1 6-6z"/><rect x="2" y="9" width="4" height="12"/><circle cx="4" cy="4" r="2"/>',
110
+ github: '<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/>',
111
+ linkedin: '<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"/><rect x="2" y="9" width="4" height="12"/><circle cx="4" cy="4" r="2"/>',
99
112
  };
100
113
 
101
114
  const ICONS = { ...NAV, ...ACTIONS, ...STATUS, ...DATA, ...FILES, ...COMMS, ...BRAND };
@@ -112,10 +125,20 @@ export const iconCategories = [
112
125
  { name: 'Brand', names: Object.keys(BRAND) },
113
126
  ];
114
127
 
128
+ // Every glyph the kit ships is DECORATIVE: it repeats meaning the control's own
129
+ // text or aria-label already carries. So icon() always emits `aria-hidden="true"`
130
+ // (never announced) + `focusable="false"` (never a stray tab stop in legacy IE/
131
+ // Edge), exactly like the hand-written SVGs elsewhere in the kit.
132
+ //
133
+ // The corollary is a rule, not a suggestion: an icon-only control must name
134
+ // itself — button({ iconOnly }) does it from `label`, everything else needs an
135
+ // explicit aria-label. There is no `icon(name, cls, label)` escape hatch on
136
+ // purpose; a named graphic would let a caller hang a control's only accessible
137
+ // name off a decorative glyph, which is what we just fixed.
115
138
  export const icon = (name, cls = '') =>
116
- `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"${cls ? ` class="${cls}"` : ''}>${ICONS[name] || ''}</svg>`;
139
+ `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"${cls ? ` class="${cls}"` : ''}>${ICONS[name] || ''}</svg>`;
117
140
 
118
141
  export const iconNames = Object.keys(ICONS);
119
142
 
120
- export const sun = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>';
121
- export const moon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>';
143
+ export const sun = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>';
144
+ export const moon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>';
@@ -10,9 +10,11 @@
10
10
  // ship in styles/feedback.css (part of the kit stylesheet). Accent-aware.
11
11
  import { esc } from './index.js';
12
12
 
13
- const IC_MSG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H8l-4 4V5a2 2 0 0 1 2-2h13a2 2 0 0 1 2 2z"/></svg>';
14
- const IC_LINES = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 5h16M4 12h10M4 19h7"/></svg>';
15
- const IC_X = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>';
13
+ // Decorative, like every glyph in the kit the pill and the buttons carry their
14
+ // own text or aria-label, so the SVGs stay out of the accessibility tree.
15
+ const IC_MSG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M21 15a2 2 0 0 1-2 2H8l-4 4V5a2 2 0 0 1 2-2h13a2 2 0 0 1 2 2z"/></svg>';
16
+ const IC_LINES = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" focusable="false"><path d="M4 5h16M4 12h10M4 19h7"/></svg>';
17
+ const IC_X = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M6 6l12 12M18 6L6 18"/></svg>';
16
18
  const CHECK = '<svg class="ui-fbck" viewBox="0 0 150 150" aria-hidden="true"><path class="ui-fbck-t" d="M40 78l24 24 46-50"/><path class="ui-fbck-m" d="M40 78l24 24 46-50"/><path class="ui-fbck-s" d="M40 78l24 24 46-50"/></svg>';
17
19
 
18
20
  // The widget markup — append once to the page (e.g. document.body). Copy is
@@ -36,21 +38,21 @@ export function feedbackWidget({
36
38
  <div data-fb-form>
37
39
  <div class="ui-fbc__head">
38
40
  <span class="ui-fbc__chip">${IC_LINES}<span data-fb-chip>this page</span></span>
39
- <button class="ui-fbc__x" data-fb-close aria-label="Close">${IC_X}</button>
41
+ <button type="button" class="ui-fbc__x" data-fb-close aria-label="Close">${IC_X}</button>
40
42
  </div>
41
43
  <div class="ui-fbc__quote"><span class="ui-fbc__qm">&#8220;</span><q data-fb-quote></q></div>
42
44
  <div class="ui-fbc__body"><textarea data-fb-note maxlength="6000" placeholder="${esc(placeholder)}" aria-label="Your note"></textarea></div>
43
45
  <div class="ui-fbc__err" data-fb-err></div>
44
46
  <div class="ui-fbc__foot">
45
- <button class="ui-fbbtn ghost" data-fb-cancel>Cancel</button>
46
- <button class="ui-fbbtn primary" data-fb-send disabled><span class="ui-fbc__send" data-fb-sendlb>Send feedback</span></button>
47
+ <button type="button" class="ui-fbbtn ghost" data-fb-cancel>Cancel</button>
48
+ <button type="button" class="ui-fbbtn primary" data-fb-send disabled><span class="ui-fbc__send" data-fb-sendlb>Send feedback</span></button>
47
49
  </div>
48
50
  </div>
49
51
  <div class="ui-fbc__done" data-fb-done style="display:none">
50
52
  <div class="ui-fbc__ck">${CHECK}</div>
51
53
  <h4>${esc(doneTitle)}</h4>
52
54
  <p>${esc(doneBody)}</p>
53
- <div class="row"><button class="ui-fbbtn ghost" data-fb-done-close>Close</button></div>
55
+ <div class="row"><button type="button" class="ui-fbbtn ghost" data-fb-done-close>Close</button></div>
54
56
  </div>
55
57
  </div>`;
56
58
  }
@@ -11,6 +11,11 @@ export const esc = (s) => String(s == null ? '' : s).replace(/[&<>"]/g, (c) => (
11
11
  // branded glyphs the kit's icon set doesn't own — e.g. a Google "G". It takes
12
12
  // the leading slot; `icon` is the named-icon shorthand for kit glyphs. This is
13
13
  // what lets third-party buttons go through button() instead of hand-rolled markup.
14
+ //
15
+ // `iconOnly` drops the visible text, and kit glyphs are aria-hidden — so `label`
16
+ // stops being decoration and becomes the control's ONLY accessible name. It is
17
+ // mirrored into aria-label + title, and an empty label falls back to the icon
18
+ // name rather than shipping a nameless button.
14
19
  export function button({
15
20
  label = 'Button', variant = 'secondary', size = 'md', icon: ic, iconSvg, iconRight,
16
21
  block = false, disabled = false, busy = false, type = 'button', href, iconOnly = false,
@@ -26,7 +31,9 @@ export function button({
26
31
  const lead = iconSvg || (ic ? icon(ic) : '');
27
32
  const inner = `${lead}${iconOnly ? '' : `<span>${esc(label)}</span>`}${iconRight ? icon(iconRight) : ''}${bars}`;
28
33
  // busy ⇒ disabled (not clickable while it works)
29
- const attrs = `class="${cls}"${disabled || busy ? ' disabled aria-disabled="true"' : ''}${busy ? ' aria-busy="true"' : ''}${iconOnly ? ` aria-label="${esc(label)}"` : ''}`;
34
+ const name = String(label == null ? '' : label).trim() || ic || 'Button';
35
+ const named = iconOnly ? ` aria-label="${esc(name)}" title="${esc(name)}"` : '';
36
+ const attrs = `class="${cls}"${disabled || busy ? ' disabled aria-disabled="true"' : ''}${busy ? ' aria-busy="true"' : ''}${named}`;
30
37
  return href
31
38
  ? `<a href="${href}" ${attrs}>${inner}</a>`
32
39
  : `<button type="${type}" ${attrs}>${inner}</button>`;
@@ -55,15 +62,34 @@ export function card({ title, sub, body = '', variant, pad, icon: ic } = {}) {
55
62
  }
56
63
 
57
64
  // ---- Segmented control ---------------------------------------------------
58
- export function segmented({ options = [], active = 0, size, block, name = 'seg' } = {}) {
65
+ // A strip of mutually exclusive toggle buttons a filter, a unit switch, a
66
+ // language picker. It is NOT a tablist: it controls no panel, so it must not
67
+ // announce one. `role="toolbar"` with a roving tabindex is the honest shape —
68
+ // one Tab stop for the whole strip, ArrowLeft/ArrowRight to move between the
69
+ // options, Home/End to jump to the ends, and `aria-pressed` to say which is on.
70
+ // wireTopbar() ships that keyboard behaviour (see topbar.js); the markup here
71
+ // carries the state it reads.
72
+ //
73
+ // When the pill drives a real change of view with content behind it, reach for
74
+ // tabs() instead — that one owns panels and earns the tab announcement.
75
+ //
76
+ // `ariaLabel` names the strip. `name` seeds data-seg (a hook for callers) and
77
+ // is deliberately not an accessible name — it is an identifier, not prose.
78
+ // Defaults to "Options" so a strip is never left unlabelled, the same way
79
+ // switchToggle() defaults its label.
80
+ export function segmented({ options = [], active = 0, size, block, name = 'seg', ariaLabel = 'Options' } = {}) {
59
81
  const cls = cx('ui-seg', size && `ui-seg--${size}`, block && 'ui-seg--block');
82
+ // Exactly one option holds the Tab stop. If `active` points nowhere (nothing
83
+ // selected yet) the first option holds it, so the strip is never unreachable.
84
+ const rove = active >= 0 && active < options.length ? active : 0;
60
85
  const btns = options.map((o, i) => {
61
86
  const label = typeof o === 'string' ? o : o.label;
62
87
  const val = typeof o === 'string' ? o : (o.value ?? o.label);
63
88
  const on = i === active;
64
- return `<button type="button" role="tab" aria-selected="${on}" data-value="${esc(val)}"${on ? ' class="is-active"' : ''}>${esc(label)}</button>`;
89
+ return `<button type="button" aria-pressed="${on}" tabindex="${i === rove ? '0' : '-1'}"`
90
+ + ` data-value="${esc(val)}"${on ? ' class="is-active"' : ''}>${esc(label)}</button>`;
65
91
  }).join('');
66
- return `<div class="${cls}" role="tablist" data-seg="${name}">${btns}</div>`;
92
+ return `<div class="${cls}" role="toolbar" aria-label="${esc(ariaLabel)}" data-seg="${name}">${btns}</div>`;
67
93
  }
68
94
 
69
95
  // ---- Accent picker -------------------------------------------------------
@@ -73,9 +99,14 @@ const ACCENT_SWATCH = {
73
99
  ocean: 'linear-gradient(135deg,#5ab0ff,#3b9dff)',
74
100
  emerald: 'linear-gradient(135deg,#3ad9a0,#16c98a)',
75
101
  };
102
+ // Swatches have no text and no glyph at all — the aria-label is the whole
103
+ // accessible name, and aria-pressed carries which one is on.
76
104
  export function accentPicker({ active = 'default', options = ['default', 'phoenix', 'ocean', 'emerald'] } = {}) {
77
- return `<div class="ui-accent-picker" data-accent-group>${options.map((o) =>
78
- `<button type="button" data-accent-pick="${o}"${o === active ? ' class="is-active"' : ''} style="--swatch:${ACCENT_SWATCH[o]}" aria-label="${o} accent" title="${o[0].toUpperCase() + o.slice(1)}"></button>`).join('')}</div>`;
105
+ const title = (o) => o.charAt(0).toUpperCase() + o.slice(1);
106
+ return `<div class="ui-accent-picker" data-accent-group role="group" aria-label="Accent">${options.map((o) =>
107
+ `<button type="button" data-accent-pick="${esc(o)}"${o === active ? ' class="is-active"' : ''}`
108
+ + ` style="--swatch:${ACCENT_SWATCH[o] || 'transparent'}" aria-pressed="${o === active ? 'true' : 'false'}"`
109
+ + ` aria-label="${esc(title(o))} accent" title="${esc(title(o))}"></button>`).join('')}</div>`;
79
110
  }
80
111
 
81
112
  // ---- Field / input -------------------------------------------------------
@@ -93,8 +124,32 @@ function withControlId(html) {
93
124
  const out = html.replace(/<(input|textarea|select)\b/, (m) => { id = nextId('field'); return `${m} id="${id}"`; });
94
125
  return { html: out, id };
95
126
  }
127
+ // Same trick, for the attributes that carry state rather than identity:
128
+ // aria-describedby, aria-invalid, required. Anything the control already spells
129
+ // out for itself wins — a caller who passed `invalid` to input() keeps their own
130
+ // aria-invalid. `true` renders a bare boolean attribute.
131
+ function withControlAttrs(html, attrs) {
132
+ const pairs = Object.entries(attrs).filter(([, v]) => v != null && v !== false);
133
+ if (!pairs.length) return html;
134
+ return html.replace(/<(input|textarea|select)\b([^>]*?)(\s*\/?)>/, (m, tag, rest, tail) => {
135
+ const add = pairs
136
+ .filter(([k]) => !new RegExp(`\\s${k}(?=[\\s=>/]|$)`).test(rest))
137
+ .map(([k, v]) => (v === true ? ` ${k}` : ` ${k}="${esc(v)}"`))
138
+ .join('');
139
+ return `<${tag}${rest}${add}${tail}>`;
140
+ });
141
+ }
96
142
 
97
- export function field({ label, hint, error, control = '', id } = {}) {
143
+ // A labelled control with, optionally, a hint or an error under it.
144
+ //
145
+ // The label half was wired in #119 (withControlId above → a real `for=`). This
146
+ // finishes the job for the rest: the hint or error gets an id and the control
147
+ // points at it with aria-describedby, so the reason a value was rejected is read
148
+ // out with the field instead of sitting beside it as loose text. An errored
149
+ // control is marked aria-invalid, and `required` puts the requirement in the
150
+ // markup rather than in the label's wording — the asterisk is decoration on top
151
+ // of that, so it is hidden from assistive technology.
152
+ export function field({ label, hint, error, control = '', id, required = false } = {}) {
98
153
  let ctl = control;
99
154
  let forId = id;
100
155
  if (label && control) {
@@ -102,17 +157,29 @@ export function field({ label, hint, error, control = '', id } = {}) {
102
157
  ctl = r.html;
103
158
  forId = id || r.id;
104
159
  }
160
+ // Error wins over hint — only one message shows, so only one id is minted.
161
+ const msg = error || hint;
162
+ const msgId = msg && control ? nextId(error ? 'field-err' : 'field-hint') : null;
163
+ ctl = withControlAttrs(ctl, {
164
+ 'aria-describedby': msgId,
165
+ 'aria-invalid': error ? 'true' : null,
166
+ required: required || null,
167
+ });
105
168
  const lab = label
106
- ? `<label class="ui-field__label"${forId ? ` for="${forId}"` : ''}>${esc(label)}</label>`
169
+ ? `<label class="ui-field__label"${forId ? ` for="${forId}"` : ''}>${esc(label)}`
170
+ + `${required ? '<span class="ui-field__req" aria-hidden="true">*</span>' : ''}</label>`
107
171
  : '';
108
172
  const foot = error
109
- ? `<div class="ui-field__error">${icon('alert')}${esc(error)}</div>`
110
- : hint ? `<div class="ui-field__hint">${esc(hint)}</div>` : '';
173
+ ? `<div class="ui-field__error"${msgId ? ` id="${msgId}"` : ''}>${icon('alert')}${esc(error)}</div>`
174
+ : hint ? `<div class="ui-field__hint"${msgId ? ` id="${msgId}"` : ''}>${esc(hint)}</div>` : '';
111
175
  return `<div class="ui-field">${lab}${ctl}${foot}</div>`;
112
176
  }
113
177
  // `ariaLabel` gives a standalone control (no wrapping field) an accessible name.
114
- export function input({ type = 'text', placeholder = '', value = '', icon: ic, invalid, disabled, name, id, ariaLabel } = {}) {
115
- const attrs = `${id ? ` id="${esc(id)}"` : ''}${name ? ` name="${name}"` : ''}${ariaLabel ? ` aria-label="${esc(ariaLabel)}"` : ''}${disabled ? ' disabled' : ''}`;
178
+ // `invalid` paints the control red AND says so in aria-invalid the red on its
179
+ // own is a state only a sighted user can read.
180
+ export function input({ type = 'text', placeholder = '', value = '', icon: ic, invalid, disabled, required, name, id, ariaLabel } = {}) {
181
+ const attrs = `${id ? ` id="${esc(id)}"` : ''}${name ? ` name="${name}"` : ''}${ariaLabel ? ` aria-label="${esc(ariaLabel)}"` : ''}`
182
+ + `${invalid ? ' aria-invalid="true"' : ''}${required ? ' required' : ''}${disabled ? ' disabled' : ''}`;
116
183
  const el = `<input class="${cx('ui-input', invalid && 'is-invalid')}" type="${type}" placeholder="${esc(placeholder)}" value="${esc(value)}"${attrs}>`;
117
184
  if (!ic) return el;
118
185
  return `<div class="ui-input-group"><span class="ui-input-group__icon">${icon(ic)}</span>${el}</div>`;
@@ -158,7 +225,8 @@ export function toast({
158
225
  const cls = cx('ui-toast', `ui-toast--${variant}`, `ui-toast--${style}`, compact && 'ui-toast--compact');
159
226
  const actLabel = typeof action === 'string' ? action : action && action.label;
160
227
  const actBtn = actLabel ? `<button class="ui-toast__action" type="button">${esc(actLabel)}</button>` : '';
161
- const closeBtn = dismissible ? `<button class="ui-toast__close" aria-label="Dismiss">${icon('x')}</button>` : '';
228
+ // Icon-only: the glyph is aria-hidden, so aria-label IS the accessible name.
229
+ const closeBtn = dismissible ? `<button type="button" class="ui-toast__close" aria-label="Dismiss">${icon('x')}</button>` : '';
162
230
  const bodyHtml = compact || !body ? '' : `<div class="ui-toast__text">${esc(body)}</div>`;
163
231
  const dur = typeof timer === 'number' ? ` style="--toast-dur:${timer}s"` : '';
164
232
  const timerBar = timer ? '<span class="ui-toast__timer" data-toast-timer></span>' : '';
@@ -187,8 +255,15 @@ export function emptyState({ art, icon: ic, title, sub, actions } = {}) {
187
255
  }
188
256
 
189
257
  // ---- Snippet -------------------------------------------------------------
190
- export function snippet({ label = 'shell', code = '', reveal = false, copy = true } = {}) {
191
- return `<div class="${cx('ui-snippet', reveal && 'ui-snippet--reveal')}"><div class="ui-snippet__bar"><span>${esc(label)}</span>${copy ? `<button class="ui-snippet__copy">${icon('copy')}Copy</button>` : ''}</div><pre>${code}</pre></div>`;
258
+ // `copyLabel` is both the visible text and the accessible name of the copy
259
+ // button. wireTopbar() swaps that text to " Copied" and back, so the button is
260
+ // never left nameless — the glyph beside it is decorative (aria-hidden).
261
+ // `type="button"`: without it a snippet dropped inside a <form> submits the form.
262
+ export function snippet({ label = 'shell', code = '', reveal = false, copy = true, copyLabel = 'Copy' } = {}) {
263
+ const copyBtn = copy
264
+ ? `<button type="button" class="ui-snippet__copy" data-orig="${esc(copyLabel)}">${icon('copy')}${esc(copyLabel)}</button>`
265
+ : '';
266
+ return `<div class="${cx('ui-snippet', reveal && 'ui-snippet--reveal')}"><div class="ui-snippet__bar"><span>${esc(label)}</span>${copyBtn}</div><pre>${code}</pre></div>`;
192
267
  }
193
268
 
194
269
  // Tiny shell highlighter: escapes first, then wraps comments/strings/URLs/flags/command.
@@ -20,7 +20,7 @@ export function tabs({ items = [], active = 0, name = 'tabs', ariaLabel = 'Tabs'
20
20
  const list = items
21
21
  .map((it, i) => {
22
22
  const on = i === active;
23
- return `<button class="ui-tabs__tab${on ? ' is-active' : ''}" role="tab"`
23
+ return `<button type="button" class="ui-tabs__tab${on ? ' is-active' : ''}" role="tab"`
24
24
  + ` id="${name}-tab-${i}" aria-controls="${name}-panel-${i}"`
25
25
  + ` aria-selected="${on ? 'true' : 'false'}" tabindex="${on ? '0' : '-1'}">${it.label}</button>`;
26
26
  })
@@ -28,8 +28,12 @@ export function tabs({ items = [], active = 0, name = 'tabs', ariaLabel = 'Tabs'
28
28
  const panels = items
29
29
  .map((it, i) => {
30
30
  const on = i === active;
31
+ // tabindex="0" on the panel: the APG requires it when the panel holds no
32
+ // focusable content of its own, and most panels here are plain text. Without
33
+ // it a keyboard user reaches the tab strip and then cannot reach what it
34
+ // switched to — Tab jumps straight past the panel to whatever follows.
31
35
  return `<div class="ui-tabs__panel" role="tabpanel" id="${name}-panel-${i}"`
32
- + ` aria-labelledby="${name}-tab-${i}"${on ? '' : ' hidden'}>${it.panel || ''}</div>`;
36
+ + ` aria-labelledby="${name}-tab-${i}" tabindex="0"${on ? '' : ' hidden'}>${it.panel || ''}</div>`;
33
37
  })
34
38
  .join('');
35
39
  return `<div class="${cls}" data-tabs>`
@@ -106,14 +106,44 @@ export function wireTopbar(root = document) {
106
106
  a.setAttribute('aria-current', 'page');
107
107
  });
108
108
  });
109
- // Segmented controls (.ui-seg) — visual toggle
109
+ // Segmented controls (.ui-seg) — a toolbar of toggle buttons (see segmented()
110
+ // in components/index.js). Click selects; ArrowLeft/ArrowRight move and wrap,
111
+ // Home/End jump to the ends. The strip keeps ONE Tab stop: selecting an option
112
+ // hands it the tabindex and takes it off the rest, so a page with three of
113
+ // these costs three Tab presses, not nine.
110
114
  root.querySelectorAll('.ui-seg').forEach((seg) => {
115
+ const btns = () => Array.prototype.slice.call(seg.querySelectorAll('button'));
116
+ const select = (b, focus) => {
117
+ btns().forEach((x) => {
118
+ const on = x === b;
119
+ x.classList.toggle('is-active', on);
120
+ // Older hand-written .ui-seg markup carries aria-selected; segmented()
121
+ // now emits aria-pressed. Keep whichever the button actually declares in
122
+ // step, and never invent the other one.
123
+ if (x.hasAttribute('aria-pressed')) x.setAttribute('aria-pressed', on ? 'true' : 'false');
124
+ if (x.hasAttribute('aria-selected')) x.setAttribute('aria-selected', on ? 'true' : 'false');
125
+ x.tabIndex = on ? 0 : -1;
126
+ });
127
+ if (focus) b.focus();
128
+ };
111
129
  seg.addEventListener('click', (e) => {
112
130
  const b = e.target.closest('button');
113
131
  if (!b || !seg.contains(b)) return;
114
- seg.querySelectorAll('button').forEach((x) => { x.classList.remove('is-active'); x.setAttribute('aria-selected', 'false'); });
115
- b.classList.add('is-active');
116
- b.setAttribute('aria-selected', 'true');
132
+ select(b);
133
+ });
134
+ seg.addEventListener('keydown', (e) => {
135
+ const b = e.target.closest('button');
136
+ if (!b || !seg.contains(b)) return;
137
+ const all = btns();
138
+ const i = all.indexOf(b);
139
+ let n = null;
140
+ if (e.key === 'ArrowRight') n = (i + 1) % all.length;
141
+ else if (e.key === 'ArrowLeft') n = (i - 1 + all.length) % all.length;
142
+ else if (e.key === 'Home') n = 0;
143
+ else if (e.key === 'End') n = all.length - 1;
144
+ if (n === null) return;
145
+ e.preventDefault();
146
+ select(all[n], true);
117
147
  });
118
148
  });
119
149
  // Version switcher + account menu — the shared dropdown wiring (open/close +
@@ -124,7 +154,11 @@ export function wireTopbar(root = document) {
124
154
  chip.addEventListener('click', () => {
125
155
  applyAccent(chip.getAttribute('data-accent-pick'), document.documentElement);
126
156
  const group = chip.closest('[data-accent-group]') || root;
127
- group.querySelectorAll('[data-accent-pick]').forEach((c) => c.classList.toggle('is-active', c === chip));
157
+ group.querySelectorAll('[data-accent-pick]').forEach((c) => {
158
+ c.classList.toggle('is-active', c === chip);
159
+ // Keep the announced state in step with the visual one.
160
+ if (c.hasAttribute('aria-pressed')) c.setAttribute('aria-pressed', c === chip ? 'true' : 'false');
161
+ });
128
162
  });
129
163
  });
130
164
  // Copy buttons