mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-15 20:01:35 +00:00
Compare commits
969 Commits
fix/cli-va
...
shadcn@4.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
9fbd3b1a72 | ||
|
|
c6dd35a092 | ||
|
|
470c6f42b0 | ||
|
|
e6956e45ac | ||
|
|
a2b9dedbb7 | ||
|
|
384129609f | ||
|
|
5be0811f01 | ||
|
|
1a10b4671a | ||
|
|
e7d36b7e21 | ||
|
|
290fac9115 | ||
|
|
0633333db4 | ||
|
|
630323ad47 | ||
|
|
51fc7f5457 | ||
|
|
44a9b3bd12 | ||
|
|
2b879a5ec8 | ||
|
|
381f2ef165 | ||
|
|
825ebca3f0 | ||
|
|
e0063070a6 | ||
|
|
013ae51d10 | ||
|
|
08e54510ed | ||
|
|
a95606cee9 | ||
|
|
c990476d99 | ||
|
|
44c8f02d06 | ||
|
|
a012542015 | ||
|
|
926df433a7 | ||
|
|
5c09e0d8fa | ||
|
|
dba86053f5 | ||
|
|
cd188b267d | ||
|
|
8a09fbaac9 | ||
|
|
9676c8f4ee | ||
|
|
9b5aeab889 | ||
|
|
28ebf1b88a | ||
|
|
f922e82f53 | ||
|
|
beec1e060e | ||
|
|
26a24d3d5c | ||
|
|
c3c7f03f04 | ||
|
|
4af29d6c20 | ||
|
|
b28f77f893 | ||
|
|
b8c7ae8088 | ||
|
|
d21c74fb3a | ||
|
|
d6548b4ae8 | ||
|
|
110a4ec10b | ||
|
|
851562f4f2 | ||
|
|
b7b839ebc2 | ||
|
|
8d9be074a3 | ||
|
|
a0c077da9e | ||
|
|
540cd031c3 | ||
|
|
4d9720449f | ||
|
|
f1e10f3da8 | ||
|
|
e2225d4a93 | ||
|
|
444f6889c8 | ||
|
|
03a7804c42 | ||
|
|
acc847bed3 | ||
|
|
abfa2ddb74 | ||
|
|
5e92c160dd | ||
|
|
d41e857ba3 | ||
|
|
99651191cc | ||
|
|
712285f60e | ||
|
|
aed95086e0 | ||
|
|
1990280d66 | ||
|
|
2bf55c9133 | ||
|
|
3192a3db55 | ||
|
|
afa2a7adf2 | ||
|
|
728d8af275 | ||
|
|
38de7fddc2 | ||
|
|
c719d24f3a | ||
|
|
4479965555 | ||
|
|
7ea124b25d | ||
|
|
f746368369 | ||
|
|
164b6ff6c1 | ||
|
|
7ae522e610 | ||
|
|
e1a0ec3061 | ||
|
|
f8222528eb | ||
|
|
759003c781 | ||
|
|
6d467d2e1d | ||
|
|
893cddd2dc | ||
|
|
1781186def | ||
|
|
89b9a76368 | ||
|
|
6529256e98 | ||
|
|
b142bd2fd5 | ||
|
|
0266253841 | ||
|
|
4a39de5c56 | ||
|
|
e5fda2c139 | ||
|
|
d53f7489ce | ||
|
|
dfe784b44a | ||
|
|
40b9de46e9 | ||
|
|
6d97ab0b9b | ||
|
|
d06e84a007 | ||
|
|
a29185c9cf | ||
|
|
84c801ac67 | ||
|
|
3dbe9e6a3e | ||
|
|
4fa8f9b4c2 | ||
|
|
24205601e1 | ||
|
|
267d45ac7a | ||
|
|
caadc3d7e8 | ||
|
|
a4ee54836e | ||
|
|
7b5c919eae | ||
|
|
f1cacdc051 | ||
|
|
8cb8fb66b3 | ||
|
|
ef01cd4315 | ||
|
|
6cb2a1fd65 | ||
|
|
ee88d296f4 | ||
|
|
598f17812d | ||
|
|
0ae734bdb2 | ||
|
|
18bd8f07cb | ||
|
|
5fc9ced0fd | ||
|
|
b5dff005f6 | ||
|
|
c5c08bb773 | ||
|
|
5998e59839 | ||
|
|
4b7e38ab42 | ||
|
|
e2ba2d241e | ||
|
|
13e2a6c598 | ||
|
|
47c47eaed2 | ||
|
|
25e88fe4e9 | ||
|
|
d3590ceff9 | ||
|
|
d04bc84a51 | ||
|
|
f68465e815 | ||
|
|
094edfcfe6 | ||
|
|
5a42652c41 | ||
|
|
3409681949 | ||
|
|
1c989f9155 | ||
|
|
0aea23013c | ||
|
|
bfce3031a3 | ||
|
|
cfb81c61de | ||
|
|
7860ab83d1 | ||
|
|
2acaf954d7 | ||
|
|
1e9e337923 | ||
|
|
66d2400784 | ||
|
|
682c98989d | ||
|
|
77d7b39ef7 | ||
|
|
5b3ba49aec | ||
|
|
54edfd228d | ||
|
|
fd3e5515f3 | ||
|
|
65ad910bca | ||
|
|
d4a1c89e8e | ||
|
|
78023693c6 | ||
|
|
0fc52a7f4d | ||
|
|
8fcfc563a9 | ||
|
|
f393c251fe | ||
|
|
f2583391ea | ||
|
|
c2fd847d65 | ||
|
|
f6f2dfa5b2 | ||
|
|
d07a7af82b | ||
|
|
b6d845f8a6 | ||
|
|
bd29630e4e | ||
|
|
93ad19e4da | ||
|
|
31f8af8409 | ||
|
|
9317a93152 |
@@ -9,5 +9,6 @@
|
|||||||
"WebFetch(domain:github.com)"
|
"WebFetch(domain:github.com)"
|
||||||
],
|
],
|
||||||
"deny": []
|
"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).
|
||||||
63
.github/ISSUE_TEMPLATE/registry_directory.yml
vendored
63
.github/ISSUE_TEMPLATE/registry_directory.yml
vendored
@@ -1,63 +0,0 @@
|
|||||||
name: Add registry to directory
|
|
||||||
description: Add your registry to the directory
|
|
||||||
title: "[Registry Directory]: "
|
|
||||||
labels: ["registry", "directory"]
|
|
||||||
assignees: []
|
|
||||||
body:
|
|
||||||
- type: input
|
|
||||||
id: name
|
|
||||||
attributes:
|
|
||||||
label: Name
|
|
||||||
description: The name of your registry. This is also the namespace.
|
|
||||||
placeholder: e.g., "@acme"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: url
|
|
||||||
attributes:
|
|
||||||
label: URL
|
|
||||||
description: The URL to your registry index. Use {name} placeholder.
|
|
||||||
placeholder: https://ui.acme.com/r/{name}.json
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: homepage
|
|
||||||
attributes:
|
|
||||||
label: Homepage
|
|
||||||
description: The URL to your registry homepage. This is where users can browse your registry.
|
|
||||||
placeholder: https://ui.acme.com
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: description
|
|
||||||
attributes:
|
|
||||||
label: Description
|
|
||||||
description: Briefly describe what is your registry and what type of components or code it distributes.
|
|
||||||
placeholder:
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: logo
|
|
||||||
attributes:
|
|
||||||
label: Logo
|
|
||||||
description: Add your SVG logo here.
|
|
||||||
placeholder:
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: requirements
|
|
||||||
attributes:
|
|
||||||
label: Checklist
|
|
||||||
description: Verify that your registry meets the following requirements.
|
|
||||||
options:
|
|
||||||
- label: The registry must be open source and publicly accessible.
|
|
||||||
- label: The registry must be a valid JSON file that conforms to the [registry schema](https://ui.shadcn.com/docs/registry/registry-json) specification.
|
|
||||||
- label: The `files` array, if present on your registry items, must NOT include a `content` property.
|
|
||||||
- label: I've attached a square SVG logo to this issue
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
10
.github/changeset-version.js
vendored
10
.github/changeset-version.js
vendored
@@ -1,12 +1,12 @@
|
|||||||
// ORIGINALLY FROM CLOUDFLARE WRANGLER:
|
// ORIGINALLY FROM CLOUDFLARE WRANGLER:
|
||||||
// https://github.com/cloudflare/wrangler2/blob/main/.github/changeset-version.js
|
// https://github.com/cloudflare/wrangler2/blob/main/.github/changeset-version.js
|
||||||
|
|
||||||
import { exec } from "child_process"
|
import { execSync } from "child_process"
|
||||||
|
|
||||||
// This script is used by the `release.yml` workflow to update the version of the packages being released.
|
// This script is used by the `release.yml` workflow to update the version of the packages being released.
|
||||||
// The standard step is only to run `changeset version` but this does not update the package-lock.json file.
|
// The standard step is only to run `changeset version` but this does not update the pnpm-lock.yaml file.
|
||||||
// So we also run `npm install`, which does this update.
|
// So we also run `pnpm install`, which does this update.
|
||||||
// This is a workaround until this is handled automatically by `changeset version`.
|
// This is a workaround until this is handled automatically by `changeset version`.
|
||||||
// See https://github.com/changesets/changesets/issues/421.
|
// See https://github.com/changesets/changesets/issues/421.
|
||||||
exec("npx changeset version")
|
execSync("npx changeset version", { stdio: "inherit" })
|
||||||
exec("npm install")
|
execSync("pnpm install --lockfile-only", { stdio: "inherit" })
|
||||||
|
|||||||
40
.github/dependabot.yml
vendored
40
.github/dependabot.yml
vendored
@@ -4,3 +4,43 @@ updates:
|
|||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
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
|
name: Install pnpm
|
||||||
id: pnpm-install
|
id: pnpm-install
|
||||||
with:
|
with:
|
||||||
version: 9.0.6
|
version: 10.33.4
|
||||||
run_install: false
|
run_install: false
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
- name: Get pnpm store directory
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
id: pnpm-install
|
id: pnpm-install
|
||||||
with:
|
with:
|
||||||
version: 9.0.6
|
version: 10.33.4
|
||||||
run_install: false
|
run_install: false
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
- name: Get pnpm store directory
|
||||||
@@ -77,6 +77,9 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Build packages
|
||||||
|
run: pnpm --filter=shadcn build
|
||||||
|
|
||||||
- run: pnpm format:check
|
- run: pnpm format:check
|
||||||
|
|
||||||
tsc:
|
tsc:
|
||||||
@@ -96,7 +99,7 @@ jobs:
|
|||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
id: pnpm-install
|
id: pnpm-install
|
||||||
with:
|
with:
|
||||||
version: 9.0.6
|
version: 10.33.4
|
||||||
run_install: false
|
run_install: false
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
- 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.
|
# Adapted from create-t3-app.
|
||||||
name: Write Beta Release comment
|
name: Write Prerelease comment
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
workflow_run:
|
||||||
workflows: ["Release - Beta"]
|
workflows: ["Release"]
|
||||||
types:
|
types:
|
||||||
- completed
|
- completed
|
||||||
|
|
||||||
@@ -11,12 +11,13 @@ jobs:
|
|||||||
comment:
|
comment:
|
||||||
if: |
|
if: |
|
||||||
github.repository_owner == 'shadcn-ui' &&
|
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
|
runs-on: ubuntu-latest
|
||||||
name: Write comment to the PR
|
name: Write comment to the PR
|
||||||
steps:
|
steps:
|
||||||
- name: "Comment on PR"
|
- name: "Comment on PR"
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
@@ -31,9 +32,13 @@ jobs:
|
|||||||
const match = /^npm-package-shadcn@(.*?)-pr-(\d+)/.exec(artifact.name);
|
const match = /^npm-package-shadcn@(.*?)-pr-(\d+)/.exec(artifact.name);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
|
const version = match[1];
|
||||||
|
const channel = version.includes("-rc.") ? "rc" : "beta";
|
||||||
require("fs").appendFileSync(
|
require("fs").appendFileSync(
|
||||||
process.env.GITHUB_ENV,
|
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_PR=${match[2]}` +
|
||||||
`\nWORKFLOW_RUN_ID=${context.payload.workflow_run.id}`
|
`\nWORKFLOW_RUN_ID=${context.payload.workflow_run.id}`
|
||||||
);
|
);
|
||||||
@@ -46,20 +51,30 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
number: ${{ env.WORKFLOW_RUN_PR }}
|
number: ${{ env.WORKFLOW_RUN_PR }}
|
||||||
message: |
|
message: |
|
||||||
A new prerelease is available for testing:
|
A new ${{ env.PRERELEASE_CHANNEL }} prerelease is available for testing:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pnpm dlx shadcn@${{ env.BETA_PACKAGE_VERSION }}
|
pnpm dlx shadcn@${{ env.PRERELEASE_PACKAGE_VERSION }}
|
||||||
```
|
```
|
||||||
|
|
||||||
- name: "Remove the autorelease label once published"
|
View on npm: https://www.npmjs.com/package/shadcn/v/${{ env.PRERELEASE_PACKAGE_VERSION }}
|
||||||
uses: actions/github-script@v6
|
|
||||||
|
- name: "Remove the prerelease label once published"
|
||||||
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
github.rest.issues.removeLabel({
|
try {
|
||||||
owner: context.repo.owner,
|
await github.rest.issues.removeLabel({
|
||||||
repo: context.repo.repo,
|
owner: context.repo.owner,
|
||||||
issue_number: '${{ env.WORKFLOW_RUN_PR }}',
|
repo: context.repo.repo,
|
||||||
name: '🚀 autorelease',
|
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
|
name: Release
|
||||||
|
|
||||||
|
run-name: ${{ github.event_name == 'pull_request' && format('Release Prerelease - PR {0}', github.event.number) || 'Release Stable' }}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [labeled]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
prerelease:
|
||||||
if: ${{ github.repository_owner == 'shadcn-ui' }}
|
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: Create a PR for release workflow
|
name: Publish Prerelease to NPM
|
||||||
runs-on: ubuntu-latest
|
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:
|
steps:
|
||||||
- name: Checkout Repo
|
- name: Checkout Repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Use PNPM
|
- name: Use PNPM
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 9.0.6
|
version: 10.33.4
|
||||||
|
|
||||||
- name: Use Node.js 20
|
- name: Use Node.js 20
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -47,10 +157,19 @@ jobs:
|
|||||||
- name: Build the package
|
- name: Build the package
|
||||||
run: pnpm shadcn:build
|
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
|
- name: Create Version PR or Publish to NPM
|
||||||
id: changesets
|
id: changesets
|
||||||
uses: changesets/action@v1
|
uses: changesets/action@v1
|
||||||
with:
|
with:
|
||||||
|
setupGitUser: false
|
||||||
commit: "chore(release): version packages"
|
commit: "chore(release): version packages"
|
||||||
title: "chore(release): version packages"
|
title: "chore(release): version packages"
|
||||||
version: node .github/changeset-version.js
|
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
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
id: pnpm-install
|
id: pnpm-install
|
||||||
with:
|
with:
|
||||||
version: 9.0.6
|
version: 10.33.4
|
||||||
run_install: false
|
run_install: false
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
- name: Get pnpm store directory
|
||||||
@@ -39,10 +39,10 @@ jobs:
|
|||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pnpm-store-
|
${{ runner.os }}-pnpm-store-
|
||||||
|
- name: Install Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
||||||
- name: Build packages
|
|
||||||
run: pnpm build --filter=shadcn
|
|
||||||
|
|
||||||
- run: pnpm test
|
- 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:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- "apps/v4/public/r/registries.json"
|
- "apps/v4/registry/directory.json"
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- "apps/v4/public/r/registries.json"
|
- "apps/v4/registry/directory.json"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
validate:
|
validate:
|
||||||
@@ -26,11 +26,44 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 20
|
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
|
- uses: pnpm/action-setup@v4
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
id: pnpm-install
|
id: pnpm-install
|
||||||
with:
|
with:
|
||||||
version: 9.0.6
|
version: 10.33.4
|
||||||
run_install: false
|
run_install: false
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
- name: Get pnpm store directory
|
||||||
@@ -47,8 +80,5 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
||||||
- name: Build packages
|
|
||||||
run: pnpm build --filter=shadcn
|
|
||||||
|
|
||||||
- name: Validate registries
|
- name: Validate registries
|
||||||
run: pnpm --filter=v4 validate:registries
|
run: pnpm --filter=v4 validate:registries
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -15,6 +15,7 @@ build
|
|||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.eslintcache
|
||||||
*.pem
|
*.pem
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
@@ -41,3 +42,7 @@ tsconfig.tsbuildinfo
|
|||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
.notes
|
.notes
|
||||||
|
.playwright-mcp
|
||||||
|
.playwright-cli
|
||||||
|
shadcn-workspace
|
||||||
|
.codex-artifacts
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ build
|
|||||||
.contentlayer
|
.contentlayer
|
||||||
**/fixtures
|
**/fixtures
|
||||||
deprecated
|
deprecated
|
||||||
|
apps/v4/registry/styles/**/*.css
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ A set of beautifully designed components that you can customize, extend, and bui
|
|||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
Visit http://ui.shadcn.com/docs to view the documentation.
|
Visit https://ui.shadcn.com/docs to view the documentation.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
@@ -14,4 +14,4 @@ Please read the [contributing guide](/CONTRIBUTING.md).
|
|||||||
|
|
||||||
## License
|
## 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
|
.contentlayer
|
||||||
registry/__index__.tsx
|
registry/__index__.tsx
|
||||||
content/docs/components/calendar.mdx
|
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,138 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
|
||||||
|
|
||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
||||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
|
||||||
import {
|
|
||||||
Field,
|
|
||||||
FieldContent,
|
|
||||||
FieldDescription,
|
|
||||||
FieldGroup,
|
|
||||||
FieldLabel,
|
|
||||||
FieldLegend,
|
|
||||||
FieldSeparator,
|
|
||||||
FieldSet,
|
|
||||||
FieldTitle,
|
|
||||||
} from "@/registry/new-york-v4/ui/field"
|
|
||||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
|
||||||
import {
|
|
||||||
RadioGroup,
|
|
||||||
RadioGroupItem,
|
|
||||||
} from "@/registry/new-york-v4/ui/radio-group"
|
|
||||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
|
||||||
|
|
||||||
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-8 !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,120 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import {
|
|
||||||
ArchiveIcon,
|
|
||||||
ArrowLeftIcon,
|
|
||||||
CalendarPlusIcon,
|
|
||||||
ClockIcon,
|
|
||||||
ListFilterIcon,
|
|
||||||
MailCheckIcon,
|
|
||||||
MoreHorizontalIcon,
|
|
||||||
TagIcon,
|
|
||||||
Trash2Icon,
|
|
||||||
} from "lucide-react"
|
|
||||||
|
|
||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
||||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
|
||||||
|
|
||||||
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 [--radius:1rem]">
|
|
||||||
<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,58 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
||||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
|
||||||
import {
|
|
||||||
InputGroup,
|
|
||||||
InputGroupAddon,
|
|
||||||
InputGroupButton,
|
|
||||||
InputGroupInput,
|
|
||||||
} from "@/registry/new-york-v4/ui/input-group"
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/registry/new-york-v4/ui/tooltip"
|
|
||||||
|
|
||||||
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,32 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
||||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
|
||||||
|
|
||||||
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,45 +0,0 @@
|
|||||||
import { BotIcon, ChevronDownIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
||||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "@/registry/new-york-v4/ui/popover"
|
|
||||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
|
||||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
|
||||||
|
|
||||||
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 { PlusIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
AvatarFallback,
|
|
||||||
AvatarImage,
|
|
||||||
} from "@/registry/new-york-v4/ui/avatar"
|
|
||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
||||||
import {
|
|
||||||
Empty,
|
|
||||||
EmptyContent,
|
|
||||||
EmptyDescription,
|
|
||||||
EmptyHeader,
|
|
||||||
EmptyMedia,
|
|
||||||
EmptyTitle,
|
|
||||||
} from "@/registry/new-york-v4/ui/empty"
|
|
||||||
|
|
||||||
export function EmptyAvatarGroup() {
|
|
||||||
return (
|
|
||||||
<Empty className="flex-none border">
|
|
||||||
<EmptyHeader>
|
|
||||||
<EmptyMedia>
|
|
||||||
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:size-12 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]: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>
|
|
||||||
</div>
|
|
||||||
</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 "@/registry/new-york-v4/ui/checkbox"
|
|
||||||
import { Field, FieldLabel } from "@/registry/new-york-v4/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,153 +0,0 @@
|
|||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
||||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
|
||||||
import {
|
|
||||||
Field,
|
|
||||||
FieldDescription,
|
|
||||||
FieldGroup,
|
|
||||||
FieldLabel,
|
|
||||||
FieldLegend,
|
|
||||||
FieldSeparator,
|
|
||||||
FieldSet,
|
|
||||||
} from "@/registry/new-york-v4/ui/field"
|
|
||||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/registry/new-york-v4/ui/select"
|
|
||||||
import { Textarea } from "@/registry/new-york-v4/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>
|
|
||||||
<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>
|
|
||||||
</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>
|
|
||||||
<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>
|
|
||||||
</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 "@/registry/new-york-v4/ui/card"
|
|
||||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
|
||||||
import {
|
|
||||||
Field,
|
|
||||||
FieldDescription,
|
|
||||||
FieldGroup,
|
|
||||||
FieldLabel,
|
|
||||||
FieldLegend,
|
|
||||||
FieldSet,
|
|
||||||
FieldTitle,
|
|
||||||
} from "@/registry/new-york-v4/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,35 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useState } from "react"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Field,
|
|
||||||
FieldDescription,
|
|
||||||
FieldTitle,
|
|
||||||
} from "@/registry/new-york-v4/ui/field"
|
|
||||||
import { Slider } from "@/registry/new-york-v4/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 "@/registry/new-york-v4/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,68 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
|
||||||
|
|
||||||
import {
|
|
||||||
InputGroup,
|
|
||||||
InputGroupAddon,
|
|
||||||
InputGroupButton,
|
|
||||||
InputGroupInput,
|
|
||||||
} from "@/registry/new-york-v4/ui/input-group"
|
|
||||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "@/registry/new-york-v4/ui/popover"
|
|
||||||
|
|
||||||
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,102 +0,0 @@
|
|||||||
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
|
|
||||||
import { ArrowUpIcon, Search } from "lucide-react"
|
|
||||||
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
|
||||||
import {
|
|
||||||
InputGroup,
|
|
||||||
InputGroupAddon,
|
|
||||||
InputGroupButton,
|
|
||||||
InputGroupInput,
|
|
||||||
InputGroupText,
|
|
||||||
InputGroupTextarea,
|
|
||||||
} from "@/registry/new-york-v4/ui/input-group"
|
|
||||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/registry/new-york-v4/ui/tooltip"
|
|
||||||
|
|
||||||
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"
|
|
||||||
className="[--radius:0.95rem]"
|
|
||||||
>
|
|
||||||
<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,42 +0,0 @@
|
|||||||
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
|
||||||
|
|
||||||
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 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,456 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useMemo, useState } from "react"
|
|
||||||
import {
|
|
||||||
IconApps,
|
|
||||||
IconArrowUp,
|
|
||||||
IconAt,
|
|
||||||
IconBook,
|
|
||||||
IconCircleDashedPlus,
|
|
||||||
IconPaperclip,
|
|
||||||
IconPlus,
|
|
||||||
IconWorld,
|
|
||||||
IconX,
|
|
||||||
} from "@tabler/icons-react"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
AvatarFallback,
|
|
||||||
AvatarImage,
|
|
||||||
} from "@/registry/new-york-v4/ui/avatar"
|
|
||||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
|
||||||
import {
|
|
||||||
Command,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
CommandList,
|
|
||||||
} from "@/registry/new-york-v4/ui/command"
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
|
||||||
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
|
|
||||||
import {
|
|
||||||
InputGroup,
|
|
||||||
InputGroupAddon,
|
|
||||||
InputGroupButton,
|
|
||||||
InputGroupTextarea,
|
|
||||||
} from "@/registry/new-york-v4/ui/input-group"
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "@/registry/new-york-v4/ui/popover"
|
|
||||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/registry/new-york-v4/ui/tooltip"
|
|
||||||
|
|
||||||
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 className="[--radius:1.2rem]">
|
|
||||||
<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 [--radius:1.2rem]" 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="[--radius:1rem]"
|
|
||||||
>
|
|
||||||
<DropdownMenuGroup className="w-42">
|
|
||||||
<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="[--radius:1rem]"
|
|
||||||
>
|
|
||||||
<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 "@/registry/new-york-v4/ui/badge"
|
|
||||||
import { Spinner } from "@/registry/new-york-v4/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 "@/registry/new-york-v4/ui/button"
|
|
||||||
import {
|
|
||||||
Empty,
|
|
||||||
EmptyContent,
|
|
||||||
EmptyDescription,
|
|
||||||
EmptyHeader,
|
|
||||||
EmptyMedia,
|
|
||||||
EmptyTitle,
|
|
||||||
} from "@/registry/new-york-v4/ui/empty"
|
|
||||||
import { Spinner } from "@/registry/new-york-v4/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,22 +1,18 @@
|
|||||||
import { type Metadata } from "next"
|
import { type Metadata } from "next"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { PlusSignIcon } from "@hugeicons/core-free-icons"
|
import { IconArrowRight } from "@tabler/icons-react"
|
||||||
import { HugeiconsIcon } from "@hugeicons/react"
|
|
||||||
|
|
||||||
import { Announcement } from "@/components/announcement"
|
import { Announcement } from "@/components/announcement"
|
||||||
import { ExamplesNav } from "@/components/examples-nav"
|
|
||||||
import {
|
import {
|
||||||
PageActions,
|
PageActions,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
PageHeaderDescription,
|
PageHeaderDescription,
|
||||||
PageHeaderHeading,
|
PageHeaderHeading,
|
||||||
} from "@/components/page-header"
|
} from "@/components/page-header"
|
||||||
import { PageNav } from "@/components/page-nav"
|
import { Button } from "@/styles/radix-nova/ui/button"
|
||||||
import { ThemeSelector } from "@/components/theme-selector"
|
|
||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
||||||
|
|
||||||
import { RootComponents } from "./components"
|
import { CardsDemo } from "./cards"
|
||||||
|
|
||||||
const title = "The Foundation for your Design System"
|
const title = "The Foundation for your Design System"
|
||||||
const description =
|
const description =
|
||||||
@@ -52,48 +48,40 @@ export const metadata: Metadata = {
|
|||||||
export default function IndexPage() {
|
export default function IndexPage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 flex-col">
|
<div className="flex flex-1 flex-col">
|
||||||
<PageHeader>
|
<PageHeader className="md:**:[.container]:pb-8 lg:**:[.container]:pb-12">
|
||||||
<Announcement />
|
<Announcement />
|
||||||
<PageHeaderHeading className="max-w-4xl">{title}</PageHeaderHeading>
|
<PageHeaderHeading className="max-w-4xl">{title}</PageHeaderHeading>
|
||||||
<PageHeaderDescription>{description}</PageHeaderDescription>
|
<PageHeaderDescription>{description}</PageHeaderDescription>
|
||||||
<PageActions>
|
<PageActions>
|
||||||
<Button asChild size="sm" className="h-[31px] rounded-lg">
|
<Button asChild className="h-[31px] rounded-lg">
|
||||||
<Link href="/create">
|
<Link href="/create?preset=b27GcrRo">
|
||||||
<HugeiconsIcon icon={PlusSignIcon} />
|
Build Your Own <IconArrowRight data-icon="inline-end" />
|
||||||
New Project
|
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild size="sm" variant="ghost" className="rounded-lg">
|
|
||||||
<Link href="/docs/components">View Components</Link>
|
|
||||||
</Button>
|
|
||||||
</PageActions>
|
</PageActions>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageNav className="hidden md:flex">
|
<div className="container-wrapper flex-1 p-0">
|
||||||
<ExamplesNav className="[&>a:first-child]:text-primary flex-1 overflow-hidden" />
|
<div className="container overflow-hidden md:px-0 lg:max-w-none">
|
||||||
<ThemeSelector className="mr-4 hidden md:flex" />
|
<section className="-mx-4 w-[140vw] overflow-hidden md:hidden">
|
||||||
</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]">
|
|
||||||
<Image
|
<Image
|
||||||
src="/r/styles/new-york-v4/dashboard-01-light.png"
|
src="/images/full-light.png"
|
||||||
width={1400}
|
width={2560}
|
||||||
height={875}
|
height={2764}
|
||||||
alt="Dashboard"
|
alt="Dashboard"
|
||||||
className="block dark:hidden"
|
className="block h-auto w-full dark:hidden"
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
<Image
|
<Image
|
||||||
src="/r/styles/new-york-v4/dashboard-01-dark.png"
|
src="/images/full-dark.png"
|
||||||
width={1400}
|
width={2560}
|
||||||
height={875}
|
height={2764}
|
||||||
alt="Dashboard"
|
alt="Dashboard"
|
||||||
className="hidden dark:block"
|
className="hidden h-auto w-full dark:block"
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<section className="theme-container hidden md:block">
|
<section className="hidden md:block">
|
||||||
<RootComponents />
|
<CardsDemo />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user