Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50866cc78f | ||
|
|
cabf14249b | ||
|
|
e64d5d06fe | ||
|
|
0b13066168 | ||
|
|
a9ca4ba5e7 | ||
|
|
6fc2fda97a | ||
|
|
70a8782935 | ||
|
|
966974b2ad | ||
|
|
7abd3a043d | ||
|
|
a3848d9bf2 | ||
|
|
c48b4a2c14 | ||
|
|
9119b18039 | ||
|
|
f0c8acb5e2 | ||
|
|
b229280dfb | ||
|
|
9e16f81f1e | ||
|
|
2b87691f18 | ||
|
|
9157e02ec6 | ||
|
|
04c248f773 | ||
|
|
284a05bf27 | ||
|
|
9b1cf5a739 | ||
|
|
f62cbe2ba5 | ||
|
|
5c2755af78 | ||
|
|
c53cb9778e | ||
|
|
93bcc496af | ||
|
|
b6e5622a04 | ||
|
|
6af9fab92a | ||
|
|
739fcbe44f | ||
|
|
cbf0c2a6ef | ||
|
|
ca377c6137 | ||
|
|
d9aa826573 | ||
|
|
03d6e6c208 | ||
|
|
abc68238ad | ||
|
|
79eacfddfd | ||
|
|
b319364262 | ||
|
|
23d1cb40b5 | ||
|
|
a51eaa5cc5 | ||
|
|
fee99dfd20 | ||
|
|
234b72c12d | ||
|
|
5fadbd1e69 | ||
|
|
bf1209e932 | ||
|
|
f77700d32d | ||
|
|
c1054bf087 | ||
|
|
838d8a6278 | ||
|
|
76fc06eac3 | ||
|
|
42afd59cb9 | ||
|
|
80dc47e92a | ||
|
|
74e8a68890 | ||
|
|
fe9ed309fb | ||
|
|
d3f49d98d2 | ||
|
|
53b5d2def4 | ||
|
|
8bc9457a5c | ||
|
|
13aa314b24 | ||
|
|
7cef132b16 | ||
|
|
8bdf1d0f0c | ||
|
|
a84e524a7a | ||
|
|
622c163aea | ||
|
|
00a4bf74e7 | ||
|
|
b5abe9dfbd | ||
|
|
76be97d7bc | ||
|
|
14d5509ef1 | ||
|
|
6528649fe8 | ||
|
|
868482e8ae | ||
|
|
c0e68efb79 | ||
|
|
88fdfaa2e6 | ||
|
|
bcc5d358e5 | ||
|
|
4a081819dd | ||
|
|
dfba804282 | ||
|
|
486b6e2d42 | ||
|
|
3e9e53aaa3 | ||
|
|
7fa0ea01cc | ||
|
|
7d51ae6b3c | ||
|
|
94f39804e2 | ||
|
|
5c83da7dae | ||
|
|
37f95ffcf9 | ||
|
|
97fe57b281 | ||
|
|
89eeb8bee1 | ||
|
|
e77f08ee41 | ||
|
|
fab8a54e1e | ||
|
|
a201b7db4f | ||
|
|
243186dc44 | ||
|
|
39c35cf448 | ||
|
|
8e4f441734 | ||
|
|
2459c209ed | ||
|
|
7f1340606f | ||
|
|
69fb9cfd41 | ||
|
|
872325ce4d | ||
|
|
ed999ce89a | ||
|
|
eb7f21d2dc | ||
|
|
d46d226722 | ||
|
|
4944f1e034 | ||
|
|
3e2ff3d4e0 | ||
|
|
4cc7dd4bcc | ||
|
|
ba68995a85 | ||
|
|
2fc3281fe8 | ||
|
|
9db68f68cf | ||
|
|
4540b8fdb5 | ||
|
|
93694e5506 | ||
|
|
ab1dc498f3 | ||
|
|
5a8d302a05 | ||
|
|
9c0a227de6 | ||
|
|
b419e7e9a0 | ||
|
|
f49b77a663 | ||
|
|
3a28257a00 | ||
|
|
60452d08d0 | ||
|
|
399a593a7f | ||
|
|
2366c16cbd | ||
|
|
2c895fa69c | ||
|
|
26499a107d | ||
|
|
7ff154498e | ||
|
|
c658889710 | ||
|
|
235ea1c3eb | ||
|
|
bbd732f084 | ||
|
|
5e9aae9d37 | ||
|
|
e15fd9956e | ||
|
|
90057de1cf | ||
|
|
85b8b91c1e | ||
|
|
90bbcbee85 | ||
|
|
b173966d8b | ||
|
|
279178a0f7 | ||
|
|
7e684040b1 | ||
|
|
f05fd429c0 | ||
|
|
1e7da05905 | ||
|
|
ba26f1935c | ||
|
|
1fe492da02 | ||
|
|
91c80d126f | ||
|
|
a07b68c0ee | ||
|
|
0cdbcdd450 | ||
|
|
55b91f4b5f | ||
|
|
ce2df116f8 | ||
|
|
2d6a74669e | ||
|
|
45c927a2eb | ||
|
|
cf2c7bf832 | ||
|
|
b35c09c0b1 | ||
|
|
ee9a3480c5 | ||
|
|
c03e5639b4 | ||
|
|
9da86fc98a | ||
|
|
b6fcdcc06a | ||
|
|
662a75bb26 | ||
|
|
e8e004f571 | ||
|
|
5bac9937d0 | ||
|
|
db5d29c6e2 | ||
|
|
6cfe471e79 | ||
|
|
5661dceabd | ||
|
|
a029a4e88b | ||
|
|
7bb496a46c | ||
|
|
932bc5f106 | ||
|
|
1e5218288b | ||
|
|
2059bff5d5 | ||
|
|
7cc17f9244 | ||
|
|
430dd47267 | ||
|
|
191025ae7c | ||
|
|
33840263e6 | ||
|
|
4b49fe9092 | ||
|
|
9e59e5f0bd | ||
|
|
4d83974ba4 | ||
|
|
777a1d9e0c | ||
|
|
3f70e6ab44 | ||
|
|
5d1f5bb532 | ||
|
|
8ca528b97b | ||
|
|
a101127646 | ||
|
|
c9320b05a8 | ||
|
|
06eb9e79b1 | ||
|
|
ef550da718 | ||
|
|
282bd4a26b | ||
|
|
23ad3edc0f | ||
|
|
e98829fa80 | ||
|
|
c785a06d0d | ||
|
|
5d021bf39e | ||
|
|
24cb107711 | ||
|
|
e16014b0a6 | ||
|
|
499356744d | ||
|
|
9419a78344 | ||
|
|
e16c53a5f7 | ||
|
|
974e413f72 | ||
|
|
ba9a522e67 | ||
|
|
14b2567b39 | ||
|
|
45d8004ad0 | ||
|
|
9a8f5ca76b | ||
|
|
a39aaa9677 | ||
|
|
e87779801b | ||
|
|
4db0fc4b47 | ||
|
|
ea25355308 | ||
|
|
45b7d6e62e | ||
|
|
8577262450 | ||
|
|
c1b033ae26 | ||
|
|
4899a17ea6 | ||
|
|
5ecba26daa | ||
|
|
0a9c481021 | ||
|
|
1921550a89 | ||
|
|
8793ddf009 | ||
|
|
7cad2203b6 | ||
|
|
fe243e534e | ||
|
|
a3d8535576 | ||
|
|
4e54616efc | ||
|
|
ed6c1246bc | ||
|
|
1e35546fa9 | ||
|
|
84189e159a | ||
|
|
e6fa431c2e | ||
|
|
bfa3169c67 | ||
|
|
15a5c388bf | ||
|
|
15f721b13e | ||
|
|
9998c1dac3 | ||
|
|
4fd803cf05 | ||
|
|
fb08507748 | ||
|
|
3b62532cf0 | ||
|
|
805016abd2 | ||
|
|
9cd06d9ab3 | ||
|
|
da5a607faf | ||
|
|
c658d0cec6 | ||
|
|
3331e29c49 | ||
|
|
a174422e63 | ||
|
|
0ec9b57341 | ||
|
|
39674dcc10 | ||
|
|
ef8bcd893e | ||
|
|
8cbd33e678 | ||
|
|
38f0ce81c4 | ||
|
|
36969d2b09 | ||
|
|
91ad2dcfb0 | ||
|
|
9387595834 | ||
|
|
9f4991b84c | ||
|
|
abc1fe3d46 | ||
|
|
d69e4749c5 | ||
|
|
170ef8a96d | ||
|
|
eba349c31f | ||
|
|
34aaa5c87a | ||
|
|
81a10609f8 | ||
|
|
69b9132541 | ||
|
|
dee9360285 | ||
|
|
91c8a22d27 | ||
|
|
42e05d138c | ||
|
|
01dfebe632 | ||
|
|
717666ea23 | ||
|
|
202f5c4624 | ||
|
|
3520c13545 | ||
|
|
a13db9dea9 | ||
|
|
69b676c887 | ||
|
|
98ef769cc5 | ||
|
|
fb74dd9860 | ||
|
|
4bdbdf7650 | ||
|
|
f829cf09c6 | ||
|
|
1421a5c943 | ||
|
|
ec92f88807 | ||
|
|
d67fd924f6 | ||
|
|
5ae01bf1d1 | ||
|
|
d9e15dda77 | ||
|
|
e37eb1543f | ||
|
|
eee6b2cd9b | ||
|
|
5e56c99f73 | ||
|
|
6c303bfedf | ||
|
|
a36331636c | ||
|
|
9a80568783 | ||
|
|
a8027a9642 | ||
|
|
36ccc01a3b | ||
|
|
054d9dd62c | ||
|
|
6676c15035 | ||
|
|
a08323a205 | ||
|
|
0434bb75c3 | ||
|
|
db9bbb6526 | ||
|
|
1134f188c2 | ||
|
|
ce48b1e1f2 | ||
|
|
6f37d40e27 | ||
|
|
f4e135f41a | ||
|
|
698491c213 | ||
|
|
106ab4fa62 | ||
|
|
31f766b08b | ||
|
|
bfd9435bf1 | ||
|
|
85f1fc4bdf | ||
|
|
8a467183ae | ||
|
|
7f7982dc33 | ||
|
|
13f1c32456 | ||
|
|
712fb800b4 | ||
|
|
5848ce8b31 | ||
|
|
22360f7246 | ||
|
|
6a54d4a442 | ||
|
|
4edad7ab4f | ||
|
|
b96744043a | ||
|
|
3eff625216 | ||
|
|
e8f4885aa5 | ||
|
|
fbc529dcf2 | ||
|
|
ba15176f59 | ||
|
|
80e5590847 | ||
|
|
e6142672bc | ||
|
|
642ec1c620 | ||
|
|
ad961e19a9 | ||
|
|
5a877ca98c | ||
|
|
cfbe192062 | ||
|
|
0f328b85bc | ||
|
|
67d7d57028 | ||
|
|
7700d5da80 | ||
|
|
7f306a9964 | ||
|
|
083ad913ee | ||
|
|
3c6bdc3e25 | ||
|
|
1bd0cd0533 | ||
|
|
b2ecc9f1a0 | ||
|
|
34f5d96e59 | ||
|
|
1a27722c5d | ||
|
|
2c759fde3d | ||
|
|
4db89f3790 | ||
|
|
69848dfed0 | ||
|
|
a80962e4fc | ||
|
|
0cd53ae01b | ||
|
|
5dc51dc568 | ||
|
|
69e7cdc9de | ||
|
|
5bd4f95694 | ||
|
|
766e7017c9 | ||
|
|
86ef1af4c6 | ||
|
|
a1e21e3643 | ||
|
|
d6f7f14f9b | ||
|
|
5771e92265 | ||
|
|
e9fbb2e9ab | ||
|
|
2f389a5043 | ||
|
|
603c841b7a | ||
|
|
b25c554d10 | ||
|
|
53d2831611 | ||
|
|
2e1d103bd4 | ||
|
|
bb55a023b1 | ||
|
|
10da394158 | ||
|
|
f80001d2fe | ||
|
|
7eb5afac80 | ||
|
|
323b18a79c | ||
|
|
8c6506ff68 | ||
|
|
e6cec7fb9b | ||
|
|
030707ccfe | ||
|
|
c598dcfc88 | ||
|
|
29e51fc6d9 | ||
|
|
b89771d7d0 | ||
|
|
e15137016c | ||
|
|
cb8ba1bdce | ||
|
|
5126376e6d | ||
|
|
54b1877258 | ||
|
|
1b3b1bcaf8 | ||
|
|
6671dcb907 | ||
|
|
099e01adfd | ||
|
|
7d641d91f5 | ||
|
|
08ead8b9d1 | ||
|
|
6a150256e6 | ||
|
|
5946d11f38 | ||
|
|
661697fba8 | ||
|
|
800c9f894f | ||
|
|
b9cba1e5a9 | ||
|
|
b8da02b088 | ||
|
|
a38d41a6e5 | ||
|
|
1bb471b5d5 | ||
|
|
4497f32b60 | ||
|
|
08c333fc82 | ||
|
|
c257a3f20c | ||
|
|
c988f0a54d | ||
|
|
1d07c38fb4 | ||
|
|
f6a2b64c2b | ||
|
|
412e5d5a1d | ||
|
|
8cc89f6a45 | ||
|
|
07e3620194 | ||
|
|
00cc8f0364 | ||
|
|
9a2e745008 | ||
|
|
d406d5956e | ||
|
|
95b6b5f077 | ||
|
|
752794fbbb | ||
|
|
82def836a1 | ||
|
|
29328539fe | ||
|
|
59c40734d7 | ||
|
|
042cb42552 | ||
|
|
0f466ecf38 | ||
|
|
7cca3ae109 | ||
|
|
c0eba739fc | ||
|
|
6be623963a | ||
|
|
0aeb5a6c4c | ||
|
|
057ce672b8 | ||
|
|
199512067e | ||
|
|
0991dad745 | ||
|
|
5ea764e165 | ||
|
|
8f8d5e19d7 | ||
|
|
b4c511173f | ||
|
|
6057935361 | ||
|
|
4872507e33 | ||
|
|
d360dc3932 | ||
|
|
d40d837a82 | ||
|
|
d54117736d | ||
|
|
a30f14fdfd | ||
|
|
fc09f7e034 | ||
|
|
158733ebb9 | ||
|
|
b9bb97b841 | ||
|
|
be533ced37 | ||
|
|
ff4659bb01 | ||
|
|
8c2e63ad6f | ||
|
|
21252ad4ff | ||
|
|
ee7ade5eb3 | ||
|
|
7941efa617 | ||
|
|
5b29d45ec5 | ||
|
|
78c2021396 | ||
|
|
1fbea2e6ea | ||
|
|
a3b0b5471e | ||
|
|
c80f9236e3 | ||
|
|
8b5cbcba79 | ||
|
|
927ff10074 | ||
|
|
59d3596c50 | ||
|
|
59cd091d19 | ||
|
|
ff9f722b34 | ||
|
|
7dc7e2edf7 | ||
|
|
01be9e51d2 | ||
|
|
e2d19b9173 | ||
|
|
f06c8767c8 | ||
|
|
aa89e7b15f | ||
|
|
c5c4498b29 | ||
|
|
618b972bb7 | ||
|
|
c77f953faa | ||
|
|
5f94f59f20 | ||
|
|
f345d07a13 | ||
|
|
9fc087b8b7 | ||
|
|
74db9f115f | ||
|
|
376b1edd78 | ||
|
|
05d72c9203 | ||
|
|
15f3ffd14c | ||
|
|
8e3997a686 | ||
|
|
8e9862e64d | ||
|
|
abe7cc1a8c | ||
|
|
d96c63d11f | ||
|
|
6ae72a2f26 | ||
|
|
0f59a7d507 | ||
|
|
2b40b752e5 | ||
|
|
c0b2a785be | ||
|
|
64bb6f6631 | ||
|
|
de67f6d7bd | ||
|
|
0148f683df | ||
|
|
ec4b53e129 | ||
|
|
b6c4999ff3 | ||
|
|
d08624b570 | ||
|
|
1035be332c | ||
|
|
a2d9a677ed | ||
|
|
ed45e1fee6 | ||
|
|
36dad89f50 | ||
|
|
e8878bbea5 | ||
|
|
764e9d7dfa | ||
|
|
08f25bf4b7 | ||
|
|
32df3366ef | ||
|
|
239ca8d6aa | ||
|
|
9770bf226b | ||
|
|
e76035712a | ||
|
|
bc83a8bcbd | ||
|
|
3e4070a7bf | ||
|
|
4aba98cdc5 | ||
|
|
d9877ad2fe | ||
|
|
1e7099fc01 | ||
|
|
9c3fb62e02 | ||
|
|
4355763d8f | ||
|
|
bd26ce04e6 | ||
|
|
919a50362d | ||
|
|
2aa16e65cf | ||
|
|
a115421f71 | ||
|
|
85bfaf2a63 | ||
|
|
4866271348 | ||
|
|
4be525d899 | ||
|
|
e8bf2e79a4 | ||
|
|
06e3ff8eec | ||
|
|
a5b2495071 | ||
|
|
5c43583cd4 | ||
|
|
18457f1bc4 | ||
|
|
dd9d81a87e | ||
|
|
90a8813c5a | ||
|
|
6419deeb63 | ||
|
|
951db810b9 | ||
|
|
39d35a3c6f | ||
|
|
21b20405c5 | ||
|
|
7b7d671d21 | ||
|
|
2bb5a201dd | ||
|
|
c9ab3a5495 | ||
|
|
558b17af7f | ||
|
|
1bb37f7ff8 | ||
|
|
18459fddc0 | ||
|
|
508e5bf64c | ||
|
|
67c646e454 | ||
|
|
ed48513606 | ||
|
|
9958264445 | ||
|
|
fdd1026068 | ||
|
|
74cd55a889 | ||
|
|
221fa8c886 | ||
|
|
35f99634f4 | ||
|
|
98869c8487 | ||
|
|
f9d6747b20 | ||
|
|
3972ac2321 | ||
|
|
669f61020e | ||
|
|
1221da2e05 | ||
|
|
7a4d93de71 | ||
|
|
194e0ee944 | ||
|
|
8dfa4ba837 | ||
|
|
5e99c888a9 | ||
|
|
197e932088 | ||
|
|
121a38315f | ||
|
|
50db0dc478 | ||
|
|
575e24aff5 | ||
|
|
28dcb82bc0 | ||
|
|
1a645f10a6 | ||
|
|
5c8631f304 | ||
|
|
8f5206680f | ||
|
|
129938001c | ||
|
|
a18c2a80ce | ||
|
|
ad40323cb7 | ||
|
|
a7eb5905fe | ||
|
|
2ab1000b5a | ||
|
|
4911e876f9 | ||
|
|
b3cd7f559c | ||
|
|
5f09c02687 | ||
|
|
20fbba7722 | ||
|
|
0cc2fdecb7 | ||
|
|
8a8808d703 | ||
|
|
d21da87902 | ||
|
|
140c80e32b | ||
|
|
ab54778134 | ||
|
|
79c80b9e60 | ||
|
|
7ff49c9635 | ||
|
|
8dbbe9825e | ||
|
|
3d648f35db | ||
|
|
b65df02c72 | ||
|
|
be06468ff9 | ||
|
|
234bc47bf2 | ||
|
|
aa8985de74 | ||
|
|
075882d1d9 | ||
|
|
267523dbc3 | ||
|
|
536148f8bc | ||
|
|
a8d795eb71 | ||
|
|
d2517b0344 | ||
|
|
f212ebe0a9 | ||
|
|
c1ab97f253 | ||
|
|
724d5e1c84 | ||
|
|
c2445e386d | ||
|
|
0e8c62e484 | ||
|
|
2e4d0c65d5 | ||
|
|
d8c1b5d05d | ||
|
|
ffc05bae7e | ||
|
|
95a0d9ddb1 | ||
|
|
d362cd4788 | ||
|
|
b0b78ddc41 | ||
|
|
dc5f181b93 | ||
|
|
bf4f628df3 | ||
|
|
47ef5ceae7 | ||
|
|
6157c01213 | ||
|
|
9b09d96158 | ||
|
|
f32c5479ce | ||
|
|
e0266d2a49 | ||
|
|
c87d539f8d | ||
|
|
1243795ae5 | ||
|
|
a0b5b1dd3c | ||
|
|
e39f1b11b1 | ||
|
|
527b0f1b60 | ||
|
|
c0b4d29143 | ||
|
|
f9055747c7 | ||
|
|
c8560aa15e | ||
|
|
f9b4949b14 | ||
|
|
33c1b94aed | ||
|
|
9bc988034c | ||
|
|
d0c8af0089 | ||
|
|
0f111e426e | ||
|
|
935769e768 | ||
|
|
46cfc080b7 | ||
|
|
3e2a9d58cc | ||
|
|
9f5a9d31af | ||
|
|
cf8f54fe13 | ||
|
|
21c84cda35 | ||
|
|
018e9cd911 | ||
|
|
1714bf76ea | ||
|
|
1f1d484eee | ||
|
|
0737ddf80a | ||
|
|
5b96b360c8 | ||
|
|
a5eea26642 | ||
|
|
eb2f482023 | ||
|
|
04e419ba08 | ||
|
|
54a7de8ab2 | ||
|
|
754c73c98a | ||
|
|
c72a1c09b7 | ||
|
|
254cfc9791 | ||
|
|
db24824250 | ||
|
|
8c5a9f32be | ||
|
|
42dc6f2d6f | ||
|
|
09a94343ce | ||
|
|
cd481922d9 | ||
|
|
77767dfbea | ||
|
|
f388383190 | ||
|
|
b21f6fb183 | ||
|
|
6c01c31447 | ||
|
|
52bc1deecd | ||
|
|
25ea162565 | ||
|
|
6047052fdb | ||
|
|
e5157e5c37 | ||
|
|
e034d05c91 | ||
|
|
f2bbe3000c | ||
|
|
41fe7aec08 | ||
|
|
60fa8d198e | ||
|
|
f3c0090422 | ||
|
|
66084276d4 | ||
|
|
3b123ff799 | ||
|
|
1c76b5e47c | ||
|
|
57e5da1e63 | ||
|
|
7f1f6d3343 | ||
|
|
6ed9574a18 | ||
|
|
7c3740c8f4 | ||
|
|
3e81ba1f1a | ||
|
|
e85a120672 | ||
|
|
b91443b595 | ||
|
|
df2badbe89 | ||
|
|
da9cdd8fd6 | ||
|
|
8226fea5b2 | ||
|
|
fbb57da420 | ||
|
|
75338609e2 | ||
|
|
8f53f8b592 | ||
|
|
aa227aa5ba | ||
|
|
19bdc90e38 | ||
|
|
69d529bbe8 | ||
|
|
070f1c3824 | ||
|
|
849e6b3038 | ||
|
|
a0f859e68d | ||
|
|
2327b8e1f4 | ||
|
|
a0560c9411 | ||
|
|
7377a01af9 | ||
|
|
7eaebb8f6d | ||
|
|
68c92aa2b0 | ||
|
|
43a4e04bf9 | ||
|
|
8e73fd6fb6 | ||
|
|
79b3115e0b | ||
|
|
f05628fb23 | ||
|
|
86c8db25f7 | ||
|
|
99a03ca026 | ||
|
|
290297af00 | ||
|
|
bdcf7098a1 | ||
|
|
de512627ce | ||
|
|
af76c459dc | ||
|
|
2fb8b3527c | ||
|
|
fe13a96567 | ||
|
|
db610914ef | ||
|
|
410d318db2 | ||
|
|
fa25586d32 | ||
|
|
4ba63ec643 | ||
|
|
56dcbe713c | ||
|
|
f2f32ddf35 | ||
|
|
600a20bc1e | ||
|
|
5f61fad5ab | ||
|
|
a67cb0ef2a | ||
|
|
9cf60319c2 | ||
|
|
19505421c6 | ||
|
|
9700a3467f | ||
|
|
314a969aff | ||
|
|
533326fef0 | ||
|
|
3738275280 | ||
|
|
d5e0d4e769 | ||
|
|
818c61d1ef | ||
|
|
1038f37f7f | ||
|
|
70c7083823 | ||
|
|
1b34ed829f | ||
|
|
efc833c58a | ||
|
|
81ed5f7031 | ||
|
|
c6babe939b | ||
|
|
0e3fc6c5cf | ||
|
|
f09bf82384 | ||
|
|
270a90fca3 | ||
|
|
0bdc88694d | ||
|
|
dce7c53e4f | ||
|
|
c2b3d0e311 | ||
|
|
7f97bac978 | ||
|
|
a096054b3c | ||
|
|
3cef56bf0e | ||
|
|
7cf60ccadd | ||
|
|
763510638e | ||
|
|
727efb8aa5 | ||
|
|
8a18dcd9ae | ||
|
|
3a672f5925 | ||
|
|
8ece7b73c1 | ||
|
|
e8f29c5c3b | ||
|
|
540a86c813 | ||
|
|
0117fc9eb0 | ||
|
|
07647ad576 | ||
|
|
2ce7e068a4 | ||
|
|
412227426e | ||
|
|
e09fdb19a4 | ||
|
|
1bce47d3df | ||
|
|
5276eea100 | ||
|
|
88ca2d1d2a | ||
|
|
a03905dcb5 | ||
|
|
c2f4c6d718 | ||
|
|
c77ab3a0dd | ||
|
|
cc3225d2d1 | ||
|
|
1b57c12efe | ||
|
|
6b0009d660 | ||
|
|
2403d9121b | ||
|
|
e1e6413e4d | ||
|
|
8e5b0150f3 | ||
|
|
984bf2a0f7 | ||
|
|
18ac03b505 | ||
|
|
17f789968d | ||
|
|
c994d56efc | ||
|
|
57fb484e03 | ||
|
|
6fb3f14122 | ||
|
|
ff066a1ed5 | ||
|
|
1a21d39d36 | ||
|
|
9a26635bab | ||
|
|
f42a257b7e | ||
|
|
07c2b855d9 | ||
|
|
5640d1aade | ||
|
|
e667b6c082 | ||
|
|
a42b2a7343 | ||
|
|
1e2e37cfc1 | ||
|
|
295f9c82d2 | ||
|
|
d901ee1224 | ||
|
|
9403c5cc71 | ||
|
|
480bebd8f0 | ||
|
|
bd39c2829c | ||
|
|
c7b448a5e8 | ||
|
|
58ffb8c048 | ||
|
|
a2b6e1d728 | ||
|
|
989467ebe0 | ||
|
|
c59a5d2088 | ||
|
|
8de3f6cd92 | ||
|
|
29b402ba07 | ||
|
|
32e6d24741 | ||
|
|
2b9a42adb6 | ||
|
|
facaee251c | ||
|
|
9e7e0e450d | ||
|
|
1d69ceba86 | ||
|
|
0f1cbe73df | ||
|
|
a07116b071 | ||
|
|
8136c7370b | ||
|
|
b2edeb4ce2 | ||
|
|
f207346567 | ||
|
|
f360f34c25 | ||
|
|
1af98a1b94 | ||
|
|
fede6df5b9 | ||
|
|
dc96d7f9cf | ||
|
|
174c54a00c | ||
|
|
0c9dfc5404 | ||
|
|
cafe648d78 | ||
|
|
8c36ca3943 | ||
|
|
aeb968a00e | ||
|
|
e5a3ff56e3 | ||
|
|
14b2fd0fbb | ||
|
|
c674ed0693 | ||
|
|
15c71cddb5 | ||
|
|
4a32aaa0e6 | ||
|
|
90ff6f2bcf | ||
|
|
d1111419fc | ||
|
|
221796c4d3 | ||
|
|
7f9b8fdd02 | ||
|
|
b260a89462 | ||
|
|
743dfecf83 | ||
|
|
d8fd3384a7 | ||
|
|
b95f47243f | ||
|
|
a6bdf3ccc2 | ||
|
|
d9a504621a | ||
|
|
fb6b47f283 | ||
|
|
5843a9036f | ||
|
|
c0c12a4027 | ||
|
|
3171a28f79 | ||
|
|
42bd7a6c82 | ||
|
|
b2848e3a7e | ||
|
|
dee99fd5f6 | ||
|
|
fffdec4e5a | ||
|
|
ef7883fb46 | ||
|
|
43360de49e | ||
|
|
40b5a6cd63 | ||
|
|
17acccf3c8 | ||
|
|
c804549138 | ||
|
|
e94944e32c | ||
|
|
983ec522ac | ||
|
|
8162576082 | ||
|
|
16c06b531e | ||
|
|
1cb9fae778 | ||
|
|
8d5900e552 | ||
|
|
2aa680316d | ||
|
|
2478434376 | ||
|
|
70db4b9094 | ||
|
|
65de31faa2 | ||
|
|
05abadf6e3 | ||
|
|
a972bd07dd | ||
|
|
5c8b8f4a95 | ||
|
|
b7893f42c6 | ||
|
|
89c3be7fdb | ||
|
|
75a6dea48d | ||
|
|
8efa2e2e26 | ||
|
|
c9e2dd3080 | ||
|
|
7a3d6b2ed7 | ||
|
|
2b94bc1e05 | ||
|
|
eb04464a35 | ||
|
|
64e9c5a5a6 | ||
|
|
909e7ba5fe | ||
|
|
453391a521 | ||
|
|
e04b6382bd | ||
|
|
e475d671ca | ||
|
|
ba8df39497 | ||
|
|
06e8b83431 | ||
|
|
07e65f6ee7 | ||
|
|
afcaadcec4 | ||
|
|
7c241a0f9d | ||
|
|
45d760bb8c | ||
|
|
8f4d0ab308 | ||
|
|
5f480caae7 | ||
|
|
5fb0d00f51 | ||
|
|
a6af312ec2 | ||
|
|
96a3eb8e90 | ||
|
|
bec13e0895 | ||
|
|
46edd3ffc6 | ||
|
|
037e29de6b | ||
|
|
6859336b71 | ||
|
|
67904a3be3 | ||
|
|
95e3c1f4a8 | ||
|
|
169cdbd6aa | ||
|
|
b7d2af4a43 | ||
|
|
0a978fc2e9 | ||
|
|
554f32c33b | ||
|
|
39e6914ccd | ||
|
|
3ac93f3796 | ||
|
|
0e3b7c33b0 | ||
|
|
c04f85da55 | ||
|
|
df3cb64265 | ||
|
|
026e6777a1 | ||
|
|
6b0ba23097 | ||
|
|
c070efc3e3 | ||
|
|
770eb3ac60 | ||
|
|
2c756221e6 | ||
|
|
4e42407af3 | ||
|
|
6c4c0f6195 | ||
|
|
0c035c5821 | ||
|
|
12c8d445bc | ||
|
|
54ff4d26f8 | ||
|
|
84f56db1c5 | ||
|
|
9d41b8d61f | ||
|
|
f24a56bea9 | ||
|
|
2bb370a1aa | ||
|
|
a0cbee1457 | ||
|
|
8d1f15eed9 | ||
|
|
db848556e2 | ||
|
|
7d1a326873 | ||
|
|
6efcbdc0d8 | ||
|
|
2f252b8b39 | ||
|
|
fd5adb97be | ||
|
|
bae2c4f72e | ||
|
|
cf2d163098 | ||
|
|
ffa90938d7 | ||
|
|
10ac81350c | ||
|
|
155ed253c6 | ||
|
|
05255d06ea | ||
|
|
e59667b74e | ||
|
|
9e5253da27 | ||
|
|
013672ad27 | ||
|
|
502ad0c46f | ||
|
|
094f9afc7b | ||
|
|
368c0ee148 | ||
|
|
dc79213dfe | ||
|
|
d26ea9adb2 | ||
|
|
a9868cf5c4 | ||
|
|
546eaf7604 | ||
|
|
513a4606cc | ||
|
|
4aabb3574d | ||
|
|
746a8657c3 | ||
|
|
db269af1eb | ||
|
|
d1ee9df661 | ||
|
|
1dbfe2b407 | ||
|
|
eeb73544d2 | ||
|
|
a668a219be | ||
|
|
e7794e4a25 | ||
|
|
91c8b5c2ff | ||
|
|
aa201de7c2 | ||
|
|
2e35fde43d | ||
|
|
848ba3392e | ||
|
|
147e538ba7 | ||
|
|
9932e9cdcd | ||
|
|
17b3df1e7c | ||
|
|
6eeaa0a334 | ||
|
|
55088e08e6 | ||
|
|
afb2860597 | ||
|
|
355c591e59 | ||
|
|
b343bad250 | ||
|
|
67d24c7115 | ||
|
|
d324009381 | ||
|
|
c582b34cbc |
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(pnpm registry:build:*)",
|
||||
"Bash(pnpm build:*)",
|
||||
"Bash(pnpm add:*)",
|
||||
"Bash(pnpm dev)",
|
||||
"Bash(jq:*)",
|
||||
"Bash(curl:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": ["@commitlint/config-conventional"]
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
node_modules/
|
||||
target/
|
||||
.next/
|
||||
build/
|
||||
dist/
|
||||
|
||||
/templates/
|
||||
/fixtures/
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/eslintrc",
|
||||
"root": true,
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"turbo",
|
||||
"prettier",
|
||||
"plugin:tailwindcss/recommended"
|
||||
],
|
||||
"plugins": ["tailwindcss"],
|
||||
"ignorePatterns": ["**/fixtures/**"],
|
||||
"rules": {
|
||||
"@next/next/no-html-link-for-pages": "off",
|
||||
"tailwindcss/no-custom-classname": "off",
|
||||
"tailwindcss/classnames-order": "error"
|
||||
},
|
||||
"settings": {
|
||||
"tailwindcss": {
|
||||
"callees": ["cn", "cva"],
|
||||
"config": "tailwind.config.cjs"
|
||||
},
|
||||
"next": {
|
||||
"rootDir": ["apps/*/"]
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"parser": "@typescript-eslint/parser"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
title: "[blocks]: "
|
||||
labels: ["Blocks Request"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Thanks for taking the time to create a block request! Please search open/closed requests before submitting, as the block or a similar one may have already been requested.
|
||||
|
||||
- type: textarea
|
||||
id: block-description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Tell us about your block request
|
||||
placeholder: "A dashboard for an e-commerce website showing sales, orders, and customers..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: block-example-url
|
||||
attributes:
|
||||
label: Example
|
||||
description: Link to an example of the block
|
||||
placeholder: ex. https://example.com
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,3 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [shadcn]
|
||||
@@ -1,85 +0,0 @@
|
||||
name: "Bug report"
|
||||
description: Report an issue
|
||||
title: '[bug]: '
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Thanks for taking the time to create a bug report. Please search open/closed issues before submitting, as the issue may have already been reported/addressed.
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
#### If you aren't sure this is a bug or not, please open a discussion instead:
|
||||
- [Discussions](https://github.com/shadcn-ui/ui/discussions/new?category=general)
|
||||
|
||||
- type: textarea
|
||||
id: bug-description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us how in the description. Thanks!
|
||||
placeholder: Bug description
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: components-affected
|
||||
attributes:
|
||||
label: Affected component/components
|
||||
description: Which shadcn/ui components are affected?
|
||||
placeholder: ex. Button, Checkbox...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: How to reproduce
|
||||
description: A step-by-step description of how to reproduce the bug.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: codesandbox-stackblitz
|
||||
attributes:
|
||||
label: Codesandbox/StackBlitz link
|
||||
description: |
|
||||
A link to a CodeSandbox or StackBlitz that includes a minimal reproduction of the problem. In rare cases when not applicable, you can link to a GitHub repository that we can easily run to recreate the issue. If a report is vague and does not have a reproduction, it will be closed without warning.
|
||||
|
||||
> [!CAUTION]
|
||||
> If you skip this step, this issue might be **labeled** with `please add a reproduction` and **closed**.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs
|
||||
description: "Please include browser console and server logs around the time this bug occurred. Optional if provided reproduction. Please try not to insert an image but copy paste the log text."
|
||||
render: bash
|
||||
|
||||
- type: textarea
|
||||
id: system-info
|
||||
attributes:
|
||||
label: System Info
|
||||
description: Information about browsers, system or binaries that's relevant.
|
||||
render: bash
|
||||
placeholder: System, Binaries, Browsers
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Before submitting
|
||||
description: By submitting this issue, you agree to follow our [Contributing Guidelines](https://github.com/shadcn-ui/ui/blob/main/CONTRIBUTING.md).
|
||||
options:
|
||||
- label: I've made research efforts and searched the documentation
|
||||
required: true
|
||||
- label: I've searched for existing issues
|
||||
required: true
|
||||
@@ -1,5 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Get Help
|
||||
url: https://github.com/shadcn-ui/ui/discussions/new?category=general
|
||||
about: If you can't get something to work the way you expect, open a question in our discussion forums.
|
||||
@@ -1,55 +0,0 @@
|
||||
name: "Feature request"
|
||||
description: Create a feature request for shadcn/ui
|
||||
title: '[feat]: '
|
||||
labels: ['area: request']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Thanks for taking the time to create a feature request! Please search open/closed issues before submitting, as the issue may have already been reported/addressed.
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
#### If you aren't sure this is a bug or not, please open a discussion instead:
|
||||
- [Discussions](https://github.com/shadcn-ui/ui/discussions/new?category=general)
|
||||
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Feature description
|
||||
description: Tell us about your feature request
|
||||
placeholder: 'I think this feature would be great because...'
|
||||
value: 'Describe your feature request...'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: components-affected
|
||||
attributes:
|
||||
label: Affected component/components
|
||||
description: Is this feature request relevant to any of the already existing components?
|
||||
placeholder: ex. Button, Checkbox...
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context about the feature here.
|
||||
placeholder: ex. screenshots, Stack Overflow links, forum links, etc.
|
||||
value: 'Additional details here...'
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Before submitting
|
||||
description: By submitting this issue, you agree to follow our [Contributing Guidelines](https://github.com/shadcn-ui/ui/blob/main/CONTRIBUTING.md).
|
||||
options:
|
||||
- label: I've made research efforts and searched the documentation
|
||||
required: true
|
||||
- label: I've searched for existing issues and PRs
|
||||
required: true
|
||||
@@ -0,0 +1,12 @@
|
||||
// ORIGINALLY FROM CLOUDFLARE WRANGLER:
|
||||
// https://github.com/cloudflare/wrangler2/blob/main/.github/changeset-version.js
|
||||
|
||||
import { exec } from "child_process"
|
||||
|
||||
// This script is used by the `release.yml` workflow to update the version of the packages being released.
|
||||
// The standard step is only to run `changeset version` but this does not update the package-lock.json file.
|
||||
// So we also run `npm install`, which does this update.
|
||||
// This is a workaround until this is handled automatically by `changeset version`.
|
||||
// See https://github.com/changesets/changesets/issues/421.
|
||||
exec("npx changeset version")
|
||||
exec("npm install")
|
||||
@@ -1,46 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/astro-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/astro-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/next-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/next-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/react-router-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/react-router-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/start-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/start-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/vite-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/vite-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -1,9 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="640" viewBox="0 0 1280 640" role="img" aria-label="ui">
|
||||
<rect width="1280" height="640" fill="#0A0A0A"/>
|
||||
<svg x="96" y="215" width="210" height="210" viewBox="0 0 67 67"><path d="M22.21 67V44.6369H0V67H22.21Z" fill="#fff"/><path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#fff"/><path d="M22.21 0H0V22.3184H22.21V0Z" fill="#fff"/><path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#fff"/><path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#fff"/></svg>
|
||||
<text x="378" y="276" font-family="Inter,system-ui,-apple-system,sans-serif" font-size="78" font-weight="800" letter-spacing="-2" fill="#ffffff">ui</text>
|
||||
<text x="378" y="322" font-family="Inter,system-ui,sans-serif" font-size="30" fill="#ffffff" opacity=".66">React component library for AI applications</text>
|
||||
<rect x="378" y="338" width="806" height="3" rx="1.5" fill="#ffffff" opacity=".9"/>
|
||||
<text x="378" y="390" font-family="Inter,system-ui,sans-serif" font-size="24" font-weight="600" fill="#ffffff" opacity=".5">github.com/hanzoai</text>
|
||||
<text x="1184" y="390" text-anchor="end" font-family="Inter,system-ui,sans-serif" font-size="24" font-weight="600" fill="#ffffff" opacity=".5">hanzo.ai</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -4,7 +4,7 @@
|
||||
import { exec } from "child_process"
|
||||
import fs from "fs"
|
||||
|
||||
const pkgJsonPath = "pkgs/shadcn/package.json"
|
||||
const pkgJsonPath = "pkg/cli/package.json"
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath))
|
||||
exec("git rev-parse --short HEAD", (err, stdout) => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { exec } from "child_process"
|
||||
import fs from "fs"
|
||||
|
||||
const pkgJsonPath = "pkgs/shadcn/package.json"
|
||||
const pkgJsonPath = "pkg/cli/package.json"
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath))
|
||||
exec("git rev-parse --short HEAD", (err, stdout) => {
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run linting
|
||||
run: pnpm run lint
|
||||
|
||||
typecheck:
|
||||
name: Type Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run type checking
|
||||
run: |
|
||||
cd app && pnpm run typecheck
|
||||
cd ../pkg/ui && pnpm run tc --noEmit
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build UI package
|
||||
run: cd pkg/ui && pnpm run build:full
|
||||
|
||||
- name: Build app
|
||||
run: cd app && pnpm run build
|
||||
env:
|
||||
NEXT_PUBLIC_APP_URL: https://ui.hanzo.ai
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: |
|
||||
app/.next
|
||||
pkg/ui/dist
|
||||
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run pkg/ui tests
|
||||
run: cd pkg/ui && pnpm test -- --run
|
||||
|
||||
- name: Run app tests
|
||||
run: |
|
||||
if [ -f "app/package.json" ] && grep -q '"test"' app/package.json 2>/dev/null; then
|
||||
cd app && pnpm test || echo "No tests configured"
|
||||
else
|
||||
echo "No test script found"
|
||||
fi
|
||||
|
||||
- name: Upload coverage
|
||||
if: always()
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./pkg/ui/coverage/lcov.info
|
||||
flags: unit
|
||||
|
||||
deploy-preview:
|
||||
name: Deploy Preview
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, typecheck, build]
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Install Vercel CLI
|
||||
run: npm install --global vercel@latest
|
||||
|
||||
- name: Pull Vercel Environment Information
|
||||
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
|
||||
- name: Build Project Artifacts
|
||||
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
||||
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
id: deploy
|
||||
run: |
|
||||
url=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})
|
||||
echo "preview_url=$url" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Comment PR with preview URL
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '🚀 Preview deployed to: ${{ steps.deploy.outputs.preview_url }}'
|
||||
})
|
||||
|
||||
status:
|
||||
name: CI Status
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, typecheck, build, test]
|
||||
if: always()
|
||||
steps:
|
||||
- name: Check status
|
||||
run: |
|
||||
if [ "${{ needs.lint.result }}" != "success" ] || \
|
||||
[ "${{ needs.typecheck.result }}" != "success" ] || \
|
||||
[ "${{ needs.build.result }}" != "success" ] || \
|
||||
[ "${{ needs.test.result }}" != "success" ]; then
|
||||
echo "CI checks failed"
|
||||
exit 1
|
||||
else
|
||||
echo "All CI checks passed!"
|
||||
fi
|
||||
@@ -0,0 +1,116 @@
|
||||
name: Code check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["*"]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
name: pnpm lint
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8.6.1
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
#- run: pnpm lint
|
||||
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
name: pnpm format:check
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8.6.1
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- run: pnpm format:check
|
||||
|
||||
tsc:
|
||||
runs-on: ubuntu-latest
|
||||
name: pnpm typecheck
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8.6.1
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- run: pnpm typecheck
|
||||
@@ -0,0 +1,60 @@
|
||||
name: Test Coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
name: Test Coverage
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: cd pkg/ui && pnpm test:coverage
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./pkg/ui/coverage/lcov.info
|
||||
flags: unit
|
||||
name: hanzo-ui-coverage
|
||||
|
||||
- name: Comment PR with coverage
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const coverage = JSON.parse(fs.readFileSync('./pkg/ui/coverage/coverage-summary.json', 'utf8'));
|
||||
const total = coverage.total;
|
||||
const comment = `## Test Coverage Report\n\n` +
|
||||
`Lines: ${total.lines.pct}%\n` +
|
||||
`Statements: ${total.statements.pct}%\n` +
|
||||
`Functions: ${total.functions.pct}%\n` +
|
||||
`Branches: ${total.branches.pct}%`;
|
||||
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: comment
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
name: Deploy to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build documentation
|
||||
working-directory: ./app
|
||||
run: |
|
||||
pnpm build
|
||||
touch out/.nojekyll
|
||||
env:
|
||||
NODE_ENV: production
|
||||
GITHUB_ACTIONS: true
|
||||
NEXT_PUBLIC_APP_URL: https://ui.hanzo.ai
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./app/out
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
@@ -0,0 +1,123 @@
|
||||
name: NPM Publish
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: 'Package to publish'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- ui
|
||||
- ui-mcp
|
||||
- auth
|
||||
- commerce
|
||||
- brand
|
||||
- react
|
||||
- all
|
||||
version_bump:
|
||||
description: 'Version bump type'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build packages
|
||||
run: |
|
||||
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "ui" ]; then
|
||||
cd pkg/ui && pnpm build
|
||||
cd ../..
|
||||
fi
|
||||
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "auth" ]; then
|
||||
cd pkg/auth && pnpm build
|
||||
cd ../..
|
||||
fi
|
||||
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "commerce" ]; then
|
||||
cd pkg/commerce && pnpm build
|
||||
cd ../..
|
||||
fi
|
||||
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "brand" ]; then
|
||||
cd pkg/brand && pnpm build
|
||||
cd ../..
|
||||
fi
|
||||
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "react" ]; then
|
||||
cd pkg/react && pnpm build
|
||||
cd ../..
|
||||
fi
|
||||
|
||||
- name: Bump version and publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
|
||||
run: |
|
||||
npm config set //registry.npmjs.org/:_authToken $NODE_AUTH_TOKEN
|
||||
|
||||
publish_package() {
|
||||
local pkg_dir=$1
|
||||
cd $pkg_dir
|
||||
npm version ${{ github.event.inputs.version_bump }}
|
||||
npm publish --access public
|
||||
cd -
|
||||
}
|
||||
|
||||
case "${{ github.event.inputs.package }}" in
|
||||
ui)
|
||||
publish_package "pkg/ui"
|
||||
;;
|
||||
ui-mcp)
|
||||
publish_package "pkg/ui-mcp"
|
||||
;;
|
||||
auth)
|
||||
publish_package "pkg/auth"
|
||||
;;
|
||||
commerce)
|
||||
publish_package "pkg/commerce"
|
||||
;;
|
||||
brand)
|
||||
publish_package "pkg/brand"
|
||||
;;
|
||||
react)
|
||||
publish_package "pkg/react"
|
||||
;;
|
||||
all)
|
||||
publish_package "pkg/ui"
|
||||
publish_package "pkg/ui-mcp"
|
||||
publish_package "pkg/auth"
|
||||
publish_package "pkg/commerce"
|
||||
publish_package "pkg/brand"
|
||||
publish_package "pkg/react"
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: 'chore: bump ${{ github.event.inputs.package }} version to ${{ github.event.inputs.version_bump }}'
|
||||
title: 'chore: bump ${{ github.event.inputs.package }} version'
|
||||
body: |
|
||||
Automated version bump for ${{ github.event.inputs.package }} package(s).
|
||||
|
||||
Version bump type: ${{ github.event.inputs.version_bump }}
|
||||
branch: version-bump-${{ github.event.inputs.package }}-${{ github.run_number }}
|
||||
@@ -0,0 +1,65 @@
|
||||
# Adapted from create-t3-app.
|
||||
name: Write Beta Release comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Release - Beta"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
if: |
|
||||
github.repository_owner == 'hanzoai-ui' &&
|
||||
${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
name: Write comment to the PR
|
||||
steps:
|
||||
- name: "Comment on PR"
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
});
|
||||
|
||||
for (const artifact of allArtifacts.data.artifacts) {
|
||||
// Extract the PR number and package version from the artifact name
|
||||
const match = /^npm-package-hanzoai-ui@(.*?)-pr-(\d+)/.exec(artifact.name);
|
||||
|
||||
if (match) {
|
||||
require("fs").appendFileSync(
|
||||
process.env.GITHUB_ENV,
|
||||
`\nBETA_PACKAGE_VERSION=${match[1]}` +
|
||||
`\nWORKFLOW_RUN_PR=${match[2]}` +
|
||||
`\nWORKFLOW_RUN_ID=${context.payload.workflow_run.id}`
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- name: "Comment on PR with Link"
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
number: ${{ env.WORKFLOW_RUN_PR }}
|
||||
message: |
|
||||
A new prerelease is available for testing:
|
||||
|
||||
```sh
|
||||
npx hanzoai-ui@${{ env.BETA_PACKAGE_VERSION }}
|
||||
```
|
||||
|
||||
- name: "Remove the autorelease label once published"
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: '${{ env.WORKFLOW_RUN_PR }}',
|
||||
name: '🚀 autorelease',
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
# Adapted from create-t3-app.
|
||||
|
||||
name: Release - Beta
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
prerelease:
|
||||
if: |
|
||||
github.repository_owner == 'hanzoai' &&
|
||||
contains(github.event.pull_request.labels.*.name, '🚀 autorelease')
|
||||
name: Build & Publish a beta release to NPM
|
||||
runs-on: ubuntu-latest
|
||||
environment: Preview
|
||||
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use PNPM
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 8.6.1
|
||||
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Modify package.json version
|
||||
run: node .github/version-script-beta.js
|
||||
|
||||
- name: Authenticate to NPM
|
||||
run: echo "//registry.npmjs.org/:_authToken=$NPM_ACCESS_TOKEN" >> pkg/cli/.npmrc
|
||||
env:
|
||||
NPM_ACCESS_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
|
||||
- name: Publish Beta to NPM
|
||||
run: pnpm pub:beta
|
||||
|
||||
- name: get-npm-version
|
||||
id: package-version
|
||||
uses: martinbeentjes/npm-get-version-action@main
|
||||
with:
|
||||
path: pkg/cli
|
||||
|
||||
- name: Upload packaged artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: npm-package-hanzoai-ui@${{ steps.package-version.outputs.current-version }}-pr-${{ github.event.number }} # encode the PR number into the artifact name
|
||||
path: pkg/cli/dist/index.js
|
||||
@@ -0,0 +1,154 @@
|
||||
name: Publish on Tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*' # Match @hanzo/ui version (e.g., v5.1.1)
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build packages
|
||||
run: |
|
||||
cd pkg/ui && pnpm build && cd ../..
|
||||
cd pkg/auth && pnpm build && cd ../..
|
||||
cd pkg/commerce && pnpm build && cd ../..
|
||||
cd pkg/brand && pnpm build && cd ../..
|
||||
cd pkg/react && pnpm build && cd ../..
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd pkg/ui && pnpm test
|
||||
cd ../react && pnpm test
|
||||
cd ../..
|
||||
|
||||
publish:
|
||||
name: Publish to NPM
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build all packages
|
||||
run: |
|
||||
cd pkg/ui && pnpm build && cd ../..
|
||||
cd pkg/auth && pnpm build && cd ../..
|
||||
cd pkg/commerce && pnpm build && cd ../..
|
||||
cd pkg/brand && pnpm build && cd ../..
|
||||
cd pkg/react && pnpm build && cd ../..
|
||||
|
||||
- name: Configure npm authentication
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
|
||||
run: |
|
||||
npm config set //registry.npmjs.org/:_authToken $NODE_AUTH_TOKEN
|
||||
npm whoami
|
||||
|
||||
- name: Check and publish packages
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
|
||||
run: |
|
||||
echo "Checking all packages for unpublished versions..."
|
||||
|
||||
PUBLISHED_COUNT=0
|
||||
SKIPPED_COUNT=0
|
||||
PUBLISHED_PACKAGES=""
|
||||
|
||||
for package in ui auth commerce brand react; do
|
||||
cd "pkg/$package"
|
||||
|
||||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||
PACKAGE_NAME=$(node -p "require('./package.json').name")
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "📦 Checking $PACKAGE_NAME@$CURRENT_VERSION"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# Check if this version already exists on npm
|
||||
if npm view "$PACKAGE_NAME@$CURRENT_VERSION" version 2>/dev/null; then
|
||||
echo "⏭️ Already published - skipping"
|
||||
SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
|
||||
else
|
||||
echo "🚀 Publishing to npm..."
|
||||
npm publish --access public
|
||||
echo "✅ Successfully published $PACKAGE_NAME@$CURRENT_VERSION"
|
||||
PUBLISHED_COUNT=$((PUBLISHED_COUNT + 1))
|
||||
PUBLISHED_PACKAGES="$PUBLISHED_PACKAGES\n- $PACKAGE_NAME@$CURRENT_VERSION"
|
||||
fi
|
||||
|
||||
cd ../..
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "📊 Publishing Summary"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "✅ Published: $PUBLISHED_COUNT package(s)"
|
||||
echo "⏭️ Skipped: $SKIPPED_COUNT package(s)"
|
||||
|
||||
if [ $PUBLISHED_COUNT -gt 0 ]; then
|
||||
echo ""
|
||||
echo -e "Published packages:$PUBLISHED_PACKAGES"
|
||||
fi
|
||||
|
||||
# Save for GitHub release notes
|
||||
echo "PUBLISHED_COUNT=$PUBLISHED_COUNT" >> $GITHUB_ENV
|
||||
echo "PUBLISHED_PACKAGES<<EOF" >> $GITHUB_ENV
|
||||
echo -e "$PUBLISHED_PACKAGES" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: ${{ env.PUBLISHED_COUNT > 0 }}
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
generate_release_notes: true
|
||||
body: |
|
||||
## 📦 NPM Packages Published
|
||||
|
||||
${{ env.PUBLISHED_PACKAGES }}
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Install latest versions
|
||||
npm install @hanzo/ui @hanzo/auth @hanzo/commerce @hanzo/brand @hanzo/react
|
||||
```
|
||||
files: |
|
||||
CHANGELOG.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,53 @@
|
||||
# Adapted from create-t3-app.
|
||||
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: ${{ github.repository_owner == 'hanzoai-ui' }}
|
||||
name: Create a PR for release workflow
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use PNPM
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 8.6.1
|
||||
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
version: 8.6.1
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: pnpm install
|
||||
|
||||
# - name: Check for errors
|
||||
# run: pnpm check
|
||||
|
||||
- name: Build the package
|
||||
run: pnpm build:cli
|
||||
|
||||
- name: Create Version PR or Publish to NPM
|
||||
id: changesets
|
||||
uses: changesets/action@v1.4.1
|
||||
with:
|
||||
commit: "chore(release): version packages"
|
||||
title: "chore(release): version packages"
|
||||
version: node .github/changeset-version.js
|
||||
publish: npx changeset publish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
NODE_ENV: "production"
|
||||
@@ -0,0 +1,100 @@
|
||||
name: Sync Forks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
fork:
|
||||
description: 'Fork to sync (luxfi or zoo or all)'
|
||||
required: false
|
||||
default: 'all'
|
||||
|
||||
jobs:
|
||||
sync-luxfi:
|
||||
if: github.event.inputs.fork == 'luxfi' || github.event.inputs.fork == 'all' || github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Hanzo UI
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Git
|
||||
run: |
|
||||
git config --global user.name "Hanzo Bot"
|
||||
git config --global user.email "bot@hanzo.ai"
|
||||
|
||||
- name: Clone Luxfi Fork
|
||||
run: |
|
||||
git clone https://github.com/luxfi/ui.git luxfi-ui
|
||||
cd luxfi-ui
|
||||
git remote add upstream https://github.com/hanzoai/ui.git
|
||||
git fetch upstream
|
||||
|
||||
- name: Sync and Rebrand
|
||||
run: |
|
||||
cd luxfi-ui
|
||||
# Merge upstream changes
|
||||
git checkout main
|
||||
git merge upstream/main --no-edit || true
|
||||
|
||||
# Run rebrand script
|
||||
cp ../scripts/rebrand.sh ./
|
||||
chmod +x rebrand.sh
|
||||
./rebrand.sh luxfi
|
||||
|
||||
# Commit changes
|
||||
git add -A
|
||||
git commit -m "sync: Update from hanzoai/ui and rebrand for Luxfi" || true
|
||||
|
||||
- name: Push to Luxfi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.FORK_SYNC_TOKEN }}
|
||||
run: |
|
||||
cd luxfi-ui
|
||||
git push https://$GITHUB_TOKEN@github.com/luxfi/ui.git main || true
|
||||
|
||||
sync-zoo:
|
||||
if: github.event.inputs.fork == 'zoo' || github.event.inputs.fork == 'all' || github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Hanzo UI
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Git
|
||||
run: |
|
||||
git config --global user.name "Hanzo Bot"
|
||||
git config --global user.email "bot@hanzo.ai"
|
||||
|
||||
- name: Clone Zoo Fork
|
||||
run: |
|
||||
git clone https://github.com/zooai/ui.git zoo-ui
|
||||
cd zoo-ui
|
||||
git remote add upstream https://github.com/hanzoai/ui.git
|
||||
git fetch upstream
|
||||
|
||||
- name: Sync and Rebrand
|
||||
run: |
|
||||
cd zoo-ui
|
||||
# Merge upstream changes
|
||||
git checkout main
|
||||
git merge upstream/main --no-edit || true
|
||||
|
||||
# Run rebrand script
|
||||
cp ../scripts/rebrand.sh ./
|
||||
chmod +x rebrand.sh
|
||||
./rebrand.sh zoo
|
||||
|
||||
# Commit changes
|
||||
git add -A
|
||||
git commit -m "sync: Update from hanzoai/ui and rebrand for Zoo" || true
|
||||
|
||||
- name: Push to Zoo
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.FORK_SYNC_TOKEN }}
|
||||
run: |
|
||||
cd zoo-ui
|
||||
git push https://$GITHUB_TOKEN@github.com/zooai/ui.git main || true
|
||||
@@ -0,0 +1,58 @@
|
||||
name: E2E and Visual Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
schedule:
|
||||
# Run tests daily at 2 AM UTC to catch breaking updates
|
||||
- cron: '0 2 * * *'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 9.0.6
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: pnpm exec playwright install --with-deps
|
||||
|
||||
- name: Build the project
|
||||
run: pnpm build
|
||||
|
||||
- name: Run component health check
|
||||
run: pnpm health-check
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run E2E tests
|
||||
run: pnpm test:e2e
|
||||
|
||||
- name: Run visual regression tests
|
||||
run: pnpm test:visual
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report
|
||||
path: tests/reports/playwright-report/
|
||||
retention-days: 30
|
||||
-15
@@ -66,18 +66,3 @@ tests/artifacts/
|
||||
# They cannot be generated in CI (requires display/browser)
|
||||
# app/public/r/styles/*/ -- REMOVED to allow committing screenshots
|
||||
|
||||
|
||||
# Test artifacts
|
||||
test-results/
|
||||
playwright-report/
|
||||
*BADGE_INSPECTION*.md
|
||||
*BADGE_INSPECTION*.txt
|
||||
|
||||
# ide
|
||||
.idea
|
||||
.fleet
|
||||
|
||||
.notes
|
||||
.claude/settings.local.json
|
||||
pkg/ui/dist-test/
|
||||
app/public/api/
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# The whole caller. Every decision this pipeline makes — what to test, what to
|
||||
# build, where to roll it — is read from the root hanzo.yml, so this file only
|
||||
# ever names the triggers and the reusable.
|
||||
#
|
||||
# `.hanzo/workflows`, not `.github/workflows`, and that is not a style choice:
|
||||
# CI for this repo runs on git.hanzo.ai, which resolves ONLY `.hanzo/workflows`.
|
||||
# github.com has zero self-hosted runners registered for the `hanzo-build-*`
|
||||
# labels this pipeline asks for, so the same file under `.github/workflows`
|
||||
# would queue there forever and never report anything at all.
|
||||
name: CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: cicd-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
cicd:
|
||||
uses: hanzoai/ci/.hanzo/workflows/build.yml@v1
|
||||
secrets: inherit
|
||||
@@ -1,70 +0,0 @@
|
||||
name: deploy
|
||||
|
||||
# RETIRED as an automatic lane. The image is built by hanzo.yml's `images:` block
|
||||
# through hanzoai/ci (.hanzo/workflows/cicd.yml), and this job is kept only as a
|
||||
# manually-dispatched fallback.
|
||||
#
|
||||
# The reason is one capability, not tidiness. ui.hanzo.ai is a static export, so
|
||||
# its ingest key has to be BAKED IN at build; the key lives in KMS as
|
||||
# `deploy/PUBLISHABLE_KEY`, and hanzoai/ci is the only lane that reads it and
|
||||
# passes it as `--build-arg PUBLISHABLE_KEY=…`. The `docker build -t "$image" .`
|
||||
# below cannot — there is no KMS hop in it and nowhere to put the value — so any
|
||||
# image it produced would ship a client with no key at all, and cloud answers
|
||||
# `401 ingest_key_required` for an unattributed write. With the Dockerfile now
|
||||
# failing closed on an empty key, this job fails on every run instead of quietly
|
||||
# publishing an inert site.
|
||||
#
|
||||
# Left in place rather than deleted, and dispatch-only rather than on push:
|
||||
# deleting a repo's only build lane strands the host with no failing run to show
|
||||
# it, and two push-triggered builders for one image means one commit yields two
|
||||
# images under two tag schemes. Dispatching this publishes `<short-sha>` with an
|
||||
# EMPTY key and the Dockerfile will refuse it — so if you dispatch it, pass the
|
||||
# key yourself or expect the gate to fire.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
image:
|
||||
# hanzo-build-linux-amd64, not ubuntu-latest. This file lives in
|
||||
# .hanzo/workflows, which GitHub never reads -- git.hanzo.ai is the only
|
||||
# thing that can run it, and the fleet deliberately advertises no generic
|
||||
# ubuntu-* label (~1400 mirrored forks all ask for it; one bad job was
|
||||
# retried ~520 times across 10 runners). An unmatched label is not an
|
||||
# error here, it is silence: the job queues until the 24h timeout, so this
|
||||
# deploy has never produced an image and nothing ever said so.
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: build and push
|
||||
# GHCR_TOKEN/GHCR_USER, which are the org secrets that actually exist.
|
||||
# This job asked for REGISTRY_TOKEN, which was never defined anywhere —
|
||||
# the org has exactly GHCR_TOKEN, GHCR_USER, GH_PAT, KMS_CLIENT_ID,
|
||||
# KMS_CLIENT_SECRET, OCI_TOKEN, OCI_USER — so the guard below fired on
|
||||
# every run and nothing was ever published. Every other image build in
|
||||
# the org already uses the GHCR_* pair; this file and papers were the
|
||||
# two that invented a name.
|
||||
#
|
||||
# The username comes from the secret too, rather than being hardcoded to
|
||||
# hanzo-dev, so rotating the publishing identity is one org-level change.
|
||||
env:
|
||||
GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }}
|
||||
GHCR_USER: ${{ secrets.GHCR_USER }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${GHCR_TOKEN:-}" ] || [ -z "${GHCR_USER:-}" ]; then
|
||||
echo "::error::GHCR_TOKEN/GHCR_USER are unset, so nothing was published."
|
||||
exit 1
|
||||
fi
|
||||
tag="$(git rev-parse --short HEAD)"
|
||||
image="ghcr.io/hanzoai/ui:${tag}"
|
||||
echo "${GHCR_TOKEN}" | docker login ghcr.io -u "${GHCR_USER}" --password-stdin
|
||||
docker build -t "${image}" .
|
||||
docker push "${image}"
|
||||
echo "published ${image} — set image.tag AND image.digest in universe charts/app/values/hanzo/ui.yaml"
|
||||
@@ -1,343 +0,0 @@
|
||||
name: Publish Packages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
# BOTH package roots, and the singular one is not a typo — `@hanzo/ui` and
|
||||
# `@hanzo/data` live under pkg/, everything else under pkgs/.
|
||||
#
|
||||
# ⚠️ This list and the `for pkg_json in …` loop below are ONE FACT. The loop
|
||||
# was already fixed to walk both roots, with a comment explaining that a
|
||||
# `pkgs/*` glob made pkg/ packages invisible — but this gate was left
|
||||
# matching `pkgs/*` only, so for a pkg/-only change the corrected job could
|
||||
# never be REACHED. The fix was reasoned about in the body and not applied
|
||||
# to the thing that decides whether the body runs.
|
||||
#
|
||||
# Cost: @hanzo/ui 8.0.29 sat unpublished while npmjs served 8.0.28, so
|
||||
# every consumer resolving `^8.0.17` silently got a version without the
|
||||
# `./chat` entrypoint — an import error in the dependent, nothing at all in
|
||||
# this repo. Add a root here whenever one is added to the loop.
|
||||
- 'pkg/*/package.json'
|
||||
- 'pkgs/*/package.json'
|
||||
# A publish that fails for an infrastructure reason — the runner missing a
|
||||
# tool, npmjs answering slowly — could only be retried by pushing another
|
||||
# commit that touches a package.json, i.e. by burning a version number to
|
||||
# re-run a job that was never wrong about the code. detect-changes reads what
|
||||
# npmjs SERVES, so a dispatch is idempotent by construction: it publishes
|
||||
# exactly the packages that are ahead of the registry, and nothing when none
|
||||
# are.
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
outputs:
|
||||
packages: ${{ steps.changed.outputs.packages }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Detect changed packages
|
||||
id: changed
|
||||
run: |
|
||||
CHANGED=()
|
||||
# Both package roots. `@hanzo/ui` and `@hanzo/data` live under pkg/ (singular)
|
||||
# and were invisible to a `pkgs/*` glob, so no version bump of either could
|
||||
# ever reach npmjs from here.
|
||||
for pkg_json in pkg/*/package.json pkgs/*/package.json; do
|
||||
[ ! -f "$pkg_json" ] && continue
|
||||
# Skip private packages
|
||||
IS_PRIVATE=$(jq -r '.private // false' "$pkg_json")
|
||||
[ "$IS_PRIVATE" = "true" ] && continue
|
||||
|
||||
# Only publish @hanzo/* packages
|
||||
PKG_NAME=$(jq -r '.name' "$pkg_json")
|
||||
case "$PKG_NAME" in
|
||||
@hanzo/*) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
|
||||
# What npm SERVES, not what the previous commit said. HEAD~1 is the
|
||||
# FIRST PARENT, so a merge whose first parent is the feature branch
|
||||
# already carries the new version and the diff is empty — the publish
|
||||
# is skipped and npm keeps serving the old release, silently. Asking
|
||||
# the registry is independent of merge topology and of how many
|
||||
# commits the runner fetched.
|
||||
NEW=$(jq -r '.version' "$pkg_json")
|
||||
OLD=$(curl -sf "https://registry.npmjs.org/${PKG_NAME}" \
|
||||
| jq -r '."dist-tags".latest // "0.0.0"' 2>/dev/null || echo "0.0.0")
|
||||
|
||||
# FORWARD ONLY. `!=` fires in both directions, and several packages
|
||||
# sit BEHIND npm (a release cut elsewhere, a revert) — publishing
|
||||
# those walks the registry backwards. Only a strictly greater local
|
||||
# version is a release.
|
||||
NEWEST=$(printf '%s\n%s\n' "$OLD" "$NEW" | sort -V | tail -1)
|
||||
if [ "$OLD" != "$NEW" ] && [ "$NEWEST" = "$NEW" ]; then
|
||||
echo "Version change: $PKG_NAME ($OLD -> $NEW)"
|
||||
# The package NAME, because every downstream step spends this as a
|
||||
# `pnpm --filter` argument and a directory basename matches nothing.
|
||||
# An unmatched filter exits 0, so the build silently no-ops and the
|
||||
# publish ships an unbuilt tarball — how 8.0.17 and 8.0.19 shipped broken.
|
||||
CHANGED+=("$PKG_NAME")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#CHANGED[@]} -eq 0 ]; then
|
||||
echo "packages=[]" >> $GITHUB_OUTPUT
|
||||
else
|
||||
JSON=$(printf '%s\n' "${CHANGED[@]}" | jq -R . | jq -s -c .)
|
||||
echo "packages=$JSON" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
publish:
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.packages != '[]'
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
strategy:
|
||||
# One package's broken build must not withhold every other package's
|
||||
# release: the matrix legs are independent publishes, not one artifact.
|
||||
fail-fast: false
|
||||
matrix:
|
||||
package: ${{ fromJson(needs.detect-changes.outputs.packages) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build package
|
||||
# The trailing `...` is load-bearing: it builds the package's WORKSPACE
|
||||
# DEPENDENCIES first, in topological order.
|
||||
#
|
||||
# Without it, `pnpm --filter @hanzo/ui build` runs alone against siblings
|
||||
# that have no `dist/`. Those siblings type themselves through
|
||||
# `"types": "dist/index.d.ts"`, so tsc reports them as missing modules and
|
||||
# the errors do not name the real cause:
|
||||
# src/product/SiteNav.tsx: Cannot find module '@hanzo/products'
|
||||
# src/core/tokens.ts: Cannot find module '@hanzo/tokens'
|
||||
# src/gitops.ts: Cannot find module '@hanzo/cd'
|
||||
# plus a wave of implicit-any errors in the files that imported them,
|
||||
# which read like a source problem in @hanzo/ui and are not.
|
||||
run: pnpm --filter ${{ matrix.package }}... build
|
||||
|
||||
- name: Provision bun (only for a package whose test script asks for it)
|
||||
# @hanzo/canvas declares `bun test` and this runner has no bun, so its
|
||||
# suite has been `spawn ENOENT` on every run — which fails the leg before
|
||||
# the publish and is why canvas has sat at 0.2.2 here against 0.2.1 on
|
||||
# npmjs. The step that exists to stop an untested release was itself the
|
||||
# thing stopping the release.
|
||||
#
|
||||
# Read from the package's OWN manifest, not from a list here, so a
|
||||
# package that adopts or drops bun needs no edit to this file. A package
|
||||
# that does not ask for bun does not pay for it.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PKG_DIR=$(pnpm --filter ${{ matrix.package }} exec pwd | tail -1)
|
||||
if ! jq -r '.scripts.test // ""' "$PKG_DIR/package.json" | grep -qw bun; then
|
||||
echo "::notice::${{ matrix.package }} does not run bun"
|
||||
exit 0
|
||||
fi
|
||||
if command -v bun >/dev/null 2>&1; then
|
||||
echo "bun already on PATH: $(bun --version)"
|
||||
exit 0
|
||||
fi
|
||||
# From npmjs, not bun.sh/install: this job already reaches the npm
|
||||
# registry (it is what the job exists to publish to) and setup-node
|
||||
# already put the global bin on PATH, so this adds no new host to
|
||||
# trust and no new failure mode. The shell installer would also want
|
||||
# unzip, which a minimal runner image need not carry.
|
||||
npm install -g bun
|
||||
bun --version
|
||||
|
||||
- name: Test package
|
||||
# A published version is IMMUTABLE, so this is the last point at which a
|
||||
# defect costs nothing. Nothing else runs a package's own tests before it
|
||||
# ships: ci.yml's test job runs `cd pkgs/ui && …` — that one directory —
|
||||
# and this workflow went checkout → build → publish. So @hanzo/event's
|
||||
# suite, which pins the version the client stamps on every event, ran in
|
||||
# no workflow that gates a release, and the stale stamp it was written to
|
||||
# catch could still have reached npmjs.
|
||||
#
|
||||
# `--if-present` so a package with no test script is skipped rather than
|
||||
# failing the release: the set of testable packages is read from each
|
||||
# package.json, not listed here, so a new package is covered the day it
|
||||
# adds a `test` script.
|
||||
#
|
||||
# This step needs `test` to mean the same thing in every package — the
|
||||
# package's own suite, no browser, no network. @hanzo/ui briefly chained
|
||||
# its two CONSUMER suites onto it (pack a tarball, npm-install it into a
|
||||
# throwaway app, drive it in chromium), which is a real gate and belongs
|
||||
# exactly where hanzo.yml already runs it: on the push, in its own
|
||||
# `ui-consumer` job, next to the `playwright install` that gives it a
|
||||
# browser. Here there is no such step, so a chained `test` could only
|
||||
# fail — the last gate before an immutable version would have been one
|
||||
# that cannot pass, which is the failure mode this repo has already paid
|
||||
# for twice.
|
||||
run: pnpm --filter ${{ matrix.package }} run --if-present test
|
||||
|
||||
- name: Verify the tarball carries what package.json promises
|
||||
# `types` is a PROMISE to every consumer, and tsup writes dist/*.mjs in a
|
||||
# separate pass from dist/*.d.ts — so a declaration failure yields a
|
||||
# package whose JS is fine and whose types are absent. That is invisible
|
||||
# here and surfaces in dependents as "Could not find a declaration file".
|
||||
#
|
||||
# Asserted from the package's OWN manifest, so it holds for every package
|
||||
# this workflow publishes and needs no per-package list.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PKG_DIR=$(pnpm --filter ${{ matrix.package }} exec pwd | tail -1)
|
||||
TYPES=$(jq -r '.types // .typings // empty' "$PKG_DIR/package.json")
|
||||
if [ -z "$TYPES" ]; then
|
||||
echo "::notice::${{ matrix.package }} declares no types entrypoint — nothing to verify"
|
||||
exit 0
|
||||
fi
|
||||
if [ ! -f "$PKG_DIR/$TYPES" ]; then
|
||||
echo "::error::${{ matrix.package }} declares types at $TYPES but the build produced no such file."
|
||||
echo "::error::Publishing would ship a package that silently has no types. Fix the declaration build."
|
||||
exit 1
|
||||
fi
|
||||
echo "types present: $TYPES"
|
||||
|
||||
- name: Fetch the npm token from KMS
|
||||
id: npmtok
|
||||
# publish.yml used secrets.NPM_TOKEN, which exists on NEITHER the hanzoai
|
||||
# org nor this repo on git.hanzo.ai. An absent secret interpolates to the
|
||||
# empty string rather than failing, so every publish reached npmjs
|
||||
# unauthenticated and 401'd -- the reason @hanzo/ui has never shipped from
|
||||
# CI and each release was a hand publish.
|
||||
#
|
||||
# KMS is where a secret lives, so the token is read at run time from the
|
||||
# same org path and with the same machine identity every other workflow
|
||||
# here already uses. Nothing new to rotate, and no npm credential is
|
||||
# stored on the forge.
|
||||
env:
|
||||
KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }}
|
||||
KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }}
|
||||
KMS_ENDPOINT: ${{ vars.KMS_ENDPOINT || 'https://kms.hanzo.ai' }}
|
||||
KMS_ORG: ${{ vars.KMS_ORG || 'hanzo' }}
|
||||
KMS_SECRET_ENV: ${{ vars.KMS_SECRET_ENV || 'prod' }}
|
||||
# Where the token actually lives, MEASURED: NPM_TOKEN sits at the org
|
||||
# root, not under deploy/ (which holds CLOUDFLARE_*, GIT_TOKEN,
|
||||
# KUBECONFIG, UNIVERSE_PIN_TOKEN). Empty means root; set the
|
||||
# KMS_NPM_PATH variable to move it without editing this file.
|
||||
KMS_NPM_PATH: ${{ vars.KMS_NPM_PATH || '' }}
|
||||
run: |
|
||||
token=$(curl -fsS "${KMS_ENDPOINT}/v1/kms/auth/login" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"clientId\":\"${KMS_CLIENT_ID}\",\"clientSecret\":\"${KMS_CLIENT_SECRET}\"}" \
|
||||
| jq -r '.accessToken // empty')
|
||||
if [ -z "${token}" ]; then
|
||||
echo "::error::KMS login failed at ${KMS_ENDPOINT}/v1/kms/auth/login (check KMS_CLIENT_ID/KMS_CLIENT_SECRET)."
|
||||
exit 1
|
||||
fi
|
||||
# The read is org-scoped by the TOKEN's owner claim, not by a path
|
||||
# segment: the flat form is the contract cloud's embedded KMS serves,
|
||||
# and it answers {name, env, value}. The older
|
||||
# /v1/kms/orgs/<org>/secrets/... form with .secret.value 404s here.
|
||||
secret_url="${KMS_ENDPOINT}/v1/kms/secrets/${KMS_NPM_PATH:+${KMS_NPM_PATH}/}NPM_TOKEN?env=${KMS_SECRET_ENV}"
|
||||
# No -f: a 404 must report WHICH path was empty, not just exit 22.
|
||||
npm_token=$(curl -sS "${secret_url}" -H "Authorization: Bearer ${token}" | jq -r '.value // empty')
|
||||
if [ -z "${npm_token}" ]; then
|
||||
echo "::error::No NPM_TOKEN at ${secret_url}."
|
||||
echo "::error::Put it there, or set the KMS_NPM_PATH variable to the path that has it."
|
||||
exit 1
|
||||
fi
|
||||
# Masked before it is ever an output, so it cannot surface in a log.
|
||||
echo "::add-mask::${npm_token}"
|
||||
echo "token=${npm_token}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Publish package
|
||||
# Idempotent on purpose: the desired state is "npmjs holds this version", and
|
||||
# a version already there satisfies it. Versions are immutable, so a conflict
|
||||
# means the release already happened (a re-run, or a hand publish) — that is
|
||||
# success, not a failure. Every other error still fails the job.
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ steps.npmtok.outputs.token }}
|
||||
run: |
|
||||
set -o pipefail
|
||||
OUT=$(pnpm --filter ${{ matrix.package }} publish \
|
||||
--no-git-checks --access public 2>&1) || true
|
||||
echo "$OUT"
|
||||
case "$OUT" in
|
||||
*EPUBLISHCONFLICT*|*"cannot publish over"*|*"previously published versions"*)
|
||||
echo "::notice::${{ matrix.package }} is already on npmjs at this version — nothing to do" ;;
|
||||
*)
|
||||
# Not a conflict: confirm the version really landed, else fail loudly.
|
||||
#
|
||||
# RETRIED, because npmjs is read-after-write eventually consistent
|
||||
# and this asked it exactly once, about two seconds after the
|
||||
# write. On run 26906 both @hanzo/og@1.0.0 and @hanzo/shop@1.0.0
|
||||
# failed here having printed npm's own `+ @hanzo/og@1.0.0` success
|
||||
# line moments earlier, and both are on the registry now: the
|
||||
# publishes worked and the check was simply too early.
|
||||
#
|
||||
# A false red here is worse than a slow green. It reports that a
|
||||
# release did not happen when it did, so the next person bumps the
|
||||
# version to "fix" it and burns a number over a replication lag —
|
||||
# and the real signal, a publish that genuinely failed, becomes
|
||||
# indistinguishable from the noise.
|
||||
PKG_DIR=$(pnpm --filter ${{ matrix.package }} exec pwd | tail -1)
|
||||
NAME=$(jq -r '.name' "$PKG_DIR/package.json")
|
||||
WANT=$(jq -r '.version' "$PKG_DIR/package.json")
|
||||
landed=
|
||||
for attempt in 1 2 3 4 5 6; do
|
||||
if npm view "$NAME@$WANT" version >/dev/null 2>&1; then landed=1; break; fi
|
||||
echo "$NAME@$WANT not visible to npmjs yet (attempt $attempt/6); waiting 10s"
|
||||
sleep 10
|
||||
done
|
||||
[ -n "$landed" ] \
|
||||
|| { echo "::error::$NAME@$WANT is not on npmjs 60s after publish"; exit 1; }
|
||||
echo "published $NAME@$WANT" ;;
|
||||
esac
|
||||
|
||||
- name: Publish to the Hanzo registry
|
||||
# Second home for the same tarball: api.hanzo.ai/v1/packages/hanzo/npm, our
|
||||
# own registry (hanzoai/git), so an install does not have to reach npmjs.
|
||||
# Same artifact, same version — this republishes what the step above just
|
||||
# built, it does not rebuild or re-version.
|
||||
#
|
||||
# BEST-EFFORT by construction. No token ⇒ skip with a notice, so this can
|
||||
# land before the credential exists and starts working the moment it does.
|
||||
# An already-published version is a success, not a failure: the registry
|
||||
# holding it is the desired state either way. npmjs is authoritative and is
|
||||
# never affected by anything here.
|
||||
env:
|
||||
# REGISTRY_TOKEN is the fleet-wide name (six other workflows use it and
|
||||
# the hanzoai org carries it). This asked for HANZO_REGISTRY_TOKEN, which
|
||||
# is defined nowhere, so it read as empty and the step below took its
|
||||
# "not set" branch and exited 0 on every run -- the mirror to
|
||||
# api.hanzo.ai has never happened, and said so only as a notice.
|
||||
HANZO_REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
set -uo pipefail
|
||||
if [ -z "${HANZO_REGISTRY_TOKEN:-}" ]; then
|
||||
echo "::notice::HANZO_REGISTRY_TOKEN not set — package published to npmjs only."
|
||||
echo "::notice::Add it (a Hanzo Git token with package:write) to mirror to api.hanzo.ai."
|
||||
exit 0
|
||||
fi
|
||||
REG=https://api.hanzo.ai/v1/packages/hanzo/npm/
|
||||
NPMRC="$RUNNER_TEMP/.npmrc-hanzo"
|
||||
{
|
||||
echo "@hanzo:registry=${REG}"
|
||||
echo "//api.hanzo.ai/v1/packages/hanzo/npm/:_authToken=${HANZO_REGISTRY_TOKEN}"
|
||||
} > "$NPMRC"
|
||||
out=$(npm_config_userconfig="$NPMRC" \
|
||||
pnpm --filter ${{ matrix.package }} publish \
|
||||
--no-git-checks --access public --registry "$REG" 2>&1) || true
|
||||
echo "$out"
|
||||
case "$out" in
|
||||
*EPUBLISHCONFLICT*|*"cannot publish over"*|*"already exists"*)
|
||||
echo "::notice::already in the Hanzo registry at this version — nothing to do" ;;
|
||||
*) echo "$out" | grep -qiE 'error|failed' \
|
||||
&& echo "::warning::mirror to api.hanzo.ai failed (npmjs publish unaffected)" \
|
||||
|| echo "mirrored to the Hanzo registry" ;;
|
||||
esac
|
||||
@@ -1,91 +0,0 @@
|
||||
name: Validate Registries
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "apps/v4/public/r/registries.json"
|
||||
- "apps/v4/registry/directory.json"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "apps/v4/public/r/registries.json"
|
||||
- "apps/v4/registry/directory.json"
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
name: pnpm validate:registries
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Block reserved registry namespaces
|
||||
env:
|
||||
RESERVED_NAMESPACES: "@shadcn,@ui,@blocks,@components,@block,@component,@util,@utils,@registry,@lib,@hook,@hooks,@theme,@themes,@chart,@charts"
|
||||
run: |
|
||||
node <<'EOF'
|
||||
const fs = require("node:fs")
|
||||
|
||||
const files = [
|
||||
"apps/v4/public/r/registries.json",
|
||||
"apps/v4/registry/directory.json",
|
||||
]
|
||||
const reservedNamespaces = new Set(
|
||||
process.env.RESERVED_NAMESPACES.split(",").filter(Boolean)
|
||||
)
|
||||
|
||||
function readNames(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8")).map(
|
||||
(entry) => entry.name
|
||||
)
|
||||
}
|
||||
|
||||
const violations = files.flatMap((filePath) => {
|
||||
return readNames(filePath)
|
||||
.filter((name) => reservedNamespaces.has(name))
|
||||
.map((name) => `${filePath}: ${name}`)
|
||||
})
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error("Reserved registry namespaces are not allowed:")
|
||||
|
||||
for (const violation of violations) {
|
||||
console.error(`- ${violation}`)
|
||||
}
|
||||
|
||||
process.exit(1)
|
||||
}
|
||||
EOF
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 9.0.6
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Validate registries
|
||||
run: pnpm --filter=v4 validate:registries
|
||||
@@ -1,18 +0,0 @@
|
||||
# .kodiak.toml
|
||||
version = 1
|
||||
|
||||
[merge]
|
||||
automerge_label = "automerge"
|
||||
require_automerge_label = true
|
||||
method = "squash"
|
||||
delete_branch_on_merge = true
|
||||
optimistic_updates = false
|
||||
prioritize_ready_to_merge = true
|
||||
notify_on_conflict = true
|
||||
|
||||
[merge.message]
|
||||
title = "pull_request_title"
|
||||
body = "pull_request_body"
|
||||
include_pr_number = true
|
||||
body_type = "markdown"
|
||||
strip_html_comments = true
|
||||
@@ -1,3 +0,0 @@
|
||||
auto-install-peers=true
|
||||
link-workspace-packages=true
|
||||
puppeteer_skip_download=true
|
||||
@@ -1,14 +0,0 @@
|
||||
dist
|
||||
node_modules
|
||||
.next
|
||||
build
|
||||
.contentlayer
|
||||
**/fixtures
|
||||
deprecated
|
||||
apps/v4/registry/styles/**/*.css
|
||||
|
||||
# Byte-coupled: hz.js carries the marked region of src/anon.js VERBATIM, and
|
||||
# pkgs/event/src/anon.test.ts compares them byte for byte. Reformatting either
|
||||
# one alone breaks the build; the same region is also vendored by hanzoai/cloud.
|
||||
pkgs/event/src/anon.js
|
||||
pkgs/event/hz.js
|
||||
Vendored
+1
-20
@@ -1,24 +1,5 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib",
|
||||
"editor.gotoLocation.multipleDefinitions": "goto",
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
"eslint.workingDirectories": [
|
||||
{ "pattern": "apps/*/" },
|
||||
{ "pattern": "pkgs/*/" },
|
||||
{ "pattern": "pkgs/*/" }
|
||||
],
|
||||
"tailwindCSS.classFunctions": ["cva", "cn"],
|
||||
"vitest.debugExclude": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**",
|
||||
"**/fixtures/**"
|
||||
],
|
||||
"files.exclude": {
|
||||
"deprecated": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"apps/v4/registry/radix-*": true,
|
||||
"apps/v4/public/r/*": true,
|
||||
"pkgs/shadcn/test/fixtures/*": true
|
||||
}
|
||||
"typescript.preferences.quoteStyle": "single"
|
||||
}
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
# Hanzo UI Consolidation — one library, all polish, no duplication
|
||||
|
||||
**Goal.** RIP the fragmentation (`@hanzo/data` vs `@hanzo/ui` vs in-console duplicates) into ONE canonical, cross-platform, presentational, clean-room library: **`@hanzo/ui`, built on `@hanzo/gui`**. Preserve every polished component + token; zero loss; no fork.
|
||||
|
||||
**Status.**
|
||||
- **Step 1 — Audit:** complete (this document; four source trees read file-by-file).
|
||||
- **Step 2 — Establish `@hanzo/ui` + migrate the stable foundation:** **DONE & GREEN** (`pkg/ui` is now a real package; `tsc --noEmit` = 0 errors, `vitest` = 12/12).
|
||||
- **Step 3 — Console re-point:** **STAGED, not merged.** Gate is split (backend live, app lanes in-flight) — see [Step 3](#step-3--console-re-point-staged).
|
||||
|
||||
**One decision needs CTO sign-off before publish** — see [Naming / version](#naming--version--the-one-decision).
|
||||
|
||||
---
|
||||
|
||||
## Architecture — three layers, decomplected
|
||||
|
||||
```
|
||||
@hanzo/gui (Tamagui/One) cross-platform primitives: Text, XStack/YStack, Button, Input,
|
||||
│ Card, Popover, Select, Switch, Slider, Spinner, ScrollView …
|
||||
▼
|
||||
@hanzo/data (pkg/data) the metadata-driven RECORD layer: fields → records → views
|
||||
│ (RecordsView, DataTable, BoardView, RecordDetail, field editors)
|
||||
▼
|
||||
@hanzo/ui (pkg/ui) ◄──── THE ONE LIBRARY. Two orthogonal concerns, one package:
|
||||
• product/app layer → import { … } from '@hanzo/ui'
|
||||
• record layer → import { … } from '@hanzo/ui/data' (re-exports @hanzo/data)
|
||||
```
|
||||
|
||||
`@hanzo/data` stays the **source of truth** for the record layer; `@hanzo/ui` **composes** it (`export * from '@hanzo/data'` on the `./data` subpath) rather than copying — so the CMS/ERP/Help app lanes that consume `@hanzo/data` today keep working untouched, and there is exactly one home. This mirrors how the gui base wraps `hanzogui`.
|
||||
|
||||
Both layers own a `DataTable` (product = generic typed `<T>` list; data = field-driven record grid). Keeping the record layer on the `./data` subpath keeps each name unambiguous — no rename, no collision.
|
||||
|
||||
---
|
||||
|
||||
## The preserve-and-merge manifest
|
||||
|
||||
Legend: **✅ migrated** (in `pkg/ui`, GREEN) · **↪ re-exported** (composed from `@hanzo/data`) · **⏳ staged** (Step 3, after app lanes land) · **🔧 pending prop-injection** (app-coupled; decouple before it can live in a presentational lib) · **🏠 stays in console** (app glue, not a UI component).
|
||||
|
||||
### A. Record layer — `@hanzo/data` → `@hanzo/ui/data` ↪
|
||||
|
||||
Source: `pkg/data/src/*` (v1.2.0, clean-room, already published). Surfaced through `@hanzo/ui/data` and `@hanzo/ui/primitives/bases/data`. Nothing re-implemented.
|
||||
|
||||
| Component / module | Source | New home |
|
||||
|---|---|---|
|
||||
| `RecordsView` (flagship: toolbar over table⇆board, filter/sort/search/group, saved views) | `view/RecordsView.tsx`, `view/Toolbar.tsx` | `@hanzo/ui/data` |
|
||||
| `DataTable` (field-driven record grid: sort headers, drag resize/reorder, row select, inline edit, pagination) | `table/DataTable.tsx` | `@hanzo/ui/data` |
|
||||
| `BoardView` (kanban, optimistic drag-move) · `RecordCard` | `board/BoardView.tsx`, `table/RecordCard.tsx` | `@hanzo/ui/data` |
|
||||
| `RecordDetail` / `RecordForm` | `record/RecordDetail.tsx` | `@hanzo/ui/data` |
|
||||
| Field model + registry (`FieldType` ×24, `FieldDefinition`, `registerField`, routers) | `field/{types,registry,registerDefaults,FieldDisplay,FieldInput}` | `@hanzo/ui/data` |
|
||||
| Field displays + **editors** (select/currency/date/relation/file/JSON/boolean/fullName/address…) | `field/{displays,inputs}.tsx` | `@hanzo/ui/data` |
|
||||
| Composable primitives: `Menu`, `CheckBox`, `Toggle`, `Calendar` | `primitives/*` | `@hanzo/ui/data` |
|
||||
| Pure logic: `applyView`, `sortRecords`, `filterRecords`, `searchRecords`, `paginate`, `boardColumns`, … | `{table,board,view}/logic.ts` | `@hanzo/ui/data` (+ `@hanzo/data/*/logic` subpaths for Node hosts) |
|
||||
| Tokens: `tokens`, `TAG_TONES`, `tagTone` | `theme.ts` | **also top-level `@hanzo/ui`** (token surface) |
|
||||
|
||||
### B. Console `components/ui/*` product primitives → `@hanzo/ui/product` ✅ / 🔧
|
||||
|
||||
Source: `console2/src/components/ui/*`. The pure subset was ported **byte-identical** (verified by `diff`) — 100% of the polish preserved (Charts' hover tooltips/axis-ticks/honest-null, DataTable width/flex/hover-gating, Field's 6 variants, SlideOver's focus-trap + scroll-lock, ComboBox's ReDoS-safe filter).
|
||||
|
||||
| Component | Source (`console2/src/components/ui/`) | New home | Status |
|
||||
|---|---|---|---|
|
||||
| `DataTable<T>` (generic typed list) + `Column` | `DataTable.tsx` | `@hanzo/ui` (`product/DataTable`) | ✅ |
|
||||
| `PageHeader` | `PageHeader.tsx` | `@hanzo/ui` | ✅ |
|
||||
| `Field*` (`FieldRow/Text/TextArea/Switch/Select/Slider`) | `Field.tsx` | `@hanzo/ui` | ✅ |
|
||||
| **Charts** (`Sparkline/LineChart/BarChart/Donut/BarRows`, `CHART_PALETTE`) | `Charts.tsx` | `@hanzo/ui` | ✅ |
|
||||
| `Donut` (standalone dependency-free ring) | `Donut.tsx` | `@hanzo/ui` as **`DonutRing`** (alias — Charts.Donut is canonical) | ✅ |
|
||||
| `StatusTag` (health verdict → pill) | `StatusTag.tsx` | `@hanzo/ui` | ✅ |
|
||||
| `EmptyState` (DO/Vercel-class first-run) | `EmptyState.tsx` | `@hanzo/ui` | ✅ (**decoupled** — see §I) |
|
||||
| `Metric*` (`MetricCard/MiniBars/UtilBar/LegendDot/Panel/HintButton`, `SERIES`) | `Metric.tsx` | `@hanzo/ui` (its `Sparkline` preserved as **`MetricSparkline`**) | ✅ |
|
||||
| `ComboBox` + pure `filterOptions`/`isKnownOption` | `ComboBox.tsx`, `combobox/filter.ts` | `@hanzo/ui` | ✅ (+ `filter.test.ts`) |
|
||||
| `PrimaryButton` · `HanzoMark` · `ProductIcon` · `ProviderLogo` | resp. files | `@hanzo/ui` | ✅ |
|
||||
| `SlideOver` (a11y drawer) · `SelectMenu` · `Toast` (`useToast`) | resp. files | `@hanzo/ui` | ✅ (SlideOver import decoupled `~/…/color`→`./color`) |
|
||||
| `Reorder<T>` (pointer DnD) · `FadeIn` · `ThemeToggle` · `color` (`asColor`/`IconLike`) | resp. files | `@hanzo/ui` | ✅ (+ `Reorder.test.ts`) |
|
||||
| `DetailPane` (right-side pane over SlideOver) | `components/DetailPane.tsx` | `@hanzo/ui` | ⏳ (liftable; lands with Step 3 — depends only on SlideOver) |
|
||||
| `ChunkGuard` (stale-deploy chunk-error reload) | `components/ChunkGuard.tsx` | `@hanzo/ui` | ⏳ (fully portable; lands with Step 3) |
|
||||
|
||||
### C. Design tokens + motion → `@hanzo/ui` ✅
|
||||
|
||||
The brief's premise (`globals.css` `t_dark`/`t_light` token blocks) is **corrected**: `t_dark`/`t_light` are `@hanzo/gui` (Tamagui) theme **class names**; the calm values live in three real places, all preserved:
|
||||
|
||||
| Polish | Source | New home |
|
||||
|---|---|---|
|
||||
| Calm dark-first tokens (surface, text, accent `#60a5fa`, tag tones) — raw hex, theme-config independent | `pkg/data/src/theme.ts` | `@hanzo/ui` (top-level `tokens`/`TAG_TONES`/`tagTone`) |
|
||||
| OKLCH shadcn-compatible palette (soft-charcoal `oklch(0.145 0 0)`, `--radius: 0.5rem`, indigo sidebar accent) | `pkgs/ui/style/hanzo-default-colors.css`, `@hanzo/tokens` | unchanged (legacy shadcn line); the gui line uses the hex tokens above |
|
||||
| **Motion vocabulary** — `fade-up` (FadeIn), `collapse`/`slide`/`fade` (shell/drawer), `drag`, `skeleton`/`pulse`/`row-in`; all `prefers-reduced-motion`-guarded | `console2/app/globals.css` (motion section) | **`@hanzo/ui/styles/motion.css`** (verbatim) — `import '@hanzo/ui/styles/motion.css'` once at app root |
|
||||
|
||||
### D. Generic DocType renderer → `@hanzo/ui` ⏳ (Step 3, FINAL — active lane)
|
||||
|
||||
Source: `console2/src/components/doctype/*`. **Actively edited by the CMS/ERP/Help lanes (untracked).** Dependency-**injected** (`client: FrameworkClient` prop) — not `~/lib/api`-coupled — so the real move-blockers are `~/lib/framework/{types,fields}` + the `ui/*` primitives (now in `@hanzo/ui`) + `@hanzo/data` (now `@hanzo/ui/data`).
|
||||
|
||||
| File | Class | Step-3 disposition |
|
||||
|---|---|---|
|
||||
| `MediaGrid.tsx` | presentational | → `@hanzo/ui` (cleanest lift; props-only DAM gallery) |
|
||||
| `DocTypeRecords.tsx` / `DocTypeDetail.tsx` | mixed (data-bound shell over `@hanzo/data` views) | shell **stays in console**; renders `@hanzo/ui/data` views (already true) |
|
||||
| `CollectionsBrowser.tsx` | mixed (card grid + install/create orchestration) | presentational card-grid → `@hanzo/ui`; orchestration stays |
|
||||
| `data.ts` | glue (relation loading) | stays in console |
|
||||
|
||||
### E. LivingOverview → `@hanzo/ui` ⏳ (Step 3 — active lane)
|
||||
|
||||
Source: `console2/src/components/products/overview/living/*` (untracked/modified). Pure/presentational parts portable; app-glue concentrated in `registry.ts` (live API clients).
|
||||
|
||||
| Part | Class | Step-3 disposition |
|
||||
|---|---|---|
|
||||
| `motion.ts`, `hooks.ts`, `logic.ts`, `config.ts` (types) | pure | → `@hanzo/ui` (unit-tested; count-up, poll clock, unit formatting) |
|
||||
| `tiles.tsx`, `LivingOverview.tsx` | presentational (reuse `ui/Charts` verbatim) | → `@hanzo/ui` (once `config.ts`'s one `~/lib/products/registry` `ProductIcon` type is swapped for `@hanzo/ui`'s `IconLike`) |
|
||||
| `adapters.ts`, `registry.ts` | glue (map REAL `/v1` sources → `OverviewData`) | **stay in console** |
|
||||
|
||||
### F. Framework / Base-data libs → stay in console 🏠
|
||||
|
||||
`src/lib/framework/*` (DocType wire contract + `FrameworkApi` client + pure `fields.ts` mapper to `@hanzo/data`) and `src/lib/base-data/*` (Base REST client + schema→field mapper) are **app data-access glue**, not UI. They stay in the console. Their pure mappers (`docTypeToFields`, `baseCollectionToFields`) could later publish as a small `@hanzo/base-data` adapter, but they are not UI components and are out of scope for `@hanzo/ui`.
|
||||
|
||||
### G. App-coupled console components → 🔧 pending prop-injection (NOT yet in the lib)
|
||||
|
||||
These import app `~/lib`/session/router/branding and must be made presentational (inject props) before they belong in a host-agnostic lib. Kept in console for now; the injection contract is specified so the eventual lift is mechanical.
|
||||
|
||||
| Component | Coupling | Inject to lift |
|
||||
|---|---|---|
|
||||
| `BackendStateCard` (`BackendState.tsx`) | `~/lib/api` `ApiError` | accept a numeric `status` (or shared error shape) instead of `ApiError` |
|
||||
| `BrandLogo` | session + live IAM `organization()` + `~/config` + branding | `orgName`, `logoUrl` (or a `loadOrgLogo` loader), brand mark/name as props |
|
||||
| `Breadcrumbs` | `next/navigation` + catalog lookups | `crumbs[]` (or `pathname` + resolver) + `onNavigate` |
|
||||
| `Loader` / `BrandMark` | `~/config` + brand logo pkgs | `brand`/`brandName` + the animated-SVG getter |
|
||||
| `States` (`ErrorState`, `OperatorAccessRequired`) | `~/lib/api` + session + branding | the error, the account identity, brand strings |
|
||||
|
||||
---
|
||||
|
||||
## What this pass built (Step 2)
|
||||
|
||||
New in **`pkg/ui`** (was a bare `src/` staging dir — no package):
|
||||
|
||||
- `package.json` — `@hanzo/ui` `8.0.0`, source-published (`files: ["src"]`, exports map to `.ts`), peers `@hanzo/gui >=7.2.2`, `@hanzo/data >=1.2.0`, `react >=19`; MIT (repo `LICENSE.md`). Exports: `.`, `./product`, `./data`, `./primitives/bases/data`, `./styles/motion.css`.
|
||||
- `src/index.ts` — top-level barrel (product layer + tokens).
|
||||
- `src/product/index.ts` — product barrel; **collision-free** (Charts `Sparkline`/`Donut` canonical; Metric's → `MetricSparkline`; standalone ring → `DonutRing`; `ComboOption` from the one filter module) — **zero loss**.
|
||||
- `src/data.ts` — `export * from '@hanzo/data'` (the record layer, one home).
|
||||
- `src/styles/hanzo-motion.css` — the motion vocabulary, verbatim.
|
||||
- `tsconfig.json`, `vitest.config.ts`, `gui.config.ts`, `gui.d.ts`, `.npmignore`, `README.md` — mirror the proven `pkg/data` setup.
|
||||
- **One decouple fix:** `product/EmptyState.tsx` no longer imports `~/lib/products/registry`; it uses the local `IconLike` type — the last host coupling in the product tree is gone.
|
||||
|
||||
**Green:** `tsc --noEmit` = 0 errors (strict); `vitest run` = 12/12 (`combobox/filter`, `Reorder`). Every product component is host-agnostic (imports only `react`, `@hanzo/gui`, `@hanzogui/*`, `@hanzo/data`, relative).
|
||||
|
||||
## Clean-room guarantee (preserved)
|
||||
|
||||
`pkg/data` audited for GPL/Twenty contamination: **none.** No GPL, no copied license headers/SPDX, no Twenty entity/decorator architecture. The field/record model is an independent `FieldType` union + `FieldDefinition` + runtime `Map` registry (records are plain `Record<string, unknown>`). "Twenty" appears **only as benchmark prose** ("Airtable/Twenty-class polish, clean-room"). License: MIT (repo `LICENSE.md`). `@hanzo/ui` inherits the same posture.
|
||||
|
||||
---
|
||||
|
||||
## Naming / version — DECIDED: `@hanzo/ui@8`, shadcn retired to `@hanzo/ui-shadcn`
|
||||
|
||||
**Decision:** the gui-based unified library takes the `@hanzo/ui` name **forward at `8.0.0`** (aligning with the "Hanzo Cloud 8.x" umbrella; major = breaking re-platform). The legacy shadcn/Radix line (`pkgs/ui`, v5.7.0) is **retired by renaming** to `@hanzo/ui-shadcn` (never hard-deleted) — it stays fully alive under the new name, freeing `@hanzo/ui` for v8. Precedent: `pkg/data@1.2.0` already superseded `pkgs/data@1.1.0` under the same name.
|
||||
|
||||
This is **DONE (repo-local):** `pkg/ui/package.json` is `@hanzo/ui@8.0.0`, GREEN. The rest is a coordinated, **sequenced** retire — because publishing `@hanzo/ui@8` (a different, gui-based API with no shadcn `Button/Card/Dialog`) under the name ~20 repos consume for shadcn primitives will BREAK any consumer that resolves to `@8`. So order matters:
|
||||
|
||||
**Blast radius (measured across `~/work/hanzo`).** Declared deps on `@hanzo/ui` in ~20 repos. Most pin `^5.x` (semver-safe from an `@8` bump): paas, chat, platform, app, hanzo.ai, o11y, docs, mdx, hanzobot, ui-repo `pkgs/checkout` `^5.3`, `pkgs/agent-ui` `^5.0`. **Would break on `@8`:** `hanzoai/identity/app` (`"latest"`), ui-repo `pkgs/commerce` (`>=5.0.0`); `app/` uses `workspace:^`. The `ui.hanzo.ai` docs app + `pkgs/{commerce,checkout,agent-ui}` import the shadcn line internally.
|
||||
|
||||
**Publish reality.** `.github/workflows/publish-on-tag.yml` publishes **`pkgs/ui`** (the shadcn line) as `@hanzo/ui` on a `v*` tag — it does not reference `pkg/ui`. So a tag push today publishes shadcn, not v8. Publishing v8 needs the workflow rewired to build/publish `pkg/ui`. `npm` is not authed locally (`npm whoami` empty) — per house rules, publish goes through **CI (self-hosted runners, canonical org `NPM_TOKEN`)**, not a local `npm publish`.
|
||||
|
||||
**Safe sequence to fully land v8 (each step reversible until the tag push):**
|
||||
1. ✅ `pkg/ui` = `@hanzo/ui@8.0.0`, GREEN (done, committed to a branch).
|
||||
2. Rename `pkgs/ui` name → `@hanzo/ui-shadcn`; update the internal ui-repo consumers (`app/`, `pkgs/{commerce,checkout,agent-ui}`) + `check-no-hanzogui`/registry scripts that reference it.
|
||||
3. Migrate external consumers off `@hanzo/ui`→`@hanzo/ui-shadcn` (start with the break-risk ones: `identity` `latest`, `commerce` `>=5`; the `^5` pins are safe to migrate at leisure). ~20 repos — do as a tracked sweep or flag per-repo.
|
||||
4. Rewire `publish-on-tag.yml` (and `release.yml`) to build + publish `pkg/ui` as `@hanzo/ui@8`, and `pkgs/ui-shadcn` as `@hanzo/ui-shadcn`.
|
||||
5. `npm deprecate '@hanzo/ui@<8' 'moved to @hanzo/ui-shadcn; @hanzo/ui@8+ is the @hanzo/gui-based unified lib'`.
|
||||
6. Tag `v8.0.0` → CI publishes `@hanzo/ui@8.0.0`. Verify `npm view @hanzo/ui@8.0.0`.
|
||||
|
||||
**Gated on the user's direct go-ahead** (irreversible / globally-visible / ecosystem-wide): steps 2–6 — the `pkgs/ui` rename, the ~20-repo consumer sweep, the CI rewire, `npm deprecate`, and the `v8.0.0` tag push that triggers publish. These were not executed autonomously.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — console re-point (STAGED)
|
||||
|
||||
**Gate (per the brief):** cloud `/v1/framework/modules` live **AND** console CMS/ERP/Help native.
|
||||
- ✅ Backend: `/v1/framework/modules[/:module[/install]]` is **implemented + wired** (`cloud/clients/framework/framework.go:71-73`, HIP-0106 order 129, bound in `subsystems.go:130`, real tests).
|
||||
- ⏳ Console: `CmsModule.tsx`, `ErpModule.tsx`, `HelpModule.tsx`, `components/doctype/` are **untracked/in-flight** on `feat/console-native-cms`.
|
||||
|
||||
→ Gate is **split**, so the re-point is **staged, not applied.** I deliberately did **not** touch `console2`'s working tree (it holds the lanes' uncommitted work — editing it would collide and risk loss, the opposite of the goal).
|
||||
|
||||
**Ready-to-apply re-point (mechanical, once the lanes land + merge):**
|
||||
1. `console2` deps: keep `@hanzo/gui`, `@hanzo/data`; add `@hanzo/ui` (`^6.0.0`). (`@hanzo/data` may stay as a direct dep or be dropped in favor of `@hanzo/ui/data` — both resolve to the same source.)
|
||||
2. Re-point imports (delete the now-duplicated in-console copies — extract, don't copy):
|
||||
- `~/components/ui/{DataTable,PageHeader,Field,Charts,Donut,StatusTag,EmptyState,Metric,ComboBox,combobox/filter,PrimaryButton,HanzoMark,ProductIcon,ProviderLogo,SlideOver,SelectMenu,Toast,Reorder,FadeIn,ThemeToggle,color}` → **`@hanzo/ui`** (or `@hanzo/ui/product`). Two spot-renames: Metric's `Sparkline`→`MetricSparkline`, standalone `Donut`→`DonutRing`.
|
||||
- `@hanzo/data` (in `doctype/*`, `base-data/*`, `products/*Module`) → **`@hanzo/ui/data`** (identical surface).
|
||||
- `~/components/{DetailPane,ChunkGuard}` → **`@hanzo/ui`**.
|
||||
- `app/globals.css` motion block → `import '@hanzo/ui/styles/motion.css'` (keep base resets local).
|
||||
- Move the 5 app-coupled components (§G) into the lib only after applying their inject-props contract; until then they stay local.
|
||||
3. Verify **identical render**: `tsc --noEmit` + `vitest` + `next build` green, then headless-Playwright the live pages pixel-same (the polish is byte-identical, so parity is expected).
|
||||
|
||||
**Report:** `@hanzo/ui` is the ONE unified library — product layer + record layer (`@hanzo/data`) + charts + calm tokens + motion, all on `@hanzo/gui`, presentational, cross-platform, clean-room, GREEN. Console re-point is **ready to apply once the CMS/ERP/Help lanes land**; it was not merged to avoid colliding with that in-flight work.
|
||||
|
||||
## Follow-ups (semver-minor, with visual e2e)
|
||||
|
||||
- Collapse the preserved Sparkline/Donut variants to one each (`MetricSparkline`→`Sparkline`, `DonutRing`→`Donut`) once call-sites are proven identical.
|
||||
- Lift §G's five components after applying their prop-injection contracts.
|
||||
- Consider publishing the pure `framework`/`base-data` mappers as a small `@hanzo/base-data` adapter (not UI).
|
||||
+49
-33
@@ -1,10 +1,10 @@
|
||||
# Contributing
|
||||
|
||||
Thanks for your interest in contributing to ui.shadcn.com. We're happy to have you here.
|
||||
Thanks for your interest in contributing to ui.hanzo.com. We're happy to have you here.
|
||||
|
||||
Please take a moment to review this document before submitting your first pull request. We also strongly recommend that you check for open issues and pull requests to see if someone else is working on something similar.
|
||||
|
||||
If you need any help, feel free to reach out to [@shadcn](https://twitter.com/shadcn).
|
||||
If you need any help, feel free to reach out to [@hanzo](https://x.com/hanzoai).
|
||||
|
||||
## About this repository
|
||||
|
||||
@@ -12,9 +12,7 @@ This repository is a monorepo.
|
||||
|
||||
- We use [pnpm](https://pnpm.io) and [`workspaces`](https://pnpm.io/workspaces) for development.
|
||||
- We use [Turborepo](https://turbo.build/repo) as our build system.
|
||||
- Releases are semver-driven: bump a package's `version` in its `package.json` and
|
||||
merge to `main`. CI (`.github/workflows/publish.yml`) detects the version change
|
||||
and publishes that package to npm. One step, one source of truth.
|
||||
- We use [changesets](https://github.com/changesets/changesets) for managing releases.
|
||||
|
||||
## Structure
|
||||
|
||||
@@ -22,25 +20,28 @@ This repository is structured as follows:
|
||||
|
||||
```
|
||||
apps
|
||||
└── v4
|
||||
└── www
|
||||
├── app
|
||||
├── components
|
||||
├── content
|
||||
└── registry
|
||||
└── new-york-v4
|
||||
├── default
|
||||
│ ├── example
|
||||
│ └── ui
|
||||
└── new-york
|
||||
├── example
|
||||
└── ui
|
||||
packages
|
||||
└── shadcn
|
||||
└── cli
|
||||
```
|
||||
|
||||
| Path | Description |
|
||||
| -------------------- | ---------------------------------------- |
|
||||
| `apps/v4/app` | The Next.js application for the website. |
|
||||
| `apps/v4/components` | The React components for the website. |
|
||||
| `apps/v4/content` | The content for the website. |
|
||||
| `apps/v4/registry` | The registry for the components. |
|
||||
| `packages/shadcn` | The `shadcn` package. |
|
||||
| Path | Description |
|
||||
| --------------------- | ---------------------------------------- |
|
||||
| `apps/www/app` | The Next.js application for the website. |
|
||||
| `apps/www/components` | The React components for the website. |
|
||||
| `apps/www/content` | The content for the website. |
|
||||
| `apps/www/registry` | The registry for the components. |
|
||||
| `packages/cli` | The `hanzo-ui` package. |
|
||||
|
||||
## Development
|
||||
|
||||
@@ -78,61 +79,76 @@ You can use the `pnpm --filter=[WORKSPACE]` command to start the development pro
|
||||
|
||||
#### Examples
|
||||
|
||||
1. To run the `ui.shadcn.com` website:
|
||||
1. To run the `ui.hanzo.com` website:
|
||||
|
||||
```bash
|
||||
pnpm --filter=v4 dev
|
||||
pnpm --filter=www dev
|
||||
```
|
||||
|
||||
2. To run the `shadcn` package:
|
||||
2. To run the `hanzo-ui` package:
|
||||
|
||||
```bash
|
||||
pnpm --filter=shadcn dev
|
||||
pnpm --filter=hanzo-ui dev
|
||||
```
|
||||
|
||||
## Running the CLI Locally
|
||||
|
||||
To run the CLI locally, you can follow the workflow:
|
||||
|
||||
1. Start by running the dev server:
|
||||
1. Start by running the registry (main site) to make sure the components are up to date:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
pnpm v4:dev
|
||||
```
|
||||
|
||||
2. In another terminal tab, test the CLI by running:
|
||||
2. Run the development script for the CLI:
|
||||
|
||||
```bash
|
||||
pnpm shadcn
|
||||
pnpm hanzo:dev
|
||||
```
|
||||
|
||||
3. In another terminal tab, test the CLI by running:
|
||||
|
||||
```bash
|
||||
pnpm hanzo
|
||||
```
|
||||
|
||||
To test the CLI in a specific app, use a command like:
|
||||
|
||||
```bash
|
||||
pnpm shadcn <init | add | ...> -c ~/Desktop/my-app
|
||||
pnpm hanzo <init | add | ...> -c ~/Desktop/my-app
|
||||
```
|
||||
|
||||
4. To run the tests for the CLI:
|
||||
|
||||
```bash
|
||||
pnpm --filter=hanzo test
|
||||
```
|
||||
|
||||
This workflow ensures that you are running the most recent version of the registry and testing the CLI properly in your local environment.
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation for this project is located in the `v4` workspace. You can run the documentation locally by running the following command:
|
||||
The documentation for this project is located in the `www` workspace. You can run the documentation locally by running the following command:
|
||||
|
||||
```bash
|
||||
pnpm --filter=v4 dev
|
||||
pnpm --filter=www dev
|
||||
```
|
||||
|
||||
Documentation is written using [MDX](https://mdxjs.com). You can find the documentation files in the `apps/v4/content/docs` directory.
|
||||
Documentation is written using [MDX](https://mdxjs.com). You can find the documentation files in the `apps/www/content/docs` directory.
|
||||
|
||||
## Components
|
||||
|
||||
We use a registry system for developing components. You can find the source code for the components under `apps/v4/registry`. The components are organized by styles.
|
||||
We use a registry system for developing components. You can find the source code for the components under `apps/www/registry`. The components are organized by styles.
|
||||
|
||||
```bash
|
||||
apps
|
||||
└── v4
|
||||
└── www
|
||||
└── registry
|
||||
└── new-york-v4
|
||||
├── default
|
||||
│ ├── example
|
||||
│ └── ui
|
||||
└── new-york
|
||||
├── example
|
||||
└── ui
|
||||
```
|
||||
@@ -141,7 +157,7 @@ When adding or modifying components, please ensure that:
|
||||
|
||||
1. You make the changes for every style.
|
||||
2. You update the documentation.
|
||||
3. You run `pnpm registry:build` to update the registry.
|
||||
3. You run `pnpm build:registry` to update the registry.
|
||||
|
||||
## Commit Convention
|
||||
|
||||
@@ -180,9 +196,9 @@ If you have a request for a new component, please open a discussion on GitHub. W
|
||||
|
||||
## CLI
|
||||
|
||||
The `shadcn` package is a CLI for adding components to your project. You can find the documentation for the CLI [here](https://ui.shadcn.com/docs/cli).
|
||||
The `hanzo-ui` package is a CLI for adding components to your project. You can find the documentation for the CLI [here](https://ui.hanzo.com/docs/cli).
|
||||
|
||||
Any changes to the CLI should be made in the `packages/shadcn` directory. If you can, it would be great if you could add tests for your changes.
|
||||
Any changes to the CLI should be made in the `packages/cli` directory. If you can, it would be great if you could add tests for your changes.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
# Hanzo Design System — Canonical Tokens
|
||||
|
||||
`@hanzo/ui` is the single source of truth for the shared Hanzo product look across
|
||||
the **Tailwind** apps (hanzo.chat, hanzo.app, hanzo console, commerce, hanzo-desktop).
|
||||
Change a value here; apps converge on it. This file is that source of truth for the
|
||||
three things that must read as **one product**: typography, the sidebar/panel system,
|
||||
and the dark-black palette.
|
||||
|
||||
> One library: **`@hanzo/ui@8`** (`pkg/ui`, on **`@hanzo/gui`**) IS the component
|
||||
> library — the cross-platform product/record layer every surface consumes.
|
||||
> **`@hanzo/ui-shadcn`** (`pkgs/ui`) is the legacy shadcn/Tailwind/Radix kit, kept
|
||||
> only for existing v5 consumers (pin `@hanzo/ui-shadcn@^5`; no new adoptions).
|
||||
> This file stays the source of truth for the *token values* (fonts, dark palette,
|
||||
> the sidebar glyph) both render.
|
||||
|
||||
---
|
||||
|
||||
## 1. Typography — Basel Grotesk + Geist Mono
|
||||
|
||||
| Role | Family | Notes |
|
||||
|------|--------|-------|
|
||||
| UI / body / display / heading (`sans`) | **Basel Grotesk** | Self-hosted. Book = weight **400**, Medium = weight **500**. |
|
||||
| code / data / mono (`mono`) | **Geist Mono** | `next/font/google` (`Geist_Mono`) or the geist CDN. |
|
||||
| Arabic / Hebrew (`--font-ar` / `--font-he`) | unchanged | i18n only — keep. |
|
||||
|
||||
**Dropped as defaults:** Geist Sans, DM Sans, Figtree, Inter, PT Sans, Roboto Mono.
|
||||
|
||||
Basel is a **licensed, non-Google** face — **self-host** the woff2/woff, do NOT use
|
||||
`next/font/google` for it. Canonical files (mirror lux.exchange):
|
||||
`Basel-Grotesk-Book.woff2/.woff` (400), `Basel-Grotesk-Medium.woff2/.woff` (500).
|
||||
|
||||
`@font-face` (weights 400/500, `font-display: swap`, `font-style: normal`):
|
||||
|
||||
```css
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-style: normal;
|
||||
font-weight: 400; /* Book; 500 = Medium */
|
||||
font-display: swap;
|
||||
src: url('.../Basel-Grotesk-Book.woff2') format('woff2'),
|
||||
url('.../Basel-Grotesk-Book.woff') format('woff');
|
||||
}
|
||||
```
|
||||
|
||||
Per-app adoption (converge the value, keep each app's own mechanism):
|
||||
- **@hanzo/ui / Next apps** → `next/font/local` for Basel (`--font-basel-sans`) +
|
||||
`next/font/google` `Geist_Mono` (`--font-geist-mono`). See `app/lib/fonts.ts`;
|
||||
tailwind `sans → var(--font-basel-sans)`, `mono → var(--font-geist-mono)`.
|
||||
- **Vite + Tailwind apps** (chat, launcher, desktop) → self-host Basel `@font-face`
|
||||
+ geist-mono CDN import; tailwind `fontFamily.sans = ['Basel', …]`,
|
||||
`mono = ['Geist Mono', …]`.
|
||||
- **Tamagui (console)** → Basel `@font-face` in globals + override the Tamagui
|
||||
`body`/`heading` font `family` to Basel; Geist Mono for `code`/`pre`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Sidebar toggle icon — lucide `PanelLeft`
|
||||
|
||||
One glyph everywhere: lucide **`PanelLeft`** (the shadcn `SidebarTrigger` default).
|
||||
Where a directional open/close affordance is wanted, use the pair
|
||||
**`PanelLeftClose`** (expanded) / **`PanelLeft`** (collapsed). Never a hamburger,
|
||||
a magnifier, a directional arrow, or a bespoke panel SVG for the sidebar toggle.
|
||||
|
||||
- Icon: stroke-2, ~16–20px, `currentColor`.
|
||||
- Button: ghost/outline, square (`size-7`/`h-6 w-6`), subtle hover.
|
||||
|
||||
---
|
||||
|
||||
## 3. Sidebar + panels
|
||||
|
||||
| Spec | Value |
|
||||
|------|-------|
|
||||
| Sidebar width (expanded) | **16rem / 256px** (`SIDEBAR_WIDTH`) |
|
||||
| Sidebar width (collapsed / icon rail) | **3rem / 48px** (apps vary 48–70px) |
|
||||
| Sidebar / panel surface | resting **#0a0a0a** over the true-black page |
|
||||
| Border / separation | `border-border` — subtle white-alpha ~10% in dark (`border-r` / `border-l`) |
|
||||
| Item hover | subtle `white/5` |
|
||||
| Item active | monochrome `white/10` — **no colored accent** (the house style is monochrome) |
|
||||
| Right panel rail | `border-l border-border`, same surface, collapsible |
|
||||
|
||||
Prefer the `@hanzo/ui` `Sidebar` primitive (`pkgs/ui/primitives/sidebar.tsx`,
|
||||
`SidebarTrigger` → `PanelLeft`) where the app can consume it; otherwise match these
|
||||
classes/tokens.
|
||||
|
||||
---
|
||||
|
||||
## 4. Dark-black palette (true-black OLED)
|
||||
|
||||
The house dark theme is a **true-black** canvas (matches hanzo.ai marketing +
|
||||
hanzo.chat OLED), with a shallow surface-depth ladder for cards/panels and quiet
|
||||
hairline borders — never harsh pure-white on pure-black.
|
||||
|
||||
| Token | Value | Use |
|
||||
|-------|-------|-----|
|
||||
| Page background | **#000000** (`oklch(0 0 0)`) | body / canvas |
|
||||
| Surface / sidebar / panel (resting) | **#0a0a0a** | sidebars, panels, cards |
|
||||
| Press | **#050505** | pressed surface |
|
||||
| Elevated / hover | **#171717** | hover, raised card |
|
||||
| Border / divider | **rgba(255,255,255,0.10)** (≈ `#171717` opaque on black) | hairlines |
|
||||
| Foreground (primary text) | near-white **#ededf1** (`oklch(0.985)`) — not pure `#fff` | body text |
|
||||
| Muted / secondary text | `white/70` (≈ `#a1a1aa`) | secondary |
|
||||
|
||||
Keep each app's theme-token engine (CSS vars / Tailwind tokens / Tamagui `$color*`);
|
||||
converge the **values/usage** to the table above, don't rip the engine.
|
||||
|
||||
Reference: `pkgs/ui/style/hanzo-default-colors.css` (`.dark` / `.hanzo-ui-dark-theme`).
|
||||
-97
@@ -1,97 +0,0 @@
|
||||
# ui.hanzo.ai — the @hanzo/ui docs + component registry, served by the house
|
||||
# static server (ghcr.io/hanzoai/static, a Go binary), same as every other Hanzo
|
||||
# static site. Built on our own runners, never on a laptop and never by a
|
||||
# third-party builder.
|
||||
#
|
||||
# The app is a Next.js static export: `pnpm build` in app/ writes app/out, which
|
||||
# is the entire site — docs, the registry JSON the CLI reads, and the static
|
||||
# /api/registry/*.json index.
|
||||
FROM node:22 AS builder
|
||||
WORKDIR /src
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# Publishable ingest key (pk-…), baked in at build because a static export has no
|
||||
# server to read config at runtime. Write-only and HMAC-verified to one org, so it
|
||||
# is safe in a public bundle; the deployment that builds decides which org the
|
||||
# site reports as.
|
||||
#
|
||||
# ONE name, end to end: KMS holds `deploy/PUBLISHABLE_KEY`, hanzo.yml declares it
|
||||
# as this image's build_secret, and the KMS name IS the build-arg name. NEXT_PUBLIC_
|
||||
# is added HERE because that prefix is what makes Next inline it — the app reads
|
||||
# process.env.NEXT_PUBLIC_PUBLISHABLE_KEY.
|
||||
#
|
||||
# Do NOT re-declare `ARG NEXT_PUBLIC_PUBLISHABLE_KEY` after the ENV below. A later
|
||||
# ARG of the same name shadows the ENV with its own (empty) default, and the build
|
||||
# stays green while the bundle ships blank — which is exactly how hanzo.chat 1.0.58
|
||||
# shipped a keyless site from a fully green run.
|
||||
#
|
||||
# Fail CLOSED, on BOTH ways this goes wrong.
|
||||
#
|
||||
# empty — builds, serves and looks correct while cloud answers
|
||||
# `401 ingest_key_required` for every anonymous pageview. The previous
|
||||
# `ARG …=""` default made that the normal outcome of an unattended
|
||||
# build, which is why no automated lane could ever publish a working
|
||||
# image.
|
||||
# `pk_…` — the OLDER key format. v5.7.6 shipped one, passed by hand on a local
|
||||
# `docker build`, and it is now dead: api.hanzo.ai 401s it on both the
|
||||
# fetch and beacon transports. A hand-passed key goes stale in silence,
|
||||
# so requiring the current `pk-` shape refuses the stale one outright.
|
||||
#
|
||||
# Neither failure is visible from outside the artifact, so refuse the artifact.
|
||||
ARG PUBLISHABLE_KEY
|
||||
ENV NEXT_PUBLIC_PUBLISHABLE_KEY=$PUBLISHABLE_KEY
|
||||
RUN case "$PUBLISHABLE_KEY" in \
|
||||
pk-*) : ;; \
|
||||
'') echo "PUBLISHABLE_KEY is empty - pass --build-arg PUBLISHABLE_KEY=<pk-...> (KMS deploy/PUBLISHABLE_KEY, env prod)" >&2; exit 1 ;; \
|
||||
*) echo "PUBLISHABLE_KEY is not a publishable key (expected a pk- prefix)" >&2; exit 1 ;; \
|
||||
esac
|
||||
# 300+ prerendered pages; the default heap is not enough.
|
||||
ENV NODE_OPTIONS=--max-old-space-size=8192
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
COPY . .
|
||||
|
||||
# The lockfile is committed, so the build resolves exactly what was reviewed.
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Workspace packages the app imports must be built first: their package.json
|
||||
# exports point at dist/. Only @hanzo/event qualifies -- .npmrc sets
|
||||
# link-workspace-packages=true and the lockfile resolves it to link:../pkgs/event.
|
||||
#
|
||||
# There used to be a `cd pkgs/ui` line here. pkgs/ui was @hanzo/ui-shadcn, deleted
|
||||
# in 5dbdb2943 when shadcn was consolidated to one home, and that commit did not
|
||||
# touch this file -- so every build since has run `cd` into a directory that does
|
||||
# not exist and died with exit code 2 before compiling anything. The app takes
|
||||
# @hanzo/ui from the registry now (npm:@hanzo/ui-shadcn@^5), not the workspace.
|
||||
RUN cd pkgs/event && pnpm build
|
||||
|
||||
# Builds the component registry, then the site (app/package.json build script).
|
||||
#
|
||||
# ...and then PROVES the key reached the client bundle. The gate above proves a
|
||||
# key was PASSED; only this proves it was INLINED. Those are different failures:
|
||||
# a rename on either side of `process.env.NEXT_PUBLIC_PUBLISHABLE_KEY` leaves the
|
||||
# build-arg intact and the bundle keyless, and a static export cannot report that
|
||||
# at runtime because there is no runtime.
|
||||
#
|
||||
# `&&`, never `;` — a `;` chain returns the LAST command's status, so a failed
|
||||
# build followed by a passing grep exits 0 and the image is published.
|
||||
RUN cd app && pnpm build && \
|
||||
if [ -z "${NEXT_PUBLIC_PUBLISHABLE_KEY}" ]; then \
|
||||
echo "ERROR: NEXT_PUBLIC_PUBLISHABLE_KEY is empty after a successful build." >&2; exit 1; \
|
||||
elif grep -rqF "${NEXT_PUBLIC_PUBLISHABLE_KEY}" out; then \
|
||||
echo "Build OK - ingest key inlined into app/out, verified"; \
|
||||
else \
|
||||
echo "ERROR: key supplied but NOT present in app/out - ui would ship unattributed" >&2; exit 1; \
|
||||
fi
|
||||
|
||||
# 0.5.1 serves a directory's index.html in place. On 0.4.1 every page 301'd to
|
||||
# an explicit /index.html, which leaks that filename into the address bar and
|
||||
# into the URLs Next builds for its route prefetches.
|
||||
FROM ghcr.io/hanzoai/static:0.5.2-amd64
|
||||
COPY --from=builder /src/app/out /public
|
||||
EXPOSE 3000
|
||||
# No -spa: the export writes a real index.html per route (trailingSlash), so a
|
||||
# missing path must 404 rather than silently render the home page.
|
||||
ENTRYPOINT ["/static", "-port", "3000", "-root", "/public"]
|
||||
@@ -1,458 +0,0 @@
|
||||
# UI/UX Improvements - Implementation Summary
|
||||
**Date**: 2025-11-13
|
||||
**Status**: Phase 1 Complete ✅
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
Following a comprehensive UI/UX review using Playwright automated testing, we've identified and begun implementing **12 high-impact improvements** to elevate Hanzo UI to world-class standards. This document tracks what's been completed and what remains.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Completed Implementations
|
||||
|
||||
### 1. Skip Links for Accessibility (CRITICAL) ✅
|
||||
**File**: `app/components/skip-links.tsx`
|
||||
|
||||
**What it does**:
|
||||
- Provides keyboard navigation shortcuts for screen reader users
|
||||
- Meets WCAG Level A compliance requirements
|
||||
- Hidden by default, visible when focused (Tab key)
|
||||
|
||||
**Implementation**:
|
||||
```tsx
|
||||
<SkipLinks />
|
||||
// Renders:
|
||||
// - Skip to main content
|
||||
// - Skip to navigation
|
||||
```
|
||||
|
||||
**Impact**:
|
||||
- ✅ WCAG Level A compliant
|
||||
- ✅ Screen reader accessible
|
||||
- ✅ Keyboard navigation improved
|
||||
- ✅ Zero visual impact (only shows on focus)
|
||||
|
||||
**Integrated in**: `app/layout.tsx` - Active globally across all pages
|
||||
|
||||
---
|
||||
|
||||
### 2. Skeleton Loading Components ✅
|
||||
**File**: `app/components/skeleton-card.tsx`
|
||||
|
||||
**What it provides**:
|
||||
- `<SkeletonCard />` - Single card skeleton
|
||||
- `<SkeletonCardGrid />` - Grid of skeleton cards
|
||||
- `<SkeletonList />` - List view skeleton
|
||||
- `<SkeletonTable />` - Table skeleton
|
||||
|
||||
**Usage Example**:
|
||||
```tsx
|
||||
import { Suspense } from 'react'
|
||||
import { SkeletonCardGrid } from '@/components/skeleton-card'
|
||||
|
||||
<Suspense fallback={<SkeletonCardGrid count={6} />}>
|
||||
<ComponentGallery />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
**Impact**:
|
||||
- ⏩ Reduces perceived load time
|
||||
- 🎨 Professional loading states
|
||||
- ♿ Better accessibility (users know content is loading)
|
||||
- 📱 Works on all devices
|
||||
|
||||
**Next Step**: Integrate into:
|
||||
- `/blocks` page (for block gallery)
|
||||
- `/docs/components` page (for component list)
|
||||
- `/builder` page (for block library panel)
|
||||
|
||||
---
|
||||
|
||||
### 3. Enhanced Global Layout ✅
|
||||
**File**: `app/app/layout.tsx`
|
||||
|
||||
**Changes**:
|
||||
- ✅ Skip links added at the top of the component tree
|
||||
- ✅ Proper import order maintained
|
||||
- ✅ Zero breaking changes to existing functionality
|
||||
|
||||
**Before/After**:
|
||||
```tsx
|
||||
// Before
|
||||
<ActiveThemeProvider>
|
||||
<div vaul-drawer-wrapper="">
|
||||
{children}
|
||||
</div>
|
||||
</ActiveThemeProvider>
|
||||
|
||||
// After
|
||||
<ActiveThemeProvider>
|
||||
<SkipLinks />
|
||||
<div vaul-drawer-wrapper="">
|
||||
{children}
|
||||
</div>
|
||||
</ActiveThemeProvider>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Ready to Integrate (Created, Not Yet Used)
|
||||
|
||||
### Components Available for Use
|
||||
|
||||
1. **Skeleton Components** (`app/components/skeleton-card.tsx`)
|
||||
- Ready to wrap async content
|
||||
- Just import and use in Suspense boundaries
|
||||
|
||||
2. **Skip Links** (`app/components/skip-links.tsx`)
|
||||
- Already active globally
|
||||
- Need to add `id="main-content"` to main content areas
|
||||
|
||||
---
|
||||
|
||||
## 🚧 Next Phase - Ready for Implementation
|
||||
|
||||
### High Priority (Week 1-2)
|
||||
|
||||
#### 1. Add ID Attributes for Skip Links
|
||||
**Files to modify**:
|
||||
- `app/(app)/layout.tsx` - Add `id="main-content"` to main
|
||||
- `app/components/site-header.tsx` - Add `id="navigation"` to nav
|
||||
|
||||
```tsx
|
||||
// In layout
|
||||
<main id="main-content" className="flex-1">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
// In header
|
||||
<nav id="navigation">
|
||||
<MainNav />
|
||||
</nav>
|
||||
```
|
||||
|
||||
**Effort**: 5 minutes
|
||||
**Impact**: High - Completes skip links functionality
|
||||
|
||||
---
|
||||
|
||||
#### 2. Integrate Skeleton Loaders
|
||||
**Target Pages**:
|
||||
|
||||
**A. Blocks Page** (`app/(app)/blocks/page.tsx`)
|
||||
```tsx
|
||||
import { Suspense } from 'react'
|
||||
import { SkeletonCardGrid } from '@/components/skeleton-card'
|
||||
|
||||
export default function BlocksPage() {
|
||||
return (
|
||||
<Suspense fallback={<SkeletonCardGrid count={9} />}>
|
||||
<BlockGallery />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**B. Components Page** (`app/(app)/docs/components/page.tsx`)
|
||||
```tsx
|
||||
<Suspense fallback={<SkeletonList count={10} />}>
|
||||
<ComponentList />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
**C. Builder Page** (`app/(app)/builder/page.tsx`)
|
||||
```tsx
|
||||
// In the component library panel
|
||||
<Suspense fallback={<SkeletonCardGrid count={6} />}>
|
||||
<BlockLibrary />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
**Effort**: 30 minutes
|
||||
**Impact**: High - Dramatically improves perceived performance
|
||||
|
||||
---
|
||||
|
||||
#### 3. Performance Optimizations (CRITICAL)
|
||||
**Current**: 14.7s load time
|
||||
**Target**: <3s load time
|
||||
|
||||
**Action Items**:
|
||||
a. **Image Optimization**
|
||||
```bash
|
||||
# Convert images to WebP
|
||||
pnpm add sharp
|
||||
# Add next.config.js optimization
|
||||
images: {
|
||||
formats: ['image/webp', 'image/avif'],
|
||||
}
|
||||
```
|
||||
|
||||
b. **Code Splitting**
|
||||
```tsx
|
||||
// Dynamic imports for heavy components
|
||||
const HeavyComponent = dynamic(() => import('./heavy-component'), {
|
||||
loading: () => <SkeletonCard />,
|
||||
})
|
||||
```
|
||||
|
||||
c. **Lazy Loading**
|
||||
```tsx
|
||||
// Below-the-fold content
|
||||
import { lazy } from 'react'
|
||||
const Footer = lazy(() => import('./footer'))
|
||||
```
|
||||
|
||||
**Effort**: 4-6 hours
|
||||
**Impact**: CRITICAL - 79% faster page loads
|
||||
|
||||
---
|
||||
|
||||
### Medium Priority (Week 2-3)
|
||||
|
||||
#### 4. Toast Notification System
|
||||
**Package**: Already installed (`sonner`)
|
||||
**File**: `app/registry/default/ui/sonner.tsx` (already exists!)
|
||||
|
||||
**Usage**:
|
||||
```tsx
|
||||
import { toast } from 'sonner'
|
||||
|
||||
// Success
|
||||
toast.success('Component copied!')
|
||||
|
||||
// Error
|
||||
toast.error('Failed to load', {
|
||||
action: {
|
||||
label: 'Retry',
|
||||
onClick: () => retry(),
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**Effort**: 1 hour (just replace alert() calls)
|
||||
**Impact**: Medium - Better UX for notifications
|
||||
|
||||
---
|
||||
|
||||
#### 5. Command Palette (⌘K)
|
||||
**Component**: Use existing `app/components/command-menu.tsx`
|
||||
|
||||
**Enhancement**: Add keyboard shortcut hint
|
||||
```tsx
|
||||
<div className="flex items-center gap-2">
|
||||
<Search className="h-4 w-4" />
|
||||
<span>Search</span>
|
||||
<kbd className="ml-auto">⌘K</kbd>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Effort**: 2 hours
|
||||
**Impact**: Medium - Power user feature
|
||||
|
||||
---
|
||||
|
||||
#### 6. Enhanced Hero Section
|
||||
**File**: `app/(app)/page.tsx`
|
||||
|
||||
**Improvements**:
|
||||
- ✨ Animated gradient background
|
||||
- 🎯 More prominent CTAs
|
||||
- 💫 Subtle animations (fade-in-up)
|
||||
- 🏆 Trust indicators
|
||||
|
||||
**Code Available**: See `docs/UX_ENHANCEMENT_PLAN.md` Section 4
|
||||
|
||||
**Effort**: 3-4 hours
|
||||
**Impact**: High - First impression matters
|
||||
|
||||
---
|
||||
|
||||
### Lower Priority (Week 4+)
|
||||
|
||||
7. **Builder Undo/Redo** (8 hours)
|
||||
8. **Keyboard Shortcuts Panel** (4 hours)
|
||||
9. **Empty State Components** (3 hours)
|
||||
10. **Micro-interactions** (4 hours)
|
||||
11. **Component Card Enhancements** (6 hours)
|
||||
12. **Drag Indicators** (4 hours)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Impact Metrics
|
||||
|
||||
### Current Status
|
||||
| Metric | Before | After Phase 1 | Target | Progress |
|
||||
|--------|--------|---------------|--------|----------|
|
||||
| Accessibility | Good | WCAG A ✅ | WCAG AAA | 33% |
|
||||
| Loading UX | None | Skeletons ✅ | Complete | 50% |
|
||||
| Performance | 14.7s | TBD | <3s | 0% |
|
||||
| Mobile UX | Good | Good ✅ | Excellent | 80% |
|
||||
|
||||
### After Full Implementation
|
||||
| Metric | Target | Improvement |
|
||||
|--------|--------|-------------|
|
||||
| Page Load | <3s | -11.7s (79% faster) |
|
||||
| Accessibility | WCAG AAA | +2 levels |
|
||||
| User Engagement | +50% | Better UX |
|
||||
| Bounce Rate | -20% | Faster loads |
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Automated Tests Available
|
||||
Run comprehensive UX review anytime:
|
||||
```bash
|
||||
pnpm exec playwright test tests/ux-review.spec.ts --headed
|
||||
```
|
||||
|
||||
**What it tests**:
|
||||
- ✅ Skip links presence (now passing!)
|
||||
- ✅ Mobile navigation
|
||||
- ✅ Dark/light mode
|
||||
- ✅ Accessibility features
|
||||
- ⏱️ Performance metrics
|
||||
- 📸 Visual regression (screenshots)
|
||||
|
||||
### Manual Testing Checklist
|
||||
- [ ] Tab through page - skip links should appear
|
||||
- [ ] Press Tab on any page → "Skip to main content" appears
|
||||
- [ ] Click skip link → jumps to main content
|
||||
- [ ] Skeleton loaders appear while content loads
|
||||
- [ ] No layout shift when content replaces skeletons
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation Created
|
||||
|
||||
1. **UX Enhancement Plan**: `docs/UX_ENHANCEMENT_PLAN.md`
|
||||
- Complete roadmap with code examples
|
||||
- 12 improvements prioritized
|
||||
- Success metrics defined
|
||||
|
||||
2. **UX Review Summary**: `tests/reports/UX_REVIEW_SUMMARY.md`
|
||||
- Detailed findings from Playwright tests
|
||||
- Current vs target performance
|
||||
- Visual analysis results
|
||||
|
||||
3. **Test Suite**: `tests/ux-review.spec.ts`
|
||||
- 13 comprehensive tests
|
||||
- Reusable for ongoing monitoring
|
||||
- Screenshots for visual tracking
|
||||
|
||||
4. **This Document**: `IMPROVEMENTS_IMPLEMENTED.md`
|
||||
- Track implementation progress
|
||||
- Integration guides
|
||||
- Priority roadmap
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Wins (Do These First!)
|
||||
|
||||
### 5-Minute Tasks
|
||||
1. ✅ Skip links - **DONE**
|
||||
2. Add ID attributes (`id="main-content"`, `id="navigation"`)
|
||||
3. Import skeletons where needed
|
||||
|
||||
### 30-Minute Tasks
|
||||
4. Wrap async content in Suspense with skeleton fallbacks
|
||||
5. Replace alert() with toast notifications
|
||||
6. Add ⌘K hint to search
|
||||
|
||||
### 1-Hour Tasks
|
||||
7. Hero section CTA improvements
|
||||
8. Add loading="lazy" to below-fold images
|
||||
|
||||
---
|
||||
|
||||
## 💡 Tips for Implementation
|
||||
|
||||
### Using Skeleton Loaders
|
||||
```tsx
|
||||
// Pattern 1: Simple async component
|
||||
<Suspense fallback={<SkeletonCard />}>
|
||||
<AsyncComponent />
|
||||
</Suspense>
|
||||
|
||||
// Pattern 2: Grid of items
|
||||
<Suspense fallback={<SkeletonCardGrid count={6} />}>
|
||||
<ItemGrid />
|
||||
</Suspense>
|
||||
|
||||
// Pattern 3: With error boundary
|
||||
<ErrorBoundary fallback={<ErrorState />}>
|
||||
<Suspense fallback={<SkeletonList />}>
|
||||
<DataList />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
```
|
||||
|
||||
### Performance Best Practices
|
||||
```tsx
|
||||
// 1. Dynamic imports for heavy components
|
||||
const Chart = dynamic(() => import('./chart'), {
|
||||
ssr: false,
|
||||
loading: () => <SkeletonCard />,
|
||||
})
|
||||
|
||||
// 2. Image optimization
|
||||
import Image from 'next/image'
|
||||
<Image
|
||||
src="/image.jpg"
|
||||
alt="Description"
|
||||
width={800}
|
||||
height={600}
|
||||
loading="lazy"
|
||||
placeholder="blur"
|
||||
/>
|
||||
|
||||
// 3. Code splitting
|
||||
const ModalContent = lazy(() => import('./modal-content'))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Next Steps
|
||||
|
||||
**Immediate (This Week)**:
|
||||
1. Add ID attributes to complete skip links
|
||||
2. Integrate skeletons on blocks page
|
||||
3. Test accessibility with screen reader
|
||||
|
||||
**Short-term (Next 2 Weeks)**:
|
||||
4. Performance optimization sprint
|
||||
5. Hero section redesign
|
||||
6. Toast notifications integration
|
||||
|
||||
**Long-term (Month 2)**:
|
||||
7. Builder enhancements (undo/redo, drag indicators)
|
||||
8. Keyboard shortcuts
|
||||
9. Polish and micro-interactions
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
**Questions?** See the detailed plans:
|
||||
- `docs/UX_ENHANCEMENT_PLAN.md` - Full implementation guide
|
||||
- `tests/reports/UX_REVIEW_SUMMARY.md` - Test findings
|
||||
- `tests/ux-review.spec.ts` - Test suite source
|
||||
|
||||
**Running Tests**:
|
||||
```bash
|
||||
# Full UX review
|
||||
pnpm test:e2e tests/ux-review.spec.ts
|
||||
|
||||
# Just accessibility tests
|
||||
pnpm test:e2e tests/ux-review.spec.ts -g "Accessibility"
|
||||
|
||||
# With UI (see what's happening)
|
||||
pnpm test:e2e tests/ux-review.spec.ts --headed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2025-11-13*
|
||||
*Phase: 1 of 4 Complete*
|
||||
*Next review: After Week 1 integrations*
|
||||
@@ -1,6 +0,0 @@
|
||||
ui
|
||||
Copyright (c) 2023 Hanzo AI, Inc.
|
||||
|
||||
This product includes software from shadcn/ui (https://github.com/shadcn-ui/ui), licensed under MIT:
|
||||
|
||||
Copyright (c) 2023 shadcn
|
||||
+159
-24
@@ -1,39 +1,174 @@
|
||||
# Publishing
|
||||
# NPM Publishing Guide - React 19 Packages
|
||||
|
||||
Two lanes, one trigger each. No changesets, no version-PR bot.
|
||||
## Current Package Versions
|
||||
|
||||
## 1. `pkgs/*` — auto-publish on version bump (`publish.yml`)
|
||||
All packages updated to support **React 19.2.0**:
|
||||
|
||||
Bump a package's `version` in `pkgs/<name>/package.json` and merge to `main`.
|
||||
`.github/workflows/publish.yml` detects the changed public `@hanzo/*` package,
|
||||
builds it, and publishes to npm (repo secret `NPM_TOKEN`). Patch bumps only
|
||||
(`x.y.z` → `x.y.z+1`).
|
||||
- `@hanzo/ui` - v5.1.1
|
||||
- `@hanzo/auth` - Latest
|
||||
- `@hanzo/commerce` - Latest
|
||||
- `@hanzo/brand` - Latest
|
||||
- `@hanzo/react` - v1.0.0
|
||||
|
||||
## 2. `pkg/ui` — `@hanzo/ui@8`, the v8 lane (maintainer flow)
|
||||
## Publishing Methods
|
||||
|
||||
`pkg/ui` (with `pkg/data`) is the modern cross-platform library on `@hanzo/gui`.
|
||||
It publishes from the package directory (`prepack` builds the `types/`):
|
||||
### 1. Automatic Publishing (Tag-based)
|
||||
|
||||
When you push a git tag starting with `v` (typically matching @hanzo/ui version), the workflow automatically checks all packages and publishes any with new versions:
|
||||
|
||||
```bash
|
||||
# Tag with @hanzo/ui version (workflow checks all packages)
|
||||
git tag v5.1.1
|
||||
git push origin v5.1.1
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
1. Tests run (pkg/ui and pkg/react)
|
||||
2. All 5 packages build
|
||||
3. **Automatic version detection:**
|
||||
- Checks each package's current version in package.json
|
||||
- Queries npm to see if that version already exists
|
||||
- Only publishes packages with new versions not on npm
|
||||
4. GitHub release created (only if packages were published)
|
||||
|
||||
**Example workflow output:**
|
||||
```
|
||||
📦 Checking @hanzo/ui@5.1.1
|
||||
⏭️ Already published - skipping
|
||||
|
||||
📦 Checking @hanzo/auth@2.5.5
|
||||
🚀 Publishing to npm...
|
||||
✅ Successfully published @hanzo/auth@2.5.5
|
||||
|
||||
📊 Publishing Summary
|
||||
✅ Published: 1 package(s)
|
||||
⏭️ Skipped: 4 package(s)
|
||||
```
|
||||
|
||||
This approach means you:
|
||||
- Only need to tag once (with @hanzo/ui version)
|
||||
- Don't need to track which packages need publishing
|
||||
- Can bump any package version and it auto-publishes on next tag
|
||||
- Similar to python-sdk monorepo publishing
|
||||
|
||||
**Workflow:** `.github/workflows/publish-on-tag.yml`
|
||||
|
||||
### 2. Manual Publishing (Workflow Dispatch)
|
||||
|
||||
Use GitHub Actions UI to manually publish specific packages:
|
||||
|
||||
1. Go to **Actions** → **NPM Publish**
|
||||
2. Click **Run workflow**
|
||||
3. Select package: `ui`, `auth`, `commerce`, `brand`, `react`, or `all`
|
||||
4. Select version bump: `patch`, `minor`, or `major`
|
||||
5. Click **Run workflow**
|
||||
|
||||
**What happens:**
|
||||
- Selected package(s) build
|
||||
- Version bumped automatically
|
||||
- Package(s) published to npm
|
||||
- PR created with version bump
|
||||
|
||||
**Workflow:** `.github/workflows/npm-publish.yml`
|
||||
|
||||
### 3. Local Publishing (Manual)
|
||||
|
||||
For quick patches or testing:
|
||||
|
||||
```bash
|
||||
# Build and test
|
||||
cd pkg/ui
|
||||
pnpm typecheck && pnpm test && pnpm build
|
||||
# bump "version" in package.json (patch), commit to main, then:
|
||||
pnpm build
|
||||
pnpm test
|
||||
|
||||
# Bump version
|
||||
npm version patch # or minor/major
|
||||
|
||||
# Publish
|
||||
npm publish --access public
|
||||
```
|
||||
|
||||
`@hanzo/ui-shadcn` (`pkgs/ui`) is the legacy v5 kit — existing consumers pin
|
||||
`@hanzo/ui-shadcn@^5`; it rides lane 1 like any other `pkgs/*` package.
|
||||
|
||||
> The old tag-driven flow (`publish-on-tag.yml`, `npm-publish.yml`, the
|
||||
> `pkg/commerce|brand|react` paths) is gone — do not tag to publish here.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `NPM_TOKEN` repo secret (lane 1) / npm auth as a maintainer (lane 2)
|
||||
- Every package carries `"publishConfig": { "access": "public" }`
|
||||
### NPM Authentication Token
|
||||
|
||||
## Checklist
|
||||
The GitHub secret `NPM_AUTH_TOKEN` must be set:
|
||||
|
||||
- [ ] `pnpm typecheck` + `pnpm test` green in the package
|
||||
- [ ] Patch version bump (check the last published patch first)
|
||||
- [ ] Commit to `main`; lane 1 publishes on merge, lane 2 via `npm publish`
|
||||
1. Generate token at https://www.npmjs.com/settings/tokens
|
||||
2. Add to GitHub: Settings → Secrets → Actions → `NPM_AUTH_TOKEN`
|
||||
|
||||
### Package Publish Configuration
|
||||
|
||||
All packages already configured with:
|
||||
```json
|
||||
{
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## React 19 Compatibility
|
||||
|
||||
### Key Updates Made
|
||||
|
||||
1. **Type Declarations**: Added `hanzo-ui.d.ts` files for React 19 compatibility
|
||||
2. **Peer Dependencies**: Force React 19.2.0 via pnpm overrides
|
||||
3. **Test Environment**: Switched to happy-dom for better React 19 support
|
||||
4. **Build Configuration**: All packages build successfully with React 19
|
||||
|
||||
### Test Status
|
||||
|
||||
- **pkg/ui**: 206/207 tests passing (99.5%)
|
||||
- **pkg/react**: 10/10 tests passing (100%)
|
||||
|
||||
## Publishing Checklist
|
||||
|
||||
Before publishing:
|
||||
|
||||
- [ ] All packages build successfully: `pnpm build`
|
||||
- [ ] Tests pass: `pnpm test`
|
||||
- [ ] Types check: `cd app && pnpm typecheck`
|
||||
- [ ] Lint passes: `cd app && pnpm lint`
|
||||
- [ ] Update CHANGELOG.md
|
||||
- [ ] Update version in package.json (if manual)
|
||||
- [ ] Commit changes
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build Failures
|
||||
|
||||
```bash
|
||||
# Clean install
|
||||
rm -rf node_modules pnpm-lock.yaml
|
||||
pnpm install
|
||||
|
||||
# Rebuild
|
||||
pnpm build
|
||||
```
|
||||
|
||||
### Test Failures
|
||||
|
||||
```bash
|
||||
# Run specific package tests
|
||||
cd pkg/ui && pnpm test
|
||||
cd pkg/react && pnpm test
|
||||
```
|
||||
|
||||
### Publish Failures
|
||||
|
||||
- Check NPM_AUTH_TOKEN is valid
|
||||
- Ensure version is unique (not already published)
|
||||
- Verify package builds: `cd pkg/<name> && pnpm build`
|
||||
|
||||
## Package URLs
|
||||
|
||||
- npm: https://www.npmjs.com/org/hanzo
|
||||
- GitHub: https://github.com/hanzoai/ui
|
||||
- Docs: https://ui.hanzo.ai
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2025-10-05
|
||||
**React Version:** 19.2.0
|
||||
|
||||
@@ -1,42 +1,35 @@
|
||||
<p align="center"><img src=".github/hero.svg" alt="@hanzo/ui" width="880"></p>
|
||||
|
||||
# @hanzo/ui
|
||||
|
||||
**The React component library for AI applications.** Accessible, customizable primitives for React, Vue, Svelte, and React Native — built on shadcn/ui, extended with AI, 3D, animation, and commerce components, and a single typed import surface.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/@hanzo/ui"><img src="https://img.shields.io/npm/v/@hanzo/ui?color=black&label=%40hanzo%2Fui" alt="npm"></a>
|
||||
<a href="./LICENSE.md"><img src="https://img.shields.io/badge/license-MIT-black" alt="MIT"></a>
|
||||
<a href="https://ui.hanzo.ai"><img src="https://img.shields.io/badge/docs-ui.hanzo.ai-black" alt="docs"></a>
|
||||
</p>
|
||||
Accessible and customizable components for React, Vue, Svelte, and React Native. **Built on shadcn/ui with multi-framework support, 3D components, AI components, and advanced features.**
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- **161+ components** — 3x the surface of upstream shadcn/ui
|
||||
- **Multi-framework** — React, Vue, Svelte, React Native
|
||||
- **Two themes** — Default & New York variants
|
||||
- **AI components** — chat, assistants, agent UI, playground
|
||||
- **3D components** — interactive 3D elements
|
||||
- **Animations** — advanced motion components
|
||||
- **Page builder** — visual drag-and-drop assembly, export to TSX
|
||||
- **Blocks** — 24+ production-ready full-page templates
|
||||
- **White-label** — fork and rebrand by domain (Zoo, Lux, …)
|
||||
- **Accessible** — built on Radix UI primitives
|
||||
- **Customizable** — Tailwind CSS 4 (OKLCH), fully typed TypeScript
|
||||
- 🎯 **161+ Components** - 3x more than shadcn/ui
|
||||
- 🌐 **Multi-Framework** - React, Vue, Svelte, React Native
|
||||
- 🎨 **Two Themes** - Default & New York variants
|
||||
- 🤖 **AI Components** - Chat, assistants, playground
|
||||
- 🎮 **3D Components** - Interactive 3D elements
|
||||
- ✨ **Animations** - Advanced motion components
|
||||
- 🎛️ **Page Builder** - Visual drag-drop interface
|
||||
- 🏷️ **White-Label** - Fork and rebrand easily
|
||||
- 📦 **Blocks** - 24+ production-ready templates
|
||||
- ♿ **Accessible** - Built with Radix UI primitives
|
||||
- 🎭 **Customizable** - Tailwind CSS powered
|
||||
- 📘 **TypeScript** - Fully typed
|
||||
|
||||
## Quick start
|
||||
## Quick Start
|
||||
|
||||
### Install
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
pnpm add @hanzo/ui
|
||||
# or
|
||||
npm install @hanzo/ui
|
||||
# or
|
||||
pnpm add @hanzo/ui
|
||||
```
|
||||
|
||||
### Use
|
||||
### Usage
|
||||
|
||||
```tsx
|
||||
import { Button, Card, Input } from '@hanzo/ui'
|
||||
@@ -58,99 +51,110 @@ export function App() {
|
||||
}
|
||||
```
|
||||
|
||||
## One import surface (v8)
|
||||
## Multi-Framework Support
|
||||
|
||||
`@hanzo/ui@8` is the single entry point for the whole kit. Each capability is a
|
||||
thin subpath that re-exports its home package — code lives once, and each home is
|
||||
an optional peer, pulled only when you use its subpath.
|
||||
```tsx
|
||||
// React (default)
|
||||
import { Button } from '@hanzo/ui'
|
||||
|
||||
| Import | What you get |
|
||||
|---|---|
|
||||
| `@hanzo/ui` · `/product` | charts, metrics, PageHeader, StatusTag, EmptyState, ComboBox, SlideOver, Toast |
|
||||
| `@hanzo/ui/data` | RecordsView, DataTable, typed field editors |
|
||||
| `@hanzo/ui/canvas` | ProjectCanvas, ServiceNode, DeployTimeline, EnvSwitcher |
|
||||
| `@hanzo/ui/dashboard` | landing + deploy-pipeline + overview kit |
|
||||
| `@hanzo/ui/usage` | UsageMeter, UsageProviderCard, UsageDashboard |
|
||||
| `@hanzo/ui/gitops` | GitopsAppList, tree, diff, sync/rollback, HealthBadge |
|
||||
// Vue
|
||||
import { Button } from '@hanzo/ui/vue'
|
||||
|
||||
Also available as granular imports:
|
||||
// Svelte
|
||||
import { Button } from '@hanzo/ui/svelte'
|
||||
|
||||
```ts
|
||||
import { Button, Card } from '@hanzo/ui/components'
|
||||
import * as Dialog from '@hanzo/ui/primitives/dialog'
|
||||
import { cn } from '@hanzo/ui/lib/utils'
|
||||
// React Native
|
||||
import { Button } from '@hanzo/ui/react-native'
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
📚 Visit **[ui.hanzo.ai](https://ui.hanzo.ai)** for:
|
||||
|
||||
- [Component Documentation](https://ui.hanzo.ai/docs/components)
|
||||
- [Framework Guides](https://ui.hanzo.ai/docs/frameworks)
|
||||
- [Installation Guide](https://ui.hanzo.ai/docs/installation)
|
||||
- [Examples](https://ui.hanzo.ai/examples)
|
||||
- [Page Builder](https://ui.hanzo.ai/builder)
|
||||
- [Theme Generator](https://ui.hanzo.ai/theme-generator)
|
||||
|
||||
## CLI
|
||||
|
||||
Add components straight into your project — the CLI copies source you own:
|
||||
Install components individually:
|
||||
|
||||
```bash
|
||||
npx @hanzo/ui add button
|
||||
npx @hanzo/ui add card dialog
|
||||
```
|
||||
|
||||
Install from 35+ external registries too:
|
||||
## What's Different from shadcn/ui?
|
||||
|
||||
```bash
|
||||
npx @hanzo/ui add @aceternity/spotlight
|
||||
```
|
||||
| Feature | shadcn/ui | @hanzo/ui |
|
||||
|---------|-----------|-----------|
|
||||
| Components | 58 | **161** |
|
||||
| Frameworks | React only | React, Vue, Svelte, React Native |
|
||||
| 3D Components | ❌ | ✅ (9 components) |
|
||||
| AI Components | ❌ | ✅ (12 components) |
|
||||
| Page Builder | ❌ | ✅ |
|
||||
| White-Label | ❌ | ✅ |
|
||||
| Blocks | Limited | **24+ templates** |
|
||||
|
||||
## Packages
|
||||
|
||||
The workspace publishes a family of scoped packages under `@hanzo/*`:
|
||||
- `@hanzo/ui` - Main UI library (161 components)
|
||||
- `@hanzo/auth` - Authentication components
|
||||
- `@hanzo/commerce` - E-commerce components
|
||||
- `@hanzo/brand` - Branding system
|
||||
|
||||
| Package | Purpose |
|
||||
|---|---|
|
||||
| `@hanzo/ui` | Core library + the v8 import surface (161+ components) |
|
||||
| `@hanzo/react` | React primitives |
|
||||
| `@hanzo/data` | Records, data tables, typed field editors |
|
||||
| `@hanzo/canvas` | Service/deploy canvas components |
|
||||
| `@hanzo/dashboard` | Dashboard + deploy-pipeline kit |
|
||||
| `@hanzo/commerce` · `@hanzo/checkout` · `@hanzo/shop` | Commerce components |
|
||||
| `@hanzo/agent-ui` | AI agent UI components |
|
||||
| `@hanzo/brand` · `@hanzo/tokens` | Branding system & design tokens |
|
||||
| `@hanzo/event` | Telemetry client (`POST /v1/event`) |
|
||||
## Examples
|
||||
|
||||
```tsx
|
||||
// 3D Components
|
||||
import { ThreeDButton, ThreeDCard } from '@hanzo/ui'
|
||||
|
||||
// AI Components
|
||||
import { AIChat, AIAssistant } from '@hanzo/ui'
|
||||
|
||||
// Animations
|
||||
import { AnimatedBeam, AnimatedText } from '@hanzo/ui'
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/hanzoai/ui.git
|
||||
cd ui
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
pnpm build:registry # generate the component registry FIRST
|
||||
pnpm dev # docs site + registry (http://localhost:3003)
|
||||
# Start dev server
|
||||
pnpm dev
|
||||
|
||||
# Build packages
|
||||
pnpm build
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
```
|
||||
|
||||
> The registry generates the JSON the CLI reads, so `build:registry` must run
|
||||
> before `build`. Keep the Default and New York themes in sync when adding
|
||||
> components. Use pnpm — not npm or yarn.
|
||||
|
||||
```bash
|
||||
pnpm build # build the docs app
|
||||
pnpm lint # lint all workspaces
|
||||
pnpm typecheck # type check
|
||||
pnpm test # unit tests
|
||||
pnpm test:e2e # Playwright E2E
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
Full docs, live previews, and the component catalog: **[ui.hanzo.ai](https://ui.hanzo.ai)**.
|
||||
|
||||
## Contributing
|
||||
|
||||
See the [contributing guide](/CONTRIBUTING.md).
|
||||
We welcome contributions! Please see our [Contributing Guide](./CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE.md](./LICENSE.md).
|
||||
MIT - See [LICENSE.md](./LICENSE.md) for details.
|
||||
|
||||
## Links
|
||||
|
||||
- [Documentation](https://ui.hanzo.ai)
|
||||
- [GitHub](https://github.com/hanzoai/ui)
|
||||
- [npm](https://www.npmjs.com/package/@hanzo/ui)
|
||||
- [Discord](https://discord.gg/hanzo)
|
||||
- [Twitter](https://twitter.com/hanzoai)
|
||||
|
||||
---
|
||||
|
||||
## Hanzo — the Open AI Cloud
|
||||
|
||||
Open source · every language · on-chain settlement. [hanzo.ai](https://hanzo.ai) · [docs.hanzo.ai](https://docs.hanzo.ai)
|
||||
|
||||
**SDKs in every language** — [Python](https://github.com/hanzoai/python-sdk) (flagship) · [TypeScript](https://github.com/hanzo-js/sdk) · [Go](https://github.com/hanzo-go/sdk) · [Rust](https://github.com/hanzo-rs/sdk) · [C++](https://github.com/hanzo-cpp/sdk) · [Swift](https://github.com/hanzo-swift/sdk) · [Kotlin](https://github.com/hanzo-kt/sdk) · [umbrella](https://github.com/hanzoai/sdk)
|
||||
Built with ❤️ by [Hanzo](https://hanzo.ai)
|
||||
|
||||
+1
-1
@@ -6,4 +6,4 @@ We will investigate all legitimate reports and do our best to quickly fix the pr
|
||||
|
||||
Our preference is that you make use of GitHub's private vulnerability reporting feature to disclose potential security vulnerabilities in our Open Source Software.
|
||||
|
||||
To do this, please visit the security tab of the repository and click the [Report a vulnerability](https://github.com/shadcn-ui/ui/security/advisories/new) button.
|
||||
To do this, please visit the security tab of the repository and click the "Report a vulnerability" button.
|
||||
|
||||
@@ -1,434 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import { browser } from "@hanzo/docs-mdx/runtime/browser"
|
||||
|
||||
import type * as Config from "../source.config"
|
||||
|
||||
const create = browser<
|
||||
typeof Config,
|
||||
import("@hanzo/docs-mdx/runtime/types").InternalTypeConfig & {
|
||||
DocData: {}
|
||||
}
|
||||
>()
|
||||
const browserCollections = {
|
||||
docs: create.doc("docs", {
|
||||
"about.mdx": () => import("../content/docs/about.mdx?collection=docs"),
|
||||
"changelog.mdx": () =>
|
||||
import("../content/docs/changelog.mdx?collection=docs"),
|
||||
"cli.mdx": () => import("../content/docs/cli.mdx?collection=docs"),
|
||||
"components-json.mdx": () =>
|
||||
import("../content/docs/components-json.mdx?collection=docs"),
|
||||
"components.mdx": () =>
|
||||
import("../content/docs/components.mdx?collection=docs"),
|
||||
"figma.mdx": () => import("../content/docs/figma.mdx?collection=docs"),
|
||||
"index.mdx": () => import("../content/docs/index.mdx?collection=docs"),
|
||||
"mcp.mdx": () => import("../content/docs/mcp.mdx?collection=docs"),
|
||||
"theming.mdx": () => import("../content/docs/theming.mdx?collection=docs"),
|
||||
"ai/actions.mdx": () =>
|
||||
import("../content/docs/ai/actions.mdx?collection=docs"),
|
||||
"ai/agents.mdx": () =>
|
||||
import("../content/docs/ai/agents.mdx?collection=docs"),
|
||||
"ai/assistant.mdx": () =>
|
||||
import("../content/docs/ai/assistant.mdx?collection=docs"),
|
||||
"ai/chat.mdx": () => import("../content/docs/ai/chat.mdx?collection=docs"),
|
||||
"ai/code.mdx": () => import("../content/docs/ai/code.mdx?collection=docs"),
|
||||
"ai/index.mdx": () =>
|
||||
import("../content/docs/ai/index.mdx?collection=docs"),
|
||||
"ai/models.mdx": () =>
|
||||
import("../content/docs/ai/models.mdx?collection=docs"),
|
||||
"ai/playground.mdx": () =>
|
||||
import("../content/docs/ai/playground.mdx?collection=docs"),
|
||||
"ai/tools.mdx": () =>
|
||||
import("../content/docs/ai/tools.mdx?collection=docs"),
|
||||
"ai/vision.mdx": () =>
|
||||
import("../content/docs/ai/vision.mdx?collection=docs"),
|
||||
"ai/voice-settings.mdx": () =>
|
||||
import("../content/docs/ai/voice-settings.mdx?collection=docs"),
|
||||
"ai/voice.mdx": () =>
|
||||
import("../content/docs/ai/voice.mdx?collection=docs"),
|
||||
"block-examples/billing.mdx": () =>
|
||||
import("../content/docs/block-examples/billing.mdx?collection=docs"),
|
||||
"block-examples/compose-spec.mdx": () =>
|
||||
import("../content/docs/block-examples/compose-spec.mdx?collection=docs"),
|
||||
"block-examples/newsletter.mdx": () =>
|
||||
import("../content/docs/block-examples/newsletter.mdx?collection=docs"),
|
||||
"block-examples/pricing.mdx": () =>
|
||||
import("../content/docs/block-examples/pricing.mdx?collection=docs"),
|
||||
"frameworks/index.mdx": () =>
|
||||
import("../content/docs/frameworks/index.mdx?collection=docs"),
|
||||
"frameworks/react.mdx": () =>
|
||||
import("../content/docs/frameworks/react.mdx?collection=docs"),
|
||||
"frameworks/vue.mdx": () =>
|
||||
import("../content/docs/frameworks/vue.mdx?collection=docs"),
|
||||
"dark-mode/astro.mdx": () =>
|
||||
import("../content/docs/dark-mode/astro.mdx?collection=docs"),
|
||||
"dark-mode/index.mdx": () =>
|
||||
import("../content/docs/dark-mode/index.mdx?collection=docs"),
|
||||
"dark-mode/next.mdx": () =>
|
||||
import("../content/docs/dark-mode/next.mdx?collection=docs"),
|
||||
"dark-mode/remix.mdx": () =>
|
||||
import("../content/docs/dark-mode/remix.mdx?collection=docs"),
|
||||
"dark-mode/vite.mdx": () =>
|
||||
import("../content/docs/dark-mode/vite.mdx?collection=docs"),
|
||||
"guides/page-builder.mdx": () =>
|
||||
import("../content/docs/guides/page-builder.mdx?collection=docs"),
|
||||
"guides/visual-workflows.mdx": () =>
|
||||
import("../content/docs/guides/visual-workflows.mdx?collection=docs"),
|
||||
"hanzo-react/getting-started.mdx": () =>
|
||||
import("../content/docs/hanzo-react/getting-started.mdx?collection=docs"),
|
||||
"hanzo-react/hooks.mdx": () =>
|
||||
import("../content/docs/hanzo-react/hooks.mdx?collection=docs"),
|
||||
"installation/astro.mdx": () =>
|
||||
import("../content/docs/installation/astro.mdx?collection=docs"),
|
||||
"installation/gatsby.mdx": () =>
|
||||
import("../content/docs/installation/gatsby.mdx?collection=docs"),
|
||||
"installation/index.mdx": () =>
|
||||
import("../content/docs/installation/index.mdx?collection=docs"),
|
||||
"installation/laravel.mdx": () =>
|
||||
import("../content/docs/installation/laravel.mdx?collection=docs"),
|
||||
"installation/manual.mdx": () =>
|
||||
import("../content/docs/installation/manual.mdx?collection=docs"),
|
||||
"installation/next.mdx": () =>
|
||||
import("../content/docs/installation/next.mdx?collection=docs"),
|
||||
"installation/remix.mdx": () =>
|
||||
import("../content/docs/installation/remix.mdx?collection=docs"),
|
||||
"installation/vite.mdx": () =>
|
||||
import("../content/docs/installation/vite.mdx?collection=docs"),
|
||||
"packages/index.mdx": () =>
|
||||
import("../content/docs/packages/index.mdx?collection=docs"),
|
||||
"components/3d-card.mdx": () =>
|
||||
import("../content/docs/components/3d-card.mdx?collection=docs"),
|
||||
"components/3d-marquee.mdx": () =>
|
||||
import("../content/docs/components/3d-marquee.mdx?collection=docs"),
|
||||
"components/3d-pin.mdx": () =>
|
||||
import("../content/docs/components/3d-pin.mdx?collection=docs"),
|
||||
"components/accordion.mdx": () =>
|
||||
import("../content/docs/components/accordion.mdx?collection=docs"),
|
||||
"components/advanced-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/advanced-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/ai-model-selector-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/ai-model-selector-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/alert-dialog.mdx": () =>
|
||||
import("../content/docs/components/alert-dialog.mdx?collection=docs"),
|
||||
"components/alert.mdx": () =>
|
||||
import("../content/docs/components/alert.mdx?collection=docs"),
|
||||
"components/android.mdx": () =>
|
||||
import("../content/docs/components/android.mdx?collection=docs"),
|
||||
"components/animated-beam.mdx": () =>
|
||||
import("../content/docs/components/animated-beam.mdx?collection=docs"),
|
||||
"components/animated-cursor.mdx": () =>
|
||||
import("../content/docs/components/animated-cursor.mdx?collection=docs"),
|
||||
"components/animated-testimonials.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/animated-testimonials.mdx?collection=docs"
|
||||
),
|
||||
"components/animated-tooltip.mdx": () =>
|
||||
import("../content/docs/components/animated-tooltip.mdx?collection=docs"),
|
||||
"components/animation-components.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/animation-components.mdx?collection=docs"
|
||||
),
|
||||
"components/announcement.mdx": () =>
|
||||
import("../content/docs/components/announcement.mdx?collection=docs"),
|
||||
"components/app-switcher-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/app-switcher-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/apple-cards-carousel.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/apple-cards-carousel.mdx?collection=docs"
|
||||
),
|
||||
"components/apple-hello-effect.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/apple-hello-effect.mdx?collection=docs"
|
||||
),
|
||||
"components/aspect-ratio.mdx": () =>
|
||||
import("../content/docs/components/aspect-ratio.mdx?collection=docs"),
|
||||
"components/avatar-group.mdx": () =>
|
||||
import("../content/docs/components/avatar-group.mdx?collection=docs"),
|
||||
"components/avatar.mdx": () =>
|
||||
import("../content/docs/components/avatar.mdx?collection=docs"),
|
||||
"components/badge.mdx": () =>
|
||||
import("../content/docs/components/badge.mdx?collection=docs"),
|
||||
"components/banner.mdx": () =>
|
||||
import("../content/docs/components/banner.mdx?collection=docs"),
|
||||
"components/breadcrumb-and-filters-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/breadcrumb-and-filters-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/breadcrumb-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/breadcrumb-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/breadcrumb.mdx": () =>
|
||||
import("../content/docs/components/breadcrumb.mdx?collection=docs"),
|
||||
"components/button-group.mdx": () =>
|
||||
import("../content/docs/components/button-group.mdx?collection=docs"),
|
||||
"components/button.mdx": () =>
|
||||
import("../content/docs/components/button.mdx?collection=docs"),
|
||||
"components/calendar.mdx": () =>
|
||||
import("../content/docs/components/calendar.mdx?collection=docs"),
|
||||
"components/card.mdx": () =>
|
||||
import("../content/docs/components/card.mdx?collection=docs"),
|
||||
"components/carousel.mdx": () =>
|
||||
import("../content/docs/components/carousel.mdx?collection=docs"),
|
||||
"components/centered-logo-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/centered-logo-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/checkbox.mdx": () =>
|
||||
import("../content/docs/components/checkbox.mdx?collection=docs"),
|
||||
"components/choicebox.mdx": () =>
|
||||
import("../content/docs/components/choicebox.mdx?collection=docs"),
|
||||
"components/code-block.mdx": () =>
|
||||
import("../content/docs/components/code-block.mdx?collection=docs"),
|
||||
"components/code-editor.mdx": () =>
|
||||
import("../content/docs/components/code-editor.mdx?collection=docs"),
|
||||
"components/code-tabs.mdx": () =>
|
||||
import("../content/docs/components/code-tabs.mdx?collection=docs"),
|
||||
"components/collaboration-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/collaboration-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/collapsible.mdx": () =>
|
||||
import("../content/docs/components/collapsible.mdx?collection=docs"),
|
||||
"components/color-picker.mdx": () =>
|
||||
import("../content/docs/components/color-picker.mdx?collection=docs"),
|
||||
"components/combobox.mdx": () =>
|
||||
import("../content/docs/components/combobox.mdx?collection=docs"),
|
||||
"components/command.mdx": () =>
|
||||
import("../content/docs/components/command.mdx?collection=docs"),
|
||||
"components/communication-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/communication-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/comparison.mdx": () =>
|
||||
import("../content/docs/components/comparison.mdx?collection=docs"),
|
||||
"components/context-menu.mdx": () =>
|
||||
import("../content/docs/components/context-menu.mdx?collection=docs"),
|
||||
"components/context-switcher-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/context-switcher-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/credit-card.mdx": () =>
|
||||
import("../content/docs/components/credit-card.mdx?collection=docs"),
|
||||
"components/cursor.mdx": () =>
|
||||
import("../content/docs/components/cursor.mdx?collection=docs"),
|
||||
"components/dashboard-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/dashboard-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/data-table.mdx": () =>
|
||||
import("../content/docs/components/data-table.mdx?collection=docs"),
|
||||
"components/date-picker.mdx": () =>
|
||||
import("../content/docs/components/date-picker.mdx?collection=docs"),
|
||||
"components/desktop-hooks.mdx": () =>
|
||||
import("../content/docs/components/desktop-hooks.mdx?collection=docs"),
|
||||
"components/desktop-spotlight.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/desktop-spotlight.mdx?collection=docs"
|
||||
),
|
||||
"components/desktop-window.mdx": () =>
|
||||
import("../content/docs/components/desktop-window.mdx?collection=docs"),
|
||||
"components/desktop.mdx": () =>
|
||||
import("../content/docs/components/desktop.mdx?collection=docs"),
|
||||
"components/dialog-stack.mdx": () =>
|
||||
import("../content/docs/components/dialog-stack.mdx?collection=docs"),
|
||||
"components/dialog.mdx": () =>
|
||||
import("../content/docs/components/dialog.mdx?collection=docs"),
|
||||
"components/dock.mdx": () =>
|
||||
import("../content/docs/components/dock.mdx?collection=docs"),
|
||||
"components/drawer.mdx": () =>
|
||||
import("../content/docs/components/drawer.mdx?collection=docs"),
|
||||
"components/dropdown-menu.mdx": () =>
|
||||
import("../content/docs/components/dropdown-menu.mdx?collection=docs"),
|
||||
"components/dropzone.mdx": () =>
|
||||
import("../content/docs/components/dropzone.mdx?collection=docs"),
|
||||
"components/e-commerce-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/e-commerce-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/editor.mdx": () =>
|
||||
import("../content/docs/components/editor.mdx?collection=docs"),
|
||||
"components/empty.mdx": () =>
|
||||
import("../content/docs/components/empty.mdx?collection=docs"),
|
||||
"components/field.mdx": () =>
|
||||
import("../content/docs/components/field.mdx?collection=docs"),
|
||||
"components/form.mdx": () =>
|
||||
import("../content/docs/components/form.mdx?collection=docs"),
|
||||
"components/gantt.mdx": () =>
|
||||
import("../content/docs/components/gantt.mdx?collection=docs"),
|
||||
"components/glimpse.mdx": () =>
|
||||
import("../content/docs/components/glimpse.mdx?collection=docs"),
|
||||
"components/grid-pattern.mdx": () =>
|
||||
import("../content/docs/components/grid-pattern.mdx?collection=docs"),
|
||||
"components/hover-card.mdx": () =>
|
||||
import("../content/docs/components/hover-card.mdx?collection=docs"),
|
||||
"components/icon-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/icon-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/image-crop.mdx": () =>
|
||||
import("../content/docs/components/image-crop.mdx?collection=docs"),
|
||||
"components/image-zoom.mdx": () =>
|
||||
import("../content/docs/components/image-zoom.mdx?collection=docs"),
|
||||
"components/index.mdx": () =>
|
||||
import("../content/docs/components/index.mdx?collection=docs"),
|
||||
"components/input-group.mdx": () =>
|
||||
import("../content/docs/components/input-group.mdx?collection=docs"),
|
||||
"components/input-otp.mdx": () =>
|
||||
import("../content/docs/components/input-otp.mdx?collection=docs"),
|
||||
"components/input.mdx": () =>
|
||||
import("../content/docs/components/input.mdx?collection=docs"),
|
||||
"components/interactive-grid-pattern.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/interactive-grid-pattern.mdx?collection=docs"
|
||||
),
|
||||
"components/iphone-15-pro.mdx": () =>
|
||||
import("../content/docs/components/iphone-15-pro.mdx?collection=docs"),
|
||||
"components/item.mdx": () =>
|
||||
import("../content/docs/components/item.mdx?collection=docs"),
|
||||
"components/kanban.mdx": () =>
|
||||
import("../content/docs/components/kanban.mdx?collection=docs"),
|
||||
"components/kbd.mdx": () =>
|
||||
import("../content/docs/components/kbd.mdx?collection=docs"),
|
||||
"components/label.mdx": () =>
|
||||
import("../content/docs/components/label.mdx?collection=docs"),
|
||||
"components/limelight-nav.mdx": () =>
|
||||
import("../content/docs/components/limelight-nav.mdx?collection=docs"),
|
||||
"components/list.mdx": () =>
|
||||
import("../content/docs/components/list.mdx?collection=docs"),
|
||||
"components/macos-dock.mdx": () =>
|
||||
import("../content/docs/components/macos-dock.mdx?collection=docs"),
|
||||
"components/magnetic.mdx": () =>
|
||||
import("../content/docs/components/magnetic.mdx?collection=docs"),
|
||||
"components/marquee.mdx": () =>
|
||||
import("../content/docs/components/marquee.mdx?collection=docs"),
|
||||
"components/menu-dock.mdx": () =>
|
||||
import("../content/docs/components/menu-dock.mdx?collection=docs"),
|
||||
"components/menubar.mdx": () =>
|
||||
import("../content/docs/components/menubar.mdx?collection=docs"),
|
||||
"components/message-dock.mdx": () =>
|
||||
import("../content/docs/components/message-dock.mdx?collection=docs"),
|
||||
"components/mini-calendar.mdx": () =>
|
||||
import("../content/docs/components/mini-calendar.mdx?collection=docs"),
|
||||
"components/minimal-tiptap.mdx": () =>
|
||||
import("../content/docs/components/minimal-tiptap.mdx?collection=docs"),
|
||||
"components/motion-highlight.mdx": () =>
|
||||
import("../content/docs/components/motion-highlight.mdx?collection=docs"),
|
||||
"components/navigation-menu.mdx": () =>
|
||||
import("../content/docs/components/navigation-menu.mdx?collection=docs"),
|
||||
"components/pagination.mdx": () =>
|
||||
import("../content/docs/components/pagination.mdx?collection=docs"),
|
||||
"components/particles-background.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/particles-background.mdx?collection=docs"
|
||||
),
|
||||
"components/particles.mdx": () =>
|
||||
import("../content/docs/components/particles.mdx?collection=docs"),
|
||||
"components/pill.mdx": () =>
|
||||
import("../content/docs/components/pill.mdx?collection=docs"),
|
||||
"components/pin-list.mdx": () =>
|
||||
import("../content/docs/components/pin-list.mdx?collection=docs"),
|
||||
"components/pixel-image.mdx": () =>
|
||||
import("../content/docs/components/pixel-image.mdx?collection=docs"),
|
||||
"components/popover.mdx": () =>
|
||||
import("../content/docs/components/popover.mdx?collection=docs"),
|
||||
"components/progress.mdx": () =>
|
||||
import("../content/docs/components/progress.mdx?collection=docs"),
|
||||
"components/qr-code.mdx": () =>
|
||||
import("../content/docs/components/qr-code.mdx?collection=docs"),
|
||||
"components/radio-group.mdx": () =>
|
||||
import("../content/docs/components/radio-group.mdx?collection=docs"),
|
||||
"components/relative-time.mdx": () =>
|
||||
import("../content/docs/components/relative-time.mdx?collection=docs"),
|
||||
"components/resizable.mdx": () =>
|
||||
import("../content/docs/components/resizable.mdx?collection=docs"),
|
||||
"components/safari.mdx": () =>
|
||||
import("../content/docs/components/safari.mdx?collection=docs"),
|
||||
"components/sandbox.mdx": () =>
|
||||
import("../content/docs/components/sandbox.mdx?collection=docs"),
|
||||
"components/scroll-area.mdx": () =>
|
||||
import("../content/docs/components/scroll-area.mdx?collection=docs"),
|
||||
"components/search-and-toggle-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/search-and-toggle-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/select.mdx": () =>
|
||||
import("../content/docs/components/select.mdx?collection=docs"),
|
||||
"components/separator.mdx": () =>
|
||||
import("../content/docs/components/separator.mdx?collection=docs"),
|
||||
"components/sheet.mdx": () =>
|
||||
import("../content/docs/components/sheet.mdx?collection=docs"),
|
||||
"components/simple-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/simple-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/skeleton.mdx": () =>
|
||||
import("../content/docs/components/skeleton.mdx?collection=docs"),
|
||||
"components/slider.mdx": () =>
|
||||
import("../content/docs/components/slider.mdx?collection=docs"),
|
||||
"components/snippet.mdx": () =>
|
||||
import("../content/docs/components/snippet.mdx?collection=docs"),
|
||||
"components/sonner.mdx": () =>
|
||||
import("../content/docs/components/sonner.mdx?collection=docs"),
|
||||
"components/spinner.mdx": () =>
|
||||
import("../content/docs/components/spinner.mdx?collection=docs"),
|
||||
"components/stars-scrolling-wheel.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/stars-scrolling-wheel.mdx?collection=docs"
|
||||
),
|
||||
"components/status-dashboard-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/status-dashboard-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/status.mdx": () =>
|
||||
import("../content/docs/components/status.mdx?collection=docs"),
|
||||
"components/switch.mdx": () =>
|
||||
import("../content/docs/components/switch.mdx?collection=docs"),
|
||||
"components/table.mdx": () =>
|
||||
import("../content/docs/components/table.mdx?collection=docs"),
|
||||
"components/tabs.mdx": () =>
|
||||
import("../content/docs/components/tabs.mdx?collection=docs"),
|
||||
"components/tags.mdx": () =>
|
||||
import("../content/docs/components/tags.mdx?collection=docs"),
|
||||
"components/team-switcher-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/team-switcher-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/terminal.mdx": () =>
|
||||
import("../content/docs/components/terminal.mdx?collection=docs"),
|
||||
"components/textarea.mdx": () =>
|
||||
import("../content/docs/components/textarea.mdx?collection=docs"),
|
||||
"components/timeline.mdx": () =>
|
||||
import("../content/docs/components/timeline.mdx?collection=docs"),
|
||||
"components/toast.mdx": () =>
|
||||
import("../content/docs/components/toast.mdx?collection=docs"),
|
||||
"components/toggle-group.mdx": () =>
|
||||
import("../content/docs/components/toggle-group.mdx?collection=docs"),
|
||||
"components/toggle.mdx": () =>
|
||||
import("../content/docs/components/toggle.mdx?collection=docs"),
|
||||
"components/tooltip.mdx": () =>
|
||||
import("../content/docs/components/tooltip.mdx?collection=docs"),
|
||||
"components/two-tier-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/two-tier-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/typography.mdx": () =>
|
||||
import("../content/docs/components/typography.mdx?collection=docs"),
|
||||
"components/underline-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/underline-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/video-player.mdx": () =>
|
||||
import("../content/docs/components/video-player.mdx?collection=docs"),
|
||||
"white-label/index.mdx": () =>
|
||||
import("../content/docs/white-label/index.mdx?collection=docs"),
|
||||
"testing/index.mdx": () =>
|
||||
import("../content/docs/testing/index.mdx?collection=docs"),
|
||||
}),
|
||||
}
|
||||
export default browserCollections
|
||||
@@ -1,15 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import { dynamic } from "@hanzo/docs-mdx/runtime/dynamic"
|
||||
|
||||
import * as Config from "../source.config"
|
||||
|
||||
const create = await dynamic<
|
||||
typeof Config,
|
||||
import("@hanzo/docs-mdx/runtime/types").InternalTypeConfig & {
|
||||
DocData: {}
|
||||
}
|
||||
>(
|
||||
Config,
|
||||
{ configPath: "source.config.ts", environment: "next", outDir: ".docs" },
|
||||
{ doc: { passthroughs: ["extractedReferences"] } }
|
||||
)
|
||||
@@ -1,393 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import { server } from "@hanzo/docs-mdx/runtime/server"
|
||||
|
||||
import * as __fd_glob_0 from "../content/docs/about.mdx?collection=docs"
|
||||
import * as __fd_glob_9 from "../content/docs/ai/actions.mdx?collection=docs"
|
||||
import * as __fd_glob_10 from "../content/docs/ai/agents.mdx?collection=docs"
|
||||
import * as __fd_glob_11 from "../content/docs/ai/assistant.mdx?collection=docs"
|
||||
import * as __fd_glob_12 from "../content/docs/ai/chat.mdx?collection=docs"
|
||||
import * as __fd_glob_13 from "../content/docs/ai/code.mdx?collection=docs"
|
||||
import * as __fd_glob_14 from "../content/docs/ai/index.mdx?collection=docs"
|
||||
import * as __fd_glob_15 from "../content/docs/ai/models.mdx?collection=docs"
|
||||
import * as __fd_glob_16 from "../content/docs/ai/playground.mdx?collection=docs"
|
||||
import * as __fd_glob_17 from "../content/docs/ai/tools.mdx?collection=docs"
|
||||
import * as __fd_glob_18 from "../content/docs/ai/vision.mdx?collection=docs"
|
||||
import * as __fd_glob_19 from "../content/docs/ai/voice-settings.mdx?collection=docs"
|
||||
import * as __fd_glob_20 from "../content/docs/ai/voice.mdx?collection=docs"
|
||||
import * as __fd_glob_21 from "../content/docs/block-examples/billing.mdx?collection=docs"
|
||||
import * as __fd_glob_22 from "../content/docs/block-examples/compose-spec.mdx?collection=docs"
|
||||
import * as __fd_glob_23 from "../content/docs/block-examples/newsletter.mdx?collection=docs"
|
||||
import * as __fd_glob_24 from "../content/docs/block-examples/pricing.mdx?collection=docs"
|
||||
import * as __fd_glob_1 from "../content/docs/changelog.mdx?collection=docs"
|
||||
import * as __fd_glob_2 from "../content/docs/cli.mdx?collection=docs"
|
||||
import * as __fd_glob_3 from "../content/docs/components-json.mdx?collection=docs"
|
||||
import * as __fd_glob_4 from "../content/docs/components.mdx?collection=docs"
|
||||
import * as __fd_glob_46 from "../content/docs/components/3d-card.mdx?collection=docs"
|
||||
import * as __fd_glob_47 from "../content/docs/components/3d-marquee.mdx?collection=docs"
|
||||
import * as __fd_glob_48 from "../content/docs/components/3d-pin.mdx?collection=docs"
|
||||
import * as __fd_glob_49 from "../content/docs/components/accordion.mdx?collection=docs"
|
||||
import * as __fd_glob_50 from "../content/docs/components/advanced-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_51 from "../content/docs/components/ai-model-selector-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_52 from "../content/docs/components/alert-dialog.mdx?collection=docs"
|
||||
import * as __fd_glob_53 from "../content/docs/components/alert.mdx?collection=docs"
|
||||
import * as __fd_glob_54 from "../content/docs/components/android.mdx?collection=docs"
|
||||
import * as __fd_glob_55 from "../content/docs/components/animated-beam.mdx?collection=docs"
|
||||
import * as __fd_glob_56 from "../content/docs/components/animated-cursor.mdx?collection=docs"
|
||||
import * as __fd_glob_57 from "../content/docs/components/animated-testimonials.mdx?collection=docs"
|
||||
import * as __fd_glob_58 from "../content/docs/components/animated-tooltip.mdx?collection=docs"
|
||||
import * as __fd_glob_59 from "../content/docs/components/animation-components.mdx?collection=docs"
|
||||
import * as __fd_glob_60 from "../content/docs/components/announcement.mdx?collection=docs"
|
||||
import * as __fd_glob_61 from "../content/docs/components/app-switcher-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_62 from "../content/docs/components/apple-cards-carousel.mdx?collection=docs"
|
||||
import * as __fd_glob_63 from "../content/docs/components/apple-hello-effect.mdx?collection=docs"
|
||||
import * as __fd_glob_64 from "../content/docs/components/aspect-ratio.mdx?collection=docs"
|
||||
import * as __fd_glob_65 from "../content/docs/components/avatar-group.mdx?collection=docs"
|
||||
import * as __fd_glob_66 from "../content/docs/components/avatar.mdx?collection=docs"
|
||||
import * as __fd_glob_67 from "../content/docs/components/badge.mdx?collection=docs"
|
||||
import * as __fd_glob_68 from "../content/docs/components/banner.mdx?collection=docs"
|
||||
import * as __fd_glob_69 from "../content/docs/components/breadcrumb-and-filters-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_70 from "../content/docs/components/breadcrumb-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_71 from "../content/docs/components/breadcrumb.mdx?collection=docs"
|
||||
import * as __fd_glob_72 from "../content/docs/components/button-group.mdx?collection=docs"
|
||||
import * as __fd_glob_73 from "../content/docs/components/button.mdx?collection=docs"
|
||||
import * as __fd_glob_74 from "../content/docs/components/calendar.mdx?collection=docs"
|
||||
import * as __fd_glob_75 from "../content/docs/components/card.mdx?collection=docs"
|
||||
import * as __fd_glob_76 from "../content/docs/components/carousel.mdx?collection=docs"
|
||||
import * as __fd_glob_77 from "../content/docs/components/centered-logo-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_78 from "../content/docs/components/checkbox.mdx?collection=docs"
|
||||
import * as __fd_glob_79 from "../content/docs/components/choicebox.mdx?collection=docs"
|
||||
import * as __fd_glob_80 from "../content/docs/components/code-block.mdx?collection=docs"
|
||||
import * as __fd_glob_81 from "../content/docs/components/code-editor.mdx?collection=docs"
|
||||
import * as __fd_glob_82 from "../content/docs/components/code-tabs.mdx?collection=docs"
|
||||
import * as __fd_glob_83 from "../content/docs/components/collaboration-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_84 from "../content/docs/components/collapsible.mdx?collection=docs"
|
||||
import * as __fd_glob_85 from "../content/docs/components/color-picker.mdx?collection=docs"
|
||||
import * as __fd_glob_86 from "../content/docs/components/combobox.mdx?collection=docs"
|
||||
import * as __fd_glob_87 from "../content/docs/components/command.mdx?collection=docs"
|
||||
import * as __fd_glob_88 from "../content/docs/components/communication-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_89 from "../content/docs/components/comparison.mdx?collection=docs"
|
||||
import * as __fd_glob_90 from "../content/docs/components/context-menu.mdx?collection=docs"
|
||||
import * as __fd_glob_91 from "../content/docs/components/context-switcher-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_92 from "../content/docs/components/credit-card.mdx?collection=docs"
|
||||
import * as __fd_glob_93 from "../content/docs/components/cursor.mdx?collection=docs"
|
||||
import * as __fd_glob_94 from "../content/docs/components/dashboard-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_95 from "../content/docs/components/data-table.mdx?collection=docs"
|
||||
import * as __fd_glob_96 from "../content/docs/components/date-picker.mdx?collection=docs"
|
||||
import * as __fd_glob_97 from "../content/docs/components/desktop-hooks.mdx?collection=docs"
|
||||
import * as __fd_glob_98 from "../content/docs/components/desktop-spotlight.mdx?collection=docs"
|
||||
import * as __fd_glob_99 from "../content/docs/components/desktop-window.mdx?collection=docs"
|
||||
import * as __fd_glob_100 from "../content/docs/components/desktop.mdx?collection=docs"
|
||||
import * as __fd_glob_101 from "../content/docs/components/dialog-stack.mdx?collection=docs"
|
||||
import * as __fd_glob_102 from "../content/docs/components/dialog.mdx?collection=docs"
|
||||
import * as __fd_glob_103 from "../content/docs/components/dock.mdx?collection=docs"
|
||||
import * as __fd_glob_104 from "../content/docs/components/drawer.mdx?collection=docs"
|
||||
import * as __fd_glob_105 from "../content/docs/components/dropdown-menu.mdx?collection=docs"
|
||||
import * as __fd_glob_106 from "../content/docs/components/dropzone.mdx?collection=docs"
|
||||
import * as __fd_glob_107 from "../content/docs/components/e-commerce-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_108 from "../content/docs/components/editor.mdx?collection=docs"
|
||||
import * as __fd_glob_109 from "../content/docs/components/empty.mdx?collection=docs"
|
||||
import * as __fd_glob_110 from "../content/docs/components/field.mdx?collection=docs"
|
||||
import * as __fd_glob_111 from "../content/docs/components/form.mdx?collection=docs"
|
||||
import * as __fd_glob_112 from "../content/docs/components/gantt.mdx?collection=docs"
|
||||
import * as __fd_glob_113 from "../content/docs/components/glimpse.mdx?collection=docs"
|
||||
import * as __fd_glob_114 from "../content/docs/components/grid-pattern.mdx?collection=docs"
|
||||
import * as __fd_glob_115 from "../content/docs/components/hover-card.mdx?collection=docs"
|
||||
import * as __fd_glob_116 from "../content/docs/components/icon-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_117 from "../content/docs/components/image-crop.mdx?collection=docs"
|
||||
import * as __fd_glob_118 from "../content/docs/components/image-zoom.mdx?collection=docs"
|
||||
import * as __fd_glob_119 from "../content/docs/components/index.mdx?collection=docs"
|
||||
import * as __fd_glob_120 from "../content/docs/components/input-group.mdx?collection=docs"
|
||||
import * as __fd_glob_121 from "../content/docs/components/input-otp.mdx?collection=docs"
|
||||
import * as __fd_glob_122 from "../content/docs/components/input.mdx?collection=docs"
|
||||
import * as __fd_glob_123 from "../content/docs/components/interactive-grid-pattern.mdx?collection=docs"
|
||||
import * as __fd_glob_124 from "../content/docs/components/iphone-15-pro.mdx?collection=docs"
|
||||
import * as __fd_glob_125 from "../content/docs/components/item.mdx?collection=docs"
|
||||
import * as __fd_glob_126 from "../content/docs/components/kanban.mdx?collection=docs"
|
||||
import * as __fd_glob_127 from "../content/docs/components/kbd.mdx?collection=docs"
|
||||
import * as __fd_glob_128 from "../content/docs/components/label.mdx?collection=docs"
|
||||
import * as __fd_glob_129 from "../content/docs/components/limelight-nav.mdx?collection=docs"
|
||||
import * as __fd_glob_130 from "../content/docs/components/list.mdx?collection=docs"
|
||||
import * as __fd_glob_131 from "../content/docs/components/macos-dock.mdx?collection=docs"
|
||||
import * as __fd_glob_132 from "../content/docs/components/magnetic.mdx?collection=docs"
|
||||
import * as __fd_glob_133 from "../content/docs/components/marquee.mdx?collection=docs"
|
||||
import * as __fd_glob_134 from "../content/docs/components/menu-dock.mdx?collection=docs"
|
||||
import * as __fd_glob_135 from "../content/docs/components/menubar.mdx?collection=docs"
|
||||
import * as __fd_glob_136 from "../content/docs/components/message-dock.mdx?collection=docs"
|
||||
import * as __fd_glob_137 from "../content/docs/components/mini-calendar.mdx?collection=docs"
|
||||
import * as __fd_glob_138 from "../content/docs/components/minimal-tiptap.mdx?collection=docs"
|
||||
import * as __fd_glob_139 from "../content/docs/components/motion-highlight.mdx?collection=docs"
|
||||
import * as __fd_glob_140 from "../content/docs/components/navigation-menu.mdx?collection=docs"
|
||||
import * as __fd_glob_141 from "../content/docs/components/pagination.mdx?collection=docs"
|
||||
import * as __fd_glob_142 from "../content/docs/components/particles-background.mdx?collection=docs"
|
||||
import * as __fd_glob_143 from "../content/docs/components/particles.mdx?collection=docs"
|
||||
import * as __fd_glob_144 from "../content/docs/components/pill.mdx?collection=docs"
|
||||
import * as __fd_glob_145 from "../content/docs/components/pin-list.mdx?collection=docs"
|
||||
import * as __fd_glob_146 from "../content/docs/components/pixel-image.mdx?collection=docs"
|
||||
import * as __fd_glob_147 from "../content/docs/components/popover.mdx?collection=docs"
|
||||
import * as __fd_glob_148 from "../content/docs/components/progress.mdx?collection=docs"
|
||||
import * as __fd_glob_149 from "../content/docs/components/qr-code.mdx?collection=docs"
|
||||
import * as __fd_glob_150 from "../content/docs/components/radio-group.mdx?collection=docs"
|
||||
import * as __fd_glob_151 from "../content/docs/components/relative-time.mdx?collection=docs"
|
||||
import * as __fd_glob_152 from "../content/docs/components/resizable.mdx?collection=docs"
|
||||
import * as __fd_glob_153 from "../content/docs/components/safari.mdx?collection=docs"
|
||||
import * as __fd_glob_154 from "../content/docs/components/sandbox.mdx?collection=docs"
|
||||
import * as __fd_glob_155 from "../content/docs/components/scroll-area.mdx?collection=docs"
|
||||
import * as __fd_glob_156 from "../content/docs/components/search-and-toggle-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_157 from "../content/docs/components/select.mdx?collection=docs"
|
||||
import * as __fd_glob_158 from "../content/docs/components/separator.mdx?collection=docs"
|
||||
import * as __fd_glob_159 from "../content/docs/components/sheet.mdx?collection=docs"
|
||||
import * as __fd_glob_160 from "../content/docs/components/simple-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_161 from "../content/docs/components/skeleton.mdx?collection=docs"
|
||||
import * as __fd_glob_162 from "../content/docs/components/slider.mdx?collection=docs"
|
||||
import * as __fd_glob_163 from "../content/docs/components/snippet.mdx?collection=docs"
|
||||
import * as __fd_glob_164 from "../content/docs/components/sonner.mdx?collection=docs"
|
||||
import * as __fd_glob_165 from "../content/docs/components/spinner.mdx?collection=docs"
|
||||
import * as __fd_glob_166 from "../content/docs/components/stars-scrolling-wheel.mdx?collection=docs"
|
||||
import * as __fd_glob_167 from "../content/docs/components/status-dashboard-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_168 from "../content/docs/components/status.mdx?collection=docs"
|
||||
import * as __fd_glob_169 from "../content/docs/components/switch.mdx?collection=docs"
|
||||
import * as __fd_glob_170 from "../content/docs/components/table.mdx?collection=docs"
|
||||
import * as __fd_glob_171 from "../content/docs/components/tabs.mdx?collection=docs"
|
||||
import * as __fd_glob_172 from "../content/docs/components/tags.mdx?collection=docs"
|
||||
import * as __fd_glob_173 from "../content/docs/components/team-switcher-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_174 from "../content/docs/components/terminal.mdx?collection=docs"
|
||||
import * as __fd_glob_175 from "../content/docs/components/textarea.mdx?collection=docs"
|
||||
import * as __fd_glob_176 from "../content/docs/components/timeline.mdx?collection=docs"
|
||||
import * as __fd_glob_177 from "../content/docs/components/toast.mdx?collection=docs"
|
||||
import * as __fd_glob_178 from "../content/docs/components/toggle-group.mdx?collection=docs"
|
||||
import * as __fd_glob_179 from "../content/docs/components/toggle.mdx?collection=docs"
|
||||
import * as __fd_glob_180 from "../content/docs/components/tooltip.mdx?collection=docs"
|
||||
import * as __fd_glob_181 from "../content/docs/components/two-tier-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_182 from "../content/docs/components/typography.mdx?collection=docs"
|
||||
import * as __fd_glob_183 from "../content/docs/components/underline-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_184 from "../content/docs/components/video-player.mdx?collection=docs"
|
||||
import * as __fd_glob_28 from "../content/docs/dark-mode/astro.mdx?collection=docs"
|
||||
import * as __fd_glob_29 from "../content/docs/dark-mode/index.mdx?collection=docs"
|
||||
import * as __fd_glob_30 from "../content/docs/dark-mode/next.mdx?collection=docs"
|
||||
import * as __fd_glob_31 from "../content/docs/dark-mode/remix.mdx?collection=docs"
|
||||
import * as __fd_glob_32 from "../content/docs/dark-mode/vite.mdx?collection=docs"
|
||||
import * as __fd_glob_5 from "../content/docs/figma.mdx?collection=docs"
|
||||
import * as __fd_glob_25 from "../content/docs/frameworks/index.mdx?collection=docs"
|
||||
import * as __fd_glob_26 from "../content/docs/frameworks/react.mdx?collection=docs"
|
||||
import * as __fd_glob_27 from "../content/docs/frameworks/vue.mdx?collection=docs"
|
||||
import * as __fd_glob_33 from "../content/docs/guides/page-builder.mdx?collection=docs"
|
||||
import * as __fd_glob_34 from "../content/docs/guides/visual-workflows.mdx?collection=docs"
|
||||
import * as __fd_glob_35 from "../content/docs/hanzo-react/getting-started.mdx?collection=docs"
|
||||
import * as __fd_glob_36 from "../content/docs/hanzo-react/hooks.mdx?collection=docs"
|
||||
import * as __fd_glob_6 from "../content/docs/index.mdx?collection=docs"
|
||||
import * as __fd_glob_37 from "../content/docs/installation/astro.mdx?collection=docs"
|
||||
import * as __fd_glob_38 from "../content/docs/installation/gatsby.mdx?collection=docs"
|
||||
import * as __fd_glob_39 from "../content/docs/installation/index.mdx?collection=docs"
|
||||
import * as __fd_glob_40 from "../content/docs/installation/laravel.mdx?collection=docs"
|
||||
import * as __fd_glob_41 from "../content/docs/installation/manual.mdx?collection=docs"
|
||||
import * as __fd_glob_42 from "../content/docs/installation/next.mdx?collection=docs"
|
||||
import * as __fd_glob_43 from "../content/docs/installation/remix.mdx?collection=docs"
|
||||
import * as __fd_glob_44 from "../content/docs/installation/vite.mdx?collection=docs"
|
||||
import * as __fd_glob_7 from "../content/docs/mcp.mdx?collection=docs"
|
||||
import * as __fd_glob_45 from "../content/docs/packages/index.mdx?collection=docs"
|
||||
import * as __fd_glob_186 from "../content/docs/testing/index.mdx?collection=docs"
|
||||
import * as __fd_glob_8 from "../content/docs/theming.mdx?collection=docs"
|
||||
import * as __fd_glob_185 from "../content/docs/white-label/index.mdx?collection=docs"
|
||||
import type * as Config from "../source.config"
|
||||
|
||||
const create = server<
|
||||
typeof Config,
|
||||
import("@hanzo/docs-mdx/runtime/types").InternalTypeConfig & {
|
||||
DocData: {}
|
||||
}
|
||||
>({ doc: { passthroughs: ["extractedReferences"] } })
|
||||
|
||||
export const docs = await create.docs(
|
||||
"docs",
|
||||
"content/docs",
|
||||
{},
|
||||
{
|
||||
"about.mdx": __fd_glob_0,
|
||||
"changelog.mdx": __fd_glob_1,
|
||||
"cli.mdx": __fd_glob_2,
|
||||
"components-json.mdx": __fd_glob_3,
|
||||
"components.mdx": __fd_glob_4,
|
||||
"figma.mdx": __fd_glob_5,
|
||||
"index.mdx": __fd_glob_6,
|
||||
"mcp.mdx": __fd_glob_7,
|
||||
"theming.mdx": __fd_glob_8,
|
||||
"ai/actions.mdx": __fd_glob_9,
|
||||
"ai/agents.mdx": __fd_glob_10,
|
||||
"ai/assistant.mdx": __fd_glob_11,
|
||||
"ai/chat.mdx": __fd_glob_12,
|
||||
"ai/code.mdx": __fd_glob_13,
|
||||
"ai/index.mdx": __fd_glob_14,
|
||||
"ai/models.mdx": __fd_glob_15,
|
||||
"ai/playground.mdx": __fd_glob_16,
|
||||
"ai/tools.mdx": __fd_glob_17,
|
||||
"ai/vision.mdx": __fd_glob_18,
|
||||
"ai/voice-settings.mdx": __fd_glob_19,
|
||||
"ai/voice.mdx": __fd_glob_20,
|
||||
"block-examples/billing.mdx": __fd_glob_21,
|
||||
"block-examples/compose-spec.mdx": __fd_glob_22,
|
||||
"block-examples/newsletter.mdx": __fd_glob_23,
|
||||
"block-examples/pricing.mdx": __fd_glob_24,
|
||||
"frameworks/index.mdx": __fd_glob_25,
|
||||
"frameworks/react.mdx": __fd_glob_26,
|
||||
"frameworks/vue.mdx": __fd_glob_27,
|
||||
"dark-mode/astro.mdx": __fd_glob_28,
|
||||
"dark-mode/index.mdx": __fd_glob_29,
|
||||
"dark-mode/next.mdx": __fd_glob_30,
|
||||
"dark-mode/remix.mdx": __fd_glob_31,
|
||||
"dark-mode/vite.mdx": __fd_glob_32,
|
||||
"guides/page-builder.mdx": __fd_glob_33,
|
||||
"guides/visual-workflows.mdx": __fd_glob_34,
|
||||
"hanzo-react/getting-started.mdx": __fd_glob_35,
|
||||
"hanzo-react/hooks.mdx": __fd_glob_36,
|
||||
"installation/astro.mdx": __fd_glob_37,
|
||||
"installation/gatsby.mdx": __fd_glob_38,
|
||||
"installation/index.mdx": __fd_glob_39,
|
||||
"installation/laravel.mdx": __fd_glob_40,
|
||||
"installation/manual.mdx": __fd_glob_41,
|
||||
"installation/next.mdx": __fd_glob_42,
|
||||
"installation/remix.mdx": __fd_glob_43,
|
||||
"installation/vite.mdx": __fd_glob_44,
|
||||
"packages/index.mdx": __fd_glob_45,
|
||||
"components/3d-card.mdx": __fd_glob_46,
|
||||
"components/3d-marquee.mdx": __fd_glob_47,
|
||||
"components/3d-pin.mdx": __fd_glob_48,
|
||||
"components/accordion.mdx": __fd_glob_49,
|
||||
"components/advanced-navigation-bar.mdx": __fd_glob_50,
|
||||
"components/ai-model-selector-navigation-bar.mdx": __fd_glob_51,
|
||||
"components/alert-dialog.mdx": __fd_glob_52,
|
||||
"components/alert.mdx": __fd_glob_53,
|
||||
"components/android.mdx": __fd_glob_54,
|
||||
"components/animated-beam.mdx": __fd_glob_55,
|
||||
"components/animated-cursor.mdx": __fd_glob_56,
|
||||
"components/animated-testimonials.mdx": __fd_glob_57,
|
||||
"components/animated-tooltip.mdx": __fd_glob_58,
|
||||
"components/animation-components.mdx": __fd_glob_59,
|
||||
"components/announcement.mdx": __fd_glob_60,
|
||||
"components/app-switcher-navigation-bar.mdx": __fd_glob_61,
|
||||
"components/apple-cards-carousel.mdx": __fd_glob_62,
|
||||
"components/apple-hello-effect.mdx": __fd_glob_63,
|
||||
"components/aspect-ratio.mdx": __fd_glob_64,
|
||||
"components/avatar-group.mdx": __fd_glob_65,
|
||||
"components/avatar.mdx": __fd_glob_66,
|
||||
"components/badge.mdx": __fd_glob_67,
|
||||
"components/banner.mdx": __fd_glob_68,
|
||||
"components/breadcrumb-and-filters-navigation-bar.mdx": __fd_glob_69,
|
||||
"components/breadcrumb-navigation-bar.mdx": __fd_glob_70,
|
||||
"components/breadcrumb.mdx": __fd_glob_71,
|
||||
"components/button-group.mdx": __fd_glob_72,
|
||||
"components/button.mdx": __fd_glob_73,
|
||||
"components/calendar.mdx": __fd_glob_74,
|
||||
"components/card.mdx": __fd_glob_75,
|
||||
"components/carousel.mdx": __fd_glob_76,
|
||||
"components/centered-logo-navigation-bar.mdx": __fd_glob_77,
|
||||
"components/checkbox.mdx": __fd_glob_78,
|
||||
"components/choicebox.mdx": __fd_glob_79,
|
||||
"components/code-block.mdx": __fd_glob_80,
|
||||
"components/code-editor.mdx": __fd_glob_81,
|
||||
"components/code-tabs.mdx": __fd_glob_82,
|
||||
"components/collaboration-navigation-bar.mdx": __fd_glob_83,
|
||||
"components/collapsible.mdx": __fd_glob_84,
|
||||
"components/color-picker.mdx": __fd_glob_85,
|
||||
"components/combobox.mdx": __fd_glob_86,
|
||||
"components/command.mdx": __fd_glob_87,
|
||||
"components/communication-navigation-bar.mdx": __fd_glob_88,
|
||||
"components/comparison.mdx": __fd_glob_89,
|
||||
"components/context-menu.mdx": __fd_glob_90,
|
||||
"components/context-switcher-navigation-bar.mdx": __fd_glob_91,
|
||||
"components/credit-card.mdx": __fd_glob_92,
|
||||
"components/cursor.mdx": __fd_glob_93,
|
||||
"components/dashboard-navigation-bar.mdx": __fd_glob_94,
|
||||
"components/data-table.mdx": __fd_glob_95,
|
||||
"components/date-picker.mdx": __fd_glob_96,
|
||||
"components/desktop-hooks.mdx": __fd_glob_97,
|
||||
"components/desktop-spotlight.mdx": __fd_glob_98,
|
||||
"components/desktop-window.mdx": __fd_glob_99,
|
||||
"components/desktop.mdx": __fd_glob_100,
|
||||
"components/dialog-stack.mdx": __fd_glob_101,
|
||||
"components/dialog.mdx": __fd_glob_102,
|
||||
"components/dock.mdx": __fd_glob_103,
|
||||
"components/drawer.mdx": __fd_glob_104,
|
||||
"components/dropdown-menu.mdx": __fd_glob_105,
|
||||
"components/dropzone.mdx": __fd_glob_106,
|
||||
"components/e-commerce-navigation-bar.mdx": __fd_glob_107,
|
||||
"components/editor.mdx": __fd_glob_108,
|
||||
"components/empty.mdx": __fd_glob_109,
|
||||
"components/field.mdx": __fd_glob_110,
|
||||
"components/form.mdx": __fd_glob_111,
|
||||
"components/gantt.mdx": __fd_glob_112,
|
||||
"components/glimpse.mdx": __fd_glob_113,
|
||||
"components/grid-pattern.mdx": __fd_glob_114,
|
||||
"components/hover-card.mdx": __fd_glob_115,
|
||||
"components/icon-navigation-bar.mdx": __fd_glob_116,
|
||||
"components/image-crop.mdx": __fd_glob_117,
|
||||
"components/image-zoom.mdx": __fd_glob_118,
|
||||
"components/index.mdx": __fd_glob_119,
|
||||
"components/input-group.mdx": __fd_glob_120,
|
||||
"components/input-otp.mdx": __fd_glob_121,
|
||||
"components/input.mdx": __fd_glob_122,
|
||||
"components/interactive-grid-pattern.mdx": __fd_glob_123,
|
||||
"components/iphone-15-pro.mdx": __fd_glob_124,
|
||||
"components/item.mdx": __fd_glob_125,
|
||||
"components/kanban.mdx": __fd_glob_126,
|
||||
"components/kbd.mdx": __fd_glob_127,
|
||||
"components/label.mdx": __fd_glob_128,
|
||||
"components/limelight-nav.mdx": __fd_glob_129,
|
||||
"components/list.mdx": __fd_glob_130,
|
||||
"components/macos-dock.mdx": __fd_glob_131,
|
||||
"components/magnetic.mdx": __fd_glob_132,
|
||||
"components/marquee.mdx": __fd_glob_133,
|
||||
"components/menu-dock.mdx": __fd_glob_134,
|
||||
"components/menubar.mdx": __fd_glob_135,
|
||||
"components/message-dock.mdx": __fd_glob_136,
|
||||
"components/mini-calendar.mdx": __fd_glob_137,
|
||||
"components/minimal-tiptap.mdx": __fd_glob_138,
|
||||
"components/motion-highlight.mdx": __fd_glob_139,
|
||||
"components/navigation-menu.mdx": __fd_glob_140,
|
||||
"components/pagination.mdx": __fd_glob_141,
|
||||
"components/particles-background.mdx": __fd_glob_142,
|
||||
"components/particles.mdx": __fd_glob_143,
|
||||
"components/pill.mdx": __fd_glob_144,
|
||||
"components/pin-list.mdx": __fd_glob_145,
|
||||
"components/pixel-image.mdx": __fd_glob_146,
|
||||
"components/popover.mdx": __fd_glob_147,
|
||||
"components/progress.mdx": __fd_glob_148,
|
||||
"components/qr-code.mdx": __fd_glob_149,
|
||||
"components/radio-group.mdx": __fd_glob_150,
|
||||
"components/relative-time.mdx": __fd_glob_151,
|
||||
"components/resizable.mdx": __fd_glob_152,
|
||||
"components/safari.mdx": __fd_glob_153,
|
||||
"components/sandbox.mdx": __fd_glob_154,
|
||||
"components/scroll-area.mdx": __fd_glob_155,
|
||||
"components/search-and-toggle-navigation-bar.mdx": __fd_glob_156,
|
||||
"components/select.mdx": __fd_glob_157,
|
||||
"components/separator.mdx": __fd_glob_158,
|
||||
"components/sheet.mdx": __fd_glob_159,
|
||||
"components/simple-navigation-bar.mdx": __fd_glob_160,
|
||||
"components/skeleton.mdx": __fd_glob_161,
|
||||
"components/slider.mdx": __fd_glob_162,
|
||||
"components/snippet.mdx": __fd_glob_163,
|
||||
"components/sonner.mdx": __fd_glob_164,
|
||||
"components/spinner.mdx": __fd_glob_165,
|
||||
"components/stars-scrolling-wheel.mdx": __fd_glob_166,
|
||||
"components/status-dashboard-navigation-bar.mdx": __fd_glob_167,
|
||||
"components/status.mdx": __fd_glob_168,
|
||||
"components/switch.mdx": __fd_glob_169,
|
||||
"components/table.mdx": __fd_glob_170,
|
||||
"components/tabs.mdx": __fd_glob_171,
|
||||
"components/tags.mdx": __fd_glob_172,
|
||||
"components/team-switcher-navigation-bar.mdx": __fd_glob_173,
|
||||
"components/terminal.mdx": __fd_glob_174,
|
||||
"components/textarea.mdx": __fd_glob_175,
|
||||
"components/timeline.mdx": __fd_glob_176,
|
||||
"components/toast.mdx": __fd_glob_177,
|
||||
"components/toggle-group.mdx": __fd_glob_178,
|
||||
"components/toggle.mdx": __fd_glob_179,
|
||||
"components/tooltip.mdx": __fd_glob_180,
|
||||
"components/two-tier-navigation-bar.mdx": __fd_glob_181,
|
||||
"components/typography.mdx": __fd_glob_182,
|
||||
"components/underline-navigation-bar.mdx": __fd_glob_183,
|
||||
"components/video-player.mdx": __fd_glob_184,
|
||||
"white-label/index.mdx": __fd_glob_185,
|
||||
"testing/index.mdx": __fd_glob_186,
|
||||
}
|
||||
)
|
||||
@@ -1,40 +0,0 @@
|
||||
// source.config.ts
|
||||
import {
|
||||
defineConfig,
|
||||
defineDocs,
|
||||
frontmatterSchema
|
||||
} from "@hanzo/docs-mdx/config";
|
||||
import rehypePrettyCode from "rehype-pretty-code";
|
||||
import { z } from "zod";
|
||||
var source_config_default = defineConfig({
|
||||
mdxOptions: {
|
||||
rehypePlugins: [
|
||||
[
|
||||
rehypePrettyCode,
|
||||
{
|
||||
theme: {
|
||||
dark: "github-dark",
|
||||
light: "github-light-default"
|
||||
},
|
||||
keepBackground: false
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
});
|
||||
var docs = defineDocs({
|
||||
dir: "content/docs",
|
||||
docs: {
|
||||
schema: frontmatterSchema.extend({
|
||||
links: z.object({
|
||||
doc: z.string().optional(),
|
||||
api: z.string().optional()
|
||||
}).optional(),
|
||||
toc: z.boolean().optional()
|
||||
})
|
||||
}
|
||||
});
|
||||
export {
|
||||
source_config_default as default,
|
||||
docs
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
node_modules/
|
||||
.next/
|
||||
out/
|
||||
build/
|
||||
dist/
|
||||
next-env.d.ts
|
||||
__registry__/
|
||||
.source/
|
||||
*.config.js
|
||||
*.config.mjs
|
||||
.turbo/
|
||||
coverage/
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"plugins": ["@typescript-eslint", "react"],
|
||||
"ignorePatterns": [
|
||||
"__registry__/**",
|
||||
".source/**",
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"dist/**",
|
||||
".turbo/**",
|
||||
"node_modules/**"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-empty-object-type": "warn",
|
||||
"@typescript-eslint/ban-ts-comment": "warn",
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"@typescript-eslint/no-require-imports": "warn",
|
||||
"@typescript-eslint/no-this-alias": "warn",
|
||||
"@typescript-eslint/triple-slash-reference": "warn",
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"react/no-unescaped-entities": "warn",
|
||||
"react/jsx-no-comment-textnodes": "warn",
|
||||
"react/no-find-dom-node": "warn",
|
||||
"prefer-const": "warn"
|
||||
}
|
||||
}
|
||||
@@ -4,5 +4,3 @@ node_modules
|
||||
build
|
||||
.contentlayer
|
||||
apps/web/pages/api/registry.json
|
||||
**/fixtures
|
||||
deprecated
|
||||
|
||||
@@ -94,7 +94,7 @@ forge script script/DeployIdentitySystem.s.sol:DeployIdentitySystem \
|
||||
|
||||
# For Lux Mainnet
|
||||
forge script script/DeployIdentitySystem.s.sol:DeployIdentitySystem \
|
||||
--rpc-url https://api.lux.network/v1/bc/C/rpc \
|
||||
--rpc-url https://api.lux.network/ext/bc/C/rpc \
|
||||
--broadcast
|
||||
|
||||
# For Zoo Mainnet
|
||||
@@ -131,8 +131,8 @@ Visit `http://localhost:3333/identity` to access the identity registration page.
|
||||
|---------|----------|---------|----------|
|
||||
| Hanzo Mainnet | 36963 | https://rpc.hanzo.ai | https://explorer.hanzo.ai |
|
||||
| Hanzo Testnet | 36962 | https://testnet-rpc.hanzo.ai | https://testnet-explorer.hanzo.ai |
|
||||
| Lux Mainnet | 96369 | https://api.lux.network/v1/bc/C/rpc | https://explorer.lux.network |
|
||||
| Lux Testnet | 96368 | https://testnet-api.lux.network/v1/bc/C/rpc | https://testnet-explorer.lux.network |
|
||||
| Lux Mainnet | 96369 | https://api.lux.network/ext/bc/C/rpc | https://explorer.lux.network |
|
||||
| Lux Testnet | 96368 | https://testnet-api.lux.network/ext/bc/C/rpc | https://testnet-explorer.lux.network |
|
||||
| Zoo Mainnet | 200200 | https://rpc.zoo.network | https://explorer.zoo.network |
|
||||
| Zoo Testnet | 200201 | https://testnet-rpc.zoo.network | https://testnet-explorer.zoo.network |
|
||||
|
||||
|
||||
@@ -63,11 +63,10 @@ export default function Dashboard() {
|
||||
<div className="hidden bg-muted lg:block">
|
||||
<Image
|
||||
src="/placeholder.svg"
|
||||
alt="Authentication page cover image"
|
||||
alt="Image"
|
||||
width="1920"
|
||||
height="1080"
|
||||
className="h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -267,7 +267,6 @@ export default function Dashboard() {
|
||||
height={36}
|
||||
alt="Avatar"
|
||||
className="overflow-hidden rounded-full"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@@ -256,7 +256,6 @@ export default function Dashboard() {
|
||||
height={36}
|
||||
alt="Avatar"
|
||||
className="overflow-hidden rounded-full"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -357,7 +356,6 @@ export default function Dashboard() {
|
||||
height="64"
|
||||
src="/placeholder.svg"
|
||||
width="64"
|
||||
loading="lazy"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
@@ -403,7 +401,6 @@ export default function Dashboard() {
|
||||
height="64"
|
||||
src="/placeholder.svg"
|
||||
width="64"
|
||||
loading="lazy"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
@@ -449,7 +446,6 @@ export default function Dashboard() {
|
||||
height="64"
|
||||
src="/placeholder.svg"
|
||||
width="64"
|
||||
loading="lazy"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
@@ -495,7 +491,6 @@ export default function Dashboard() {
|
||||
height="64"
|
||||
src="/placeholder.svg"
|
||||
width="64"
|
||||
loading="lazy"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
@@ -541,7 +536,6 @@ export default function Dashboard() {
|
||||
height="64"
|
||||
src="/placeholder.svg"
|
||||
width="64"
|
||||
loading="lazy"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
@@ -587,7 +581,6 @@ export default function Dashboard() {
|
||||
height="64"
|
||||
src="/placeholder.svg"
|
||||
width="64"
|
||||
loading="lazy"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
|
||||
@@ -259,7 +259,6 @@ export default function Dashboard() {
|
||||
src="/placeholder-user.jpg"
|
||||
width={36}
|
||||
height={36}
|
||||
loading="lazy"
|
||||
alt="Avatar"
|
||||
className="overflow-hidden rounded-full"
|
||||
/>
|
||||
@@ -552,27 +551,24 @@ export default function Dashboard() {
|
||||
height="300"
|
||||
src="/placeholder.svg"
|
||||
width="300"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<button>
|
||||
<Image
|
||||
alt="Product thumbnail image"
|
||||
alt="Product image"
|
||||
className="aspect-square w-full rounded-md object-cover"
|
||||
height="84"
|
||||
src="/placeholder.svg"
|
||||
width="84"
|
||||
loading="lazy"
|
||||
/>
|
||||
</button>
|
||||
<button>
|
||||
<Image
|
||||
alt="Product thumbnail image"
|
||||
alt="Product image"
|
||||
className="aspect-square w-full rounded-md object-cover"
|
||||
height="84"
|
||||
src="/placeholder.svg"
|
||||
width="84"
|
||||
loading="lazy"
|
||||
/>
|
||||
</button>
|
||||
<button className="flex aspect-square w-full items-center justify-center rounded-md border border-dashed">
|
||||
|
||||
@@ -203,17 +203,6 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"direction": {
|
||||
name: "direction",
|
||||
type: "components:ui",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/ui/direction")),
|
||||
source: "",
|
||||
files: ["registry/default/ui/direction.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"dialog": {
|
||||
name: "dialog",
|
||||
type: "components:ui",
|
||||
@@ -247,17 +236,6 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"empty-state": {
|
||||
name: "empty-state",
|
||||
type: "components:ui",
|
||||
registryDependencies: ["button"],
|
||||
component: React.lazy(() => import("@/registry/default/ui/empty-state")),
|
||||
source: "",
|
||||
files: ["registry/default/ui/empty-state.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"gantt": {
|
||||
name: "gantt",
|
||||
type: "components:ui",
|
||||
@@ -1809,17 +1787,6 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"youtube-pip-player": {
|
||||
name: "youtube-pip-player",
|
||||
type: "components:ui",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/ui/youtube-pip-player")),
|
||||
source: "",
|
||||
files: ["registry/default/ui/youtube-pip-player.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"music-player": {
|
||||
name: "music-player",
|
||||
type: "components:ui",
|
||||
@@ -4372,17 +4339,6 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"youtube-pip-player-demo": {
|
||||
name: "youtube-pip-player-demo",
|
||||
type: "components:example",
|
||||
registryDependencies: ["youtube-pip-player"],
|
||||
component: React.lazy(() => import("@/registry/default/example/youtube-pip-player-demo")),
|
||||
source: "",
|
||||
files: ["registry/default/example/youtube-pip-player-demo.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"android-demo": {
|
||||
name: "android-demo",
|
||||
type: "components:example",
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
/**
|
||||
* docs-no-404.test.ts
|
||||
*
|
||||
* Static validation: every URL in docs.ts config has a matching MDX file.
|
||||
* No server needed — runs in CI before deployment.
|
||||
*
|
||||
* Run: pnpm test (via vitest)
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
const CONTENT_DIR = path.resolve(__dirname, '../content/docs')
|
||||
const CONFIG_PATH = path.resolve(__dirname, '../config/docs.ts')
|
||||
|
||||
/** Extract all href values from docs.ts config */
|
||||
function extractUrlsFromConfig(): string[] {
|
||||
const content = fs.readFileSync(CONFIG_PATH, 'utf-8')
|
||||
const hrefRegex = /href:\s*["'`]([^"'`]+)["'`]/g
|
||||
const urls: string[] = []
|
||||
let match
|
||||
while ((match = hrefRegex.exec(content)) !== null) {
|
||||
urls.push(match[1])
|
||||
}
|
||||
return urls.filter(u => u.startsWith('/docs'))
|
||||
}
|
||||
|
||||
/** Convert a /docs/... URL to its expected MDX file path */
|
||||
function urlToMdxPath(url: string): string[] {
|
||||
// /docs -> index.mdx
|
||||
// /docs/charts/bar -> charts/bar.mdx OR charts/bar/index.mdx
|
||||
const slug = url.replace(/^\/docs\/?/, '') || ''
|
||||
if (!slug) {
|
||||
return [path.join(CONTENT_DIR, 'index.mdx')]
|
||||
}
|
||||
return [
|
||||
path.join(CONTENT_DIR, `${slug}.mdx`),
|
||||
path.join(CONTENT_DIR, slug, 'index.mdx'),
|
||||
]
|
||||
}
|
||||
|
||||
/** Recursively find all MDX files in content/docs */
|
||||
function findAllMdxFiles(dir: string, prefix = ''): string[] {
|
||||
const results: string[] = []
|
||||
if (!fs.existsSync(dir)) return results
|
||||
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const fullPath = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...findAllMdxFiles(fullPath, `${prefix}${entry.name}/`))
|
||||
} else if (entry.name.endsWith('.mdx')) {
|
||||
const slug = entry.name === 'index.mdx'
|
||||
? prefix.replace(/\/$/, '')
|
||||
: `${prefix}${entry.name.replace('.mdx', '')}`
|
||||
if (slug) {
|
||||
results.push(`/docs/${slug}`)
|
||||
} else {
|
||||
results.push('/docs')
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
describe('Documentation pages — zero 404s', () => {
|
||||
const configUrls = extractUrlsFromConfig()
|
||||
const mdxPages = findAllMdxFiles(CONTENT_DIR)
|
||||
|
||||
it('should find docs config with URLs', () => {
|
||||
expect(configUrls.length).toBeGreaterThan(100)
|
||||
})
|
||||
|
||||
it('should find MDX content files', () => {
|
||||
expect(mdxPages.length).toBeGreaterThan(100)
|
||||
})
|
||||
|
||||
describe('every config URL has a matching MDX file', () => {
|
||||
for (const url of configUrls) {
|
||||
it(`${url}`, () => {
|
||||
const possiblePaths = urlToMdxPath(url)
|
||||
const exists = possiblePaths.some(p => fs.existsSync(p))
|
||||
if (!exists) {
|
||||
throw new Error(
|
||||
`Missing MDX file for ${url}. Expected one of:\n` +
|
||||
possiblePaths.map(p => ` - ${p}`).join('\n')
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('every MDX file is reachable from config', () => {
|
||||
const configSet = new Set(configUrls)
|
||||
const unreachable: string[] = []
|
||||
|
||||
for (const page of mdxPages) {
|
||||
if (!configSet.has(page)) {
|
||||
unreachable.push(page)
|
||||
}
|
||||
}
|
||||
|
||||
it('should have no orphaned MDX files (or document known exceptions)', () => {
|
||||
// Allow a small number of known orphans (category index pages, etc.)
|
||||
const KNOWN_ORPHANS = new Set([
|
||||
'/docs/ai/voice-settings',
|
||||
'/docs/components/animation-components',
|
||||
])
|
||||
const unexpected = unreachable.filter(u => !KNOWN_ORPHANS.has(u))
|
||||
if (unexpected.length > 0) {
|
||||
console.warn(`Orphaned MDX files (not in nav config):\n${unexpected.map(u => ` ${u}`).join('\n')}`)
|
||||
}
|
||||
// Warn but don't fail for orphans — they just won't be in nav
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('summary: config URLs vs MDX files', () => {
|
||||
const configSet = new Set(configUrls)
|
||||
const mdxSet = new Set(mdxPages)
|
||||
|
||||
const missingMdx = configUrls.filter(u => {
|
||||
const paths = urlToMdxPath(u)
|
||||
return !paths.some(p => fs.existsSync(p))
|
||||
})
|
||||
const orphanedMdx = mdxPages.filter(p => !configSet.has(p))
|
||||
|
||||
console.log(`\n📊 Docs coverage report:`)
|
||||
console.log(` Config URLs: ${configUrls.length}`)
|
||||
console.log(` MDX files: ${mdxPages.length}`)
|
||||
console.log(` Missing MDX: ${missingMdx.length}`)
|
||||
console.log(` Orphaned: ${orphanedMdx.length}`)
|
||||
|
||||
if (missingMdx.length > 0) {
|
||||
console.error(`\n❌ URLs with no MDX file:\n${missingMdx.map(u => ` ${u}`).join('\n')}`)
|
||||
}
|
||||
if (orphanedMdx.length > 0) {
|
||||
console.warn(`\n⚠️ MDX files not in nav:\n${orphanedMdx.map(u => ` ${u}`).join('\n')}`)
|
||||
}
|
||||
|
||||
// This is the hard check — every config URL must have content
|
||||
expect(missingMdx).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -42,8 +42,8 @@ describe("@hanzo/ui namespace imports - Type Resolution", () => {
|
||||
* import { GridPattern } from '@hanzo/ui/pattern/grid'
|
||||
* ```
|
||||
*
|
||||
* The package exports are properly configured in pkgs/ui/package.json.
|
||||
* Build artifacts exist in pkgs/ui/dist/{code,3d,pattern}/.
|
||||
* The package exports are properly configured in pkg/ui/package.json.
|
||||
* Build artifacts exist in pkg/ui/dist/{code,3d,pattern}/.
|
||||
*
|
||||
* Vitest runtime resolution fails due to internal cross-package imports
|
||||
* (e.g., CodeBlock imports from @hanzo/ui/lib/utils), which require
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { Suspense } from "react"
|
||||
|
||||
import { getAllBlockIds } from "@/lib/blocks"
|
||||
import { registryCategories } from "@/lib/categories"
|
||||
import { BlockDisplay } from "@/components/block-display"
|
||||
import { SkeletonCardGrid } from "@/components/skeleton-card"
|
||||
|
||||
export const revalidate = false
|
||||
export const dynamic = "force-static"
|
||||
@@ -15,30 +12,19 @@ export async function generateStaticParams() {
|
||||
}))
|
||||
}
|
||||
|
||||
async function BlockGallery({ categories }: { categories: string[] }) {
|
||||
const blocks = await getAllBlockIds(["registry:block"], categories)
|
||||
|
||||
return (
|
||||
<>
|
||||
{blocks.map((name) => (
|
||||
<BlockDisplay name={name} key={name} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default async function BlocksPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ categories?: string[] }>
|
||||
}) {
|
||||
const { categories = [] } = await params
|
||||
const blocks = await getAllBlockIds(["registry:block"], categories)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-12 md:gap-24">
|
||||
<Suspense fallback={<SkeletonCardGrid count={9} />}>
|
||||
<BlockGallery categories={categories} />
|
||||
</Suspense>
|
||||
{blocks.map((name) => (
|
||||
<BlockDisplay name={name} key={name} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,311 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Index } from "@/__registry__"
|
||||
import { AlertCircle, RefreshCw } from "lucide-react"
|
||||
|
||||
import { Badge } from "@/registry/default/ui/badge"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Card } from "@/registry/default/ui/card"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { ScrollArea } from "@/registry/default/ui/scroll-area"
|
||||
import { Skeleton } from "@/registry/default/ui/skeleton"
|
||||
import { Spinner } from "@/registry/default/ui/spinner"
|
||||
|
||||
interface IframeState {
|
||||
loaded: boolean
|
||||
error: boolean
|
||||
showContent: boolean
|
||||
}
|
||||
|
||||
function BlockPreview({ blockName }: { blockName: string }) {
|
||||
const [state, setState] = React.useState<IframeState>({
|
||||
loaded: false,
|
||||
error: false,
|
||||
showContent: false,
|
||||
})
|
||||
const [isInView, setIsInView] = React.useState(false)
|
||||
const iframeRef = React.useRef<HTMLIFrameElement>(null)
|
||||
const containerRef = React.useRef<HTMLDivElement>(null)
|
||||
const minLoadTimeRef = React.useRef<NodeJS.Timeout | undefined>(undefined)
|
||||
|
||||
// Intersection Observer for lazy loading
|
||||
React.useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsInView(true)
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
rootMargin: "100px", // Start loading 100px before entering viewport
|
||||
}
|
||||
)
|
||||
|
||||
if (containerRef.current) {
|
||||
observer.observe(containerRef.current)
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleLoad = React.useCallback(() => {
|
||||
setState((prev) => ({ ...prev, loaded: true, error: false }))
|
||||
// Ensure minimum loading time of 200ms for smooth UX
|
||||
minLoadTimeRef.current = setTimeout(() => {
|
||||
setState((prev) => ({ ...prev, showContent: true }))
|
||||
}, 200)
|
||||
}, [])
|
||||
|
||||
const handleError = React.useCallback(() => {
|
||||
setState({ loaded: true, error: true, showContent: true })
|
||||
}, [])
|
||||
|
||||
const handleRetry = React.useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setState({ loaded: false, error: false, showContent: false })
|
||||
if (iframeRef.current) {
|
||||
// Force reload by resetting src
|
||||
const src = iframeRef.current.src
|
||||
iframeRef.current.src = ""
|
||||
setTimeout(() => {
|
||||
if (iframeRef.current) {
|
||||
iframeRef.current.src = src
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (minLoadTimeRef.current) {
|
||||
clearTimeout(minLoadTimeRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative aspect-[4/3] min-h-[400px] bg-muted/50"
|
||||
>
|
||||
{/* Loading skeleton */}
|
||||
{!state.showContent && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Spinner className="size-6" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isInView ? "Loading preview..." : "Scroll to load..."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error state */}
|
||||
{state.error && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-3 p-4 text-center">
|
||||
<AlertCircle className="size-8 text-destructive" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">Failed to load preview</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
The block could not be loaded
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRetry}
|
||||
className="gap-2"
|
||||
>
|
||||
<RefreshCw className="size-3" />
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Iframe with lazy loading and fade-in animation */}
|
||||
{isInView && (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={`/view/${blockName}`}
|
||||
className={`h-full w-full border-0 transition-opacity duration-300 ${
|
||||
state.showContent && !state.error ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
title={blockName}
|
||||
onLoad={handleLoad}
|
||||
onError={handleError}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function BuilderPage() {
|
||||
const [filter, setFilter] = React.useState("")
|
||||
const [blocks, setBlocks] = React.useState<string[]>([])
|
||||
const [isLoadingBlocks, setIsLoadingBlocks] = React.useState(true)
|
||||
|
||||
React.useEffect(() => {
|
||||
// Simulate async block loading with minimum display time
|
||||
const loadBlocks = async () => {
|
||||
setIsLoadingBlocks(true)
|
||||
const startTime = Date.now()
|
||||
|
||||
// Get blocks from registry (flat structure)
|
||||
const blockIds = Object.keys(Index).filter((key) => {
|
||||
const item = Index[key]
|
||||
return item?.type === "components:block"
|
||||
})
|
||||
|
||||
// Ensure minimum loading time of 200ms
|
||||
const elapsed = Date.now() - startTime
|
||||
if (elapsed < 200) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200 - elapsed))
|
||||
}
|
||||
|
||||
setBlocks(blockIds.sort())
|
||||
setIsLoadingBlocks(false)
|
||||
}
|
||||
|
||||
loadBlocks()
|
||||
}, [])
|
||||
|
||||
const filteredBlocks = React.useMemo(() => {
|
||||
if (!filter) return blocks
|
||||
return blocks.filter((block) =>
|
||||
block.toLowerCase().includes(filter.toLowerCase())
|
||||
)
|
||||
}, [blocks, filter])
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col">
|
||||
{/* Header with loading state */}
|
||||
<div className="border-b p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-lg font-semibold">Blocks</h1>
|
||||
{isLoadingBlocks && <Spinner className="size-4" />}
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Filter blocks..."
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
className="max-w-xs"
|
||||
disabled={isLoadingBlocks}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{isLoadingBlocks ? "Loading..." : `${filteredBlocks.length} blocks`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grid with loading and empty states */}
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="grid grid-cols-1 gap-4 p-6 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{/* Loading skeleton */}
|
||||
{isLoadingBlocks &&
|
||||
Array.from({ length: 8 }).map((_, i) => (
|
||||
<Card
|
||||
key={i}
|
||||
className="overflow-hidden rounded-lg border-2 border-border"
|
||||
>
|
||||
<Skeleton className="aspect-[4/3] min-h-[400px] w-full rounded-none" />
|
||||
<div className="border-t bg-card p-4">
|
||||
<Skeleton className="mb-2 h-5 w-3/4" />
|
||||
<Skeleton className="mb-1 h-4 w-20" />
|
||||
<Skeleton className="mt-2 h-4 w-full" />
|
||||
<Skeleton className="mt-1 h-4 w-2/3" />
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{/* Empty state */}
|
||||
{!isLoadingBlocks && filteredBlocks.length === 0 && (
|
||||
<div className="col-span-full flex min-h-[400px] items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-medium text-muted-foreground">
|
||||
No blocks found
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{filter
|
||||
? `No blocks match "${filter}"`
|
||||
: "No blocks available"}
|
||||
</p>
|
||||
{filter && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setFilter("")}
|
||||
className="mt-4"
|
||||
>
|
||||
Clear filter
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Block cards */}
|
||||
{!isLoadingBlocks &&
|
||||
filteredBlocks.map((blockName) => {
|
||||
const block = Index[blockName]
|
||||
return (
|
||||
<a
|
||||
key={blockName}
|
||||
href={`/view/${blockName}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group"
|
||||
>
|
||||
<Card className="overflow-hidden rounded-lg border-2 border-border shadow-sm transition-all duration-300 hover:scale-[1.02] hover:border-primary hover:shadow-lg hover:bg-accent/5">
|
||||
{/* Preview with loading states */}
|
||||
<BlockPreview blockName={blockName} />
|
||||
|
||||
{/* Block info - hidden until hover */}
|
||||
<div className="border-t bg-card p-4 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<div className="mb-2 flex items-start justify-between gap-2">
|
||||
<h3 className="text-base font-semibold leading-tight">
|
||||
{blockName}
|
||||
</h3>
|
||||
{block?.category && block?.category !== "undefined" && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="shrink-0 text-xs font-medium"
|
||||
>
|
||||
{block.category}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{block?.subcategory &&
|
||||
block?.subcategory !== "undefined" && (
|
||||
<div className="mb-2">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-xs font-normal"
|
||||
>
|
||||
{block.subcategory}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
{block?.description && (
|
||||
<p className="mt-2 text-sm text-muted-foreground line-clamp-2">
|
||||
{block.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Suspense } from "react"
|
||||
import Link from "next/link"
|
||||
|
||||
import { BlockDisplay } from "@/components/block-display"
|
||||
import { SkeletonCardGrid } from "@/components/skeleton-card"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
|
||||
export const dynamic = "force-static"
|
||||
@@ -20,11 +18,9 @@ const FEATURED_BLOCKS = [
|
||||
export default function BlocksPage() {
|
||||
return (
|
||||
<div className="flex flex-col gap-12 md:gap-24">
|
||||
<Suspense fallback={<SkeletonCardGrid count={5} />}>
|
||||
{FEATURED_BLOCKS.map((name) => (
|
||||
<BlockDisplay name={name} key={name} />
|
||||
))}
|
||||
</Suspense>
|
||||
{FEATURED_BLOCKS.map((name) => (
|
||||
<BlockDisplay name={name} key={name} />
|
||||
))}
|
||||
<div className="container-wrapper">
|
||||
<div className="container flex justify-center py-6">
|
||||
<Button asChild variant="outline">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -63,14 +63,14 @@ export default function EnhancedBuilder() {
|
||||
|
||||
React.useEffect(() => {
|
||||
// Get blocks from registry
|
||||
const blockIds = Object.keys(Index || {}).filter((key) => {
|
||||
const blockIds = Object.keys(Index).filter((key) => {
|
||||
const item = Index[key]
|
||||
return item?.type === "components:block"
|
||||
})
|
||||
setBlocks(blockIds.sort())
|
||||
|
||||
// Get UI components from registry
|
||||
const componentIds = Object.keys(Index || {}).filter((key) => {
|
||||
const componentIds = Object.keys(Index).filter((key) => {
|
||||
const item = Index[key]
|
||||
return item?.type === "components:ui"
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,427 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import dynamic from "next/dynamic"
|
||||
import { Index } from "@/__registry__"
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragOverlay,
|
||||
DragStartEvent,
|
||||
} from "@dnd-kit/core"
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable"
|
||||
import { CSS } from "@dnd-kit/utilities"
|
||||
import { Download, Eye, GripVertical, Plus, Trash2 } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york/ui/button"
|
||||
import { Card } from "@/registry/new-york/ui/card"
|
||||
import { Input } from "@/registry/new-york/ui/input"
|
||||
import { ScrollArea } from "@/registry/new-york/ui/scroll-area"
|
||||
import { Separator } from "@/registry/new-york/ui/separator"
|
||||
|
||||
// Dynamic block component loader
|
||||
const DynamicBlock = ({ blockName, scale = 1 }: { blockName: string; scale?: number }) => {
|
||||
const [BlockComponent, setBlockComponent] = React.useState<React.ComponentType | null>(null)
|
||||
const [error, setError] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
import(`@/registry/default/block/${blockName}`)
|
||||
.then((mod) => {
|
||||
setBlockComponent(() => mod.default)
|
||||
setError(false)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Failed to load block ${blockName}:`, err)
|
||||
setError(true)
|
||||
})
|
||||
}, [blockName])
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-muted/50 p-4 text-center">
|
||||
<p className="text-xs text-muted-foreground">Failed to load {blockName}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!BlockComponent) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-muted/50">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ transform: `scale(${scale})`, transformOrigin: "top left" }}>
|
||||
<BlockComponent />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface PageBlock {
|
||||
id: string
|
||||
blockName: string
|
||||
}
|
||||
|
||||
export default function PageBuilder() {
|
||||
const [blocks, setBlocks] = React.useState<string[]>([])
|
||||
const [availableBlocks, setAvailableBlocks] = React.useState<string[]>([])
|
||||
const [pageBlocks, setPageBlocks] = React.useState<PageBlock[]>([])
|
||||
const [activeId, setActiveId] = React.useState<string | null>(null)
|
||||
const [filter, setFilter] = React.useState("")
|
||||
const [viewport, setViewport] = React.useState<"desktop" | "tablet" | "mobile">("desktop")
|
||||
|
||||
React.useEffect(() => {
|
||||
// Get block IDs from the registry index
|
||||
const blockIds = Object.keys(Index.default || {}).filter((key) => {
|
||||
const item = Index.default[key]
|
||||
return item?.type === "components:block"
|
||||
})
|
||||
setBlocks(blockIds)
|
||||
setAvailableBlocks(blockIds)
|
||||
}, [])
|
||||
|
||||
const filteredBlocks = availableBlocks.filter((block) =>
|
||||
block.toLowerCase().includes(filter.toLowerCase())
|
||||
)
|
||||
|
||||
const addBlock = (blockName: string) => {
|
||||
setPageBlocks([...pageBlocks, { id: crypto.randomUUID(), blockName }])
|
||||
}
|
||||
|
||||
const removeBlock = (id: string) => {
|
||||
setPageBlocks(pageBlocks.filter((b) => b.id !== id))
|
||||
}
|
||||
|
||||
const handleDragStart = (event: DragStartEvent) => {
|
||||
setActiveId(event.active.id as string)
|
||||
}
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
setActiveId(null)
|
||||
|
||||
if (!over || active.id === over.id) return
|
||||
|
||||
setPageBlocks((items) => {
|
||||
const oldIndex = items.findIndex((item) => item.id === active.id)
|
||||
const newIndex = items.findIndex((item) => item.id === over.id)
|
||||
return arrayMove(items, oldIndex, newIndex)
|
||||
})
|
||||
}
|
||||
|
||||
const generatePageCode = () => {
|
||||
const imports = pageBlocks
|
||||
.map((block) => `import ${toPascalCase(block.blockName)} from "@/registry/default/block/${block.blockName}"`)
|
||||
.join("\n")
|
||||
|
||||
const components = pageBlocks
|
||||
.map((block) => ` <${toPascalCase(block.blockName)} />`)
|
||||
.join("\n")
|
||||
|
||||
return `"use client"
|
||||
|
||||
import * as React from "react"
|
||||
${imports}
|
||||
|
||||
export default function CustomPage() {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col">
|
||||
${components}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
const copyCode = async () => {
|
||||
const code = generatePageCode()
|
||||
await navigator.clipboard.writeText(code)
|
||||
// TODO: Show toast notification
|
||||
}
|
||||
|
||||
const downloadCode = () => {
|
||||
const code = generatePageCode()
|
||||
const blob = new Blob([code], { type: "text/typescript" })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = "page.tsx"
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
const deployWithHanzo = () => {
|
||||
// TODO: Integrate with Hanzo deployment API
|
||||
const code = generatePageCode()
|
||||
console.log("Deploying with Hanzo:", code)
|
||||
// This would call hanzo deployment service
|
||||
window.open("https://hanzo.ai/deploy", "_blank")
|
||||
}
|
||||
|
||||
const toPascalCase = (str: string) => {
|
||||
return str
|
||||
.split("-")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join("")
|
||||
}
|
||||
|
||||
const viewportWidths = {
|
||||
desktop: "100%",
|
||||
tablet: "768px",
|
||||
mobile: "375px",
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen max-h-screen gap-4 p-6">
|
||||
{/* Left Sidebar - Block Library */}
|
||||
<div className="w-64 space-y-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">Block Library</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Drag blocks to build your page
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
placeholder="Filter blocks..."
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
/>
|
||||
|
||||
<ScrollArea className="h-[calc(100vh-200px)]">
|
||||
<div className="space-y-4">
|
||||
{filteredBlocks.map((block) => (
|
||||
<Card
|
||||
key={block}
|
||||
className="cursor-grab overflow-hidden transition-colors hover:bg-muted"
|
||||
onClick={() => addBlock(block)}
|
||||
>
|
||||
{/* 1/4 Scale Block Preview */}
|
||||
<div className="relative h-32 overflow-hidden bg-muted/50">
|
||||
<div className="pointer-events-none">
|
||||
<DynamicBlock blockName={block} scale={0.25} />
|
||||
</div>
|
||||
{/* Overlay with block name and add button */}
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-background/0 opacity-0 transition-opacity hover:bg-background/80 hover:opacity-100">
|
||||
<div className="flex items-center gap-2">
|
||||
<Plus className="h-5 w-5" />
|
||||
<span className="text-sm font-medium">Add to page</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t p-2">
|
||||
<p className="truncate text-xs font-medium">{block}</p>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
<Separator orientation="vertical" />
|
||||
|
||||
{/* Center - Page Builder Canvas */}
|
||||
<div className="flex-1 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">Page Builder</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{pageBlocks.length} blocks in page
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Viewport Controls */}
|
||||
<div className="flex rounded-lg border">
|
||||
<Button
|
||||
variant={viewport === "mobile" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setViewport("mobile")}
|
||||
className="rounded-r-none"
|
||||
>
|
||||
Mobile
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewport === "tablet" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setViewport("tablet")}
|
||||
className="rounded-none border-x"
|
||||
>
|
||||
Tablet
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewport === "desktop" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setViewport("desktop")}
|
||||
className="rounded-l-none"
|
||||
>
|
||||
Desktop
|
||||
</Button>
|
||||
</div>
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={copyCode}
|
||||
disabled={pageBlocks.length === 0}
|
||||
>
|
||||
<svg
|
||||
className="mr-2 h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<rect width="13" height="13" x="9" y="9" rx="2" ry="2" />
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
||||
</svg>
|
||||
Copy Code
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={downloadCode}
|
||||
disabled={pageBlocks.length === 0}
|
||||
>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={deployWithHanzo}
|
||||
disabled={pageBlocks.length === 0}
|
||||
>
|
||||
<svg
|
||||
className="mr-2 h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||
</svg>
|
||||
Deploy with Hanzo
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="h-[calc(100vh-140px)] rounded-lg border bg-background">
|
||||
<div className="flex min-h-full items-start justify-center p-4">
|
||||
<div
|
||||
style={{
|
||||
width: viewportWidths[viewport],
|
||||
maxWidth: "100%",
|
||||
transition: "width 0.3s ease",
|
||||
}}
|
||||
>
|
||||
<DndContext
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={pageBlocks.map((b) => b.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="min-h-[600px] bg-background">
|
||||
{pageBlocks.length === 0 ? (
|
||||
<div className="flex h-96 items-center justify-center rounded-lg border border-dashed text-center">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Your page is empty
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Click blocks from the left to add them
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
pageBlocks.map((block) => (
|
||||
<SortableBlock
|
||||
key={block.id}
|
||||
id={block.id}
|
||||
blockName={block.blockName}
|
||||
onRemove={() => removeBlock(block.id)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</SortableContext>
|
||||
|
||||
<DragOverlay>
|
||||
{activeId ? (
|
||||
<div className="rounded-lg border bg-card p-4 shadow-lg">
|
||||
<p className="text-sm font-medium">
|
||||
{pageBlocks.find((b) => b.id === activeId)?.blockName}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SortableBlock({
|
||||
id,
|
||||
blockName,
|
||||
onRemove,
|
||||
}: {
|
||||
id: string
|
||||
blockName: string
|
||||
onRemove: () => void
|
||||
}) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id })
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} className="group relative">
|
||||
<div className="absolute -left-12 top-2 z-10 flex flex-col items-center gap-2">
|
||||
<button
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="cursor-grab rounded bg-card p-1 shadow-sm hover:shadow active:cursor-grabbing"
|
||||
>
|
||||
<GripVertical className="h-4 w-4 text-muted-foreground" />
|
||||
</button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onRemove}
|
||||
className="h-6 w-6 bg-background/80 opacity-0 backdrop-blur transition-opacity group-hover:opacity-100"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Block Preview - No gaps between blocks */}
|
||||
<div className="relative overflow-hidden border-b last:border-b-0">
|
||||
<DynamicBlock blockName={blockName} scale={1} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { ChartDisplay } from "@/components/chart-display"
|
||||
import { Separator } from "@/registry/default/ui/separator"
|
||||
|
||||
import * as Charts from "./charts"
|
||||
|
||||
export function ChartsClient() {
|
||||
return (
|
||||
<div className="container mx-auto py-10">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Charts</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Beautiful charts built with Recharts. Copy and paste into your apps.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="gap-6 md:flex md:flex-row-reverse md:items-start">
|
||||
<div className="grid flex-1 gap-12">
|
||||
<h2 className="sr-only">Examples</h2>
|
||||
<div
|
||||
id="examples"
|
||||
className="grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-area-stacked">
|
||||
<Charts.ChartAreaStacked />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-multiple">
|
||||
<Charts.ChartBarMultiple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay
|
||||
name="chart-pie-donut-text"
|
||||
className="[&_[data-chart]]:xl:max-h-[243px]"
|
||||
>
|
||||
<Charts.ChartPieDonutText />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="area-chart"
|
||||
className="grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-area-default">
|
||||
<Charts.ChartAreaDefault />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-linear">
|
||||
<Charts.ChartAreaLinear />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-step">
|
||||
<Charts.ChartAreaStep />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-stacked">
|
||||
<Charts.ChartAreaStacked />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-stacked-expand">
|
||||
<Charts.ChartAreaStackedExpand />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-legend">
|
||||
<Charts.ChartAreaLegend />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-icons">
|
||||
<Charts.ChartAreaIcons />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-gradient">
|
||||
<Charts.ChartAreaGradient />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-axes">
|
||||
<Charts.ChartAreaAxes />
|
||||
</ChartDisplay>
|
||||
<div className="md:col-span-2 lg:col-span-3">
|
||||
<ChartDisplay name="chart-area-interactive">
|
||||
<Charts.ChartAreaInteractive />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="bar-chart"
|
||||
className="grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-bar-default">
|
||||
<Charts.ChartBarDefault />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-horizontal">
|
||||
<Charts.ChartBarHorizontal />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-multiple">
|
||||
<Charts.ChartBarMultiple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-label">
|
||||
<Charts.ChartBarLabel />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-label-custom">
|
||||
<Charts.ChartBarLabelCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-mixed">
|
||||
<Charts.ChartBarMixed />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-stacked">
|
||||
<Charts.ChartBarStacked />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-active">
|
||||
<Charts.ChartBarActive />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-negative">
|
||||
<Charts.ChartBarNegative />
|
||||
</ChartDisplay>
|
||||
<div className="md:col-span-2 lg:col-span-3">
|
||||
<ChartDisplay name="chart-bar-interactive">
|
||||
<Charts.ChartBarInteractive />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="line-chart"
|
||||
className="grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-line-default">
|
||||
<Charts.ChartLineDefault />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-linear">
|
||||
<Charts.ChartLineLinear />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-step">
|
||||
<Charts.ChartLineStep />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-multiple">
|
||||
<Charts.ChartLineMultiple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-dots">
|
||||
<Charts.ChartLineDots />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-dots-custom">
|
||||
<Charts.ChartLineDotsCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-dots-colors">
|
||||
<Charts.ChartLineDotsColors />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-label">
|
||||
<Charts.ChartLineLabel />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-label-custom">
|
||||
<Charts.ChartLineLabelCustom />
|
||||
</ChartDisplay>
|
||||
<div className="md:col-span-2 lg:col-span-3">
|
||||
<ChartDisplay name="chart-line-interactive">
|
||||
<Charts.ChartLineInteractive />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="pie-chart"
|
||||
className="grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-pie-simple">
|
||||
<Charts.ChartPieSimple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-separator-none">
|
||||
<Charts.ChartPieSeparatorNone />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-label">
|
||||
<Charts.ChartPieLabel />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-label-custom">
|
||||
<Charts.ChartPieLabelCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-label-list">
|
||||
<Charts.ChartPieLabelList />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-legend">
|
||||
<Charts.ChartPieLegend />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-donut">
|
||||
<Charts.ChartPieDonut />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-donut-active">
|
||||
<Charts.ChartPieDonutActive />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-donut-text">
|
||||
<Charts.ChartPieDonutText />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-stacked">
|
||||
<Charts.ChartPieStacked />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-interactive">
|
||||
<Charts.ChartPieInteractive />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="radar-chart"
|
||||
className="grid flex-1 scroll-mt-20 gap-6 md:grid-cols-2 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-radar-default">
|
||||
<Charts.ChartRadarDefault />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-dots">
|
||||
<Charts.ChartRadarDots />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-multiple">
|
||||
<Charts.ChartRadarMultiple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-lines-only">
|
||||
<Charts.ChartRadarLinesOnly />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-label-custom">
|
||||
<Charts.ChartRadarLabelCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-radius">
|
||||
<Charts.ChartRadarRadius />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-custom">
|
||||
<Charts.ChartRadarGridCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-fill">
|
||||
<Charts.ChartRadarGridFill />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-none">
|
||||
<Charts.ChartRadarGridNone />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-circle">
|
||||
<Charts.ChartRadarGridCircle />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-circle-no-lines">
|
||||
<Charts.ChartRadarGridCircleNoLines />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-circle-fill">
|
||||
<Charts.ChartRadarGridCircleFill />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-legend">
|
||||
<Charts.ChartRadarLegend />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-icons">
|
||||
<Charts.ChartRadarIcons />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="radial-chart"
|
||||
className="grid flex-1 scroll-mt-20 gap-6 md:grid-cols-2 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-radial-simple">
|
||||
<Charts.ChartRadialSimple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radial-label">
|
||||
<Charts.ChartRadialLabel />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radial-grid">
|
||||
<Charts.ChartRadialGrid />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radial-text">
|
||||
<Charts.ChartRadialText />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radial-shape">
|
||||
<Charts.ChartRadialShape />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radial-stacked">
|
||||
<Charts.ChartRadialStacked />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="tooltip"
|
||||
className="chart-wrapper grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-tooltip-default">
|
||||
<Charts.ChartTooltipDefault />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-indicator-line">
|
||||
<Charts.ChartTooltipIndicatorLine />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-indicator-none">
|
||||
<Charts.ChartTooltipIndicatorNone />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-label-custom">
|
||||
<Charts.ChartTooltipLabelCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-label-formatter">
|
||||
<Charts.ChartTooltipLabelFormatter />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-label-none">
|
||||
<Charts.ChartTooltipLabelNone />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-formatter">
|
||||
<Charts.ChartTooltipFormatter />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-icons">
|
||||
<Charts.ChartTooltipIcons />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-advanced">
|
||||
<Charts.ChartTooltipAdvanced />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+298
-22
@@ -1,28 +1,304 @@
|
||||
"use client"
|
||||
|
||||
import dynamic from "next/dynamic"
|
||||
import { ChartDisplay } from "@/components/chart-display"
|
||||
import { Separator } from "@/registry/default/ui/separator"
|
||||
|
||||
import { SkeletonCardGrid } from "@/components/skeleton-card"
|
||||
|
||||
// Dynamically import heavy chart components
|
||||
const ChartsClient = dynamic(
|
||||
() => import("./charts-client").then((mod) => mod.ChartsClient),
|
||||
{
|
||||
loading: () => (
|
||||
<div className="container mx-auto py-10">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="h-10 w-48 bg-muted animate-pulse rounded" />
|
||||
<div className="h-6 w-96 bg-muted animate-pulse rounded mt-2" />
|
||||
</div>
|
||||
<SkeletonCardGrid count={9} />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
ssr: false, // Charts are interactive with Recharts
|
||||
}
|
||||
)
|
||||
import * as Charts from "./charts"
|
||||
|
||||
export default function ChartsPage() {
|
||||
return <ChartsClient />
|
||||
return (
|
||||
<div className="container mx-auto py-10">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Charts</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Beautiful charts built with Recharts. Copy and paste into your apps.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="gap-6 md:flex md:flex-row-reverse md:items-start">
|
||||
<div className="grid flex-1 gap-12">
|
||||
<h2 className="sr-only">Examples</h2>
|
||||
<div
|
||||
id="examples"
|
||||
className="grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-area-stacked">
|
||||
<Charts.ChartAreaStacked />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-multiple">
|
||||
<Charts.ChartBarMultiple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay
|
||||
name="chart-pie-donut-text"
|
||||
className="[&_[data-chart]]:xl:max-h-[243px]"
|
||||
>
|
||||
<Charts.ChartPieDonutText />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="area-chart"
|
||||
className="grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-area-default">
|
||||
<Charts.ChartAreaDefault />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-linear">
|
||||
<Charts.ChartAreaLinear />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-step">
|
||||
<Charts.ChartAreaStep />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-stacked">
|
||||
<Charts.ChartAreaStacked />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-stacked-expand">
|
||||
<Charts.ChartAreaStackedExpand />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-legend">
|
||||
<Charts.ChartAreaLegend />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-icons">
|
||||
<Charts.ChartAreaIcons />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-gradient">
|
||||
<Charts.ChartAreaGradient />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-axes">
|
||||
<Charts.ChartAreaAxes />
|
||||
</ChartDisplay>
|
||||
<div className="md:col-span-2 lg:col-span-3">
|
||||
<ChartDisplay name="chart-area-interactive">
|
||||
<Charts.ChartAreaInteractive />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="bar-chart"
|
||||
className="grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-bar-default">
|
||||
<Charts.ChartBarDefault />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-horizontal">
|
||||
<Charts.ChartBarHorizontal />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-multiple">
|
||||
<Charts.ChartBarMultiple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-label">
|
||||
<Charts.ChartBarLabel />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-label-custom">
|
||||
<Charts.ChartBarLabelCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-mixed">
|
||||
<Charts.ChartBarMixed />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-stacked">
|
||||
<Charts.ChartBarStacked />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-active">
|
||||
<Charts.ChartBarActive />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-negative">
|
||||
<Charts.ChartBarNegative />
|
||||
</ChartDisplay>
|
||||
<div className="md:col-span-2 lg:col-span-3">
|
||||
<ChartDisplay name="chart-bar-interactive">
|
||||
<Charts.ChartBarInteractive />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="line-chart"
|
||||
className="grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-line-default">
|
||||
<Charts.ChartLineDefault />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-linear">
|
||||
<Charts.ChartLineLinear />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-step">
|
||||
<Charts.ChartLineStep />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-multiple">
|
||||
<Charts.ChartLineMultiple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-dots">
|
||||
<Charts.ChartLineDots />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-dots-custom">
|
||||
<Charts.ChartLineDotsCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-dots-colors">
|
||||
<Charts.ChartLineDotsColors />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-label">
|
||||
<Charts.ChartLineLabel />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-label-custom">
|
||||
<Charts.ChartLineLabelCustom />
|
||||
</ChartDisplay>
|
||||
<div className="md:col-span-2 lg:col-span-3">
|
||||
<ChartDisplay name="chart-line-interactive">
|
||||
<Charts.ChartLineInteractive />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="pie-chart"
|
||||
className="grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-pie-simple">
|
||||
<Charts.ChartPieSimple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-separator-none">
|
||||
<Charts.ChartPieSeparatorNone />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-label">
|
||||
<Charts.ChartPieLabel />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-label-custom">
|
||||
<Charts.ChartPieLabelCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-label-list">
|
||||
<Charts.ChartPieLabelList />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-legend">
|
||||
<Charts.ChartPieLegend />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-donut">
|
||||
<Charts.ChartPieDonut />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-donut-active">
|
||||
<Charts.ChartPieDonutActive />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-donut-text">
|
||||
<Charts.ChartPieDonutText />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-stacked">
|
||||
<Charts.ChartPieStacked />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-interactive">
|
||||
<Charts.ChartPieInteractive />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="radar-chart"
|
||||
className="grid flex-1 scroll-mt-20 gap-6 md:grid-cols-2 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-radar-default">
|
||||
<Charts.ChartRadarDefault />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-dots">
|
||||
<Charts.ChartRadarDots />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-multiple">
|
||||
<Charts.ChartRadarMultiple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-lines-only">
|
||||
<Charts.ChartRadarLinesOnly />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-label-custom">
|
||||
<Charts.ChartRadarLabelCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-radius">
|
||||
<Charts.ChartRadarRadius />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-custom">
|
||||
<Charts.ChartRadarGridCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-fill">
|
||||
<Charts.ChartRadarGridFill />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-none">
|
||||
<Charts.ChartRadarGridNone />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-circle">
|
||||
<Charts.ChartRadarGridCircle />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-circle-no-lines">
|
||||
<Charts.ChartRadarGridCircleNoLines />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-circle-fill">
|
||||
<Charts.ChartRadarGridCircleFill />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-legend">
|
||||
<Charts.ChartRadarLegend />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-icons">
|
||||
<Charts.ChartRadarIcons />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="radial-chart"
|
||||
className="grid flex-1 scroll-mt-20 gap-6 md:grid-cols-2 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-radial-simple">
|
||||
<Charts.ChartRadialSimple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radial-label">
|
||||
<Charts.ChartRadialLabel />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radial-grid">
|
||||
<Charts.ChartRadialGrid />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radial-text">
|
||||
<Charts.ChartRadialText />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radial-shape">
|
||||
<Charts.ChartRadialShape />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radial-stacked">
|
||||
<Charts.ChartRadialStacked />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="tooltip"
|
||||
className="chart-wrapper grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-tooltip-default">
|
||||
<Charts.ChartTooltipDefault />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-indicator-line">
|
||||
<Charts.ChartTooltipIndicatorLine />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-indicator-none">
|
||||
<Charts.ChartTooltipIndicatorNone />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-label-custom">
|
||||
<Charts.ChartTooltipLabelCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-label-formatter">
|
||||
<Charts.ChartTooltipLabelFormatter />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-label-none">
|
||||
<Charts.ChartTooltipLabelNone />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-formatter">
|
||||
<Charts.ChartTooltipFormatter />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-icons">
|
||||
<Charts.ChartTooltipIcons />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-advanced">
|
||||
<Charts.ChartTooltipAdvanced />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,606 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
addEdge,
|
||||
Background,
|
||||
Connection,
|
||||
Controls,
|
||||
Edge,
|
||||
MiniMap,
|
||||
Node,
|
||||
NodeTypes,
|
||||
Panel,
|
||||
ReactFlow,
|
||||
useEdgesState,
|
||||
useNodesState,
|
||||
} from "@xyflow/react"
|
||||
import yaml from "js-yaml"
|
||||
// import "@xyflow/react/dist/style.css" // Causes build issues, styles inlined in component
|
||||
import {
|
||||
Download,
|
||||
HardDrive,
|
||||
Library,
|
||||
Network,
|
||||
Plus,
|
||||
Settings2,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Card } from "@/registry/default/ui/card"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
import { ScrollArea } from "@/registry/default/ui/scroll-area"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/default/ui/select"
|
||||
import { Separator } from "@/registry/default/ui/separator"
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/registry/default/ui/sheet"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/default/ui/tabs"
|
||||
|
||||
const COMMON_IMAGES = [
|
||||
{ name: "nginx", image: "nginx:alpine", category: "proxy" },
|
||||
{ name: "postgres", image: "postgres:16-alpine", category: "database" },
|
||||
{ name: "mysql", image: "mysql:8.4", category: "database" },
|
||||
{ name: "mongodb", image: "mongo:7", category: "database" },
|
||||
{ name: "redis", image: "redis:7-alpine", category: "cache" },
|
||||
{ name: "node", image: "node:20-alpine", category: "runtime" },
|
||||
{ name: "php", image: "php:8.3-fpm-alpine", category: "runtime" },
|
||||
{ name: "ruby", image: "ruby:3.2-alpine", category: "runtime" },
|
||||
{ name: "python", image: "python:3.12-alpine", category: "runtime" },
|
||||
{
|
||||
name: "clickhouse",
|
||||
image: "clickhouse/clickhouse-server:latest",
|
||||
category: "analytics",
|
||||
},
|
||||
{
|
||||
name: "kafka",
|
||||
image: "confluentinc/cp-kafka:latest",
|
||||
category: "streaming",
|
||||
},
|
||||
{ name: "grafana", image: "grafana/grafana:latest", category: "monitoring" },
|
||||
]
|
||||
|
||||
interface ComposeService {
|
||||
image: string
|
||||
ports?: string[]
|
||||
environment?: Record<string, string>
|
||||
networks?: string[]
|
||||
volumes?: string[]
|
||||
depends_on?: string[]
|
||||
}
|
||||
|
||||
function ServiceNode({ data }: { data: any }) {
|
||||
return (
|
||||
<div className="rounded-lg border-2 bg-card p-4 shadow-lg min-w-[200px]">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="rounded bg-primary/10 p-2">
|
||||
<Settings2 className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-semibold">{data.name}</div>
|
||||
<div className="text-xs text-muted-foreground">{data.image}</div>
|
||||
</div>
|
||||
</div>
|
||||
{data.ports && (
|
||||
<div className="mt-2 text-xs text-muted-foreground">
|
||||
📡 {data.ports[0]}
|
||||
</div>
|
||||
)}
|
||||
{data.networks && (
|
||||
<div className="mt-1 flex gap-1">
|
||||
{data.networks.map((net: string) => (
|
||||
<span
|
||||
key={net}
|
||||
className="rounded bg-muted px-1.5 py-0.5 text-[10px]"
|
||||
>
|
||||
{net}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const nodeTypes: NodeTypes = {
|
||||
service: ServiceNode,
|
||||
}
|
||||
|
||||
export function ComposeClient() {
|
||||
const [projectName, setProjectName] = React.useState("my-stack")
|
||||
const [services, setServices] = React.useState<
|
||||
Record<string, ComposeService>
|
||||
>({})
|
||||
const [networks, setNetworks] = React.useState<string[]>(["default"])
|
||||
const [volumes, setVolumes] = React.useState<string[]>([])
|
||||
const [selectedService, setSelectedService] = React.useState<string | null>(
|
||||
null
|
||||
)
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
|
||||
|
||||
// Update nodes when services change
|
||||
React.useEffect(() => {
|
||||
const newNodes: Node[] = Object.entries(services).map(
|
||||
([name, config], i) => ({
|
||||
id: name,
|
||||
type: "service",
|
||||
position: { x: 100 + (i % 4) * 250, y: 100 + Math.floor(i / 4) * 150 },
|
||||
data: { name, ...config },
|
||||
})
|
||||
)
|
||||
|
||||
const newEdges: Edge[] = []
|
||||
Object.entries(services).forEach(([name, config]) => {
|
||||
config.depends_on?.forEach((dep) => {
|
||||
newEdges.push({
|
||||
id: `${dep}-${name}`,
|
||||
source: dep,
|
||||
target: name,
|
||||
animated: true,
|
||||
style: { stroke: "hsl(var(--primary))" },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
setNodes(newNodes)
|
||||
setEdges(newEdges)
|
||||
}, [services, setNodes, setEdges])
|
||||
|
||||
const addService = (template: (typeof COMMON_IMAGES)[0]) => {
|
||||
const serviceName = `${template.name}_${Object.keys(services).length + 1}`
|
||||
setServices({
|
||||
...services,
|
||||
[serviceName]: {
|
||||
image: template.image,
|
||||
networks: ["default"],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const onConnect = React.useCallback(
|
||||
(params: Connection) => {
|
||||
// Add dependency when connecting nodes
|
||||
if (params.source && params.target) {
|
||||
setServices((prev) => ({
|
||||
...prev,
|
||||
[params.target!]: {
|
||||
...prev[params.target!],
|
||||
depends_on: [
|
||||
...(prev[params.target!]?.depends_on || []),
|
||||
params.source!,
|
||||
],
|
||||
},
|
||||
}))
|
||||
}
|
||||
setEdges((eds) => addEdge(params, eds))
|
||||
},
|
||||
[setEdges]
|
||||
)
|
||||
|
||||
const generateYAML = () => {
|
||||
const compose = {
|
||||
version: "3.8",
|
||||
services,
|
||||
networks: networks.reduce((acc, net) => ({ ...acc, [net]: {} }), {}),
|
||||
volumes: volumes.reduce((acc, vol) => ({ ...acc, [vol]: {} }), {}),
|
||||
}
|
||||
return yaml.dump(compose, { indent: 2 })
|
||||
}
|
||||
|
||||
const downloadCompose = () => {
|
||||
const yamlContent = generateYAML()
|
||||
const blob = new Blob([yamlContent], { type: "text/yaml" })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = "docker-compose.yml"
|
||||
a.click()
|
||||
}
|
||||
|
||||
const updateServiceConfig = (
|
||||
serviceName: string,
|
||||
key: string,
|
||||
value: any
|
||||
) => {
|
||||
setServices((prev) => ({
|
||||
...prev,
|
||||
[serviceName]: {
|
||||
...prev[serviceName],
|
||||
[key]: value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
{/* Main Canvas */}
|
||||
<div className="flex-1">
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Top Toolbar */}
|
||||
<div className="flex items-center justify-between border-b bg-card p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-xl font-semibold">Docker Compose Editor</h1>
|
||||
<span className="rounded bg-muted px-2 py-1 text-xs">
|
||||
{Object.keys(services).length} services
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setServices({})
|
||||
setNetworks(["default"])
|
||||
setVolumes([])
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const input = document.createElement("input")
|
||||
input.type = "file"
|
||||
input.accept = ".yml,.yaml"
|
||||
input.onchange = (e: any) => {
|
||||
const file = e.target.files[0]
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
const content = e.target?.result as string
|
||||
const parsed = yaml.load(content) as any
|
||||
setServices(parsed.services || {})
|
||||
setNetworks(
|
||||
Object.keys(parsed.networks || { default: {} })
|
||||
)
|
||||
setVolumes(Object.keys(parsed.volumes || {}))
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
input.click()
|
||||
}}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button size="sm">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add service
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Add Service</SheetTitle>
|
||||
</SheetHeader>
|
||||
<ScrollArea className="mt-4 h-[calc(100vh-100px)]">
|
||||
<div className="space-y-2">
|
||||
{COMMON_IMAGES.map((template) => (
|
||||
<Button
|
||||
key={template.image}
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
onClick={() => addService(template)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
<div className="flex-1 text-left">
|
||||
<div className="font-medium">{template.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{template.image}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Canvas */}
|
||||
<div className="flex-1 bg-background">
|
||||
{Object.keys(services).length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Settings2 className="mx-auto h-12 w-12 text-muted-foreground/50" />
|
||||
<h3 className="mt-4 text-lg font-semibold">
|
||||
No services yet
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Click "Add service" to start building your stack
|
||||
</p>
|
||||
<Button
|
||||
className="mt-4"
|
||||
onClick={() => addService(COMMON_IMAGES[1])}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add your first service
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodeClick={(_, node) => setSelectedService(node.id)}
|
||||
fitView
|
||||
className="bg-background"
|
||||
>
|
||||
<Background
|
||||
gap={16}
|
||||
color="hsl(var(--muted-foreground) / 0.3)"
|
||||
/>
|
||||
<Controls className="border-border bg-card" />
|
||||
<Panel position="top-right" className="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setNetworks([...networks, `network_${networks.length}`])
|
||||
}
|
||||
>
|
||||
<Network className="mr-2 h-4 w-4" />
|
||||
Add network
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setVolumes([...volumes, `vol_${volumes.length}`])
|
||||
}
|
||||
>
|
||||
<HardDrive className="mr-2 h-4 w-4" />
|
||||
Add volume
|
||||
</Button>
|
||||
</Panel>
|
||||
</ReactFlow>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Sidebar - Configuration */}
|
||||
<div className="flex w-80 flex-col border-l bg-card">
|
||||
<div className="border-b p-4">
|
||||
<h2 className="text-lg font-semibold">Compose</h2>
|
||||
<div className="mt-4 space-y-2">
|
||||
<Label htmlFor="project-name">Project Name</Label>
|
||||
<Input
|
||||
id="project-name"
|
||||
value={projectName}
|
||||
onChange={(e) => setProjectName(e.target.value)}
|
||||
placeholder="my-stack"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="services" className="flex-1">
|
||||
<TabsList className="w-full rounded-none">
|
||||
<TabsTrigger value="services" className="flex-1">
|
||||
<Settings2 className="mr-2 h-4 w-4" />
|
||||
Services
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="config" className="flex-1">
|
||||
<Network className="mr-2 h-4 w-4" />
|
||||
Config
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="services" className="mt-0 flex-1">
|
||||
<ScrollArea className="h-[calc(100vh-220px)]">
|
||||
<div className="space-y-2 p-4">
|
||||
{selectedService ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-semibold">{selectedService}</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedService(null)}
|
||||
>
|
||||
✕
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Image</Label>
|
||||
<Input
|
||||
value={services[selectedService]?.image || ""}
|
||||
onChange={(e) =>
|
||||
updateServiceConfig(
|
||||
selectedService,
|
||||
"image",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Ports</Label>
|
||||
<Input
|
||||
placeholder="80:80"
|
||||
onBlur={(e) => {
|
||||
if (e.target.value) {
|
||||
updateServiceConfig(selectedService, "ports", [
|
||||
e.target.value,
|
||||
])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Networks</Label>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
updateServiceConfig(selectedService, "networks", [
|
||||
value,
|
||||
])
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select network" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{networks.map((net) => (
|
||||
<SelectItem key={net} value={net}>
|
||||
{net}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
Click a service node to configure
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="config" className="mt-0 flex-1">
|
||||
<div className="space-y-4 p-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Networks</Label>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const name = prompt("Network name:")
|
||||
if (name) setNetworks([...networks, name])
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-2 space-y-1">
|
||||
{networks.map((net) => (
|
||||
<div
|
||||
key={net}
|
||||
className="rounded bg-muted px-2 py-1 text-sm"
|
||||
>
|
||||
{net}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Volumes</Label>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const name = prompt("Volume name:")
|
||||
if (name) setVolumes([...volumes, name])
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-2 space-y-1">
|
||||
{volumes.map((vol) => (
|
||||
<div
|
||||
key={vol}
|
||||
className="rounded bg-muted px-2 py-1 text-sm"
|
||||
>
|
||||
{vol}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2 p-4">
|
||||
<Button onClick={downloadCompose} className="w-full">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download compose
|
||||
</Button>
|
||||
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline" className="w-full">
|
||||
<Library className="mr-2 h-4 w-4" />
|
||||
Library
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Service Library</SheetTitle>
|
||||
</SheetHeader>
|
||||
<ScrollArea className="mt-4 h-[calc(100vh-100px)]">
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
"proxy",
|
||||
"database",
|
||||
"cache",
|
||||
"runtime",
|
||||
"analytics",
|
||||
"streaming",
|
||||
"monitoring",
|
||||
].map((category) => (
|
||||
<div key={category}>
|
||||
<h4 className="mb-2 text-sm font-semibold capitalize">
|
||||
{category}
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{COMMON_IMAGES.filter(
|
||||
(img) => img.category === category
|
||||
).map((template) => (
|
||||
<Button
|
||||
key={template.image}
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
onClick={() => {
|
||||
addService(template)
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{template.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+596
-19
@@ -1,29 +1,606 @@
|
||||
"use client"
|
||||
|
||||
import dynamic from "next/dynamic"
|
||||
import * as React from "react"
|
||||
import {
|
||||
addEdge,
|
||||
Background,
|
||||
Connection,
|
||||
Controls,
|
||||
Edge,
|
||||
MiniMap,
|
||||
Node,
|
||||
NodeTypes,
|
||||
Panel,
|
||||
ReactFlow,
|
||||
useEdgesState,
|
||||
useNodesState,
|
||||
} from "@xyflow/react"
|
||||
import yaml from "js-yaml"
|
||||
// import "@xyflow/react/dist/style.css" // Causes build issues, styles inlined in component
|
||||
import {
|
||||
Download,
|
||||
HardDrive,
|
||||
Library,
|
||||
Network,
|
||||
Plus,
|
||||
Settings2,
|
||||
} from "lucide-react"
|
||||
|
||||
import { SkeletonCard } from "@/components/skeleton-card"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Card } from "@/registry/default/ui/card"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
import { ScrollArea } from "@/registry/default/ui/scroll-area"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/default/ui/select"
|
||||
import { Separator } from "@/registry/default/ui/separator"
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/registry/default/ui/sheet"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/default/ui/tabs"
|
||||
|
||||
// Dynamically import ReactFlow and heavy compose editor
|
||||
const ComposeClient = dynamic(
|
||||
() => import("./compose-client").then((mod) => mod.ComposeClient),
|
||||
const COMMON_IMAGES = [
|
||||
{ name: "nginx", image: "nginx:alpine", category: "proxy" },
|
||||
{ name: "postgres", image: "postgres:16-alpine", category: "database" },
|
||||
{ name: "mysql", image: "mysql:8.4", category: "database" },
|
||||
{ name: "mongodb", image: "mongo:7", category: "database" },
|
||||
{ name: "redis", image: "redis:7-alpine", category: "cache" },
|
||||
{ name: "node", image: "node:20-alpine", category: "runtime" },
|
||||
{ name: "php", image: "php:8.3-fpm-alpine", category: "runtime" },
|
||||
{ name: "ruby", image: "ruby:3.2-alpine", category: "runtime" },
|
||||
{ name: "python", image: "python:3.12-alpine", category: "runtime" },
|
||||
{
|
||||
loading: () => (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="space-y-4 w-full max-w-md">
|
||||
<div className="h-8 w-64 bg-muted animate-pulse rounded" />
|
||||
<SkeletonCard className="h-[400px]" />
|
||||
<div className="flex gap-2">
|
||||
<div className="h-10 flex-1 bg-muted animate-pulse rounded" />
|
||||
<div className="h-10 flex-1 bg-muted animate-pulse rounded" />
|
||||
name: "clickhouse",
|
||||
image: "clickhouse/clickhouse-server:latest",
|
||||
category: "analytics",
|
||||
},
|
||||
{
|
||||
name: "kafka",
|
||||
image: "confluentinc/cp-kafka:latest",
|
||||
category: "streaming",
|
||||
},
|
||||
{ name: "grafana", image: "grafana/grafana:latest", category: "monitoring" },
|
||||
]
|
||||
|
||||
interface ComposeService {
|
||||
image: string
|
||||
ports?: string[]
|
||||
environment?: Record<string, string>
|
||||
networks?: string[]
|
||||
volumes?: string[]
|
||||
depends_on?: string[]
|
||||
}
|
||||
|
||||
function ServiceNode({ data }: { data: any }) {
|
||||
return (
|
||||
<div className="rounded-lg border-2 bg-card p-4 shadow-lg min-w-[200px]">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="rounded bg-primary/10 p-2">
|
||||
<Settings2 className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-semibold">{data.name}</div>
|
||||
<div className="text-xs text-muted-foreground">{data.image}</div>
|
||||
</div>
|
||||
</div>
|
||||
{data.ports && (
|
||||
<div className="mt-2 text-xs text-muted-foreground">
|
||||
📡 {data.ports[0]}
|
||||
</div>
|
||||
)}
|
||||
{data.networks && (
|
||||
<div className="mt-1 flex gap-1">
|
||||
{data.networks.map((net: string) => (
|
||||
<span
|
||||
key={net}
|
||||
className="rounded bg-muted px-1.5 py-0.5 text-[10px]"
|
||||
>
|
||||
{net}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const nodeTypes: NodeTypes = {
|
||||
service: ServiceNode,
|
||||
}
|
||||
|
||||
export default function ComposeSpecPage() {
|
||||
const [projectName, setProjectName] = React.useState("my-stack")
|
||||
const [services, setServices] = React.useState<
|
||||
Record<string, ComposeService>
|
||||
>({})
|
||||
const [networks, setNetworks] = React.useState<string[]>(["default"])
|
||||
const [volumes, setVolumes] = React.useState<string[]>([])
|
||||
const [selectedService, setSelectedService] = React.useState<string | null>(
|
||||
null
|
||||
)
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
|
||||
|
||||
// Update nodes when services change
|
||||
React.useEffect(() => {
|
||||
const newNodes: Node[] = Object.entries(services).map(
|
||||
([name, config], i) => ({
|
||||
id: name,
|
||||
type: "service",
|
||||
position: { x: 100 + (i % 4) * 250, y: 100 + Math.floor(i / 4) * 150 },
|
||||
data: { name, ...config },
|
||||
})
|
||||
)
|
||||
|
||||
const newEdges: Edge[] = []
|
||||
Object.entries(services).forEach(([name, config]) => {
|
||||
config.depends_on?.forEach((dep) => {
|
||||
newEdges.push({
|
||||
id: `${dep}-${name}`,
|
||||
source: dep,
|
||||
target: name,
|
||||
animated: true,
|
||||
style: { stroke: "hsl(var(--primary))" },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
setNodes(newNodes)
|
||||
setEdges(newEdges)
|
||||
}, [services, setNodes, setEdges])
|
||||
|
||||
const addService = (template: (typeof COMMON_IMAGES)[0]) => {
|
||||
const serviceName = `${template.name}_${Object.keys(services).length + 1}`
|
||||
setServices({
|
||||
...services,
|
||||
[serviceName]: {
|
||||
image: template.image,
|
||||
networks: ["default"],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const onConnect = React.useCallback(
|
||||
(params: Connection) => {
|
||||
// Add dependency when connecting nodes
|
||||
if (params.source && params.target) {
|
||||
setServices((prev) => ({
|
||||
...prev,
|
||||
[params.target!]: {
|
||||
...prev[params.target!],
|
||||
depends_on: [
|
||||
...(prev[params.target!]?.depends_on || []),
|
||||
params.source!,
|
||||
],
|
||||
},
|
||||
}))
|
||||
}
|
||||
setEdges((eds) => addEdge(params, eds))
|
||||
},
|
||||
[setEdges]
|
||||
)
|
||||
|
||||
const generateYAML = () => {
|
||||
const compose = {
|
||||
version: "3.8",
|
||||
services,
|
||||
networks: networks.reduce((acc, net) => ({ ...acc, [net]: {} }), {}),
|
||||
volumes: volumes.reduce((acc, vol) => ({ ...acc, [vol]: {} }), {}),
|
||||
}
|
||||
return yaml.dump(compose, { indent: 2 })
|
||||
}
|
||||
|
||||
const downloadCompose = () => {
|
||||
const yamlContent = generateYAML()
|
||||
const blob = new Blob([yamlContent], { type: "text/yaml" })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = "docker-compose.yml"
|
||||
a.click()
|
||||
}
|
||||
|
||||
const updateServiceConfig = (
|
||||
serviceName: string,
|
||||
key: string,
|
||||
value: any
|
||||
) => {
|
||||
setServices((prev) => ({
|
||||
...prev,
|
||||
[serviceName]: {
|
||||
...prev[serviceName],
|
||||
[key]: value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
{/* Main Canvas */}
|
||||
<div className="flex-1">
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Top Toolbar */}
|
||||
<div className="flex items-center justify-between border-b bg-card p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-xl font-semibold">Docker Compose Editor</h1>
|
||||
<span className="rounded bg-muted px-2 py-1 text-xs">
|
||||
{Object.keys(services).length} services
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setServices({})
|
||||
setNetworks(["default"])
|
||||
setVolumes([])
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const input = document.createElement("input")
|
||||
input.type = "file"
|
||||
input.accept = ".yml,.yaml"
|
||||
input.onchange = (e: any) => {
|
||||
const file = e.target.files[0]
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
const content = e.target?.result as string
|
||||
const parsed = yaml.load(content) as any
|
||||
setServices(parsed.services || {})
|
||||
setNetworks(
|
||||
Object.keys(parsed.networks || { default: {} })
|
||||
)
|
||||
setVolumes(Object.keys(parsed.volumes || {}))
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
input.click()
|
||||
}}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button size="sm">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add service
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Add Service</SheetTitle>
|
||||
</SheetHeader>
|
||||
<ScrollArea className="mt-4 h-[calc(100vh-100px)]">
|
||||
<div className="space-y-2">
|
||||
{COMMON_IMAGES.map((template) => (
|
||||
<Button
|
||||
key={template.image}
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
onClick={() => addService(template)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
<div className="flex-1 text-left">
|
||||
<div className="font-medium">{template.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{template.image}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Canvas */}
|
||||
<div className="flex-1 bg-background">
|
||||
{Object.keys(services).length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Settings2 className="mx-auto h-12 w-12 text-muted-foreground/50" />
|
||||
<h3 className="mt-4 text-lg font-semibold">
|
||||
No services yet
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Click "Add service" to start building your stack
|
||||
</p>
|
||||
<Button
|
||||
className="mt-4"
|
||||
onClick={() => addService(COMMON_IMAGES[1])}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add your first service
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodeClick={(_, node) => setSelectedService(node.id)}
|
||||
fitView
|
||||
className="bg-background"
|
||||
>
|
||||
<Background
|
||||
gap={16}
|
||||
color="hsl(var(--muted-foreground) / 0.3)"
|
||||
/>
|
||||
<Controls className="border-border bg-card" />
|
||||
<Panel position="top-right" className="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setNetworks([...networks, `network_${networks.length}`])
|
||||
}
|
||||
>
|
||||
<Network className="mr-2 h-4 w-4" />
|
||||
Add network
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setVolumes([...volumes, `vol_${volumes.length}`])
|
||||
}
|
||||
>
|
||||
<HardDrive className="mr-2 h-4 w-4" />
|
||||
Add volume
|
||||
</Button>
|
||||
</Panel>
|
||||
</ReactFlow>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
ssr: false, // ReactFlow requires client-side rendering
|
||||
}
|
||||
)
|
||||
|
||||
export default function ComposePage() {
|
||||
return <ComposeClient />
|
||||
{/* Right Sidebar - Configuration */}
|
||||
<div className="flex w-80 flex-col border-l bg-card">
|
||||
<div className="border-b p-4">
|
||||
<h2 className="text-lg font-semibold">Compose</h2>
|
||||
<div className="mt-4 space-y-2">
|
||||
<Label htmlFor="project-name">Project Name</Label>
|
||||
<Input
|
||||
id="project-name"
|
||||
value={projectName}
|
||||
onChange={(e) => setProjectName(e.target.value)}
|
||||
placeholder="my-stack"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="services" className="flex-1">
|
||||
<TabsList className="w-full rounded-none">
|
||||
<TabsTrigger value="services" className="flex-1">
|
||||
<Settings2 className="mr-2 h-4 w-4" />
|
||||
Services
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="config" className="flex-1">
|
||||
<Network className="mr-2 h-4 w-4" />
|
||||
Config
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="services" className="mt-0 flex-1">
|
||||
<ScrollArea className="h-[calc(100vh-220px)]">
|
||||
<div className="space-y-2 p-4">
|
||||
{selectedService ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-semibold">{selectedService}</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedService(null)}
|
||||
>
|
||||
✕
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Image</Label>
|
||||
<Input
|
||||
value={services[selectedService]?.image || ""}
|
||||
onChange={(e) =>
|
||||
updateServiceConfig(
|
||||
selectedService,
|
||||
"image",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Ports</Label>
|
||||
<Input
|
||||
placeholder="80:80"
|
||||
onBlur={(e) => {
|
||||
if (e.target.value) {
|
||||
updateServiceConfig(selectedService, "ports", [
|
||||
e.target.value,
|
||||
])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Networks</Label>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
updateServiceConfig(selectedService, "networks", [
|
||||
value,
|
||||
])
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select network" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{networks.map((net) => (
|
||||
<SelectItem key={net} value={net}>
|
||||
{net}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
Click a service node to configure
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="config" className="mt-0 flex-1">
|
||||
<div className="space-y-4 p-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Networks</Label>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const name = prompt("Network name:")
|
||||
if (name) setNetworks([...networks, name])
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-2 space-y-1">
|
||||
{networks.map((net) => (
|
||||
<div
|
||||
key={net}
|
||||
className="rounded bg-muted px-2 py-1 text-sm"
|
||||
>
|
||||
{net}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Volumes</Label>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const name = prompt("Volume name:")
|
||||
if (name) setVolumes([...volumes, name])
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-2 space-y-1">
|
||||
{volumes.map((vol) => (
|
||||
<div
|
||||
key={vol}
|
||||
className="rounded bg-muted px-2 py-1 text-sm"
|
||||
>
|
||||
{vol}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2 p-4">
|
||||
<Button onClick={downloadCompose} className="w-full">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download compose
|
||||
</Button>
|
||||
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline" className="w-full">
|
||||
<Library className="mr-2 h-4 w-4" />
|
||||
Library
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Service Library</SheetTitle>
|
||||
</SheetHeader>
|
||||
<ScrollArea className="mt-4 h-[calc(100vh-100px)]">
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
"proxy",
|
||||
"database",
|
||||
"cache",
|
||||
"runtime",
|
||||
"analytics",
|
||||
"streaming",
|
||||
"monitoring",
|
||||
].map((category) => (
|
||||
<div key={category}>
|
||||
<h4 className="mb-2 text-sm font-semibold capitalize">
|
||||
{category}
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{COMMON_IMAGES.filter(
|
||||
(img) => img.category === category
|
||||
).map((template) => (
|
||||
<Button
|
||||
key={template.image}
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
onClick={() => {
|
||||
addService(template)
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{template.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import Link from "next/link"
|
||||
import { notFound } from "next/navigation"
|
||||
import { mdxComponents } from "@/mdx-components"
|
||||
import { findNeighbour } from "@hanzo/docs-core/page-tree"
|
||||
import {
|
||||
IconArrowLeft,
|
||||
IconArrowRight,
|
||||
IconArrowUpRight,
|
||||
} from "@tabler/icons-react"
|
||||
import { findNeighbour } from "fumadocs-core/server"
|
||||
|
||||
import { source } from "@/lib/source"
|
||||
import { absoluteUrl } from "@/lib/utils"
|
||||
@@ -180,8 +180,10 @@ export default async function Page(props: {
|
||||
</div>
|
||||
<div className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[calc(100svh-var(--footer-height)+2rem)] w-72 flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex">
|
||||
<div className="h-(--top-spacing) shrink-0" />
|
||||
{/* @ts-expect-error - revisit fumadocs types. */}
|
||||
{page.data.toc?.length ? (
|
||||
<div className="no-scrollbar overflow-y-auto px-8">
|
||||
{/* @ts-expect-error - revisit fumadocs types. */}
|
||||
<DocsTableOfContents toc={page.data.toc} />
|
||||
<div className="h-12" />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { docsConfig } from "@/config/docs"
|
||||
import { DocsNav as DocsSidebarNav } from "@/components/sidebar-nav"
|
||||
import { DocsSidebarNav } from "@/components/sidebar-nav"
|
||||
import { ScrollArea } from "@/registry/default/ui/scroll-area"
|
||||
|
||||
interface DocsLayoutProps {
|
||||
@@ -12,7 +12,7 @@ export default function DocsLayout({ children }: DocsLayoutProps) {
|
||||
<div className="container flex-1 items-start md:grid md:grid-cols-[220px_minmax(0,1fr)] md:gap-6 lg:grid-cols-[240px_minmax(0,1fr)] lg:gap-10">
|
||||
<aside className="fixed top-14 z-30 hidden h-[calc(100vh-3.5rem)] w-full shrink-0 md:sticky md:block">
|
||||
<ScrollArea className="h-full py-6 px-6 lg:py-8 lg:pl-4 lg:pr-6">
|
||||
<DocsSidebarNav config={docsConfig} />
|
||||
<DocsSidebarNav items={docsConfig.sidebarNav} />
|
||||
</ScrollArea>
|
||||
</aside>
|
||||
{children}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { ChevronDown, Circle, Plus, Star } from "lucide-react"
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
CircleIcon,
|
||||
PlusIcon,
|
||||
StarIcon,
|
||||
} from "@radix-ui/react-icons"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
@@ -32,14 +37,14 @@ export function DemoGithub() {
|
||||
</div>
|
||||
<div className="flex items-center space-x-1 rounded-md bg-secondary text-secondary-foreground">
|
||||
<Button variant="secondary" className="px-3 shadow-none">
|
||||
<Star />
|
||||
<StarIcon className="mr-2 h-4 w-4" />
|
||||
Star
|
||||
</Button>
|
||||
<Separator orientation="vertical" className="h-[20px]" />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="secondary" className="px-2 shadow-none">
|
||||
<ChevronDown className="text-secondary-foreground" />
|
||||
<ChevronDownIcon className="h-4 w-4 text-secondary-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
@@ -57,7 +62,7 @@ export function DemoGithub() {
|
||||
<DropdownMenuCheckboxItem>Inspiration</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Plus /> Create List
|
||||
<PlusIcon className="mr-2 h-4 w-4" /> Create List
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -66,11 +71,11 @@ export function DemoGithub() {
|
||||
<CardContent>
|
||||
<div className="flex space-x-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center">
|
||||
<Circle className="mr-1 h-3 w-3 fill-sky-400 text-sky-400" />
|
||||
<CircleIcon className="mr-1 h-3 w-3 fill-sky-400 text-sky-400" />
|
||||
TypeScript
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Star className="mr-1 h-3 w-3" />
|
||||
<StarIcon className="mr-1 h-3 w-3" />
|
||||
20k
|
||||
</div>
|
||||
<div>Updated April 2023</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Bell, EyeOff, User } from "lucide-react"
|
||||
import { BellIcon, EyeNoneIcon, PersonIcon } from "@radix-ui/react-icons"
|
||||
|
||||
import {
|
||||
Card,
|
||||
@@ -19,7 +19,7 @@ export function DemoNotifications() {
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-1">
|
||||
<div className="-mx-2 flex items-start space-x-4 rounded-md p-2 transition-all hover:bg-accent hover:text-accent-foreground">
|
||||
<Bell className="mt-px h-5 w-5" />
|
||||
<BellIcon className="mt-px h-5 w-5" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium leading-none">Everything</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@@ -28,7 +28,7 @@ export function DemoNotifications() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="-mx-2 flex items-start space-x-4 rounded-md bg-accent p-2 text-accent-foreground transition-all">
|
||||
<User className="mt-px h-5 w-5" />
|
||||
<PersonIcon className="mt-px h-5 w-5" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium leading-none">Available</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@@ -37,7 +37,7 @@ export function DemoNotifications() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="-mx-2 flex items-start space-x-4 rounded-md p-2 transition-all hover:bg-accent hover:text-accent-foreground">
|
||||
<EyeOff className="mt-px h-5 w-5" />
|
||||
<EyeNoneIcon className="mt-px h-5 w-5" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium leading-none">Ignoring</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
|
||||
@@ -41,7 +41,7 @@ export function DemoShareDocument() {
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm font-medium">People with access</div>
|
||||
<h4 className="text-sm font-medium">People with access</h4>
|
||||
<div className="grid gap-6">
|
||||
<div className="flex items-center justify-between space-x-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChevronDown } from "lucide-react"
|
||||
import { ChevronDownIcon } from "@radix-ui/react-icons"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
@@ -51,7 +51,8 @@ export function DemoTeamMembers() {
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="ml-auto">
|
||||
Owner <ChevronDown className="text-muted-foreground" />
|
||||
Owner{" "}
|
||||
<ChevronDownIcon className="ml-2 h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="end">
|
||||
@@ -104,7 +105,8 @@ export function DemoTeamMembers() {
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="ml-auto">
|
||||
Member <ChevronDown className="text-muted-foreground" />
|
||||
Member{" "}
|
||||
<ChevronDownIcon className="ml-2 h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="end">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { CalendarIcon } from "@radix-ui/react-icons"
|
||||
import { addDays, format } from "date-fns"
|
||||
import { CalendarIcon } from "lucide-react"
|
||||
import { DateRange } from "react-day-picker"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Check, ChevronsUpDown, PlusCircle } from "lucide-react"
|
||||
import {
|
||||
CaretSortIcon,
|
||||
CheckIcon,
|
||||
PlusCircledIcon,
|
||||
} from "@radix-ui/react-icons"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
@@ -101,13 +105,13 @@ export default function TeamSwitcher({ className }: TeamSwitcherProps) {
|
||||
<AvatarFallback>SC</AvatarFallback>
|
||||
</Avatar>
|
||||
{selectedTeam.label}
|
||||
<ChevronsUpDown className="ml-auto opacity-50" />
|
||||
<CaretSortIcon className="ml-auto h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search team..." />
|
||||
<CommandList>
|
||||
<CommandInput placeholder="Search team..." />
|
||||
<CommandEmpty>No team found.</CommandEmpty>
|
||||
{groups.map((group) => (
|
||||
<CommandGroup key={group.label} heading={group.label}>
|
||||
@@ -129,9 +133,9 @@ export default function TeamSwitcher({ className }: TeamSwitcherProps) {
|
||||
<AvatarFallback>SC</AvatarFallback>
|
||||
</Avatar>
|
||||
{team.label}
|
||||
<Check
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto",
|
||||
"ml-auto h-4 w-4",
|
||||
selectedTeam.value === team.value
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
@@ -152,7 +156,7 @@ export default function TeamSwitcher({ className }: TeamSwitcherProps) {
|
||||
setShowNewTeamDialog(true)
|
||||
}}
|
||||
>
|
||||
<PlusCircle className="h-5 w-5" />
|
||||
<PlusCircledIcon className="mr-2 h-5 w-5" />
|
||||
Create Team
|
||||
</CommandItem>
|
||||
</DialogTrigger>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { CalendarIcon, CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"
|
||||
import { format } from "date-fns"
|
||||
import { CalendarIcon, Check, ChevronsUpDown } from "lucide-react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/registry/default/ui/command"
|
||||
import {
|
||||
Form,
|
||||
@@ -170,37 +169,35 @@ export function AccountForm() {
|
||||
(language) => language.value === field.value
|
||||
)?.label
|
||||
: "Select language"}
|
||||
<ChevronsUpDown className="opacity-50" />
|
||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search language..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No language found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{languages.map((language) => (
|
||||
<CommandItem
|
||||
value={language.label}
|
||||
key={language.value}
|
||||
onSelect={() => {
|
||||
form.setValue("language", language.value)
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2",
|
||||
language.value === field.value
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{language.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
<CommandEmpty>No language found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{languages.map((language) => (
|
||||
<CommandItem
|
||||
value={language.label}
|
||||
key={language.value}
|
||||
onSelect={() => {
|
||||
form.setValue("language", language.value)
|
||||
}}
|
||||
>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
language.value === field.value
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{language.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
import { ChevronDownIcon } from "@radix-ui/react-icons"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
|
||||
@@ -73,7 +73,7 @@ export function AppearanceForm() {
|
||||
<option value="system">System</option>
|
||||
</select>
|
||||
</FormControl>
|
||||
<ChevronDown className="absolute right-3 top-2.5 h-4 w-4 opacity-50" />
|
||||
<ChevronDownIcon className="absolute right-3 top-2.5 h-4 w-4 opacity-50" />
|
||||
</div>
|
||||
<FormDescription>
|
||||
Set the font you want to use in the dashboard.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Image from "next/image"
|
||||
import { PlusCircle } from "lucide-react"
|
||||
import { PlusCircledIcon } from "@radix-ui/react-icons"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
@@ -45,7 +45,6 @@ export function AlbumArtwork({
|
||||
"h-auto w-auto object-cover transition-all hover:scale-105",
|
||||
aspectRatio === "portrait" ? "aspect-[3/4]" : "aspect-square"
|
||||
)}
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
@@ -55,7 +54,7 @@ export function AlbumArtwork({
|
||||
<ContextMenuSubTrigger>Add to Playlist</ContextMenuSubTrigger>
|
||||
<ContextMenuSubContent className="w-48">
|
||||
<ContextMenuItem>
|
||||
<PlusCircle className="mr-2 h-4 w-4" />
|
||||
<PlusCircledIcon className="mr-2 h-4 w-4" />
|
||||
New Playlist
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Metadata } from "next"
|
||||
import Image from "next/image"
|
||||
import { PlusCircle } from "lucide-react"
|
||||
import { PlusCircledIcon } from "@radix-ui/react-icons"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { ScrollArea, ScrollBar } from "@/registry/default/ui/scroll-area"
|
||||
@@ -64,7 +64,7 @@ export default function MusicPage() {
|
||||
</TabsList>
|
||||
<div className="ml-auto mr-4">
|
||||
<Button>
|
||||
<PlusCircle />
|
||||
<PlusCircledIcon className="mr-2 h-4 w-4" />
|
||||
Add music
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -71,15 +71,13 @@ export default function ExamplesPage() {
|
||||
<div className="aspect-video overflow-hidden">
|
||||
<img
|
||||
src={example.image}
|
||||
alt={`${example.name} - Light mode preview`}
|
||||
alt={example.name}
|
||||
className="block h-full w-full object-cover transition-transform group-hover:scale-105 dark:hidden"
|
||||
loading="lazy"
|
||||
/>
|
||||
<img
|
||||
src={example.imageDark}
|
||||
alt={`${example.name} - Dark mode preview`}
|
||||
alt={example.name}
|
||||
className="hidden h-full w-full object-cover transition-transform group-hover:scale-105 dark:block"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"
|
||||
import { PopoverProps } from "@radix-ui/react-popover"
|
||||
import { Check, ChevronsUpDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useMutationObserver } from "@/hooks/use-mutation-observer"
|
||||
@@ -64,7 +64,7 @@ export function ModelSelector({ models, types, ...props }: ModelSelectorProps) {
|
||||
className="w-full justify-between"
|
||||
>
|
||||
{selectedModel ? selectedModel.name : "Select a model..."}
|
||||
<ChevronsUpDown className="opacity-50" />
|
||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="w-[250px] p-0">
|
||||
@@ -135,15 +135,13 @@ function ModelItem({ model, isSelected, onSelect, onPeek }: ModelItemProps) {
|
||||
const ref = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
useMutationObserver(ref, (mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (
|
||||
mutation.type === "attributes" &&
|
||||
mutation.attributeName === "aria-selected" &&
|
||||
ref.current?.getAttribute("aria-selected") === "true"
|
||||
) {
|
||||
onPeek(model)
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === "attributes") {
|
||||
if (mutation.attributeName === "aria-selected") {
|
||||
onPeek(model)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -154,7 +152,7 @@ function ModelItem({ model, isSelected, onSelect, onPeek }: ModelItemProps) {
|
||||
className="aria-selected:bg-primary aria-selected:text-primary-foreground"
|
||||
>
|
||||
{model.name}
|
||||
<Check
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
isSelected ? "opacity-100" : "opacity-0"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from "react"
|
||||
import { Dialog } from "@radix-ui/react-dialog"
|
||||
import { MoreHorizontal } from "lucide-react"
|
||||
import { DotsHorizontalIcon } from "@radix-ui/react-icons"
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -40,9 +40,9 @@ export function PresetActions() {
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="secondary" size="icon">
|
||||
<Button variant="secondary">
|
||||
<span className="sr-only">Actions</span>
|
||||
<MoreHorizontal />
|
||||
<DotsHorizontalIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import * as React from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"
|
||||
import { PopoverProps } from "@radix-ui/react-popover"
|
||||
import { Check, ChevronsUpDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/registry/default/ui/command"
|
||||
import {
|
||||
Popover,
|
||||
@@ -43,41 +42,39 @@ export function PresetSelector({ presets, ...props }: PresetSelectorProps) {
|
||||
className="flex-1 justify-between md:max-w-[200px] lg:max-w-[300px]"
|
||||
>
|
||||
{selectedPreset ? selectedPreset.name : "Load a preset..."}
|
||||
<ChevronsUpDown className="opacity-50" />
|
||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[300px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search presets..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No presets found.</CommandEmpty>
|
||||
<CommandGroup heading="Examples">
|
||||
{presets.map((preset) => (
|
||||
<CommandItem
|
||||
key={preset.id}
|
||||
onSelect={() => {
|
||||
setSelectedPreset(preset)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{preset.name}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto",
|
||||
selectedPreset?.id === preset.id
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
<CommandGroup className="pt-0">
|
||||
<CommandItem onSelect={() => router.push("/examples")}>
|
||||
More examples
|
||||
<CommandEmpty>No presets found.</CommandEmpty>
|
||||
<CommandGroup heading="Examples">
|
||||
{presets.map((preset) => (
|
||||
<CommandItem
|
||||
key={preset.id}
|
||||
onSelect={() => {
|
||||
setSelectedPreset(preset)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{preset.name}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
selectedPreset?.id === preset.id
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
))}
|
||||
</CommandGroup>
|
||||
<CommandGroup className="pt-0">
|
||||
<CommandItem onSelect={() => router.push("/examples")}>
|
||||
More examples
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Copy } from "lucide-react"
|
||||
import { CopyIcon } from "@radix-ui/react-icons"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
@@ -37,7 +37,7 @@ export function PresetShare() {
|
||||
</div>
|
||||
<Button type="submit" size="sm" className="px-3">
|
||||
<span className="sr-only">Copy</span>
|
||||
<Copy />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Metadata } from "next"
|
||||
import Image from "next/image"
|
||||
import { RotateCcw } from "lucide-react"
|
||||
import { CounterClockwiseClockIcon } from "@radix-ui/react-icons"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
@@ -265,7 +265,7 @@ export default function PlaygroundPage() {
|
||||
<Button>Submit</Button>
|
||||
<Button variant="secondary">
|
||||
<span className="sr-only">Show history</span>
|
||||
<RotateCcw />
|
||||
<CounterClockwiseClockIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -283,7 +283,7 @@ export default function PlaygroundPage() {
|
||||
<Button>Submit</Button>
|
||||
<Button variant="secondary">
|
||||
<span className="sr-only">Show history</span>
|
||||
<RotateCcw />
|
||||
<CounterClockwiseClockIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -314,7 +314,7 @@ export default function PlaygroundPage() {
|
||||
<Button>Submit</Button>
|
||||
<Button variant="secondary">
|
||||
<span className="sr-only">Show history</span>
|
||||
<RotateCcw />
|
||||
<CounterClockwiseClockIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+199
-20
@@ -1,8 +1,24 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import {
|
||||
AdvancedChart,
|
||||
CompanyProfile,
|
||||
CryptoScreener,
|
||||
Financials,
|
||||
ForexScreener,
|
||||
MarketOverview,
|
||||
NewsTimeline,
|
||||
OrderEntry,
|
||||
OrdersHistory,
|
||||
PositionsList,
|
||||
StockScreener,
|
||||
SymbolInfo,
|
||||
TechnicalAnalysis,
|
||||
TickerTape,
|
||||
TradingPanel,
|
||||
} from "@hanzo/ui/finance"
|
||||
|
||||
import AdvancedChart from "@/registry/default/ui/advanced-chart"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -11,20 +27,6 @@ import {
|
||||
CardTitle,
|
||||
} from "@/registry/default/ui/card"
|
||||
import { Separator } from "@/registry/default/ui/separator"
|
||||
// import { CompanyProfile } from "@/registry/default/ui/company-profile" // TODO: Create this component
|
||||
// import { CryptoScreener } from "@/registry/default/ui/crypto-screener" // TODO: Create this component
|
||||
// import { Financials } from "@/registry/default/ui/financials" // TODO: Create this component
|
||||
// import { ForexScreener } from "@/registry/default/ui/forex-screener" // TODO: Create this component
|
||||
// import { MarketOverview } from "@/registry/default/ui/market-overview" // TODO: Create this component
|
||||
// import { NewsTimeline } from "@/registry/default/ui/news-timeline" // TODO: Create this component
|
||||
// import { OrderEntry } from "@/registry/default/ui/order-entry" // TODO: Create this component
|
||||
// import { OrdersHistory } from "@/registry/default/ui/orders-history" // TODO: Create this component
|
||||
// import { PositionsList } from "@/registry/default/ui/positions-list" // TODO: Create this component
|
||||
import StockScreener from "@/registry/default/ui/stock-screener"
|
||||
// import { SymbolInfo } from "@/registry/default/ui/symbol-info" // TODO: Create this component
|
||||
// import { TechnicalAnalysis } from "@/registry/default/ui/technical-analysis" // TODO: Create this component
|
||||
// import { TickerTape } from "@/registry/default/ui/ticker-tape" // TODO: Create this component
|
||||
import { TradingPanel } from "@/registry/default/ui/trading-panel"
|
||||
|
||||
export default function FinancePage() {
|
||||
const [positions, setPositions] = useState([
|
||||
@@ -111,7 +113,31 @@ export default function FinancePage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* TODO: Add TickerTape and MarketOverview components */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Ticker Tape</CardTitle>
|
||||
<CardDescription>
|
||||
Scrolling ticker with major market indices
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<TickerTape />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Market Overview</CardTitle>
|
||||
<CardDescription>
|
||||
Multi-asset market overview with tabs
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div style={{ height: "450px" }}>
|
||||
<MarketOverview />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -122,11 +148,11 @@ export default function FinancePage() {
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">Market Screeners</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Asset screeners for stocks (crypto and forex coming soon)
|
||||
Asset screeners for stocks, crypto, and forex
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6">
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Stock Screener</CardTitle>
|
||||
@@ -137,18 +163,129 @@ export default function FinancePage() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Crypto Screener</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div style={{ height: "400px" }}>
|
||||
<CryptoScreener />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Forex Screener</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div style={{ height: "400px" }}>
|
||||
<ForexScreener />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Symbol Analysis */}
|
||||
<section className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">Symbol Analysis</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Comprehensive symbol information and analysis tools
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Symbol Info</CardTitle>
|
||||
<CardDescription>
|
||||
Real-time symbol details and price information
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div style={{ height: "140px" }}>
|
||||
<SymbolInfo symbol="NASDAQ:AAPL" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Company Profile</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div style={{ height: "400px" }}>
|
||||
<CompanyProfile symbol="NASDAQ:AAPL" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Technical Analysis</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div style={{ height: "400px" }}>
|
||||
<TechnicalAnalysis symbol="NASDAQ:AAPL" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Financials</CardTitle>
|
||||
<CardDescription>
|
||||
Financial statements and fundamental data
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div style={{ height: "500px" }}>
|
||||
<Financials symbol="NASDAQ:AAPL" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* News */}
|
||||
<section className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">News & Timeline</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Financial news feed and updates
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>News Timeline</CardTitle>
|
||||
<CardDescription>Real-time financial news feed</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div style={{ height: "400px" }}>
|
||||
<NewsTimeline />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Trading Interface */}
|
||||
<section className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">Trading Interface</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Trading panel component (order entry and position management
|
||||
coming soon)
|
||||
Order entry, positions, and order management components
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -164,6 +301,48 @@ export default function FinancePage() {
|
||||
<TradingPanel symbol="NASDAQ:AAPL" currentPrice={175} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid lg:grid-cols-3 gap-6">
|
||||
<Card className="lg:col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle>Order Entry</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<OrderEntry
|
||||
symbol="NASDAQ:AAPL"
|
||||
accountBalance={10000}
|
||||
onPlaceOrder={handlePlaceOrder}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="lg:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Positions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PositionsList positions={positions} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Orders History</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<OrdersHistory
|
||||
orders={orders}
|
||||
onCancelOrder={(id) => {
|
||||
setOrders(
|
||||
orders.map((o) =>
|
||||
o.id === id ? { ...o, status: "cancelled" } : o
|
||||
)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { ConnectButton } from "@rainbow-me/rainbowkit"
|
||||
import { formatEther } from "viem"
|
||||
import {
|
||||
useAccount,
|
||||
@@ -10,7 +11,6 @@ import {
|
||||
useWriteContract,
|
||||
} from "wagmi"
|
||||
|
||||
import { ConnectWallet } from "@/components/connect-wallet"
|
||||
import {
|
||||
AI_TOKEN_ABI,
|
||||
CONTRACT_ADDRESSES,
|
||||
@@ -208,7 +208,7 @@ export function IdentityForm() {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ConnectWallet />
|
||||
<ConnectButton />
|
||||
|
||||
{isConnected && aiBalance !== undefined && (
|
||||
<div className="mt-4">
|
||||
|
||||
@@ -9,9 +9,7 @@ export default function AppLayout({ children }: AppLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<SiteHeader />
|
||||
<main id="main-content" className="flex-1">
|
||||
{children}
|
||||
</main>
|
||||
<main className="flex-1">{children}</main>
|
||||
<SiteFooter />
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -304,7 +304,7 @@ export default function MCPPage() {
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
<Button asChild>
|
||||
<Link href="https://discord.gg/CJCyAsm9Vr">Join Discord</Link>
|
||||
<Link href="https://discord.gg/hanzo">Join Discord</Link>
|
||||
</Button>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/docs">Read Documentation</Link>
|
||||
|
||||
+20
-74
@@ -1,7 +1,13 @@
|
||||
import { Metadata } from "next"
|
||||
import Link from "next/link"
|
||||
import { ArrowRight, CheckCircle2 } from "lucide-react"
|
||||
|
||||
import { Announcement } from "@/components/announcement"
|
||||
import {
|
||||
PageActions,
|
||||
PageHeader,
|
||||
PageHeaderDescription,
|
||||
PageHeaderHeading,
|
||||
} from "@/components/page-header"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
|
||||
import { HomeContent } from "./home-content"
|
||||
@@ -40,79 +46,19 @@ export const metadata: Metadata = {
|
||||
export default function IndexPage() {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col">
|
||||
{/* Enhanced Hero Section */}
|
||||
<section className="relative overflow-hidden">
|
||||
{/* Animated gradient background */}
|
||||
<div className="absolute inset-0 -z-10">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/10 via-transparent to-accent/10 animate-gradient bg-[length:200%_200%]" />
|
||||
<div className="absolute inset-0 bg-grid-white/[0.02]" />
|
||||
</div>
|
||||
|
||||
{/* Hero content */}
|
||||
<div className="container py-16 md:py-24 lg:py-32 space-y-8 animate-fade-in-up">
|
||||
{/* Animated badge with pulse indicator */}
|
||||
<div className="flex justify-center">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-muted text-sm font-medium border">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
|
||||
</span>
|
||||
New: 150+ components, Page Builder, and more
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main heading with gradient text */}
|
||||
<h1 className="text-center text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight leading-tight">
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-foreground via-foreground to-foreground/70">
|
||||
{title}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{/* Subtitle */}
|
||||
<p className="text-center text-lg sm:text-xl md:text-2xl text-muted-foreground max-w-3xl mx-auto leading-relaxed px-4">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
{/* CTAs with clear hierarchy - larger and more prominent */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center pt-4">
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="gap-2 text-base md:text-lg h-11 md:h-12 px-6 md:px-8 min-w-[160px] shadow-lg hover:shadow-xl transition-shadow"
|
||||
>
|
||||
<Link href="/docs/installation">
|
||||
Get Started
|
||||
<ArrowRight className="h-4 w-4 md:h-5 md:w-5" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="text-base md:text-lg h-11 md:h-12 px-6 md:px-8 min-w-[160px]"
|
||||
>
|
||||
<Link href="/docs/components">View Components</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Trust indicators */}
|
||||
<div className="flex flex-wrap justify-center gap-6 md:gap-8 text-sm md:text-base text-muted-foreground pt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 md:h-5 md:w-5 text-primary" />
|
||||
<span>Open Source</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 md:h-5 md:w-5 text-primary" />
|
||||
<span>TypeScript</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 md:h-5 md:w-5 text-primary" />
|
||||
<span>Accessible</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<PageHeader>
|
||||
<Announcement />
|
||||
<PageHeaderHeading className="max-w-4xl">{title}</PageHeaderHeading>
|
||||
<PageHeaderDescription>{description}</PageHeaderDescription>
|
||||
<PageActions>
|
||||
<Button asChild size="sm">
|
||||
<Link href="/docs/installation">Get Started</Link>
|
||||
</Button>
|
||||
<Button asChild size="sm" variant="ghost">
|
||||
<Link href="/docs/components">View Components</Link>
|
||||
</Button>
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
<div className="container-wrapper section-soft flex-1 pb-6">
|
||||
<div className="container overflow-hidden">
|
||||
<section className="theme-container">
|
||||
|
||||
@@ -1,802 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
Check,
|
||||
Copy,
|
||||
Download,
|
||||
Monitor,
|
||||
Moon,
|
||||
Palette,
|
||||
RotateCcw,
|
||||
Settings2,
|
||||
Shuffle,
|
||||
Sparkles,
|
||||
Sun,
|
||||
} from "lucide-react"
|
||||
import { HexColorInput, HexColorPicker } from "react-colorful"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/registry/default/ui/alert"
|
||||
import { Badge } from "@/registry/default/ui/badge"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/default/ui/card"
|
||||
import { Checkbox } from "@/registry/default/ui/checkbox"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/default/ui/popover"
|
||||
import { Progress } from "@/registry/default/ui/progress"
|
||||
import { RadioGroup, RadioGroupItem } from "@/registry/default/ui/radio-group"
|
||||
import { ScrollArea } from "@/registry/default/ui/scroll-area"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/default/ui/select"
|
||||
import { Separator } from "@/registry/default/ui/separator"
|
||||
import { Slider } from "@/registry/default/ui/slider"
|
||||
import { Switch } from "@/registry/default/ui/switch"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/default/ui/tabs"
|
||||
import { Textarea } from "@/registry/default/ui/textarea"
|
||||
|
||||
const DEFAULT_LIGHT_COLORS = {
|
||||
background: "#ffffff",
|
||||
foreground: "#0a0a0a",
|
||||
card: "#ffffff",
|
||||
"card-foreground": "#0a0a0a",
|
||||
popover: "#ffffff",
|
||||
"popover-foreground": "#0a0a0a",
|
||||
primary: "#0099ff",
|
||||
"primary-foreground": "#ffffff",
|
||||
secondary: "#f4f4f5",
|
||||
"secondary-foreground": "#0a0a0a",
|
||||
muted: "#f4f4f5",
|
||||
"muted-foreground": "#71717a",
|
||||
accent: "#f4f4f5",
|
||||
"accent-foreground": "#0a0a0a",
|
||||
destructive: "#ef4444",
|
||||
"destructive-foreground": "#ffffff",
|
||||
border: "#e4e4e7",
|
||||
input: "#e4e4e7",
|
||||
ring: "#0099ff",
|
||||
}
|
||||
|
||||
const DEFAULT_DARK_COLORS = {
|
||||
background: "#0a0a0a",
|
||||
foreground: "#fafafa",
|
||||
card: "#0a0a0a",
|
||||
"card-foreground": "#fafafa",
|
||||
popover: "#0a0a0a",
|
||||
"popover-foreground": "#fafafa",
|
||||
primary: "#0099ff",
|
||||
"primary-foreground": "#ffffff",
|
||||
secondary: "#262626",
|
||||
"secondary-foreground": "#fafafa",
|
||||
muted: "#262626",
|
||||
"muted-foreground": "#a3a3a3",
|
||||
accent: "#262626",
|
||||
"accent-foreground": "#fafafa",
|
||||
destructive: "#dc2626",
|
||||
"destructive-foreground": "#ffffff",
|
||||
border: "#262626",
|
||||
input: "#262626",
|
||||
ring: "#0099ff",
|
||||
}
|
||||
|
||||
const PRESET_THEMES = {
|
||||
"Electric Blue": {
|
||||
primary: "#0099ff",
|
||||
secondary: "#1e293b",
|
||||
accent: "#06b6d4",
|
||||
},
|
||||
Forest: {
|
||||
primary: "#10b981",
|
||||
secondary: "#065f46",
|
||||
accent: "#34d399",
|
||||
},
|
||||
Sunset: {
|
||||
primary: "#f97316",
|
||||
secondary: "#dc2626",
|
||||
accent: "#fbbf24",
|
||||
},
|
||||
"Purple Haze": {
|
||||
primary: "#8b5cf6",
|
||||
secondary: "#7c3aed",
|
||||
accent: "#a78bfa",
|
||||
},
|
||||
Ocean: {
|
||||
primary: "#0ea5e9",
|
||||
secondary: "#0284c7",
|
||||
accent: "#38bdf8",
|
||||
},
|
||||
}
|
||||
|
||||
export default function ThemeGeneratorPage() {
|
||||
const [colors, setColors] = React.useState(DEFAULT_LIGHT_COLORS)
|
||||
const [radius, setRadius] = React.useState(0.5)
|
||||
const [mode, setMode] = React.useState<"light" | "dark">("light")
|
||||
const [copied, setCopied] = React.useState(false)
|
||||
const [activeColorKey, setActiveColorKey] = React.useState<string | null>(
|
||||
null
|
||||
)
|
||||
|
||||
const updateColor = (key: string, value: string) => {
|
||||
setColors((prev) => ({ ...prev, [key]: value }))
|
||||
}
|
||||
|
||||
const toggleMode = () => {
|
||||
const newMode = mode === "light" ? "dark" : "light"
|
||||
setMode(newMode)
|
||||
setColors(newMode === "light" ? DEFAULT_LIGHT_COLORS : DEFAULT_DARK_COLORS)
|
||||
}
|
||||
|
||||
const hexToHSL = (hex: string) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
if (!result) return "0 0% 0%"
|
||||
|
||||
const r = parseInt(result[1], 16) / 255
|
||||
const g = parseInt(result[2], 16) / 255
|
||||
const b = parseInt(result[3], 16) / 255
|
||||
|
||||
const max = Math.max(r, g, b)
|
||||
const min = Math.min(r, g, b)
|
||||
let h = 0,
|
||||
s = 0,
|
||||
l = (max + min) / 2
|
||||
|
||||
if (max !== min) {
|
||||
const d = max - min
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
||||
switch (max) {
|
||||
case r:
|
||||
h = ((g - b) / d + (g < b ? 6 : 0)) / 6
|
||||
break
|
||||
case g:
|
||||
h = ((b - r) / d + 2) / 6
|
||||
break
|
||||
case b:
|
||||
h = ((r - g) / d + 4) / 6
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return `${Math.round(h * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`
|
||||
}
|
||||
|
||||
const generateCSS = () => {
|
||||
const css = `@layer base {
|
||||
:root {
|
||||
${Object.entries(colors)
|
||||
.map(([key, value]) => ` --${key}: ${hexToHSL(value)};`)
|
||||
.join("\n")}
|
||||
--radius: ${radius}rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
${Object.entries(colors)
|
||||
.map(([key, value]) => ` --${key}: ${hexToHSL(value)};`)
|
||||
.join("\n")}
|
||||
}
|
||||
}`
|
||||
return css
|
||||
}
|
||||
|
||||
const copyCSS = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(generateCSS())
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
toast.success("CSS copied to clipboard")
|
||||
} catch (err) {
|
||||
console.error("Failed to copy CSS:", err)
|
||||
toast.error("Failed to copy CSS", {
|
||||
description: "Please try again",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const applyPresetTheme = (presetName: string) => {
|
||||
const preset = PRESET_THEMES[presetName as keyof typeof PRESET_THEMES]
|
||||
if (preset) {
|
||||
setColors((prev) => ({
|
||||
...prev,
|
||||
primary: preset.primary,
|
||||
"primary-foreground": mode === "dark" ? "#ffffff" : "#ffffff",
|
||||
secondary: preset.secondary,
|
||||
"secondary-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
accent: preset.accent,
|
||||
"accent-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
ring: preset.primary,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
const randomizeTheme = () => {
|
||||
const randomHex = () => {
|
||||
const hue = Math.floor(Math.random() * 360)
|
||||
const saturation = 50 + Math.floor(Math.random() * 50)
|
||||
const lightness = 40 + Math.floor(Math.random() * 30)
|
||||
return hslToHex(hue, saturation, lightness)
|
||||
}
|
||||
|
||||
const primary = randomHex()
|
||||
const secondary = randomHex()
|
||||
const accent = randomHex()
|
||||
|
||||
setColors((prev) => ({
|
||||
...prev,
|
||||
primary,
|
||||
"primary-foreground": "#ffffff",
|
||||
secondary,
|
||||
"secondary-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
accent,
|
||||
"accent-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
ring: primary,
|
||||
}))
|
||||
}
|
||||
|
||||
const hslToHex = (h: number, s: number, l: number) => {
|
||||
l /= 100
|
||||
const a = (s * Math.min(l, 1 - l)) / 100
|
||||
const f = (n: number) => {
|
||||
const k = (n + h / 30) % 12
|
||||
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1)
|
||||
return Math.round(255 * color)
|
||||
.toString(16)
|
||||
.padStart(2, "0")
|
||||
}
|
||||
return `#${f(0)}${f(8)}${f(4)}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container max-w-[1600px] py-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold tracking-tight flex items-center gap-3">
|
||||
<Palette className="h-8 w-8 text-primary" />
|
||||
Theme Generator
|
||||
</h1>
|
||||
<p className="mt-2 text-lg text-muted-foreground">
|
||||
Create beautiful themes with live preview and instant export
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={toggleMode}
|
||||
className="relative"
|
||||
>
|
||||
{mode === "light" ? (
|
||||
<Moon className="h-5 w-5" />
|
||||
) : (
|
||||
<Sun className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 lg:grid-cols-[420px_1fr]">
|
||||
{/* Enhanced Controls Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Preset Themes */}
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader className="bg-gradient-to-r from-primary/10 to-primary/5">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5" />
|
||||
Preset Themes
|
||||
</CardTitle>
|
||||
<CardDescription>Start with a pre-designed theme</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{Object.keys(PRESET_THEMES).map((presetName) => (
|
||||
<Button
|
||||
key={presetName}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => applyPresetTheme(presetName)}
|
||||
className="justify-start text-xs"
|
||||
>
|
||||
{presetName}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Color Customization */}
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Settings2 className="h-5 w-5" />
|
||||
Color Palette
|
||||
</CardTitle>
|
||||
<CardDescription>Fine-tune your color scheme</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<ScrollArea className="h-[400px] pr-4">
|
||||
<div className="space-y-3">
|
||||
{Object.entries(colors).map(([key, value]) => (
|
||||
<div key={key} className="space-y-1.5">
|
||||
<Label className="text-xs font-medium capitalize">
|
||||
{key.replace(/-/g, " ")}
|
||||
</Label>
|
||||
<Popover
|
||||
open={activeColorKey === key}
|
||||
onOpenChange={(open) =>
|
||||
setActiveColorKey(open ? key : null)
|
||||
}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full justify-between font-mono text-xs h-9"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="h-5 w-5 rounded-md border shadow-sm"
|
||||
style={{ backgroundColor: value }}
|
||||
/>
|
||||
<span>{value.toUpperCase()}</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground">
|
||||
HSL: {hexToHSL(value).split(" ").join(", ")}
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-3" align="start">
|
||||
<div className="space-y-3">
|
||||
<HexColorPicker
|
||||
color={value}
|
||||
onChange={(v) => updateColor(key, v)}
|
||||
style={{ width: 200, height: 200 }}
|
||||
/>
|
||||
<HexColorInput
|
||||
color={value}
|
||||
onChange={(v) => updateColor(key, v)}
|
||||
className="w-full rounded-md border px-3 py-1 text-sm font-mono"
|
||||
prefixed
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Border Radius Control */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Border Radius</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<Slider
|
||||
value={[radius]}
|
||||
onValueChange={(v) => setRadius(v[0])}
|
||||
min={0}
|
||||
max={1.5}
|
||||
step={0.05}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>Sharp</span>
|
||||
<span className="font-mono">{radius}rem</span>
|
||||
<span>Rounded</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
onClick={randomizeTheme}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
>
|
||||
<Shuffle className="mr-2 h-4 w-4" />
|
||||
Randomize
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setColors(
|
||||
mode === "light" ? DEFAULT_LIGHT_COLORS : DEFAULT_DARK_COLORS
|
||||
)
|
||||
}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
>
|
||||
<RotateCcw className="mr-2 h-4 w-4" />
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button onClick={copyCSS} className="w-full" size="lg">
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="mr-2 h-4 w-4" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy CSS Variables
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Enhanced Preview Area */}
|
||||
<div className="space-y-6">
|
||||
<Tabs defaultValue="preview" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="preview">Live Preview</TabsTrigger>
|
||||
<TabsTrigger value="components">Components</TabsTrigger>
|
||||
<TabsTrigger value="code">Export Code</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="preview" className="mt-6">
|
||||
<div className="theme-preview-container" data-theme="custom">
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
.theme-preview-container[data-theme="custom"] {
|
||||
--background: ${hexToHSL(colors.background)};
|
||||
--foreground: ${hexToHSL(colors.foreground)};
|
||||
--card: ${hexToHSL(colors.card)};
|
||||
--card-foreground: ${hexToHSL(colors["card-foreground"])};
|
||||
--popover: ${hexToHSL(colors.popover)};
|
||||
--popover-foreground: ${hexToHSL(colors["popover-foreground"])};
|
||||
--primary: ${hexToHSL(colors.primary)};
|
||||
--primary-foreground: ${hexToHSL(colors["primary-foreground"])};
|
||||
--secondary: ${hexToHSL(colors.secondary)};
|
||||
--secondary-foreground: ${hexToHSL(colors["secondary-foreground"])};
|
||||
--muted: ${hexToHSL(colors.muted)};
|
||||
--muted-foreground: ${hexToHSL(colors["muted-foreground"])};
|
||||
--accent: ${hexToHSL(colors.accent)};
|
||||
--accent-foreground: ${hexToHSL(colors["accent-foreground"])};
|
||||
--destructive: ${hexToHSL(colors.destructive)};
|
||||
--destructive-foreground: ${hexToHSL(colors["destructive-foreground"])};
|
||||
--border: ${hexToHSL(colors.border)};
|
||||
--input: ${hexToHSL(colors.input)};
|
||||
--ring: ${hexToHSL(colors.ring)};
|
||||
--radius: ${radius}rem;
|
||||
}
|
||||
.theme-preview-container[data-theme="custom"] * {
|
||||
border-radius: calc(var(--radius) * 1);
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="rounded-lg border bg-background p-8 shadow-sm"
|
||||
style={{
|
||||
backgroundColor: `hsl(var(--background))`,
|
||||
color: `hsl(var(--foreground))`,
|
||||
borderColor: `hsl(var(--border))`,
|
||||
}}
|
||||
>
|
||||
<div className="space-y-8">
|
||||
{/* Hero Section Preview */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-3xl font-bold">
|
||||
Welcome to Your App
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
This is how your theme looks in a real application
|
||||
context
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button size="lg">Get Started</Button>
|
||||
<Button size="lg" variant="secondary">
|
||||
Learn More
|
||||
</Button>
|
||||
<Button size="lg" variant="outline">
|
||||
Documentation
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Form Elements */}
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Form Elements</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
placeholder="name@example.com"
|
||||
type="email"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="message">Message</Label>
|
||||
<Textarea
|
||||
id="message"
|
||||
placeholder="Type your message here..."
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox id="terms" />
|
||||
<Label htmlFor="terms" className="text-sm">
|
||||
Accept terms and conditions
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Selections</h3>
|
||||
<div className="space-y-3">
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an option" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="option1">Option 1</SelectItem>
|
||||
<SelectItem value="option2">Option 2</SelectItem>
|
||||
<SelectItem value="option3">Option 3</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<RadioGroup defaultValue="option-one">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="option-one"
|
||||
id="option-one"
|
||||
/>
|
||||
<Label htmlFor="option-one">Option One</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="option-two"
|
||||
id="option-two"
|
||||
/>
|
||||
<Label htmlFor="option-two">Option Two</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="notifications" />
|
||||
<Label htmlFor="notifications">
|
||||
Enable notifications
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cards */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Card Title</CardTitle>
|
||||
<CardDescription>
|
||||
Card description goes here
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This is how cards appear with your custom theme. The
|
||||
borders, shadows, and colors all adapt to your
|
||||
selections.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Statistics</CardTitle>
|
||||
<CardDescription>
|
||||
Your performance metrics
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm">Progress</span>
|
||||
<span className="text-sm font-medium">75%</span>
|
||||
</div>
|
||||
<Progress value={75} className="h-2" />
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Badge>Active</Badge>
|
||||
<Badge variant="secondary">Updated</Badge>
|
||||
<Badge variant="outline">New</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Alert */}
|
||||
<Alert>
|
||||
<AlertTitle>Heads up!</AlertTitle>
|
||||
<AlertDescription>
|
||||
You can customize every aspect of your theme using the
|
||||
controls on the left. Changes are reflected in
|
||||
real-time.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="components" className="mt-6">
|
||||
<div
|
||||
className="theme-components-container"
|
||||
data-theme="custom-components"
|
||||
>
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
.theme-components-container[data-theme="custom-components"] {
|
||||
--background: ${hexToHSL(colors.background)};
|
||||
--foreground: ${hexToHSL(colors.foreground)};
|
||||
--card: ${hexToHSL(colors.card)};
|
||||
--card-foreground: ${hexToHSL(colors["card-foreground"])};
|
||||
--popover: ${hexToHSL(colors.popover)};
|
||||
--popover-foreground: ${hexToHSL(colors["popover-foreground"])};
|
||||
--primary: ${hexToHSL(colors.primary)};
|
||||
--primary-foreground: ${hexToHSL(colors["primary-foreground"])};
|
||||
--secondary: ${hexToHSL(colors.secondary)};
|
||||
--secondary-foreground: ${hexToHSL(colors["secondary-foreground"])};
|
||||
--muted: ${hexToHSL(colors.muted)};
|
||||
--muted-foreground: ${hexToHSL(colors["muted-foreground"])};
|
||||
--accent: ${hexToHSL(colors.accent)};
|
||||
--accent-foreground: ${hexToHSL(colors["accent-foreground"])};
|
||||
--destructive: ${hexToHSL(colors.destructive)};
|
||||
--destructive-foreground: ${hexToHSL(colors["destructive-foreground"])};
|
||||
--border: ${hexToHSL(colors.border)};
|
||||
--input: ${hexToHSL(colors.input)};
|
||||
--ring: ${hexToHSL(colors.ring)};
|
||||
--radius: ${radius}rem;
|
||||
}
|
||||
.theme-components-container[data-theme="custom-components"] * {
|
||||
border-radius: calc(var(--radius) * 1);
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="rounded-lg border bg-background p-8 space-y-8"
|
||||
style={{
|
||||
backgroundColor: `hsl(var(--background))`,
|
||||
color: `hsl(var(--foreground))`,
|
||||
borderColor: `hsl(var(--border))`,
|
||||
}}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
Button Variants
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button>Primary</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="outline">Outline</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
<Button variant="destructive">Destructive</Button>
|
||||
<Button variant="link">Link</Button>
|
||||
<Button size="sm">Small</Button>
|
||||
<Button size="lg">Large</Button>
|
||||
<Button disabled>Disabled</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
Badge Variants
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge>Default</Badge>
|
||||
<Badge variant="secondary">Secondary</Badge>
|
||||
<Badge variant="outline">Outline</Badge>
|
||||
<Badge variant="destructive">Destructive</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Typography</h3>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-4xl font-bold">Heading 1</h1>
|
||||
<h2 className="text-3xl font-bold">Heading 2</h2>
|
||||
<h3 className="text-2xl font-bold">Heading 3</h3>
|
||||
<h4 className="text-xl font-bold">Heading 4</h4>
|
||||
<p className="text-lg">Large paragraph text</p>
|
||||
<p>Regular paragraph text</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Muted text
|
||||
</p>
|
||||
<p className="text-xs">Small text</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="code" className="mt-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>CSS Variables</CardTitle>
|
||||
<CardDescription>
|
||||
Copy this code and paste it into your globals.css file
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="relative">
|
||||
<pre className="overflow-auto rounded-lg bg-muted p-6 text-sm">
|
||||
<code className="language-css">{generateCSS()}</code>
|
||||
</pre>
|
||||
<Button
|
||||
onClick={copyCSS}
|
||||
size="sm"
|
||||
className="absolute right-2 top-2"
|
||||
variant="ghost"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 rounded-lg bg-muted/50 p-4">
|
||||
<p className="text-sm font-medium mb-2">
|
||||
Usage Instructions:
|
||||
</p>
|
||||
<ol className="text-sm text-muted-foreground space-y-1 list-decimal list-inside">
|
||||
<li>Copy the CSS variables above</li>
|
||||
<li>
|
||||
Open your project's global CSS file (usually globals.css
|
||||
or app.css)
|
||||
</li>
|
||||
<li>
|
||||
Replace the existing :root variables with your new theme
|
||||
</li>
|
||||
<li>Save the file and refresh your application</li>
|
||||
</ol>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,35 +1,797 @@
|
||||
"use client"
|
||||
|
||||
import dynamic from "next/dynamic"
|
||||
import * as React from "react"
|
||||
import {
|
||||
Check,
|
||||
Copy,
|
||||
Download,
|
||||
Monitor,
|
||||
Moon,
|
||||
Palette,
|
||||
RotateCcw,
|
||||
Settings2,
|
||||
Shuffle,
|
||||
Sparkles,
|
||||
Sun,
|
||||
} from "lucide-react"
|
||||
import { HexColorInput, HexColorPicker } from "react-colorful"
|
||||
|
||||
import { SkeletonCard } from "@/components/skeleton-card"
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/registry/default/ui/alert"
|
||||
import { Badge } from "@/registry/default/ui/badge"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/default/ui/card"
|
||||
import { Checkbox } from "@/registry/default/ui/checkbox"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/default/ui/popover"
|
||||
import { Progress } from "@/registry/default/ui/progress"
|
||||
import { RadioGroup, RadioGroupItem } from "@/registry/default/ui/radio-group"
|
||||
import { ScrollArea } from "@/registry/default/ui/scroll-area"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/default/ui/select"
|
||||
import { Separator } from "@/registry/default/ui/separator"
|
||||
import { Slider } from "@/registry/default/ui/slider"
|
||||
import { Switch } from "@/registry/default/ui/switch"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/default/ui/tabs"
|
||||
import { Textarea } from "@/registry/default/ui/textarea"
|
||||
|
||||
// Dynamically import the color picker library and theme generator
|
||||
const ThemeGeneratorClient = dynamic(
|
||||
() =>
|
||||
import("./theme-generator-client").then((mod) => mod.ThemeGeneratorClient),
|
||||
{
|
||||
loading: () => (
|
||||
<div className="container max-w-[1600px] py-6">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="h-12 w-64 bg-muted animate-pulse rounded" />
|
||||
<div className="h-6 w-96 bg-muted animate-pulse rounded mt-2" />
|
||||
</div>
|
||||
<div className="grid gap-8 lg:grid-cols-[420px_1fr]">
|
||||
<div className="space-y-4">
|
||||
<SkeletonCard />
|
||||
<SkeletonCard />
|
||||
</div>
|
||||
<SkeletonCard className="h-[600px]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
ssr: false, // Color picker needs client-side rendering
|
||||
}
|
||||
)
|
||||
const DEFAULT_LIGHT_COLORS = {
|
||||
background: "#ffffff",
|
||||
foreground: "#0a0a0a",
|
||||
card: "#ffffff",
|
||||
"card-foreground": "#0a0a0a",
|
||||
popover: "#ffffff",
|
||||
"popover-foreground": "#0a0a0a",
|
||||
primary: "#0099ff",
|
||||
"primary-foreground": "#ffffff",
|
||||
secondary: "#f4f4f5",
|
||||
"secondary-foreground": "#0a0a0a",
|
||||
muted: "#f4f4f5",
|
||||
"muted-foreground": "#71717a",
|
||||
accent: "#f4f4f5",
|
||||
"accent-foreground": "#0a0a0a",
|
||||
destructive: "#ef4444",
|
||||
"destructive-foreground": "#ffffff",
|
||||
border: "#e4e4e7",
|
||||
input: "#e4e4e7",
|
||||
ring: "#0099ff",
|
||||
}
|
||||
|
||||
const DEFAULT_DARK_COLORS = {
|
||||
background: "#0a0a0a",
|
||||
foreground: "#fafafa",
|
||||
card: "#0a0a0a",
|
||||
"card-foreground": "#fafafa",
|
||||
popover: "#0a0a0a",
|
||||
"popover-foreground": "#fafafa",
|
||||
primary: "#0099ff",
|
||||
"primary-foreground": "#ffffff",
|
||||
secondary: "#262626",
|
||||
"secondary-foreground": "#fafafa",
|
||||
muted: "#262626",
|
||||
"muted-foreground": "#a3a3a3",
|
||||
accent: "#262626",
|
||||
"accent-foreground": "#fafafa",
|
||||
destructive: "#dc2626",
|
||||
"destructive-foreground": "#ffffff",
|
||||
border: "#262626",
|
||||
input: "#262626",
|
||||
ring: "#0099ff",
|
||||
}
|
||||
|
||||
const PRESET_THEMES = {
|
||||
"Electric Blue": {
|
||||
primary: "#0099ff",
|
||||
secondary: "#1e293b",
|
||||
accent: "#06b6d4",
|
||||
},
|
||||
Forest: {
|
||||
primary: "#10b981",
|
||||
secondary: "#065f46",
|
||||
accent: "#34d399",
|
||||
},
|
||||
Sunset: {
|
||||
primary: "#f97316",
|
||||
secondary: "#dc2626",
|
||||
accent: "#fbbf24",
|
||||
},
|
||||
"Purple Haze": {
|
||||
primary: "#8b5cf6",
|
||||
secondary: "#7c3aed",
|
||||
accent: "#a78bfa",
|
||||
},
|
||||
Ocean: {
|
||||
primary: "#0ea5e9",
|
||||
secondary: "#0284c7",
|
||||
accent: "#38bdf8",
|
||||
},
|
||||
}
|
||||
|
||||
export default function ThemeGeneratorPage() {
|
||||
return <ThemeGeneratorClient />
|
||||
const [colors, setColors] = React.useState(DEFAULT_LIGHT_COLORS)
|
||||
const [radius, setRadius] = React.useState(0.5)
|
||||
const [mode, setMode] = React.useState<"light" | "dark">("light")
|
||||
const [copied, setCopied] = React.useState(false)
|
||||
const [activeColorKey, setActiveColorKey] = React.useState<string | null>(
|
||||
null
|
||||
)
|
||||
|
||||
const updateColor = (key: string, value: string) => {
|
||||
setColors((prev) => ({ ...prev, [key]: value }))
|
||||
}
|
||||
|
||||
const toggleMode = () => {
|
||||
const newMode = mode === "light" ? "dark" : "light"
|
||||
setMode(newMode)
|
||||
setColors(newMode === "light" ? DEFAULT_LIGHT_COLORS : DEFAULT_DARK_COLORS)
|
||||
}
|
||||
|
||||
const hexToHSL = (hex: string) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
if (!result) return "0 0% 0%"
|
||||
|
||||
const r = parseInt(result[1], 16) / 255
|
||||
const g = parseInt(result[2], 16) / 255
|
||||
const b = parseInt(result[3], 16) / 255
|
||||
|
||||
const max = Math.max(r, g, b)
|
||||
const min = Math.min(r, g, b)
|
||||
let h = 0,
|
||||
s = 0,
|
||||
l = (max + min) / 2
|
||||
|
||||
if (max !== min) {
|
||||
const d = max - min
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
||||
switch (max) {
|
||||
case r:
|
||||
h = ((g - b) / d + (g < b ? 6 : 0)) / 6
|
||||
break
|
||||
case g:
|
||||
h = ((b - r) / d + 2) / 6
|
||||
break
|
||||
case b:
|
||||
h = ((r - g) / d + 4) / 6
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return `${Math.round(h * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`
|
||||
}
|
||||
|
||||
const generateCSS = () => {
|
||||
const css = `@layer base {
|
||||
:root {
|
||||
${Object.entries(colors)
|
||||
.map(([key, value]) => ` --${key}: ${hexToHSL(value)};`)
|
||||
.join("\n")}
|
||||
--radius: ${radius}rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
${Object.entries(colors)
|
||||
.map(([key, value]) => ` --${key}: ${hexToHSL(value)};`)
|
||||
.join("\n")}
|
||||
}
|
||||
}`
|
||||
return css
|
||||
}
|
||||
|
||||
const copyCSS = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(generateCSS())
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch (err) {
|
||||
console.error("Failed to copy CSS:", err)
|
||||
}
|
||||
}
|
||||
|
||||
const applyPresetTheme = (presetName: string) => {
|
||||
const preset = PRESET_THEMES[presetName as keyof typeof PRESET_THEMES]
|
||||
if (preset) {
|
||||
setColors((prev) => ({
|
||||
...prev,
|
||||
primary: preset.primary,
|
||||
"primary-foreground": mode === "dark" ? "#ffffff" : "#ffffff",
|
||||
secondary: preset.secondary,
|
||||
"secondary-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
accent: preset.accent,
|
||||
"accent-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
ring: preset.primary,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
const randomizeTheme = () => {
|
||||
const randomHex = () => {
|
||||
const hue = Math.floor(Math.random() * 360)
|
||||
const saturation = 50 + Math.floor(Math.random() * 50)
|
||||
const lightness = 40 + Math.floor(Math.random() * 30)
|
||||
return hslToHex(hue, saturation, lightness)
|
||||
}
|
||||
|
||||
const primary = randomHex()
|
||||
const secondary = randomHex()
|
||||
const accent = randomHex()
|
||||
|
||||
setColors((prev) => ({
|
||||
...prev,
|
||||
primary,
|
||||
"primary-foreground": "#ffffff",
|
||||
secondary,
|
||||
"secondary-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
accent,
|
||||
"accent-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
ring: primary,
|
||||
}))
|
||||
}
|
||||
|
||||
const hslToHex = (h: number, s: number, l: number) => {
|
||||
l /= 100
|
||||
const a = (s * Math.min(l, 1 - l)) / 100
|
||||
const f = (n: number) => {
|
||||
const k = (n + h / 30) % 12
|
||||
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1)
|
||||
return Math.round(255 * color)
|
||||
.toString(16)
|
||||
.padStart(2, "0")
|
||||
}
|
||||
return `#${f(0)}${f(8)}${f(4)}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container max-w-[1600px] py-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold tracking-tight flex items-center gap-3">
|
||||
<Palette className="h-8 w-8 text-primary" />
|
||||
Theme Generator
|
||||
</h1>
|
||||
<p className="mt-2 text-lg text-muted-foreground">
|
||||
Create beautiful themes with live preview and instant export
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={toggleMode}
|
||||
className="relative"
|
||||
>
|
||||
{mode === "light" ? (
|
||||
<Moon className="h-5 w-5" />
|
||||
) : (
|
||||
<Sun className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 lg:grid-cols-[420px_1fr]">
|
||||
{/* Enhanced Controls Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Preset Themes */}
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader className="bg-gradient-to-r from-primary/10 to-primary/5">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5" />
|
||||
Preset Themes
|
||||
</CardTitle>
|
||||
<CardDescription>Start with a pre-designed theme</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{Object.keys(PRESET_THEMES).map((presetName) => (
|
||||
<Button
|
||||
key={presetName}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => applyPresetTheme(presetName)}
|
||||
className="justify-start text-xs"
|
||||
>
|
||||
{presetName}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Color Customization */}
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Settings2 className="h-5 w-5" />
|
||||
Color Palette
|
||||
</CardTitle>
|
||||
<CardDescription>Fine-tune your color scheme</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<ScrollArea className="h-[400px] pr-4">
|
||||
<div className="space-y-3">
|
||||
{Object.entries(colors).map(([key, value]) => (
|
||||
<div key={key} className="space-y-1.5">
|
||||
<Label className="text-xs font-medium capitalize">
|
||||
{key.replace(/-/g, " ")}
|
||||
</Label>
|
||||
<Popover
|
||||
open={activeColorKey === key}
|
||||
onOpenChange={(open) =>
|
||||
setActiveColorKey(open ? key : null)
|
||||
}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full justify-between font-mono text-xs h-9"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="h-5 w-5 rounded-md border shadow-sm"
|
||||
style={{ backgroundColor: value }}
|
||||
/>
|
||||
<span>{value.toUpperCase()}</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground">
|
||||
HSL: {hexToHSL(value).split(" ").join(", ")}
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-3" align="start">
|
||||
<div className="space-y-3">
|
||||
<HexColorPicker
|
||||
color={value}
|
||||
onChange={(v) => updateColor(key, v)}
|
||||
style={{ width: 200, height: 200 }}
|
||||
/>
|
||||
<HexColorInput
|
||||
color={value}
|
||||
onChange={(v) => updateColor(key, v)}
|
||||
className="w-full rounded-md border px-3 py-1 text-sm font-mono"
|
||||
prefixed
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Border Radius Control */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Border Radius</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<Slider
|
||||
value={[radius]}
|
||||
onValueChange={(v) => setRadius(v[0])}
|
||||
min={0}
|
||||
max={1.5}
|
||||
step={0.05}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>Sharp</span>
|
||||
<span className="font-mono">{radius}rem</span>
|
||||
<span>Rounded</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
onClick={randomizeTheme}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
>
|
||||
<Shuffle className="mr-2 h-4 w-4" />
|
||||
Randomize
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setColors(
|
||||
mode === "light" ? DEFAULT_LIGHT_COLORS : DEFAULT_DARK_COLORS
|
||||
)
|
||||
}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
>
|
||||
<RotateCcw className="mr-2 h-4 w-4" />
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button onClick={copyCSS} className="w-full" size="lg">
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="mr-2 h-4 w-4" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy CSS Variables
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Enhanced Preview Area */}
|
||||
<div className="space-y-6">
|
||||
<Tabs defaultValue="preview" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="preview">Live Preview</TabsTrigger>
|
||||
<TabsTrigger value="components">Components</TabsTrigger>
|
||||
<TabsTrigger value="code">Export Code</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="preview" className="mt-6">
|
||||
<div className="theme-preview-container" data-theme="custom">
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
.theme-preview-container[data-theme="custom"] {
|
||||
--background: ${hexToHSL(colors.background)};
|
||||
--foreground: ${hexToHSL(colors.foreground)};
|
||||
--card: ${hexToHSL(colors.card)};
|
||||
--card-foreground: ${hexToHSL(colors["card-foreground"])};
|
||||
--popover: ${hexToHSL(colors.popover)};
|
||||
--popover-foreground: ${hexToHSL(colors["popover-foreground"])};
|
||||
--primary: ${hexToHSL(colors.primary)};
|
||||
--primary-foreground: ${hexToHSL(colors["primary-foreground"])};
|
||||
--secondary: ${hexToHSL(colors.secondary)};
|
||||
--secondary-foreground: ${hexToHSL(colors["secondary-foreground"])};
|
||||
--muted: ${hexToHSL(colors.muted)};
|
||||
--muted-foreground: ${hexToHSL(colors["muted-foreground"])};
|
||||
--accent: ${hexToHSL(colors.accent)};
|
||||
--accent-foreground: ${hexToHSL(colors["accent-foreground"])};
|
||||
--destructive: ${hexToHSL(colors.destructive)};
|
||||
--destructive-foreground: ${hexToHSL(colors["destructive-foreground"])};
|
||||
--border: ${hexToHSL(colors.border)};
|
||||
--input: ${hexToHSL(colors.input)};
|
||||
--ring: ${hexToHSL(colors.ring)};
|
||||
--radius: ${radius}rem;
|
||||
}
|
||||
.theme-preview-container[data-theme="custom"] * {
|
||||
border-radius: calc(var(--radius) * 1);
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="rounded-lg border bg-background p-8 shadow-sm"
|
||||
style={{
|
||||
backgroundColor: `hsl(var(--background))`,
|
||||
color: `hsl(var(--foreground))`,
|
||||
borderColor: `hsl(var(--border))`,
|
||||
}}
|
||||
>
|
||||
<div className="space-y-8">
|
||||
{/* Hero Section Preview */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-3xl font-bold">
|
||||
Welcome to Your App
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
This is how your theme looks in a real application
|
||||
context
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button size="lg">Get Started</Button>
|
||||
<Button size="lg" variant="secondary">
|
||||
Learn More
|
||||
</Button>
|
||||
<Button size="lg" variant="outline">
|
||||
Documentation
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Form Elements */}
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Form Elements</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
placeholder="name@example.com"
|
||||
type="email"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="message">Message</Label>
|
||||
<Textarea
|
||||
id="message"
|
||||
placeholder="Type your message here..."
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox id="terms" />
|
||||
<Label htmlFor="terms" className="text-sm">
|
||||
Accept terms and conditions
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Selections</h3>
|
||||
<div className="space-y-3">
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an option" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="option1">Option 1</SelectItem>
|
||||
<SelectItem value="option2">Option 2</SelectItem>
|
||||
<SelectItem value="option3">Option 3</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<RadioGroup defaultValue="option-one">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="option-one"
|
||||
id="option-one"
|
||||
/>
|
||||
<Label htmlFor="option-one">Option One</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="option-two"
|
||||
id="option-two"
|
||||
/>
|
||||
<Label htmlFor="option-two">Option Two</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="notifications" />
|
||||
<Label htmlFor="notifications">
|
||||
Enable notifications
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cards */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Card Title</CardTitle>
|
||||
<CardDescription>
|
||||
Card description goes here
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This is how cards appear with your custom theme. The
|
||||
borders, shadows, and colors all adapt to your
|
||||
selections.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Statistics</CardTitle>
|
||||
<CardDescription>
|
||||
Your performance metrics
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm">Progress</span>
|
||||
<span className="text-sm font-medium">75%</span>
|
||||
</div>
|
||||
<Progress value={75} className="h-2" />
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Badge>Active</Badge>
|
||||
<Badge variant="secondary">Updated</Badge>
|
||||
<Badge variant="outline">New</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Alert */}
|
||||
<Alert>
|
||||
<AlertTitle>Heads up!</AlertTitle>
|
||||
<AlertDescription>
|
||||
You can customize every aspect of your theme using the
|
||||
controls on the left. Changes are reflected in
|
||||
real-time.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="components" className="mt-6">
|
||||
<div
|
||||
className="theme-components-container"
|
||||
data-theme="custom-components"
|
||||
>
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
.theme-components-container[data-theme="custom-components"] {
|
||||
--background: ${hexToHSL(colors.background)};
|
||||
--foreground: ${hexToHSL(colors.foreground)};
|
||||
--card: ${hexToHSL(colors.card)};
|
||||
--card-foreground: ${hexToHSL(colors["card-foreground"])};
|
||||
--popover: ${hexToHSL(colors.popover)};
|
||||
--popover-foreground: ${hexToHSL(colors["popover-foreground"])};
|
||||
--primary: ${hexToHSL(colors.primary)};
|
||||
--primary-foreground: ${hexToHSL(colors["primary-foreground"])};
|
||||
--secondary: ${hexToHSL(colors.secondary)};
|
||||
--secondary-foreground: ${hexToHSL(colors["secondary-foreground"])};
|
||||
--muted: ${hexToHSL(colors.muted)};
|
||||
--muted-foreground: ${hexToHSL(colors["muted-foreground"])};
|
||||
--accent: ${hexToHSL(colors.accent)};
|
||||
--accent-foreground: ${hexToHSL(colors["accent-foreground"])};
|
||||
--destructive: ${hexToHSL(colors.destructive)};
|
||||
--destructive-foreground: ${hexToHSL(colors["destructive-foreground"])};
|
||||
--border: ${hexToHSL(colors.border)};
|
||||
--input: ${hexToHSL(colors.input)};
|
||||
--ring: ${hexToHSL(colors.ring)};
|
||||
--radius: ${radius}rem;
|
||||
}
|
||||
.theme-components-container[data-theme="custom-components"] * {
|
||||
border-radius: calc(var(--radius) * 1);
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="rounded-lg border bg-background p-8 space-y-8"
|
||||
style={{
|
||||
backgroundColor: `hsl(var(--background))`,
|
||||
color: `hsl(var(--foreground))`,
|
||||
borderColor: `hsl(var(--border))`,
|
||||
}}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
Button Variants
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button>Primary</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="outline">Outline</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
<Button variant="destructive">Destructive</Button>
|
||||
<Button variant="link">Link</Button>
|
||||
<Button size="sm">Small</Button>
|
||||
<Button size="lg">Large</Button>
|
||||
<Button disabled>Disabled</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
Badge Variants
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge>Default</Badge>
|
||||
<Badge variant="secondary">Secondary</Badge>
|
||||
<Badge variant="outline">Outline</Badge>
|
||||
<Badge variant="destructive">Destructive</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Typography</h3>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-4xl font-bold">Heading 1</h1>
|
||||
<h2 className="text-3xl font-bold">Heading 2</h2>
|
||||
<h3 className="text-2xl font-bold">Heading 3</h3>
|
||||
<h4 className="text-xl font-bold">Heading 4</h4>
|
||||
<p className="text-lg">Large paragraph text</p>
|
||||
<p>Regular paragraph text</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Muted text
|
||||
</p>
|
||||
<p className="text-xs">Small text</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="code" className="mt-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>CSS Variables</CardTitle>
|
||||
<CardDescription>
|
||||
Copy this code and paste it into your globals.css file
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="relative">
|
||||
<pre className="overflow-auto rounded-lg bg-muted p-6 text-sm">
|
||||
<code className="language-css">{generateCSS()}</code>
|
||||
</pre>
|
||||
<Button
|
||||
onClick={copyCSS}
|
||||
size="sm"
|
||||
className="absolute right-2 top-2"
|
||||
variant="ghost"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 rounded-lg bg-muted/50 p-4">
|
||||
<p className="text-sm font-medium mb-2">
|
||||
Usage Instructions:
|
||||
</p>
|
||||
<ol className="text-sm text-muted-foreground space-y-1 list-decimal list-inside">
|
||||
<li>Copy the CSS variables above</li>
|
||||
<li>
|
||||
Open your project's global CSS file (usually globals.css
|
||||
or app.css)
|
||||
</li>
|
||||
<li>
|
||||
Replace the existing :root variables with your new theme
|
||||
</li>
|
||||
<li>Save the file and refresh your application</li>
|
||||
</ol>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,797 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
Check,
|
||||
Copy,
|
||||
Download,
|
||||
Monitor,
|
||||
Moon,
|
||||
Palette,
|
||||
RotateCcw,
|
||||
Settings2,
|
||||
Shuffle,
|
||||
Sparkles,
|
||||
Sun,
|
||||
} from "lucide-react"
|
||||
import { HexColorInput, HexColorPicker } from "react-colorful"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/registry/default/ui/alert"
|
||||
import { Badge } from "@/registry/default/ui/badge"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/default/ui/card"
|
||||
import { Checkbox } from "@/registry/default/ui/checkbox"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/default/ui/popover"
|
||||
import { Progress } from "@/registry/default/ui/progress"
|
||||
import { RadioGroup, RadioGroupItem } from "@/registry/default/ui/radio-group"
|
||||
import { ScrollArea } from "@/registry/default/ui/scroll-area"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/default/ui/select"
|
||||
import { Separator } from "@/registry/default/ui/separator"
|
||||
import { Slider } from "@/registry/default/ui/slider"
|
||||
import { Switch } from "@/registry/default/ui/switch"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/default/ui/tabs"
|
||||
import { Textarea } from "@/registry/default/ui/textarea"
|
||||
|
||||
const DEFAULT_LIGHT_COLORS = {
|
||||
background: "#ffffff",
|
||||
foreground: "#0a0a0a",
|
||||
card: "#ffffff",
|
||||
"card-foreground": "#0a0a0a",
|
||||
popover: "#ffffff",
|
||||
"popover-foreground": "#0a0a0a",
|
||||
primary: "#0099ff",
|
||||
"primary-foreground": "#ffffff",
|
||||
secondary: "#f4f4f5",
|
||||
"secondary-foreground": "#0a0a0a",
|
||||
muted: "#f4f4f5",
|
||||
"muted-foreground": "#71717a",
|
||||
accent: "#f4f4f5",
|
||||
"accent-foreground": "#0a0a0a",
|
||||
destructive: "#ef4444",
|
||||
"destructive-foreground": "#ffffff",
|
||||
border: "#e4e4e7",
|
||||
input: "#e4e4e7",
|
||||
ring: "#0099ff",
|
||||
}
|
||||
|
||||
const DEFAULT_DARK_COLORS = {
|
||||
background: "#0a0a0a",
|
||||
foreground: "#fafafa",
|
||||
card: "#0a0a0a",
|
||||
"card-foreground": "#fafafa",
|
||||
popover: "#0a0a0a",
|
||||
"popover-foreground": "#fafafa",
|
||||
primary: "#0099ff",
|
||||
"primary-foreground": "#ffffff",
|
||||
secondary: "#262626",
|
||||
"secondary-foreground": "#fafafa",
|
||||
muted: "#262626",
|
||||
"muted-foreground": "#a3a3a3",
|
||||
accent: "#262626",
|
||||
"accent-foreground": "#fafafa",
|
||||
destructive: "#dc2626",
|
||||
"destructive-foreground": "#ffffff",
|
||||
border: "#262626",
|
||||
input: "#262626",
|
||||
ring: "#0099ff",
|
||||
}
|
||||
|
||||
const PRESET_THEMES = {
|
||||
"Electric Blue": {
|
||||
primary: "#0099ff",
|
||||
secondary: "#1e293b",
|
||||
accent: "#06b6d4",
|
||||
},
|
||||
Forest: {
|
||||
primary: "#10b981",
|
||||
secondary: "#065f46",
|
||||
accent: "#34d399",
|
||||
},
|
||||
Sunset: {
|
||||
primary: "#f97316",
|
||||
secondary: "#dc2626",
|
||||
accent: "#fbbf24",
|
||||
},
|
||||
"Purple Haze": {
|
||||
primary: "#8b5cf6",
|
||||
secondary: "#7c3aed",
|
||||
accent: "#a78bfa",
|
||||
},
|
||||
Ocean: {
|
||||
primary: "#0ea5e9",
|
||||
secondary: "#0284c7",
|
||||
accent: "#38bdf8",
|
||||
},
|
||||
}
|
||||
|
||||
export function ThemeGeneratorClient() {
|
||||
const [colors, setColors] = React.useState(DEFAULT_LIGHT_COLORS)
|
||||
const [radius, setRadius] = React.useState(0.5)
|
||||
const [mode, setMode] = React.useState<"light" | "dark">("light")
|
||||
const [copied, setCopied] = React.useState(false)
|
||||
const [activeColorKey, setActiveColorKey] = React.useState<string | null>(
|
||||
null
|
||||
)
|
||||
|
||||
const updateColor = (key: string, value: string) => {
|
||||
setColors((prev) => ({ ...prev, [key]: value }))
|
||||
}
|
||||
|
||||
const toggleMode = () => {
|
||||
const newMode = mode === "light" ? "dark" : "light"
|
||||
setMode(newMode)
|
||||
setColors(newMode === "light" ? DEFAULT_LIGHT_COLORS : DEFAULT_DARK_COLORS)
|
||||
}
|
||||
|
||||
const hexToHSL = (hex: string) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
if (!result) return "0 0% 0%"
|
||||
|
||||
const r = parseInt(result[1], 16) / 255
|
||||
const g = parseInt(result[2], 16) / 255
|
||||
const b = parseInt(result[3], 16) / 255
|
||||
|
||||
const max = Math.max(r, g, b)
|
||||
const min = Math.min(r, g, b)
|
||||
let h = 0,
|
||||
s = 0,
|
||||
l = (max + min) / 2
|
||||
|
||||
if (max !== min) {
|
||||
const d = max - min
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
||||
switch (max) {
|
||||
case r:
|
||||
h = ((g - b) / d + (g < b ? 6 : 0)) / 6
|
||||
break
|
||||
case g:
|
||||
h = ((b - r) / d + 2) / 6
|
||||
break
|
||||
case b:
|
||||
h = ((r - g) / d + 4) / 6
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return `${Math.round(h * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`
|
||||
}
|
||||
|
||||
const generateCSS = () => {
|
||||
const css = `@layer base {
|
||||
:root {
|
||||
${Object.entries(colors)
|
||||
.map(([key, value]) => ` --${key}: ${hexToHSL(value)};`)
|
||||
.join("\n")}
|
||||
--radius: ${radius}rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
${Object.entries(colors)
|
||||
.map(([key, value]) => ` --${key}: ${hexToHSL(value)};`)
|
||||
.join("\n")}
|
||||
}
|
||||
}`
|
||||
return css
|
||||
}
|
||||
|
||||
const copyCSS = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(generateCSS())
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch (err) {
|
||||
console.error("Failed to copy CSS:", err)
|
||||
}
|
||||
}
|
||||
|
||||
const applyPresetTheme = (presetName: string) => {
|
||||
const preset = PRESET_THEMES[presetName as keyof typeof PRESET_THEMES]
|
||||
if (preset) {
|
||||
setColors((prev) => ({
|
||||
...prev,
|
||||
primary: preset.primary,
|
||||
"primary-foreground": mode === "dark" ? "#ffffff" : "#ffffff",
|
||||
secondary: preset.secondary,
|
||||
"secondary-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
accent: preset.accent,
|
||||
"accent-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
ring: preset.primary,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
const randomizeTheme = () => {
|
||||
const randomHex = () => {
|
||||
const hue = Math.floor(Math.random() * 360)
|
||||
const saturation = 50 + Math.floor(Math.random() * 50)
|
||||
const lightness = 40 + Math.floor(Math.random() * 30)
|
||||
return hslToHex(hue, saturation, lightness)
|
||||
}
|
||||
|
||||
const primary = randomHex()
|
||||
const secondary = randomHex()
|
||||
const accent = randomHex()
|
||||
|
||||
setColors((prev) => ({
|
||||
...prev,
|
||||
primary,
|
||||
"primary-foreground": "#ffffff",
|
||||
secondary,
|
||||
"secondary-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
accent,
|
||||
"accent-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
ring: primary,
|
||||
}))
|
||||
}
|
||||
|
||||
const hslToHex = (h: number, s: number, l: number) => {
|
||||
l /= 100
|
||||
const a = (s * Math.min(l, 1 - l)) / 100
|
||||
const f = (n: number) => {
|
||||
const k = (n + h / 30) % 12
|
||||
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1)
|
||||
return Math.round(255 * color)
|
||||
.toString(16)
|
||||
.padStart(2, "0")
|
||||
}
|
||||
return `#${f(0)}${f(8)}${f(4)}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container max-w-[1600px] py-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold tracking-tight flex items-center gap-3">
|
||||
<Palette className="h-8 w-8 text-primary" />
|
||||
Theme Generator
|
||||
</h1>
|
||||
<p className="mt-2 text-lg text-muted-foreground">
|
||||
Create beautiful themes with live preview and instant export
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={toggleMode}
|
||||
className="relative"
|
||||
>
|
||||
{mode === "light" ? (
|
||||
<Moon className="h-5 w-5" />
|
||||
) : (
|
||||
<Sun className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 lg:grid-cols-[420px_1fr]">
|
||||
{/* Enhanced Controls Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Preset Themes */}
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader className="bg-gradient-to-r from-primary/10 to-primary/5">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5" />
|
||||
Preset Themes
|
||||
</CardTitle>
|
||||
<CardDescription>Start with a pre-designed theme</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{Object.keys(PRESET_THEMES).map((presetName) => (
|
||||
<Button
|
||||
key={presetName}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => applyPresetTheme(presetName)}
|
||||
className="justify-start text-xs"
|
||||
>
|
||||
{presetName}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Color Customization */}
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Settings2 className="h-5 w-5" />
|
||||
Color Palette
|
||||
</CardTitle>
|
||||
<CardDescription>Fine-tune your color scheme</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<ScrollArea className="h-[400px] pr-4">
|
||||
<div className="space-y-3">
|
||||
{Object.entries(colors).map(([key, value]) => (
|
||||
<div key={key} className="space-y-1.5">
|
||||
<Label className="text-xs font-medium capitalize">
|
||||
{key.replace(/-/g, " ")}
|
||||
</Label>
|
||||
<Popover
|
||||
open={activeColorKey === key}
|
||||
onOpenChange={(open) =>
|
||||
setActiveColorKey(open ? key : null)
|
||||
}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full justify-between font-mono text-xs h-9"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="h-5 w-5 rounded-md border shadow-sm"
|
||||
style={{ backgroundColor: value }}
|
||||
/>
|
||||
<span>{value.toUpperCase()}</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground">
|
||||
HSL: {hexToHSL(value).split(" ").join(", ")}
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-3" align="start">
|
||||
<div className="space-y-3">
|
||||
<HexColorPicker
|
||||
color={value}
|
||||
onChange={(v) => updateColor(key, v)}
|
||||
style={{ width: 200, height: 200 }}
|
||||
/>
|
||||
<HexColorInput
|
||||
color={value}
|
||||
onChange={(v) => updateColor(key, v)}
|
||||
className="w-full rounded-md border px-3 py-1 text-sm font-mono"
|
||||
prefixed
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Border Radius Control */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Border Radius</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<Slider
|
||||
value={[radius]}
|
||||
onValueChange={(v) => setRadius(v[0])}
|
||||
min={0}
|
||||
max={1.5}
|
||||
step={0.05}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>Sharp</span>
|
||||
<span className="font-mono">{radius}rem</span>
|
||||
<span>Rounded</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
onClick={randomizeTheme}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
>
|
||||
<Shuffle className="mr-2 h-4 w-4" />
|
||||
Randomize
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setColors(
|
||||
mode === "light" ? DEFAULT_LIGHT_COLORS : DEFAULT_DARK_COLORS
|
||||
)
|
||||
}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
>
|
||||
<RotateCcw className="mr-2 h-4 w-4" />
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button onClick={copyCSS} className="w-full" size="lg">
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="mr-2 h-4 w-4" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy CSS Variables
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Enhanced Preview Area */}
|
||||
<div className="space-y-6">
|
||||
<Tabs defaultValue="preview" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="preview">Live Preview</TabsTrigger>
|
||||
<TabsTrigger value="components">Components</TabsTrigger>
|
||||
<TabsTrigger value="code">Export Code</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="preview" className="mt-6">
|
||||
<div className="theme-preview-container" data-theme="custom">
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
.theme-preview-container[data-theme="custom"] {
|
||||
--background: ${hexToHSL(colors.background)};
|
||||
--foreground: ${hexToHSL(colors.foreground)};
|
||||
--card: ${hexToHSL(colors.card)};
|
||||
--card-foreground: ${hexToHSL(colors["card-foreground"])};
|
||||
--popover: ${hexToHSL(colors.popover)};
|
||||
--popover-foreground: ${hexToHSL(colors["popover-foreground"])};
|
||||
--primary: ${hexToHSL(colors.primary)};
|
||||
--primary-foreground: ${hexToHSL(colors["primary-foreground"])};
|
||||
--secondary: ${hexToHSL(colors.secondary)};
|
||||
--secondary-foreground: ${hexToHSL(colors["secondary-foreground"])};
|
||||
--muted: ${hexToHSL(colors.muted)};
|
||||
--muted-foreground: ${hexToHSL(colors["muted-foreground"])};
|
||||
--accent: ${hexToHSL(colors.accent)};
|
||||
--accent-foreground: ${hexToHSL(colors["accent-foreground"])};
|
||||
--destructive: ${hexToHSL(colors.destructive)};
|
||||
--destructive-foreground: ${hexToHSL(colors["destructive-foreground"])};
|
||||
--border: ${hexToHSL(colors.border)};
|
||||
--input: ${hexToHSL(colors.input)};
|
||||
--ring: ${hexToHSL(colors.ring)};
|
||||
--radius: ${radius}rem;
|
||||
}
|
||||
.theme-preview-container[data-theme="custom"] * {
|
||||
border-radius: calc(var(--radius) * 1);
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="rounded-lg border bg-background p-8 shadow-sm"
|
||||
style={{
|
||||
backgroundColor: `hsl(var(--background))`,
|
||||
color: `hsl(var(--foreground))`,
|
||||
borderColor: `hsl(var(--border))`,
|
||||
}}
|
||||
>
|
||||
<div className="space-y-8">
|
||||
{/* Hero Section Preview */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-3xl font-bold">
|
||||
Welcome to Your App
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
This is how your theme looks in a real application
|
||||
context
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button size="lg">Get Started</Button>
|
||||
<Button size="lg" variant="secondary">
|
||||
Learn More
|
||||
</Button>
|
||||
<Button size="lg" variant="outline">
|
||||
Documentation
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Form Elements */}
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Form Elements</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
placeholder="name@example.com"
|
||||
type="email"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="message">Message</Label>
|
||||
<Textarea
|
||||
id="message"
|
||||
placeholder="Type your message here..."
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox id="terms" />
|
||||
<Label htmlFor="terms" className="text-sm">
|
||||
Accept terms and conditions
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Selections</h3>
|
||||
<div className="space-y-3">
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an option" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="option1">Option 1</SelectItem>
|
||||
<SelectItem value="option2">Option 2</SelectItem>
|
||||
<SelectItem value="option3">Option 3</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<RadioGroup defaultValue="option-one">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="option-one"
|
||||
id="option-one"
|
||||
/>
|
||||
<Label htmlFor="option-one">Option One</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="option-two"
|
||||
id="option-two"
|
||||
/>
|
||||
<Label htmlFor="option-two">Option Two</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="notifications" />
|
||||
<Label htmlFor="notifications">
|
||||
Enable notifications
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cards */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Card Title</CardTitle>
|
||||
<CardDescription>
|
||||
Card description goes here
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This is how cards appear with your custom theme. The
|
||||
borders, shadows, and colors all adapt to your
|
||||
selections.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Statistics</CardTitle>
|
||||
<CardDescription>
|
||||
Your performance metrics
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm">Progress</span>
|
||||
<span className="text-sm font-medium">75%</span>
|
||||
</div>
|
||||
<Progress value={75} className="h-2" />
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Badge>Active</Badge>
|
||||
<Badge variant="secondary">Updated</Badge>
|
||||
<Badge variant="outline">New</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Alert */}
|
||||
<Alert>
|
||||
<AlertTitle>Heads up!</AlertTitle>
|
||||
<AlertDescription>
|
||||
You can customize every aspect of your theme using the
|
||||
controls on the left. Changes are reflected in
|
||||
real-time.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="components" className="mt-6">
|
||||
<div
|
||||
className="theme-components-container"
|
||||
data-theme="custom-components"
|
||||
>
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
.theme-components-container[data-theme="custom-components"] {
|
||||
--background: ${hexToHSL(colors.background)};
|
||||
--foreground: ${hexToHSL(colors.foreground)};
|
||||
--card: ${hexToHSL(colors.card)};
|
||||
--card-foreground: ${hexToHSL(colors["card-foreground"])};
|
||||
--popover: ${hexToHSL(colors.popover)};
|
||||
--popover-foreground: ${hexToHSL(colors["popover-foreground"])};
|
||||
--primary: ${hexToHSL(colors.primary)};
|
||||
--primary-foreground: ${hexToHSL(colors["primary-foreground"])};
|
||||
--secondary: ${hexToHSL(colors.secondary)};
|
||||
--secondary-foreground: ${hexToHSL(colors["secondary-foreground"])};
|
||||
--muted: ${hexToHSL(colors.muted)};
|
||||
--muted-foreground: ${hexToHSL(colors["muted-foreground"])};
|
||||
--accent: ${hexToHSL(colors.accent)};
|
||||
--accent-foreground: ${hexToHSL(colors["accent-foreground"])};
|
||||
--destructive: ${hexToHSL(colors.destructive)};
|
||||
--destructive-foreground: ${hexToHSL(colors["destructive-foreground"])};
|
||||
--border: ${hexToHSL(colors.border)};
|
||||
--input: ${hexToHSL(colors.input)};
|
||||
--ring: ${hexToHSL(colors.ring)};
|
||||
--radius: ${radius}rem;
|
||||
}
|
||||
.theme-components-container[data-theme="custom-components"] * {
|
||||
border-radius: calc(var(--radius) * 1);
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="rounded-lg border bg-background p-8 space-y-8"
|
||||
style={{
|
||||
backgroundColor: `hsl(var(--background))`,
|
||||
color: `hsl(var(--foreground))`,
|
||||
borderColor: `hsl(var(--border))`,
|
||||
}}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
Button Variants
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button>Primary</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="outline">Outline</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
<Button variant="destructive">Destructive</Button>
|
||||
<Button variant="link">Link</Button>
|
||||
<Button size="sm">Small</Button>
|
||||
<Button size="lg">Large</Button>
|
||||
<Button disabled>Disabled</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
Badge Variants
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge>Default</Badge>
|
||||
<Badge variant="secondary">Secondary</Badge>
|
||||
<Badge variant="outline">Outline</Badge>
|
||||
<Badge variant="destructive">Destructive</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Typography</h3>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-4xl font-bold">Heading 1</h1>
|
||||
<h2 className="text-3xl font-bold">Heading 2</h2>
|
||||
<h3 className="text-2xl font-bold">Heading 3</h3>
|
||||
<h4 className="text-xl font-bold">Heading 4</h4>
|
||||
<p className="text-lg">Large paragraph text</p>
|
||||
<p>Regular paragraph text</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Muted text
|
||||
</p>
|
||||
<p className="text-xs">Small text</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="code" className="mt-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>CSS Variables</CardTitle>
|
||||
<CardDescription>
|
||||
Copy this code and paste it into your globals.css file
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="relative">
|
||||
<pre className="overflow-auto rounded-lg bg-muted p-6 text-sm">
|
||||
<code className="language-css">{generateCSS()}</code>
|
||||
</pre>
|
||||
<Button
|
||||
onClick={copyCSS}
|
||||
size="sm"
|
||||
className="absolute right-2 top-2"
|
||||
variant="ghost"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 rounded-lg bg-muted/50 p-4">
|
||||
<p className="text-sm font-medium mb-2">
|
||||
Usage Instructions:
|
||||
</p>
|
||||
<ol className="text-sm text-muted-foreground space-y-1 list-decimal list-inside">
|
||||
<li>Copy the CSS variables above</li>
|
||||
<li>
|
||||
Open your project's global CSS file (usually globals.css
|
||||
or app.css)
|
||||
</li>
|
||||
<li>
|
||||
Replace the existing :root variables with your new theme
|
||||
</li>
|
||||
<li>Save the file and refresh your application</li>
|
||||
</ol>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* POST /api/chat — AI chat about UI components.
|
||||
*
|
||||
* Uses Hanzo AI (zen-coder-flash) to answer questions about
|
||||
* Hanzo UI components, props, patterns, and composition.
|
||||
* Compatible with Vercel AI SDK useChat hook.
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export const dynamic = "force-static"
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json(
|
||||
{ error: 'AI chat requires a server runtime. Use api.hanzo.ai/v1 directly.' },
|
||||
{ status: 501 },
|
||||
)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/**
|
||||
* GET /api/registry/components/:name — Get component with full source.
|
||||
*
|
||||
* Returns the component JSON including embedded source code.
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getComponent, getComponentMap } from "../../lib"
|
||||
|
||||
export const dynamic = "force-static"
|
||||
export const dynamicParams = false
|
||||
|
||||
export function generateStaticParams() {
|
||||
const map = getComponentMap()
|
||||
return Array.from(map.keys()).map((name) => ({ name }))
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: Promise<{ name: string }> }
|
||||
) {
|
||||
const { name } = await params
|
||||
const component = getComponent(name)
|
||||
|
||||
if (!component) {
|
||||
return NextResponse.json(
|
||||
{ error: "Component not found", name },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(component, {
|
||||
headers: {
|
||||
"Cache-Control": "public, s-maxage=900, stale-while-revalidate=1800",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* GET /api/registry/index — Full registry manifest.
|
||||
*
|
||||
* Returns ALL components with source in a single payload.
|
||||
* MCP clients can hydrate their cache with one HTTP call.
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server"
|
||||
import { getFullManifest } from "../lib"
|
||||
|
||||
export const dynamic = "force-static"
|
||||
|
||||
export async function GET() {
|
||||
const manifest = getFullManifest()
|
||||
|
||||
return NextResponse.json(manifest, {
|
||||
headers: {
|
||||
"Cache-Control": "public, s-maxage=900, stale-while-revalidate=1800",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user