mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-11 18:01:34 +00:00
Compare commits
863 Commits
shadcn@3.8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea9d371a2d | ||
|
|
d15f17d717 | ||
|
|
46ca8c5d4b | ||
|
|
a5eb279650 | ||
|
|
1994caba0b | ||
|
|
1450bea8d6 | ||
|
|
ced2a5beb5 | ||
|
|
10f1717a3e | ||
|
|
deba036d50 | ||
|
|
5bd81beebf | ||
|
|
3f2ff18157 | ||
|
|
05eb2b968b | ||
|
|
a721cc08e5 | ||
|
|
8da4592308 | ||
|
|
f47d48f316 | ||
|
|
e6d9d6023b | ||
|
|
7dfd933102 | ||
|
|
9c6a5ee1b1 | ||
|
|
c87897b2a5 | ||
|
|
c61197f627 | ||
|
|
a1fb619cef | ||
|
|
d84c4a8ca5 | ||
|
|
cd54e0927f | ||
|
|
adac7cae1f | ||
|
|
7c63c46736 | ||
|
|
916c012132 | ||
|
|
460ad60d84 | ||
|
|
8e2d2d1439 | ||
|
|
67cef8fcb9 | ||
|
|
4ff43ba694 | ||
|
|
efdec3ca45 | ||
|
|
5c849297d0 | ||
|
|
2baa86081d | ||
|
|
980f288149 | ||
|
|
07900769d9 | ||
|
|
360e8a19c3 | ||
|
|
e2fa0101e3 | ||
|
|
55ea86f252 | ||
|
|
f584f05489 | ||
|
|
a06ba18dcc | ||
|
|
f3e16e7db7 | ||
|
|
64afddefd9 | ||
|
|
c873713992 | ||
|
|
3751fdfa4c | ||
|
|
c824d6b78d | ||
|
|
df1752dfe0 | ||
|
|
e826e543f2 | ||
|
|
f7eecafb45 | ||
|
|
6e6cf9ee96 | ||
|
|
5b628e23e3 | ||
|
|
4a4dc8eb0f | ||
|
|
a33becad35 | ||
|
|
d60e8b6ce3 | ||
|
|
072c27fcd5 | ||
|
|
194dcc4571 | ||
|
|
51e3cfaf32 | ||
|
|
c8ab3801ec | ||
|
|
731e6dd8a2 | ||
|
|
d7066f4a2d | ||
|
|
5274de83d6 | ||
|
|
7e4dac7f31 | ||
|
|
28122dba18 | ||
|
|
93cde61946 | ||
|
|
c2dc06a99c | ||
|
|
c9930b7fda | ||
|
|
d1149454a8 | ||
|
|
36139f6200 | ||
|
|
15ac1be92b | ||
|
|
8ca30ed32c | ||
|
|
e2605bc7c2 | ||
|
|
b8608d0976 | ||
|
|
fc1ca40af4 | ||
|
|
f454f6e4d1 | ||
|
|
8cc7073aec | ||
|
|
031387a471 | ||
|
|
dd3567c39d | ||
|
|
ad2b8891a5 | ||
|
|
f6e18c65cf | ||
|
|
1c4a53a37a | ||
|
|
bc2db187aa | ||
|
|
92b4927a80 | ||
|
|
3cbabe012e | ||
|
|
1137b24a97 | ||
|
|
bb251e2ab6 | ||
|
|
28b3e5f360 | ||
|
|
309d95017f | ||
|
|
eb42ae25fd | ||
|
|
3977fb9ace | ||
|
|
7865621397 | ||
|
|
b07070cd07 | ||
|
|
ad68a44717 | ||
|
|
56161142f1 | ||
|
|
c2e1a5793f | ||
|
|
ea6086cbcc | ||
|
|
68a69d81f7 | ||
|
|
55fd4dc71b | ||
|
|
6dea65ebcb | ||
|
|
ba10089b8d | ||
|
|
8a814f926b | ||
|
|
c236d0c009 | ||
|
|
fd0e0c369b | ||
|
|
07d14abde1 | ||
|
|
8dd51c49f8 | ||
|
|
c20e0cc596 | ||
|
|
0126502236 | ||
|
|
94074e4bb2 | ||
|
|
eb6e783fb3 | ||
|
|
f785bfab44 | ||
|
|
cc20c8a794 | ||
|
|
05948dce8e | ||
|
|
5d23df4e35 | ||
|
|
abbdd32953 | ||
|
|
3f14ffa632 | ||
|
|
5927f6de80 | ||
|
|
39eb34104b | ||
|
|
7cbc7e8d53 | ||
|
|
d0ac558ce2 | ||
|
|
bc0c46a93c | ||
|
|
a64575d8a4 | ||
|
|
5d0cd7819b | ||
|
|
13478b26b6 | ||
|
|
aee8a71679 | ||
|
|
4507f1c794 | ||
|
|
81cd2266aa | ||
|
|
cf756b1b55 | ||
|
|
5e61f9c4a4 | ||
|
|
c4def9305f | ||
|
|
e456fed9d3 | ||
|
|
b95cd29508 | ||
|
|
11cbc32840 | ||
|
|
01539fb4d7 | ||
|
|
e47ee89dcf | ||
|
|
2f5c32c0b1 | ||
|
|
fbfe9f34bb | ||
|
|
d55e059fda | ||
|
|
9c572ab778 | ||
|
|
91403eeb63 | ||
|
|
3411d53856 | ||
|
|
efa2b38d07 | ||
|
|
d00605c5fb | ||
|
|
4bdeea4c63 | ||
|
|
f632f5d798 | ||
|
|
7d6d489f83 | ||
|
|
e8b1be1f22 | ||
|
|
d987955893 | ||
|
|
7b5435ac0b | ||
|
|
f289497e35 | ||
|
|
0d266984e6 | ||
|
|
cf92d4f8f2 | ||
|
|
b7cfc364ac | ||
|
|
de385d04fc | ||
|
|
b9f78c8a35 | ||
|
|
97b9e7b0ae | ||
|
|
e4b25981bf | ||
|
|
1017410468 | ||
|
|
fa71bb8624 | ||
|
|
d99839ec2a | ||
|
|
70b6bfd687 | ||
|
|
541c08f112 | ||
|
|
420433ae6f | ||
|
|
a7d77e0cf7 | ||
|
|
7ec2acc87d | ||
|
|
eeb5d22fe5 | ||
|
|
a757e80242 | ||
|
|
84d1d476b1 | ||
|
|
a52a606fb5 | ||
|
|
6ba39bb720 | ||
|
|
dd4b5c287c | ||
|
|
aa534e5875 | ||
|
|
2be9640c88 | ||
|
|
56567ae21a | ||
|
|
429e258322 | ||
|
|
2f57100061 | ||
|
|
fc62d5781d | ||
|
|
d86c5e5939 | ||
|
|
8006dd1c93 | ||
|
|
1dcbb4c88a | ||
|
|
4f4ffde4aa | ||
|
|
6d7a0ed93b | ||
|
|
b909b0363f | ||
|
|
a6fa6893eb | ||
|
|
561586bd98 | ||
|
|
7ddb30aade | ||
|
|
024425d45a | ||
|
|
4bdaf48f9b | ||
|
|
e9546e87ff | ||
|
|
0b34d581f9 | ||
|
|
5c2ed5e90e | ||
|
|
e9443ccd4a | ||
|
|
1fe0fe65e8 | ||
|
|
6823bad998 | ||
|
|
398e6c3406 | ||
|
|
710cc27de7 | ||
|
|
08212a478d | ||
|
|
d718a8045f | ||
|
|
2c4678c8c8 | ||
|
|
2466a300f4 | ||
|
|
66fcf1e853 | ||
|
|
5ebd54198d | ||
|
|
3a2d812510 | ||
|
|
7811557088 | ||
|
|
575f1602a1 | ||
|
|
50dc9b506b | ||
|
|
ae70ecc2f3 | ||
|
|
42284f4e64 | ||
|
|
6b5aa16668 | ||
|
|
706806a207 | ||
|
|
8a7502d7fa | ||
|
|
abc65a4871 | ||
|
|
7d5af61468 | ||
|
|
2badcdc31f | ||
|
|
64b8263450 | ||
|
|
13b4593f37 | ||
|
|
7dc65da6b2 | ||
|
|
98e56b773c | ||
|
|
7ff9778ff0 | ||
|
|
4af7bbf4ba | ||
|
|
f00a94d9e5 | ||
|
|
187ae44fa7 | ||
|
|
034178bf7d | ||
|
|
4064c78bc7 | ||
|
|
943b023b7c | ||
|
|
e3d654fd26 | ||
|
|
71d0470be1 | ||
|
|
53bbdc738f | ||
|
|
97707ec08e | ||
|
|
b9ce2f10c3 | ||
|
|
7cb3b13a33 | ||
|
|
e3d2b14911 | ||
|
|
58c9dc2a7e | ||
|
|
3bdf60340d | ||
|
|
c1e29824cd | ||
|
|
62f6df75f2 | ||
|
|
62bae86e86 | ||
|
|
aa69fbf85a | ||
|
|
8d41295f2c | ||
|
|
2b053d916d | ||
|
|
0d1309f322 | ||
|
|
c26250dcfe | ||
|
|
07c5c36be8 | ||
|
|
21c9cc5246 | ||
|
|
058960046a | ||
|
|
be80c18ea9 | ||
|
|
3c59a0cd95 | ||
|
|
26d0228ee9 | ||
|
|
9050646893 | ||
|
|
3ca09b9647 | ||
|
|
720ccca653 | ||
|
|
1e3dff8daa | ||
|
|
c116b325ab | ||
|
|
5b266d3fc9 | ||
|
|
6095e6272d | ||
|
|
f3fc5a62f2 | ||
|
|
ef7507cc9a | ||
|
|
16b7bea50d | ||
|
|
ccc4caad9c | ||
|
|
ba2c4fc586 | ||
|
|
bb5afb2df1 | ||
|
|
53f45f5f6f | ||
|
|
990040691c | ||
|
|
83857679cb | ||
|
|
61989da8ec | ||
|
|
768d8a808f | ||
|
|
95479a06bb | ||
|
|
4289d5fe02 | ||
|
|
5a6702845d | ||
|
|
ebf2192d98 | ||
|
|
44c09a19b0 | ||
|
|
4101ec98af | ||
|
|
a7c3300d7a | ||
|
|
b50acc9d21 | ||
|
|
fc76a9ada2 | ||
|
|
d6b4bf8ddc | ||
|
|
2c334c3c2d | ||
|
|
d3de6aa760 | ||
|
|
23b2ac4dcf | ||
|
|
e56c476105 | ||
|
|
14bb486174 | ||
|
|
12b49c986f | ||
|
|
64c8cd99ee | ||
|
|
7d718ddaa9 | ||
|
|
5570b3e24a | ||
|
|
945298ed2d | ||
|
|
f9b216af77 | ||
|
|
6525227036 | ||
|
|
214b1b8479 | ||
|
|
8bd161d453 | ||
|
|
64b88b6cdb | ||
|
|
0c25e712e1 | ||
|
|
6a070bf8c5 | ||
|
|
124495f0df | ||
|
|
43f64065b7 | ||
|
|
4f421aba65 | ||
|
|
8bec9c1234 | ||
|
|
ba6ac6ec63 | ||
|
|
b75796ed76 | ||
|
|
d82b4a7d98 | ||
|
|
5b79499d23 | ||
|
|
d78ff8b858 | ||
|
|
ef78384bfd | ||
|
|
d3ab7fb00b | ||
|
|
bebc4356af | ||
|
|
14bc966fee | ||
|
|
6a4b27b80d | ||
|
|
c5b4080649 | ||
|
|
408b25c82a | ||
|
|
228b0e3ecd | ||
|
|
f900bd57d0 | ||
|
|
6b190c6a18 | ||
|
|
c43bc4f5d6 | ||
|
|
9cd14a684f | ||
|
|
fc1675e54d | ||
|
|
a5abe1aa0f | ||
|
|
031998436f | ||
|
|
29cb65c26b | ||
|
|
179c0c0b23 | ||
|
|
03430e03bf | ||
|
|
169682d87a | ||
|
|
336eee688e | ||
|
|
32e4827559 | ||
|
|
7a81328b23 | ||
|
|
5b40b9de5a | ||
|
|
e327cef2c1 | ||
|
|
563d572ba0 | ||
|
|
687f09817b | ||
|
|
31dbc6fc91 | ||
|
|
8db2be8b09 | ||
|
|
a8bd00466a | ||
|
|
e78bb7b4f3 | ||
|
|
acaa0953df | ||
|
|
632e2c012e | ||
|
|
78f6a8b0f0 | ||
|
|
a9f997d00a | ||
|
|
dbe1fa76b3 | ||
|
|
74c4c7508b | ||
|
|
4809da6f9c | ||
|
|
7ffefce9e0 | ||
|
|
6cad522930 | ||
|
|
d683b05d7f | ||
|
|
e000e17856 | ||
|
|
1be8f98c46 | ||
|
|
6e476e4756 | ||
|
|
e2d36a3a7d | ||
|
|
a97ebe54f1 | ||
|
|
b2cc0dfe59 | ||
|
|
af99d4ebd3 | ||
|
|
a0a072dcdd | ||
|
|
447c7aac06 | ||
|
|
752615f231 | ||
|
|
f9b365bc7f | ||
|
|
17a1a9093a | ||
|
|
8159e98075 | ||
|
|
6a527b3e75 | ||
|
|
ebe689e85c | ||
|
|
8b683b44e6 | ||
|
|
725ca574f6 | ||
|
|
1dc39e2484 | ||
|
|
090556691c | ||
|
|
8e9f781cdb | ||
|
|
9d7c205442 | ||
|
|
902379fa3e | ||
|
|
94dcf37add | ||
|
|
843a5e2334 | ||
|
|
cdaad392ae | ||
|
|
49abe0d594 | ||
|
|
eeb33ae9c9 | ||
|
|
55fa1bb7cc | ||
|
|
90bbbb7993 | ||
|
|
fc9705665c | ||
|
|
52c477e118 | ||
|
|
41a4573002 | ||
|
|
f813fb5884 | ||
|
|
429c001412 | ||
|
|
cd7743cbc1 | ||
|
|
cadc3f96de | ||
|
|
a74515d6e1 | ||
|
|
0df9af0d75 | ||
|
|
e653c1a833 | ||
|
|
8b819e1db4 | ||
|
|
5236bfdf07 | ||
|
|
7e93eb81ea | ||
|
|
abf1555a65 | ||
|
|
584db77fee | ||
|
|
3faa91d670 | ||
|
|
2bf8ef86b9 | ||
|
|
624a4fe320 | ||
|
|
5508b5e4ec | ||
|
|
3af2ba80e8 | ||
|
|
40a00278ab | ||
|
|
5ab89f3ae3 | ||
|
|
40dc195fad | ||
|
|
ca374ad0a0 | ||
|
|
bc9f556c38 | ||
|
|
d06e54d2bb | ||
|
|
65ddce2886 | ||
|
|
f413598ba3 | ||
|
|
34c04d5f01 | ||
|
|
0029b3b6f7 | ||
|
|
75cc35272a | ||
|
|
821ac7ee4d | ||
|
|
8df46c4ded | ||
|
|
2303ce2372 | ||
|
|
cf672a9575 | ||
|
|
5ee4567353 | ||
|
|
6f72dba9c4 | ||
|
|
cd717896fa | ||
|
|
d47562cc08 | ||
|
|
aff5d7f0c1 | ||
|
|
4c0be13dcc | ||
|
|
afa410e47f | ||
|
|
7ee55e8bd3 | ||
|
|
e3c9a3f9dc | ||
|
|
aa841e35cf | ||
|
|
598fb2f55a | ||
|
|
aa786280a3 | ||
|
|
07fd9d0ea4 | ||
|
|
6ad0590d87 | ||
|
|
ff51e9ca3c | ||
|
|
7958cc6a33 | ||
|
|
2871e15418 | ||
|
|
c7d57548e5 | ||
|
|
dac13c90f2 | ||
|
|
50d8b764a9 | ||
|
|
90fc0b2dff | ||
|
|
d9d43d5b3b | ||
|
|
ce2c3ca688 | ||
|
|
3bd1bbe858 | ||
|
|
448fb0bc06 | ||
|
|
09a84892d9 | ||
|
|
16da5f2a56 | ||
|
|
f5f2a02eda | ||
|
|
dad006aa1e | ||
|
|
20a94ddb77 | ||
|
|
ae733168cd | ||
|
|
49616d2e16 | ||
|
|
7bc47bb858 | ||
|
|
e149aac756 | ||
|
|
62abc6be99 | ||
|
|
0072c9801f | ||
|
|
729708ad2e | ||
|
|
a4c6504c96 | ||
|
|
1bd5f3d7c8 | ||
|
|
3d6ea09c50 | ||
|
|
f45b8f3066 | ||
|
|
ad99fc9a73 | ||
|
|
da05ee321c | ||
|
|
de497a36bb | ||
|
|
882a9cb145 | ||
|
|
65cb5b49ff | ||
|
|
ae6f2e67aa | ||
|
|
d13d42d434 | ||
|
|
67c99dd33c | ||
|
|
ce012faf1e | ||
|
|
554a1a69a7 | ||
|
|
e489552614 | ||
|
|
8386198073 | ||
|
|
9c570f1435 | ||
|
|
ed2d9a6728 | ||
|
|
f336513d18 | ||
|
|
5755d6aa1f | ||
|
|
e363e343b7 | ||
|
|
fe955258c3 | ||
|
|
f5ac4a0d2a | ||
|
|
97ed7eb35c | ||
|
|
6909385aea | ||
|
|
8dabe113fa | ||
|
|
f5556230f1 | ||
|
|
327551f8b6 | ||
|
|
cdb4a4547f | ||
|
|
52f72b9cf7 | ||
|
|
048dac9359 | ||
|
|
f93d44730e | ||
|
|
21c64cb561 | ||
|
|
7e405f1568 | ||
|
|
04248d752e | ||
|
|
15f6a0fe49 | ||
|
|
54d254100d | ||
|
|
6d7f3479d1 | ||
|
|
5e1fca8b4e | ||
|
|
30229bfd14 | ||
|
|
1ce9c2dd6a | ||
|
|
5edf9c95b7 | ||
|
|
35657b4d5f | ||
|
|
b7786c4b42 | ||
|
|
6ca3784b67 | ||
|
|
c1b92c3175 | ||
|
|
b7afa9ba73 | ||
|
|
119d534e85 | ||
|
|
4e416dea5e | ||
|
|
b600dd7091 | ||
|
|
d59e5be214 | ||
|
|
cddbc1f3ff | ||
|
|
7c0d413e3c | ||
|
|
00de8addfe | ||
|
|
869e7bb17f | ||
|
|
8491d4207a | ||
|
|
6f31c22f11 | ||
|
|
a4c806ec26 | ||
|
|
1445fb769d | ||
|
|
a6bdaa6776 | ||
|
|
b44ca370f1 | ||
|
|
d2776903c2 | ||
|
|
936ee754b1 | ||
|
|
3a431547bb | ||
|
|
066e1e9abd | ||
|
|
b19fa88dec | ||
|
|
3aa50ddc9d | ||
|
|
f26db39334 | ||
|
|
3003e9e67a | ||
|
|
ee1303198a | ||
|
|
acb92a8df9 | ||
|
|
78410f9738 | ||
|
|
edf571debd | ||
|
|
257448bead | ||
|
|
e9f4cfb010 | ||
|
|
3c5f594b94 | ||
|
|
cf3f9f134a | ||
|
|
a643dc6ab5 | ||
|
|
8c705f8af9 | ||
|
|
28104c684d | ||
|
|
eccf6a2522 | ||
|
|
8ba3d50d7d | ||
|
|
75031d4461 | ||
|
|
13e64ea341 | ||
|
|
6034ffcd3c | ||
|
|
a749633d51 | ||
|
|
dad8a74ab4 | ||
|
|
3f03d30ce5 | ||
|
|
3365f4ebb2 | ||
|
|
68b8932406 | ||
|
|
a24351838a | ||
|
|
67b1083f3a | ||
|
|
aa4a97730a | ||
|
|
02f34a3b31 | ||
|
|
7cebd74ce5 | ||
|
|
bd1d93bbbc | ||
|
|
37ff1a3d12 | ||
|
|
308ebdbd3b | ||
|
|
cb6e798b90 | ||
|
|
2224411358 | ||
|
|
586f09a0c0 | ||
|
|
475ae744e6 | ||
|
|
553b6454f1 | ||
|
|
5805be2a2a | ||
|
|
c44d89a742 | ||
|
|
ce3fc7625a | ||
|
|
2532aeaa1d | ||
|
|
a4dafd1b32 | ||
|
|
07c87ff431 | ||
|
|
4a4b379f21 | ||
|
|
837e2bcc93 | ||
|
|
33dc7ea273 | ||
|
|
b8da7ce8b8 | ||
|
|
da3c255575 | ||
|
|
5eaad6ea6c | ||
|
|
f68e240293 | ||
|
|
ddc68e480a | ||
|
|
c31ebfaf6b | ||
|
|
e79f6e74bb | ||
|
|
57f9d875be | ||
|
|
a59144d8e1 | ||
|
|
3d8837bddb | ||
|
|
4d89b13e6f | ||
|
|
7d9689ba01 | ||
|
|
81a1dde380 | ||
|
|
8448acdf90 | ||
|
|
51b867e5dc | ||
|
|
c97ab6ee18 | ||
|
|
9584703534 | ||
|
|
f31ed81983 | ||
|
|
e85a698821 | ||
|
|
2bb09a50a1 | ||
|
|
17ed9baedb | ||
|
|
b40685050d | ||
|
|
0dab4f92ac | ||
|
|
0ddc3503a5 | ||
|
|
29ea3a7d67 | ||
|
|
823a1a42b4 | ||
|
|
0b66b1c473 | ||
|
|
934afbcf15 | ||
|
|
e0c924d2f4 | ||
|
|
a92b56491e | ||
|
|
6dcd9f4fef | ||
|
|
f5c36e520e | ||
|
|
fb2a3433e2 | ||
|
|
87ddddf41e | ||
|
|
45c8c1b873 | ||
|
|
68c9ada079 | ||
|
|
16a0473b10 | ||
|
|
4210d1ab05 | ||
|
|
bb7cf2c425 | ||
|
|
1a67379f57 | ||
|
|
d99fcf4a1c | ||
|
|
9954e2b014 | ||
|
|
7d28dfdb15 | ||
|
|
fd9c64f416 | ||
|
|
7e766f4714 | ||
|
|
9dc307f7cc | ||
|
|
47c0330610 | ||
|
|
ded8a4086f | ||
|
|
f6dc35c9a1 | ||
|
|
408d15f73f | ||
|
|
a50f6795cc | ||
|
|
da10396f2b | ||
|
|
c2f28e3ef5 | ||
|
|
40ab22fded | ||
|
|
db0482ed1f | ||
|
|
9f8a877e8f | ||
|
|
331fe02c2a | ||
|
|
34ee2a17c2 | ||
|
|
8dbb61cdd4 | ||
|
|
cc86750dfb | ||
|
|
646f884e8f | ||
|
|
fbdf6c02c1 | ||
|
|
8ab757be8d | ||
|
|
b557df5840 | ||
|
|
8271bb7f40 | ||
|
|
0008c487e9 | ||
|
|
ae68204542 | ||
|
|
e68e081d7f | ||
|
|
006dc8f9d0 | ||
|
|
b9b30a23e6 | ||
|
|
8af3cfd031 | ||
|
|
fae5e78292 | ||
|
|
a13adf8f3a | ||
|
|
dc89adf190 | ||
|
|
3fc793287b | ||
|
|
7d4dd65acd | ||
|
|
d4a2a5fe80 | ||
|
|
d9a01999e8 | ||
|
|
6bb4060686 | ||
|
|
605246f93b | ||
|
|
5ef76dece1 | ||
|
|
d24d2e6fd0 | ||
|
|
9546f3ad1e | ||
|
|
6d2c00376e | ||
|
|
117136ada3 | ||
|
|
f130d4d8c7 | ||
|
|
a46eea77a6 | ||
|
|
0b42927d38 | ||
|
|
b979ca6e79 | ||
|
|
b57e192965 | ||
|
|
91ce4cc854 | ||
|
|
b58195e154 | ||
|
|
0d3f6a0812 | ||
|
|
22ce4605d8 | ||
|
|
474d461b1c | ||
|
|
339de90b8a | ||
|
|
048313aefa | ||
|
|
805f73582f | ||
|
|
a6ab998e5c | ||
|
|
92075c8426 | ||
|
|
751c520865 | ||
|
|
4fa2ef66ed | ||
|
|
aa735ef562 | ||
|
|
a927f9c458 | ||
|
|
82f03d0f1d | ||
|
|
40aca13fb0 | ||
|
|
e2832bac7c | ||
|
|
5f96916701 | ||
|
|
4a96d95bde | ||
|
|
dc3eb9081a | ||
|
|
2ddd920e4d | ||
|
|
e1e9940a04 | ||
|
|
bc8626c6f8 | ||
|
|
f2817b7c49 | ||
|
|
fc79e82108 | ||
|
|
58052634fa | ||
|
|
c1374c5592 | ||
|
|
3a5d636345 | ||
|
|
642d802eee | ||
|
|
76ba624dce | ||
|
|
01d5f034b9 | ||
|
|
b7ced9f289 | ||
|
|
1df2bf4d9b | ||
|
|
9c39e1ddc9 | ||
|
|
bbac1cb663 | ||
|
|
3bc23a60c7 | ||
|
|
c171ae4761 | ||
|
|
b530f4928e | ||
|
|
9fc6afd181 | ||
|
|
eb3d88afbf | ||
|
|
8ded0658d4 | ||
|
|
d032f81fd6 | ||
|
|
75becccf78 | ||
|
|
bfb84e2960 | ||
|
|
2f64c5a407 | ||
|
|
9e6765f4e2 | ||
|
|
d77c84b7c9 | ||
|
|
7172f787ac | ||
|
|
77f66d5357 | ||
|
|
4307815c0f | ||
|
|
b484f36a22 | ||
|
|
360a649d2a | ||
|
|
4bdd23291c | ||
|
|
1ee480122b | ||
|
|
382a5220e0 | ||
|
|
627155b13c | ||
|
|
0ca4dd1b32 | ||
|
|
383bcc4fc1 | ||
|
|
8028a0d75d | ||
|
|
31c1c5eb56 | ||
|
|
ca9295016a | ||
|
|
7b90fe9833 | ||
|
|
330786352c | ||
|
|
da309ae929 | ||
|
|
3877ae5328 | ||
|
|
9c99070d54 | ||
|
|
5751250a7f | ||
|
|
f97ff8124c | ||
|
|
7f37ed96d1 | ||
|
|
7ff7049018 | ||
|
|
ae895787c1 | ||
|
|
305f5c7d47 | ||
|
|
f0d3984376 | ||
|
|
b8f355ac4f | ||
|
|
29195a17a7 | ||
|
|
e90efd4fa9 | ||
|
|
70c158990d | ||
|
|
6e2efb4b55 | ||
|
|
18db1a78ab | ||
|
|
bd87d729fd | ||
|
|
a6f3ef591f | ||
|
|
aaed0a186c | ||
|
|
2b74bbca5c | ||
|
|
36758f61b4 | ||
|
|
f9de81f032 | ||
|
|
444aa53803 | ||
|
|
4e9f3e6e05 | ||
|
|
3fc4482d7c | ||
|
|
ad851375dd | ||
|
|
dd3e942057 | ||
|
|
dd4439c34a | ||
|
|
e81d850438 | ||
|
|
779453be26 | ||
|
|
867d341182 | ||
|
|
78b51f9a11 | ||
|
|
417772dd9c | ||
|
|
b86885512f | ||
|
|
65381cd614 | ||
|
|
e3f11d8fe1 | ||
|
|
093eb419a8 | ||
|
|
ad25490cf9 | ||
|
|
e94d3d80fa | ||
|
|
0e6b6d90bc | ||
|
|
ce1f9259bf | ||
|
|
8cec12b98b | ||
|
|
028b1b2d93 | ||
|
|
d8e5d0d4f1 | ||
|
|
0da9826821 | ||
|
|
b416e09e8b | ||
|
|
2ef58bd75d | ||
|
|
cac794208e | ||
|
|
a22aec8694 | ||
|
|
6f11e820b5 | ||
|
|
6a75b60b4f | ||
|
|
c494adbd87 | ||
|
|
3aa0f13869 | ||
|
|
e9af9efaf3 | ||
|
|
1ecc8066db | ||
|
|
525775fb36 | ||
|
|
3e4c608aca | ||
|
|
bd5028e331 | ||
|
|
4207614600 | ||
|
|
e1af950724 | ||
|
|
e91388a010 | ||
|
|
8648ddb528 | ||
|
|
feff5b6a57 | ||
|
|
32198910ce | ||
|
|
07f7147ff3 | ||
|
|
0e8a006adc | ||
|
|
4193e3c78f | ||
|
|
d2f91d6f1e | ||
|
|
9ed5093474 | ||
|
|
a12dd019d3 | ||
|
|
e53bc92f41 | ||
|
|
597a8db2d9 | ||
|
|
0b0f639cd0 | ||
|
|
6b4ba6bca1 | ||
|
|
3cdd67b5b4 | ||
|
|
2b03bc7a53 | ||
|
|
f6447b8936 | ||
|
|
4069c33671 | ||
|
|
4dbc5581a5 | ||
|
|
3fc5c1c995 | ||
|
|
f123057ae5 | ||
|
|
5025ec818f | ||
|
|
65c6c8146d | ||
|
|
b93745f24a | ||
|
|
bbb59c9fe1 | ||
|
|
fb56f6571a | ||
|
|
082af1f82c | ||
|
|
f5b3a0cbad | ||
|
|
d602ccc224 | ||
|
|
ab54e7b7bd | ||
|
|
0137b07f66 | ||
|
|
ae95fbd1be | ||
|
|
625bd97d8b | ||
|
|
603fce7cd3 | ||
|
|
c759f460d5 | ||
|
|
e1c00667f7 | ||
|
|
46631fc4d4 | ||
|
|
f235a5d951 | ||
|
|
b0b711f181 | ||
|
|
f1b7102583 | ||
|
|
f076420e68 | ||
|
|
4ce0a7eaa1 | ||
|
|
270b730c21 | ||
|
|
14a6cc5999 | ||
|
|
0067873f60 | ||
|
|
fc16e1461f | ||
|
|
8f01916bb2 | ||
|
|
87d522f249 | ||
|
|
ead138b4cd | ||
|
|
ef39979548 | ||
|
|
ab6c8caf2f | ||
|
|
ba9206bded | ||
|
|
c5838cf955 | ||
|
|
0c41fc30e4 | ||
|
|
8270cfa39e | ||
|
|
06e356cab9 | ||
|
|
f24631dc48 | ||
|
|
ec936bcd06 | ||
|
|
6c7975e400 | ||
|
|
8acef7ab66 | ||
|
|
4ddfd39b0d | ||
|
|
3ba37cc24c | ||
|
|
da080118b0 | ||
|
|
e8897ea80a | ||
|
|
9d26f582fa | ||
|
|
0a2ad2176c | ||
|
|
7c36439836 | ||
|
|
a1e3afed06 | ||
|
|
be5b1bbae3 | ||
|
|
52de23bf95 | ||
|
|
1d16fe46cd | ||
|
|
cbecda13f9 | ||
|
|
24649ec103 | ||
|
|
b9f62a8399 | ||
|
|
689d45e095 | ||
|
|
33f7b3f2bb | ||
|
|
2cce072393 | ||
|
|
d64bdec2f9 | ||
|
|
5adacdecad | ||
|
|
f2552d3f3b | ||
|
|
b435e01199 | ||
|
|
cd576df6e4 | ||
|
|
51fc7f5457 | ||
|
|
08e54510ed | ||
|
|
a95606cee9 | ||
|
|
c990476d99 | ||
|
|
c719d24f3a | ||
|
|
f746368369 | ||
|
|
164b6ff6c1 | ||
|
|
7ae522e610 | ||
|
|
e1a0ec3061 | ||
|
|
f8222528eb | ||
|
|
b142bd2fd5 | ||
|
|
3dbe9e6a3e | ||
|
|
4fa8f9b4c2 | ||
|
|
24205601e1 | ||
|
|
31f8af8409 | ||
|
|
9317a93152 |
@@ -9,5 +9,6 @@
|
||||
"WebFetch(domain:github.com)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
},
|
||||
"outputStyle": "Explanatory"
|
||||
}
|
||||
|
||||
41
.cursor-plugin/plugin.json
Normal file
41
.cursor-plugin/plugin.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "shadcn",
|
||||
"displayName": "shadcn/ui",
|
||||
"version": "1.0.0",
|
||||
"description": "UI component and design system framework. Search registries, install components as source code, and audit your project.",
|
||||
"author": {
|
||||
"name": "shadcn"
|
||||
},
|
||||
"homepage": "https://ui.shadcn.com",
|
||||
"repository": "https://github.com/shadcn-ui/ui",
|
||||
"license": "MIT",
|
||||
"logo": "skills/shadcn/assets/shadcn.png",
|
||||
"keywords": [
|
||||
"shadcn",
|
||||
"shadcn-ui",
|
||||
"ui",
|
||||
"components",
|
||||
"tailwind",
|
||||
"tailwindcss",
|
||||
"radix",
|
||||
"react",
|
||||
"design-system",
|
||||
"registry",
|
||||
"mcp"
|
||||
],
|
||||
"category": "developer-tools",
|
||||
"tags": [
|
||||
"ui",
|
||||
"components",
|
||||
"design-system",
|
||||
"react",
|
||||
"tailwind"
|
||||
],
|
||||
"skills": "./skills/",
|
||||
"mcpServers": {
|
||||
"shadcn": {
|
||||
"command": "npx",
|
||||
"args": ["shadcn@latest", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
22
.cursor/rules/registry-bases-parity.mdc
Normal file
22
.cursor/rules/registry-bases-parity.mdc
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
description: Keep registry base and radix trees in sync when editing shared UI
|
||||
globs: apps/v4/registry/bases/**/*
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Registry bases: Base UI ↔ Radix parity
|
||||
|
||||
`apps/v4/registry/bases/base` and `apps/v4/registry/bases/radix` are **parallel registries**. Anything that exists in both trees for the same purpose (preview blocks, mirrored examples, shared card layouts, etc.) **must stay in sync**.
|
||||
|
||||
## When editing
|
||||
|
||||
- If you change a file under **`bases/base/...`**, apply the **same behavioral and visual change** to the matching path under **`bases/radix/...`** (and the reverse).
|
||||
- Only diverge where APIs differ (e.g. import paths like `@/registry/bases/base/ui/*` vs `@/registry/bases/radix/ui/*`, or Base UI vs Radix component props).
|
||||
- Do **not** update only one side unless the user explicitly asks for a single-base change.
|
||||
|
||||
## Typical mirrored paths
|
||||
|
||||
- `blocks/preview/**` — preview cards and blocks
|
||||
- Parallel `ui/*` components when both exist for the same component
|
||||
|
||||
After edits, briefly confirm both trees were updated (or state why one side is intentionally unchanged).
|
||||
40
.github/dependabot.yml
vendored
40
.github/dependabot.yml
vendored
@@ -4,3 +4,43 @@ updates:
|
||||
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"
|
||||
|
||||
21
.github/version-script-beta.js
vendored
21
.github/version-script-beta.js
vendored
@@ -1,21 +0,0 @@
|
||||
// ORIGINALLY FROM CLOUDFLARE WRANGLER:
|
||||
// https://github.com/cloudflare/wrangler2/blob/main/.github/version-script.js
|
||||
|
||||
import { exec } from "child_process"
|
||||
import fs from "fs"
|
||||
|
||||
const pkgJsonPath = "packages/shadcn/package.json"
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath))
|
||||
exec("git rev-parse --short HEAD", (err, stdout) => {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
process.exit(1)
|
||||
}
|
||||
pkg.version = "0.0.0-beta." + stdout.trim()
|
||||
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, "\t") + "\n")
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
}
|
||||
37
.github/version-script-prerelease.js
vendored
Normal file
37
.github/version-script-prerelease.js
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
import fs from "fs"
|
||||
|
||||
const pkgJsonPath = "packages/shadcn/package.json"
|
||||
const channel = process.argv[2]
|
||||
const headSha = process.argv[3]
|
||||
|
||||
if (!["beta", "rc"].includes(channel)) {
|
||||
console.error(
|
||||
`Expected prerelease channel to be "beta" or "rc", got "${channel}".`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!headSha) {
|
||||
console.error("Expected pull request head SHA.")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"))
|
||||
const shortSha = headSha.trim().slice(0, 7)
|
||||
const baseVersion = channel === "beta" ? "0.0.0" : pkg.version
|
||||
|
||||
if (channel === "rc" && baseVersion.includes("-")) {
|
||||
console.error(
|
||||
`Expected a stable planned version for rc, got "${baseVersion}".`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
pkg.version = `${baseVersion}-${channel}.${shortSha}`
|
||||
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, "\t") + "\n")
|
||||
console.log(`Prepared shadcn@${pkg.version}`)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
}
|
||||
9
.github/workflows/code-check.yml
vendored
9
.github/workflows/code-check.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 9.0.6
|
||||
version: 10.33.4
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 9.0.6
|
||||
version: 10.33.4
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
@@ -77,6 +77,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm --filter=shadcn build
|
||||
|
||||
- run: pnpm format:check
|
||||
|
||||
tsc:
|
||||
@@ -96,7 +99,7 @@ jobs:
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 9.0.6
|
||||
version: 10.33.4
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
|
||||
78
.github/workflows/deprecated.yml
vendored
78
.github/workflows/deprecated.yml
vendored
@@ -1,78 +0,0 @@
|
||||
name: Deprecated
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
deprecated:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v46
|
||||
with:
|
||||
files: |
|
||||
apps/www/**
|
||||
files_ignore: |
|
||||
apps/www/public/r/**
|
||||
base_sha: ${{ github.event.pull_request.base.sha }}
|
||||
sha: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Comment on PR if www files changed
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const changedFiles = `${{ steps.changed-files.outputs.all_changed_files }}`.split(' ');
|
||||
const wwwFiles = changedFiles.filter(file =>
|
||||
file.startsWith('apps/www/') &&
|
||||
!file.startsWith('apps/www/public/r/') &&
|
||||
file !== 'apps/www/package.json'
|
||||
);
|
||||
|
||||
if (wwwFiles.length > 0) {
|
||||
const comment = `Looks like this PR modifies files in \`apps/www\`, which is deprecated.
|
||||
|
||||
Consider applying the change to \`apps/v4\` if relevant.`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: comment
|
||||
});
|
||||
|
||||
// Add deprecated label
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['deprecated']
|
||||
});
|
||||
} else {
|
||||
// Remove deprecated label if no www files are changed
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
name: 'deprecated'
|
||||
});
|
||||
} catch (error) {
|
||||
// Label doesn't exist, which is fine
|
||||
console.log('Deprecated label not found, skipping removal');
|
||||
}
|
||||
}
|
||||
45
.github/workflows/prerelease-comment.yml
vendored
45
.github/workflows/prerelease-comment.yml
vendored
@@ -1,9 +1,9 @@
|
||||
# Adapted from create-t3-app.
|
||||
name: Write Beta Release comment
|
||||
name: Write Prerelease comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Release - Beta"]
|
||||
workflows: ["Release"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
@@ -11,12 +11,13 @@ jobs:
|
||||
comment:
|
||||
if: |
|
||||
github.repository_owner == 'shadcn-ui' &&
|
||||
${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
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
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
@@ -31,9 +32,13 @@ jobs:
|
||||
const match = /^npm-package-shadcn@(.*?)-pr-(\d+)/.exec(artifact.name);
|
||||
|
||||
if (match) {
|
||||
const version = match[1];
|
||||
const channel = version.includes("-rc.") ? "rc" : "beta";
|
||||
require("fs").appendFileSync(
|
||||
process.env.GITHUB_ENV,
|
||||
`\nBETA_PACKAGE_VERSION=${match[1]}` +
|
||||
`\nPRERELEASE_PACKAGE_VERSION=${version}` +
|
||||
`\nPRERELEASE_CHANNEL=${channel}` +
|
||||
`\nPRERELEASE_LABEL=release: ${channel}` +
|
||||
`\nWORKFLOW_RUN_PR=${match[2]}` +
|
||||
`\nWORKFLOW_RUN_ID=${context.payload.workflow_run.id}`
|
||||
);
|
||||
@@ -46,20 +51,30 @@ jobs:
|
||||
with:
|
||||
number: ${{ env.WORKFLOW_RUN_PR }}
|
||||
message: |
|
||||
A new prerelease is available for testing:
|
||||
A new ${{ env.PRERELEASE_CHANNEL }} prerelease is available for testing:
|
||||
|
||||
```sh
|
||||
pnpm dlx shadcn@${{ env.BETA_PACKAGE_VERSION }}
|
||||
pnpm dlx shadcn@${{ env.PRERELEASE_PACKAGE_VERSION }}
|
||||
```
|
||||
|
||||
- name: "Remove the autorelease label once published"
|
||||
uses: actions/github-script@v6
|
||||
View on npm: https://www.npmjs.com/package/shadcn/v/${{ env.PRERELEASE_PACKAGE_VERSION }}
|
||||
|
||||
- name: "Remove the prerelease label once published"
|
||||
uses: actions/github-script@v7
|
||||
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',
|
||||
});
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: '${{ env.WORKFLOW_RUN_PR }}',
|
||||
name: '${{ env.PRERELEASE_LABEL }}',
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
core.info("The prerelease label was already removed.");
|
||||
}
|
||||
|
||||
64
.github/workflows/prerelease.yml
vendored
64
.github/workflows/prerelease.yml
vendored
@@ -1,64 +0,0 @@
|
||||
# Adapted from create-t3-app.
|
||||
|
||||
name: Release - Beta
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
prerelease:
|
||||
if: |
|
||||
github.repository_owner == 'shadcn-ui' &&
|
||||
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@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.0.6
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Update npm for OIDC support
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Modify package.json version
|
||||
run: node .github/version-script-beta.js
|
||||
|
||||
- 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: packages/shadcn
|
||||
|
||||
- name: Upload packaged artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: npm-package-shadcn@${{ steps.package-version.outputs.current-version }}-pr-${{ github.event.number }} # encode the PR number into the artifact name
|
||||
path: packages/shadcn/dist/index.js
|
||||
139
.github/workflows/release.yml
vendored
139
.github/workflows/release.yml
vendored
@@ -2,31 +2,141 @@
|
||||
|
||||
name: Release
|
||||
|
||||
run-name: ${{ github.event_name == 'pull_request' && format('Release Prerelease - PR {0}', github.event.number) || 'Release Stable' }}
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: ${{ github.repository_owner == 'shadcn-ui' }}
|
||||
name: Create a PR for release workflow
|
||||
prerelease:
|
||||
if: "${{ github.event_name == 'pull_request' && github.repository_owner == 'shadcn-ui' && (contains(github.event.pull_request.labels.*.name, 'release: beta') || contains(github.event.pull_request.labels.*.name, 'release: rc')) }}"
|
||||
name: Publish Prerelease to NPM
|
||||
runs-on: ubuntu-latest
|
||||
environment: Preview
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Select prerelease channel
|
||||
id: prerelease
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const prereleaseLabels = [
|
||||
{ name: "release: beta", channel: "beta" },
|
||||
{ name: "release: rc", channel: "rc" },
|
||||
];
|
||||
const labels = context.payload.pull_request.labels.map((label) => label.name);
|
||||
const selectedLabels = prereleaseLabels.filter((label) =>
|
||||
labels.includes(label.name)
|
||||
);
|
||||
|
||||
if (selectedLabels.length !== 1) {
|
||||
throw new Error(
|
||||
`Expected exactly one prerelease label, found: ${
|
||||
selectedLabels.map((label) => label.name).join(", ") || "none"
|
||||
}.`
|
||||
);
|
||||
}
|
||||
|
||||
const selected = selectedLabels[0];
|
||||
const pullRequest = context.payload.pull_request;
|
||||
|
||||
if (
|
||||
selected.channel === "rc" &&
|
||||
(pullRequest.head.ref !== "changeset-release/main" ||
|
||||
pullRequest.title !== "chore(release): version packages")
|
||||
) {
|
||||
throw new Error(
|
||||
"The release: rc label can only be used on the Changesets version PR from changeset-release/main."
|
||||
);
|
||||
}
|
||||
|
||||
core.setOutput("channel", selected.channel);
|
||||
core.setOutput("label", selected.name);
|
||||
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Use PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.33.4
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Update npm for OIDC support
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Modify package.json version
|
||||
run: node .github/version-script-prerelease.js ${{ steps.prerelease.outputs.channel }} ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: get-npm-version
|
||||
id: package-version
|
||||
uses: martinbeentjes/npm-get-version-action@main
|
||||
with:
|
||||
path: packages/shadcn
|
||||
|
||||
- name: Check package version on NPM
|
||||
id: package-exists
|
||||
run: |
|
||||
if npm view "shadcn@${{ steps.package-version.outputs.current-version }}" version >/dev/null 2>&1; then
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Publish Prerelease to NPM
|
||||
if: ${{ steps.package-exists.outputs.exists == 'false' }}
|
||||
run: pnpm pub:${{ steps.prerelease.outputs.channel }}
|
||||
|
||||
- name: Build packaged artifact
|
||||
if: ${{ steps.package-exists.outputs.exists == 'true' }}
|
||||
run: pnpm shadcn:build
|
||||
|
||||
- name: Upload packaged artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: npm-package-shadcn@${{ steps.package-version.outputs.current-version }}-pr-${{ github.event.number }} # encode the PR number into the artifact name
|
||||
path: packages/shadcn/dist/index.js
|
||||
|
||||
release:
|
||||
if: ${{ github.event_name == 'push' && github.repository_owner == 'shadcn-ui' }}
|
||||
name: Create Version PR or Publish Stable Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.0.6
|
||||
version: 10.33.4
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
@@ -47,10 +157,19 @@ jobs:
|
||||
- name: Build the package
|
||||
run: pnpm shadcn:build
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
|
||||
git_user_signingkey: true
|
||||
git_commit_gpgsign: true
|
||||
git_tag_gpgsign: true
|
||||
|
||||
- name: Create Version PR or Publish to NPM
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
setupGitUser: false
|
||||
commit: "chore(release): version packages"
|
||||
title: "chore(release): version packages"
|
||||
version: node .github/changeset-version.js
|
||||
|
||||
75
.github/workflows/signed-commits.yml
vendored
Normal file
75
.github/workflows/signed-commits.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: Signed commits
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
signed-commits:
|
||||
if: github.repository_owner == 'shadcn-ui'
|
||||
runs-on: ubuntu-latest
|
||||
name: Signed commits
|
||||
steps:
|
||||
- name: Check PR commits
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const body = "Can you sign the commits please? See https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits. Thank you."
|
||||
|
||||
const { owner, repo } = context.repo
|
||||
const pullNumber = context.payload.pull_request.number
|
||||
|
||||
const commits = await github.paginate(github.rest.pulls.listCommits, {
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pullNumber,
|
||||
per_page: 100,
|
||||
})
|
||||
|
||||
const unsignedCommits = commits.filter((commit) => {
|
||||
return commit.commit.verification?.reason === "unsigned"
|
||||
})
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pullNumber,
|
||||
per_page: 100,
|
||||
})
|
||||
|
||||
const existingComments = comments.filter((comment) => {
|
||||
return comment.user.type === "Bot" && comment.body.trim() === body
|
||||
})
|
||||
|
||||
if (unsignedCommits.length > 0) {
|
||||
core.info(`Found ${unsignedCommits.length} unsigned commits.`)
|
||||
|
||||
if (existingComments.length === 0) {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pullNumber,
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
core.info("All commits are signed.")
|
||||
|
||||
for (const comment of existingComments) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: comment.id,
|
||||
})
|
||||
}
|
||||
314
.github/workflows/templates.yml
vendored
Normal file
314
.github/workflows/templates.yml
vendored
Normal file
@@ -0,0 +1,314 @@
|
||||
name: Templates
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["*"]
|
||||
paths:
|
||||
- ".github/workflows/templates.yml"
|
||||
- "apps/v4/registry/**"
|
||||
- "package.json"
|
||||
- "packages/shadcn/src/commands/add.ts"
|
||||
- "packages/shadcn/src/commands/init.ts"
|
||||
- "packages/shadcn/src/templates/**"
|
||||
- "packages/shadcn/src/utils/create-project.ts"
|
||||
- "packages/shadcn/src/utils/get-monorepo-info.ts"
|
||||
- "packages/shadcn/src/utils/get-package-manager.ts"
|
||||
- "pnpm-lock.yaml"
|
||||
- "templates/**"
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
name: ${{ matrix.package-manager == 'pnpm' && format('pnpm {0}', matrix.pnpm-version) || matrix.package-manager }} ${{ matrix.template }}
|
||||
permissions:
|
||||
contents: read
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
template: [next, vite, astro, start, react-router]
|
||||
package-manager: [pnpm, bun, npm, yarn]
|
||||
pnpm-version: [10.33.4, 11]
|
||||
exclude:
|
||||
- package-manager: bun
|
||||
pnpm-version: 11
|
||||
- package-manager: npm
|
||||
pnpm-version: 11
|
||||
- package-manager: yarn
|
||||
pnpm-version: 11
|
||||
env:
|
||||
NEXT_PUBLIC_APP_URL: http://localhost:4000
|
||||
NEXT_PUBLIC_V0_URL: https://v0.dev
|
||||
REGISTRY_URL: http://localhost:4000/r
|
||||
ROOT_PNPM_VERSION: 10.33.4
|
||||
TEMPLATE_PNPM_VERSION: ${{ matrix.pnpm-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: ${{ env.ROOT_PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- name: Install Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install Yarn
|
||||
if: matrix.package-manager == 'yarn'
|
||||
run: |
|
||||
corepack enable
|
||||
COREPACK_ENABLE_PROJECT_SPEC=0 corepack prepare yarn@4.12.0 --activate
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: actions/cache@v4
|
||||
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: Build packages
|
||||
run: |
|
||||
pnpm --filter=shadcn build
|
||||
pnpm --filter=v4 registry:build
|
||||
|
||||
- name: Validate templates
|
||||
env:
|
||||
TEMPLATE: ${{ matrix.template }}
|
||||
TEMPLATE_PACKAGE_MANAGER: ${{ matrix.package-manager }}
|
||||
SHADCN_TEMPLATE_DIR: ${{ github.workspace }}/templates
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
root_pnpm="$(command -v pnpm)"
|
||||
validation_script="$RUNNER_TEMP/validate-templates.sh"
|
||||
|
||||
cat > "$validation_script" <<'BASH'
|
||||
set -euo pipefail
|
||||
|
||||
bin_dir="$RUNNER_TEMP/template-pnpm-bin"
|
||||
mkdir -p "$bin_dir"
|
||||
|
||||
cat > "$bin_dir/pnpm" <<'PNPM'
|
||||
#!/usr/bin/env bash
|
||||
exec npx -y "pnpm@${TEMPLATE_PNPM_VERSION}" "$@"
|
||||
PNPM
|
||||
chmod +x "$bin_dir/pnpm"
|
||||
export PATH="$bin_dir:$PATH"
|
||||
|
||||
echo "Using template pnpm $(pnpm --version)"
|
||||
|
||||
cli="$GITHUB_WORKSPACE/packages/shadcn/dist/index.js"
|
||||
template_root="$RUNNER_TEMP/generated-template-${TEMPLATE_PACKAGE_MANAGER}-${TEMPLATE}"
|
||||
rm -rf "$template_root"
|
||||
mkdir -p "$template_root"
|
||||
|
||||
modes=(app monorepo)
|
||||
|
||||
has_script() {
|
||||
node -e "const pkg = require('./package.json'); process.exit(pkg.scripts && pkg.scripts[process.argv[1]] ? 0 : 1)" "$1"
|
||||
}
|
||||
|
||||
run_script_if_present() {
|
||||
local script="$1"
|
||||
if has_script "$script"; then
|
||||
pnpm run "$script"
|
||||
else
|
||||
echo "No $script script found; skipping."
|
||||
fi
|
||||
}
|
||||
|
||||
validate_non_pnpm_project() {
|
||||
local package_manager="$1"
|
||||
local project_path="$2"
|
||||
local check_workspace_protocol="$3"
|
||||
local is_monorepo="$4"
|
||||
|
||||
cd "$project_path"
|
||||
test ! -f pnpm-workspace.yaml
|
||||
test ! -f pnpm-lock.yaml
|
||||
|
||||
EXPECTED_PACKAGE_MANAGER="$package_manager" \
|
||||
CHECK_WORKSPACE_PROTOCOL="$check_workspace_protocol" \
|
||||
IS_MONOREPO="$is_monorepo" \
|
||||
node <<'NODE'
|
||||
const fs = require("node:fs")
|
||||
const path = require("node:path")
|
||||
|
||||
const expectedPackageManager = process.env.EXPECTED_PACKAGE_MANAGER
|
||||
const checkWorkspaceProtocol =
|
||||
process.env.CHECK_WORKSPACE_PROTOCOL === "true"
|
||||
const isMonorepo = process.env.IS_MONOREPO === "true"
|
||||
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"))
|
||||
|
||||
if (isMonorepo) {
|
||||
const workspaces = pkg.workspaces ?? []
|
||||
|
||||
if (!Array.isArray(workspaces)) {
|
||||
throw new Error("Expected package.json workspaces to be an array.")
|
||||
}
|
||||
|
||||
if (workspaces.length === 0) {
|
||||
throw new Error("Expected package.json workspaces to have entries.")
|
||||
}
|
||||
|
||||
for (const workspace of ["sharp", "unrs-resolver", "esbuild"]) {
|
||||
if (workspaces.includes(workspace)) {
|
||||
throw new Error(`Unexpected workspace entry: ${workspace}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!pkg.packageManager?.startsWith(`${expectedPackageManager}@`)) {
|
||||
throw new Error(
|
||||
`Expected packageManager to use ${expectedPackageManager}, got ${pkg.packageManager}`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (pkg.workspaces !== undefined) {
|
||||
throw new Error("Did not expect package.json workspaces for app template.")
|
||||
}
|
||||
|
||||
if (pkg.packageManager !== undefined) {
|
||||
throw new Error(
|
||||
`Did not expect packageManager for app template, got ${pkg.packageManager}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (checkWorkspaceProtocol) {
|
||||
const packageJsonFiles = []
|
||||
function collectPackageJsonFiles(dir) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (entry.name === "node_modules") {
|
||||
continue
|
||||
}
|
||||
|
||||
const fullPath = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
collectPackageJsonFiles(fullPath)
|
||||
} else if (entry.name === "package.json") {
|
||||
packageJsonFiles.push(fullPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collectPackageJsonFiles(process.cwd())
|
||||
|
||||
for (const file of packageJsonFiles) {
|
||||
const json = fs.readFileSync(file, "utf8")
|
||||
if (json.includes("workspace:")) {
|
||||
throw new Error(`Unexpected workspace: protocol in ${file}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
NODE
|
||||
}
|
||||
|
||||
for mode in "${modes[@]}"; do
|
||||
project="test-${TEMPLATE}-${mode}-${TEMPLATE_PACKAGE_MANAGER}"
|
||||
project_path="$template_root/$project"
|
||||
|
||||
echo "::group::${TEMPLATE} ${mode} ${TEMPLATE_PACKAGE_MANAGER}"
|
||||
args=(
|
||||
init
|
||||
--defaults
|
||||
--name "$project"
|
||||
--template "$TEMPLATE"
|
||||
--cwd "$template_root"
|
||||
--silent
|
||||
)
|
||||
|
||||
if [ "$mode" = "monorepo" ]; then
|
||||
args+=(--monorepo)
|
||||
is_monorepo="true"
|
||||
else
|
||||
args+=(--no-monorepo)
|
||||
is_monorepo="false"
|
||||
fi
|
||||
|
||||
case "$TEMPLATE_PACKAGE_MANAGER" in
|
||||
pnpm)
|
||||
SHADCN_TEMPLATE_DIR="$SHADCN_TEMPLATE_DIR" \
|
||||
REGISTRY_URL="$REGISTRY_URL" \
|
||||
npm_config_user_agent="pnpm/${TEMPLATE_PNPM_VERSION}" \
|
||||
node "$cli" "${args[@]}"
|
||||
|
||||
cd "$project_path"
|
||||
pnpm install --frozen-lockfile
|
||||
run_script_if_present typecheck
|
||||
run_script_if_present build
|
||||
;;
|
||||
bun)
|
||||
(
|
||||
cd "$template_root"
|
||||
SHADCN_TEMPLATE_DIR="$SHADCN_TEMPLATE_DIR" \
|
||||
REGISTRY_URL="$REGISTRY_URL" \
|
||||
npm_config_user_agent="bun/$(bun --version)" \
|
||||
bunx --bun --package "$GITHUB_WORKSPACE/packages/shadcn" \
|
||||
shadcn "${args[@]}"
|
||||
)
|
||||
validate_non_pnpm_project \
|
||||
"bun" \
|
||||
"$project_path" \
|
||||
"false" \
|
||||
"$is_monorepo"
|
||||
;;
|
||||
npm)
|
||||
(
|
||||
cd "$template_root"
|
||||
SHADCN_TEMPLATE_DIR="$SHADCN_TEMPLATE_DIR" \
|
||||
REGISTRY_URL="$REGISTRY_URL" \
|
||||
npm_config_user_agent="npm/$(npm --version)" \
|
||||
npx --yes --package "$GITHUB_WORKSPACE/packages/shadcn" \
|
||||
shadcn "${args[@]}"
|
||||
)
|
||||
validate_non_pnpm_project \
|
||||
"npm" \
|
||||
"$project_path" \
|
||||
"true" \
|
||||
"$is_monorepo"
|
||||
;;
|
||||
yarn)
|
||||
(
|
||||
cd "$template_root"
|
||||
SHADCN_TEMPLATE_DIR="$SHADCN_TEMPLATE_DIR" \
|
||||
REGISTRY_URL="$REGISTRY_URL" \
|
||||
COREPACK_ENABLE_PROJECT_SPEC=0 \
|
||||
npm_config_user_agent="yarn/$(COREPACK_ENABLE_PROJECT_SPEC=0 yarn --version)" \
|
||||
yarn dlx --package "$GITHUB_WORKSPACE/packages/shadcn" \
|
||||
shadcn "${args[@]}"
|
||||
)
|
||||
validate_non_pnpm_project \
|
||||
"yarn" \
|
||||
"$project_path" \
|
||||
"false" \
|
||||
"$is_monorepo"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "::endgroup::"
|
||||
done
|
||||
BASH
|
||||
|
||||
"$root_pnpm" exec start-server-and-test \
|
||||
"$root_pnpm v4:dev" \
|
||||
http://localhost:4000 \
|
||||
"bash $validation_script"
|
||||
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@@ -19,13 +19,13 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 9.0.6
|
||||
version: 10.33.4
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
@@ -39,10 +39,10 @@ jobs:
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Install Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm build --filter=shadcn
|
||||
|
||||
- run: pnpm test
|
||||
|
||||
42
.github/workflows/validate-registries.yml
vendored
42
.github/workflows/validate-registries.yml
vendored
@@ -3,12 +3,12 @@ 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:
|
||||
@@ -26,11 +26,44 @@ jobs:
|
||||
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 file = "apps/v4/registry/directory.json"
|
||||
const reservedNamespaces = new Set(
|
||||
process.env.RESERVED_NAMESPACES.split(",").filter(Boolean)
|
||||
)
|
||||
|
||||
function readNames() {
|
||||
return JSON.parse(fs.readFileSync(file, "utf8")).map(
|
||||
(entry) => entry.name
|
||||
)
|
||||
}
|
||||
|
||||
const violations = readNames()
|
||||
.filter((name) => reservedNamespaces.has(name))
|
||||
.map((name) => `${file}: ${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
|
||||
version: 10.33.4
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
@@ -47,8 +80,5 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm build --filter=shadcn
|
||||
|
||||
- name: Validate registries
|
||||
run: pnpm --filter=v4 validate:registries
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -15,6 +15,7 @@ build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.eslintcache
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
@@ -41,3 +42,7 @@ tsconfig.tsbuildinfo
|
||||
.vscode
|
||||
|
||||
.notes
|
||||
.playwright-mcp
|
||||
.playwright-cli
|
||||
shadcn-workspace
|
||||
.codex-artifacts
|
||||
|
||||
@@ -5,3 +5,4 @@ build
|
||||
.contentlayer
|
||||
**/fixtures
|
||||
deprecated
|
||||
apps/v4/registry/styles/**/*.css
|
||||
|
||||
@@ -141,6 +141,11 @@ When adding or modifying components, please ensure that:
|
||||
2. You update the documentation.
|
||||
3. You run `pnpm registry:build` to update the registry.
|
||||
|
||||
See [`apps/v4/registry/README.md`](apps/v4/registry/README.md) for how the
|
||||
registry pipeline is structured and for the faster targeted build modes
|
||||
(`--style`, `--registry`, `--examples`, `--indexes`) you can use while
|
||||
iterating locally. Always run the full `pnpm registry:build` before committing.
|
||||
|
||||
## Commit Convention
|
||||
|
||||
Before you create a Pull Request, please check whether your commits comply with
|
||||
|
||||
@@ -6,7 +6,7 @@ A set of beautifully designed components that you can customize, extend, and bui
|
||||
|
||||
## Documentation
|
||||
|
||||
Visit http://ui.shadcn.com/docs to view the documentation.
|
||||
Visit https://ui.shadcn.com/docs to view the documentation.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -14,4 +14,4 @@ Please read the [contributing guide](/CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the [MIT license](https://github.com/shadcn/ui/blob/main/LICENSE.md).
|
||||
Licensed under the [MIT license](./LICENSE.md).
|
||||
|
||||
@@ -5,3 +5,4 @@ build
|
||||
.contentlayer
|
||||
registry/__index__.tsx
|
||||
content/docs/components/calendar.mdx
|
||||
registry/styles/**/*.css
|
||||
|
||||
94
apps/v4/app/(app)/(root)/cards/account-access.tsx
Normal file
94
apps/v4/app/(app)/(root)/cards/account-access.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
AlertCircleIcon,
|
||||
ArrowRight01Icon,
|
||||
SquareLock02Icon,
|
||||
} from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { Button } from "@/styles/base-rhea/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/styles/base-rhea/ui/field"
|
||||
import { Input } from "@/styles/base-rhea/ui/input"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/styles/base-rhea/ui/item"
|
||||
|
||||
export function AccountAccess() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Account Access</CardTitle>
|
||||
<CardDescription>
|
||||
Update your credentials or re-authenticate.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email-address">Email Address</FieldLabel>
|
||||
<Input
|
||||
id="email-address"
|
||||
type="email"
|
||||
placeholder="artist@studio.inc"
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<div className="flex items-center justify-between">
|
||||
<FieldLabel htmlFor="current-password">
|
||||
Current Password
|
||||
</FieldLabel>
|
||||
<a
|
||||
href="#"
|
||||
className="text-xs font-medium tracking-wider text-muted-foreground uppercase hover:text-foreground"
|
||||
>
|
||||
Forgot?
|
||||
</a>
|
||||
</div>
|
||||
<Input
|
||||
id="current-password"
|
||||
type="password"
|
||||
placeholder="••••••••••••••••••••••••"
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-4">
|
||||
<Button className="w-full">
|
||||
<HugeiconsIcon icon={SquareLock02Icon} strokeWidth={2} />
|
||||
Update Security
|
||||
</Button>
|
||||
<Item variant="muted" render={<a href="#" />}>
|
||||
<ItemMedia variant="icon">
|
||||
<HugeiconsIcon
|
||||
icon={AlertCircleIcon}
|
||||
className="text-destructive"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Danger Zone</ItemTitle>
|
||||
<ItemDescription className="line-clamp-1">
|
||||
Archive account and remove catalog
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<HugeiconsIcon
|
||||
icon={ArrowRight01Icon}
|
||||
className="size-4"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</Item>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
46
apps/v4/app/(app)/(root)/cards/analytics-card.tsx
Normal file
46
apps/v4/app/(app)/(root)/cards/analytics-card.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Badge } from "@/styles/base-rhea/ui/badge"
|
||||
import { Button } from "@/styles/base-rhea/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
|
||||
const areaPath = "M0 52L18 40L36 46L54 70L72 50L100 49V86H0Z"
|
||||
const strokePath = "M0 52L18 40L36 46L54 70L72 50L100 49"
|
||||
|
||||
export function AnalyticsCard() {
|
||||
return (
|
||||
<Card className="mx-auto w-full max-w-sm data-[size=sm]:pb-0" size="sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Analytics</CardTitle>
|
||||
<CardDescription>
|
||||
418.2K Visitors <Badge>+10%</Badge>
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button variant="outline" size="sm">
|
||||
View Analytics
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<svg
|
||||
viewBox="0 0 100 86"
|
||||
preserveAspectRatio="none"
|
||||
className="aspect-[1/0.35] w-full text-chart-1"
|
||||
role="img"
|
||||
aria-label="Visitor trend"
|
||||
>
|
||||
<path d={areaPath} fill="currentColor" opacity="0.28" />
|
||||
<path
|
||||
d={strokePath}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
</svg>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
75
apps/v4/app/(app)/(root)/cards/claimable-balance.tsx
Normal file
75
apps/v4/app/(app)/(root)/cards/claimable-balance.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Badge } from "@/styles/base-rhea/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Item, ItemContent } from "@/styles/base-rhea/ui/item"
|
||||
import { Separator } from "@/styles/base-rhea/ui/separator"
|
||||
|
||||
const netRoyalties = 1248.75
|
||||
const processingFee = 37.46
|
||||
const totalClaimable = netRoyalties - processingFee
|
||||
|
||||
const formatCurrency = (amount: number) =>
|
||||
amount.toLocaleString("en-US", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
|
||||
export function ClaimableBalance() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardDescription>Claimable Balance</CardDescription>
|
||||
<CardTitle className="text-4xl tabular-nums">
|
||||
${formatCurrency(totalClaimable)}
|
||||
</CardTitle>
|
||||
<Badge variant="outline">
|
||||
<span className="size-2 rounded-full bg-yellow-500" />
|
||||
Pending Setup
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-1 flex-col justify-end">
|
||||
<Item variant="muted" className="flex-col items-stretch">
|
||||
<ItemContent className="gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Net Royalties
|
||||
</span>
|
||||
<span className="text-sm font-medium tabular-nums">
|
||||
${formatCurrency(netRoyalties)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Processing Fee
|
||||
</span>
|
||||
<span className="text-sm font-medium tabular-nums">
|
||||
-${formatCurrency(processingFee)}
|
||||
</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Total Ready to Claim
|
||||
</span>
|
||||
<span className="text-sm font-semibold tabular-nums">
|
||||
${formatCurrency(totalClaimable)} USD
|
||||
</span>
|
||||
</div>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<CardDescription>
|
||||
Once your bank is connected, balances over $10.00 are automatically
|
||||
eligible for monthly distribution on the 15th of each month.
|
||||
</CardDescription>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
88
apps/v4/app/(app)/(root)/cards/contribution-history.tsx
Normal file
88
apps/v4/app/(app)/(root)/cards/contribution-history.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Badge } from "@/styles/base-rhea/ui/badge"
|
||||
import { Button } from "@/styles/base-rhea/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Item, ItemContent, ItemDescription } from "@/styles/base-rhea/ui/item"
|
||||
|
||||
const chartData = [
|
||||
{ month: "Dec", amount: 800 },
|
||||
{ month: "Jan", amount: 1100 },
|
||||
{ month: "Feb", amount: 900 },
|
||||
{ month: "Mar", amount: 1300 },
|
||||
{ month: "Apr", amount: 750 },
|
||||
{ month: "May", amount: 1400 },
|
||||
]
|
||||
|
||||
export function ContributionHistory() {
|
||||
const maxAmount = Math.max(...chartData.map((item) => item.amount))
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Contribution History</CardTitle>
|
||||
<CardDescription>Last 6 months of activity</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div
|
||||
className="flex h-[200px] w-full items-end gap-3"
|
||||
role="img"
|
||||
aria-label="Last 6 months of contribution activity"
|
||||
>
|
||||
{chartData.map((item) => (
|
||||
<div
|
||||
key={item.month}
|
||||
className="flex h-full flex-1 flex-col justify-end gap-2"
|
||||
>
|
||||
<div
|
||||
className="min-h-2 rounded-t-md bg-chart-2"
|
||||
style={{ height: `${(item.amount / maxAmount) * 100}%` }}
|
||||
/>
|
||||
<span className="text-center text-xs text-muted-foreground">
|
||||
{item.month}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<div className="grid w-full grid-cols-1 gap-3 xl:grid-cols-2">
|
||||
<Item variant="muted" className="flex-col items-stretch">
|
||||
<ItemContent className="gap-1">
|
||||
<ItemDescription className="text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Upcoming
|
||||
</ItemDescription>
|
||||
<span className="cn-font-heading text-base font-semibold">
|
||||
May 2024
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">Scheduled</span>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
<Item
|
||||
variant="muted"
|
||||
className="hidden flex-col items-stretch xl:flex"
|
||||
>
|
||||
<ItemContent className="gap-1">
|
||||
<ItemDescription className="text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Savings Plan
|
||||
</ItemDescription>
|
||||
<span className="cn-font-heading text-base font-semibold">
|
||||
Accelerated
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">Recurring</span>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">View Full Report</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
116
apps/v4/app/(app)/(root)/cards/dividend-income.tsx
Normal file
116
apps/v4/app/(app)/(root)/cards/dividend-income.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { Cancel01Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { Button } from "@/styles/base-rhea/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemGroup,
|
||||
ItemTitle,
|
||||
} from "@/styles/base-rhea/ui/item"
|
||||
|
||||
const HOLDINGS = [
|
||||
{
|
||||
name: "Vanguard",
|
||||
shares: "450 Shares",
|
||||
amount: "$1,842.10",
|
||||
data: [
|
||||
{ q: "Q1", value: 380 },
|
||||
{ q: "Q2", value: 420 },
|
||||
{ q: "Q3", value: 390 },
|
||||
{ q: "Q4", value: 652 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "S&P 500 VOO",
|
||||
shares: "112 Shares",
|
||||
amount: "$928.40",
|
||||
data: [
|
||||
{ q: "Q1", value: 180 },
|
||||
{ q: "Q2", value: 210 },
|
||||
{ q: "Q3", value: 320 },
|
||||
{ q: "Q4", value: 218 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Apple AAPL",
|
||||
shares: "85 Shares",
|
||||
amount: "$340.00",
|
||||
data: [
|
||||
{ q: "Q1", value: 60 },
|
||||
{ q: "Q2", value: 70 },
|
||||
{ q: "Q3", value: 120 },
|
||||
{ q: "Q4", value: 90 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Realty Income",
|
||||
shares: "320 Shares",
|
||||
amount: "$1,139.50",
|
||||
data: [
|
||||
{ q: "Q1", value: 240 },
|
||||
{ q: "Q2", value: 260 },
|
||||
{ q: "Q3", value: 280 },
|
||||
{ q: "Q4", value: 360 },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export function DividendIncome() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Q2 Dividend Income</CardTitle>
|
||||
<CardDescription>
|
||||
Quarterly dividend payouts across your portfolio holdings.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="bg-muted"
|
||||
aria-label="Dismiss dividend income"
|
||||
>
|
||||
<HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} />
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ItemGroup>
|
||||
{HOLDINGS.map((holding) => (
|
||||
<Item key={holding.name} role="listitem" variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>{holding.name}</ItemTitle>
|
||||
<ItemDescription>{holding.shares}</ItemDescription>
|
||||
</ItemContent>
|
||||
<div
|
||||
className="hidden h-8 w-24 items-end gap-1 md:flex"
|
||||
role="img"
|
||||
aria-label={`${holding.name} quarterly dividends`}
|
||||
>
|
||||
{holding.data.map((item) => (
|
||||
<div
|
||||
key={item.q}
|
||||
className="min-h-1 flex-1 rounded-t-sm bg-chart-2"
|
||||
style={{
|
||||
height: `${(item.value / Math.max(...holding.data.map((point) => point.value))) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Item>
|
||||
))}
|
||||
</ItemGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
37
apps/v4/app/(app)/(root)/cards/empty-distribute-track.tsx
Normal file
37
apps/v4/app/(app)/(root)/cards/empty-distribute-track.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Add01Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { Button } from "@/styles/base-rhea/ui/button"
|
||||
import { Card, CardContent } from "@/styles/base-rhea/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/styles/base-rhea/ui/empty"
|
||||
|
||||
export function EmptyDistributeTrack() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Empty className="p-4">
|
||||
<EmptyMedia variant="icon">
|
||||
<HugeiconsIcon icon={Add01Icon} strokeWidth={2} />
|
||||
</EmptyMedia>
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>Distribute Track</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Upload your first master to start reaching listeners on Spotify,
|
||||
Apple Music, and more.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>Create Release</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
117
apps/v4/app/(app)/(root)/cards/index.tsx
Normal file
117
apps/v4/app/(app)/(root)/cards/index.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { AccountAccess } from "./account-access"
|
||||
import { AnalyticsCard } from "./analytics-card"
|
||||
import { ClaimableBalance } from "./claimable-balance"
|
||||
import { ContributionHistory } from "./contribution-history"
|
||||
import { DividendIncome } from "./dividend-income"
|
||||
import { EmptyDistributeTrack } from "./empty-distribute-track"
|
||||
import { NewMilestone } from "./new-milestone"
|
||||
import { NotificationSettings } from "./notification-settings"
|
||||
import { Payments } from "./payments"
|
||||
import { PayoutThreshold } from "./payout-threshold"
|
||||
import { PowerUsage } from "./power-usage"
|
||||
import { QrConnect } from "./qr-connect"
|
||||
import { SavingsTargets } from "./savings-targets"
|
||||
import { SidebarNav } from "./sidebar-nav"
|
||||
import { AccountAccess as SkeletonAccountAccess } from "./skeleton/account-access"
|
||||
import { AnalyticsCard as SkeletonAnalyticsCard } from "./skeleton/analytics-card"
|
||||
import { ClaimableBalance as SkeletonClaimableBalance } from "./skeleton/claimable-balance"
|
||||
import { ContributionHistory as SkeletonContributionHistory } from "./skeleton/contribution-history"
|
||||
import { DividendIncome as SkeletonDividendIncome } from "./skeleton/dividend-income"
|
||||
import { EmptyDistributeTrack as SkeletonEmptyDistributeTrack } from "./skeleton/empty-distribute-track"
|
||||
import { NewMilestone as SkeletonNewMilestone } from "./skeleton/new-milestone"
|
||||
import { NotificationSettings as SkeletonNotificationSettings } from "./skeleton/notification-settings"
|
||||
import { Payments as SkeletonPayments } from "./skeleton/payments"
|
||||
import { PayoutThreshold as SkeletonPayoutThreshold } from "./skeleton/payout-threshold"
|
||||
import { PowerUsage as SkeletonPowerUsage } from "./skeleton/power-usage"
|
||||
import { QrConnect as SkeletonQrConnect } from "./skeleton/qr-connect"
|
||||
import { SavingsTargets as SkeletonSavingsTargets } from "./skeleton/savings-targets"
|
||||
import { TransferFunds as SkeletonTransferFunds } from "./skeleton/transfer-funds"
|
||||
import { UIElements as SkeletonUIElements } from "./skeleton/ui-elements"
|
||||
import { TransferFunds } from "./transfer-funds"
|
||||
import { UIElements } from "./ui-elements"
|
||||
|
||||
function CardsSkeletonRails() {
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-x-0 top-12 z-10 hidden min-[2200px]:block [&_[data-slot=skeleton]:nth-child(even)]:hidden"
|
||||
>
|
||||
<div className="absolute top-0 left-[calc(50%-950px-var(--rail-width)-var(--gap))] grid w-(--rail-width) grid-cols-[repeat(2,var(--rail-column))] gap-(--gap) opacity-50 [--rail-column:20rem] [--rail-width:calc(var(--rail-column)*2+var(--gap))]">
|
||||
<div className="flex flex-col gap-(--gap)">
|
||||
<SkeletonContributionHistory />
|
||||
<SkeletonClaimableBalance />
|
||||
<SkeletonDividendIncome />
|
||||
<SkeletonPayoutThreshold />
|
||||
</div>
|
||||
<div className="flex flex-col gap-(--gap)">
|
||||
<SkeletonUIElements />
|
||||
<SkeletonSavingsTargets />
|
||||
<SkeletonNewMilestone />
|
||||
<SkeletonPayoutThreshold />
|
||||
<SkeletonAccountAccess />
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute top-0 right-[calc(50%-950px-var(--rail-width)-var(--gap))] grid w-(--rail-width) grid-cols-[repeat(2,var(--rail-column))] gap-(--gap) opacity-50 [--rail-column:20rem] [--rail-width:calc(var(--rail-column)*2+var(--gap))]">
|
||||
<div className="flex flex-col gap-(--gap)">
|
||||
<SkeletonNewMilestone />
|
||||
<SkeletonPayoutThreshold />
|
||||
<SkeletonAccountAccess />
|
||||
<SkeletonQrConnect />
|
||||
<SkeletonTransferFunds />
|
||||
<SkeletonPayments />
|
||||
<SkeletonEmptyDistributeTrack />
|
||||
</div>
|
||||
<div className="flex flex-col gap-(--gap)">
|
||||
<SkeletonQrConnect />
|
||||
<SkeletonTransferFunds />
|
||||
<SkeletonPayments />
|
||||
<SkeletonEmptyDistributeTrack />
|
||||
<SkeletonAnalyticsCard />
|
||||
<SkeletonNotificationSettings />
|
||||
<SkeletonPowerUsage />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function CardsDemo() {
|
||||
return (
|
||||
<div
|
||||
data-slot="demo"
|
||||
className="theme-neutral relative flex w-full max-w-none flex-col gap-(--gap) overflow-hidden bg-muted p-12 pb-0! [--gap:--spacing(8)] 3xl:[--gap:--spacing(8)] min-[1900px]:p-12 min-[1900px]:[--gap:--spacing(10)]! lg:p-6 lg:[--gap:--spacing(6)] dark:bg-background"
|
||||
>
|
||||
<CardsSkeletonRails />
|
||||
<div className="relative z-10 mx-auto grid gap-(--gap) **:data-[slot=card]:w-full min-[1400px]:grid-cols-4! min-[1900px]:grid-cols-5! md:max-w-3xl md:grid-cols-2 lg:max-w-none lg:grid-cols-3 xl:max-w-[1600px] 2xl:max-w-[1900px]">
|
||||
<div className="flex flex-col items-start gap-(--gap)">
|
||||
<UIElements />
|
||||
<SidebarNav />
|
||||
<SavingsTargets />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-(--gap) lg:flex">
|
||||
<ContributionHistory />
|
||||
<ClaimableBalance />
|
||||
<DividendIncome />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-(--gap) 3xl:flex!">
|
||||
<NewMilestone />
|
||||
<PayoutThreshold />
|
||||
<AccountAccess />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-(--gap) md:flex">
|
||||
<QrConnect />
|
||||
<TransferFunds />
|
||||
<Payments />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-(--gap) min-[1400px]:flex">
|
||||
<EmptyDistributeTrack />
|
||||
<AnalyticsCard />
|
||||
<NotificationSettings />
|
||||
<PowerUsage />
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-x-0 top-0 z-1 h-120 bg-linear-to-b from-background via-muted to-transparent dark:hidden" />
|
||||
<div className="absolute inset-x-0 bottom-0 z-20 h-48 bg-linear-to-t from-background via-muted/80 to-transparent lg:h-80 xl:h-64 dark:via-background/80" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
52
apps/v4/app/(app)/(root)/cards/new-milestone.tsx
Normal file
52
apps/v4/app/(app)/(root)/cards/new-milestone.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Button } from "@/styles/base-rhea/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/styles/base-rhea/ui/field"
|
||||
import { Input } from "@/styles/base-rhea/ui/input"
|
||||
|
||||
export function NewMilestone() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Set a new milestone</CardTitle>
|
||||
<CardDescription>
|
||||
Define your financial target and we'll help you pace your
|
||||
savings.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="goal-name">Goal Name</FieldLabel>
|
||||
<Input
|
||||
id="goal-name"
|
||||
placeholder="e.g. New Car, Home Downpayment"
|
||||
/>
|
||||
</Field>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="target-amount">Target Amount</FieldLabel>
|
||||
<Input id="target-amount" defaultValue="$15,000" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="target-date">Target Date</FieldLabel>
|
||||
<Input id="target-date" defaultValue="Dec 2025" />
|
||||
</Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2">
|
||||
<Button className="w-full">Create Goal</Button>
|
||||
<Button variant="outline" className="w-full">
|
||||
Cancel
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
76
apps/v4/app/(app)/(root)/cards/notification-settings.tsx
Normal file
76
apps/v4/app/(app)/(root)/cards/notification-settings.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Button } from "@/styles/base-rhea/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Checkbox } from "@/styles/base-rhea/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/styles/base-rhea/ui/field"
|
||||
|
||||
const NOTIFICATIONS = [
|
||||
{
|
||||
id: "transactions",
|
||||
label: "Transaction alerts",
|
||||
description: "Deposits, withdrawals, and transfers.",
|
||||
defaultChecked: true,
|
||||
},
|
||||
{
|
||||
id: "security",
|
||||
label: "Security alerts",
|
||||
description: "Login attempts and account changes.",
|
||||
defaultChecked: true,
|
||||
},
|
||||
{
|
||||
id: "goals",
|
||||
label: "Goal milestones",
|
||||
description: "Updates at 25%, 50%, 75%, and 100%.",
|
||||
defaultChecked: false,
|
||||
},
|
||||
{
|
||||
id: "market",
|
||||
label: "Market updates",
|
||||
description: "Daily portfolio summary and price alerts.",
|
||||
defaultChecked: false,
|
||||
},
|
||||
]
|
||||
|
||||
export function NotificationSettings() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Notifications</CardTitle>
|
||||
<CardDescription>
|
||||
Choose which email and push alerts you want to receive.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
{NOTIFICATIONS.map((n) => (
|
||||
<Field key={n.id} orientation="horizontal">
|
||||
<Checkbox
|
||||
id={`notify-${n.id}`}
|
||||
defaultChecked={n.defaultChecked}
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={`notify-${n.id}`}>{n.label}</FieldLabel>
|
||||
<FieldDescription>{n.description}</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
))}
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Save Preferences</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
139
apps/v4/app/(app)/(root)/cards/payments.tsx
Normal file
139
apps/v4/app/(app)/(root)/cards/payments.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import {
|
||||
ArrowRight01Icon,
|
||||
Calendar03Icon,
|
||||
MoreHorizontalCircle01Icon,
|
||||
RefreshIcon,
|
||||
Settings01Icon,
|
||||
} from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/styles/base-rhea/ui/breadcrumb"
|
||||
import { Button } from "@/styles/base-rhea/ui/button"
|
||||
import { Card, CardContent, CardHeader } from "@/styles/base-rhea/ui/card"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/base-rhea/ui/dropdown-menu"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemGroup,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/styles/base-rhea/ui/item"
|
||||
|
||||
export function Payments() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col gap-3">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#">Home</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
aria-label="Account options"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<HugeiconsIcon
|
||||
icon={MoreHorizontalCircle01Icon}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<span className="sr-only">Account options</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>Profile</DropdownMenuItem>
|
||||
<DropdownMenuItem>Statements</DropdownMenuItem>
|
||||
<DropdownMenuItem>Documents</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Payments</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ItemGroup>
|
||||
<div role="listitem" className="w-full">
|
||||
<Item variant="muted" render={<a href="#" />}>
|
||||
<ItemMedia variant="icon">
|
||||
<HugeiconsIcon icon={Settings01Icon} strokeWidth={2} />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Change transfer limit</ItemTitle>
|
||||
<ItemDescription>
|
||||
Adjust how much you can send from your balance.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<HugeiconsIcon
|
||||
icon={ArrowRight01Icon}
|
||||
className="size-4 shrink-0 text-muted-foreground"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</Item>
|
||||
</div>
|
||||
<div role="listitem" className="w-full">
|
||||
<Item variant="muted" render={<a href="#" />}>
|
||||
<ItemMedia variant="icon">
|
||||
<HugeiconsIcon icon={Calendar03Icon} strokeWidth={2} />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Scheduled transfers</ItemTitle>
|
||||
<ItemDescription>
|
||||
Set up a transfer to send at a later date.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<HugeiconsIcon
|
||||
icon={ArrowRight01Icon}
|
||||
className="size-4 shrink-0 text-muted-foreground"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</Item>
|
||||
</div>
|
||||
<div role="listitem" className="w-full">
|
||||
<Item variant="muted" render={<a href="#" />}>
|
||||
<ItemMedia variant="icon">
|
||||
<HugeiconsIcon icon={RefreshIcon} strokeWidth={2} />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Recurring card payments</ItemTitle>
|
||||
<ItemDescription>
|
||||
Manage your repeated card transactions.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<HugeiconsIcon
|
||||
icon={ArrowRight01Icon}
|
||||
className="size-4 shrink-0 text-muted-foreground"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</Item>
|
||||
</div>
|
||||
</ItemGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
112
apps/v4/app/(app)/(root)/cards/payout-threshold.tsx
Normal file
112
apps/v4/app/(app)/(root)/cards/payout-threshold.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { Cancel01Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { Button } from "@/styles/base-rhea/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/styles/base-rhea/ui/field"
|
||||
import { Progress } from "@/styles/base-rhea/ui/progress"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/styles/base-rhea/ui/select"
|
||||
import { Textarea } from "@/styles/base-rhea/ui/textarea"
|
||||
|
||||
const CURRENCIES = [
|
||||
{ label: "USD — United States Dollar", value: "usd" },
|
||||
{ label: "EUR — Euro", value: "eur" },
|
||||
{ label: "GBP — British Pound", value: "gbp" },
|
||||
{ label: "JPY — Japanese Yen", value: "jpy" },
|
||||
]
|
||||
|
||||
export function PayoutThreshold() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Payout Threshold</CardTitle>
|
||||
<CardDescription>
|
||||
Set the minimum balance required before a payout is triggered.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="bg-muted"
|
||||
aria-label="Dismiss payout threshold"
|
||||
>
|
||||
<HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} />
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="preferred-currency">
|
||||
Preferred Currency
|
||||
</FieldLabel>
|
||||
<Select items={CURRENCIES} defaultValue="usd">
|
||||
<SelectTrigger id="preferred-currency" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{CURRENCIES.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<div className="flex items-baseline justify-between">
|
||||
<FieldLabel id="min-payout-label">
|
||||
Minimum Payout Amount
|
||||
</FieldLabel>
|
||||
<span className="text-2xl font-semibold tabular-nums">
|
||||
$2500.00
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={25}
|
||||
aria-labelledby="min-payout-label"
|
||||
aria-valuetext="$2,500 of $10,000"
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<FieldDescription>$50 (MIN)</FieldDescription>
|
||||
<FieldDescription>$10,000 (MAX)</FieldDescription>
|
||||
</div>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="payout-notes">Notes</FieldLabel>
|
||||
<Textarea
|
||||
id="payout-notes"
|
||||
placeholder="Add any notes for this payout configuration..."
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Save Threshold</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
67
apps/v4/app/(app)/(root)/cards/power-usage.tsx
Normal file
67
apps/v4/app/(app)/(root)/cards/power-usage.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Separator } from "@/styles/base-rhea/ui/separator"
|
||||
|
||||
const chartData = [
|
||||
{ hour: "6a", usage: 1.2 },
|
||||
{ hour: "8a", usage: 2.8 },
|
||||
{ hour: "10a", usage: 3.1 },
|
||||
{ hour: "12p", usage: 2.4 },
|
||||
{ hour: "2p", usage: 3.4 },
|
||||
{ hour: "4p", usage: 2.9 },
|
||||
{ hour: "6p", usage: 3.8 },
|
||||
{ hour: "8p", usage: 3.2 },
|
||||
]
|
||||
|
||||
export function PowerUsage() {
|
||||
const maxUsage = Math.max(...chartData.map((item) => item.usage))
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Power Usage</CardTitle>
|
||||
<CardDescription>Whole Home</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div
|
||||
className="flex h-[140px] w-full items-end gap-2"
|
||||
role="img"
|
||||
aria-label="Power usage by hour"
|
||||
>
|
||||
{chartData.map((item) => (
|
||||
<div
|
||||
key={item.hour}
|
||||
className="flex h-full flex-1 flex-col justify-end gap-1.5"
|
||||
>
|
||||
<div
|
||||
className="min-h-2 rounded-t bg-chart-2"
|
||||
style={{ height: `${(item.usage / maxUsage) * 100}%` }}
|
||||
/>
|
||||
<span className="text-center text-xs text-muted-foreground">
|
||||
{item.hour}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Currently Using
|
||||
</span>
|
||||
<span className="text-lg font-semibold tabular-nums">3.4 kW</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-sm text-muted-foreground">Solar Gen</span>
|
||||
<span className="text-lg font-semibold tabular-nums">+1.2 kW</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
64
apps/v4/app/(app)/(root)/cards/qr-connect.tsx
Normal file
64
apps/v4/app/(app)/(root)/cards/qr-connect.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
|
||||
const qrCells = [
|
||||
"111111100101101111111",
|
||||
"100000101001001000001",
|
||||
"101110101111101011101",
|
||||
"101110100100001011101",
|
||||
"101110101010101011101",
|
||||
"100000100111001000001",
|
||||
"111111101010101111111",
|
||||
"000000001101000000000",
|
||||
"101011111001111010110",
|
||||
"010100001110010101001",
|
||||
"111010111011101111010",
|
||||
"001101000101000010101",
|
||||
"110111101111010111011",
|
||||
"000000001001010001010",
|
||||
"111111101101111101001",
|
||||
"100000100010001001111",
|
||||
"101110101011101110100",
|
||||
"101110100110100010011",
|
||||
"101110101000111101110",
|
||||
"100000101101000011001",
|
||||
"111111101011101101111",
|
||||
]
|
||||
|
||||
export function QrConnect() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex justify-center pt-6">
|
||||
<div className="rounded-xl border bg-white p-4">
|
||||
<svg
|
||||
viewBox="0 0 21 21"
|
||||
className="size-40 text-black"
|
||||
role="img"
|
||||
aria-label="Connect device QR code"
|
||||
shapeRendering="crispEdges"
|
||||
>
|
||||
<rect width="21" height="21" fill="white" />
|
||||
{qrCells.map((row, y) =>
|
||||
[...row].map((cell, x) =>
|
||||
cell === "1" ? (
|
||||
<rect key={`${x}-${y}`} x={x} y={y} width="1" height="1" />
|
||||
) : null
|
||||
)
|
||||
)}
|
||||
</svg>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle>Scan to connect your mobile device</CardTitle>
|
||||
<CardDescription className="text-balance">
|
||||
Open the Ledger mobile app and scan this code to link your device.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
81
apps/v4/app/(app)/(root)/cards/savings-targets.tsx
Normal file
81
apps/v4/app/(app)/(root)/cards/savings-targets.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemFooter,
|
||||
ItemGroup,
|
||||
} from "@/styles/base-rhea/ui/item"
|
||||
import { Progress } from "@/styles/base-rhea/ui/progress"
|
||||
|
||||
export function SavingsTargets() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Savings Targets</CardTitle>
|
||||
<CardDescription>
|
||||
Active milestones for 2024 across your portfolio. Monitor how close
|
||||
you are to each savings goal.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ItemGroup className="gap-3">
|
||||
<Item
|
||||
role="listitem"
|
||||
variant="muted"
|
||||
className="flex-col items-stretch"
|
||||
>
|
||||
<ItemContent className="gap-3">
|
||||
<ItemDescription className="cn-font-heading text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Retirement
|
||||
</ItemDescription>
|
||||
<span className="text-3xl font-semibold tabular-nums">
|
||||
$420,000
|
||||
</span>
|
||||
<Progress value={65} aria-label="Retirement savings progress" />
|
||||
</ItemContent>
|
||||
<ItemFooter>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
65% achieved
|
||||
</span>
|
||||
<span className="text-sm font-medium tabular-nums">$273,000</span>
|
||||
</ItemFooter>
|
||||
</Item>
|
||||
<Item
|
||||
role="listitem"
|
||||
variant="muted"
|
||||
className="flex-col items-stretch"
|
||||
>
|
||||
<ItemContent className="gap-3">
|
||||
<ItemDescription className="cn-font-heading text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Real Estate
|
||||
</ItemDescription>
|
||||
<span className="text-3xl font-semibold tabular-nums">
|
||||
$85,000
|
||||
</span>
|
||||
<Progress value={32} aria-label="Real estate savings progress" />
|
||||
</ItemContent>
|
||||
<ItemFooter>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
32% achieved
|
||||
</span>
|
||||
<span className="text-sm font-medium tabular-nums">$27,200</span>
|
||||
</ItemFooter>
|
||||
</Item>
|
||||
</ItemGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<CardDescription className="text-center">
|
||||
You have not met your targets for this year.
|
||||
</CardDescription>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
218
apps/v4/app/(app)/(root)/cards/sidebar-nav.tsx
Normal file
218
apps/v4/app/(app)/(root)/cards/sidebar-nav.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import * as React from "react"
|
||||
import {
|
||||
ActivityIcon,
|
||||
Analytics01Icon,
|
||||
AnalyticsUpIcon,
|
||||
ArrowDataTransferHorizontalIcon,
|
||||
BankIcon,
|
||||
BookOpen02Icon,
|
||||
Calendar03Icon,
|
||||
ChartBarLineIcon,
|
||||
CreditCardIcon,
|
||||
File02Icon,
|
||||
Globe02Icon,
|
||||
HelpCircleIcon,
|
||||
Message01Icon,
|
||||
Notification03Icon,
|
||||
PaintBoardIcon,
|
||||
PieChartIcon,
|
||||
ShieldIcon,
|
||||
Target02Icon,
|
||||
UserIcon,
|
||||
Wallet01Icon,
|
||||
} from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Card } from "@/styles/base-rhea/ui/card"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarProvider,
|
||||
} from "@/styles/base-rhea/ui/sidebar"
|
||||
|
||||
function SidebarSection({
|
||||
label,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
label: string
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<Card className={cn("w-full overflow-hidden rounded-3xl py-0", className)}>
|
||||
<SidebarProvider className="min-h-0">
|
||||
<Sidebar collapsible="none" className="w-full bg-transparent">
|
||||
<SidebarContent className="gap-0 overflow-hidden">
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>{label}</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu className="gap-1">{children}</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
</SidebarProvider>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export function SidebarNav() {
|
||||
return (
|
||||
<div className="grid w-full grid-cols-2 gap-4 xl:gap-6">
|
||||
<SidebarSection
|
||||
label="Overview"
|
||||
className="xl:col-start-1 xl:row-start-2"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton isActive>
|
||||
<HugeiconsIcon icon={Analytics01Icon} strokeWidth={2} />
|
||||
Analytics
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon
|
||||
icon={ArrowDataTransferHorizontalIcon}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
Transactions
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={AnalyticsUpIcon} strokeWidth={2} />
|
||||
Investments
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={BankIcon} strokeWidth={2} />
|
||||
Accounts
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={PieChartIcon} strokeWidth={2} />
|
||||
Spending
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarSection>
|
||||
|
||||
<SidebarSection
|
||||
label="Planning"
|
||||
className="xl:col-start-1 xl:row-start-1"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={File02Icon} strokeWidth={2} />
|
||||
Documents
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={Wallet01Icon} strokeWidth={2} />
|
||||
Budget
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={ChartBarLineIcon} strokeWidth={2} />
|
||||
Reports
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={Target02Icon} strokeWidth={2} />
|
||||
Goals
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={Calendar03Icon} strokeWidth={2} />
|
||||
Calendar
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarSection>
|
||||
|
||||
<SidebarSection
|
||||
label="Support"
|
||||
className="flex xl:col-start-2 xl:row-start-1"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={HelpCircleIcon} strokeWidth={2} />
|
||||
Help Center
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={BookOpen02Icon} strokeWidth={2} />
|
||||
Docs
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={Message01Icon} strokeWidth={2} />
|
||||
Contact Us
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={ActivityIcon} strokeWidth={2} />
|
||||
Status
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={Globe02Icon} strokeWidth={2} />
|
||||
Community
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarSection>
|
||||
|
||||
<SidebarSection
|
||||
label="Account"
|
||||
className="flex xl:col-start-2 xl:row-start-2"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={UserIcon} strokeWidth={2} />
|
||||
Profile
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton isActive>
|
||||
<HugeiconsIcon icon={CreditCardIcon} strokeWidth={2} />
|
||||
Billing
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={Notification03Icon} strokeWidth={2} />
|
||||
Notifications
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={ShieldIcon} strokeWidth={2} />
|
||||
Security
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={PaintBoardIcon} strokeWidth={2} />
|
||||
Appearance
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarSection>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
35
apps/v4/app/(app)/(root)/cards/skeleton/account-access.tsx
Normal file
35
apps/v4/app/(app)/(root)/cards/skeleton/account-access.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
export function AccountAccess() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="gap-2">
|
||||
<Skeleton className="h-5 w-36 rounded-md" />
|
||||
<Skeleton className="h-4 w-64 rounded-md" />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-3 w-24 rounded-md" />
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-3 w-32 rounded-md" />
|
||||
<Skeleton className="h-3 w-12 rounded-md" />
|
||||
</div>
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-4">
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
<Skeleton className="h-14 w-full rounded-xl" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
17
apps/v4/app/(app)/(root)/cards/skeleton/analytics-card.tsx
Normal file
17
apps/v4/app/(app)/(root)/cards/skeleton/analytics-card.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Card, CardAction, CardHeader } from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
export function AnalyticsCard() {
|
||||
return (
|
||||
<Card className="mx-auto w-full max-w-sm data-[size=sm]:pb-0" size="sm">
|
||||
<CardHeader className="gap-2">
|
||||
<Skeleton className="h-5 w-24 rounded-md" />
|
||||
<Skeleton className="h-4 w-40 rounded-md" />
|
||||
<CardAction>
|
||||
<Skeleton className="h-7 w-28 rounded-lg" />
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<Skeleton className="mx-6 mb-6 aspect-[1/0.35] w-auto rounded-lg" />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
export function ClaimableBalance() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="gap-3">
|
||||
<Skeleton className="h-4 w-36 rounded-md" />
|
||||
<Skeleton className="h-12 w-56 rounded-lg" />
|
||||
<Skeleton className="h-6 w-32 rounded-full" />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-1 flex-col justify-end">
|
||||
<div className="flex flex-col gap-3 rounded-xl bg-muted p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-28 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-4 w-20 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-32 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-4 w-16 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
<Skeleton className="h-px w-full rounded-none bg-muted-foreground/15" />
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-36 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-4 w-24 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2">
|
||||
<Skeleton className="h-3 w-full rounded-md" />
|
||||
<Skeleton className="h-3 w-11/12 rounded-md" />
|
||||
<Skeleton className="h-3 w-3/4 rounded-md" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
const bars = [60, 80, 65, 95, 50, 100]
|
||||
|
||||
export function ContributionHistory() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="gap-2">
|
||||
<Skeleton className="h-5 w-44 rounded-md" />
|
||||
<Skeleton className="h-4 w-52 rounded-md" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex h-[200px] w-full items-end gap-3">
|
||||
{bars.map((height, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex h-full flex-1 flex-col justify-end gap-2"
|
||||
>
|
||||
<Skeleton
|
||||
className="w-full rounded-t-md rounded-b-none"
|
||||
style={{ height: `${height}%` }}
|
||||
/>
|
||||
<Skeleton className="mx-auto h-3 w-6 rounded-md" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<div className="grid w-full grid-cols-1 gap-3 xl:grid-cols-2">
|
||||
<div className="flex flex-col gap-2 rounded-xl bg-muted p-4">
|
||||
<Skeleton className="h-3 w-20 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-5 w-28 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-3 w-24 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-2 rounded-xl bg-muted p-4 xl:flex">
|
||||
<Skeleton className="h-3 w-24 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-5 w-32 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-3 w-28 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
49
apps/v4/app/(app)/(root)/cards/skeleton/dividend-income.tsx
Normal file
49
apps/v4/app/(app)/(root)/cards/skeleton/dividend-income.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
const rows = [0, 1, 2, 3]
|
||||
const miniBars = [40, 60, 80, 50]
|
||||
|
||||
export function DividendIncome() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="gap-2">
|
||||
<Skeleton className="h-5 w-48 rounded-md" />
|
||||
<Skeleton className="h-4 w-64 rounded-md" />
|
||||
<CardAction>
|
||||
<Skeleton className="size-8 rounded-md" />
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-2">
|
||||
{rows.map((row) => (
|
||||
<div
|
||||
key={row}
|
||||
className="flex items-center gap-3 rounded-xl bg-muted p-3"
|
||||
>
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Skeleton className="h-4 w-28 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-3 w-20 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
<div className="hidden h-8 w-24 items-end gap-1 md:flex">
|
||||
{miniBars.map((h, i) => (
|
||||
<Skeleton
|
||||
key={i}
|
||||
className="flex-1 rounded-t-sm rounded-b-none bg-muted-foreground/15"
|
||||
style={{ height: `${h}%` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Skeleton className="hidden h-4 w-16 rounded-md bg-muted-foreground/15 md:block" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Card, CardContent } from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
export function EmptyDistributeTrack() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="flex flex-col items-center gap-4 p-4">
|
||||
<Skeleton className="size-12 rounded-xl" />
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Skeleton className="h-5 w-40 rounded-md" />
|
||||
<Skeleton className="h-3 w-64 rounded-md" />
|
||||
<Skeleton className="h-3 w-48 rounded-md" />
|
||||
</div>
|
||||
<Skeleton className="h-9 w-32 rounded-lg" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
56
apps/v4/app/(app)/(root)/cards/skeleton/index.tsx
Normal file
56
apps/v4/app/(app)/(root)/cards/skeleton/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { AccountAccess } from "./account-access"
|
||||
import { AnalyticsCard } from "./analytics-card"
|
||||
import { ClaimableBalance } from "./claimable-balance"
|
||||
import { ContributionHistory } from "./contribution-history"
|
||||
import { DividendIncome } from "./dividend-income"
|
||||
import { EmptyDistributeTrack } from "./empty-distribute-track"
|
||||
import { NewMilestone } from "./new-milestone"
|
||||
import { NotificationSettings } from "./notification-settings"
|
||||
import { Payments } from "./payments"
|
||||
import { PayoutThreshold } from "./payout-threshold"
|
||||
import { PowerUsage } from "./power-usage"
|
||||
import { QrConnect } from "./qr-connect"
|
||||
import { SavingsTargets } from "./savings-targets"
|
||||
import { SidebarNav } from "./sidebar-nav"
|
||||
import { TransferFunds } from "./transfer-funds"
|
||||
import { UIElements } from "./ui-elements"
|
||||
|
||||
export function CardsSkeletonDemo() {
|
||||
return (
|
||||
<div
|
||||
data-slot="demo"
|
||||
className="theme-neutral relative flex w-full max-w-none flex-col gap-(--gap) bg-muted p-12 pb-0! [--gap:--spacing(8)] 3xl:[--gap:--spacing(8)] min-[1900px]:[--gap:--spacing(10)]! lg:p-8 lg:[--gap:--spacing(6)] xl:p-12 dark:bg-muted/30"
|
||||
>
|
||||
<div className="relative z-10 mx-auto grid gap-(--gap) **:data-[slot=card]:w-full min-[1900px]:grid-cols-5! md:max-w-3xl md:grid-cols-2 lg:max-w-none lg:grid-cols-3 xl:max-w-[1600px] xl:grid-cols-4 2xl:max-w-[1900px]">
|
||||
<div className="flex flex-col items-start gap-(--gap)">
|
||||
<UIElements />
|
||||
<SidebarNav />
|
||||
<SavingsTargets />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-(--gap) lg:flex">
|
||||
<ContributionHistory />
|
||||
<ClaimableBalance />
|
||||
<DividendIncome />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-(--gap) 3xl:flex!">
|
||||
<NewMilestone />
|
||||
<PayoutThreshold />
|
||||
<AccountAccess />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-(--gap) md:flex">
|
||||
<QrConnect />
|
||||
<TransferFunds />
|
||||
<Payments />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-(--gap) xl:flex">
|
||||
<EmptyDistributeTrack />
|
||||
<AnalyticsCard />
|
||||
<NotificationSettings />
|
||||
<PowerUsage />
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-x-0 top-0 z-1 h-80 bg-linear-to-b from-background via-muted to-transparent dark:via-muted/30" />
|
||||
<div className="absolute inset-x-0 bottom-0 z-20 h-80 bg-linear-to-t from-background via-muted to-transparent dark:via-muted/30" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
38
apps/v4/app/(app)/(root)/cards/skeleton/new-milestone.tsx
Normal file
38
apps/v4/app/(app)/(root)/cards/skeleton/new-milestone.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
export function NewMilestone() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="gap-2">
|
||||
<Skeleton className="h-5 w-44 rounded-md" />
|
||||
<Skeleton className="h-4 w-72 rounded-md" />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-3 w-20 rounded-md" />
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-3 w-24 rounded-md" />
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-3 w-20 rounded-md" />
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2">
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
const rows = [0, 1, 2, 3]
|
||||
|
||||
export function NotificationSettings() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="gap-2">
|
||||
<Skeleton className="h-5 w-32 rounded-md" />
|
||||
<Skeleton className="h-4 w-64 rounded-md" />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{rows.map((row) => (
|
||||
<div key={row} className="flex items-start gap-3">
|
||||
<Skeleton className="size-4 rounded-sm" />
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Skeleton className="h-4 w-40 rounded-md" />
|
||||
<Skeleton className="h-3 w-56 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
37
apps/v4/app/(app)/(root)/cards/skeleton/payments.tsx
Normal file
37
apps/v4/app/(app)/(root)/cards/skeleton/payments.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Card, CardContent, CardHeader } from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
const rows = [0, 1, 2]
|
||||
|
||||
export function Payments() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-4 w-12 rounded-md" />
|
||||
<Skeleton className="size-1.5 rounded-full" />
|
||||
<Skeleton className="size-7 rounded-md" />
|
||||
<Skeleton className="size-1.5 rounded-full" />
|
||||
<Skeleton className="h-4 w-20 rounded-md" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-2">
|
||||
{rows.map((row) => (
|
||||
<div
|
||||
key={row}
|
||||
className="flex items-center gap-3 rounded-xl bg-muted p-3"
|
||||
>
|
||||
<Skeleton className="size-9 rounded-lg bg-muted-foreground/15" />
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Skeleton className="h-4 w-40 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-3 w-56 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
<Skeleton className="size-4 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
43
apps/v4/app/(app)/(root)/cards/skeleton/payout-threshold.tsx
Normal file
43
apps/v4/app/(app)/(root)/cards/skeleton/payout-threshold.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
export function PayoutThreshold() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="gap-2">
|
||||
<Skeleton className="h-5 w-44 rounded-md" />
|
||||
<Skeleton className="h-4 w-72 rounded-md" />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-3 w-32 rounded-md" />
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-baseline justify-between">
|
||||
<Skeleton className="h-3 w-40 rounded-md" />
|
||||
<Skeleton className="h-7 w-24 rounded-md" />
|
||||
</div>
|
||||
<Skeleton className="h-2 w-full rounded-full" />
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-3 w-16 rounded-md" />
|
||||
<Skeleton className="h-3 w-20 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-3 w-16 rounded-md" />
|
||||
<Skeleton className="h-[100px] w-full rounded-lg" />
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
54
apps/v4/app/(app)/(root)/cards/skeleton/power-usage.tsx
Normal file
54
apps/v4/app/(app)/(root)/cards/skeleton/power-usage.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
const bars = [30, 70, 80, 60, 90, 75, 100, 85]
|
||||
|
||||
export function PowerUsage() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="gap-2">
|
||||
<Skeleton className="h-5 w-32 rounded-md" />
|
||||
<Skeleton className="h-4 w-24 rounded-md" />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex h-[140px] w-full items-end gap-2">
|
||||
{bars.map((height, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex h-full flex-1 flex-col justify-end gap-1.5"
|
||||
>
|
||||
<Skeleton
|
||||
className="w-full rounded-t rounded-b-none"
|
||||
style={{ height: `${height}%` }}
|
||||
/>
|
||||
<Skeleton className="mx-auto h-3 w-5 rounded-md" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Skeleton className="h-px w-full rounded-none" />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Skeleton className="h-3 w-28 rounded-md" />
|
||||
<Skeleton className="h-5 w-20 rounded-md" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Skeleton className="h-3 w-20 rounded-md" />
|
||||
<Skeleton className="h-5 w-24 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2">
|
||||
<Skeleton className="h-3 w-24 rounded-md" />
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<Skeleton className="h-2 flex-1 rounded-full" />
|
||||
<Skeleton className="h-3 w-10 rounded-md" />
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
17
apps/v4/app/(app)/(root)/cards/skeleton/qr-connect.tsx
Normal file
17
apps/v4/app/(app)/(root)/cards/skeleton/qr-connect.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Card, CardContent, CardHeader } from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
export function QrConnect() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex justify-center pt-6">
|
||||
<Skeleton className="size-44 rounded-xl" />
|
||||
</CardContent>
|
||||
<CardHeader className="items-center gap-2 text-center">
|
||||
<Skeleton className="h-5 w-56 rounded-md" />
|
||||
<Skeleton className="h-4 w-64 rounded-md" />
|
||||
<Skeleton className="h-4 w-48 rounded-md" />
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
44
apps/v4/app/(app)/(root)/cards/skeleton/savings-targets.tsx
Normal file
44
apps/v4/app/(app)/(root)/cards/skeleton/savings-targets.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
const rows = [0, 1]
|
||||
|
||||
export function SavingsTargets() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="gap-2">
|
||||
<Skeleton className="h-5 w-36 rounded-md" />
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Skeleton className="h-4 w-full max-w-64 rounded-md" />
|
||||
<Skeleton className="h-4 w-48 rounded-md" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-3">
|
||||
{rows.map((row) => (
|
||||
<div
|
||||
key={row}
|
||||
className="flex flex-col gap-3 rounded-xl bg-muted p-4"
|
||||
>
|
||||
<Skeleton className="h-3 w-24 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-8 w-36 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-2 w-full rounded-full bg-muted-foreground/15" />
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-3 w-24 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-3 w-20 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-center">
|
||||
<Skeleton className="h-3 w-56 rounded-md" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
39
apps/v4/app/(app)/(root)/cards/skeleton/sidebar-nav.tsx
Normal file
39
apps/v4/app/(app)/(root)/cards/skeleton/sidebar-nav.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Card } from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
const groupA = [0, 1, 2, 3, 4]
|
||||
const groupB = [0, 1, 2, 3, 4]
|
||||
|
||||
function NavSkeleton({ groups }: { groups: number[][] }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1 p-2">
|
||||
{groups.map((items, gi) => (
|
||||
<div key={gi} className="flex flex-col gap-1 px-2 py-1.5">
|
||||
<Skeleton className="mb-1 h-3 w-20 rounded-md" />
|
||||
{items.map((item) => (
|
||||
<div key={item} className="flex items-center gap-2 px-2 py-2">
|
||||
<Skeleton className="size-4 rounded-md" />
|
||||
<Skeleton className="h-3 w-24 rounded-md" />
|
||||
</div>
|
||||
))}
|
||||
{gi < groups.length - 1 && (
|
||||
<Skeleton className="my-1 h-px w-full rounded-none" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function SidebarNav() {
|
||||
return (
|
||||
<div className="grid w-full items-start gap-4 xl:grid-cols-2 xl:gap-6">
|
||||
<Card className="w-full overflow-hidden rounded-3xl py-0">
|
||||
<NavSkeleton groups={[groupA, groupB]} />
|
||||
</Card>
|
||||
<Card className="hidden w-full overflow-hidden rounded-3xl py-0 xl:flex">
|
||||
<NavSkeleton groups={[groupA, groupB]} />
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
55
apps/v4/app/(app)/(root)/cards/skeleton/transfer-funds.tsx
Normal file
55
apps/v4/app/(app)/(root)/cards/skeleton/transfer-funds.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
export function TransferFunds() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="gap-2">
|
||||
<Skeleton className="h-5 w-36 rounded-md" />
|
||||
<Skeleton className="h-4 w-64 rounded-md" />
|
||||
<CardAction>
|
||||
<Skeleton className="size-8 rounded-md" />
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-3 w-32 rounded-md" />
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-3 w-24 rounded-md" />
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-3 w-20 rounded-md" />
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 rounded-xl bg-muted p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-28 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-4 w-24 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
<Skeleton className="h-px w-full rounded-none bg-muted-foreground/15" />
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-28 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-4 w-12 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
<Skeleton className="h-px w-full rounded-none bg-muted-foreground/15" />
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-24 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-4 w-20 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
45
apps/v4/app/(app)/(root)/cards/skeleton/ui-elements.tsx
Normal file
45
apps/v4/app/(app)/(root)/cards/skeleton/ui-elements.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Card, CardContent } from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
export function UIElements() {
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardContent className="flex flex-col gap-6">
|
||||
<Skeleton className="h-8 w-full rounded-2xl" />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Skeleton className="h-9 w-20 rounded-lg" />
|
||||
<Skeleton className="h-9 w-24 rounded-lg" />
|
||||
<Skeleton className="h-9 w-20 rounded-lg" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
<Skeleton className="h-20 w-full rounded-lg" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-5 w-12 rounded-full" />
|
||||
<Skeleton className="h-5 w-16 rounded-full" />
|
||||
<Skeleton className="hidden h-5 w-14 rounded-full 4xl:block" />
|
||||
</div>
|
||||
<div className="ml-auto flex gap-3">
|
||||
<Skeleton className="size-4 rounded-full" />
|
||||
<Skeleton className="size-4 rounded-full" />
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Skeleton className="size-4 rounded-sm" />
|
||||
<Skeleton className="hidden size-4 rounded-sm 4xl:block" />
|
||||
</div>
|
||||
<Skeleton className="ml-auto h-5 w-9 rounded-full 4xl:hidden" />
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Skeleton className="h-9 w-24 rounded-lg" />
|
||||
<div className="flex">
|
||||
<Skeleton className="h-9 w-28 rounded-l-lg rounded-r-none" />
|
||||
<Skeleton className="ml-px h-9 w-9 rounded-l-none rounded-r-lg" />
|
||||
</div>
|
||||
<Skeleton className="ml-auto hidden h-5 w-9 rounded-full 4xl:block" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
139
apps/v4/app/(app)/(root)/cards/transfer-funds.tsx
Normal file
139
apps/v4/app/(app)/(root)/cards/transfer-funds.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Cancel01Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { Button } from "@/styles/base-rhea/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/styles/base-rhea/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
} from "@/styles/base-rhea/ui/input-group"
|
||||
import { Item, ItemContent } from "@/styles/base-rhea/ui/item"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/styles/base-rhea/ui/select"
|
||||
import { Separator } from "@/styles/base-rhea/ui/separator"
|
||||
|
||||
const FROM_ACCOUNTS = [
|
||||
{ label: "Main Checking (··8402) — $12,450.00", value: "checking" },
|
||||
{ label: "Business (··7731) — $8,920.00", value: "business" },
|
||||
]
|
||||
|
||||
const TO_ACCOUNTS = [
|
||||
{ label: "High Yield Savings (··1192) — $42,100.00", value: "savings" },
|
||||
{ label: "Investment (··3349) — $18,200.00", value: "investment" },
|
||||
]
|
||||
|
||||
export function TransferFunds() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Transfer Funds</CardTitle>
|
||||
<CardDescription>
|
||||
Move money between your connected accounts.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="bg-muted"
|
||||
aria-label="Dismiss transfer funds"
|
||||
>
|
||||
<HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} />
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="transfer-amount">
|
||||
Amount to Transfer
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<InputGroupText>$</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput id="transfer-amount" defaultValue="1,200.00" />
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="from-account">From Account</FieldLabel>
|
||||
<Select items={FROM_ACCOUNTS} defaultValue="checking">
|
||||
<SelectTrigger id="from-account" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{FROM_ACCOUNTS.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="to-account">To Account</FieldLabel>
|
||||
<Select items={TO_ACCOUNTS} defaultValue="savings">
|
||||
<SelectTrigger id="to-account" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{TO_ACCOUNTS.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Item variant="muted" className="flex-col items-stretch">
|
||||
<ItemContent className="gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Estimated arrival
|
||||
</span>
|
||||
<span className="text-sm font-medium">Today, Apr 14</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Transaction fee
|
||||
</span>
|
||||
<span className="text-sm font-medium tabular-nums">$0.00</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Total amount</span>
|
||||
<span className="text-sm font-semibold tabular-nums">
|
||||
$1,200.00
|
||||
</span>
|
||||
</div>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Confirm Transfer</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
176
apps/v4/app/(app)/(root)/cards/ui-elements.tsx
Normal file
176
apps/v4/app/(app)/(root)/cards/ui-elements.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
ArrowRight02Icon,
|
||||
ArrowUp01Icon,
|
||||
Search01Icon,
|
||||
} from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/styles/base-rhea/ui/alert-dialog"
|
||||
import { Badge } from "@/styles/base-rhea/ui/badge"
|
||||
import { Button } from "@/styles/base-rhea/ui/button"
|
||||
import { ButtonGroup } from "@/styles/base-rhea/ui/button-group"
|
||||
import { Card, CardContent } from "@/styles/base-rhea/ui/card"
|
||||
import { Checkbox } from "@/styles/base-rhea/ui/checkbox"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/base-rhea/ui/dropdown-menu"
|
||||
import { Field, FieldGroup } from "@/styles/base-rhea/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
} from "@/styles/base-rhea/ui/input-group"
|
||||
import { RadioGroup, RadioGroupItem } from "@/styles/base-rhea/ui/radio-group"
|
||||
import { Switch } from "@/styles/base-rhea/ui/switch"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/styles/base-rhea/ui/tabs"
|
||||
import { Textarea } from "@/styles/base-rhea/ui/textarea"
|
||||
|
||||
export function UIElements() {
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardContent className="flex flex-col gap-6">
|
||||
<div className="flex gap-2">
|
||||
<Button>
|
||||
Button{" "}
|
||||
<HugeiconsIcon
|
||||
icon={ArrowRight02Icon}
|
||||
strokeWidth={2}
|
||||
data-icon="inline-end"
|
||||
/>
|
||||
</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="outline">Outline</Button>
|
||||
</div>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="Name" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>
|
||||
<HugeiconsIcon icon={Search01Icon} strokeWidth={2} />
|
||||
</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field className="flex-1">
|
||||
<Textarea placeholder="Message" className="resize-none" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-2">
|
||||
<Badge>Badge</Badge>
|
||||
<Badge variant="secondary">Secondary</Badge>
|
||||
<Badge variant="outline" className="hidden 4xl:flex">
|
||||
Outline
|
||||
</Badge>
|
||||
</div>
|
||||
<RadioGroup
|
||||
defaultValue="apple"
|
||||
className="ml-auto flex w-fit gap-3"
|
||||
aria-label="Fruit preference"
|
||||
>
|
||||
<RadioGroupItem value="apple" aria-label="Apple" />
|
||||
<RadioGroupItem value="banana" aria-label="Banana" />
|
||||
</RadioGroup>
|
||||
<div className="flex gap-3">
|
||||
<Checkbox defaultChecked aria-label="Enable email alerts" />
|
||||
<Checkbox
|
||||
className="hidden 4xl:flex"
|
||||
aria-label="Enable push alerts"
|
||||
/>
|
||||
</div>
|
||||
<Switch
|
||||
defaultChecked
|
||||
className="flex 4xl:hidden"
|
||||
aria-label="Enable compact notifications"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger render={<Button variant="outline" />}>
|
||||
<span className="hidden md:flex style-sera:md:hidden">
|
||||
Alert Dialog
|
||||
</span>
|
||||
<span className="flex md:hidden style-sera:md:flex">Dialog</span>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent size="sm">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Allow accessory to connect?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Do you want to allow the USB accessory to connect to this
|
||||
device and your data?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Don't allow</AlertDialogCancel>
|
||||
<AlertDialogAction>Allow</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ButtonGroup className="ml-auto">
|
||||
<Button variant="outline">
|
||||
<span className="style-sera:hidden">Button Group</span>
|
||||
<span className="hidden style-sera:block">Group</span>
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="Open quick actions"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<HugeiconsIcon icon={ArrowUp01Icon} strokeWidth={2} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" side="top" className="w-40">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Quick Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem>Mute Conversation</DropdownMenuItem>
|
||||
<DropdownMenuItem>Mark as Read</DropdownMenuItem>
|
||||
<DropdownMenuItem>Block User</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
Delete Conversation
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
<Switch
|
||||
defaultChecked
|
||||
className="hidden 4xl:flex"
|
||||
aria-label="Enable advanced setting"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/examples/radix/ui/field"
|
||||
import { Input } from "@/examples/radix/ui/input"
|
||||
import { RadioGroup, RadioGroupItem } from "@/examples/radix/ui/radio-group"
|
||||
import { Switch } from "@/examples/radix/ui/switch"
|
||||
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
||||
|
||||
export function AppearanceSettings() {
|
||||
const [gpuCount, setGpuCount] = React.useState(8)
|
||||
|
||||
const handleGpuAdjustment = React.useCallback((adjustment: number) => {
|
||||
setGpuCount((prevCount) =>
|
||||
Math.max(1, Math.min(99, prevCount + adjustment))
|
||||
)
|
||||
}, [])
|
||||
|
||||
const handleGpuInputChange = React.useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(e.target.value, 10)
|
||||
if (!isNaN(value) && value >= 1 && value <= 99) {
|
||||
setGpuCount(value)
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend>Compute Environment</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select the compute environment for your cluster.
|
||||
</FieldDescription>
|
||||
<RadioGroup defaultValue="kubernetes">
|
||||
<FieldLabel htmlFor="kubernetes-r2h">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Kubernetes</FieldTitle>
|
||||
<FieldDescription>
|
||||
Run GPU workloads on a K8s configured cluster. This is the
|
||||
default.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="kubernetes"
|
||||
id="kubernetes-r2h"
|
||||
aria-label="Kubernetes"
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="vm-z4k">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Virtual Machine</FieldTitle>
|
||||
<FieldDescription>
|
||||
Access a VM configured cluster to run workloads. (Coming
|
||||
soon)
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="vm"
|
||||
id="vm-z4k"
|
||||
aria-label="Virtual Machine"
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="number-of-gpus-f6l">Number of GPUs</FieldLabel>
|
||||
<FieldDescription>You can add more later.</FieldDescription>
|
||||
</FieldContent>
|
||||
<ButtonGroup>
|
||||
<Input
|
||||
id="number-of-gpus-f6l"
|
||||
value={gpuCount}
|
||||
onChange={handleGpuInputChange}
|
||||
size={3}
|
||||
className="h-7 !w-14 font-mono"
|
||||
maxLength={3}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
aria-label="Decrement"
|
||||
onClick={() => handleGpuAdjustment(-1)}
|
||||
disabled={gpuCount <= 1}
|
||||
>
|
||||
<IconMinus />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
aria-label="Increment"
|
||||
onClick={() => handleGpuAdjustment(1)}
|
||||
disabled={gpuCount >= 99}
|
||||
>
|
||||
<IconPlus />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="tinting">Wallpaper Tinting</FieldLabel>
|
||||
<FieldDescription>
|
||||
Allow the wallpaper to be tinted.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="tinting" defaultChecked />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
)
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/examples/radix/ui/dropdown-menu"
|
||||
import {
|
||||
ArchiveIcon,
|
||||
ArrowLeftIcon,
|
||||
CalendarPlusIcon,
|
||||
ClockIcon,
|
||||
ListFilterIcon,
|
||||
MailCheckIcon,
|
||||
MoreHorizontalIcon,
|
||||
TagIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react"
|
||||
|
||||
export function ButtonGroupDemo() {
|
||||
const [label, setLabel] = React.useState("personal")
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<ButtonGroup className="hidden sm:flex">
|
||||
<Button variant="outline" size="icon-sm" aria-label="Go Back">
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
Archive
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Report
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
Snooze
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon-sm" aria-label="More Options">
|
||||
<MoreHorizontalIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<MailCheckIcon />
|
||||
Mark as Read
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<ArchiveIcon />
|
||||
Archive
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<ClockIcon />
|
||||
Snooze
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CalendarPlusIcon />
|
||||
Add to Calendar
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<ListFilterIcon />
|
||||
Add to List
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<TagIcon />
|
||||
Label As...
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuRadioGroup
|
||||
value={label}
|
||||
onValueChange={setLabel}
|
||||
>
|
||||
<DropdownMenuRadioItem value="personal">
|
||||
Personal
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="work">
|
||||
Work
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="other">
|
||||
Other
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
<Trash2Icon />
|
||||
Trash
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/examples/radix/ui/tooltip"
|
||||
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
||||
|
||||
export function ButtonGroupInputGroup() {
|
||||
const [voiceEnabled, setVoiceEnabled] = React.useState(false)
|
||||
return (
|
||||
<ButtonGroup className="[--radius:9999rem]">
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="icon" aria-label="Add">
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="flex-1">
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
placeholder={
|
||||
voiceEnabled ? "Record and send audio..." : "Send a message..."
|
||||
}
|
||||
disabled={voiceEnabled}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton
|
||||
onClick={() => setVoiceEnabled(!voiceEnabled)}
|
||||
data-active={voiceEnabled}
|
||||
className="data-[active=true]:bg-primary data-[active=true]:text-primary-foreground"
|
||||
aria-pressed={voiceEnabled}
|
||||
size="icon-xs"
|
||||
aria-label="Voice Mode"
|
||||
>
|
||||
<AudioLinesIcon />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Voice Mode</TooltipContent>
|
||||
</Tooltip>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"
|
||||
|
||||
export function ButtonGroupNested() {
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
1
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
2
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
3
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="icon-sm" aria-label="Previous">
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon-sm" aria-label="Next">
|
||||
<ArrowRightIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/examples/radix/ui/popover"
|
||||
import { Separator } from "@/examples/radix/ui/separator"
|
||||
import { Textarea } from "@/examples/radix/ui/textarea"
|
||||
import { BotIcon, ChevronDownIcon } from "lucide-react"
|
||||
|
||||
export function ButtonGroupPopover() {
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
<BotIcon /> Copilot
|
||||
</Button>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="icon-sm" aria-label="Open Popover">
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="rounded-xl p-0 text-sm">
|
||||
<div className="px-4 py-3">
|
||||
<div className="text-sm font-medium">Agent Tasks</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="p-4 text-sm *:[p:not(:last-child)]:mb-2">
|
||||
<Textarea
|
||||
placeholder="Describe your task in natural language."
|
||||
className="mb-4 resize-none"
|
||||
/>
|
||||
<p className="font-medium">Start a new task with Copilot</p>
|
||||
<p className="text-muted-foreground">
|
||||
Describe your task in natural language. Copilot will work in the
|
||||
background and open a pull request for your review.
|
||||
</p>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarGroup,
|
||||
AvatarImage,
|
||||
} from "@/examples/radix/ui/avatar"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/examples/radix/ui/empty"
|
||||
import { PlusIcon } from "lucide-react"
|
||||
|
||||
export function EmptyAvatarGroup() {
|
||||
return (
|
||||
<Empty className="flex-none border py-10">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia>
|
||||
<AvatarGroup className="grayscale">
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</AvatarGroup>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No Team Members</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Invite your team to collaborate on this project.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button size="sm">
|
||||
<PlusIcon />
|
||||
Invite Members
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { SearchIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyTitle,
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
|
||||
|
||||
export function EmptyInputGroup() {
|
||||
return (
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>404 - Not Found</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
The page you're looking for doesn't exist. Try searching for
|
||||
what you need below.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<InputGroup className="w-3/4">
|
||||
<InputGroupInput placeholder="Try searching for pages..." />
|
||||
<InputGroupAddon>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Kbd>/</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<EmptyDescription>
|
||||
Need help? <a href="#">Contact support</a>
|
||||
</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Checkbox } from "@/examples/radix/ui/checkbox"
|
||||
import { Field, FieldLabel } from "@/examples/radix/ui/field"
|
||||
|
||||
export function FieldCheckbox() {
|
||||
return (
|
||||
<FieldLabel htmlFor="checkbox-demo">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="checkbox-demo" defaultChecked />
|
||||
<FieldLabel htmlFor="checkbox-demo" className="line-clamp-1">
|
||||
I agree to the terms and conditions
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
)
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
|
||||
export function FieldChoiceCard() {
|
||||
return (
|
||||
<div className="w-full max-w-md">
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLabel htmlFor="compute-environment-p8w">
|
||||
Compute Environment
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Select the compute environment for your cluster.
|
||||
</FieldDescription>
|
||||
<RadioGroup defaultValue="kubernetes">
|
||||
<FieldLabel htmlFor="kubernetes-r2h">
|
||||
<Field orientation="horizontal">
|
||||
<RadioGroupItem
|
||||
value="kubernetes"
|
||||
id="kubernetes-r2h"
|
||||
aria-label="Kubernetes"
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldTitle>Kubernetes</FieldTitle>
|
||||
<FieldDescription>
|
||||
Run GPU workloads on a K8s configured cluster.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="vm-z4k">
|
||||
<Field orientation="horizontal">
|
||||
<RadioGroupItem
|
||||
value="vm"
|
||||
id="vm-z4k"
|
||||
aria-label="Virtual Machine"
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldTitle>Virtual Machine</FieldTitle>
|
||||
<FieldDescription>
|
||||
Access a VM configured cluster to run workloads.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { Checkbox } from "@/examples/radix/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
} from "@/examples/radix/ui/field"
|
||||
import { Input } from "@/examples/radix/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/examples/radix/ui/select"
|
||||
import { Textarea } from "@/examples/radix/ui/textarea"
|
||||
|
||||
export function FieldDemo() {
|
||||
return (
|
||||
<div className="w-full max-w-md rounded-lg border p-6">
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend>Payment Method</FieldLegend>
|
||||
<FieldDescription>
|
||||
All transactions are secure and encrypted
|
||||
</FieldDescription>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="checkout-7j9-card-name-43j">
|
||||
Name on Card
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="checkout-7j9-card-name-43j"
|
||||
placeholder="John Doe"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Field className="col-span-2">
|
||||
<FieldLabel htmlFor="checkout-7j9-card-number-uw1">
|
||||
Card Number
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="checkout-7j9-card-number-uw1"
|
||||
placeholder="1234 5678 9012 3456"
|
||||
required
|
||||
/>
|
||||
<FieldDescription>
|
||||
Enter your 16-digit number.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field className="col-span-1">
|
||||
<FieldLabel htmlFor="checkout-7j9-cvv">CVV</FieldLabel>
|
||||
<Input id="checkout-7j9-cvv" placeholder="123" required />
|
||||
</Field>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="checkout-7j9-exp-month-ts6">
|
||||
Month
|
||||
</FieldLabel>
|
||||
<Select defaultValue="">
|
||||
<SelectTrigger id="checkout-7j9-exp-month-ts6">
|
||||
<SelectValue placeholder="MM" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="01">01</SelectItem>
|
||||
<SelectItem value="02">02</SelectItem>
|
||||
<SelectItem value="03">03</SelectItem>
|
||||
<SelectItem value="04">04</SelectItem>
|
||||
<SelectItem value="05">05</SelectItem>
|
||||
<SelectItem value="06">06</SelectItem>
|
||||
<SelectItem value="07">07</SelectItem>
|
||||
<SelectItem value="08">08</SelectItem>
|
||||
<SelectItem value="09">09</SelectItem>
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
<SelectItem value="11">11</SelectItem>
|
||||
<SelectItem value="12">12</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="checkout-7j9-exp-year-f59">
|
||||
Year
|
||||
</FieldLabel>
|
||||
<Select defaultValue="">
|
||||
<SelectTrigger id="checkout-7j9-exp-year-f59">
|
||||
<SelectValue placeholder="YYYY" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="2024">2024</SelectItem>
|
||||
<SelectItem value="2025">2025</SelectItem>
|
||||
<SelectItem value="2026">2026</SelectItem>
|
||||
<SelectItem value="2027">2027</SelectItem>
|
||||
<SelectItem value="2028">2028</SelectItem>
|
||||
<SelectItem value="2029">2029</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<FieldSet>
|
||||
<FieldLegend>Billing Address</FieldLegend>
|
||||
<FieldDescription>
|
||||
The billing address associated with your payment method
|
||||
</FieldDescription>
|
||||
<FieldGroup>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
id="checkout-7j9-same-as-shipping-wgm"
|
||||
defaultChecked
|
||||
/>
|
||||
<FieldLabel
|
||||
htmlFor="checkout-7j9-same-as-shipping-wgm"
|
||||
className="font-normal"
|
||||
>
|
||||
Same as shipping address
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="checkout-7j9-optional-comments">
|
||||
Comments
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
id="checkout-7j9-optional-comments"
|
||||
placeholder="Add any additional comments"
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<Field orientation="horizontal">
|
||||
<Button type="submit">Submit</Button>
|
||||
<Button variant="outline" type="button">
|
||||
Cancel
|
||||
</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import { Card, CardContent } from "@/examples/radix/ui/card"
|
||||
import { Checkbox } from "@/examples/radix/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/examples/radix/ui/field"
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: "Social Media",
|
||||
value: "social-media",
|
||||
},
|
||||
|
||||
{
|
||||
label: "Search Engine",
|
||||
value: "search-engine",
|
||||
},
|
||||
{
|
||||
label: "Referral",
|
||||
value: "referral",
|
||||
},
|
||||
{
|
||||
label: "Other",
|
||||
value: "other",
|
||||
},
|
||||
]
|
||||
|
||||
export function FieldHear() {
|
||||
return (
|
||||
<Card className="py-4 shadow-none">
|
||||
<CardContent className="px-4">
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<FieldSet className="gap-4">
|
||||
<FieldLegend>How did you hear about us?</FieldLegend>
|
||||
<FieldDescription className="line-clamp-1">
|
||||
Select the option that best describes how you heard about us.
|
||||
</FieldDescription>
|
||||
<FieldGroup className="flex flex-row flex-wrap gap-2 [--radius:9999rem]">
|
||||
{options.map((option) => (
|
||||
<FieldLabel
|
||||
htmlFor={option.value}
|
||||
key={option.value}
|
||||
className="!w-fit"
|
||||
>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
className="gap-1.5 overflow-hidden px-3! py-1.5! transition-all duration-100 ease-linear group-has-data-[state=checked]/field-label:px-2!"
|
||||
>
|
||||
<Checkbox
|
||||
value={option.value}
|
||||
id={option.value}
|
||||
defaultChecked={option.value === "social-media"}
|
||||
className="-ml-6 -translate-x-1 rounded-full transition-all duration-100 ease-linear data-[state=checked]:ml-0 data-[state=checked]:translate-x-0"
|
||||
/>
|
||||
<FieldTitle>{option.label}</FieldTitle>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Field, FieldDescription, FieldTitle } from "@/examples/radix/ui/field"
|
||||
import { Slider } from "@/examples/radix/ui/slider"
|
||||
|
||||
export function FieldSlider() {
|
||||
const [value, setValue] = useState([200, 800])
|
||||
return (
|
||||
<div className="w-full max-w-md">
|
||||
<Field>
|
||||
<FieldTitle>Price Range</FieldTitle>
|
||||
<FieldDescription>
|
||||
Set your budget range ($
|
||||
<span className="font-medium tabular-nums">{value[0]}</span> -{" "}
|
||||
<span className="font-medium tabular-nums">{value[1]}</span>).
|
||||
</FieldDescription>
|
||||
<Slider
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
max={1000}
|
||||
min={0}
|
||||
step={10}
|
||||
className="mt-2 w-full"
|
||||
aria-label="Price Range"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { FieldSeparator } from "@/examples/radix/ui/field"
|
||||
|
||||
import { AppearanceSettings } from "./appearance-settings"
|
||||
import { ButtonGroupDemo } from "./button-group-demo"
|
||||
import { ButtonGroupInputGroup } from "./button-group-input-group"
|
||||
import { ButtonGroupNested } from "./button-group-nested"
|
||||
import { ButtonGroupPopover } from "./button-group-popover"
|
||||
import { EmptyAvatarGroup } from "./empty-avatar-group"
|
||||
import { FieldCheckbox } from "./field-checkbox"
|
||||
import { FieldDemo } from "./field-demo"
|
||||
import { FieldHear } from "./field-hear"
|
||||
import { FieldSlider } from "./field-slider"
|
||||
import { InputGroupButtonExample } from "./input-group-button"
|
||||
import { InputGroupDemo } from "./input-group-demo"
|
||||
import { ItemDemo } from "./item-demo"
|
||||
import { NotionPromptForm } from "./notion-prompt-form"
|
||||
import { SpinnerBadge } from "./spinner-badge"
|
||||
import { SpinnerEmpty } from "./spinner-empty"
|
||||
|
||||
export function RootComponents() {
|
||||
return (
|
||||
<div className="theme-container mx-auto grid gap-8 py-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-6 2xl:gap-8">
|
||||
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
|
||||
<FieldDemo />
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
|
||||
<EmptyAvatarGroup />
|
||||
<SpinnerBadge />
|
||||
<ButtonGroupInputGroup />
|
||||
<FieldSlider />
|
||||
<InputGroupDemo />
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
|
||||
<InputGroupButtonExample />
|
||||
<ItemDemo />
|
||||
<FieldSeparator className="my-4">Appearance Settings</FieldSeparator>
|
||||
<AppearanceSettings />
|
||||
</div>
|
||||
<div className="order-first flex flex-col gap-6 lg:hidden xl:order-last xl:flex *:[div]:w-full *:[div]:max-w-full">
|
||||
<NotionPromptForm />
|
||||
<ButtonGroupDemo />
|
||||
<FieldCheckbox />
|
||||
<div className="flex justify-between gap-4">
|
||||
<ButtonGroupNested />
|
||||
<ButtonGroupPopover />
|
||||
</div>
|
||||
<FieldHear />
|
||||
<SpinnerEmpty />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
import { Label } from "@/examples/radix/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/examples/radix/ui/popover"
|
||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
||||
|
||||
export function InputGroupButtonExample() {
|
||||
const [isFavorite, setIsFavorite] = React.useState(false)
|
||||
|
||||
return (
|
||||
<div className="grid w-full max-w-sm gap-6">
|
||||
<Label htmlFor="input-secure-19" className="sr-only">
|
||||
Input Secure
|
||||
</Label>
|
||||
<InputGroup className="[--radius:9999px]">
|
||||
<InputGroupInput id="input-secure-19" className="!pl-0.5" />
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupAddon>
|
||||
<InputGroupButton
|
||||
variant="secondary"
|
||||
size="icon-xs"
|
||||
aria-label="Info"
|
||||
>
|
||||
<IconInfoCircle />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
alignOffset={10}
|
||||
className="flex flex-col gap-1 rounded-xl text-sm"
|
||||
>
|
||||
<p className="font-medium">Your connection is not secure.</p>
|
||||
<p>You should not enter any sensitive information on this site.</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<InputGroupAddon className="text-muted-foreground !pl-1">
|
||||
https://
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton
|
||||
onClick={() => setIsFavorite(!isFavorite)}
|
||||
size="icon-xs"
|
||||
aria-label="Favorite"
|
||||
>
|
||||
<IconStar
|
||||
data-favorite={isFavorite}
|
||||
className="data-[favorite=true]:fill-primary data-[favorite=true]:stroke-primary"
|
||||
/>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/examples/radix/ui/dropdown-menu"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
import { Separator } from "@/examples/radix/ui/separator"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/examples/radix/ui/tooltip"
|
||||
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
|
||||
import { ArrowUpIcon, Search } from "lucide-react"
|
||||
|
||||
export function InputGroupDemo() {
|
||||
return (
|
||||
<div className="grid w-full max-w-sm gap-6">
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="Search..." />
|
||||
<InputGroupAddon>
|
||||
<Search />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">12 results</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="example.com" className="!pl-1" />
|
||||
<InputGroupAddon>
|
||||
<InputGroupText>https://</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton
|
||||
className="rounded-full"
|
||||
size="icon-xs"
|
||||
aria-label="Info"
|
||||
>
|
||||
<IconInfoCircle />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>This is content in a tooltip.</TooltipContent>
|
||||
</Tooltip>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea placeholder="Ask, Search or Chat..." />
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupButton
|
||||
variant="outline"
|
||||
className="rounded-full"
|
||||
size="icon-xs"
|
||||
aria-label="Add"
|
||||
>
|
||||
<IconPlus />
|
||||
</InputGroupButton>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton variant="ghost">Auto</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" align="start">
|
||||
<DropdownMenuItem>Auto</DropdownMenuItem>
|
||||
<DropdownMenuItem>Agent</DropdownMenuItem>
|
||||
<DropdownMenuItem>Manual</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<InputGroupText className="ml-auto">52% used</InputGroupText>
|
||||
<Separator orientation="vertical" className="!h-4" />
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
className="rounded-full"
|
||||
size="icon-xs"
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
<span className="sr-only">Send</span>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="@shadcn" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<div className="bg-primary text-foreground flex size-4 items-center justify-center rounded-full">
|
||||
<IconCheck className="size-3 text-white" />
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import {
|
||||
IconBrandJavascript,
|
||||
IconCopy,
|
||||
IconCornerDownLeft,
|
||||
IconRefresh,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
|
||||
export function InputGroupTextareaExample() {
|
||||
return (
|
||||
<div className="grid w-full max-w-md gap-4">
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-code-32"
|
||||
placeholder="console.log('Hello, world!');"
|
||||
className="min-h-[180px]"
|
||||
/>
|
||||
<InputGroupAddon align="block-end" className="border-t">
|
||||
<InputGroupText>Line 1, Column 1</InputGroupText>
|
||||
<InputGroupButton size="sm" className="ml-auto" variant="default">
|
||||
Run <IconCornerDownLeft />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="block-start" className="border-b">
|
||||
<InputGroupText className="font-mono font-medium">
|
||||
<IconBrandJavascript />
|
||||
script.js
|
||||
</InputGroupText>
|
||||
<InputGroupButton className="ml-auto">
|
||||
<IconRefresh />
|
||||
</InputGroupButton>
|
||||
<InputGroupButton variant="ghost">
|
||||
<IconCopy />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { Plus } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/new-york-v4/ui/item"
|
||||
|
||||
export function ItemAvatar() {
|
||||
return (
|
||||
<div className="flex w-full max-w-lg flex-col gap-6">
|
||||
<Item variant="outline" className="hidden">
|
||||
<ItemMedia>
|
||||
<Avatar className="size-10">
|
||||
<AvatarImage src="https://github.com/maxleiter.png" />
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Max Leiter</ItemTitle>
|
||||
<ItemDescription>Last seen 5 months ago</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="outline"
|
||||
className="rounded-full"
|
||||
aria-label="Invite"
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="outline">
|
||||
<ItemMedia>
|
||||
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
|
||||
<Avatar className="hidden sm:flex">
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar className="hidden sm:flex">
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>No Team Members</ItemTitle>
|
||||
<ItemDescription>Invite your team to collaborate.</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button size="sm" variant="outline">
|
||||
Invite
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/examples/radix/ui/item"
|
||||
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
||||
|
||||
export function ItemDemo() {
|
||||
return (
|
||||
<div className="flex w-full max-w-md flex-col gap-6">
|
||||
<Item variant="outline">
|
||||
<ItemContent>
|
||||
<ItemTitle>Two-factor authentication</ItemTitle>
|
||||
<ItemDescription className="text-pretty xl:hidden 2xl:block">
|
||||
Verify via email or phone number.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button size="sm">Enable</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="outline" size="sm" asChild>
|
||||
<a href="#">
|
||||
<ItemMedia>
|
||||
<BadgeCheckIcon className="size-5" />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Your profile has been verified.</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</ItemActions>
|
||||
</a>
|
||||
</Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,443 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/examples/radix/ui/avatar"
|
||||
import { Badge } from "@/examples/radix/ui/badge"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/examples/radix/ui/command"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/examples/radix/ui/dropdown-menu"
|
||||
import { Field, FieldLabel } from "@/examples/radix/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupTextarea,
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/examples/radix/ui/popover"
|
||||
import { Switch } from "@/examples/radix/ui/switch"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/examples/radix/ui/tooltip"
|
||||
import {
|
||||
IconApps,
|
||||
IconArrowUp,
|
||||
IconAt,
|
||||
IconBook,
|
||||
IconCircleDashedPlus,
|
||||
IconPaperclip,
|
||||
IconPlus,
|
||||
IconWorld,
|
||||
IconX,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
const SAMPLE_DATA = {
|
||||
mentionable: [
|
||||
{
|
||||
type: "page",
|
||||
title: "Meeting Notes",
|
||||
image: "📝",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Project Dashboard",
|
||||
image: "📊",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Ideas & Brainstorming",
|
||||
image: "💡",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Calendar & Events",
|
||||
image: "📅",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Documentation",
|
||||
image: "📚",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Goals & Objectives",
|
||||
image: "🎯",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Budget Planning",
|
||||
image: "💰",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Team Directory",
|
||||
image: "👥",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Technical Specs",
|
||||
image: "🔧",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Analytics Report",
|
||||
image: "📈",
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
title: "shadcn",
|
||||
image: "https://github.com/shadcn.png",
|
||||
workspace: "Workspace",
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
title: "maxleiter",
|
||||
image: "https://github.com/maxleiter.png",
|
||||
workspace: "Workspace",
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
title: "evilrabbit",
|
||||
image: "https://github.com/evilrabbit.png",
|
||||
workspace: "Workspace",
|
||||
},
|
||||
],
|
||||
models: [
|
||||
{
|
||||
name: "Auto",
|
||||
},
|
||||
{
|
||||
name: "Agent Mode",
|
||||
badge: "Beta",
|
||||
},
|
||||
{
|
||||
name: "Plan Mode",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
function MentionableIcon({
|
||||
item,
|
||||
}: {
|
||||
item: (typeof SAMPLE_DATA.mentionable)[0]
|
||||
}) {
|
||||
return item.type === "page" ? (
|
||||
<span className="flex size-4 items-center justify-center">
|
||||
{item.image}
|
||||
</span>
|
||||
) : (
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage src={item.image} />
|
||||
<AvatarFallback>{item.title[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
)
|
||||
}
|
||||
|
||||
export function NotionPromptForm() {
|
||||
const [mentions, setMentions] = useState<string[]>([])
|
||||
const [mentionPopoverOpen, setMentionPopoverOpen] = useState(false)
|
||||
const [modelPopoverOpen, setModelPopoverOpen] = useState(false)
|
||||
const [selectedModel, setSelectedModel] = useState<
|
||||
(typeof SAMPLE_DATA.models)[0]
|
||||
>(SAMPLE_DATA.models[0])
|
||||
const [scopeMenuOpen, setScopeMenuOpen] = useState(false)
|
||||
|
||||
const grouped = useMemo(() => {
|
||||
return SAMPLE_DATA.mentionable.reduce(
|
||||
(acc, item) => {
|
||||
const isAvailable = !mentions.includes(item.title)
|
||||
|
||||
if (isAvailable) {
|
||||
if (!acc[item.type]) {
|
||||
acc[item.type] = []
|
||||
}
|
||||
acc[item.type].push(item)
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, typeof SAMPLE_DATA.mentionable>
|
||||
)
|
||||
}, [mentions])
|
||||
|
||||
const hasMentions = mentions.length > 0
|
||||
|
||||
return (
|
||||
<form>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="notion-prompt" className="sr-only">
|
||||
Prompt
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="notion-prompt"
|
||||
placeholder="Ask, search, or make anything..."
|
||||
/>
|
||||
<InputGroupAddon align="block-start">
|
||||
<Popover
|
||||
open={mentionPopoverOpen}
|
||||
onOpenChange={setMentionPopoverOpen}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
asChild
|
||||
onFocusCapture={(e) => e.stopPropagation()}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupButton
|
||||
variant="outline"
|
||||
size={!hasMentions ? "sm" : "icon-sm"}
|
||||
className="rounded-full transition-transform"
|
||||
>
|
||||
<IconAt /> {!hasMentions && "Add context"}
|
||||
</InputGroupButton>
|
||||
</PopoverTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Mention a person, page, or date</TooltipContent>
|
||||
</Tooltip>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search pages..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No pages found</CommandEmpty>
|
||||
{Object.entries(grouped).map(([type, items]) => (
|
||||
<CommandGroup
|
||||
key={type}
|
||||
heading={type === "page" ? "Pages" : "Users"}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<CommandItem
|
||||
key={item.title}
|
||||
value={item.title}
|
||||
onSelect={(currentValue) => {
|
||||
setMentions((prev) => [...prev, currentValue])
|
||||
setMentionPopoverOpen(false)
|
||||
}}
|
||||
>
|
||||
<MentionableIcon item={item} />
|
||||
{item.title}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div className="no-scrollbar -m-1.5 flex gap-1 overflow-y-auto p-1.5">
|
||||
{mentions.map((mention) => {
|
||||
const item = SAMPLE_DATA.mentionable.find(
|
||||
(item) => item.title === mention
|
||||
)
|
||||
|
||||
if (!item) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<InputGroupButton
|
||||
key={mention}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="rounded-full !pl-2"
|
||||
onClick={() => {
|
||||
setMentions((prev) => prev.filter((m) => m !== mention))
|
||||
}}
|
||||
>
|
||||
<MentionableIcon item={item} />
|
||||
{item.title}
|
||||
<IconX />
|
||||
</InputGroupButton>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="block-end" className="gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton
|
||||
size="icon-sm"
|
||||
className="rounded-full"
|
||||
aria-label="Attach file"
|
||||
>
|
||||
<IconPaperclip />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Attach file</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenu
|
||||
open={modelPopoverOpen}
|
||||
onOpenChange={setModelPopoverOpen}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton size="sm" className="rounded-full">
|
||||
{selectedModel.name}
|
||||
</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Select AI model</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent side="top" align="start" className="w-48">
|
||||
<DropdownMenuGroup className="w-48">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
Select Agent Mode
|
||||
</DropdownMenuLabel>
|
||||
{SAMPLE_DATA.models.map((model) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={model.name}
|
||||
checked={model.name === selectedModel.name}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setSelectedModel(model)
|
||||
}
|
||||
}}
|
||||
className="pl-2 *:[span:first-child]:right-2 *:[span:first-child]:left-auto"
|
||||
>
|
||||
{model.name}
|
||||
{model.badge && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-5 rounded-sm bg-blue-100 px-1 text-xs text-blue-800 dark:bg-blue-900 dark:text-blue-100"
|
||||
>
|
||||
{model.badge}
|
||||
</Badge>
|
||||
)}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu open={scopeMenuOpen} onOpenChange={setScopeMenuOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton size="sm" className="rounded-full">
|
||||
<IconWorld /> All Sources
|
||||
</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" align="end" className="w-72">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<label htmlFor="web-search">
|
||||
<IconWorld /> Web Search{" "}
|
||||
<Switch
|
||||
id="web-search"
|
||||
className="ml-auto"
|
||||
defaultChecked
|
||||
/>
|
||||
</label>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<label htmlFor="apps">
|
||||
<IconApps /> Apps and Integrations
|
||||
<Switch id="apps" className="ml-auto" defaultChecked />
|
||||
</label>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconCircleDashedPlus /> All Sources I can access
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage src="https://github.com/shadcn.png" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
shadcn
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="w-72 p-0 [--radius:1rem]">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Find or use knowledge in..."
|
||||
autoFocus
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No knowledge found</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{SAMPLE_DATA.mentionable
|
||||
.filter((item) => item.type === "user")
|
||||
.map((user) => (
|
||||
<CommandItem
|
||||
key={user.title}
|
||||
value={user.title}
|
||||
onSelect={() => {
|
||||
// Handle user selection here
|
||||
console.log("Selected user:", user.title)
|
||||
}}
|
||||
>
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage src={user.image} />
|
||||
<AvatarFallback>
|
||||
{user.title[0]}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
{user.title}{" "}
|
||||
<span className="text-muted-foreground">
|
||||
- {user.workspace}
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuItem>
|
||||
<IconBook /> Help Center
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<IconPlus /> Connect Apps
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
We'll only search in the sources selected here.
|
||||
</DropdownMenuLabel>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<InputGroupButton
|
||||
aria-label="Send"
|
||||
className="ml-auto rounded-full"
|
||||
variant="default"
|
||||
size="icon-sm"
|
||||
>
|
||||
<IconArrowUp />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Badge } from "@/examples/radix/ui/badge"
|
||||
import { Spinner } from "@/examples/radix/ui/spinner"
|
||||
|
||||
export function SpinnerBadge() {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge>
|
||||
<Spinner />
|
||||
Syncing
|
||||
</Badge>
|
||||
<Badge variant="secondary">
|
||||
<Spinner />
|
||||
Updating
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
<Spinner />
|
||||
Loading
|
||||
</Badge>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/examples/radix/ui/empty"
|
||||
import { Spinner } from "@/examples/radix/ui/spinner"
|
||||
|
||||
export function SpinnerEmpty() {
|
||||
return (
|
||||
<Empty className="w-full border md:p-6">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<Spinner />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Processing your request</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Please wait while we process your request. Do not refresh the page.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +1,18 @@
|
||||
import { type Metadata } from "next"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import { IconArrowRight } from "@tabler/icons-react"
|
||||
|
||||
import { Announcement } from "@/components/announcement"
|
||||
import { ExamplesNav } from "@/components/examples-nav"
|
||||
import {
|
||||
PageActions,
|
||||
PageHeader,
|
||||
PageHeaderDescription,
|
||||
PageHeaderHeading,
|
||||
} from "@/components/page-header"
|
||||
import { PageNav } from "@/components/page-nav"
|
||||
import { ThemeSelector } from "@/components/theme-selector"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
|
||||
import { RootComponents } from "./components"
|
||||
import { CardsDemo } from "./cards"
|
||||
|
||||
const title = "The Foundation for your Design System"
|
||||
const description =
|
||||
@@ -50,45 +48,40 @@ export const metadata: Metadata = {
|
||||
export default function IndexPage() {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col">
|
||||
<PageHeader>
|
||||
<PageHeader className="md:**:[.container]:pb-8 lg:**:[.container]:pb-12">
|
||||
<Announcement />
|
||||
<PageHeaderHeading className="max-w-4xl">{title}</PageHeaderHeading>
|
||||
<PageHeaderDescription>{description}</PageHeaderDescription>
|
||||
<PageActions>
|
||||
<Button asChild size="sm" className="h-[31px] rounded-lg">
|
||||
<Link href="/docs/installation">Get Started</Link>
|
||||
</Button>
|
||||
<Button asChild size="sm" variant="ghost" className="rounded-lg">
|
||||
<Link href="/docs/components">View Components</Link>
|
||||
<Button asChild className="h-[31px] rounded-lg">
|
||||
<Link href="/create?preset=b27GcrRo">
|
||||
Build Your Own <IconArrowRight data-icon="inline-end" />
|
||||
</Link>
|
||||
</Button>
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
<PageNav className="hidden md:flex">
|
||||
<ExamplesNav className="[&>a:first-child]:text-primary flex-1 overflow-hidden" />
|
||||
<ThemeSelector className="mr-4 hidden md:flex" />
|
||||
</PageNav>
|
||||
<div className="container-wrapper section-soft flex-1 pb-6">
|
||||
<div className="container overflow-hidden">
|
||||
<section className="border-border/50 -mx-4 w-[160vw] overflow-hidden rounded-lg border md:hidden md:w-[150vw]">
|
||||
<div className="container-wrapper flex-1 p-0">
|
||||
<div className="container overflow-hidden md:px-0 lg:max-w-none">
|
||||
<section className="-mx-4 w-[140vw] overflow-hidden md:hidden">
|
||||
<Image
|
||||
src="/r/styles/new-york-v4/dashboard-01-light.png"
|
||||
width={1400}
|
||||
height={875}
|
||||
src="/images/full-light.png"
|
||||
width={2560}
|
||||
height={2764}
|
||||
alt="Dashboard"
|
||||
className="block dark:hidden"
|
||||
className="block h-auto w-full dark:hidden"
|
||||
priority
|
||||
/>
|
||||
<Image
|
||||
src="/r/styles/new-york-v4/dashboard-01-dark.png"
|
||||
width={1400}
|
||||
height={875}
|
||||
src="/images/full-dark.png"
|
||||
width={2560}
|
||||
height={2764}
|
||||
alt="Dashboard"
|
||||
className="hidden dark:block"
|
||||
className="hidden h-auto w-full dark:block"
|
||||
priority
|
||||
/>
|
||||
</section>
|
||||
<section className="theme-container hidden md:block">
|
||||
<RootComponents />
|
||||
<section className="hidden md:block">
|
||||
<CardsDemo />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
"use client"
|
||||
|
||||
import { ChevronLeftIcon, ChevronRightIcon, SearchIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Badge } from "@/styles/base-sera/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/styles/base-sera/ui/input-group"
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
} from "@/styles/base-sera/ui/pagination"
|
||||
import { Progress, ProgressValue } from "@/styles/base-sera/ui/progress"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/styles/base-sera/ui/table"
|
||||
|
||||
const ARTICLE_ROWS = [
|
||||
{
|
||||
title: "The Future of Sustainable Architecture",
|
||||
wordProgress: "1.4k / 2.6k words",
|
||||
author: "Elena Rostova",
|
||||
issue: "Summer 2024",
|
||||
status: "in-revision",
|
||||
statusLabel: "In revision",
|
||||
progress: 45,
|
||||
},
|
||||
{
|
||||
title: "Brutalism's Second Act",
|
||||
wordProgress: "2.1k / 2.5k words",
|
||||
author: "Marcus Chen",
|
||||
issue: "Summer 2024",
|
||||
status: "final-edit",
|
||||
statusLabel: "Final edit",
|
||||
progress: 90,
|
||||
},
|
||||
{
|
||||
title: "The Typography of Public Spaces",
|
||||
wordProgress: "0.5k / 1.5k words",
|
||||
author: "Sarah Jenkins",
|
||||
issue: "Autumn 2024",
|
||||
status: "drafting",
|
||||
statusLabel: "Drafting",
|
||||
progress: 20,
|
||||
},
|
||||
{
|
||||
title: "Rethinking Urban Canopies",
|
||||
wordProgress: "1.8k / 1.8k words",
|
||||
author: "David O'Connor",
|
||||
issue: "Summer 2024",
|
||||
status: "published",
|
||||
statusLabel: "Published",
|
||||
progress: 100,
|
||||
},
|
||||
{
|
||||
title: "Light, Glass, and the Modern Museum",
|
||||
wordProgress: "1.2k / 2.0k words",
|
||||
author: "Amara Osei",
|
||||
issue: "Autumn 2024",
|
||||
status: "in-revision",
|
||||
statusLabel: "In revision",
|
||||
progress: 55,
|
||||
},
|
||||
{
|
||||
title: "Concrete Utopias: Housing in the 21st Century",
|
||||
wordProgress: "3.0k / 3.0k words",
|
||||
author: "Tomás Herrera",
|
||||
issue: "Summer 2024",
|
||||
status: "published",
|
||||
statusLabel: "Published",
|
||||
progress: 100,
|
||||
},
|
||||
{
|
||||
title: "Designing for Silence",
|
||||
wordProgress: "0.8k / 2.2k words",
|
||||
author: "Ingrid Solberg",
|
||||
issue: "Winter 2024",
|
||||
status: "drafting",
|
||||
statusLabel: "Drafting",
|
||||
progress: 30,
|
||||
},
|
||||
{
|
||||
title: "The Invisible Infrastructure of Cities",
|
||||
wordProgress: "2.4k / 2.8k words",
|
||||
author: "James Whitfield",
|
||||
issue: "Autumn 2024",
|
||||
status: "final-edit",
|
||||
statusLabel: "Final edit",
|
||||
progress: 85,
|
||||
},
|
||||
] as const
|
||||
|
||||
const STATUS_BADGE_VARIANT = {
|
||||
"in-revision": "outline",
|
||||
"final-edit": "default",
|
||||
drafting: "ghost",
|
||||
published: "secondary",
|
||||
} as const
|
||||
|
||||
const STATUS_DOT_CLASSNAME = {
|
||||
"in-revision": "bg-amber-600/80",
|
||||
"final-edit": "bg-foreground/90",
|
||||
drafting: "bg-muted-foreground/60",
|
||||
published: "bg-emerald-600/80",
|
||||
}
|
||||
|
||||
export function ArticleDirectory() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput type="search" placeholder="Search articles..." />
|
||||
</InputGroup>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableHead>Title</TableHead>
|
||||
<TableHead className="w-[170px]">Author</TableHead>
|
||||
<TableHead className="w-[150px]">Issue</TableHead>
|
||||
<TableHead className="w-[180px]">Status</TableHead>
|
||||
<TableHead className="w-[140px]">Progress</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{ARTICLE_ROWS.map((row) => (
|
||||
<TableRow key={row.title}>
|
||||
<TableCell>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="font-heading text-xl tracking-tight text-foreground">
|
||||
{row.title}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{row.wordProgress}
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{row.author}</TableCell>
|
||||
<TableCell>{row.issue}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={STATUS_BADGE_VARIANT[row.status]}>
|
||||
<span
|
||||
className={cn(
|
||||
"size-1.5 rounded-full",
|
||||
STATUS_DOT_CLASSNAME[row.status]
|
||||
)}
|
||||
/>
|
||||
{row.statusLabel}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Progress
|
||||
value={row.progress}
|
||||
aria-label={`${row.progress}% complete`}
|
||||
className="flex flex-row-reverse items-center **:data-[slot=progress-track]:w-16"
|
||||
>
|
||||
<ProgressValue>
|
||||
{(formattedValue) => `${formattedValue}`}
|
||||
</ProgressValue>
|
||||
</Progress>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationLink
|
||||
href="#"
|
||||
size="icon-sm"
|
||||
aria-label="Previous page"
|
||||
>
|
||||
<ChevronLeftIcon className="cn-rtl-flip" />
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
{[1, 2, 3].map((page) => (
|
||||
<PaginationItem key={page}>
|
||||
<PaginationLink href="#" size="icon-sm" isActive={page === 1}>
|
||||
{page}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
))}
|
||||
<PaginationItem>
|
||||
<PaginationLink href="#" size="icon-sm" aria-label="Next page">
|
||||
<ChevronRightIcon className="cn-rtl-flip" />
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ArrowLeftIcon, PlusIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/styles/base-sera/ui/breadcrumb"
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import { ButtonGroup } from "@/styles/base-sera/ui/button-group"
|
||||
|
||||
export function PreviewHeader() {
|
||||
return (
|
||||
<header>
|
||||
<div className="container flex flex-col items-center justify-center gap-(--gap) py-(--gap) sm:flex-row sm:justify-between">
|
||||
<div className="flex flex-col gap-2 text-center sm:text-left">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList className="justify-center md:justify-start">
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink
|
||||
href="#"
|
||||
className="inline-flex items-center gap-1.5"
|
||||
>
|
||||
<ArrowLeftIcon className="size-3" />
|
||||
Editorial Dashboard
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<h1 className="line-clamp-1 font-heading text-3xl tracking-wide uppercase md:text-3xl lg:text-4xl">
|
||||
Article Directory
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<ButtonGroup className="gap-2 sm:ml-auto md:gap-4">
|
||||
<Button>
|
||||
<PlusIcon data-icon="inline-start" />
|
||||
New Article
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
16
apps/v4/app/(app)/(styles)/sera/article-directory/index.tsx
Normal file
16
apps/v4/app/(app)/(styles)/sera/article-directory/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Separator } from "@/styles/base-sera/ui/separator"
|
||||
|
||||
import { ArticleDirectory as ArticleDirectoryList } from "./components/article-directory"
|
||||
import { PreviewHeader } from "./components/preview-header"
|
||||
|
||||
export function ArticleDirectory() {
|
||||
return (
|
||||
<div className="preview theme-taupe @container/preview w-full flex-1 bg-muted pt-4 font-sans ring-1 ring-foreground/5 [--gap:--spacing(4)] sm:pt-0 md:[--gap:--spacing(6)] xl:[--gap:--spacing(8)] 2xl:py-8 **:[.container]:px-(--gap)">
|
||||
<PreviewHeader />
|
||||
<Separator className="hidden sm:block" />
|
||||
<div className="container py-(--gap)">
|
||||
<ArticleDirectoryList />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { MoveRightIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
import {
|
||||
Progress,
|
||||
ProgressLabel,
|
||||
ProgressValue,
|
||||
} from "@/styles/base-sera/ui/progress"
|
||||
|
||||
const DEMOGRAPHIC_DATA = [
|
||||
{ age: "18 - 24", percentage: 22 },
|
||||
{ age: "25 - 34", percentage: 64 },
|
||||
{ age: "35 - 44", percentage: 12 },
|
||||
{ age: "45+", percentage: 5 },
|
||||
]
|
||||
|
||||
export function Demographics({ ...props }: React.ComponentProps<typeof Card>) {
|
||||
return (
|
||||
<Card {...props}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Demographics</CardTitle>
|
||||
<CardDescription>Reader Profile</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-10">
|
||||
{DEMOGRAPHIC_DATA.map((item) => (
|
||||
<Progress
|
||||
key={item.age}
|
||||
value={item.percentage}
|
||||
aria-label={item.age}
|
||||
>
|
||||
<ProgressLabel>{item.age}</ProgressLabel>
|
||||
<ProgressValue>
|
||||
{(formattedValue) => `${formattedValue}`}
|
||||
</ProgressValue>
|
||||
</Progress>
|
||||
))}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="link" className="w-full">
|
||||
View all source <MoveRightIcon data-icon="inline-end" />
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { TrendingDownIcon, TrendingUpIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
|
||||
type Metric = {
|
||||
label: string
|
||||
value: string
|
||||
comparison: string
|
||||
change: string
|
||||
trend: "up" | "down"
|
||||
}
|
||||
|
||||
const METRIC_CARDS: Metric[] = [
|
||||
{
|
||||
label: "Total visitors",
|
||||
value: "248.5k",
|
||||
comparison: "12.4%",
|
||||
change: "vs last period",
|
||||
trend: "up",
|
||||
},
|
||||
{
|
||||
label: "Unique readers",
|
||||
value: "182.1k",
|
||||
comparison: "8.7%",
|
||||
change: "vs last period",
|
||||
trend: "up",
|
||||
},
|
||||
{
|
||||
label: "Avg. time on page",
|
||||
value: "3m 42s",
|
||||
comparison: "1.2%",
|
||||
change: "vs last period",
|
||||
trend: "down",
|
||||
},
|
||||
{
|
||||
label: "Bounce rate",
|
||||
value: "42.8%",
|
||||
comparison: "3.5%",
|
||||
change: "vs last period",
|
||||
trend: "down",
|
||||
},
|
||||
]
|
||||
|
||||
export function MetricsGrid() {
|
||||
return (
|
||||
<>
|
||||
{METRIC_CARDS.map((metric) => (
|
||||
<MetricCard
|
||||
key={metric.label}
|
||||
metric={metric}
|
||||
className="col-span-full md:col-span-6 lg:col-span-3"
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function MetricCard({
|
||||
metric,
|
||||
className,
|
||||
}: {
|
||||
metric: Metric
|
||||
className: string
|
||||
}) {
|
||||
return (
|
||||
<Card className={cn("gap-0", className)}>
|
||||
<CardContent className="flex flex-col gap-2">
|
||||
<CardDescription className="text-xs uppercase">
|
||||
{metric.label}
|
||||
</CardDescription>
|
||||
<CardTitle className="text-5xl tracking-tight lowercase">
|
||||
{metric.value}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{metric.trend === "up" ? (
|
||||
<TrendingUpIcon className="inline-block size-2.5 text-muted-foreground" />
|
||||
) : (
|
||||
<TrendingDownIcon className="inline-block size-2.5 text-muted-foreground" />
|
||||
)}{" "}
|
||||
<span className="text-foreground">{metric.comparison}</span>{" "}
|
||||
<span>{metric.change}</span>
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ChevronDownIcon, DownloadIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import { ButtonGroup } from "@/styles/base-sera/ui/button-group"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/base-sera/ui/dropdown-menu"
|
||||
|
||||
const EXPORT_DATE_OPTIONS = [
|
||||
{
|
||||
label: "Last 7 days",
|
||||
value: "last-7-days",
|
||||
},
|
||||
{
|
||||
label: "Last 30 days",
|
||||
value: "last-30-days",
|
||||
},
|
||||
{
|
||||
label: "This month",
|
||||
value: "this-month",
|
||||
},
|
||||
{
|
||||
label: "Last month",
|
||||
value: "last-month",
|
||||
},
|
||||
]
|
||||
|
||||
export function PreviewHeader() {
|
||||
const [selectedDateRange, setSelectedDateRange] =
|
||||
React.useState("last-30-days")
|
||||
|
||||
const selectedDateRangeLabel = React.useMemo(() => {
|
||||
const selectedOption = EXPORT_DATE_OPTIONS.find(
|
||||
(option) => option.value === selectedDateRange
|
||||
)
|
||||
|
||||
if (!selectedOption) {
|
||||
return "Last 30 days"
|
||||
}
|
||||
|
||||
return selectedOption.label
|
||||
}, [selectedDateRange])
|
||||
|
||||
return (
|
||||
<header>
|
||||
<div className="container flex flex-col items-center justify-center gap-(--gap) py-(--gap) sm:flex-row sm:justify-between">
|
||||
<div className="flex flex-col gap-2 text-center sm:text-left">
|
||||
<h1 className="line-clamp-1 font-heading text-3xl tracking-wide uppercase md:text-3xl lg:text-4xl">
|
||||
Audience Analytics
|
||||
</h1>
|
||||
<div className="line-clamp-1 text-sm font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Editorial Performance Dashboard
|
||||
</div>
|
||||
</div>
|
||||
<ButtonGroup className="gap-2 sm:ml-auto md:gap-4">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-background hover:bg-background/80 data-popup-open:bg-background"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{selectedDateRangeLabel}{" "}
|
||||
<ChevronDownIcon data-icon="inline-end" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuRadioGroup
|
||||
value={selectedDateRange}
|
||||
onValueChange={setSelectedDateRange}
|
||||
>
|
||||
{EXPORT_DATE_OPTIONS.map((option) => (
|
||||
<DropdownMenuRadioItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
>
|
||||
{option.label}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button>
|
||||
<DownloadIcon data-icon="inline-start" />
|
||||
<span className="lg:hidden">Export</span>
|
||||
<span className="hidden lg:inline">Export Report</span>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ArrowDownIcon, MoreHorizontalIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/base-sera/ui/dropdown-menu"
|
||||
import { Spinner } from "@/styles/base-sera/ui/spinner"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/styles/base-sera/ui/table"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/styles/base-sera/ui/toggle-group"
|
||||
|
||||
type EditorialMetric = "views" | "time" | "shares"
|
||||
|
||||
type EditorialRow = {
|
||||
rank: number
|
||||
title: string
|
||||
author: string
|
||||
published: string
|
||||
pageviews: string
|
||||
avgTime: string
|
||||
}
|
||||
|
||||
const METRIC_LABEL: Record<EditorialMetric, string> = {
|
||||
views: "VIEWS",
|
||||
time: "TIME",
|
||||
shares: "SHARES",
|
||||
}
|
||||
|
||||
const EDITORIAL_ROWS: EditorialRow[] = [
|
||||
{
|
||||
rank: 1,
|
||||
title: "The New Vanguard of Minimalist Architecture",
|
||||
author: "Elena Rostova",
|
||||
published: "Oct 12",
|
||||
pageviews: "45.2k",
|
||||
avgTime: "04:15",
|
||||
},
|
||||
{
|
||||
rank: 2,
|
||||
title: "Autumn Sartorial Code: Deconstructed Classics",
|
||||
author: "Julian Vance",
|
||||
published: "Oct 05",
|
||||
pageviews: "38.9k",
|
||||
avgTime: "03:42",
|
||||
},
|
||||
{
|
||||
rank: 3,
|
||||
title: "Interview: Director Sofia Coppola on The Aesthetics of Isolation",
|
||||
author: "Marcus Trent",
|
||||
published: "Sep 28",
|
||||
pageviews: "31.4k",
|
||||
avgTime: "06:20",
|
||||
},
|
||||
{
|
||||
rank: 4,
|
||||
title: "Sourcing Ceramics from Kyoto's Oldest Kilns",
|
||||
author: "Sarah Lin",
|
||||
published: "Oct 18",
|
||||
pageviews: "22.1k",
|
||||
avgTime: "02:55",
|
||||
},
|
||||
{
|
||||
rank: 5,
|
||||
title: "Field Notes from Copenhagen Design Week",
|
||||
author: "Noah Bennett",
|
||||
published: "Oct 21",
|
||||
pageviews: "19.7k",
|
||||
avgTime: "03:18",
|
||||
},
|
||||
{
|
||||
rank: 6,
|
||||
title: "A Studio Visit with Milan's Most Elusive Lighting Designer",
|
||||
author: "Claire Duval",
|
||||
published: "Oct 09",
|
||||
pageviews: "17.4k",
|
||||
avgTime: "04:02",
|
||||
},
|
||||
{
|
||||
rank: 7,
|
||||
title: "Collecting the New Avant-Garde in Contemporary Furniture",
|
||||
author: "Tommy Rhodes",
|
||||
published: "Sep 30",
|
||||
pageviews: "15.9k",
|
||||
avgTime: "03:36",
|
||||
},
|
||||
{
|
||||
rank: 8,
|
||||
title: "Inside Lisbon's Quiet Culinary Renaissance",
|
||||
author: "Amara Iqbal",
|
||||
published: "Oct 14",
|
||||
pageviews: "14.2k",
|
||||
avgTime: "05:08",
|
||||
},
|
||||
{
|
||||
rank: 9,
|
||||
title: "Why Slow Interiors Are Defining the Next Luxury Wave",
|
||||
author: "Henry Vale",
|
||||
published: "Oct 03",
|
||||
pageviews: "12.7k",
|
||||
avgTime: "03:11",
|
||||
},
|
||||
{
|
||||
rank: 10,
|
||||
title: "The Return of Print: Independent Magazine Covers to Watch",
|
||||
author: "Mina Okafor",
|
||||
published: "Sep 26",
|
||||
pageviews: "11.3k",
|
||||
avgTime: "02:49",
|
||||
},
|
||||
]
|
||||
|
||||
type TopEditorialProps = React.ComponentProps<typeof Card> & {
|
||||
selectedMetric?: EditorialMetric
|
||||
}
|
||||
|
||||
export function TopEditorial({
|
||||
selectedMetric = "views",
|
||||
...props
|
||||
}: TopEditorialProps) {
|
||||
const [visibleCount, setVisibleCount] = React.useState(5)
|
||||
const [isLoadingMore, setIsLoadingMore] = React.useState(false)
|
||||
const hasMoreRows = visibleCount < EDITORIAL_ROWS.length
|
||||
const visibleRows = EDITORIAL_ROWS.slice(0, visibleCount)
|
||||
|
||||
const handleLoadMore = React.useCallback(() => {
|
||||
if (!hasMoreRows || isLoadingMore) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoadingMore(true)
|
||||
|
||||
window.setTimeout(() => {
|
||||
setVisibleCount(EDITORIAL_ROWS.length)
|
||||
setIsLoadingMore(false)
|
||||
}, 2000)
|
||||
}, [hasMoreRows, isLoadingMore])
|
||||
|
||||
return (
|
||||
<Card {...props}>
|
||||
<CardHeader>
|
||||
<div className="flex flex-col gap-(--gap) sm:flex-row">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<CardTitle className="text-2xl">Top Editorials</CardTitle>
|
||||
<CardDescription>Ranked by engagement</CardDescription>
|
||||
</div>
|
||||
<ToggleGroup
|
||||
aria-label="Top editorials metric selector"
|
||||
value={[selectedMetric]}
|
||||
variant="outline"
|
||||
className="w-full sm:ml-auto sm:w-fit"
|
||||
>
|
||||
{(["views", "time", "shares"] as const).map((metric) => {
|
||||
return (
|
||||
<ToggleGroupItem key={metric} value={metric} className="flex-1">
|
||||
{METRIC_LABEL[metric]}
|
||||
</ToggleGroupItem>
|
||||
)
|
||||
})}
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 **:data-[slot=table-container]:no-scrollbar **:data-[slot=table-container]:overflow-y-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>#</TableHead>
|
||||
<TableHead>Title</TableHead>
|
||||
<TableHead>Published</TableHead>
|
||||
<TableHead>Page Views</TableHead>
|
||||
<TableHead>Read Time</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{visibleRows.map((row) => (
|
||||
<TableRow key={row.rank}>
|
||||
<TableCell className="translate-y-1 align-text-top">
|
||||
{row.rank}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="font-heading text-xl tracking-tight text-foreground">
|
||||
{row.title}
|
||||
</p>
|
||||
<p className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">
|
||||
By {row.author}
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{row.published}</TableCell>
|
||||
<TableCell>{row.pageviews}</TableCell>
|
||||
<TableCell>{row.avgTime}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<Button variant="ghost" size="icon-xs" />}
|
||||
aria-label={`Open actions for ${row.title}`}
|
||||
>
|
||||
<MoreHorizontalIcon />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Publish</DropdownMenuItem>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-center">
|
||||
{hasMoreRows ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleLoadMore}
|
||||
disabled={isLoadingMore}
|
||||
>
|
||||
Load more content{" "}
|
||||
{isLoadingMore ? (
|
||||
<Spinner data-icon="inline-end" />
|
||||
) : (
|
||||
<ArrowDownIcon data-icon="inline-end" />
|
||||
)}
|
||||
</Button>
|
||||
) : null}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import dynamic from "next/dynamic"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
|
||||
const TrafficOverviewContent = dynamic(
|
||||
() => import("./traffic-overview").then((mod) => mod.TrafficOverview),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <TrafficOverviewFallback />,
|
||||
}
|
||||
)
|
||||
|
||||
export function TrafficOverviewDeferred({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Card>) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<TrafficOverviewContent {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TrafficOverviewFallback() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Traffic Overview</CardTitle>
|
||||
<CardDescription>
|
||||
Traffic for the last 30 days has increased by 12.4% compared to the
|
||||
previous period.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="flex h-82 w-full flex-col justify-end gap-6 overflow-hidden bg-muted/40 p-5"
|
||||
>
|
||||
<div className="h-px w-full bg-border" />
|
||||
<div className="h-px w-full bg-border" />
|
||||
<div className="h-px w-full bg-border" />
|
||||
<div className="h-px w-full bg-border" />
|
||||
<div className="h-px w-full bg-border" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
"use client"
|
||||
|
||||
import { TrendingUpIcon } from "lucide-react"
|
||||
import {
|
||||
CartesianGrid,
|
||||
Line,
|
||||
LineChart,
|
||||
ReferenceDot,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts"
|
||||
|
||||
import { Badge } from "@/styles/base-sera/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/styles/base-sera/ui/chart"
|
||||
|
||||
const TRAFFIC_OVERVIEW_DATA = [
|
||||
{ date: "2025-10-01", views: 2600, unique: 1600 },
|
||||
{ date: "2025-10-04", views: 4500, unique: 3000 },
|
||||
{ date: "2025-10-08", views: 3500, unique: 2500 },
|
||||
{ date: "2025-10-10", views: 6400, unique: 4500 },
|
||||
{ date: "2025-10-13", views: 5400, unique: 4000 },
|
||||
{ date: "2025-10-15", views: 8300, unique: 6500 },
|
||||
{ date: "2025-10-17", views: 7400, unique: 6000 },
|
||||
{ date: "2025-10-18", views: 9240, unique: 7105 },
|
||||
{ date: "2025-10-22", views: 7700, unique: 6400 },
|
||||
{ date: "2025-10-26", views: 8800, unique: 7000 },
|
||||
{ date: "2025-10-29", views: 9800, unique: 8400 },
|
||||
]
|
||||
|
||||
const TRAFFIC_CHART_CONFIG = {
|
||||
views: {
|
||||
label: "Views",
|
||||
theme: {
|
||||
light: "var(--chart-5)",
|
||||
dark: "var(--chart-1)",
|
||||
},
|
||||
},
|
||||
unique: {
|
||||
label: "Unique",
|
||||
theme: {
|
||||
light: "var(--chart-1)",
|
||||
dark: "var(--chart-2)",
|
||||
},
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
const X_AXIS_DATE_FORMATTER = new Intl.DateTimeFormat("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
timeZone: "UTC",
|
||||
})
|
||||
|
||||
function formatYAxisTick(value: number) {
|
||||
if (value === 0) {
|
||||
return "0"
|
||||
}
|
||||
|
||||
if (value % 1000 === 0) {
|
||||
return `${value / 1000}k`
|
||||
}
|
||||
|
||||
return `${value / 1000}k`
|
||||
}
|
||||
|
||||
function formatXAxisTick(value: string) {
|
||||
const date = new Date(`${value}T00:00:00Z`)
|
||||
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return value
|
||||
}
|
||||
|
||||
return X_AXIS_DATE_FORMATTER.format(date)
|
||||
}
|
||||
|
||||
export function TrafficOverview({
|
||||
...props
|
||||
}: React.ComponentProps<typeof Card>) {
|
||||
return (
|
||||
<Card {...props}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Traffic Overview</CardTitle>
|
||||
<CardDescription>
|
||||
Traffic for the last 30 days has increased by 12.4% compared to the
|
||||
previous period.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={TRAFFIC_CHART_CONFIG} className="h-82 w-full">
|
||||
<LineChart data={TRAFFIC_OVERVIEW_DATA}>
|
||||
<CartesianGrid
|
||||
vertical={false}
|
||||
strokeDasharray="3 6"
|
||||
stroke="var(--border)"
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
interval="preserveStartEnd"
|
||||
tickMargin={10}
|
||||
tickFormatter={formatXAxisTick}
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
width={44}
|
||||
domain={[0, 10000]}
|
||||
ticks={[0, 2500, 5000, 7500, 10000]}
|
||||
tickFormatter={formatYAxisTick}
|
||||
hide
|
||||
/>
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Line
|
||||
type="linear"
|
||||
dataKey="views"
|
||||
stroke="var(--color-views)"
|
||||
strokeWidth={2.2}
|
||||
dot={false}
|
||||
activeDot={{ r: 3.5, fill: "var(--color-views)" }}
|
||||
/>
|
||||
<Line
|
||||
type="linear"
|
||||
dataKey="unique"
|
||||
stroke="var(--color-unique)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray="4 6"
|
||||
dot={false}
|
||||
activeDot={false}
|
||||
/>
|
||||
<ReferenceDot
|
||||
x="2025-10-18"
|
||||
y={9240}
|
||||
r={2.5}
|
||||
fill="var(--color-views)"
|
||||
stroke="var(--color-views)"
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
22
apps/v4/app/(app)/(styles)/sera/audience-analytics/index.tsx
Normal file
22
apps/v4/app/(app)/(styles)/sera/audience-analytics/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Separator } from "@/styles/base-sera/ui/separator"
|
||||
|
||||
import { Demographics } from "./components/demographics"
|
||||
import { MetricsGrid } from "./components/metrics-grid"
|
||||
import { PreviewHeader } from "./components/preview-header"
|
||||
import { TopEditorial } from "./components/top-editorial"
|
||||
import { TrafficOverviewDeferred } from "./components/traffic-overview-deferred"
|
||||
|
||||
export function AudienceAnalytics() {
|
||||
return (
|
||||
<div className="preview theme-taupe @container/preview w-full flex-1 bg-muted pt-4 font-sans ring-1 ring-foreground/5 [--gap:--spacing(4)] sm:pt-0 md:[--gap:--spacing(6)] xl:[--gap:--spacing(8)] 2xl:py-8 **:[.container]:px-(--gap)">
|
||||
<PreviewHeader />
|
||||
<Separator className="hidden sm:block" />
|
||||
<div className="container grid grid-cols-12 gap-(--gap) py-(--gap)">
|
||||
<MetricsGrid />
|
||||
<TrafficOverviewDeferred className="col-span-full md:col-span-6 lg:col-span-8" />
|
||||
<Demographics className="col-span-full md:col-span-6 lg:col-span-4" />
|
||||
<TopEditorial className="col-span-full" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
46
apps/v4/app/(app)/(styles)/sera/components/image-preview.tsx
Normal file
46
apps/v4/app/(app)/(styles)/sera/components/image-preview.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import Image from "next/image"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export function ImagePreview() {
|
||||
return (
|
||||
<div className="mt-8 flex flex-col overflow-hidden md:hidden">
|
||||
<ImagePreviewItem name="sera-01" />
|
||||
<ImagePreviewItem name="sera-03" />
|
||||
<ImagePreviewItem name="sera-02" />
|
||||
<ImagePreviewItem name="sera-06" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ImagePreviewItem({
|
||||
name,
|
||||
className,
|
||||
}: {
|
||||
name: string
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"theme-taupe overflow-hidden bg-muted px-4 py-2 first:pt-4 last:pb-4",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
src={`/images/${name}-light.png`}
|
||||
alt={name}
|
||||
width={1440}
|
||||
height={900}
|
||||
className="dark:hidden"
|
||||
/>
|
||||
<Image
|
||||
src={`/images/${name}-dark.png`}
|
||||
alt={name}
|
||||
width={1440}
|
||||
height={900}
|
||||
className="hidden dark:block"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
148
apps/v4/app/(app)/(styles)/sera/components/lazy-preview.tsx
Normal file
148
apps/v4/app/(app)/(styles)/sera/components/lazy-preview.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import dynamic from "next/dynamic"
|
||||
|
||||
type LazyPreviewName =
|
||||
| "articleDirectory"
|
||||
| "emptyState"
|
||||
| "editArticle"
|
||||
| "mediaLibrary"
|
||||
| "mediaLibraryTable"
|
||||
|
||||
const PREVIEW_MIN_HEIGHTS: Record<LazyPreviewName, number> = {
|
||||
articleDirectory: 760,
|
||||
emptyState: 560,
|
||||
editArticle: 980,
|
||||
mediaLibrary: 880,
|
||||
mediaLibraryTable: 980,
|
||||
}
|
||||
|
||||
const ArticleDirectoryPreview = dynamic(
|
||||
() => import("../article-directory").then((mod) => mod.ArticleDirectory),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<PreviewPlaceholder minHeight={PREVIEW_MIN_HEIGHTS.articleDirectory} />
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
const EmptyStatePreview = dynamic(
|
||||
() => import("../empty-state").then((mod) => mod.EmptyState),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<PreviewPlaceholder minHeight={PREVIEW_MIN_HEIGHTS.emptyState} />
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
const EditArticlePreview = dynamic(
|
||||
() => import("../edit-article").then((mod) => mod.EditArticle),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<PreviewPlaceholder minHeight={PREVIEW_MIN_HEIGHTS.editArticle} />
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
const MediaLibraryPreview = dynamic(
|
||||
() => import("../media-library").then((mod) => mod.MediaLibrary),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<PreviewPlaceholder minHeight={PREVIEW_MIN_HEIGHTS.mediaLibrary} />
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
const MediaLibraryTablePreview = dynamic(
|
||||
() => import("../media-library-table").then((mod) => mod.MediaLibraryTable),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<PreviewPlaceholder minHeight={PREVIEW_MIN_HEIGHTS.mediaLibraryTable} />
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
const PREVIEW_COMPONENTS: Record<LazyPreviewName, React.ComponentType> = {
|
||||
articleDirectory: ArticleDirectoryPreview,
|
||||
emptyState: EmptyStatePreview,
|
||||
editArticle: EditArticlePreview,
|
||||
mediaLibrary: MediaLibraryPreview,
|
||||
mediaLibraryTable: MediaLibraryTablePreview,
|
||||
}
|
||||
|
||||
export function LazyPreview({ name }: { name: LazyPreviewName }) {
|
||||
const containerRef = React.useRef<HTMLDivElement>(null)
|
||||
const [shouldRender, setShouldRender] = React.useState(false)
|
||||
const PreviewComponent = PREVIEW_COMPONENTS[name]
|
||||
|
||||
React.useEffect(() => {
|
||||
if (shouldRender) {
|
||||
return
|
||||
}
|
||||
|
||||
const container = containerRef.current
|
||||
|
||||
if (!container || !("IntersectionObserver" in window)) {
|
||||
setShouldRender(true)
|
||||
return
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (!entries.some((entry) => entry.isIntersecting)) {
|
||||
return
|
||||
}
|
||||
|
||||
setShouldRender(true)
|
||||
observer.disconnect()
|
||||
},
|
||||
{
|
||||
rootMargin: "800px 0px",
|
||||
}
|
||||
)
|
||||
|
||||
observer.observe(container)
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [shouldRender])
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
{shouldRender ? (
|
||||
<PreviewComponent />
|
||||
) : (
|
||||
<PreviewPlaceholder minHeight={PREVIEW_MIN_HEIGHTS[name]} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PreviewPlaceholder({ minHeight }: { minHeight: number }) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="preview theme-taupe @container/preview w-full flex-1 bg-muted p-4 font-sans ring-1 ring-foreground/5 [--gap:--spacing(4)] sm:p-6 md:[--gap:--spacing(6)] xl:[--gap:--spacing(8)]"
|
||||
style={{ minHeight }}
|
||||
>
|
||||
<div className="container flex flex-col gap-(--gap) py-(--gap)">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="h-5 w-44 bg-background/80" />
|
||||
<div className="h-3 w-56 max-w-full bg-background/60" />
|
||||
</div>
|
||||
<div className="hidden h-8 w-28 bg-background/70 sm:block" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-(--gap) md:grid-cols-3">
|
||||
<div className="min-h-48 bg-background/70 md:col-span-2" />
|
||||
<div className="min-h-48 bg-background/70" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const THEME_OPTIONS = [
|
||||
{ label: "Taupe", value: "theme-taupe" },
|
||||
{ label: "Neutral", value: "theme-neutral" },
|
||||
{ label: "Stone", value: "theme-stone" },
|
||||
{ label: "Zinc", value: "theme-zinc" },
|
||||
{ label: "Mauve", value: "theme-mauve" },
|
||||
{ label: "Olive", value: "theme-olive" },
|
||||
{ label: "Mist", value: "theme-mist" },
|
||||
] as const
|
||||
|
||||
const DEFAULT_THEME = "theme-taupe"
|
||||
|
||||
function applyThemeToPreviews(theme: string) {
|
||||
const previewElements = document.querySelectorAll<HTMLElement>(".preview")
|
||||
|
||||
previewElements.forEach((element) => {
|
||||
THEME_OPTIONS.forEach((option) => {
|
||||
element.classList.remove(option.value)
|
||||
})
|
||||
|
||||
element.classList.add(theme)
|
||||
})
|
||||
}
|
||||
|
||||
export function ThemeSwitcher() {
|
||||
const [theme, setTheme] = React.useState<string>(DEFAULT_THEME)
|
||||
|
||||
React.useEffect(() => {
|
||||
applyThemeToPreviews(theme)
|
||||
}, [theme])
|
||||
|
||||
return (
|
||||
<div className="fixed inset-x-0 bottom-8 z-50 flex justify-center px-4">
|
||||
<div className="w-full max-w-[60vw] rounded-full border-0 bg-neutral-950/50 p-1.5 shadow-xl backdrop-blur-xl sm:max-w-fit">
|
||||
<div className="no-scrollbar flex snap-x snap-mandatory items-center overflow-x-auto">
|
||||
{THEME_OPTIONS.map((option) => (
|
||||
<button
|
||||
data-active={theme === option.value}
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setTheme(option.value)
|
||||
}}
|
||||
className="shrink-0 snap-center rounded-full px-3 py-1.5 text-sm font-medium text-neutral-300 outline-hidden transition-colors select-none hover:text-neutral-100 data-active:bg-neutral-500 data-active:text-neutral-100"
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
AlignCenterIcon,
|
||||
AlignLeftIcon,
|
||||
AlignRightIcon,
|
||||
BoldIcon,
|
||||
ChevronDownIcon,
|
||||
Code2Icon,
|
||||
Heading1Icon,
|
||||
Heading2Icon,
|
||||
Heading3Icon,
|
||||
ImageIcon,
|
||||
ItalicIcon,
|
||||
LinkIcon,
|
||||
ListIcon,
|
||||
ListOrderedIcon,
|
||||
RedoIcon,
|
||||
StrikethroughIcon,
|
||||
TypeIcon,
|
||||
UnderlineIcon,
|
||||
UndoIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import {
|
||||
ButtonGroup,
|
||||
ButtonGroupSeparator,
|
||||
} from "@/styles/base-sera/ui/button-group"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
import { Checkbox } from "@/styles/base-sera/ui/checkbox"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/base-sera/ui/dropdown-menu"
|
||||
import {
|
||||
Field,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
} from "@/styles/base-sera/ui/field"
|
||||
import { Input } from "@/styles/base-sera/ui/input"
|
||||
import {
|
||||
Progress,
|
||||
ProgressLabel,
|
||||
ProgressValue,
|
||||
} from "@/styles/base-sera/ui/progress"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/styles/base-sera/ui/select"
|
||||
import { Textarea } from "@/styles/base-sera/ui/textarea"
|
||||
|
||||
type Milestone = {
|
||||
name: string
|
||||
complete: boolean
|
||||
note?: string
|
||||
}
|
||||
|
||||
const MILESTONES: Milestone[] = [
|
||||
{
|
||||
name: "Outline & Commissioning",
|
||||
complete: true,
|
||||
},
|
||||
{
|
||||
name: "First Draft Submitted",
|
||||
complete: true,
|
||||
},
|
||||
{
|
||||
name: "Review & Revisions",
|
||||
complete: false,
|
||||
note: "Waiting on editor",
|
||||
},
|
||||
{
|
||||
name: "Final Copy Edit",
|
||||
complete: false,
|
||||
},
|
||||
{
|
||||
name: "Art Direction & Layout",
|
||||
complete: false,
|
||||
},
|
||||
]
|
||||
|
||||
const ISSUES = [
|
||||
{ label: "Spring Issue 2024", value: "spring-2024" },
|
||||
{ label: "Summer Issue 2024", value: "summer-2024" },
|
||||
{ label: "Autumn Issue 2024", value: "autumn-2024" },
|
||||
{ label: "Winter Issue 2024", value: "winter-2024" },
|
||||
]
|
||||
|
||||
export function EditorWorkspace() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 items-start gap-6 xl:grid-cols-[minmax(0,1fr)_300px]">
|
||||
<section className="flex flex-col border border-border/70 bg-background">
|
||||
<div className="flex border-b p-2">
|
||||
<ButtonGroup>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button variant="ghost" size="sm">
|
||||
Normal Text
|
||||
<ChevronDownIcon data-icon="inline-end" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<TypeIcon />
|
||||
Normal Text
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Heading1Icon />
|
||||
Heading 1
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Heading2Icon />
|
||||
Heading 2
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Heading3Icon />
|
||||
Heading 3
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<ListIcon />
|
||||
Bullet List
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<ListOrderedIcon />
|
||||
Numbered List
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ButtonGroupSeparator className="mx-2 data-vertical:h-4 data-vertical:self-center" />
|
||||
<Button variant="ghost" size="icon-sm" aria-label="Bold">
|
||||
<BoldIcon />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon-sm" aria-label="Italic">
|
||||
<ItalicIcon />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon-sm" aria-label="Underline">
|
||||
<UnderlineIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="Strikethrough"
|
||||
className="hidden md:flex"
|
||||
>
|
||||
<StrikethroughIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="Code"
|
||||
className="hidden md:flex"
|
||||
>
|
||||
<Code2Icon />
|
||||
</Button>
|
||||
<ButtonGroupSeparator className="mx-2 hidden md:flex data-vertical:h-4 data-vertical:self-center" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="Align Left"
|
||||
className="hidden md:flex"
|
||||
>
|
||||
<AlignLeftIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="Align Center"
|
||||
className="hidden md:flex"
|
||||
>
|
||||
<AlignCenterIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="Align Right"
|
||||
className="hidden md:flex"
|
||||
>
|
||||
<AlignRightIcon />
|
||||
</Button>
|
||||
<ButtonGroupSeparator className="mx-2 hidden md:flex data-vertical:h-4 data-vertical:self-center" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="Link"
|
||||
className="hidden md:flex"
|
||||
>
|
||||
<LinkIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="Image"
|
||||
className="hidden md:flex"
|
||||
>
|
||||
<ImageIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="ml-auto">
|
||||
<Button variant="ghost" size="icon-sm" aria-label="Undo">
|
||||
<UndoIcon />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon-sm" aria-label="Redo">
|
||||
<RedoIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div className="mx-auto flex max-w-2xl flex-1 flex-col gap-8 px-10 py-10 leading-loose md:px-14 lg:py-18">
|
||||
<h1 className="font-heading text-4xl leading-12 font-medium tracking-wide uppercase">
|
||||
The Future of Sustainable Architecture
|
||||
</h1>
|
||||
<p>
|
||||
As cities continue to expand at an unprecedented rate, the
|
||||
architectural paradigm is shifting from mere expansion to
|
||||
sustainable integration. The concrete jungles of the 20th century
|
||||
are making way for structures that breathe, adapt, and give back to
|
||||
their environments.
|
||||
</p>
|
||||
<p>
|
||||
Historically, urban development has been a zero-sum game with
|
||||
nature.
|
||||
</p>
|
||||
<h2 className="font-heading text-2xl tracking-wide uppercase">
|
||||
The Living Building Challenge
|
||||
</h2>
|
||||
<p>
|
||||
Sterling's latest project in downtown Seattle is a testament to
|
||||
this new philosophy. "We are no longer designing static
|
||||
structures," Sterling explained during a recent site visit.
|
||||
"We are engineering localized ecosystems."
|
||||
</p>
|
||||
<p>
|
||||
The building features a facade of responsive biomaterials that
|
||||
adjust their porosity based on humidity and temperature,
|
||||
significantly reducing the need for artificial climate control.
|
||||
Rainwater is not merely channeled away but captured, filtered
|
||||
through a series of integrated rooftop wetlands, and reused within
|
||||
the building's greywater system.
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This shift requires more than just innovative materials; it demands
|
||||
a fundamental change in how we value space. Check with engineering
|
||||
team for specific stats.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<aside className="grid grid-cols-12 gap-(--gap) xl:flex xl:flex-col">
|
||||
<Card className="col-span-full md:col-span-6 lg:col-span-4">
|
||||
<CardHeader>
|
||||
<CardTitle>Article Details</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel>Issue</FieldLabel>
|
||||
<Select items={ISSUES} defaultValue="summer-2024">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{ISSUES.map((issue) => (
|
||||
<SelectItem key={issue.value} value={issue.value}>
|
||||
{issue.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Author</FieldLabel>
|
||||
<Input defaultValue="Elena Rostova" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="col-span-full md:col-span-6 lg:col-span-4">
|
||||
<CardHeader>
|
||||
<CardTitle>Publication Flow</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend>Required Milestones</FieldLegend>
|
||||
<Field>
|
||||
{MILESTONES.map((milestone) => (
|
||||
<Field key={milestone.name} orientation="horizontal">
|
||||
<Checkbox
|
||||
defaultChecked={milestone.complete}
|
||||
name={milestone.name}
|
||||
id={milestone.name}
|
||||
/>
|
||||
<FieldLabel htmlFor={milestone.name}>
|
||||
{milestone.name}
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
))}
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<Field>
|
||||
<FieldLabel>Add note for editor</FieldLabel>
|
||||
<Textarea placeholder="This article needs to be revised for clarity and accuracy." />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="col-span-full lg:col-span-4">
|
||||
<CardHeader>
|
||||
<CardTitle>Word Count</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Progress value={70}>
|
||||
<ProgressLabel>1,402 / 2,000 words</ProgressLabel>
|
||||
<ProgressValue />
|
||||
</Progress>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</aside>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { ArrowLeftIcon, ExternalLinkIcon } from "lucide-react"
|
||||
|
||||
import { Badge } from "@/styles/base-sera/ui/badge"
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
} from "@/styles/base-sera/ui/breadcrumb"
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import { ButtonGroup } from "@/styles/base-sera/ui/button-group"
|
||||
|
||||
export function PreviewHeader() {
|
||||
return (
|
||||
<header>
|
||||
<div className="container flex flex-col items-center justify-center gap-(--gap) py-(--gap) sm:flex-row sm:justify-between">
|
||||
<div className="flex flex-col gap-2 text-center sm:text-left">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#" className="flex items-center gap-1.5">
|
||||
<ArrowLeftIcon className="size-3.5" />
|
||||
Back to articles
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<h1 className="line-clamp-1 font-heading text-3xl tracking-wide uppercase md:text-3xl lg:text-4xl">
|
||||
EDIT ARTICLE
|
||||
</h1>
|
||||
</div>
|
||||
<ButtonGroup className="gap-2 md:gap-4">
|
||||
<Badge title="2 minutes ago">Autosaved</Badge>
|
||||
<ButtonGroup className="gap-2 md:gap-4">
|
||||
<Button variant="link">
|
||||
Preview
|
||||
<ExternalLinkIcon data-icon="inline-end" />
|
||||
</Button>
|
||||
<Button>Submit Draft</Button>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
16
apps/v4/app/(app)/(styles)/sera/edit-article/index.tsx
Normal file
16
apps/v4/app/(app)/(styles)/sera/edit-article/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Separator } from "@/styles/base-sera/ui/separator"
|
||||
|
||||
import { EditorWorkspace } from "./components/editor-workspace"
|
||||
import { PreviewHeader } from "./components/preview-header"
|
||||
|
||||
export function EditArticle() {
|
||||
return (
|
||||
<div className="preview theme-taupe @container/preview w-full flex-1 bg-muted pt-4 font-sans ring-1 ring-foreground/5 [--gap:--spacing(4)] sm:pt-0 md:[--gap:--spacing(6)] xl:[--gap:--spacing(8)] 2xl:py-8 **:[.container]:px-(--gap)">
|
||||
<PreviewHeader />
|
||||
<Separator className="hidden sm:block" />
|
||||
<div className="container py-(--gap)">
|
||||
<EditorWorkspace />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { FileTextIcon, PlusIcon } from "lucide-react"
|
||||
|
||||
import { Badge } from "@/styles/base-sera/ui/badge"
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import { Card, CardContent } from "@/styles/base-sera/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/styles/base-sera/ui/empty"
|
||||
import { Separator } from "@/styles/base-sera/ui/separator"
|
||||
|
||||
type Stage = {
|
||||
id: string
|
||||
label: string
|
||||
description: string
|
||||
dotClassName: string
|
||||
}
|
||||
|
||||
const STAGES: Stage[] = [
|
||||
{
|
||||
id: "drafting",
|
||||
label: "Drafting",
|
||||
description:
|
||||
"Start the writing process. Articles here are works in progress, visible only to editors and authors.",
|
||||
dotClassName: "bg-amber-600",
|
||||
},
|
||||
{
|
||||
id: "in-revision",
|
||||
label: "In Revision",
|
||||
description:
|
||||
"Content undergoing editorial review. Track changes and word counts as pieces take shape.",
|
||||
dotClassName: "bg-orange-700",
|
||||
},
|
||||
{
|
||||
id: "final-edit",
|
||||
label: "Final Edit",
|
||||
description:
|
||||
"The final polish before publication. Ensure all styling and factual checks are complete.",
|
||||
dotClassName: "bg-foreground",
|
||||
},
|
||||
]
|
||||
|
||||
export function EmptyDirectory() {
|
||||
return (
|
||||
<Card className="py-24">
|
||||
<CardContent className="flex flex-col items-center gap-10">
|
||||
<Empty className="min-h-96">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia
|
||||
variant="icon"
|
||||
className="size-14 rounded-full bg-muted/70 text-muted-foreground"
|
||||
>
|
||||
<FileTextIcon className="size-5" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle className="font-heading text-2xl tracking-normal normal-case">
|
||||
A Blank Canvas
|
||||
</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Your editorial directory is currently empty. Start building your
|
||||
publication's next issue by drafting the first piece.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>
|
||||
<PlusIcon data-icon="inline-start" />
|
||||
Create first article
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<Separator className="max-w-2xl" />
|
||||
<div className="grid w-full max-w-2xl grid-cols-1 gap-8 sm:grid-cols-3">
|
||||
{STAGES.map((stage) => (
|
||||
<div key={stage.id} className="flex flex-col gap-2">
|
||||
<Badge>
|
||||
<span
|
||||
aria-hidden
|
||||
className={`size-1.5 rounded-full ${stage.dotClassName}`}
|
||||
/>
|
||||
|
||||
{stage.label}
|
||||
</Badge>
|
||||
<p className="text-xs leading-relaxed text-muted-foreground">
|
||||
{stage.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ArrowLeftIcon, PlusIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
} from "@/styles/base-sera/ui/breadcrumb"
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
|
||||
export function PreviewHeader() {
|
||||
return (
|
||||
<header>
|
||||
<div className="container flex flex-col items-center justify-center gap-(--gap) py-(--gap) sm:flex-row sm:justify-between">
|
||||
<div className="flex flex-col gap-2 text-center sm:text-left">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#" className="flex items-center gap-1.5">
|
||||
<ArrowLeftIcon className="size-3.5" />
|
||||
Editorial Dashboard
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<h1 className="line-clamp-1 font-heading text-3xl tracking-wide uppercase md:text-3xl lg:text-4xl">
|
||||
Article Directory
|
||||
</h1>
|
||||
</div>
|
||||
<Button className="sm:ml-auto">
|
||||
<PlusIcon data-icon="inline-start" />
|
||||
New Article
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
16
apps/v4/app/(app)/(styles)/sera/empty-state/index.tsx
Normal file
16
apps/v4/app/(app)/(styles)/sera/empty-state/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Separator } from "@/styles/base-sera/ui/separator"
|
||||
|
||||
import { EmptyDirectory } from "./components/empty-directory"
|
||||
import { PreviewHeader } from "./components/preview-header"
|
||||
|
||||
export function EmptyState() {
|
||||
return (
|
||||
<div className="preview theme-taupe @container/preview w-full flex-1 bg-muted pt-4 font-sans ring-1 ring-foreground/5 [--gap:--spacing(4)] sm:pt-0 md:[--gap:--spacing(6)] xl:[--gap:--spacing(8)] 2xl:py-8 **:[.container]:px-(--gap)">
|
||||
<PreviewHeader />
|
||||
<Separator className="hidden sm:block" />
|
||||
<div className="container py-(--gap)">
|
||||
<EmptyDirectory />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
FileTextIcon,
|
||||
ImageIcon,
|
||||
MoreVerticalIcon,
|
||||
SearchIcon,
|
||||
VideoIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Badge } from "@/styles/base-sera/ui/badge"
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
import { Checkbox } from "@/styles/base-sera/ui/checkbox"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/base-sera/ui/dropdown-menu"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/styles/base-sera/ui/input-group"
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/styles/base-sera/ui/pagination"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/styles/base-sera/ui/table"
|
||||
|
||||
import { ASSETS, type AssetType } from "../../media-library/data"
|
||||
|
||||
function AssetTypeIcon({
|
||||
type,
|
||||
className,
|
||||
}: {
|
||||
type: AssetType
|
||||
className?: string
|
||||
}) {
|
||||
if (type === "MP4") {
|
||||
return <VideoIcon className={className} />
|
||||
}
|
||||
|
||||
if (type === "PDF") {
|
||||
return <FileTextIcon className={className} />
|
||||
}
|
||||
|
||||
return <ImageIcon className={className} />
|
||||
}
|
||||
|
||||
export function AssetTable() {
|
||||
const [selectedIds, setSelectedIds] = React.useState<Set<string>>(
|
||||
new Set(["1"])
|
||||
)
|
||||
|
||||
const toggleSelection = React.useCallback((id: string) => {
|
||||
setSelectedIds((previous) => {
|
||||
const next = new Set(previous)
|
||||
|
||||
if (next.has(id)) {
|
||||
next.delete(id)
|
||||
} else {
|
||||
next.add(id)
|
||||
}
|
||||
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<InputGroup className="w-full">
|
||||
<InputGroupAddon>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput placeholder="Search files, tags, or metadata..." />
|
||||
</InputGroup>
|
||||
</CardHeader>
|
||||
<CardContent className="px-0 py-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-10 pl-6" aria-label="Select" />
|
||||
<TableHead className="w-20" aria-label="Preview" />
|
||||
<TableHead>Filename</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Dimensions</TableHead>
|
||||
<TableHead>Size</TableHead>
|
||||
<TableHead>Uploaded By</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead className="w-10 pr-6" aria-label="Actions" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{ASSETS.map((asset) => {
|
||||
const isSelected = selectedIds.has(asset.id)
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={asset.id}
|
||||
data-state={isSelected ? "selected" : undefined}
|
||||
className="cursor-pointer"
|
||||
onClick={() => toggleSelection(asset.id)}
|
||||
>
|
||||
<TableCell className="pl-6">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
aria-label={`Select ${asset.name}`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onCheckedChange={() => toggleSelection(asset.id)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="relative flex aspect-4/3 w-16 items-center justify-center bg-muted/60 ring-1 ring-border/70 ring-inset">
|
||||
{asset.duration ? (
|
||||
<span className="absolute right-1 bottom-1 bg-foreground/90 px-1 text-[0.5rem] font-semibold tracking-wider text-background">
|
||||
{asset.duration}
|
||||
</span>
|
||||
) : null}
|
||||
<AssetTypeIcon
|
||||
type={asset.type}
|
||||
className="size-4 text-muted-foreground/60"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm font-medium text-foreground">
|
||||
{asset.name}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border px-2 py-0.5 text-[0.625rem]"
|
||||
>
|
||||
{asset.type}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{asset.dimensions}</TableCell>
|
||||
<TableCell className="text-sm">{asset.size}</TableCell>
|
||||
<TableCell>{asset.uploadedBy}</TableCell>
|
||||
<TableCell className="text-xs font-semibold tracking-wider text-muted-foreground uppercase">
|
||||
{asset.date}
|
||||
</TableCell>
|
||||
<TableCell className="pr-6 text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<Button variant="ghost" size="icon-xs" />}
|
||||
aria-label={`Open actions for ${asset.name}`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>Preview</DropdownMenuItem>
|
||||
<DropdownMenuItem>Download</DropdownMenuItem>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-center py-4">
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious href="#" text="" />
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink href="#" isActive>
|
||||
1
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink href="#">2</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink href="#">3</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext href="#" text="" />
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { addDays, format } from "date-fns"
|
||||
import { CalendarIcon, FilterIcon, XIcon } from "lucide-react"
|
||||
import { type DateRange } from "react-day-picker"
|
||||
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import { Calendar } from "@/styles/base-sera/ui/calendar"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
import { Checkbox } from "@/styles/base-sera/ui/checkbox"
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxChip,
|
||||
ComboboxChips,
|
||||
ComboboxChipsInput,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxItem,
|
||||
ComboboxList,
|
||||
ComboboxValue,
|
||||
useComboboxAnchor,
|
||||
} from "@/styles/base-sera/ui/combobox"
|
||||
import {
|
||||
Field,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
} from "@/styles/base-sera/ui/field"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/styles/base-sera/ui/popover"
|
||||
import { RadioGroup, RadioGroupItem } from "@/styles/base-sera/ui/radio-group"
|
||||
import { Slider } from "@/styles/base-sera/ui/slider"
|
||||
|
||||
const FILE_TYPES = [
|
||||
{
|
||||
id: "images",
|
||||
label: "Images (JPEG, PNG, WEBP)",
|
||||
defaultChecked: true,
|
||||
},
|
||||
{
|
||||
id: "video",
|
||||
label: "Video (MP4, MOV)",
|
||||
defaultChecked: false,
|
||||
},
|
||||
{
|
||||
id: "documents",
|
||||
label: "Documents (PDF)",
|
||||
defaultChecked: false,
|
||||
},
|
||||
{
|
||||
id: "audio",
|
||||
label: "Audio (MP3, WAV)",
|
||||
defaultChecked: false,
|
||||
},
|
||||
]
|
||||
|
||||
const DATE_OPTIONS = [
|
||||
{ value: "any", label: "Any time" },
|
||||
{ value: "24h", label: "Past 24 hours" },
|
||||
{ value: "week", label: "Past week" },
|
||||
{ value: "month", label: "Past month" },
|
||||
]
|
||||
|
||||
const TAGS = [
|
||||
"architecture",
|
||||
"brutalism",
|
||||
"ceramics",
|
||||
"design-week",
|
||||
"editorial",
|
||||
"exterior",
|
||||
"film",
|
||||
"food",
|
||||
"furniture",
|
||||
"interior",
|
||||
"kyoto",
|
||||
"minimalism",
|
||||
"print",
|
||||
"sustainability",
|
||||
"summer-issue",
|
||||
"video",
|
||||
] as const
|
||||
|
||||
export function FilterLibrary() {
|
||||
const tagAnchor = useComboboxAnchor()
|
||||
const [dateRange, setDateRange] = React.useState<DateRange | undefined>({
|
||||
from: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
|
||||
to: addDays(
|
||||
new Date(new Date().getFullYear(), new Date().getMonth(), 1),
|
||||
21
|
||||
),
|
||||
})
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle>Filter Library</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend>File Type</FieldLegend>
|
||||
<Field>
|
||||
{FILE_TYPES.map((type) => (
|
||||
<Field key={type.id} orientation="horizontal">
|
||||
<Checkbox id={type.id} defaultChecked={type.defaultChecked} />
|
||||
<FieldLabel htmlFor={type.id}>{type.label}</FieldLabel>
|
||||
</Field>
|
||||
))}
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<FieldSet>
|
||||
<FieldLegend>Date Uploaded</FieldLegend>
|
||||
<RadioGroup defaultValue="any">
|
||||
{DATE_OPTIONS.map((option) => (
|
||||
<Field key={option.value} orientation="horizontal">
|
||||
<RadioGroupItem
|
||||
value={option.value}
|
||||
id={`date-${option.value}`}
|
||||
/>
|
||||
<FieldLabel htmlFor={`date-${option.value}`}>
|
||||
{option.label}
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="custom-range">Custom Range</FieldLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="outline"
|
||||
id="custom-range"
|
||||
className="w-full"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<CalendarIcon data-icon="inline-start" />
|
||||
{dateRange?.from ? (
|
||||
dateRange.to ? (
|
||||
<>
|
||||
{format(dateRange.from, "LLL dd, y")} –{" "}
|
||||
{format(dateRange.to, "LLL dd, y")}
|
||||
</>
|
||||
) : (
|
||||
format(dateRange.from, "LLL dd, y")
|
||||
)
|
||||
) : (
|
||||
<span>Pick a date range</span>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="end">
|
||||
<Calendar
|
||||
mode="range"
|
||||
defaultMonth={dateRange?.from}
|
||||
selected={dateRange}
|
||||
onSelect={setDateRange}
|
||||
numberOfMonths={2}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Field>
|
||||
<FieldSet>
|
||||
<FieldLegend>File Size</FieldLegend>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
<span>0 MB</span>
|
||||
<span>500+ MB</span>
|
||||
</div>
|
||||
<Slider defaultValue={[0, 60]} max={100} step={1} />
|
||||
<div className="flex items-center justify-between text-xs font-medium">
|
||||
<span>Min: 0 MB</span>
|
||||
<span>Max: 300 MB</span>
|
||||
</div>
|
||||
</div>
|
||||
</FieldSet>
|
||||
<FieldSet>
|
||||
<FieldLegend>Tags</FieldLegend>
|
||||
<Field>
|
||||
<Combobox
|
||||
multiple
|
||||
autoHighlight
|
||||
items={TAGS}
|
||||
defaultValue={["architecture", "brutalism"]}
|
||||
>
|
||||
<ComboboxChips ref={tagAnchor}>
|
||||
<ComboboxValue>
|
||||
{(values) => (
|
||||
<React.Fragment>
|
||||
{values.map((value: string) => (
|
||||
<ComboboxChip key={value}>{value}</ComboboxChip>
|
||||
))}
|
||||
<ComboboxChipsInput placeholder="Filter by tag..." />
|
||||
</React.Fragment>
|
||||
)}
|
||||
</ComboboxValue>
|
||||
</ComboboxChips>
|
||||
<ComboboxContent anchor={tagAnchor}>
|
||||
<ComboboxEmpty>No tags found.</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(item) => (
|
||||
<ComboboxItem key={item} value={item}>
|
||||
{item}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col gap-2 border-t">
|
||||
<Button className="w-full">Apply Filters</Button>
|
||||
<Button variant="ghost" className="w-full">
|
||||
Reset
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ArrowLeftIcon, SlidersHorizontalIcon, UploadIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
} from "@/styles/base-sera/ui/breadcrumb"
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import { ButtonGroup } from "@/styles/base-sera/ui/button-group"
|
||||
|
||||
export function PreviewHeader() {
|
||||
return (
|
||||
<header>
|
||||
<div className="container flex flex-col items-center justify-center gap-(--gap) py-(--gap) sm:flex-row sm:justify-between">
|
||||
<div className="flex flex-col gap-2 text-center sm:text-left">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#" className="flex items-center gap-1.5">
|
||||
<ArrowLeftIcon className="size-3.5" />
|
||||
Asset management
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<h1 className="line-clamp-1 font-heading text-3xl tracking-wide uppercase md:text-3xl lg:text-4xl">
|
||||
Media Library
|
||||
</h1>
|
||||
</div>
|
||||
<ButtonGroup className="gap-2 sm:ml-auto md:gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-background hover:bg-background/80"
|
||||
>
|
||||
<SlidersHorizontalIcon data-icon="inline-start" />
|
||||
Filters
|
||||
</Button>
|
||||
<Button>
|
||||
<UploadIcon data-icon="inline-start" />
|
||||
Upload Assets
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Separator } from "@/styles/base-sera/ui/separator"
|
||||
|
||||
import { AssetTable } from "./components/asset-table"
|
||||
import { FilterLibrary } from "./components/filter-library"
|
||||
import { PreviewHeader } from "./components/preview-header"
|
||||
|
||||
export function MediaLibraryTable() {
|
||||
return (
|
||||
<div className="preview theme-taupe @container/preview w-full flex-1 bg-muted pt-4 font-sans ring-1 ring-foreground/5 [--gap:--spacing(4)] sm:pt-0 md:[--gap:--spacing(6)] xl:[--gap:--spacing(8)] 2xl:py-8 **:[.container]:px-(--gap)">
|
||||
<PreviewHeader />
|
||||
<Separator className="hidden sm:block" />
|
||||
<div className="container grid grid-cols-1 items-start gap-(--gap) py-(--gap) xl:grid-cols-[minmax(0,1fr)_320px]">
|
||||
<AssetTable />
|
||||
<FilterLibrary />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
DownloadIcon,
|
||||
FileTextIcon,
|
||||
ImageIcon,
|
||||
PlusIcon,
|
||||
VideoIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Badge } from "@/styles/base-sera/ui/badge"
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import { Card, CardContent, CardFooter } from "@/styles/base-sera/ui/card"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemTitle,
|
||||
} from "@/styles/base-sera/ui/item"
|
||||
import { Separator } from "@/styles/base-sera/ui/separator"
|
||||
|
||||
import { type Asset, type AssetType } from "../data"
|
||||
|
||||
const TYPE_LABEL: Record<AssetType, string> = {
|
||||
JPEG: "Image / JPEG",
|
||||
PNG: "Image / PNG",
|
||||
WEBP: "Image / WEBP",
|
||||
MP4: "Video / MP4",
|
||||
PDF: "Document / PDF",
|
||||
}
|
||||
|
||||
export function AssetDetails({ asset }: { asset: Asset }) {
|
||||
return (
|
||||
<Card className="gap-0">
|
||||
<CardContent className="flex flex-col gap-6">
|
||||
<div className="flex aspect-5/4 items-center justify-center bg-muted/60 text-muted-foreground/60 ring-1 ring-border/70 ring-inset">
|
||||
{asset.type === "MP4" ? (
|
||||
<VideoIcon className="size-8" />
|
||||
) : asset.type === "PDF" ? (
|
||||
<FileTextIcon className="size-8" />
|
||||
) : (
|
||||
<ImageIcon className="size-8" />
|
||||
)}
|
||||
</div>
|
||||
<h2 className="line-clamp-2 font-heading text-xl tracking-wide">
|
||||
{asset.name}
|
||||
</h2>
|
||||
<Separator />
|
||||
<dl className="flex flex-col gap-5 text-sm">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<dt className="text-[0.625rem] font-semibold tracking-widest text-muted-foreground uppercase">
|
||||
Asset Type
|
||||
</dt>
|
||||
<dd>{TYPE_LABEL[asset.type]}</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<dt className="text-[0.625rem] font-semibold tracking-widest text-muted-foreground uppercase">
|
||||
Dimensions
|
||||
</dt>
|
||||
<dd>{asset.dimensions}</dd>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<dt className="text-[0.625rem] font-semibold tracking-widest text-muted-foreground uppercase">
|
||||
File Size
|
||||
</dt>
|
||||
<dd>{asset.size}</dd>
|
||||
</div>
|
||||
</div>
|
||||
</dl>
|
||||
<Separator />
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-[0.625rem] font-semibold tracking-widest text-muted-foreground uppercase">
|
||||
Tags
|
||||
</h3>
|
||||
<Button variant="ghost" size="icon-xs" aria-label="Add tag">
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-2">
|
||||
{asset.tags.map((tag) => (
|
||||
<Badge key={tag}>{tag}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex flex-col gap-3">
|
||||
<h3 className="text-[0.625rem] font-semibold tracking-widest text-muted-foreground uppercase">
|
||||
Used In
|
||||
</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
{asset.usedIn.map((usage) => (
|
||||
<Item key={usage.title} variant="outline">
|
||||
<ItemContent>
|
||||
<ItemTitle>{usage.title}</ItemTitle>
|
||||
<ItemDescription>{usage.role}</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="mt-6 border-t pt-6">
|
||||
<Button className="w-full">
|
||||
<DownloadIcon data-icon="inline-start" />
|
||||
Download
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user