fvn-ui 0.1.0-alpha.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.
- package/README.md +57 -0
- package/package.json +63 -0
- package/src/fvn-ui/LLM.md +312 -0
- package/src/fvn-ui/components/avatar.css +53 -0
- package/src/fvn-ui/components/avatar.js +69 -0
- package/src/fvn-ui/components/button.css +143 -0
- package/src/fvn-ui/components/button.js +136 -0
- package/src/fvn-ui/components/card.css +6 -0
- package/src/fvn-ui/components/card.js +63 -0
- package/src/fvn-ui/components/checkbox.css +5 -0
- package/src/fvn-ui/components/checkbox.js +82 -0
- package/src/fvn-ui/components/collapsible.css +22 -0
- package/src/fvn-ui/components/collapsible.js +72 -0
- package/src/fvn-ui/components/confirm.js +109 -0
- package/src/fvn-ui/components/dashboard.css +25 -0
- package/src/fvn-ui/components/dashboard.js +130 -0
- package/src/fvn-ui/components/dialog.css +79 -0
- package/src/fvn-ui/components/dialog.js +302 -0
- package/src/fvn-ui/components/form.css +99 -0
- package/src/fvn-ui/components/image.css +21 -0
- package/src/fvn-ui/components/image.js +70 -0
- package/src/fvn-ui/components/index.js +73 -0
- package/src/fvn-ui/components/input.css +30 -0
- package/src/fvn-ui/components/input.js +81 -0
- package/src/fvn-ui/components/radio.css +3 -0
- package/src/fvn-ui/components/radio.js +99 -0
- package/src/fvn-ui/components/select.css +160 -0
- package/src/fvn-ui/components/select.js +366 -0
- package/src/fvn-ui/components/svg.css +5 -0
- package/src/fvn-ui/components/svg.js +85 -0
- package/src/fvn-ui/components/switch.css +34 -0
- package/src/fvn-ui/components/switch.js +85 -0
- package/src/fvn-ui/components/tabs.css +168 -0
- package/src/fvn-ui/components/tabs.js +181 -0
- package/src/fvn-ui/components/text.css +62 -0
- package/src/fvn-ui/components/text.js +105 -0
- package/src/fvn-ui/components/toggle.css +46 -0
- package/src/fvn-ui/components/toggle.js +60 -0
- package/src/fvn-ui/dom.js +495 -0
- package/src/fvn-ui/helpers.js +29 -0
- package/src/fvn-ui/index.js +53 -0
- package/src/fvn-ui/style.css +432 -0
- package/src/fvn-ui/template.js +135 -0
- package/src/fvn-ui/template.md +26 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// https://feathericons.com/
|
|
2
|
+
// https://lucide.dev/icons
|
|
3
|
+
|
|
4
|
+
import './svg.css';
|
|
5
|
+
|
|
6
|
+
const shapes = {
|
|
7
|
+
check: '<polyline points="20 6 9 17 4 12"></polyline>',
|
|
8
|
+
chevronDown: '<polyline points="6 9 12 15 18 9"></polyline>',
|
|
9
|
+
chevronUp: '<polyline points="18 15 12 9 6 15"></polyline>',
|
|
10
|
+
chevronLeft: '<polyline points="15 18 9 12 15 6"></polyline>',
|
|
11
|
+
chevronRight: '<polyline points="9 18 15 12 9 6"></polyline>',
|
|
12
|
+
arrowRight: '<line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline>',
|
|
13
|
+
arrowLeft: '<line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline>',
|
|
14
|
+
arrowUp: '<line x1="12" y1="19" x2="12" y2="5"></line><polyline points="5 12 12 5 19 12"></polyline>',
|
|
15
|
+
arrowDown: '<line x1="12" y1="5" x2="12" y2="19"></line><polyline points="19 12 12 19 5 12"></polyline>',
|
|
16
|
+
settings: '<path d="M10 5H3"/><path d="M12 19H3"/><path d="M14 3v4"/><path d="M16 17v4"/><path d="M21 12h-9"/><path d="M21 19h-5"/><path d="M21 5h-7"/><path d="M8 10v4"/><path d="M8 12H3"/>',
|
|
17
|
+
x: '<line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line>',
|
|
18
|
+
dots: '<circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="19" r="1"></circle>',
|
|
19
|
+
dotsHorizontal: '<circle cx="12" cy="12" r="1"></circle><circle cx="19" cy="12" r="1"></circle><circle cx="5" cy="12" r="1"></circle>',
|
|
20
|
+
menu: '<line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line>',
|
|
21
|
+
code: '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="m10 8 4 4-4 4"/>',
|
|
22
|
+
enter: '<path d="M11 9a1 1 0 0 0 1-1V5.061a1 1 0 0 1 1.811-.75l6.836 6.836a1.207 1.207 0 0 1 0 1.707l-6.836 6.835a1 1 0 0 1-1.811-.75V16a1 1 0 0 0-1-1H9a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1z"/><path d="M4 9v6"/>',
|
|
23
|
+
logout: '<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line>',
|
|
24
|
+
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"></path>',
|
|
25
|
+
moon: '<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>',
|
|
26
|
+
sun: '<circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>',
|
|
27
|
+
hexagon: '<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"/>',
|
|
28
|
+
circle: '<circle cx="12" cy="12" r="10"></circle>',
|
|
29
|
+
plus: '<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>',
|
|
30
|
+
minus: '<line x1="5" y1="12" x2="19" y2="12"></line>',
|
|
31
|
+
search: '<circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line>',
|
|
32
|
+
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><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>',
|
|
33
|
+
trash: '<polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>',
|
|
34
|
+
home: '<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline>',
|
|
35
|
+
user: '<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle>',
|
|
36
|
+
users: '<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path>',
|
|
37
|
+
mail: '<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline>',
|
|
38
|
+
calendar: '<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line>',
|
|
39
|
+
clock: '<circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline>',
|
|
40
|
+
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"></path>',
|
|
41
|
+
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"></polygon>',
|
|
42
|
+
bell: '<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path>',
|
|
43
|
+
info: '<circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line>',
|
|
44
|
+
alertCircle: '<circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line>',
|
|
45
|
+
alertTriangle: '<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><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line>',
|
|
46
|
+
checkCircle: '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline>',
|
|
47
|
+
xCircle: '<circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line>',
|
|
48
|
+
eye: '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle>',
|
|
49
|
+
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"></path><line x1="1" y1="1" x2="23" y2="23"></line>',
|
|
50
|
+
copy: '<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>',
|
|
51
|
+
download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line>',
|
|
52
|
+
upload: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line>',
|
|
53
|
+
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><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>',
|
|
54
|
+
externalLink: '<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line>',
|
|
55
|
+
filter: '<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>',
|
|
56
|
+
sliders: '<line x1="4" y1="21" x2="4" y2="14"></line><line x1="4" y1="10" x2="4" y2="3"></line><line x1="12" y1="21" x2="12" y2="12"></line><line x1="12" y1="8" x2="12" y2="3"></line><line x1="20" y1="21" x2="20" y2="16"></line><line x1="20" y1="12" x2="20" y2="3"></line><line x1="1" y1="14" x2="7" y2="14"></line><line x1="9" y1="8" x2="15" y2="8"></line><line x1="17" y1="16" x2="23" y2="16"></line>',
|
|
57
|
+
refresh: '<polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><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"></path>',
|
|
58
|
+
loader: '<line x1="12" y1="2" x2="12" y2="6"></line><line x1="12" y1="18" x2="12" y2="22"></line><line x1="4.93" y1="4.93" x2="7.76" y2="7.76"></line><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"></line><line x1="2" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="22" y2="12"></line><line x1="4.93" y1="19.07" x2="7.76" y2="16.24"></line><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"></line>',
|
|
59
|
+
lock: '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path>',
|
|
60
|
+
doc: '<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/>',
|
|
61
|
+
unlock: '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 9.9-1"></path>',
|
|
62
|
+
save: '<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline>',
|
|
63
|
+
file: '<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline>',
|
|
64
|
+
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"></path>',
|
|
65
|
+
image: '<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline>',
|
|
66
|
+
grid: '<rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect>',
|
|
67
|
+
list: '<line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line>',
|
|
68
|
+
maximize: '<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path>',
|
|
69
|
+
minimize: '<path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"></path>',
|
|
70
|
+
zap: '<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon>',
|
|
71
|
+
play: '<polygon points="5 3 19 12 5 21 5 3"></polygon>',
|
|
72
|
+
pause: '<rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect>',
|
|
73
|
+
rabbit: '<path d="M13 16a3 3 0 0 1 2.24 5"/><path d="M18 12h.01"/><path d="M18 21h-8a4 4 0 0 1-4-4 7 7 0 0 1 7-7h.2L9.6 6.4a1 1 0 1 1 2.8-2.8L15.8 7h.2c3.3 0 6 2.7 6 6v1a2 2 0 0 1-2 2h-1a3 3 0 0 0-3 3"/><path d="M20 8.54V4a2 2 0 1 0-4 0v3"/><path d="M7.612 12.524a3 3 0 1 0-1.6 4.3"/>',
|
|
74
|
+
bird: '<path d="M16 7h.01"/><path d="M3.4 18H12a8 8 0 0 0 8-8V7a4 4 0 0 0-7.28-2.3L2 20"/><path d="m20 7 2 .5-2 .5"/><path d="M10 18v3"/><path d="M14 17.75V21"/><path d="M7 18a6 6 0 0 0 3.84-10.61"/>'
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const wrap = (shape, n) => !shape
|
|
78
|
+
? ''
|
|
79
|
+
: `
|
|
80
|
+
<svg class="ui-icon ui-icon-${n}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
|
81
|
+
${shape}
|
|
82
|
+
</svg>
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
export const svg = n => wrap(shapes[n], n);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
.ui-switch {
|
|
2
|
+
--bg: var(--text);
|
|
3
|
+
display: inline-flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
gap: var(--space-2);
|
|
6
|
+
}
|
|
7
|
+
.ui-switch__button {
|
|
8
|
+
--switch-size: calc(1.75 * var(--space));
|
|
9
|
+
width: calc(2 * var(--switch-size));
|
|
10
|
+
height: var(--switch-size);
|
|
11
|
+
border-radius: 999px;
|
|
12
|
+
border: none;
|
|
13
|
+
background: hsl(var(--hsl-muted) / .25);
|
|
14
|
+
position: relative;
|
|
15
|
+
cursor: pointer;
|
|
16
|
+
padding: 0;
|
|
17
|
+
transition: var(--core-transition);
|
|
18
|
+
}
|
|
19
|
+
.ui-switch__thumb {
|
|
20
|
+
width: calc(.8 * var(--switch-size));
|
|
21
|
+
height: calc(.8 * var(--switch-size));
|
|
22
|
+
border-radius: 999px;
|
|
23
|
+
background: hsl(var(--hsl-white));
|
|
24
|
+
position: absolute;
|
|
25
|
+
top: 50%;
|
|
26
|
+
left: calc(.1 * var(--switch-size));
|
|
27
|
+
transform: translateY(-50%);
|
|
28
|
+
transition: left var(--core-transition);
|
|
29
|
+
}
|
|
30
|
+
.ui-switch[data-checked="true"] .ui-switch__button {
|
|
31
|
+
background: var(--bg);
|
|
32
|
+
border-color: var(--bg);
|
|
33
|
+
}
|
|
34
|
+
.ui-switch[data-checked="true"] .ui-switch__thumb { left: calc(1.1 * var(--switch-size)); }
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { el, getCallback, withValue, parseArgs, configToClasses, bemFactory } from '../dom.js'
|
|
2
|
+
import { label as textLabel } from './text.js'
|
|
3
|
+
import './switch.css'
|
|
4
|
+
|
|
5
|
+
const bem = bemFactory('switch');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a toggle switch
|
|
9
|
+
* @param {Object} config
|
|
10
|
+
* @param {string} [config.label] - Switch label (clickable)
|
|
11
|
+
* @param {boolean} [config.checked] - Initial checked state
|
|
12
|
+
* @param {boolean} [config.disabled] - Disabled state
|
|
13
|
+
* @param {'default'|'primary'|'red'|'green'|'blue'} [config.color='default'] - Switch color when checked
|
|
14
|
+
* @param {Function} [config.onChange] - Called with (checked, event)
|
|
15
|
+
* @param {string} [config.id] - Registers to dom.switch[id] and dom[id]
|
|
16
|
+
* @returns {HTMLDivElement} Switch element with .value getter/setter
|
|
17
|
+
* @example
|
|
18
|
+
* switchComponent({ label: 'Dark mode', checked: true })
|
|
19
|
+
* switchComponent({ label: 'Notifications', color: 'primary', onChange: (v) => save(v) })
|
|
20
|
+
*/
|
|
21
|
+
export function switchComponent(...args) {
|
|
22
|
+
const {
|
|
23
|
+
parent,
|
|
24
|
+
checked,
|
|
25
|
+
disabled,
|
|
26
|
+
color = 'default',
|
|
27
|
+
label,
|
|
28
|
+
id,
|
|
29
|
+
props,
|
|
30
|
+
...rest
|
|
31
|
+
} = parseArgs(...args);
|
|
32
|
+
|
|
33
|
+
const cb = getCallback('onChange', rest);
|
|
34
|
+
let btnEl;
|
|
35
|
+
let state = !!checked;
|
|
36
|
+
|
|
37
|
+
const setState = (next, e) => {
|
|
38
|
+
state = !!next;
|
|
39
|
+
root.dataset.checked = state;
|
|
40
|
+
btnEl.setAttribute('aria-checked', state);
|
|
41
|
+
e && cb?.(state, e);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const toggle = (e) => {
|
|
45
|
+
if (disabled) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
setState(!state, e);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const onKeydown = (e) => {
|
|
52
|
+
if (e.key !== ' ' && e.key !== 'Enter') {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
toggle(e);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const root = el('div', parent, {
|
|
60
|
+
...rest,
|
|
61
|
+
class: [bem(), configToClasses(props), rest.class],
|
|
62
|
+
data: { checked: state, uiCol: color },
|
|
63
|
+
children: [
|
|
64
|
+
el('button', {
|
|
65
|
+
type: 'button',
|
|
66
|
+
class: bem.el('button'),
|
|
67
|
+
attrs: { role: 'switch', 'aria-checked': state },
|
|
68
|
+
id,
|
|
69
|
+
disabled,
|
|
70
|
+
ref: (e) => btnEl = e,
|
|
71
|
+
children: [el('span', { class: bem.el('thumb') })],
|
|
72
|
+
onClick: toggle,
|
|
73
|
+
onKeydown
|
|
74
|
+
}),
|
|
75
|
+
label && textLabel({
|
|
76
|
+
text: label,
|
|
77
|
+
small: true,
|
|
78
|
+
onClick: toggle,
|
|
79
|
+
style: { cursor: disabled ? 'default' : 'pointer' }
|
|
80
|
+
})
|
|
81
|
+
]
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return withValue(root, () => state, setState);
|
|
85
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
.ui-tabs .ui-btn[aria-selected="true"] {
|
|
2
|
+
pointer-events: none;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.ui-tabs__buttons {
|
|
6
|
+
display: inline-flex;
|
|
7
|
+
gap: var(--space-1);
|
|
8
|
+
flex-wrap: wrap;
|
|
9
|
+
background: var(--shade);
|
|
10
|
+
padding: var(--space-1);
|
|
11
|
+
border-radius: calc(1.5 * var(--radius));
|
|
12
|
+
}
|
|
13
|
+
.ui-tabs__panel {
|
|
14
|
+
width: 100%;
|
|
15
|
+
border-radius: var(--radius);
|
|
16
|
+
}
|
|
17
|
+
.ui-tabs__tab {
|
|
18
|
+
background: transparent;
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
user-select: none;
|
|
21
|
+
transition: var(--core-transition);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* --- */
|
|
25
|
+
|
|
26
|
+
.ui-tabs--shade .ui-tabs__panel {
|
|
27
|
+
background: var(--shade);
|
|
28
|
+
}
|
|
29
|
+
.ui-tabs--shade .ui-btn[aria-selected="false"] {
|
|
30
|
+
--bg: var(--shade);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* --- */
|
|
34
|
+
|
|
35
|
+
.ui-tabs--round .ui-tabs--default {
|
|
36
|
+
border-radius: 999px;
|
|
37
|
+
}
|
|
38
|
+
.ui-tabs--round .ui-tabs--default .ui-btn {
|
|
39
|
+
--input-padding: calc(var(--space) * .5) calc(var(--space) * 1.5);
|
|
40
|
+
--bg: transparent;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* --- */
|
|
44
|
+
|
|
45
|
+
.ui-tabs--default .ui-btn {
|
|
46
|
+
--input-padding: calc(var(--space) * .5) calc(var(--space) * .75);
|
|
47
|
+
}
|
|
48
|
+
.ui-tabs--default .ui-btn[aria-selected="true"] {
|
|
49
|
+
background: hsl(var(--hsl-white)) !important;
|
|
50
|
+
color: hsl(var(--hsl-black)) !important;
|
|
51
|
+
box-shadow: 0 1px 4px rgba(0,0,0,.2);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* --- */
|
|
55
|
+
|
|
56
|
+
.ui-tabs--outline {
|
|
57
|
+
padding: 0;
|
|
58
|
+
background: none;
|
|
59
|
+
gap: 0;
|
|
60
|
+
}
|
|
61
|
+
.ui-tabs--outline:not(.justify-center) + .ui-tabs__panel {
|
|
62
|
+
border-top-left-radius: 0;
|
|
63
|
+
}
|
|
64
|
+
.ui-tabs--outline .ui-btn {
|
|
65
|
+
--core-transition: none;
|
|
66
|
+
border-color: transparent;
|
|
67
|
+
border-radius: 0;
|
|
68
|
+
}
|
|
69
|
+
.ui-tabs--outline .ui-btn:first-child {
|
|
70
|
+
border-top-left-radius: var(--radius);
|
|
71
|
+
}
|
|
72
|
+
.ui-tabs--outline .ui-btn:last-child {
|
|
73
|
+
border-top-right-radius: var(--radius);
|
|
74
|
+
}
|
|
75
|
+
.ui-tabs--outline .ui-btn[aria-selected="false"] {
|
|
76
|
+
--fg: var(--muted);
|
|
77
|
+
--bg: var(--shade);
|
|
78
|
+
}
|
|
79
|
+
.ui-tabs--outline .ui-btn[aria-selected="true"] {
|
|
80
|
+
--fg: var(--text);
|
|
81
|
+
--bg: var(--shade-back, var(--back));
|
|
82
|
+
border-color: var(--border);
|
|
83
|
+
}
|
|
84
|
+
.ui-tabs--outline .ui-btn[aria-selected="true"]:after {
|
|
85
|
+
content: '';
|
|
86
|
+
background: var(--bg);
|
|
87
|
+
bottom: 0;
|
|
88
|
+
left: 0;
|
|
89
|
+
width: 100%;
|
|
90
|
+
height: 3px;
|
|
91
|
+
position: absolute;
|
|
92
|
+
transform: translatey(100%);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* --- */
|
|
96
|
+
|
|
97
|
+
.ui-tabs--ghost {
|
|
98
|
+
gap: 0;
|
|
99
|
+
background: transparent;
|
|
100
|
+
padding: 0;
|
|
101
|
+
}
|
|
102
|
+
.ui-tabs--ghost .ui-btn[aria-selected="true"]:not([data-ui-col]) {
|
|
103
|
+
--bg: var(--hover);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* --- */
|
|
107
|
+
|
|
108
|
+
.ui-tabs--border {
|
|
109
|
+
padding: 0;
|
|
110
|
+
background: none;
|
|
111
|
+
gap: 0;
|
|
112
|
+
}
|
|
113
|
+
.ui-tabs--border:not(.justify-center) + .ui-tabs__panel {
|
|
114
|
+
border-top-left-radius: 0;
|
|
115
|
+
}
|
|
116
|
+
.ui-tabs--border .ui-btn {
|
|
117
|
+
border-color: transparent;
|
|
118
|
+
border-width: 2px;
|
|
119
|
+
border-radius: 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.ui-tabs--border .ui-btn:after {
|
|
123
|
+
content: '';
|
|
124
|
+
background: var(--bg);
|
|
125
|
+
bottom: 0;
|
|
126
|
+
left: 0;
|
|
127
|
+
width: 100%;
|
|
128
|
+
height: 2px;
|
|
129
|
+
position: absolute;
|
|
130
|
+
transform: translatey(100%);
|
|
131
|
+
pointer-events: none;
|
|
132
|
+
opacity: 0;
|
|
133
|
+
transition: var(--core-transition);
|
|
134
|
+
}
|
|
135
|
+
.ui-tabs--border .ui-btn:hover {
|
|
136
|
+
background: transparent;
|
|
137
|
+
}
|
|
138
|
+
.ui-tabs--border .ui-btn:hover:after {
|
|
139
|
+
background: var(--fg);
|
|
140
|
+
opacity: .5;
|
|
141
|
+
}
|
|
142
|
+
.ui-tabs--border .ui-btn[aria-selected="true"]:after {
|
|
143
|
+
background: var(--fg);
|
|
144
|
+
opacity: 1;
|
|
145
|
+
}
|
|
146
|
+
.ui-tabs--border .ui-btn[aria-selected="false"] {
|
|
147
|
+
--fg: var(--muted);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* --- */
|
|
151
|
+
|
|
152
|
+
.ui-tabs--minimal {
|
|
153
|
+
gap: var(--space-4);
|
|
154
|
+
padding: var(--space-2) 0;
|
|
155
|
+
background: transparent;
|
|
156
|
+
}
|
|
157
|
+
.ui-tabs--minimal .ui-btn {
|
|
158
|
+
--fg: var(--muted);
|
|
159
|
+
background: transparent;
|
|
160
|
+
padding: 0;
|
|
161
|
+
--space: 0px;
|
|
162
|
+
}
|
|
163
|
+
.ui-tabs--minimal .ui-btn[aria-selected="true"] {
|
|
164
|
+
--fg: var(--text);
|
|
165
|
+
&:before {
|
|
166
|
+
opacity: 1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { el, getCallback, withValue, parseArgs, configToClasses, bemFactory } from '../dom.js'
|
|
2
|
+
import { button } from './button.js'
|
|
3
|
+
import './tabs.css'
|
|
4
|
+
|
|
5
|
+
const bem = bemFactory('tabs');
|
|
6
|
+
|
|
7
|
+
const getItemValue = (item) => {
|
|
8
|
+
if (!item) {
|
|
9
|
+
return '';
|
|
10
|
+
}
|
|
11
|
+
return item.value !== undefined ? String(item.value) : String(item.label);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates a tabbed interface
|
|
16
|
+
* @param {Object} config
|
|
17
|
+
* @param {{label: string, value?: string, icon?: string, render?: Function}[]} config.items - Tab items
|
|
18
|
+
* @param {number} [config.active=0] - Initially active tab index
|
|
19
|
+
* @param {string} [config.value] - Initially active tab value
|
|
20
|
+
* @param {'default'|'outline'|'border'|'minimal'|'ghost'} [config.variant] - Tab style
|
|
21
|
+
* @param {'primary'|'red'|'green'|'blue'} [config.color] - Tab color
|
|
22
|
+
* @param {'round'} [config.shape] - Tab button shape
|
|
23
|
+
* @param {boolean} [config.center] - Center tabs
|
|
24
|
+
* @param {boolean} [config.shade] - Shaded background
|
|
25
|
+
* @param {Function} [config.onChange] - Called with (value, item)
|
|
26
|
+
* @returns {HTMLDivElement} Tabs element with .value getter/setter
|
|
27
|
+
* @example
|
|
28
|
+
* tabs({ variant: 'outline', items: [{ label: 'Tab 1', render: () => content1 }] })
|
|
29
|
+
* @see buttonGroup - Alias for tabs without content panel
|
|
30
|
+
*/
|
|
31
|
+
export function tabs(...args) {
|
|
32
|
+
const {
|
|
33
|
+
parent,
|
|
34
|
+
items: passedItems = [],
|
|
35
|
+
active = 0,
|
|
36
|
+
value,
|
|
37
|
+
variant,
|
|
38
|
+
color,
|
|
39
|
+
shape,
|
|
40
|
+
appendButtons,
|
|
41
|
+
appendContent,
|
|
42
|
+
border,
|
|
43
|
+
shade,
|
|
44
|
+
padding,
|
|
45
|
+
width,
|
|
46
|
+
asButtonGroup,
|
|
47
|
+
props,
|
|
48
|
+
...rest
|
|
49
|
+
} = parseArgs(...args);
|
|
50
|
+
|
|
51
|
+
const cb = getCallback('onChange', rest);
|
|
52
|
+
const items = passedItems.flat();
|
|
53
|
+
const tabBtns = [];
|
|
54
|
+
|
|
55
|
+
const isCentered = rest.align === 'center' || rest.center;
|
|
56
|
+
const isColorizable = ['border', 'ghost'].includes(variant);
|
|
57
|
+
const buttonVariant = variant === 'border' ? 'outline' : (variant || 'none');
|
|
58
|
+
const withBorder = border !== false && !shade;
|
|
59
|
+
|
|
60
|
+
let output;
|
|
61
|
+
let current = value || getItemValue(items[active]) || '';
|
|
62
|
+
|
|
63
|
+
const renderPanel = (v) => {
|
|
64
|
+
output.innerHTML = '';
|
|
65
|
+
const item = items.find((o) => getItemValue(o) === String(v));
|
|
66
|
+
if (!item?.render) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const out = item.render();
|
|
71
|
+
const tab = el('div', { class: bem.el('tab') });
|
|
72
|
+
|
|
73
|
+
if (typeof out === 'string') {
|
|
74
|
+
tab.innerHTML = out;
|
|
75
|
+
} else if (Array.isArray(out)) {
|
|
76
|
+
for (const o of out) tab.appendChild(o);
|
|
77
|
+
} else {
|
|
78
|
+
tab.appendChild(out);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
output.appendChild(tab);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const setActive = (v, skipCallback) => {
|
|
85
|
+
current = String(v);
|
|
86
|
+
for (const btn of tabBtns) {
|
|
87
|
+
const isActive = btn.dataset.value === current;
|
|
88
|
+
btn.setAttribute('aria-selected', isActive);
|
|
89
|
+
const item = items.find((o) => getItemValue(o) === btn.dataset.value) || {};
|
|
90
|
+
const col = item.color || color;
|
|
91
|
+
btn.dataset.uiCol = isActive && col && isColorizable
|
|
92
|
+
? `${variant !== 'ghost' ? 'sub-' : ''}${col}`
|
|
93
|
+
: '';
|
|
94
|
+
btn.tabIndex = isActive ? 0 : -1;
|
|
95
|
+
}
|
|
96
|
+
!asButtonGroup && renderPanel(current);
|
|
97
|
+
!skipCallback && cb?.(current);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const focusTabAt = (i) => {
|
|
101
|
+
if (!tabBtns.length) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
tabBtns[(i + tabBtns.length) % tabBtns.length].focus();
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const handleKeydown = (e, idx, val) => {
|
|
108
|
+
const keys = {
|
|
109
|
+
ArrowRight: () => focusTabAt(idx + 1),
|
|
110
|
+
ArrowLeft: () => focusTabAt(idx - 1),
|
|
111
|
+
Home: () => focusTabAt(0),
|
|
112
|
+
End: () => focusTabAt(tabBtns.length - 1),
|
|
113
|
+
Enter: () => setActive(val),
|
|
114
|
+
' ': () => setActive(val)
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const action = keys[e.key];
|
|
118
|
+
if (!action) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
action();
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const root = el('div', parent, {
|
|
126
|
+
...rest,
|
|
127
|
+
class: [
|
|
128
|
+
bem(),
|
|
129
|
+
shape && bem(shape),
|
|
130
|
+
`w-${width || 'full'}`,
|
|
131
|
+
'flex',
|
|
132
|
+
'flex-col',
|
|
133
|
+
!withBorder && 'gap-2',
|
|
134
|
+
shade && bem('shade'),
|
|
135
|
+
configToClasses(props, ['shade']),
|
|
136
|
+
rest.class
|
|
137
|
+
],
|
|
138
|
+
children: [
|
|
139
|
+
el('div', {
|
|
140
|
+
class: [
|
|
141
|
+
bem.el('buttons'),
|
|
142
|
+
bem(variant || 'default'),
|
|
143
|
+
isCentered && ['justify-center', 'ma'],
|
|
144
|
+
],
|
|
145
|
+
attrs: { role: 'tablist' },
|
|
146
|
+
children: items.map((o, i) => {
|
|
147
|
+
const val = getItemValue(o);
|
|
148
|
+
const btn = button({
|
|
149
|
+
variant: buttonVariant,
|
|
150
|
+
label: o.label,
|
|
151
|
+
icon: o.icon,
|
|
152
|
+
shape,
|
|
153
|
+
dataset: { value: val },
|
|
154
|
+
attrs: { role: 'tab' },
|
|
155
|
+
aria: { selected: 'false' },
|
|
156
|
+
onmousedown: () => setActive(val),
|
|
157
|
+
onkeydown: (e) => handleKeydown(e, i, val)
|
|
158
|
+
});
|
|
159
|
+
tabBtns.push(btn);
|
|
160
|
+
return btn;
|
|
161
|
+
})
|
|
162
|
+
}),
|
|
163
|
+
!asButtonGroup && el('div', {
|
|
164
|
+
class: [
|
|
165
|
+
bem.el('panel'),
|
|
166
|
+
withBorder && 'border',
|
|
167
|
+
Number.isInteger(padding) ? `pad-${padding}` : (withBorder || shade) && 'pad'
|
|
168
|
+
],
|
|
169
|
+
ref: (e) => output = e
|
|
170
|
+
})
|
|
171
|
+
]
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
appendButtons?.append(root.children[0]);
|
|
175
|
+
appendContent?.append(root.children[1]);
|
|
176
|
+
|
|
177
|
+
setActive(current, true);
|
|
178
|
+
withValue(root, () => current, setActive);
|
|
179
|
+
|
|
180
|
+
return root;
|
|
181
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/* ====================================================
|
|
2
|
+
Text Primitives
|
|
3
|
+
Shared typography building blocks
|
|
4
|
+
==================================================== */
|
|
5
|
+
|
|
6
|
+
/* Title - primary headings */
|
|
7
|
+
.ui-title {
|
|
8
|
+
line-height: 1.2;
|
|
9
|
+
font-weight: 650;
|
|
10
|
+
font-size: 1.1em;
|
|
11
|
+
margin: 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.ui-title--large {
|
|
15
|
+
font-weight: 700;
|
|
16
|
+
font-size: 1.5em;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* Description - secondary text, usually paired with title */
|
|
20
|
+
.ui-description {
|
|
21
|
+
line-height: 1.4;
|
|
22
|
+
margin: 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Label - form labels and small headings */
|
|
26
|
+
.ui-label {
|
|
27
|
+
display: block;
|
|
28
|
+
font-size: 0.875em;
|
|
29
|
+
font-weight: 500;
|
|
30
|
+
color: var(--text);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.ui-label--soft {
|
|
34
|
+
font-weight: 400;
|
|
35
|
+
opacity: 0.7;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Truncate - single line text with ellipsis */
|
|
39
|
+
.ui-truncate {
|
|
40
|
+
white-space: nowrap;
|
|
41
|
+
overflow: hidden;
|
|
42
|
+
text-overflow: ellipsis;
|
|
43
|
+
min-width: 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Name - bold truncated text (for avatars, lists) */
|
|
47
|
+
.ui-name {
|
|
48
|
+
font-weight: 500;
|
|
49
|
+
font-size: .95em;
|
|
50
|
+
white-space: nowrap;
|
|
51
|
+
overflow: hidden;
|
|
52
|
+
text-overflow: ellipsis;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Subtitle - muted truncated text */
|
|
56
|
+
.ui-subtitle {
|
|
57
|
+
font-size: .85em;
|
|
58
|
+
color: var(--muted);
|
|
59
|
+
white-space: nowrap;
|
|
60
|
+
overflow: hidden;
|
|
61
|
+
text-overflow: ellipsis;
|
|
62
|
+
}
|