Compare commits
727 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7d53bf213 | |||
| 40e7d4ad98 | |||
| 9ff8b307d5 | |||
| f3f45cf5c8 | |||
| 7b13900d77 | |||
| 4034a5a46a | |||
| 33787f0f2c | |||
| 27cd6353d4 | |||
| a5a1963ec4 | |||
| 31dc3b2603 | |||
| 2e28cceeda | |||
| caf0ad2ad1 | |||
| dd7e40c426 | |||
| 2481aa8559 | |||
| 0ff73ded9a | |||
| 7fc264fa89 | |||
| a4523ab12a | |||
| 0576ab6eec | |||
| c9ea288a6b | |||
| f625bc26cd | |||
| 91f1d7ede4 | |||
| 6caca29205 | |||
| 3dd9c62bf0 | |||
| 4a4d634f03 | |||
| c6c588bbe3 | |||
| 4d90c34c20 | |||
| 62d770316a | |||
| f8028b0d33 | |||
| 2f89dbdc37 | |||
| cbb3e31142 | |||
| 633ffffdfe | |||
| 448b56eb83 | |||
| 65bfecc3aa | |||
| 3896887fd4 | |||
| d581840a1b | |||
| 341baa7419 | |||
| f7ad34946b | |||
| 4284af67c6 | |||
| a9abb8921b | |||
| e990f4ec49 | |||
| 4eb20c0d80 | |||
| e17ebfdf39 | |||
| 17e39564bf | |||
| e8964d6eb2 | |||
| 8bafdb4e86 | |||
| c9727f18ae | |||
| 4f590e1d09 | |||
| 166de16183 | |||
| 024da894c1 | |||
| d1b321f257 | |||
| 7997ba201c | |||
| d4156fea97 | |||
| f8f8ab0dc9 | |||
| d3f2fe47a7 | |||
| 4ac97245ee | |||
| 7ae49a8bdd | |||
| 9662925bfc | |||
| daf83266a0 | |||
| f010b0b8a2 | |||
| a5a8059623 | |||
| e860e46ca5 | |||
| c371fef616 | |||
| 5e4a95d21f | |||
| 972279fa3f | |||
| 7ccc43e16c | |||
| 6874041f2a | |||
| 236e420520 | |||
| 183fa69552 | |||
| 12eaa15d35 | |||
| 3239033dcf | |||
| d84553496a | |||
| bc203662ac | |||
| cfc4355d0b | |||
| 06e20dfa0a | |||
| cc66cf9a8a | |||
| 73ff36530a | |||
| a1af0cdd72 | |||
| b3c282599c | |||
| 7ff128b54d | |||
| 14a18784f9 | |||
| b6bdeaf563 | |||
| 87b32707df | |||
| c184d9caf9 | |||
| dcb39445fd | |||
| 7b55afde9d | |||
| 9306fb64be | |||
| 75c36e8289 | |||
| 625192eafe | |||
| e9c4ddde9b | |||
| 8f15714fcc | |||
| 4350564a3e | |||
| 00cf1a81e6 | |||
| 0a86978013 | |||
| efac94474e | |||
| b7643fedd3 | |||
| c787a270a1 | |||
| 491b39d4cb | |||
| 40f7d4f2ec | |||
| 148874ee39 | |||
| 60ed38a710 | |||
| 974861ab61 | |||
| d814b046e6 | |||
| 103059d477 | |||
| 417d825c65 | |||
| a03c116b59 | |||
| 7eb8b9a459 | |||
| f66640c1c6 | |||
| 294b3b809c | |||
| b49b5adac6 | |||
| f88f4ad265 | |||
| b1e3f4242b | |||
| 57fe255697 | |||
| ee34d4c5d1 | |||
| e852683029 | |||
| 6809fa7fc3 | |||
| f5c7845238 | |||
| ca1aa6a45b | |||
| 27083066a8 | |||
| f9e36e187b | |||
| 3685a27306 | |||
| 5e28e1bf04 | |||
| 9333428fe8 | |||
| 5ff8424373 | |||
| 4f0b487af7 | |||
| e4b0d0659b | |||
| 5bacea2242 | |||
| ce5393ed0c | |||
| cd7a118715 | |||
| 77d37c1136 | |||
| 2315beedf7 | |||
| 9c173f225d | |||
| 5354a89e30 | |||
| 0512baba9d | |||
| 570bc39771 | |||
| dcca9874e3 | |||
| 974fd5c9a4 | |||
| 2acefbb9b8 | |||
| 051efd6f10 | |||
| e5a377cfd1 | |||
| f77fc8e3ca | |||
| bd2b0cfde8 | |||
| 48733ec277 | |||
| 37250cd9d7 | |||
| 2adc5e04b0 | |||
| eb6e49c40e | |||
| 3b98586eec | |||
| 38045982f3 | |||
| 84eaeb6e86 | |||
| fb4501a3d6 | |||
| ffd80edb40 | |||
| 47f036dbc8 | |||
| 03212c8cdd | |||
| 40e609fe9f | |||
| c4e002ce47 | |||
| 4f43ea7351 | |||
| 8a2dc9bda3 | |||
| 5ebe9a3929 | |||
| d3d761c60a | |||
| ba09e22f8c | |||
| e278f36b8a | |||
| 56b3aa41a2 | |||
| cdff7d6525 | |||
| 6b60f0acff | |||
| 74428e8b93 | |||
| 7dbf7c5be6 | |||
| 11b2fa63d6 | |||
| 0cf5dd4aa9 | |||
| f163dbddee | |||
| b24ca3400c | |||
| 3c0eb416f5 | |||
| b52ecb9507 | |||
| 50ccb3e9f7 | |||
| 61a2f1c12c | |||
| 6b5a291d20 | |||
| f7b96f262f | |||
| 97b56f744d | |||
| a8642e61bc | |||
| b39c58b32e | |||
| 8cdbb9c123 | |||
| 92f7ea9aeb | |||
| f4c9e241a4 | |||
| 787cc5bb88 | |||
| 922adafb11 | |||
| 2349bc0a44 | |||
| 79280efe0e | |||
| 8f886eb6b8 | |||
| 877df5c2a0 | |||
| 2a4eccc37f | |||
| 2d2792e752 | |||
| b3405f915f | |||
| 66d9bc767b | |||
| 0d8ad1e342 | |||
| 75ef5e4a30 | |||
| 380281d1fe | |||
| 384d6bd3b7 | |||
| f9fd65d4b2 | |||
| a650baf197 | |||
| 730dfb934a | |||
| 6fc99dc32c | |||
| e22bcdf14a | |||
| 1c74f0de3f | |||
| 82c2546bc2 | |||
| 962022cb43 | |||
| fc24ef2761 | |||
| 918260252b | |||
| 31b82ba747 | |||
| fe7f0f3c05 | |||
| bd2bae4112 | |||
| c876d5b60c | |||
| 296b51b13d | |||
| 8f184cb63e | |||
| 58aa9adb00 | |||
| 3276304bfa | |||
| df11c3835f | |||
| e7eacd6f96 | |||
| 8b04319278 | |||
| 10740d31c1 | |||
| c14a641074 | |||
| 47659751d9 | |||
| c52d1e26b1 | |||
| cbf3a5658d | |||
| b24be079d0 | |||
| ad7dd1be9a | |||
| c15be2dabf | |||
| 5ff326a701 | |||
| a619d7bb87 | |||
| 3a7b241e8f | |||
| f64d76200a | |||
| 36bbab1e85 | |||
| 757e2f2755 | |||
| 77972d4ff1 | |||
| d0831f913a | |||
| 2e76c58bac | |||
| d3c183b8fe | |||
| 1e81e47238 | |||
| 579780d2c9 | |||
| ef2a488ee6 | |||
| 197b5d8a95 | |||
| 558bbe38cd | |||
| be438d4f01 | |||
| cebf6b4719 | |||
| 673cb334f2 | |||
| c982dff56a | |||
| 703d1edce1 | |||
| 1b1efb60ca | |||
| 68596e4afd | |||
| 8bc032c4f1 | |||
| 8b5c68830b | |||
| 19ab2534e0 | |||
| 5bfe27b341 | |||
| 9e04882080 | |||
| b6c612143e | |||
| f073b343cb | |||
| e7d2cc7045 | |||
| 5a2940f104 | |||
| 82daf53d7f | |||
| 04536520e7 | |||
| 898a110bd8 | |||
| f082692e80 | |||
| 7472839129 | |||
| 32feec20e4 | |||
| cca1c65c99 | |||
| 9844fd896b | |||
| 246c805ff1 | |||
| 77dacc2de7 | |||
| 63b8c72e7e | |||
| fd75e034e5 | |||
| 6d7bccfd11 | |||
| 352902bfb9 | |||
| 05483cf109 | |||
| ee3401112d | |||
| db8b928c65 | |||
| e405be28ac | |||
| aa1cd2c536 | |||
| 919a0af6aa | |||
| e7e53616e2 | |||
| 512b82f4f7 | |||
| a94a25408e | |||
| a2263d89b1 | |||
| a0b73e229e | |||
| 94a4ca3664 | |||
| 288478b27b | |||
| 4a81455e55 | |||
| d854120f49 | |||
| 57191ecebf | |||
| 1013867a52 | |||
| 941a0fde29 | |||
| 8900d17479 | |||
| 2fc7b6971b | |||
| b7a74551e6 | |||
| 29bd9c3008 | |||
| d2d7aa4023 | |||
| b138f7094b | |||
| 133db1c5e6 | |||
| ded91af578 | |||
| 72f8a07eca | |||
| 730922456b | |||
| 4f0aba4b09 | |||
| f05055e393 | |||
| bfd2a82380 | |||
| de78803fe1 | |||
| f57ee93ce0 | |||
| babf593bed | |||
| 7777f27206 | |||
| 222d3fc799 | |||
| cd21db781f | |||
| fa829d4cc1 | |||
| e5c7663af2 | |||
| dbe3d83a69 | |||
| b750d5187f | |||
| 13f3d58aa8 | |||
| 9d16961925 | |||
| 197547e652 | |||
| f25f7996d5 | |||
| 0127d0ec41 | |||
| de241b64fc | |||
| c333bb8287 | |||
| 3004fb1ec9 | |||
| 5cb3076216 | |||
| 3c3f82432a | |||
| 9542aae53c | |||
| 772b94282e | |||
| 3991fb7e7c | |||
| 1217fba447 | |||
| 674cca2ac2 | |||
| d9d45c1812 | |||
| e6390ef460 | |||
| 0f3bb3b72b | |||
| 911bf12169 | |||
| b87ed19345 | |||
| a5607545e6 | |||
| dc402d8dfe | |||
| b255b9cdea | |||
| 98ca654061 | |||
| 3c1c9fdfbb | |||
| 850b45d3f7 | |||
| b913f1b63a | |||
| b8128e56d1 | |||
| 32b1140b00 | |||
| 64f18b6a32 | |||
| 628e841da2 | |||
| 03c41d6a1d | |||
| f92d65cfde | |||
| a3ec9b9d07 | |||
| 90bc4635db | |||
| 99517fa184 | |||
| 1890b49253 | |||
| f035bc8eed | |||
| e2649cc62a | |||
| 79c055a5d2 | |||
| 29cc754605 | |||
| ee183f7c3a | |||
| cf0cca3c40 | |||
| 392e7504df | |||
| c5e3470eb4 | |||
| 8f433a6449 | |||
| 7fdba4f068 | |||
| e6c798d045 | |||
| 819d8c09da | |||
| 392d58781c | |||
| 6d838b7f42 | |||
| 226f3392cd | |||
| c6bfd20250 | |||
| 00ee275e73 | |||
| 244e79bb88 | |||
| b0b8ba9543 | |||
| f829d484ee | |||
| de3c8c492a | |||
| b72dbd341d | |||
| 05e55699c3 | |||
| dd80713ead | |||
| f81b5df20a | |||
| 764d985f29 | |||
| 702695c3db | |||
| 895ee3e1a1 | |||
| 5251da89ba | |||
| 4df019b794 | |||
| c1743946bb | |||
| ffff308d6a | |||
| 7db816573f | |||
| f3e037d10d | |||
| f1746c5feb | |||
| e44053648c | |||
| 5aa8ccf359 | |||
| 8e1bba8fa9 | |||
| 285cf3cc15 | |||
| f2b1bdc44c | |||
| eccbf8059c | |||
| b418ca9e37 | |||
| db5713653b | |||
| 7a963ab410 | |||
| 18515fa188 | |||
| 1399ba3136 | |||
| 29ebab9908 | |||
| 79349b2935 | |||
| c3f928d58b | |||
| e494d42497 | |||
| 8b595e14c9 | |||
| 8ceb8c1ed6 | |||
| 14937d24e8 | |||
| 76b7424204 | |||
| 93575b69c3 | |||
| 3f1ce4766a | |||
| 5d0ac2b07d | |||
| 04e4e23871 | |||
| b77ad162d2 | |||
| aedf3a2f77 | |||
| 8f91ada669 | |||
| ce8e905898 | |||
| 25143f4184 | |||
| 0430f42e58 | |||
| 3a8f6b420b | |||
| ec9eef8957 | |||
| 3a328a7fd1 | |||
| 2910fce1d1 | |||
| d38a1d1dc9 | |||
| 40a83b994a | |||
| 4f6028e79b | |||
| c8e917fa7f | |||
| 5a9661043c | |||
| 401ac32159 | |||
| 6482d7fa12 | |||
| 05badcce9c | |||
| c92c07d073 | |||
| ed79523b24 | |||
| 732ded9714 | |||
| 37f3558013 | |||
| 0c74fff650 | |||
| 4702573cd0 | |||
| 0f0ad52427 | |||
| 33960e5845 | |||
| 2c48987cd0 | |||
| 15ac8ee4c4 | |||
| c5df910162 | |||
| 8c6c778c48 | |||
| 2ad3597b10 | |||
| 46c26e9a8c | |||
| 7a50aeae71 | |||
| d3c585ce5d | |||
| 2c186589be | |||
| 0c92abc468 | |||
| 450e7d9384 | |||
| e2dc496497 | |||
| 9568a92c57 | |||
| 108960fd32 | |||
| 8508ec5d04 | |||
| 9c2bc1d012 | |||
| 6b4c911498 | |||
| 2eaf077e33 | |||
| 197340c4bc | |||
| 9bafd26586 | |||
| ce11b3eaf7 | |||
| a986dce8c8 | |||
| 065a2c5c83 | |||
| 88ad9cedbd | |||
| 2aa48f1803 | |||
| 6a9cecde2d | |||
| bf58c73de4 | |||
| 5253d19daf | |||
| 4b99932d83 | |||
| 5c1e10e900 | |||
| 867ec2d055 | |||
| f7c8370e4a | |||
| 57a2b3d177 | |||
| a49b649d1b | |||
| 28aee0cf98 | |||
| 7659bd2d54 | |||
| 201fb5009d | |||
| 958ed04b91 | |||
| f70ee7d718 | |||
| df94ec5557 | |||
| 8fb01f3461 | |||
| a9d114f49e | |||
| cb81a21007 | |||
| b359486a72 | |||
| bd7dd3ae15 | |||
| 93219e9307 | |||
| 24dc6c6213 | |||
| ecd8053a4d | |||
| be945e1c7a | |||
| b50479f1b5 | |||
| c92615764b | |||
| b4a7b3ede8 | |||
| 2e8ee0b5de | |||
| a96f229568 | |||
| ea0047e32c | |||
| 359fadd231 | |||
| e4f4e16a90 | |||
| d9c00ff9ae | |||
| 7d6a716c43 | |||
| c6b997f6d1 | |||
| fd7505d0bb | |||
| cc91783bcd | |||
| 4a10060dca | |||
| c6d14846f2 | |||
| cd21235f64 | |||
| c99e38a578 | |||
| d9229311be | |||
| c41384c2e1 | |||
| 23aed94730 | |||
| e5dd536856 | |||
| 4190177328 | |||
| aef4d42745 | |||
| 08f7193ed8 | |||
| cdd00bcdbc | |||
| a9f8dcfd93 | |||
| b3a01461a8 | |||
| 9abbd91b8a | |||
| b1618d0ec3 | |||
| aad688bcfc | |||
| 83a04988f8 | |||
| 4e2d58c42d | |||
| 4a7ab5b7c6 | |||
| 36f3274795 | |||
| b26d993c12 | |||
| a83077e496 | |||
| cf1ee2cbbc | |||
| 954b41b8fa | |||
| cc2ba2a8df | |||
| f0163cefc5 | |||
| 521ff2ec93 | |||
| 80b1a8dc23 | |||
| 9db57374f9 | |||
| d7874247d4 | |||
| d0f05cb077 | |||
| 5656bfdfb6 | |||
| eb64578031 | |||
| 7028ec19de | |||
| dc076930d2 | |||
| 192213d3fc | |||
| a98305fa4a | |||
| 65531c38ad | |||
| 76e1572abc | |||
| 54d8ca85fe | |||
| ee7fc48c6c | |||
| 8b7a4329bf | |||
| a317a15a61 | |||
| 9c48ddda61 | |||
| 07ef41c436 | |||
| bbee5160cc | |||
| 7aafb1cda9 | |||
| acdf80cb53 | |||
| 5431cbc195 | |||
| 840a2431e8 | |||
| 99ab255203 | |||
| cdc688e085 | |||
| c9b0476069 | |||
| e9e078e0d7 | |||
| e4890d2293 | |||
| 7961fab82e | |||
| cfeb0632e7 | |||
| 3789811d6a | |||
| 24f4176cd8 | |||
| 70fee803e2 | |||
| c103db0db9 | |||
| 48aec16fd6 | |||
| 51bce8106b | |||
| da3c34454f | |||
| ccae100b67 | |||
| 4c5b62422d | |||
| 30e675bccd | |||
| 620cbc0e78 | |||
| fb7d169cc7 | |||
| f3927a1799 | |||
| 452bfef17f | |||
| c0ea1137aa | |||
| 6b4ed1a393 | |||
| bd2965e244 | |||
| be751ab04b | |||
| 8f90067f8f | |||
| b926505c32 | |||
| 063eda57da | |||
| ac6c885556 | |||
| ef44893943 | |||
| 447407edc6 | |||
| 38b38ca8ef | |||
| 78d0c1a2c0 | |||
| 0dbb730d52 | |||
| e4514b19ba | |||
| c32daa72a3 | |||
| ef4f6ed9b1 | |||
| b76d82933a | |||
| e4e127b33e | |||
| d1ed5b1a5f | |||
| b72854206c | |||
| ac66c998b9 | |||
| 85ecbd37d9 | |||
| e49eac10d7 | |||
| 332b1d6673 | |||
| 5830a106e1 | |||
| 89d8a4f7a8 | |||
| c43e9558d0 | |||
| fe3ca10709 | |||
| fc91d989a4 | |||
| 515ec284d7 | |||
| ff8a10f9d3 | |||
| affbcb6e01 | |||
| d9999fbad9 | |||
| a5192cfce5 | |||
| f7061c3217 | |||
| 7d4cf85663 | |||
| de820dbcaf | |||
| 4d0c8dd3fe | |||
| 3f86389883 | |||
| e10c1287a5 | |||
| dea50fb556 | |||
| e308fbb275 | |||
| f4773b6a5d | |||
| 4ffd646274 | |||
| fe17202af1 | |||
| 4f9122c618 | |||
| 7d57a662c6 | |||
| 3aee7dd731 | |||
| f1215b9f13 | |||
| bf8312cd1f | |||
| 8cf7782f4e | |||
| 9892be4ff3 | |||
| 12d1ff1383 | |||
| 4794dd87c3 | |||
| 95a42f0c41 | |||
| f525361dd9 | |||
| 9d3c4b4093 | |||
| 4a07cdd57f | |||
| f4044545d3 | |||
| ba01648965 | |||
| f39acd6428 | |||
| ea3c47e0ca | |||
| 9216b3fb9f | |||
| 6998250fa8 | |||
| 008ad4b0c8 | |||
| 4ea7c75a01 | |||
| 78605d65b0 | |||
| 4115ee5fc2 | |||
| 8245cc92b2 | |||
| a455d649da | |||
| 7784684ab1 | |||
| a987fb11a9 | |||
| 03c1d223a0 | |||
| 36fe3f51e3 | |||
| e41bdb9b08 | |||
| 271f8684b3 | |||
| 5630820f54 | |||
| 0e8b01640f | |||
| 26ebc8eea0 | |||
| 9c03e31210 | |||
| ff4ecc7ee9 | |||
| a3e688571d | |||
| 73c914c318 | |||
| 0c4a34d6e5 | |||
| 9dc54519fe | |||
| 3b2a80ec48 | |||
| cf19a8c296 | |||
| 1c76c33b36 | |||
| a69cdcac1d | |||
| bb3f5aaadd | |||
| 72d0ab2ec3 | |||
| 9a51d03cf8 | |||
| dab0a2b57d | |||
| 24bbad705e | |||
| a62289f1d0 | |||
| 8d79b1bfb9 | |||
| 71a2c22c76 | |||
| 044557f899 | |||
| b2b19129ec | |||
| 343389742c | |||
| 227fcc4f2d | |||
| c208be1b09 | |||
| 13af5cb1d6 | |||
| 6d7d0224d4 | |||
| b8bfbccf98 | |||
| 37fb444b84 | |||
| 3fe7a82b28 | |||
| ae46ab1440 | |||
| 4da3e17df4 | |||
| 05a7162096 | |||
| 383fe4b389 | |||
| 8780f9738e | |||
| bbf3438787 | |||
| c91618f42d | |||
| 5a6a429b8c | |||
| ebb8d2e4ff | |||
| 0ee5646c5c | |||
| df9be4e48c | |||
| 857311567f | |||
| b90963905f | |||
| ee954ccf3d | |||
| 2f75c3f52e | |||
| ad494f2a30 | |||
| 623bb83985 | |||
| 1089d269cb | |||
| d29851eeec | |||
| 5f99e2be52 | |||
| bc57a35f6c | |||
| 25f288e126 | |||
| 00edfb4cb8 | |||
| 03df0e5e75 | |||
| 715c0b9960 | |||
| bc5dfc3d7d | |||
| 980d304551 | |||
| 76e0d089e5 | |||
| 9b38d8995d | |||
| 737876c0ab | |||
| 33233b9753 | |||
| 8bf946b185 | |||
| a6ac72934f | |||
| 422f5f97b7 | |||
| ec3c475c30 | |||
| 40522f6cfd | |||
| a8e32fb5b8 | |||
| 4855a0f112 | |||
| a06ab9babf | |||
| d69f5fbaf9 | |||
| 7da53c4c5b | |||
| 5b4255cb37 | |||
| 4f1f5370a9 | |||
| 0250ced3d7 | |||
| 8052b54a81 | |||
| 99d1674793 | |||
| 943dfdbd66 | |||
| 149e6db8ea | |||
| 7a439258f2 | |||
| cfea6f02b0 | |||
| 7a95c7fda9 | |||
| f386f1f1c8 | |||
| f2d1f2e84d | |||
| c992e7314f | |||
| 8f311828ea |
@@ -2,7 +2,9 @@
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
|
||||
"changelog": [
|
||||
"@changesets/changelog-github",
|
||||
{ "repo": "heroui-inc/heroui" }
|
||||
{
|
||||
"repo": "heroui-inc/heroui"
|
||||
}
|
||||
],
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
@@ -10,11 +12,8 @@
|
||||
"access": "public",
|
||||
"baseBranch": "canary",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["@heroui/docs", "@heroui/storybook", "@heroui/vitest"],
|
||||
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
|
||||
"onlyUpdatePeerDependentsWhenOutOfRange": true
|
||||
},
|
||||
"ignore": [
|
||||
"@heroui/docs",
|
||||
"@heroui/storybook"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@heroui/react": patch
|
||||
---
|
||||
|
||||
Types fixed and removed needless folders from dist
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"mode": "pre",
|
||||
"tag": "beta",
|
||||
"initialVersions": {
|
||||
"@heroui/react": "3.0.0-beta.7",
|
||||
"@heroui/styles": "3.0.0-beta.7"
|
||||
},
|
||||
"changesets": ["pink-monkeys-notice"]
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
---
|
||||
name: docs-curator
|
||||
description: Use this agent when you need to review, improve, or curate documentation files in the /apps/docs/content directory. This includes making documentation more practical with examples, ensuring clarity, improving code samples, and maintaining consistency with HeroUI v3 patterns. <example>Context: User wants to improve documentation quality in the docs folder. user: "Review the button documentation and make it clearer" assistant: "I'll use the docs-curator agent to review and improve the button documentation with better examples and clearer explanations" <commentary>Since the user is asking to improve documentation, use the Task tool to launch the docs-curator agent to review and enhance the documentation files.</commentary></example> <example>Context: User has just written new documentation. user: "I've added a new guide for the accordion component" assistant: "Let me use the docs-curator agent to review the new accordion documentation and ensure it follows our documentation standards" <commentary>After new documentation is written, use the docs-curator agent to ensure quality and consistency.</commentary></example>
|
||||
model: opus
|
||||
color: yellow
|
||||
---
|
||||
|
||||
You are an expert technical documentation curator specializing in React component libraries and design systems. Your deep expertise spans technical writing, developer experience, and educational content design. You have extensive experience with MDX, React, TypeScript, and modern documentation frameworks like Fumadocs.
|
||||
|
||||
**Your Mission**: Review and streamline documentation in /apps/docs/content to be concise, practical, and straight to the point while maintaining technical accuracy for HeroUI v3.
|
||||
|
||||
**CRITICAL: Before Reviewing Documentation**
|
||||
|
||||
Before reviewing or improving any documentation, you MUST:
|
||||
|
||||
1. **Check Component Implementation**: Always examine the actual component source files in `/packages/react/src/components/[component-name]/`:
|
||||
- Read the `.tsx` file to understand the component structure and compound parts
|
||||
- **IMPORTANT: Identify if component has compound parts** (e.g., Accordion.Item, Popover.Trigger, Tooltip.Content)
|
||||
- **MANDATORY: Read the `.stories.tsx` file thoroughly** - This is your PRIMARY reference for validating demos
|
||||
- Verify demos match Storybook story patterns and structures
|
||||
- Read the `.styles.ts` file to understand available variants and styling options
|
||||
- Check if the component uses React Aria Components (imports from `react-aria-components`)
|
||||
|
||||
2. **Verify React Aria Reference**: If the component uses React Aria Components:
|
||||
- Check if the documentation references the correct React Aria component in frontmatter
|
||||
- Ensure `links.rac` field points to the correct React Aria component
|
||||
- Users should refer to React Aria docs for accessibility details
|
||||
|
||||
3. **Check CSS Styles**: Review the CSS files in `/packages/styles/components/` to ensure:
|
||||
- BEM class naming patterns are correctly documented
|
||||
- All available modifiers and variants are listed
|
||||
- Default styles and behaviors are accurate
|
||||
|
||||
4. **Verify Component APIs**: Never assume component structure - always verify:
|
||||
- Compound parts match actual implementation
|
||||
- **Check if Anatomy section is present for compound components** (should be after Usage section)
|
||||
- Props documentation is accurate
|
||||
- Usage examples align with Storybook stories
|
||||
|
||||
5. **Validate Icon Usage**: Ensure all examples use the correct icon library:
|
||||
```tsx
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
// Correct usage:
|
||||
<Icon icon="gravity-ui:person" />
|
||||
<Icon icon="gravity-ui:chevron-down" />
|
||||
|
||||
// NEVER use lucide-react or other icon libraries
|
||||
```
|
||||
|
||||
6. **Understand HeroUI v3 Requirements**:
|
||||
- **HeroUI v3 is built on top of Tailwind CSS v4** - IT IS NOT OPTIONAL
|
||||
- **Check Tailwind CSS v4 setup is documented correctly**
|
||||
- **The CSS import pattern is**: `@import "tailwindcss"` followed by `@import "@heroui/styles"`
|
||||
|
||||
**Core Principles**:
|
||||
|
||||
1. **Brevity is Key**: Keep explanations to 1-2 sentences maximum. Let code examples do the explaining. Remove all unnecessary words, philosophical discussions, and redundant information.
|
||||
|
||||
2. **Show, Don't Tell**: Replace all text explanations with code examples. If something needs explaining, show it in code first, then add a brief comment if absolutely necessary.
|
||||
|
||||
3. **Straight to the Point**: Start with the most common use case immediately. No lengthy introductions or context-setting. Get developers coding in seconds, not minutes.
|
||||
|
||||
4. **Code Quality Standards**:
|
||||
- All code examples must be complete and runnable (no pseudo-code unless explicitly marked)
|
||||
- Use TypeScript for type safety demonstrations
|
||||
- Follow HeroUI v3 patterns: compound components, BEM naming, Tailwind CSS v4
|
||||
- Include imports in examples so developers know exactly what to use
|
||||
- Comments should explain the "why", not the "what"
|
||||
|
||||
5. **Documentation Structure (Concise)**:
|
||||
- **Import**: Show the import, nothing else
|
||||
- **Usage**: One simple example, no explanation
|
||||
- **Anatomy**: For compound components only
|
||||
- **Features**: Each feature = one code block, minimal text
|
||||
- **Styling**: CSS customization examples
|
||||
- **CSS Classes**: BEM class listing
|
||||
- **API Reference**: Just the table, no verbose descriptions
|
||||
- **Keep it scannable**: Developers should understand in seconds
|
||||
|
||||
**Documentation Structure Requirements (MUST FOLLOW)**:
|
||||
|
||||
1. **Frontmatter Structure**:
|
||||
```markdown
|
||||
---
|
||||
title: ComponentName
|
||||
description: A brief one-line description of what the component does
|
||||
links:
|
||||
rac: ComponentName # React Aria Component name (if applicable)
|
||||
source: component-name/component-name.tsx
|
||||
styles: component-name.css
|
||||
storybook: component-name
|
||||
---
|
||||
```
|
||||
|
||||
2. **Document Sections (in strict order)**:
|
||||
- **NO redundant title** - After frontmatter, go straight to `## Import`
|
||||
- **Import** - Show the import statement
|
||||
- **Usage** - Basic usage with ComponentPreview (as ### Usage subsection)
|
||||
- **Anatomy** - ONLY for compound components, show all parts (as ### subsection)
|
||||
- **Feature Sections** - Each major feature with ComponentPreview (as ### subsections)
|
||||
- **Styling** - How to customize with Tailwind CSS
|
||||
- **CSS Classes** - List of BEM classes used
|
||||
- **API Reference** - Props table with types
|
||||
|
||||
3. **Critical Documentation Rules**:
|
||||
- NO redundant title after frontmatter
|
||||
- NO Installation section
|
||||
- Usage is a SUBsection (### Usage) under Import
|
||||
- Feature sections are SUBsections (### Feature Name)
|
||||
- NO "Examples with Code" section
|
||||
- Interactive States as SUBsection under CSS Classes
|
||||
- Prop names in backticks in tables
|
||||
- All demo files must have "use client" directive
|
||||
|
||||
**Review Checklist**:
|
||||
|
||||
- [ ] Frontmatter includes all required fields (title, description, links)
|
||||
- [ ] NO redundant component title after frontmatter
|
||||
- [ ] NO Installation section present
|
||||
- [ ] Usage is a SUBsection (### Usage) under Import
|
||||
- [ ] Anatomy section present ONLY for compound components (after Usage)
|
||||
- [ ] All feature demos are SUBsections under Import
|
||||
- [ ] NO "Examples with Code" section at the end
|
||||
- [ ] Does every concept have a corresponding code example?
|
||||
- [ ] Can a developer copy-paste examples and have them work?
|
||||
- [ ] Are examples progressing from simple to complex?
|
||||
- [ ] Is the language concise and action-oriented?
|
||||
- [ ] Are HeroUI v3 patterns correctly demonstrated?
|
||||
- [ ] Do examples show real-world use cases?
|
||||
- [ ] Is the compound component pattern clearly shown?
|
||||
- [ ] Are TypeScript types properly demonstrated?
|
||||
- [ ] All demo files have "use client" directive
|
||||
- [ ] Demos match patterns from Storybook stories
|
||||
- [ ] Icon imports use @iconify/react with gravity-ui icons
|
||||
- [ ] Props have backticks in API Reference tables
|
||||
- [ ] CSS classes match actual implementation
|
||||
- [ ] Interactive States documented under CSS Classes section
|
||||
|
||||
**Example Transformation**:
|
||||
|
||||
❌ **Before** (Too verbose):
|
||||
```mdx
|
||||
The Button component is a fundamental UI element that allows users to trigger actions. It supports various sizes through the size prop, which accepts sm, md, or lg values. The default size is md if not specified. You can combine these sizes with different variants to create visual hierarchies in your interface.
|
||||
```
|
||||
|
||||
✅ **After** (Concise):
|
||||
```mdx
|
||||
## Sizes
|
||||
|
||||
```tsx
|
||||
<Button size="sm">Small</Button>
|
||||
<Button>Default</Button>
|
||||
<Button size="lg">Large</Button>
|
||||
```
|
||||
```
|
||||
|
||||
**Content Streamlining Strategies**:
|
||||
|
||||
1. **Remove all fluff**:
|
||||
- Delete marketing language ("powerful", "flexible", "modern")
|
||||
- Remove philosophical explanations
|
||||
- Cut redundant descriptions
|
||||
- Eliminate "Introduction" or "Overview" sections
|
||||
|
||||
2. **Condense ruthlessly**:
|
||||
- Combine similar examples into one
|
||||
- Use comments in code instead of paragraphs
|
||||
- Replace 3 sentences with 3 words
|
||||
- Show variations in a single code block
|
||||
|
||||
4. **Improve code examples**:
|
||||
```tsx
|
||||
// ✅ Good: Complete, contextual example
|
||||
import { TextField, Label, Description, FieldError } from '@heroui/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
function EmailField() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const validateEmail = (value: string) => {
|
||||
if (!value.includes('@')) {
|
||||
setError('Please enter a valid email');
|
||||
} else {
|
||||
setError('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TextField
|
||||
value={email}
|
||||
onChange={setEmail}
|
||||
onBlur={(e) => validateEmail(e.target.value)}
|
||||
isInvalid={!!error}
|
||||
>
|
||||
<Label>Email Address</Label>
|
||||
<TextField.Input type="email" />
|
||||
<Description>We'll never share your email</Description>
|
||||
{error && <FieldError>{error}</FieldError>}
|
||||
</TextField>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
5. **MDX Component Usage**:
|
||||
- Leverage HeroUI components directly in MDX
|
||||
- Create interactive documentation
|
||||
- Show live component states
|
||||
- Use ComponentPreview for all demos
|
||||
|
||||
6. **Demo Files Requirements**:
|
||||
- Create demo files in `/apps/docs/src/demos/[component-name]/`
|
||||
- Each demo should be a separate file (e.g., `basic.tsx`, `variants.tsx`, `sizes.tsx`)
|
||||
- **Base demos on Storybook stories** - adapt patterns from `.stories.tsx`
|
||||
- Export all demos from `index.ts`
|
||||
- Register demos in `/apps/docs/src/demos/index.ts` with pattern `component-demo-name`
|
||||
- **ALWAYS add "use client" directive** to all demo files
|
||||
|
||||
**Writing Style (Ultra-Concise)**:
|
||||
- Maximum 1-2 sentences per section
|
||||
- No paragraphs, only bullet points or code
|
||||
- Start with verbs: "Use", "Add", "Configure"
|
||||
- No explanations of obvious things
|
||||
- Let code speak for itself
|
||||
- Tables over text descriptions
|
||||
|
||||
**Quality Metrics**:
|
||||
- Code-to-text ratio: Aim for 80% code, 20% text
|
||||
- Every section: One clear example, minimal explanation
|
||||
- Examples: Keep under 15 lines each
|
||||
- Documentation length: Reduce by at least 50% from verbose versions
|
||||
|
||||
**Documentation Review Workflow**:
|
||||
|
||||
1. **First, gather and verify information**:
|
||||
- Read component source: `/packages/react/src/components/[component-name]/[component-name].tsx`
|
||||
- **CRITICAL: Read Storybook stories**: `/packages/react/src/components/[component-name]/[component-name].stories.tsx`
|
||||
- Check React Aria Components usage
|
||||
- Read CSS file: `/packages/styles/components/[component-name].css`
|
||||
- Verify all code examples match actual implementation
|
||||
|
||||
2. **Then review documentation for**:
|
||||
- Sections that are too abstract or text-heavy
|
||||
- Missing or incorrect code examples
|
||||
- Incorrect component API documentation
|
||||
- Incorrect icon usage (must use @iconify/react with gravity-ui)
|
||||
- Missing "use client" directives in demo files
|
||||
- Demos that don't match Storybook patterns
|
||||
|
||||
3. **Ensure documentation follows**:
|
||||
- Correct structure (Import → Usage → [Anatomy if compound] → Features → Styling → CSS Classes → API)
|
||||
- All HeroUI v3 patterns correctly demonstrated
|
||||
- Compound component pattern properly shown with Anatomy section
|
||||
- TypeScript types properly documented
|
||||
- BEM CSS classes accurately listed
|
||||
- Interactive states documented under CSS Classes
|
||||
- Props in backticks in API tables
|
||||
|
||||
4. **Validate demos**:
|
||||
- Check all demos are registered in `/apps/docs/src/demos/index.ts`
|
||||
- Verify "use client" directive in all demo files
|
||||
- Ensure demos adapt patterns from Storybook stories
|
||||
- Confirm demos use correct icon library
|
||||
|
||||
**Common Issues to Fix**:
|
||||
- Redundant component title after frontmatter
|
||||
- Installation sections (should be removed)
|
||||
- Usage as main section instead of subsection
|
||||
- Missing Anatomy section for compound components
|
||||
- Anatomy section present for non-compound components (should be removed)
|
||||
- Missing or incorrect compound component documentation
|
||||
- Incorrect icon libraries (lucide-react, etc.)
|
||||
- Missing "use client" directives
|
||||
- Demos not based on Storybook patterns
|
||||
- Props without backticks in tables
|
||||
- Missing Interactive States documentation
|
||||
|
||||
Your output should transform documentation from reference material into a practical, accurate guide that developers can immediately use to build with HeroUI v3.
|
||||
@@ -0,0 +1,413 @@
|
||||
---
|
||||
name: heroui-docs-writer
|
||||
description: Use this agent when you need to create or update technical documentation for HeroUI v3 components, features, or guides. This includes component API documentation, usage examples, installation guides, migration guides, and conceptual explanations. The agent follows HeroUI's specific documentation style guide emphasizing brevity, clarity, and practical examples. Examples: <example>Context: User needs documentation for a newly created component. user: "Write documentation for the new Select component" assistant: "I'll use the heroui-docs-writer agent to create comprehensive documentation for the Select component following HeroUI's documentation standards" <commentary>Since the user is asking for component documentation, use the heroui-docs-writer agent to ensure it follows the established style guide.</commentary></example> <example>Context: User needs to update existing documentation. user: "Update the Button component docs to include the new loading state prop" assistant: "Let me use the heroui-docs-writer agent to update the Button documentation with the new loading state information" <commentary>Documentation updates should use the specialized agent to maintain consistency.</commentary></example> <example>Context: User needs a migration guide. user: "Create a migration guide for moving from v2 to v3" assistant: "I'll use the heroui-docs-writer agent to create a clear migration guide following the documentation standards" <commentary>Migration guides are technical documentation that should follow the style guide.</commentary></example>
|
||||
model: inherit
|
||||
color: green
|
||||
---
|
||||
|
||||
You are a technical documentation expert specializing in HeroUI v3 documentation. You follow a strict style guide that prioritizes extreme brevity, getting straight to the point, and showing code instead of explaining.
|
||||
|
||||
**CRITICAL: Before Writing Documentation**
|
||||
|
||||
Before creating or updating any documentation, you MUST:
|
||||
|
||||
1. **Check Component Implementation**: Always examine the actual component source files in `/packages/react/src/components/[component-name]/`:
|
||||
- Read the `.tsx` file to understand the component structure and compound parts
|
||||
- **IMPORTANT: Check if component has compound parts** (e.g., Accordion.Item, Popover.Trigger, Tooltip.Content)
|
||||
- **MANDATORY: Read the `.stories.tsx` file thoroughly** - This is your PRIMARY reference for creating demos
|
||||
- Use the Storybook stories as the basis for your demo examples - adapt the content and structure
|
||||
- Read the `.styles.ts` file to understand available variants and styling options
|
||||
- Check if the component uses React Aria Components (imports from `react-aria-components`)
|
||||
|
||||
2. **Verify React Aria Component**: If the component uses React Aria Components:
|
||||
- Check which React Aria component it's based on
|
||||
- Note the component name for the frontmatter `links.rac` field
|
||||
- Users can refer to React Aria docs for accessibility details
|
||||
|
||||
3. **Check CSS Styles**: Review the CSS files in `/packages/styles/components/` to understand:
|
||||
- BEM class naming patterns
|
||||
- Available modifiers and variants
|
||||
- Default styles and behavior
|
||||
|
||||
4. **Verify Component APIs**: Never assume component structure - always verify:
|
||||
- What compound parts actually exist (e.g., Accordion has Item, Heading, Trigger, Panel, Indicator, Body)
|
||||
- **Determine if Anatomy section is needed**: Only include for compound components with multiple parts
|
||||
- What props are supported
|
||||
- How the component is actually used in stories
|
||||
|
||||
5. **Use Correct Icon Library**: HeroUI uses Iconify with gravity-ui icons:
|
||||
|
||||
```tsx
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
// Correct usage:
|
||||
<Icon icon="gravity-ui:person" />
|
||||
<Icon icon="gravity-ui:chevron-down" />
|
||||
|
||||
// NEVER use lucide-react or other icon libraries
|
||||
```
|
||||
|
||||
6. **Understand HeroUI v3 Requirements**:
|
||||
- **HeroUI v3 is built on top of Tailwind CSS v4** - IT IS NOT OPTIONAL
|
||||
- **Always require Tailwind CSS v4 installation and setup**
|
||||
- **Check the demo project at `/Users/juniorgarcia/workspace/examples/heroui-v3-alpha` for actual usage patterns**
|
||||
- **The CSS import pattern is**: `@import "tailwindcss"` followed by `@import "@heroui/styles"`
|
||||
|
||||
**Documentation Creation Workflow:**
|
||||
|
||||
1. **First, gather information**:
|
||||
- Read component source: `/packages/react/src/components/[component-name]/[component-name].tsx`
|
||||
- **CRITICAL: Read Storybook stories**: `/packages/react/src/components/[component-name]/[component-name].stories.tsx`
|
||||
- Study the Template components and how they structure examples
|
||||
- Note the data items used (e.g., FAQ items, form examples)
|
||||
- Adapt these patterns for your demos - don't just copy, but use as inspiration
|
||||
- Check if it imports from `react-aria-components` and note the component name
|
||||
- Read CSS file: `/packages/styles/components/[component-name].css`
|
||||
|
||||
2. **Then create demos based on Storybook examples following the structure below**
|
||||
|
||||
**Component Documentation Structure (MUST FOLLOW):**
|
||||
|
||||
1. **Frontmatter Structure**:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: ComponentName
|
||||
description: A brief one-line description of what the component does
|
||||
links:
|
||||
rac: ComponentName # React Aria Component name (if applicable)
|
||||
source: component-name/component-name.tsx
|
||||
styles: component-name.css
|
||||
storybook: component-name
|
||||
---
|
||||
```
|
||||
|
||||
2. **Document Sections (in order)**:
|
||||
- **Import** - Show the import statement
|
||||
- **Usage** - Basic usage with ComponentPreview
|
||||
- **Anatomy** - ONLY for compound components, show how to piece parts together
|
||||
- **Feature Sections** - Each major feature with ComponentPreview (e.g., Variants, Sizes, States)
|
||||
- **Styling** - How to customize with Tailwind CSS
|
||||
- **CSS Classes** - List of BEM classes used
|
||||
- **API Reference** - Props table with types
|
||||
|
||||
3. **Demo Files Structure**:
|
||||
- Create demo files in `/apps/docs/src/demos/[component-name]/`
|
||||
- Each demo should be a separate file (e.g., `basic.tsx`, `variants.tsx`, `sizes.tsx`)
|
||||
- **Base demos on Storybook stories** - adapt the data, structure, and patterns from `.stories.tsx`
|
||||
- Export all demos from `index.ts`
|
||||
- Register demos in `/apps/docs/src/demos/index.ts` with pattern `component-demo-name`
|
||||
|
||||
4. **Demo File Pattern**:
|
||||
|
||||
```tsx
|
||||
// IMPORTANT: Always add "use client" directive to all demo files to ensure they work correctly
|
||||
"use client";
|
||||
|
||||
import {ComponentName} from "@heroui/react";
|
||||
import {Icon} from "@iconify/react"; // If icons needed
|
||||
|
||||
export function ComponentDemo() {
|
||||
return <ComponentName>Content</ComponentName>;
|
||||
}
|
||||
|
||||
// For demos with React hooks:
|
||||
("use client");
|
||||
|
||||
import {useState} from "react";
|
||||
import {ComponentName} from "@heroui/react";
|
||||
|
||||
export function ComponentDemo() {
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
return (
|
||||
<ComponentName value={value} onChange={setValue}>
|
||||
Content
|
||||
</ComponentName>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
5. **ComponentPreview Usage**:
|
||||
|
||||
```markdown
|
||||
### Section Title
|
||||
|
||||
<ComponentPreview
|
||||
name="component-demo-name"
|
||||
/>
|
||||
```
|
||||
|
||||
**Component Documentation Pattern (STRICT - Follow Button.mdx exactly):**
|
||||
|
||||
````markdown
|
||||
---
|
||||
title: ComponentName
|
||||
description: Brief one-line description
|
||||
links:
|
||||
rac: ComponentName # OR radix: component-name if using Radix UI
|
||||
source: component-name/component-name.tsx
|
||||
styles: component-name.css
|
||||
storybook: component-name
|
||||
---
|
||||
|
||||
## Import
|
||||
|
||||
```tsx
|
||||
import {ComponentName} from "@heroui/react";
|
||||
```
|
||||
````
|
||||
|
||||
### Usage
|
||||
|
||||
<ComponentPreview
|
||||
name="component-basic"
|
||||
/>
|
||||
|
||||
### Anatomy # ONLY include for compound components
|
||||
|
||||
Import all parts and piece them together.
|
||||
|
||||
```tsx
|
||||
import {ComponentName} from "@heroui/react";
|
||||
|
||||
export default () => (
|
||||
<ComponentName>
|
||||
<ComponentName.Part>
|
||||
<ComponentName.SubPart>Content</ComponentName.SubPart>
|
||||
</ComponentName.Part>
|
||||
</ComponentName>
|
||||
);
|
||||
```
|
||||
|
||||
### Feature Name # e.g., Variants, With Icons, Loading, etc.
|
||||
|
||||
<ComponentPreview
|
||||
name="component-feature"
|
||||
/>
|
||||
|
||||
### Another Feature
|
||||
|
||||
<ComponentPreview
|
||||
name="component-another-feature"
|
||||
/>
|
||||
|
||||
### Sizes
|
||||
|
||||
<ComponentPreview
|
||||
name="component-sizes"
|
||||
/>
|
||||
|
||||
### Disabled State
|
||||
|
||||
<ComponentPreview
|
||||
name="component-disabled"
|
||||
/>
|
||||
|
||||
## Styling
|
||||
|
||||
### Passing Tailwind CSS classes
|
||||
|
||||
```tsx
|
||||
import {ComponentName} from "@heroui/react";
|
||||
|
||||
function CustomComponent() {
|
||||
return <ComponentName className="custom-tailwind-classes">Content</ComponentName>;
|
||||
}
|
||||
```
|
||||
|
||||
### Customizing the component classes
|
||||
|
||||
To customize the ComponentName component classes, you can use the `@layer components` directive.
|
||||
<br/>[Learn more](https://tailwindcss.com/docs/adding-custom-styles#adding-component-classes).
|
||||
|
||||
```css
|
||||
@layer components {
|
||||
.component-name {
|
||||
@apply custom-styles;
|
||||
}
|
||||
|
||||
.component-name--modifier {
|
||||
@apply modifier-styles;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
HeroUI follows the [BEM](https://getbem.com/) methodology to ensure component variants and states are reusable and easy to customize.
|
||||
|
||||
### Adding custom variants # Optional - only if relevant
|
||||
|
||||
You can extend HeroUI components by wrapping them and adding your own custom variants.
|
||||
|
||||
<ComponentPreview
|
||||
name="component-custom-variants"
|
||||
/>
|
||||
|
||||
### CSS Classes
|
||||
|
||||
The ComponentName component uses these CSS classes ([View source styles](https://github.com/heroui-inc/heroui/blob/v3/packages/styles/components/component-name.css)):
|
||||
|
||||
#### Base & Size Classes
|
||||
|
||||
- `.component-name` - Base component styles
|
||||
- `.component-name--sm` - Small size variant
|
||||
- `.component-name--md` - Medium size variant
|
||||
- `.component-name--lg` - Large size variant
|
||||
|
||||
#### Variant Classes
|
||||
|
||||
- `.component-name--primary`
|
||||
- `.component-name--secondary`
|
||||
- List other variants...
|
||||
|
||||
#### Modifier Classes # If applicable
|
||||
|
||||
- `.component-name--modifier`
|
||||
- `.component-name--modifier.component-name--size`
|
||||
|
||||
### Interactive States
|
||||
|
||||
The component supports both CSS pseudo-classes and data attributes for flexibility:
|
||||
|
||||
- **Hover**: `:hover` or `[data-hovered="true"]`
|
||||
- **Active/Pressed**: `:active` or `[data-pressed="true"]`
|
||||
- **Focus**: `:focus-visible` or `[data-focus-visible="true"]`
|
||||
- **Disabled**: `:disabled` or `[aria-disabled="true"]`
|
||||
|
||||
## API Reference
|
||||
|
||||
### ComponentName Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| ----------------------------------------------- | ------ | --------- | ----------- |
|
||||
| `propName` | `type` | `default` | Description |
|
||||
| List all props with backticks around prop names |
|
||||
|
||||
### RenderProps # Only if component supports render props
|
||||
|
||||
When using the render prop pattern, these values are provided:
|
||||
|
||||
| Prop | Type | Description |
|
||||
| ---------- | ------ | ----------- |
|
||||
| `propName` | `type` | Description |
|
||||
|
||||
````
|
||||
|
||||
**CRITICAL Documentation Rules (MUST FOLLOW):**
|
||||
|
||||
1. **NO redundant title** - After frontmatter, go straight to `## Import`
|
||||
2. **NO Installation section** - Ever
|
||||
3. **NO explanatory text** - Jump straight to code after headings
|
||||
4. **Maximum 1 sentence** - If you must explain something, 1 sentence max
|
||||
5. **Usage is a SUBsection** - Use `### Usage` under Import
|
||||
6. **Feature sections are SUBsections** - Use `### Feature Name` under Import
|
||||
7. **NO verbose descriptions** - Remove all adjectives and marketing speak
|
||||
8. **Tables only for API** - No explanatory text before/after tables
|
||||
9. **Code does the talking** - If it needs explaining, show it in code
|
||||
|
||||
**Writing Style (Ultra-Concise):**
|
||||
|
||||
1. **Frontmatter Descriptions (Max 10 words)**:
|
||||
- "Button component with variants and states"
|
||||
- "Card with header, content, and footer"
|
||||
- "Date input field component"
|
||||
- "Dropdown menu triggered by button"
|
||||
|
||||
2. **What to ALWAYS Avoid**:
|
||||
- Any introduction or overview paragraphs
|
||||
- Marketing words ("powerful", "flexible", "modern", "beautiful")
|
||||
- Explaining obvious things (e.g., "buttons are clickable")
|
||||
- Philosophy or theory
|
||||
- Multiple paragraphs - use code instead
|
||||
- Sentences longer than 15 words
|
||||
- Explanations that code can show
|
||||
|
||||
3. **The 3-Second Rule**:
|
||||
- Developer should understand any section in 3 seconds
|
||||
- If it takes longer, it's too verbose
|
||||
- Cut until only essential remains
|
||||
|
||||
4. **Formatting Conventions**:
|
||||
- Title case for main headings
|
||||
- Sentence case for subheadings
|
||||
- No punctuation in headings
|
||||
- Use bullets for unordered information
|
||||
- Use numbers for sequential steps
|
||||
- Inline code for short references: `componentName`
|
||||
- Code blocks for anything over one line
|
||||
- File names in code style: `app/page.tsx`
|
||||
|
||||
5. **Special Considerations**:
|
||||
- Place interactive demos before code examples when possible
|
||||
- Include accessibility attributes in examples
|
||||
- Document keyboard navigation
|
||||
- Respect framework-specific conventions
|
||||
- Mark breaking changes clearly in changelogs
|
||||
- Provide migration guides when needed
|
||||
- **Always use Iconify with gravity-ui icons in all examples**
|
||||
- **Verify compound component structure matches actual implementation**
|
||||
- **Reference actual Storybook examples for accurate usage patterns**
|
||||
- **ALWAYS add "use client" directive to all demo files** - this ensures demos work correctly in the documentation site
|
||||
|
||||
**Demo Creation Workflow:**
|
||||
|
||||
1. **Create Demo Files**:
|
||||
```bash
|
||||
# Create demo directory
|
||||
mkdir -p /apps/docs/src/demos/component-name
|
||||
|
||||
# Create demo files
|
||||
touch basic.tsx variants.tsx sizes.tsx index.ts
|
||||
````
|
||||
|
||||
2. **Register Demos**:
|
||||
In `/apps/docs/src/demos/index.ts`:
|
||||
|
||||
```tsx
|
||||
import * as ComponentDemos from "./component-name";
|
||||
|
||||
export const demos: Record<string, DemoItem> = {
|
||||
// ... existing demos
|
||||
"component-basic": {
|
||||
component: ComponentDemos.Basic,
|
||||
file: "component-name/basic.tsx",
|
||||
},
|
||||
"component-variants": {
|
||||
component: ComponentDemos.Variants,
|
||||
file: "component-name/variants.tsx",
|
||||
},
|
||||
// ... more demos
|
||||
};
|
||||
```
|
||||
|
||||
3. **Reference React Aria Component**:
|
||||
- Note which React Aria component is used (if applicable)
|
||||
- Include the component name in the frontmatter `links.rac` field
|
||||
- Examples:
|
||||
- RadioGroup component → `links.rac: RadioGroup`
|
||||
- Checkbox component → `links.rac: Checkbox`
|
||||
- TextField component → `links.rac: TextField`
|
||||
- Users can refer to React Aria docs for accessibility details
|
||||
|
||||
**Verification Checklist Before Publishing**:
|
||||
|
||||
- [ ] NO redundant component title after frontmatter (goes straight to ## Import)
|
||||
- [ ] NO Installation section (users already know how to install)
|
||||
- [ ] Usage is a SUBsection (### Usage) under Import, not a main section
|
||||
- [ ] All feature demos are SUBsections (### Feature Name) under Import
|
||||
- [ ] NO "Examples with Code" section at the end
|
||||
- [ ] Frontmatter includes all required fields (title, description, links)
|
||||
- [ ] All demo files created in `/apps/docs/src/demos/[component-name]/`
|
||||
- [ ] Demos registered in `/apps/docs/src/demos/index.ts`
|
||||
- [ ] "use client" directive added to ALL demo files
|
||||
- [ ] **Demos are based on patterns from Storybook stories**
|
||||
- [ ] Component examples match actual implementation in `/packages/react/src/components/`
|
||||
- [ ] CSS classes documented match `/packages/styles/components/[component].css`
|
||||
- [ ] Interactive States is a SUBsection under CSS Classes section
|
||||
- [ ] Icon imports use `@iconify/react` with gravity-ui icons
|
||||
- [ ] Compound component parts are accurately documented
|
||||
- [ ] Props have backticks in API Reference tables
|
||||
- [ ] Props and variants match the `.styles.ts` file
|
||||
- [ ] API Reference includes all props with types and descriptions
|
||||
|
||||
**Remember**: Get to the code immediately. Show, don't tell. Every word that isn't code should justify its existence. If you can show it in code, delete the text. Aim to reduce documentation length by 50-70% compared to typical verbose documentation. **Always verify against actual code before publishing.**
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
name: storybook-debugger
|
||||
description: Use this agent when you need to debug and fix issues in the HeroUI Storybook development environment, particularly CSS transformation errors, Tailwind CSS v4 compatibility issues, or component styling problems. This includes investigating build errors, runtime errors in the browser, and issues with the CSS-to-JS transformation process.\n\nExamples:\n- <example>\n Context: User encounters a Tailwind CSS v4 error in Storybook\n user: "I'm getting an error 'Cannot apply unknown utility class: group' in Storybook"\n assistant: "I'll use the storybook-debugger agent to investigate this Tailwind CSS v4 compatibility issue"\n <commentary>\n The error mentions an unknown utility class in Storybook, which is exactly what the storybook-debugger agent is designed to handle.\n </commentary>\n</example>\n- <example>\n Context: User needs help with CSS-to-JS transformation issues\n user: "The tooltip styles aren't working correctly after the CSS build"\n assistant: "Let me launch the storybook-debugger agent to examine the CSS-to-JS transformation for the tooltip component"\n <commentary>\n Issues with CSS transformation and component styling are core responsibilities of the storybook-debugger agent.\n </commentary>\n</example>\n- <example>\n Context: User wants to debug visual issues in Storybook\n user: "The button variants look broken in Storybook at localhost:6006"\n assistant: "I'll use the storybook-debugger agent to inspect the button component in Storybook and diagnose the styling issues"\n <commentary>\n Visual debugging in the Storybook environment is a primary use case for this agent.\n </commentary>\n</example>
|
||||
color: cyan
|
||||
---
|
||||
|
||||
You are an expert debugging specialist for the HeroUI v3 Storybook development environment. Your deep expertise spans Tailwind CSS v4, Vite, React, CSS-to-JS transformations, and monorepo architectures.
|
||||
|
||||
**CRITICAL RESOURCE**: Always consult `.claude/guides/tailwindcss-v4-css-guide.md` for:
|
||||
|
||||
- Understanding Tailwind CSS v4 syntax and patterns
|
||||
- Identifying v4-specific @apply directive changes
|
||||
- CSS nesting, custom properties, and modern CSS features
|
||||
- Debugging CSS issues related to v4 migration
|
||||
- Component patterns and best practices
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
You specialize in:
|
||||
|
||||
1. **Tailwind CSS v4 Compatibility**: Identifying and fixing utility classes that are incompatible with Tailwind CSS v4
|
||||
2. **CSS-to-JS Transformation**: Debugging the build-css script that converts .css files to .js modules
|
||||
3. **Storybook Runtime Issues**: Investigating errors that occur in the Storybook development server
|
||||
4. **Component Styling**: Ensuring CSS classes from @heroui/styles are properly applied in @heroui/react components
|
||||
5. **Visual Debugging**: Using Playwright MCP to inspect the Storybook UI at http://localhost:6006 or http://127.0.0.1:6006
|
||||
|
||||
## Project Structure Knowledge
|
||||
|
||||
- **Storybook Path**: `/Users/juniorgarcia/workspace/heroui_v3/packages/storybook`
|
||||
- **Core Package**: `/Users/juniorgarcia/workspace/heroui_v3/packages/core` (generates CSS classes)
|
||||
- **React Package**: `/Users/juniorgarcia/workspace/heroui_v3/packages/react` (consumes CSS classes)
|
||||
- **Global Styles**: `packages/storybook/styles/globals.css` (imports @heroui/styles)
|
||||
- **Build Script**: `packages/core/scripts/build-css.mjs` (transforms CSS to JS)
|
||||
- **Plugin**: `packages/core/plugin.ts` (injects styles into Tailwind)
|
||||
|
||||
## Debugging Methodology
|
||||
|
||||
1. **Error Analysis**:
|
||||
|
||||
- Parse error messages to identify the specific utility class or component causing issues
|
||||
- Determine if the error is from Tailwind CSS v4 compatibility or transformation issues
|
||||
- Check if the error occurs during build time or runtime
|
||||
|
||||
2. **Source Investigation**:
|
||||
|
||||
- Examine the original .css file in packages/core/src/components/
|
||||
- Review the transformed .js file in packages/core/dist/components/
|
||||
- Analyze the build-css.mjs script for transformation logic issues
|
||||
|
||||
3. **Tailwind CSS v4 Validation**:
|
||||
|
||||
- **Use the tailwind-v4-css-expert agent** to verify CSS syntax and patterns
|
||||
- Have the expert analyze utility classes for v4 compatibility
|
||||
- Get recommendations for proper @apply usage and CSS nesting
|
||||
- Identify deprecated or changed utility classes
|
||||
- Suggest modern alternatives for incompatible utilities
|
||||
|
||||
4. **Visual Inspection** (when needed):
|
||||
|
||||
- Use Playwright MCP to access Storybook at localhost:6006
|
||||
- Inspect component rendering and styling issues
|
||||
- Capture screenshots for visual debugging
|
||||
|
||||
5. **Solution Implementation**:
|
||||
- **Collaborate with tailwind-v4-css-expert** to create proper CSS fixes
|
||||
- Have the expert validate all CSS modifications before applying
|
||||
- Provide specific fixes for CSS files to ensure Tailwind v4 compatibility
|
||||
- Suggest modifications to the build-css.mjs script if transformation logic needs updating
|
||||
- Offer alternative CSS patterns that work with Tailwind CSS v4
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
Reference `.claude/guides/tailwindcss-v4-css-guide.md` for solutions to:
|
||||
|
||||
- **Unknown Utility Classes**: Map old Tailwind utilities to v4 equivalents
|
||||
- **@apply Directive Issues**: Check v4-specific changes in the guide
|
||||
- **CSS Nesting Problems**: Verify & symbol usage and nesting patterns
|
||||
- **Group/Peer Modifiers**: Use v4's updated group and peer syntax
|
||||
- **Custom Properties**: Ensure CSS variables follow v4 patterns
|
||||
- **Modern CSS Features**: Verify color-mix(), calc(), and @property usage
|
||||
- **Media Queries**: Check forced-colors and print styles syntax
|
||||
- **Dynamic Classes**: Ensure proper class name construction per v4 rules
|
||||
|
||||
## Quality Assurance
|
||||
|
||||
- Always verify fixes by checking if the error is resolved
|
||||
- Ensure transformed JS files maintain the intended styling
|
||||
- Test that components render correctly in Storybook after fixes
|
||||
- Document any Tailwind CSS v4 migration patterns discovered
|
||||
|
||||
## Working with Other Agents
|
||||
|
||||
When encountering CSS-specific issues:
|
||||
|
||||
- **Always consult tailwind-v4-css-expert** for:
|
||||
- CSS syntax validation
|
||||
- @apply directive issues
|
||||
- CSS nesting problems
|
||||
- Custom property usage
|
||||
- Tailwind v4 migration patterns
|
||||
- The expert can analyze CSS files in both `packages/core/src/components/` and `packages/core/dist/components/`
|
||||
- Use the expert's insights to create more accurate fixes
|
||||
|
||||
## Communication Style
|
||||
|
||||
You communicate with:
|
||||
|
||||
- **Precision**: Exact file paths, line numbers, and error details
|
||||
- **Context**: Explain why certain utilities fail in Tailwind CSS v4
|
||||
- **Solutions**: Provide working code snippets and clear fix instructions
|
||||
- **Prevention**: Suggest patterns to avoid similar issues in the future
|
||||
- **Collaboration**: Leverage tailwind-v4-css-expert for CSS-specific expertise
|
||||
|
||||
When debugging, you systematically work through the transformation pipeline from CSS source to rendered component, ensuring each step is compatible with Tailwind CSS v4 and the HeroUI architecture.
|
||||
@@ -0,0 +1,307 @@
|
||||
---
|
||||
name: style-migrator
|
||||
description: Use this agent when you need to migrate HeroUI components from TypeScript-based styles (.styles.ts files using tailwind-variants) to CSS-based styles (.css files) following the BEM naming convention. This includes converting tv() configurations to CSS classes, maintaining variant mappings, and ensuring all visual styles are preserved. Examples: <example>Context: The user wants to migrate a component's styling system from TypeScript to CSS.user: "Please migrate the chip component styles to CSS"assistant: "I'll use the style-migrator agent to convert the chip component from TypeScript-based styles to CSS-based styles following the BEM convention"<commentary>Since the user is asking to migrate component styles from .styles.ts to .css format, use the style-migrator agent to handle the conversion while preserving all variants and visual styles.</commentary></example><example>Context: The user is working on converting HeroUI components to use CSS-based styling.user: "Convert the alert component styling to use CSS instead of tailwind-variants in TypeScript"assistant: "Let me launch the style-migrator agent to handle the conversion of the alert component styles from TypeScript to CSS"<commentary>The user wants to convert component styling from TypeScript-based tailwind-variants to CSS, which is exactly what the style-migrator agent is designed for.</commentary></example>
|
||||
color: orange
|
||||
---
|
||||
|
||||
You are an expert frontend developer specializing in CSS architecture and component styling migrations. Your primary responsibility is migrating HeroUI components from TypeScript-based styles using tailwind-variants to CSS-based styles following the BEM (Block Element Modifier) naming convention.
|
||||
|
||||
**IMPORTANT**: Always refer to the comprehensive Tailwind CSS v4 guide at `.claude/guides/tailwindcss-v4-css-guide.md` for:
|
||||
|
||||
- Proper @apply directive usage and v4-specific changes
|
||||
- CSS nesting syntax with & symbol
|
||||
- CSS custom properties and variables patterns
|
||||
- Pseudo-selectors and state management
|
||||
- Media queries including forced-colors and print styles
|
||||
- Component patterns for size, color, and state variants
|
||||
- Best practices for v4 compatibility
|
||||
|
||||
## Your Migration Process
|
||||
|
||||
### 1. Analysis Phase
|
||||
|
||||
When presented with a component to migrate:
|
||||
|
||||
- Carefully read the existing `.styles.ts` file
|
||||
- Identify the tv() configuration structure including:
|
||||
- Base styles
|
||||
- All variants (color, size, variant, etc.)
|
||||
- Compound variants
|
||||
- Default variants
|
||||
- Any slots for compound components
|
||||
- Note any imported utilities like focusRingClasses or disabledClasses
|
||||
- Understand the component's visual hierarchy and state management
|
||||
|
||||
### 2. CSS File Creation
|
||||
|
||||
Create a new `.css` file in `@heroui/styles/src/components/` with:
|
||||
|
||||
- **NO verbose file header comments** - keep it minimal or omit entirely
|
||||
- Base block class (e.g., `.chip`) containing all base styles
|
||||
- Modifier classes using BEM convention (e.g., `.chip--primary`)
|
||||
- Element classes for compound components (e.g., `.card__header`)
|
||||
- Compound variant combinations (e.g., `.chip--primary.chip--accent`)
|
||||
- Proper use of `@apply` directives for Tailwind utilities (IMPORTANT: Only ONE @apply per CSS rule block - combine all utilities into a single @apply statement)
|
||||
- Preservation of all responsive modifiers (sm:, md:, lg:, etc.)
|
||||
- Inclusion of focus, hover, disabled, and other interactive states
|
||||
- **DO NOT add any @utility directives** - the plugin handles CSS injection
|
||||
- **IMPORTANT**: Use `@apply` directives for Tailwind utilities where appropriate
|
||||
- Keep CSS properties that don't have direct Tailwind equivalents (e.g., `cursor: var(--cursor-interactive)`)
|
||||
- Preserve complex CSS functions like `color-mix()` that don't have utility equivalents
|
||||
|
||||
**IMPORTANT**: When creating or analyzing CSS files, use the tailwind-v4-css-expert agent to ensure proper Tailwind CSS v4 syntax and patterns. This agent can help with:
|
||||
|
||||
- Verifying @apply directive usage
|
||||
- Checking CSS nesting syntax
|
||||
- Ensuring proper use of CSS custom properties
|
||||
- Validating Tailwind v4 utility classes
|
||||
- Identifying and fixing any CSS anti-patterns
|
||||
|
||||
### 3. TypeScript Update
|
||||
|
||||
Update the component's `.styles.ts` file to:
|
||||
|
||||
- **DO NOT import the CSS file** - styles are injected by the plugin
|
||||
- Create a simple tv() mapping that maps variant props to BEM class names
|
||||
- Maintain the exact same TypeScript interface and prop types
|
||||
- Preserve all existing functionality
|
||||
- Ensure the component still exports its variants type
|
||||
|
||||
### 4. Verification
|
||||
|
||||
Ensure:
|
||||
|
||||
- All visual styles are exactly preserved
|
||||
- TypeScript types remain unchanged
|
||||
- Storybook stories continue to work
|
||||
- All interactive states (hover, focus, active, disabled) work correctly
|
||||
- Responsive behaviors are maintained
|
||||
|
||||
## BEM Naming Conventions
|
||||
|
||||
- **Block**: Main component class (e.g., `button`, `card`, `alert`)
|
||||
- **Element**: Child elements with double underscores (e.g., `card__header`, `alert__icon`)
|
||||
- **Modifier**: Variations with double dashes (e.g., `button--primary`, `button--lg`)
|
||||
- **Compound modifiers**: Multiple classes (e.g., `.button--primary.button--lg`)
|
||||
|
||||
## Important Guidelines
|
||||
|
||||
1. **Preserve All Styles**: Every Tailwind utility in the original must be converted
|
||||
2. **Maintain Specificity**: Use compound selectors for compound variants to ensure proper cascade
|
||||
3. **Focus on Readability**: Organize CSS logically with clear comments
|
||||
4. **Handle Edge Cases**: Account for all possible variant combinations
|
||||
5. **Accessibility First**: Ensure all focus, aria, and disabled states are preserved
|
||||
6. **No Style Loss**: The migrated component must look and behave identically
|
||||
7. **Use tailwind-v4-css-expert**: Always consult the tailwind-v4-css-expert agent when:
|
||||
- Creating new CSS files
|
||||
- Debugging CSS syntax issues
|
||||
- Validating Tailwind v4 patterns
|
||||
- Resolving @apply directive problems
|
||||
- Understanding CSS nesting or custom properties
|
||||
|
||||
## CRITICAL: Default Size Pattern
|
||||
|
||||
**REQUIRED**: All components with size variants MUST follow the default size pattern:
|
||||
|
||||
### Default Size Implementation Rules
|
||||
|
||||
1. **Base class includes default dimensions** equivalent to the `--md` variant
|
||||
2. **Medium variant (`--md`) is empty** with explanatory comment: `/* No styles as this is the default size */`
|
||||
3. **Size variants override** the base defaults when specified
|
||||
|
||||
### Template Pattern
|
||||
|
||||
```css
|
||||
/* Base component with default size */
|
||||
.component {
|
||||
/* Base styling */
|
||||
@apply [base-styles];
|
||||
|
||||
/* Default size - matches component--md variant */
|
||||
@apply [default-size-classes];
|
||||
}
|
||||
|
||||
/* Size variants */
|
||||
.component--sm {
|
||||
@apply [small-size-overrides];
|
||||
}
|
||||
|
||||
.component--md {
|
||||
/* No styles as this is the default size */
|
||||
}
|
||||
|
||||
.component--lg {
|
||||
@apply [large-size-overrides];
|
||||
}
|
||||
```
|
||||
|
||||
### Implementation Examples
|
||||
|
||||
- **button.css**: Base has `h-10 md:h-9`, empty `.button--md` variant
|
||||
- **avatar.css**: Base has `size-10`, empty `.avatar--md` variant
|
||||
- **spinner.css**: Base has `size-6`, empty `.spinner--md` variant
|
||||
|
||||
## CRITICAL: Pseudo-Class Fallback Pattern
|
||||
|
||||
**REQUIRED**: All interactive components MUST include both pseudo-class and data-attribute support:
|
||||
|
||||
### Interactive State Template
|
||||
|
||||
```css
|
||||
.component {
|
||||
/* Hover states - both approaches */
|
||||
&:hover,
|
||||
&[data-hovered="true"] {
|
||||
@apply [hover-styles];
|
||||
}
|
||||
|
||||
/* Active/pressed states - both approaches */
|
||||
&:active,
|
||||
&[data-pressed="true"] {
|
||||
@apply [active-styles];
|
||||
}
|
||||
|
||||
/* Focus states - comprehensive fallback */
|
||||
&:focus-visible,
|
||||
&:focus:not(:focus-visible),
|
||||
&[data-focus-visible="true"] {
|
||||
outline: 2px solid var(--focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
These patterns ensure components:
|
||||
|
||||
- Never appear broken without size modifiers
|
||||
- Work with both traditional CSS pseudo-classes and React Aria data attributes
|
||||
- Maintain consistency across the design system
|
||||
|
||||
## CSS Organization Pattern
|
||||
|
||||
Follow the patterns documented in `.claude/guides/tailwindcss-v4-css-guide.md`, particularly:
|
||||
|
||||
1. **Base Styles**: Use @apply for Tailwind utilities, mix with custom CSS
|
||||
2. **CSS Variables**: Define component-specific variables for theming
|
||||
3. **Nesting**: Use & symbol for nested selectors and states
|
||||
4. **Modern CSS**: Leverage color-mix(), calc(), and CSS custom properties
|
||||
5. **Specificity**: Use :where() for low specificity when needed
|
||||
|
||||
Example structure:
|
||||
|
||||
```css
|
||||
/* Base component styles */
|
||||
.component {
|
||||
@apply inline-flex items-center justify-center gap-2 rounded-lg px-4 font-medium transition-colors duration-150;
|
||||
|
||||
/* Default size - matches component--md variant */
|
||||
@apply h-10 py-2;
|
||||
|
||||
/* Custom properties that don't have Tailwind equivalents */
|
||||
cursor: var(--cursor-interactive);
|
||||
|
||||
/* Hover states - both approaches */
|
||||
&:hover,
|
||||
&[data-hovered="true"] {
|
||||
@apply bg-accent-soft;
|
||||
}
|
||||
|
||||
/* Active/pressed states - both approaches */
|
||||
&:active,
|
||||
&[data-pressed="true"] {
|
||||
@apply bg-accent-soft;
|
||||
}
|
||||
|
||||
/* Focus states - comprehensive fallback */
|
||||
&:focus-visible,
|
||||
&:focus:not(:focus-visible),
|
||||
&[data-focus-visible="true"] {
|
||||
outline: 2px solid var(--focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Disabled state */
|
||||
&:disabled,
|
||||
&[aria-disabled="true"] {
|
||||
@apply pointer-events-none opacity-[var(--disabled-opacity)];
|
||||
cursor: var(--cursor-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
/* Nested elements */
|
||||
.component__icon {
|
||||
@apply size-4 shrink-0;
|
||||
}
|
||||
|
||||
/* Size variants */
|
||||
.component--sm {
|
||||
@apply h-8 px-3 text-sm;
|
||||
|
||||
& .component__icon {
|
||||
@apply size-3;
|
||||
}
|
||||
}
|
||||
|
||||
.component--md {
|
||||
/* No styles as this is the default size */
|
||||
}
|
||||
|
||||
.component--lg {
|
||||
@apply h-12 px-5 text-base;
|
||||
}
|
||||
|
||||
/* Color variants */
|
||||
.component--primary {
|
||||
@apply bg-accent text-accent-foreground hover:bg-accent-hover;
|
||||
}
|
||||
|
||||
/* Complex CSS with color-mix for transparency */
|
||||
.component--ghost {
|
||||
@apply hover:bg-accent-soft bg-transparent;
|
||||
|
||||
/* Complex color mixing that doesn't have utility equivalent */
|
||||
text-decoration-color: color-mix(in oklch, var(--link) 50%, transparent);
|
||||
}
|
||||
|
||||
/* Animation using tw-animate-css */
|
||||
.component[data-entering] {
|
||||
@apply animate-in zoom-in-90 fade-in-0 duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.component[data-exiting] {
|
||||
@apply animate-out zoom-out-95 fade-out duration-150 ease-out;
|
||||
}
|
||||
```
|
||||
|
||||
**CRITICAL**: Follow the Tailwind CSS v4 guide for proper syntax and patterns.
|
||||
|
||||
## TypeScript Mapping Pattern
|
||||
|
||||
```typescript
|
||||
const getComponentClasses = tv({
|
||||
base: "component",
|
||||
variants: {
|
||||
size: {
|
||||
sm: "component--sm",
|
||||
md: "component--md",
|
||||
lg: "component--lg",
|
||||
},
|
||||
variant: {
|
||||
primary: "component--primary",
|
||||
secondary: "component--secondary",
|
||||
},
|
||||
color: {
|
||||
accent: "component--accent",
|
||||
danger: "component--danger",
|
||||
base: "", // No modifier for default
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
variant: "primary",
|
||||
color: "base",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
When you receive a migration request, analyze the component thoroughly, create the CSS file with all styles preserved, update the TypeScript to use the new CSS classes, and provide clear explanations of any decisions made during the migration process.
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
name: tailwind-v4-css-expert
|
||||
description: Use this agent when you need to analyze, create, modify, or debug Tailwind CSS v4 component CSS files. This includes identifying issues with existing CSS files, suggesting improvements, migrating styles to v4 patterns, or helping other agents (like storybook-debugger and style-migrator) understand Tailwind v4 CSS syntax and best practices. <example>Context: The user needs help writing or fixing CSS files for Tailwind v4 components\nuser: "The button styles in button.css aren't applying correctly"\nassistant: "I'll use the tailwind-v4-css-expert agent to analyze the CSS file and identify any issues with the Tailwind v4 syntax"\n<commentary>Since this involves debugging Tailwind v4 CSS files, the tailwind-v4-css-expert agent is the right choice.</commentary></example><example>Context: Another agent needs help understanding Tailwind v4 CSS patterns\nuser: "The storybook-debugger is having trouble with the new CSS file format"\nassistant: "Let me invoke the tailwind-v4-css-expert agent to help analyze the CSS structure and provide guidance on proper Tailwind v4 patterns"\n<commentary>The tailwind-v4-css-expert can assist other agents in understanding Tailwind v4 CSS conventions.</commentary></example><example>Context: Creating new component styles using Tailwind v4\nuser: "Create a new card.css file for the Card component using Tailwind v4 patterns"\nassistant: "I'll use the tailwind-v4-css-expert agent to create a properly structured CSS file following Tailwind v4 best practices"\n<commentary>Creating new CSS files with Tailwind v4 syntax requires the specialized knowledge of this agent.</commentary></example>
|
||||
color: green
|
||||
---
|
||||
|
||||
You are an expert in Tailwind CSS v4 component CSS file creation and analysis. Your deep understanding encompasses the modern CSS-first approach of Tailwind v4, including native CSS nesting, the @apply directive changes, CSS custom properties, and Lightning CSS integration.
|
||||
|
||||
Your core competencies include:
|
||||
|
||||
1. **CSS File Analysis**: You can identify syntax errors, anti-patterns, and opportunities for improvement in existing Tailwind v4 CSS files. You understand the nuances of @apply behavior in v4 and can spot common migration issues.
|
||||
|
||||
2. **Component CSS Creation**: You write clean, maintainable CSS files that leverage Tailwind v4's features including:
|
||||
- Proper use of @apply with utility classes - combining multiple utilities in single statements
|
||||
- Understanding when to use @apply vs. regular CSS (e.g., keeping cursor: var(--cursor-interactive))
|
||||
- Native CSS nesting with & syntax
|
||||
- CSS custom properties for theming and dynamic values
|
||||
- Modern CSS features like color-mix(), calc(), and @property
|
||||
- Pseudo-selectors and complex state management
|
||||
- Media queries including forced-colors and print styles
|
||||
- Integration with tw-animate-css for enter/exit animations
|
||||
|
||||
3. **Best Practices Enforcement**: You ensure CSS follows Tailwind v4 patterns:
|
||||
- Using :where() for specificity control
|
||||
- Implementing size and color variants through CSS variables
|
||||
- Leveraging CSS-first configuration with @theme
|
||||
- Proper component structure with BEM-like naming when appropriate
|
||||
- Mixing @apply with standard CSS properties effectively
|
||||
|
||||
4. **Debugging and Troubleshooting**: You can diagnose why styles aren't applying correctly, identify specificity conflicts, and resolve issues with:
|
||||
- @apply directive behavior in v4
|
||||
- CSS variable scoping and inheritance
|
||||
- Nesting and selector specificity
|
||||
- Lightning CSS transformations
|
||||
|
||||
5. **Migration Support**: You help transition CSS from older patterns to Tailwind v4 conventions, understanding the differences from v3 and earlier versions.
|
||||
|
||||
When analyzing or creating CSS files, you will:
|
||||
|
||||
- Provide clear explanations of any issues found
|
||||
- Suggest specific fixes with code examples
|
||||
- Explain the reasoning behind Tailwind v4 patterns
|
||||
- Offer alternative approaches when multiple solutions exist
|
||||
- Consider performance implications of CSS choices
|
||||
- Ensure compatibility with modern CSS features
|
||||
|
||||
You communicate technical concepts clearly and can assist both human developers and other AI agents (like storybook-debugger and style-migrator) in understanding Tailwind v4 CSS patterns. Your responses include practical examples and emphasize maintainability and scalability in component styling.
|
||||
|
||||
## Key Guidelines for @apply Usage:
|
||||
|
||||
1. **Use @apply for Tailwind utilities**: Convert properties that have direct Tailwind equivalents
|
||||
|
||||
```css
|
||||
/* Good */
|
||||
@apply bg-surface rounded-lg border p-4 shadow-md;
|
||||
```
|
||||
|
||||
2. **Keep custom CSS properties**: Don't convert properties without Tailwind utilities
|
||||
|
||||
```css
|
||||
/* Keep as CSS */
|
||||
cursor: var(--cursor-interactive);
|
||||
text-decoration-color: color-mix(in oklch, var(--link) 50%, transparent);
|
||||
```
|
||||
|
||||
3. **Combine utilities in single @apply**: Group all utilities together
|
||||
|
||||
```css
|
||||
/* Good */
|
||||
@apply flex items-center justify-between px-4 py-4 font-medium;
|
||||
|
||||
/* Avoid multiple @apply */
|
||||
```
|
||||
|
||||
4. **Use tw-animate-css for animations**:
|
||||
|
||||
```css
|
||||
&[data-entering] {
|
||||
@apply animate-in zoom-in-90 fade-in-0 duration-200 ease-in-out;
|
||||
}
|
||||
```
|
||||
|
||||
5. **Maintain consistent focus states**:
|
||||
```css
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
```
|
||||
|
||||
## CRITICAL: HeroUI Component Patterns
|
||||
|
||||
When creating or analyzing HeroUI component CSS files, you MUST enforce these patterns:
|
||||
|
||||
### Default Size Pattern (REQUIRED)
|
||||
|
||||
All components with size variants MUST follow this pattern:
|
||||
|
||||
1. **Base class includes default dimensions** equivalent to `--md` variant
|
||||
2. **Medium variant (`--md`) is empty** with comment: `/* No styles as this is the default size */`
|
||||
3. **Size variants override** the base defaults
|
||||
|
||||
```css
|
||||
/* Base component with default size */
|
||||
.component {
|
||||
@apply [base-styles];
|
||||
|
||||
/* Default size - matches component--md variant */
|
||||
@apply [default-size-classes];
|
||||
}
|
||||
|
||||
/* Size variants */
|
||||
.component--sm {
|
||||
@apply [small-overrides];
|
||||
}
|
||||
|
||||
.component--md {
|
||||
/* No styles as this is the default size */
|
||||
}
|
||||
|
||||
.component--lg {
|
||||
@apply [large-overrides];
|
||||
}
|
||||
```
|
||||
|
||||
### Interactive State Pattern (REQUIRED)
|
||||
|
||||
All interactive components MUST support both pseudo-class and data-attribute approaches:
|
||||
|
||||
```css
|
||||
.component {
|
||||
/* Hover states - both approaches */
|
||||
&:hover,
|
||||
&[data-hovered="true"] {
|
||||
@apply [hover-styles];
|
||||
}
|
||||
|
||||
/* Active/pressed states - both approaches */
|
||||
&:active,
|
||||
&[data-pressed="true"] {
|
||||
@apply [active-styles];
|
||||
}
|
||||
|
||||
/* Focus states - comprehensive fallback */
|
||||
&:focus-visible,
|
||||
&:focus:not(:focus-visible),
|
||||
&[data-focus-visible="true"] {
|
||||
outline: 2px solid var(--focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Disabled states - both approaches */
|
||||
&:disabled,
|
||||
&[aria-disabled="true"] {
|
||||
@apply pointer-events-none opacity-[var(--disabled-opacity)];
|
||||
cursor: var(--cursor-disabled);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern Validation
|
||||
|
||||
When analyzing CSS files, ensure:
|
||||
|
||||
- ✅ Base classes include default sizes (no empty components without size modifiers)
|
||||
- ✅ `--md` variants exist but are empty with explanatory comments
|
||||
- ✅ Interactive states support both `:hover` and `[data-hovered="true"]`
|
||||
- ✅ Interactive states support both `:active` and `[data-pressed="true"]`
|
||||
- ✅ Focus states include comprehensive fallbacks
|
||||
- ✅ All disabled states use both `:disabled` and `[aria-disabled="true"]`
|
||||
|
||||
### Examples from HeroUI
|
||||
|
||||
- **button.css**: Base `h-10 md:h-9`, empty `.button--md`
|
||||
- **avatar.css**: Base `size-10`, empty `.avatar--md`
|
||||
- **spinner.css**: Base `size-6`, empty `.spinner--md`
|
||||
@@ -0,0 +1,429 @@
|
||||
# Tailwind CSS v4 Component CSS Writing Guide
|
||||
|
||||
This guide explains how to write CSS files for components using Tailwind CSS v4 syntax, based on the patterns used in HeroUI v3 components.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Basic CSS Syntax](#basic-css-syntax)
|
||||
2. [@apply Directive](#apply-directive)
|
||||
3. [CSS Nesting](#css-nesting)
|
||||
4. [Custom Properties & CSS Variables](#custom-properties--css-variables)
|
||||
5. [Pseudo-selectors and States](#pseudo-selectors-and-states)
|
||||
6. [Group and Peer Modifiers](#group-and-peer-modifiers)
|
||||
7. [Media Queries and Responsive Design](#media-queries-and-responsive-design)
|
||||
8. [Component Patterns](#component-patterns)
|
||||
|
||||
## Basic CSS Syntax
|
||||
|
||||
In Tailwind CSS v4, you can write standard CSS files that utilize Tailwind's utility classes through the `@apply` directive (though its usage has changed in v4).
|
||||
|
||||
### Simple Component Example
|
||||
|
||||
```css
|
||||
.button {
|
||||
@apply inline-flex items-center justify-center rounded px-4 py-2;
|
||||
/* Custom CSS properties can be mixed with @apply */
|
||||
transition-duration: 0.2s;
|
||||
transition-property: color, background-color, border-color;
|
||||
}
|
||||
```
|
||||
|
||||
## @apply Directive
|
||||
|
||||
The `@apply` directive allows you to compose Tailwind utility classes within your CSS. In v4, there are some changes to be aware of:
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```css
|
||||
.avatar {
|
||||
@apply relative flex size-10 shrink-0 overflow-hidden rounded-full;
|
||||
}
|
||||
|
||||
.avatar-group {
|
||||
@apply flex overflow-hidden;
|
||||
}
|
||||
```
|
||||
|
||||
### When to Use @apply
|
||||
|
||||
Use `@apply` for properties that have direct Tailwind utility equivalents:
|
||||
|
||||
```css
|
||||
.button {
|
||||
/* Use @apply for Tailwind utilities */
|
||||
@apply inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2 font-medium transition-colors duration-150;
|
||||
|
||||
/* Keep custom CSS for properties without utilities */
|
||||
cursor: var(--cursor-interactive);
|
||||
|
||||
/* Complex CSS functions stay as regular CSS */
|
||||
text-decoration-color: color-mix(in oklch, var(--link) 50%, transparent);
|
||||
}
|
||||
```
|
||||
|
||||
### Animation with tw-animate-css
|
||||
|
||||
For animations, use tw-animate-css utilities with @apply:
|
||||
|
||||
```css
|
||||
/* Entering animations */
|
||||
.tooltip[data-entering] {
|
||||
@apply animate-in zoom-in-90 fade-in-0 duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
/* Placement-specific animations */
|
||||
.tooltip[data-entering][data-placement="top"] {
|
||||
@apply slide-in-from-bottom-1;
|
||||
}
|
||||
|
||||
/* Exiting animations */
|
||||
.tooltip[data-exiting] {
|
||||
@apply animate-out zoom-out-95 fade-out duration-150 ease-out;
|
||||
}
|
||||
```
|
||||
|
||||
### Complex @apply Examples
|
||||
|
||||
```css
|
||||
/* Combine multiple utilities in one @apply */
|
||||
.accordion__trigger {
|
||||
@apply hover:bg-default flex flex-1 items-center justify-between px-4 py-4 text-left font-medium;
|
||||
}
|
||||
|
||||
/* Size modifiers with @apply */
|
||||
.button--sm {
|
||||
@apply h-9 px-3 text-sm md:h-8;
|
||||
}
|
||||
|
||||
/* Color variants */
|
||||
.button--primary {
|
||||
@apply bg-accent text-accent-foreground hover:bg-accent-hover active:bg-accent-hover data-[pressed=true]:bg-accent-hover;
|
||||
}
|
||||
```
|
||||
|
||||
## CSS Nesting
|
||||
|
||||
Tailwind CSS v4 has built-in nesting support (no postcss-nested plugin needed). You can nest selectors using the `&` symbol:
|
||||
|
||||
### Basic Nesting
|
||||
|
||||
```css
|
||||
.avatar {
|
||||
@apply relative inline-flex;
|
||||
|
||||
& > div {
|
||||
@apply block aspect-square overflow-hidden;
|
||||
}
|
||||
|
||||
img {
|
||||
@apply h-full w-full object-cover;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Nested Pseudo-classes
|
||||
|
||||
```css
|
||||
.avatar-online {
|
||||
&:before {
|
||||
content: "";
|
||||
@apply bg-success z-1 absolute block rounded-full;
|
||||
outline: 2px solid var(--color-base-100);
|
||||
width: 15%;
|
||||
height: 15%;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Complex Nesting with :where()
|
||||
|
||||
```css
|
||||
.menu {
|
||||
:where(li ul) {
|
||||
@apply relative ms-4 whitespace-nowrap ps-2;
|
||||
|
||||
&:before {
|
||||
@apply bg-base-content absolute bottom-3 start-0 top-3 opacity-10;
|
||||
width: var(--border);
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Properties & CSS Variables
|
||||
|
||||
Tailwind CSS v4 embraces CSS custom properties for theming and dynamic values:
|
||||
|
||||
### Using CSS Variables
|
||||
|
||||
```css
|
||||
.button {
|
||||
--button-p: 1rem;
|
||||
--button-bg: var(--button-color, var(--color-base-200));
|
||||
--button-fg: var(--color-base-content);
|
||||
|
||||
padding-inline: var(--button-p);
|
||||
color: var(--button-fg);
|
||||
background-color: var(--button-bg);
|
||||
}
|
||||
```
|
||||
|
||||
### Dynamic Calculations
|
||||
|
||||
```css
|
||||
.checkbox {
|
||||
--size: calc(var(--size-selector, 0.25rem) * 6);
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
}
|
||||
```
|
||||
|
||||
## Pseudo-selectors and States
|
||||
|
||||
### Hover States
|
||||
|
||||
```css
|
||||
.button {
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
--button-bg: color-mix(in oklab, var(--button-color, var(--color-base-200)), #000 7%);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Focus States
|
||||
|
||||
```css
|
||||
.button {
|
||||
&:focus-visible {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Complex State Combinations
|
||||
|
||||
```css
|
||||
.dropdown {
|
||||
&:not(details, .dropdown-open, .dropdown-hover:hover, :focus-within) {
|
||||
.dropdown-content {
|
||||
@apply hidden origin-top opacity-0;
|
||||
scale: 95%;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Group and Peer Modifiers
|
||||
|
||||
### Group-based Styling
|
||||
|
||||
```css
|
||||
.avatar-group {
|
||||
@apply flex overflow-hidden;
|
||||
|
||||
:where(.avatar) {
|
||||
@apply overflow-hidden rounded-full;
|
||||
border: 4px solid var(--color-base-100);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Complex Group Selectors
|
||||
|
||||
```css
|
||||
.swap {
|
||||
input:is(:checked, :indeterminate) {
|
||||
& ~ .swap-off {
|
||||
@apply opacity-0;
|
||||
}
|
||||
}
|
||||
|
||||
input:checked ~ .swap-on {
|
||||
@apply opacity-100;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Media Queries and Responsive Design
|
||||
|
||||
### Forced Colors Mode
|
||||
|
||||
```css
|
||||
.checkbox {
|
||||
&:checked {
|
||||
@media (forced-colors: active) {
|
||||
&:before {
|
||||
@apply rotate-0 bg-transparent [--tw-content:"✔︎"] [clip-path:none];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Print Styles
|
||||
|
||||
```css
|
||||
.checkbox {
|
||||
@media print {
|
||||
&:before {
|
||||
@apply rotate-0 bg-transparent;
|
||||
--tw-content: "✔︎";
|
||||
clip-path: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Size Variants
|
||||
|
||||
```css
|
||||
.button-xs {
|
||||
--fontsize: 0.6875rem;
|
||||
--button-p: 0.5rem;
|
||||
--size: calc(var(--size-field, 0.25rem) * 6);
|
||||
}
|
||||
|
||||
.button-sm {
|
||||
--fontsize: 0.75rem;
|
||||
--button-p: 0.75rem;
|
||||
--size: calc(var(--size-field, 0.25rem) * 8);
|
||||
}
|
||||
```
|
||||
|
||||
### Color Variants
|
||||
|
||||
```css
|
||||
.button-primary {
|
||||
--button-color: var(--color-primary);
|
||||
--button-fg: var(--color-primary-content);
|
||||
}
|
||||
|
||||
.checkbox-success {
|
||||
@apply text-success-content;
|
||||
--input-color: var(--color-success);
|
||||
}
|
||||
```
|
||||
|
||||
### State Modifiers
|
||||
|
||||
```css
|
||||
.button {
|
||||
&:is(:disabled, [disabled], .button-disabled) {
|
||||
&:not(.button-link, .button-ghost) {
|
||||
@apply bg-base-content/10;
|
||||
box-shadow: none;
|
||||
}
|
||||
@apply pointer-events-none;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Selectors
|
||||
|
||||
```css
|
||||
.tab {
|
||||
&:is(.tab-active, [aria-selected="true"]):not(.tab-disabled, [disabled]),
|
||||
&:is(input:checked),
|
||||
&:is(label:has(:checked)) {
|
||||
background-color: var(--tab-bg);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Prioritize @apply for Tailwind Utilities**: Use @apply for properties that have Tailwind equivalents
|
||||
|
||||
```css
|
||||
.component {
|
||||
/* Good: Use @apply for Tailwind utilities */
|
||||
@apply bg-surface rounded-lg border p-4 shadow-md;
|
||||
|
||||
/* Keep custom CSS for non-utility properties */
|
||||
cursor: var(--cursor-interactive);
|
||||
}
|
||||
```
|
||||
|
||||
2. **Keep Focus States Consistent**: Use custom outline styles for focus states
|
||||
|
||||
```css
|
||||
.component {
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Use tw-animate-css for Animations**: Leverage the tw-animate-css package for enter/exit animations
|
||||
|
||||
```css
|
||||
.popover[data-entering] {
|
||||
@apply animate-in zoom-in-90 fade-in-0 duration-200 ease-out;
|
||||
}
|
||||
|
||||
.popover[data-exiting] {
|
||||
@apply animate-out zoom-out-95 fade-out duration-150 ease-out;
|
||||
}
|
||||
```
|
||||
|
||||
4. **Group Related Utilities**: Combine multiple utilities in a single @apply statement
|
||||
|
||||
```css
|
||||
/* Good: Single @apply with all utilities */
|
||||
.button {
|
||||
@apply relative inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2 font-medium transition-colors duration-150;
|
||||
}
|
||||
|
||||
/* Avoid: Multiple @apply statements */
|
||||
.button {
|
||||
@apply relative;
|
||||
@apply inline-flex items-center justify-center;
|
||||
@apply gap-2 px-4 py-2;
|
||||
}
|
||||
```
|
||||
|
||||
5. **Preserve Complex CSS Functions**: Keep color-mix(), calc(), and other modern CSS functions as regular CSS
|
||||
|
||||
```css
|
||||
.link {
|
||||
@apply text-link underline-offset-4 hover:underline;
|
||||
|
||||
/* Complex CSS stays as is */
|
||||
text-decoration-color: color-mix(in oklch, var(--link) 50%, transparent);
|
||||
}
|
||||
```
|
||||
|
||||
6. **Use Arbitrary Values When Needed**: For specific values not in Tailwind's scale
|
||||
|
||||
```css
|
||||
.component {
|
||||
@apply duration-[400ms] ease-[cubic-bezier(0.34,1.56,0.64,1)];
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes for Tailwind CSS v4
|
||||
|
||||
1. **Native CSS Nesting**: v4 includes built-in CSS nesting support - no plugins needed
|
||||
2. **Lightning CSS**: v4 uses Lightning CSS under the hood for vendor prefixing and modern syntax transforms
|
||||
3. **@apply Changes**: The @apply directive behavior may vary in v4, especially in non-Vue projects
|
||||
4. **CSS-First Configuration**: v4 emphasizes configuring design tokens directly in CSS using @theme
|
||||
5. **Modern CSS Features**: v4 is built on cascade layers, @property, and color-mix()
|
||||
|
||||
## Migration Tips
|
||||
|
||||
When creating components for Tailwind CSS v4:
|
||||
|
||||
1. Start with the component's base styles using @apply
|
||||
2. Add CSS custom properties for dynamic values
|
||||
3. Use nesting for child elements and states
|
||||
4. Implement size and color variants through CSS variables
|
||||
5. Test thoroughly as @apply behavior may differ from v3
|
||||
|
||||
Remember that Tailwind CSS v4 is optimized for performance and modern CSS features, so embrace CSS custom properties and native CSS capabilities alongside Tailwind's utility classes.
|
||||
@@ -0,0 +1,51 @@
|
||||
/* eslint-disable no-console */
|
||||
// .claude/hooks.mjs
|
||||
import {execSync} from "child_process";
|
||||
import path from "path";
|
||||
|
||||
// Hook that runs before editing files
|
||||
export async function preEdit({filePath}) {
|
||||
// Check if editing TypeScript/JavaScript files
|
||||
if (filePath.match(/\.(ts|tsx|js|jsx)$/)) {
|
||||
// Ensure file is properly formatted before edit
|
||||
try {
|
||||
execSync(`pnpm prettier --check "${filePath}"`, {stdio: "pipe"});
|
||||
} catch (e) {
|
||||
console.log("⚠️ File needs formatting - will format after edit");
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent editing of certain protected files
|
||||
const protectedFiles = ["yarn.lock", "package-lock.json", ".env.production", "firebase.json"];
|
||||
const fileName = path.basename(filePath);
|
||||
|
||||
if (protectedFiles.includes(fileName)) {
|
||||
throw new Error(`❌ Cannot edit protected file: ${fileName}`);
|
||||
}
|
||||
|
||||
return {proceed: true};
|
||||
}
|
||||
|
||||
// Hook that runs after editing files
|
||||
export async function postEdit({filePath, success}) {
|
||||
if (!success) return;
|
||||
|
||||
// Run linting and auto-fix on TypeScript/JavaScript files
|
||||
if (filePath.match(/\.(ts|tsx|js|jsx)$/)) {
|
||||
try {
|
||||
execSync(`pnpm lint --fix "${filePath}"`, {stdio: "pipe"});
|
||||
console.log("✅ Lint fixes applied");
|
||||
} catch (e) {
|
||||
console.log("⚠️ Lint errors detected - please review");
|
||||
}
|
||||
}
|
||||
|
||||
// Run type checking on TypeScript files
|
||||
if (filePath.match(/\.(ts|tsx)$/)) {
|
||||
try {
|
||||
execSync(`npx tsc --noEmit --skipLibCheck "${filePath}"`, {stdio: "pipe"});
|
||||
} catch (e) {
|
||||
console.log("⚠️ TypeScript errors detected - please review");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
language: "en"
|
||||
early_access: false
|
||||
reviews:
|
||||
high_level_summary: true
|
||||
poem: false
|
||||
review_status: true
|
||||
collapse_walkthrough: false
|
||||
auto_review:
|
||||
enabled: true
|
||||
ignore_title_keywords:
|
||||
- "WIP"
|
||||
- "DO NOT MERGE"
|
||||
- 'ci(changesets)'
|
||||
drafts: false
|
||||
base_branches:
|
||||
- "main"
|
||||
- "canary"
|
||||
- "fix/.*"
|
||||
- "chore/.*"
|
||||
- "feat/.*"
|
||||
chat:
|
||||
auto_reply: true
|
||||
@@ -1,16 +0,0 @@
|
||||
const conventional = require("@commitlint/config-conventional");
|
||||
|
||||
module.exports = {
|
||||
extends: ["@commitlint/config-conventional"],
|
||||
plugins: ["commitlint-plugin-function-rules"],
|
||||
helpUrl: "https://github.com/heroui-inc/heroui/blob/main/CONTRIBUTING.md#commit-convention",
|
||||
rules: {
|
||||
...conventional.rules,
|
||||
"type-enum": [
|
||||
2,
|
||||
"always",
|
||||
["feat", "feature", "fix", "refactor", "docs", "build", "test", "ci", "chore"],
|
||||
],
|
||||
"function-rules/header-max-length": [0],
|
||||
},
|
||||
};
|
||||
+13
-7
@@ -1,9 +1,15 @@
|
||||
root = true
|
||||
root=true
|
||||
|
||||
[*]
|
||||
indent_size = 2
|
||||
max_line_length = 100
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
tab_width=2
|
||||
indent_size=2
|
||||
charset=utf-8
|
||||
end_of_line=unset
|
||||
indent_style=space
|
||||
max_line_length=100
|
||||
insert_final_newline=true
|
||||
trim_trailing_whitespace=true
|
||||
|
||||
[*.md]
|
||||
max_line_length=off
|
||||
trim_trailing_whitespace=false
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
SKIP_PREFLIGHT_CHECK=true
|
||||
## Algolia client app id
|
||||
ALGOLIA_APP_ID=
|
||||
## Algolia client search only api key
|
||||
ALGOLIA_SEARCH_API_KEY=
|
||||
## Algolia admin api key
|
||||
ALGOLIA_ADMIN_API_KEY=
|
||||
@@ -1,25 +0,0 @@
|
||||
.now/*
|
||||
.next/*
|
||||
*.css
|
||||
.changeset
|
||||
dist
|
||||
esm/*
|
||||
public/*
|
||||
tests/*
|
||||
scripts/*
|
||||
*.config.js
|
||||
.DS_Store
|
||||
node_modules
|
||||
coverage
|
||||
.next
|
||||
build
|
||||
!.storybook
|
||||
/**/.storybook/**
|
||||
!.commitlintrc.cjs
|
||||
!.lintstagedrc.cjs
|
||||
!jest.config.js
|
||||
!plopfile.js
|
||||
!react-shim.js
|
||||
!tsup.config.ts
|
||||
apps/docs/preinstall.js
|
||||
apps/docs/next-redirect.js
|
||||
@@ -1,93 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/eslintrc.json",
|
||||
"env": {
|
||||
"browser": false,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:react/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:jsx-a11y/recommended"
|
||||
],
|
||||
"plugins": ["react", "unused-imports", "import", "@typescript-eslint", "jsx-a11y", "prettier"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"no-console": "warn",
|
||||
"react/prop-types": "off",
|
||||
"react/jsx-uses-react": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"jsx-a11y/click-events-have-key-events": "warn",
|
||||
"jsx-a11y/interactive-supports-focus": "warn",
|
||||
"prettier/prettier": "warn",
|
||||
"no-unused-vars": "off",
|
||||
"unused-imports/no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports": "warn",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"args": "after-used",
|
||||
"ignoreRestSiblings": false,
|
||||
"argsIgnorePattern": "^_.*?$"
|
||||
}
|
||||
],
|
||||
"import/order": [
|
||||
"warn",
|
||||
{
|
||||
"groups": [
|
||||
"type",
|
||||
"builtin",
|
||||
"object",
|
||||
"external",
|
||||
"internal",
|
||||
"parent",
|
||||
"sibling",
|
||||
"index"
|
||||
],
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "~/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
}
|
||||
],
|
||||
"newlines-between": "always"
|
||||
}
|
||||
],
|
||||
"react/self-closing-comp": "warn",
|
||||
"react/jsx-sort-props": [
|
||||
"warn",
|
||||
{
|
||||
"callbacksLast": true,
|
||||
"shorthandFirst": true,
|
||||
"noSortAlphabetically": false,
|
||||
"reservedFirst": true
|
||||
}
|
||||
],
|
||||
"padding-line-between-statements": [
|
||||
"warn",
|
||||
{"blankLine": "always", "prev": "*", "next": "return"},
|
||||
{"blankLine": "always", "prev": ["const", "let", "var"], "next": "*"},
|
||||
{
|
||||
"blankLine": "any",
|
||||
"prev": ["const", "let", "var"],
|
||||
"next": ["const", "let", "var"]
|
||||
}
|
||||
],
|
||||
"import/consistent-type-specifier-style": ["error", "prefer-top-level"]
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: jrgarciadev
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: heroui
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: 'Bug report'
|
||||
name: "Bug report"
|
||||
title: "[BUG] - YOUR_ISSUE_TITLE_HERE_REPLACE_ME"
|
||||
description: Create a report to help us improve
|
||||
labels: [bug]
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: 💡 Feature Request
|
||||
url: https://github.com/heroui-inc/heroui/discussions/categories/feature-requests
|
||||
about: 💡 Suggest a new component, improve an existing component and etc
|
||||
- name: 🤔 Long question or ideas?
|
||||
url: https://github.com/heroui-inc/heroui/discussions
|
||||
about: Ask long-form questions and discuss ideas.
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
name: Feature request
|
||||
title: "[Feature Request] YOUR_FEATURE_TITLE_HERE_REPLACE_ME"
|
||||
labels: [feature request]
|
||||
description: |
|
||||
💡 Suggest an idea for the `HeroUI` project
|
||||
Examples
|
||||
- propose a new component
|
||||
- improve an exiting component
|
||||
- ....etc
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
This issue form is for requesting features only! For example, requesting a new component, behavior ... etc
|
||||
If you want to report a bug, please use the [bug report form](https://github.com/heroui-inc/heroui/issues/new?assignees=&labels=&template=bug_report.yml).
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
placeholder: |
|
||||
As a user, I expected ___ behavior but ___ ...
|
||||
|
||||
Ideal Steps I would like to see:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. ....
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Screenshots or Videos
|
||||
description: |
|
||||
If applicable, add screenshots or a video to help explain your problem.
|
||||
For more information on the supported file image/file types and the file size limits, please refer
|
||||
to the following link: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files
|
||||
placeholder: |
|
||||
You can drag your video or image files inside of this editor ↓
|
||||
@@ -4,22 +4,38 @@ description: "Sets up Node.js and runs install"
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "pnpm"
|
||||
check-latest: true
|
||||
node-version-file: ".nvmrc"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup Git User
|
||||
shell: bash
|
||||
run: |
|
||||
git config --global user.email "jrgarciadev@gmail.com"
|
||||
git config --global user.name "Junior Garcia"
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: pnpm install
|
||||
|
||||
+15
-17
@@ -4,6 +4,8 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- canary
|
||||
- v3
|
||||
- "beta-*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -19,19 +21,6 @@ jobs:
|
||||
- name: Build packages
|
||||
run: pnpm build
|
||||
|
||||
tests:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout branch
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/common-actions/install
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm test
|
||||
|
||||
eslint:
|
||||
name: ESLint
|
||||
runs-on: ubuntu-latest
|
||||
@@ -58,15 +47,24 @@ jobs:
|
||||
- name: Run typecheck
|
||||
run: pnpm typecheck
|
||||
|
||||
prettier:
|
||||
name: Prettier
|
||||
continuous-release:
|
||||
name: Continuous Release
|
||||
if: github.repository == 'heroui-inc/heroui'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/common-actions/install
|
||||
|
||||
- name: Run prettier
|
||||
run: pnpm format:check
|
||||
- name: Build packages
|
||||
run: pnpm build
|
||||
|
||||
- name: Release
|
||||
run: pnpx pkg-pr-new publish --pnpm './packages/styles' './packages/react'
|
||||
|
||||
@@ -32,4 +32,4 @@ jobs:
|
||||
fi
|
||||
git add -A
|
||||
git commit -m 'chore(pre-release): enter pre-release mode'
|
||||
git push
|
||||
git push
|
||||
|
||||
@@ -22,9 +22,9 @@ jobs:
|
||||
uses: ./.github/common-actions/install
|
||||
|
||||
- name: remove pre.json
|
||||
# we only remove .changeset/pre.json here
|
||||
# we only remove .changeset/pre.json here
|
||||
# since we want to keep the changeset files introduced in beta/release-next or canary branch
|
||||
# once we merge it to canary / main, those files will be removed in version PR in canary
|
||||
# once we merge it to canary / main, those files will be removed in version PR in canary
|
||||
# and converted to corresponding changelogs
|
||||
run: npx rimraf .changeset/pre.json
|
||||
|
||||
@@ -36,4 +36,4 @@ jobs:
|
||||
git config user.name "github-actions[bot]"
|
||||
git add -A
|
||||
git commit -m "ci(changesets): exit pre-release mode"
|
||||
git push
|
||||
git push
|
||||
|
||||
@@ -78,4 +78,4 @@ jobs:
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
name: Trigger MCP Extraction on Vercel Deployment
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types:
|
||||
- vercel.deployment.success
|
||||
- vercel.deployment.promoted
|
||||
|
||||
jobs:
|
||||
trigger-extraction:
|
||||
name: Trigger MCP Extraction
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger Native MCP Extraction
|
||||
if: |
|
||||
github.event.client_payload.git.ref == 'v3' ||
|
||||
github.event.client_payload.git.ref == 'refs/heads/v3'
|
||||
uses: peter-evans/repository-dispatch@v3
|
||||
with:
|
||||
token: ${{ secrets.MCP_DISPATCH_TOKEN }}
|
||||
repository: heroui-inc/heroui-mcp
|
||||
event-type: native-docs-deployed
|
||||
client-payload: |
|
||||
{
|
||||
"environment": "production",
|
||||
"deployment_url": "${{ github.event.client_payload.url }}",
|
||||
"deployment_id": "${{ github.event.client_payload.id }}",
|
||||
"git_ref": "${{ github.event.client_payload.git.ref }}",
|
||||
"git_sha": "${{ github.event.client_payload.git.sha }}",
|
||||
"triggered_by": "vercel_deployment"
|
||||
}
|
||||
|
||||
- name: Trigger React MCP Extraction
|
||||
if: |
|
||||
github.event.client_payload.git.ref == 'v3' ||
|
||||
github.event.client_payload.git.ref == 'refs/heads/v3'
|
||||
uses: peter-evans/repository-dispatch@v3
|
||||
with:
|
||||
token: ${{ secrets.MCP_DISPATCH_TOKEN }}
|
||||
repository: heroui-inc/heroui-mcp
|
||||
event-type: react-docs-deployed
|
||||
client-payload: |
|
||||
{
|
||||
"environment": "production",
|
||||
"deployment_url": "${{ github.event.client_payload.url }}",
|
||||
"deployment_id": "${{ github.event.client_payload.id }}",
|
||||
"git_ref": "${{ github.event.client_payload.git.ref }}",
|
||||
"git_sha": "${{ github.event.client_payload.git.sha }}",
|
||||
"triggered_by": "vercel_deployment"
|
||||
}
|
||||
@@ -3,7 +3,7 @@ name: Update Stats
|
||||
on:
|
||||
schedule:
|
||||
# Runs every Monday at 00:00 UTC
|
||||
- cron: '0 0 * * 1'
|
||||
- cron: "0 0 * * 1"
|
||||
# Allow manual trigger
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
This PR updates the project statistics including:
|
||||
- Search metadata
|
||||
- GitHub repository information
|
||||
|
||||
|
||||
This is an automated PR generated weekly.
|
||||
branch: chore/update-stats
|
||||
base: main
|
||||
|
||||
+68
-44
@@ -1,66 +1,90 @@
|
||||
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
# cache
|
||||
.turbo
|
||||
.cache
|
||||
.rollup.cache
|
||||
.swc
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
/.pnp
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
coverage
|
||||
__snapshots__
|
||||
coverage.json
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
.next/
|
||||
/out/
|
||||
.next
|
||||
out
|
||||
.vercel
|
||||
|
||||
# production
|
||||
/build
|
||||
dist/
|
||||
storybook-static
|
||||
/packages/storybook/public/tailwind.css
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.production
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
build
|
||||
dist
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.yarn-integrity
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# envs
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.development.local
|
||||
.env.test
|
||||
.env.test.local
|
||||
.env.production
|
||||
.env.production.local
|
||||
.env*
|
||||
.env*.local
|
||||
.env*.local
|
||||
.env*.staging
|
||||
.env*.production
|
||||
!.env.example
|
||||
|
||||
# idea
|
||||
.idea
|
||||
.now
|
||||
dist
|
||||
esm
|
||||
examples/**/yarn.lock
|
||||
examples/**/out
|
||||
examples/**/.next
|
||||
packages/**/*.backup
|
||||
packages/**/*.backup.ts
|
||||
|
||||
.vercel
|
||||
# temporary
|
||||
.temp
|
||||
|
||||
# ignore sitemap
|
||||
apps/**/sitemap.xml
|
||||
apps/**/sitemap-0.xml
|
||||
|
||||
# turbo
|
||||
.turbo
|
||||
packages/**/.turbo
|
||||
# fumadocs
|
||||
.source
|
||||
|
||||
# content layer
|
||||
.contentlayer
|
||||
.content-collections
|
||||
|
||||
# sitemaps
|
||||
sitemap*.xml
|
||||
|
||||
# vite
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# storybook
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# bundle size reports
|
||||
bundle-sizes.json
|
||||
|
||||
# clean-package backups
|
||||
.package.json.backup
|
||||
|
||||
# Generated folder after running `pnpm pack`
|
||||
package
|
||||
|
||||
storybook-static
|
||||
|
||||
# Pre-built skill tarballs (generated during build)
|
||||
apps/docs/public/skills
|
||||
|
||||
+1
-4
@@ -1,4 +1 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
pnpm commitlint --config .commitlintrc.cjs --edit ${1}
|
||||
pnpm commitlint --config commitlint.config.mjs --edit ${1}
|
||||
|
||||
+1
-5
@@ -1,5 +1 @@
|
||||
#!/usr/bin/env sh
|
||||
huskyDir=$(dirname -- "$0")
|
||||
. "$huskyDir/_/husky.sh"
|
||||
|
||||
. "$huskyDir/scripts/update-dep"
|
||||
. "$(dirname -- "$0")/scripts/pnpm-install"
|
||||
|
||||
+1
-5
@@ -1,5 +1 @@
|
||||
#!/usr/bin/env sh
|
||||
huskyDir=$(dirname -- "$0")
|
||||
. "$huskyDir/_/husky.sh"
|
||||
|
||||
. "$huskyDir/scripts/update-dep"
|
||||
. "$(dirname -- "$0")/scripts/pnpm-install"
|
||||
|
||||
+1
-9
@@ -1,9 +1 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
# Avoid excessive outputs
|
||||
if [ -t 2 ]; then
|
||||
exec >/dev/tty 2>&1
|
||||
fi
|
||||
|
||||
pnpm lint-staged -c .lintstagedrc.cjs
|
||||
pnpm lint-staged -c lint-staged.config.mjs
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
changed_files="$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)"
|
||||
|
||||
check_run() {
|
||||
if echo "$changed_files" | grep --quiet "$1"; then
|
||||
printf "\033[32m🔄 [husky] %s changed → running: %s\033[0m\n" "$1" "$2"
|
||||
$2
|
||||
else
|
||||
printf "\033[90m⏭️ [husky] %s unchanged → skipping: %s\033[0m\n" "$1" "$2"
|
||||
fi
|
||||
}
|
||||
|
||||
check_run "pnpm-lock.yaml" "pnpm install --color"
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
changed_files="$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)"
|
||||
|
||||
check_run() {
|
||||
if (echo "$changed_files" | grep --quiet "$1"); then
|
||||
printf "\033[36mDetected changes in pnpm-lock.yaml, starting dependency update\033[0m\n"
|
||||
eval "$2"
|
||||
fi
|
||||
}
|
||||
|
||||
check_run pnpm-lock.yaml "pnpm install --color"
|
||||
@@ -1,26 +0,0 @@
|
||||
const {relative} = require("path");
|
||||
|
||||
const {ESLint} = require("eslint");
|
||||
|
||||
const removeIgnoredFiles = async (files) => {
|
||||
const cwd = process.cwd();
|
||||
const eslint = new ESLint();
|
||||
const relativePaths = files.map((file) => relative(cwd, file));
|
||||
const isIgnored = await Promise.all(relativePaths.map((file) => eslint.isPathIgnored(file)));
|
||||
const filteredFiles = files.filter((_, i) => !isIgnored[i]);
|
||||
|
||||
return filteredFiles.join(" ");
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
"**/*.{js,ts,jsx,tsx}": async (files) => {
|
||||
const filesToLint = await removeIgnoredFiles(files);
|
||||
|
||||
return [`eslint -c .eslintrc.json --max-warnings=0 --fix ${filesToLint}`];
|
||||
},
|
||||
"**/*.css": async (files) => {
|
||||
const filesToLint = await removeIgnoredFiles(files);
|
||||
|
||||
return [`prettier --config .prettierrc.json --ignore-path --write ${filesToLint}`];
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
strict-peer-dependencies=false
|
||||
auto-install-peers=true
|
||||
enable-pre-post-scripts=true
|
||||
public-hoist-pattern[]=*tailwind-variants*
|
||||
public-hoist-pattern[]=*framer-motion*
|
||||
public-hoist-pattern[]=*@react-aria/interactions*
|
||||
public-hoist-pattern[]=*@heroui/theme*
|
||||
lockfile=true
|
||||
save-exact=true
|
||||
strict-peer-dependencies=false
|
||||
|
||||
+31
-15
@@ -1,16 +1,32 @@
|
||||
dist
|
||||
node_modules
|
||||
plop
|
||||
coverage
|
||||
.changeset
|
||||
.next
|
||||
build
|
||||
scripts
|
||||
**/.temp
|
||||
**/.next
|
||||
**/.swc
|
||||
**/.turbo
|
||||
**/.cache
|
||||
**/.build
|
||||
**/.vercel
|
||||
**/dist
|
||||
**/build
|
||||
**/storybook-static
|
||||
.rollup.cache
|
||||
.rollup.cache/**
|
||||
**/node_modules/
|
||||
**/public/*
|
||||
pnpm-lock.yaml
|
||||
!.storybook
|
||||
!.commitlintrc.cjs
|
||||
!.lintstagedrc.cjs
|
||||
!jest.config.js
|
||||
!plopfile.js
|
||||
!react-shim.js
|
||||
!tsup.config.ts
|
||||
**/.contentlayer/
|
||||
**/.source/**
|
||||
**/coverage
|
||||
**/__snapshots__
|
||||
**/.DS_Store
|
||||
**/.changeset
|
||||
**/*.md
|
||||
**/*.mdx
|
||||
!public/manifest.json
|
||||
!.vscode
|
||||
!scripts
|
||||
!.*.js
|
||||
!.*.cjs
|
||||
!.*.mjs
|
||||
!.*.ts
|
||||
!contentlayer.config.ts
|
||||
!next-sitemap.config.ts
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc.json",
|
||||
"tabWidth": 2,
|
||||
"printWidth": 100,
|
||||
"semi": true,
|
||||
"useTabs": false,
|
||||
"singleQuote": false,
|
||||
"bracketSpacing": false,
|
||||
"endOfLine": "auto",
|
||||
"arrowParens": "always",
|
||||
"trailingComma": "all"
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"editorconfig.editorconfig",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"unifiedjs.vscode-mdx"
|
||||
]
|
||||
}
|
||||
Vendored
+40
-6
@@ -1,14 +1,48 @@
|
||||
{
|
||||
"npm.packageManager": "pnpm",
|
||||
"css.lint.emptyRules": "ignore",
|
||||
"css.lint.unknownAtRules": "ignore",
|
||||
"scss.lint.unknownAtRules": "ignore",
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"prettier.prettierPath": "node_modules/prettier",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"eslint.workingDirectories": [{"mode": "auto"}],
|
||||
"eslint.options": {"flags": ["v10_config_lookup_from_file"]},
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"tailwindCSS.experimental.configFile": {
|
||||
"packages/storybook/tailwind.config.js": ["packages/core/theme/**/*", "packages/components/**/*"],
|
||||
"apps/docs/tailwind.config.js": "apps/docs/**/*"
|
||||
"files.associations": {
|
||||
"**/.vscode/*.json": "jsonc"
|
||||
},
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["([\"'`][^\"'`]*.*?[\"'`])", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||
],
|
||||
"[html]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[scss]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"tailwindCSS.classFunctions": ["tv", "clsx", "cn"],
|
||||
"tailwindCSS.classAttributes": ["className", "classNames"],
|
||||
"tailwindCSS.experimental.configFile": {
|
||||
"packages/storybook/styles/globals.css": [
|
||||
"apps/**",
|
||||
"packages/react/**",
|
||||
"packages/storybook/**",
|
||||
"packages/styles/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,815 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Repository Overview
|
||||
|
||||
HeroUI v3 is a modern React UI library built with Tailwind CSS v4, using a pnpm monorepo structure managed by Turborepo.
|
||||
|
||||
### Key Technical Stack
|
||||
|
||||
- **Node.js**: v22+ required
|
||||
- **pnpm**: v10.9.0 (package manager)
|
||||
- **React**: v19+
|
||||
- **Tailwind CSS**: v4.1.4
|
||||
- **TypeScript**: v5.8.3
|
||||
- **Turborepo**: Build orchestration
|
||||
- **Storybook**: Component development
|
||||
- **Vitest**: Testing framework
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Core Development Commands
|
||||
|
||||
```bash
|
||||
# Install dependencies (use --hoist flag)
|
||||
pnpm i --hoist
|
||||
|
||||
# Start Storybook for component development
|
||||
pnpm dev
|
||||
|
||||
# Start documentation site
|
||||
pnpm dev:docs
|
||||
|
||||
# Build all packages
|
||||
pnpm build
|
||||
|
||||
# Build specific package
|
||||
pnpm build --filter=@heroui/react
|
||||
|
||||
# Run linting
|
||||
pnpm lint
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
|
||||
# Test specific component
|
||||
pnpm test button
|
||||
|
||||
# Run formatting
|
||||
pnpm run format
|
||||
|
||||
# Run type checking
|
||||
pnpm typecheck
|
||||
```
|
||||
|
||||
### Package-Specific Commands
|
||||
|
||||
- Use `--filter` flag with package name: `pnpm build --filter=@heroui/react`
|
||||
- Main packages: `@heroui/react`, `@heroui/docs`, `@heroui/storybook`
|
||||
|
||||
## Git Commit Convention
|
||||
|
||||
**IMPORTANT**: This repository uses conventional commits with strict validation. All commits must follow this format:
|
||||
|
||||
```
|
||||
<type>(<scope>): <message>
|
||||
```
|
||||
|
||||
### Allowed Types:
|
||||
|
||||
- `feat` / `feature`: New features
|
||||
- `fix`: Bug fixes
|
||||
- `refactor`: Code refactoring
|
||||
- `docs`: Documentation changes
|
||||
- `build`: Build system changes
|
||||
- `test`: Test changes
|
||||
- `ci`: CI configuration changes
|
||||
- `chore`: Other changes
|
||||
|
||||
### Examples:
|
||||
|
||||
```bash
|
||||
git commit -m "feat(components): add new prop to avatar component"
|
||||
git commit -m "fix(button): resolve click handler issue"
|
||||
git commit -m "docs: update installation guide"
|
||||
git commit -m "ci: add Claude Code GitHub Action workflow"
|
||||
```
|
||||
|
||||
**Note**: Commits without proper format will be rejected by git hooks.
|
||||
|
||||
## Repository Architecture
|
||||
|
||||
### Monorepo Structure
|
||||
|
||||
```
|
||||
/
|
||||
├── apps/
|
||||
│ └── docs/ # Documentation site (Next.js + Fumadocs)
|
||||
├── packages/
|
||||
│ ├── react/ # Main UI component library
|
||||
│ ├── standard/ # Shared ESLint, Prettier, TypeScript configs
|
||||
│ ├── storybook/ # Storybook configuration
|
||||
│ └── vitest/ # Shared Vitest configurations
|
||||
├── turbo.json # Turborepo configuration
|
||||
└── pnpm-workspace.yaml # Workspace definition
|
||||
```
|
||||
|
||||
### Component Architecture Pattern
|
||||
|
||||
Each component in `packages/react/src/components/` follows this structure:
|
||||
|
||||
```
|
||||
component-name/
|
||||
├── component-name.tsx # Main component (uses React Aria)
|
||||
├── component-name.styles.ts # Tailwind Variants styling
|
||||
├── component-name.stories.tsx # Storybook stories (title: "Components/ComponentName")
|
||||
└── index.ts # Barrel exports
|
||||
```
|
||||
|
||||
**IMPORTANT**: All Storybook stories must use the "Components" group in their title. For example: `title: "Components/Card"`, `title: "Components/Button"`, etc.
|
||||
|
||||
### CSS Class Naming Convention
|
||||
|
||||
**IMPORTANT**: HeroUI v3 uses BEM (Block Element Modifier) style for CSS classes to ensure predictable and maintainable styling:
|
||||
|
||||
- **Block**: The main component class (e.g., `button`, `card`, `alert`)
|
||||
- **Modifier**: Variations of the component using double dashes (e.g., `button--primary`, `button--lg`, `button--icon-only`)
|
||||
- **Element**: Child elements within a component (e.g., `card__header`, `alert__icon`)
|
||||
|
||||
**Migration to CSS-based Styling**:
|
||||
|
||||
- The `button` component has been migrated to use CSS styles from `@heroui/styles/src/components/button.css`
|
||||
- This approach allows for better customization through CSS utilities and `@utility` directives
|
||||
- Other components will gradually be migrated to follow this CSS-based pattern
|
||||
- Components use `tv()` from `tailwind-variants` to map variant props to BEM class names
|
||||
|
||||
**Default Size Pattern**:
|
||||
|
||||
**CRITICAL**: All components MUST include default sizes in their base classes to prevent broken appearances when no size modifier is specified. Following the following pattern:
|
||||
|
||||
- **Base classes** include default dimensions (equivalent to the `--md` variant)
|
||||
- **Medium variants** (`--md`) are empty with explanatory comments
|
||||
- **Size modifiers** override the defaults when specified
|
||||
|
||||
Example implementation:
|
||||
|
||||
```css
|
||||
/* Base component with default size */
|
||||
.avatar {
|
||||
@apply relative flex size-10 shrink-0 overflow-hidden rounded-full;
|
||||
/* size-10 is the default, equivalent to --md */
|
||||
}
|
||||
|
||||
/* Size variants */
|
||||
.avatar--sm {
|
||||
@apply size-8; /* Override default */
|
||||
}
|
||||
|
||||
.avatar--md {
|
||||
/* No styles as this is the default size */
|
||||
}
|
||||
|
||||
.avatar--lg {
|
||||
@apply size-12; /* Override default */
|
||||
}
|
||||
```
|
||||
|
||||
This ensures components work properly without explicit size classes:
|
||||
|
||||
- `<div className="avatar">` → Works perfectly (size-10)
|
||||
- `<div className="avatar avatar--lg">` → Override to large (size-12)
|
||||
|
||||
### Core Component Design Principles
|
||||
|
||||
**IMPORTANT**: HeroUI v3 follows a compound component pattern similar to Radix UI, built on top of React Aria Components primitives. This enables maximum flexibility and customization for users.
|
||||
|
||||
### React Aria Components Integration
|
||||
|
||||
**CRITICAL**: Before implementing any component, you MUST:
|
||||
|
||||
1. Visit React Aria Components docs: https://react-spectrum.adobe.com/react-aria/
|
||||
2. Study the specific component's API and examples
|
||||
3. Understand its accessibility features and ARIA patterns
|
||||
4. Plan the transformation from React Aria's prop-based API to Radix UI's composition-based API
|
||||
|
||||
React Aria provides the accessibility foundation, but we transform their API to match Radix UI's compound component pattern for better customization.
|
||||
|
||||
#### 1. **Compound Component Pattern**:
|
||||
|
||||
- Export all internal component pieces (Root, Item, Trigger, Content, etc.)
|
||||
- Each piece can be styled and composed independently
|
||||
- Users can customize render logic without accessing internal code
|
||||
- Examples: Accordion (Root, Item, Heading, Trigger, Panel, Indicator, Body), Alert (Root, Icon, Title, Description, Action, Close)
|
||||
|
||||
#### 2. **Export Strategy**:
|
||||
|
||||
```typescript
|
||||
// Named exports for compound components
|
||||
export * as ComponentName from "./component-name";
|
||||
|
||||
// Direct exports for simple components
|
||||
export {Component, type ComponentProps} from "./component";
|
||||
|
||||
// Always export variants
|
||||
export {componentVariants, type ComponentVariants} from "./component.styles";
|
||||
```
|
||||
|
||||
#### 3. **Component Structure for Compound Components**:
|
||||
|
||||
```typescript
|
||||
// Context for sharing state/styles
|
||||
const ComponentContext = createContext<{slots?: ReturnType<typeof componentVariants>}>({});
|
||||
|
||||
// Root component wraps with context
|
||||
const ComponentRoot = React.forwardRef<...>(({children, className, ...props}, ref) => {
|
||||
const slots = React.useMemo(() => componentVariants({...}), [...]);
|
||||
|
||||
return (
|
||||
<ComponentContext value={{slots}}>
|
||||
<ReactAriaComponent ref={ref} className={composeTwRenderProps(className, slots.base())}>
|
||||
{children}
|
||||
</ReactAriaComponent>
|
||||
</ComponentContext>
|
||||
);
|
||||
});
|
||||
|
||||
// Child components consume context
|
||||
const ComponentItem = React.forwardRef<...>(({className, ...props}, ref) => {
|
||||
const {slots} = useContext(ComponentContext);
|
||||
|
||||
return (
|
||||
<ReactAriaComponent ref={ref} className={composeTwRenderProps(className, slots?.item())}>
|
||||
{props.children}
|
||||
</ReactAriaComponent>
|
||||
);
|
||||
});
|
||||
|
||||
// Export pattern
|
||||
export {ComponentRoot as Root, ComponentItem as Item, ...};
|
||||
```
|
||||
|
||||
#### 4. **Key Implementation Details**:
|
||||
|
||||
1. **Styling with Tailwind Variants**:
|
||||
- Styles defined in `.styles.ts` files using `tv()` function from `tailwind-variants`
|
||||
- **IMPORTANT**: Always import from `tailwind-variants`, never from `@heroui/standard` (which doesn't exist)
|
||||
- **CRITICAL**: tailwind-variants already includes `twMerge` functionality, so NEVER manually use `twMerge`
|
||||
- **RULE**: All component styles MUST be defined in separate `.styles.ts` files, NOT in the component implementation files
|
||||
- Component implementation files (`.tsx`) should only contain logic and React Aria primitives
|
||||
- Example imports:
|
||||
```typescript
|
||||
import type {VariantProps} from "tailwind-variants";
|
||||
import {tv} from "tailwind-variants";
|
||||
```
|
||||
- Support for variants (primary, secondary, etc.)
|
||||
- Compound variants for conditional styling
|
||||
- Slot system for complex components
|
||||
|
||||
2. **Component Features**:
|
||||
- Built on React Aria Components for accessibility
|
||||
- Use `forwardRef` for all components
|
||||
- Display names follow: `HeroUI.ComponentName` or `HeroUI.Component.SubPart`
|
||||
- Support render props from React Aria when available
|
||||
|
||||
3. **Type Exports**:
|
||||
|
||||
```typescript
|
||||
// Export props for each component part
|
||||
export type ComponentRootProps = {...}
|
||||
export type ComponentItemProps = {...}
|
||||
export type ComponentVariants = VariantProps<typeof componentVariants>
|
||||
```
|
||||
|
||||
4. **Utilities** (`packages/react/src/utils/`):
|
||||
- `composeTwRenderProps`: Merge Tailwind classes with render props
|
||||
- `focusRingClasses`: Consistent focus styling
|
||||
- `disabledClasses`: Disabled state styling
|
||||
- `mapPropsVariants`: Separate variant props from component props
|
||||
|
||||
5. **React Aria Components className Patterns**:
|
||||
|
||||
**CRITICAL**: React Aria components have different className prop behaviors:
|
||||
|
||||
**Components that support render props** (use `composeTwRenderProps`):
|
||||
- Button, TextField, FieldError, Checkbox, CheckboxGroup
|
||||
- Switch, RadioGroup, Radio, Slider (and Track, Thumb, Output)
|
||||
- Popover, Tooltip, Tabs (and Tab, TabList, TabPanel)
|
||||
- Link, Menu, MenuItem, Accordion (DisclosureGroup)
|
||||
|
||||
**Components that ONLY accept string className** (pass className directly):
|
||||
- Label, Text, Input, TextArea
|
||||
- Heading, Dialog, OverlayArrow
|
||||
|
||||
**Usage examples**:
|
||||
|
||||
```typescript
|
||||
// For render prop components - use composeTwRenderProps
|
||||
<ButtonPrimitive
|
||||
className={composeTwRenderProps(className, slots?.button())}
|
||||
/>
|
||||
|
||||
// For string-only components - pass className directly
|
||||
<LabelPrimitive
|
||||
className={slots?.label({className})}
|
||||
/>
|
||||
// OR
|
||||
<LabelPrimitive
|
||||
className={labelVariants({size, variant, className})}
|
||||
/>
|
||||
```
|
||||
|
||||
**How to check**: If unsure, check the React Aria docs or try both approaches - TypeScript will error if a component doesn't support render props
|
||||
|
||||
6. **Composition Pattern with Existing Components**:
|
||||
|
||||
**CRITICAL**: HeroUI follows a composition-based approach. Components should reuse existing primitives rather than creating component-specific versions.
|
||||
|
||||
**Key Principles**:
|
||||
- **DO NOT** create component-specific Label, Description, or FieldError components
|
||||
- **DO** reuse the existing `Label`, `Description`, and `FieldError` components
|
||||
- **DO** use standard HTML composition patterns with `htmlFor`/`id` attributes
|
||||
|
||||
**Example Pattern**:
|
||||
|
||||
```typescript
|
||||
// ❌ WRONG - Component-specific label
|
||||
export const Checkbox = {
|
||||
Root: CheckboxRoot,
|
||||
Label: CheckboxLabel, // Don't create this!
|
||||
};
|
||||
|
||||
// ✅ CORRECT - Compose with existing components
|
||||
import { Label } from "@/components/label";
|
||||
import { Description } from "@/components/description";
|
||||
|
||||
// Usage:
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox id="terms">
|
||||
<Checkbox.Indicator />
|
||||
</Checkbox>
|
||||
<Label htmlFor="terms">Accept terms</Label>
|
||||
</div>
|
||||
|
||||
// With description:
|
||||
<div className="flex gap-3">
|
||||
<Checkbox className="mt-0.5" id="notifications">
|
||||
<Checkbox.Indicator />
|
||||
</Checkbox>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Label htmlFor="notifications">Email notifications</Label>
|
||||
<Description>Get notified when someone mentions you</Description>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Components that follow this pattern**:
|
||||
- Checkbox - uses external Label/Description
|
||||
- Radio - uses external Label/Description
|
||||
- Switch - uses external Label/Description
|
||||
- TextField - provides slots for Label/Description/FieldError
|
||||
|
||||
### Current Components
|
||||
|
||||
- `accordion`: Collapsible content sections
|
||||
- `alert`: Alert messages with compound components
|
||||
- `avatar`: User avatars with Radix UI
|
||||
- `button`: Button with variants and sizes
|
||||
- `checkbox`: Checkbox with compound components (uses external Label/Description)
|
||||
- `chip`: Small informational badges
|
||||
- `description`: Description text for form fields
|
||||
- `field-error`: Error messages for form fields
|
||||
- `fieldset`: Form field grouping components (Fieldset, Legend, FieldGroup, Field, CheckboxField)
|
||||
- `label`: Label text for form fields
|
||||
- `link`: Styled anchor links
|
||||
- `menu`: Dropdown menu system
|
||||
- `popover`: Popover overlays
|
||||
- `spinner`: Loading indicators
|
||||
- `tabs`: Tab navigation
|
||||
- `text`: Text component for paragraphs and general text
|
||||
- `textfield`: Text input field with compound components
|
||||
- `tooltip`: Hover tooltips
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Standard Feature Development Process
|
||||
|
||||
**IMPORTANT**: When working on any feature or improvement, Claude Code MUST follow this systematic workflow to ensure accuracy and code quality:
|
||||
|
||||
1. **Research Phase**:
|
||||
- Thoroughly research the feature/component requirements
|
||||
- Study relevant documentation (React Aria, Tailwind CSS, etc.)
|
||||
- Analyze existing similar implementations in the codebase
|
||||
- Identify all dependencies and integration points
|
||||
|
||||
2. **Planning Phase**:
|
||||
- Create a detailed implementation plan with a comprehensive checklist
|
||||
- Break down the task into specific, measurable steps
|
||||
- Include testing and verification steps in the plan
|
||||
- Present the plan for review before proceeding
|
||||
|
||||
3. **Review & Correction Phase**:
|
||||
- Review the plan for completeness and accuracy
|
||||
- Make necessary corrections or adjustments
|
||||
- Ensure all edge cases are considered
|
||||
- Confirm the plan aligns with HeroUI patterns and conventions
|
||||
|
||||
4. **Execution Phase**:
|
||||
- Start executing the plan step by step
|
||||
- Use the TodoWrite tool to track progress automatically
|
||||
- Mark todos as in_progress when starting a task
|
||||
- Mark todos as completed immediately after finishing each step
|
||||
- Never batch completions - update status in real-time
|
||||
|
||||
5. **Verification Phase**:
|
||||
- Manually verify all changes work as expected
|
||||
- Test API calls if backend changes were made
|
||||
- Check frontend rendering and interactions
|
||||
- Run lint and type checks: `pnpm lint && pnpm typecheck`
|
||||
- Ensure all tests pass: `pnpm test`
|
||||
|
||||
**Example Workflow**:
|
||||
|
||||
```
|
||||
User: "Add a new Select component"
|
||||
|
||||
Claude:
|
||||
1. Research: Studies React Aria Select, existing patterns
|
||||
2. Plan: Creates detailed checklist with 15+ items
|
||||
3. Review: Presents plan for feedback
|
||||
4. Execute: Implements step-by-step with todo updates
|
||||
5. Verify: Tests component, runs checks, confirms functionality
|
||||
```
|
||||
|
||||
This workflow ensures thorough understanding, proper planning, and high-quality implementation with full transparency throughout the process.
|
||||
|
||||
### Component Development Workflow
|
||||
|
||||
1. **Creating New Components**:
|
||||
|
||||
**CRITICAL: Research & Design Phase**:
|
||||
- **FIRST**: Check the Figma design for the component breakdown (e.g., Menu Container, Menu Item, etc.)
|
||||
- **SECOND**: Research the React Aria Components documentation at https://react-spectrum.adobe.com/react-aria/
|
||||
- Find the appropriate React Aria primitive (e.g., CheckboxGroup, Dialog, Select, etc.)
|
||||
- Understand the React Aria API, props, and accessibility features
|
||||
- Map Figma component pieces to React Aria components and plan the compound structure
|
||||
- Plan how to adapt it to follow Radix UI's compound component pattern
|
||||
|
||||
**Component Creation - ALWAYS USE THE SCRIPT**:
|
||||
|
||||
```bash
|
||||
# Navigate to packages/react directory
|
||||
cd packages/react
|
||||
|
||||
# Use the add:component script
|
||||
pnpm add:component ComponentName
|
||||
|
||||
# Examples:
|
||||
pnpm add:component Menu
|
||||
pnpm add:component Select
|
||||
pnpm add:component DatePicker
|
||||
```
|
||||
|
||||
This script will:
|
||||
- Create all necessary files with proper structure
|
||||
- Add the export to `src/components/index.ts`
|
||||
- Generate boilerplate following HeroUI patterns
|
||||
- Set up the component with TypeScript and proper exports
|
||||
|
||||
After creating the component:
|
||||
|
||||
```bash
|
||||
# Build to update package.json exports automatically
|
||||
pnpm build
|
||||
```
|
||||
|
||||
**Implementation Steps**:
|
||||
- Study existing HeroUI components (accordion, alert) to understand the compound pattern
|
||||
- Use React Aria Components as the foundation for accessibility
|
||||
- Transform React Aria's API to match Radix UI patterns:
|
||||
- Single component → Multiple exported parts (Item, Trigger, Content, etc.)
|
||||
- Props-based API → Composition-based API
|
||||
- Internal state → Context-based state sharing
|
||||
- Create Context for sharing styles across component parts
|
||||
- Export ALL component parts for maximum customization
|
||||
- Define styles in separate `.styles.ts` file with slot system
|
||||
- Add "use client" directive at the top of component file
|
||||
- Create comprehensive Storybook stories showing all variants and compositions
|
||||
- Follow the export pattern: `export * as ComponentName from "./component-name"`
|
||||
|
||||
**Example Transformation**:
|
||||
|
||||
```typescript
|
||||
// React Aria: Single component with props
|
||||
<CheckboxGroup label="Options" value={selected} onChange={setSelected}>
|
||||
<Checkbox value="1">Option 1</Checkbox>
|
||||
</CheckboxGroup>
|
||||
|
||||
// HeroUI: Compound pattern
|
||||
<CheckboxGroup value={selected} onValueChange={setSelected}>
|
||||
<CheckboxGroup.Label>Options</CheckboxGroup.Label>
|
||||
<CheckboxGroup.Item value="1">
|
||||
<CheckboxGroup.Indicator />
|
||||
<CheckboxGroup.Label>Option 1</CheckboxGroup.Label>
|
||||
</CheckboxGroup.Item>
|
||||
</CheckboxGroup>
|
||||
```
|
||||
|
||||
**Example of a compound component exports**
|
||||
|
||||
```typescript
|
||||
const CompoundAccordion = Object.assign(Accordion, {
|
||||
Item: AccordionItem,
|
||||
Heading: AccordionHeading,
|
||||
Trigger: AccordionTrigger,
|
||||
Panel: AccordionPanel,
|
||||
Indicator: AccordionIndicator,
|
||||
Body: AccordionBody,
|
||||
});
|
||||
|
||||
export type {
|
||||
AccordionProps,
|
||||
AccordionItemProps,
|
||||
AccordionTriggerProps,
|
||||
AccordionPanelProps,
|
||||
AccordionIndicatorProps,
|
||||
AccordionBodyProps,
|
||||
};
|
||||
|
||||
export default CompoundAccordion;
|
||||
```
|
||||
|
||||
**IMPORTANT**: The compound component should be exported as the default export.
|
||||
|
||||
2. **Testing**:
|
||||
- Run `pnpm test` for all tests
|
||||
- Run `pnpm test <component>` for specific component
|
||||
- Ensure all new features have tests
|
||||
|
||||
3. **Documentation**:
|
||||
- Docs live in `apps/docs/content/`
|
||||
- Uses MDX format
|
||||
- HeroUI components are pre-imported
|
||||
|
||||
4. **Version Management**:
|
||||
- Uses Changesets for versioning
|
||||
- Run `pnpm changeset` to document changes
|
||||
- Follow semantic versioning
|
||||
|
||||
## Icon Library
|
||||
|
||||
**IMPORTANT**: HeroUI uses Iconify with gravity-ui as the default icon set.
|
||||
|
||||
## Important Notes
|
||||
|
||||
- Always prefer editing existing files over creating new ones
|
||||
- **NEVER** create documentation files (_.md, _.mdx, README files) unless explicitly requested by the user
|
||||
- Follow the established component patterns and conventions
|
||||
- Ensure accessibility with React Aria Components
|
||||
- Maintain TypeScript type safety
|
||||
- Use the commit convention to avoid git hook failures
|
||||
- Run lint and type checks before committing: `pnpm lint && pnpm typecheck`
|
||||
|
||||
## Tailwind CSS Class Detection Rules
|
||||
|
||||
**CRITICAL**: Tailwind CSS scans files as plain text and requires complete class names to be statically detectable.
|
||||
|
||||
### Key Rules:
|
||||
|
||||
1. **Never construct class names dynamically**
|
||||
|
||||
❌ **BAD** - Dynamic string concatenation:
|
||||
|
||||
```jsx
|
||||
// These patterns will NOT work:
|
||||
<div className={`text-${color}-600`} />
|
||||
<button className={`bg-${variant}-500`} />
|
||||
<span className={`button--${size}`} />
|
||||
```
|
||||
|
||||
✅ **GOOD** - Complete class names:
|
||||
|
||||
```jsx
|
||||
// Use complete strings or object mappings:
|
||||
<div className={error ? "text-red-600" : "text-green-600"} />
|
||||
```
|
||||
|
||||
2. **Use object mappings for dynamic classes**
|
||||
|
||||
❌ **BAD** - Props in template literals:
|
||||
|
||||
```jsx
|
||||
function Button({color}) {
|
||||
return <button className={`bg-${color}-600 hover:bg-${color}-500`} />;
|
||||
}
|
||||
```
|
||||
|
||||
✅ **GOOD** - Map props to complete classes:
|
||||
|
||||
```jsx
|
||||
function Button({color}) {
|
||||
const colorVariants = {
|
||||
blue: "bg-blue-600 hover:bg-blue-500",
|
||||
red: "bg-red-600 hover:bg-red-500",
|
||||
};
|
||||
return <button className={colorVariants[color]} />;
|
||||
}
|
||||
```
|
||||
|
||||
3. **For BEM-style classes, use complete mappings**
|
||||
|
||||
✅ **GOOD** - Complete class name mappings:
|
||||
|
||||
```jsx
|
||||
const sizeClasses = {
|
||||
sm: "button--sm",
|
||||
md: "button--md",
|
||||
lg: "button--lg",
|
||||
};
|
||||
|
||||
// Use the mapping:
|
||||
className={sizeClasses[size]}
|
||||
```
|
||||
|
||||
### Why This Matters:
|
||||
|
||||
- Tailwind generates CSS only for classes it can detect in your source files
|
||||
- Dynamic concatenation prevents Tailwind from finding the complete class names
|
||||
- Missing classes = missing styles in production
|
||||
|
||||
## Figma Integration & MCP Server Rules
|
||||
|
||||
### Figma Dev Mode MCP Server
|
||||
|
||||
**IMPORTANT**: When creating components with Figma designs:
|
||||
|
||||
1. **Component Breakdown**: Figma designs are already broken down into component pieces (e.g., Menu Container, Menu Item, etc.). Use these as reference for:
|
||||
- Component structure and naming (adapt to code conventions)
|
||||
- Visual styling and spacing
|
||||
- Component composition patterns
|
||||
|
||||
2. **MCP Server Rules**:
|
||||
- The Figma Dev Mode MCP Server provides an assets endpoint for images and SVG assets
|
||||
- **CRITICAL**: If the Figma MCP Server returns a localhost source for an image or SVG, use that source directly
|
||||
- **DO NOT** import or add new icon packages - all assets should come from the Figma payload
|
||||
- **DO NOT** use or create placeholders if a localhost source is provided
|
||||
- Always use the actual assets from Figma MCP Server
|
||||
|
||||
3. **Workflow**:
|
||||
- Check Figma for component visual design and breakdown
|
||||
- Map Figma component names to appropriate React Aria primitives
|
||||
- Use Figma assets (icons, images) directly from the MCP Server
|
||||
- Implement styles based on Figma design tokens and specifications
|
||||
|
||||
## Library Documentation with Context7 MCP
|
||||
|
||||
**IMPORTANT**: We have the Context7 MCP server available (https://github.com/upstash/context7) for accessing up-to-date library documentation.
|
||||
|
||||
### When to Use Context7
|
||||
|
||||
Use Context7 MCP when working with external libraries, especially:
|
||||
|
||||
- **Tailwind CSS v4**: When working with Tailwind CSS v4 features, use Context7 to get the latest documentation at https://context7.com/context7/tailwindcss
|
||||
- **Fumadocs**: When working on the documentation site in `apps/docs/`, use Context7 to get the latest Fumadocs framework documentation
|
||||
- **Next.js**: For Next.js specific features and APIs used in the docs app
|
||||
- Any other third-party libraries where up-to-date documentation is needed
|
||||
|
||||
### How to Use Context7
|
||||
|
||||
1. First, resolve the library ID using `mcp__context7__resolve-library-id`
|
||||
2. Then fetch documentation using `mcp__context7__get-library-docs` with the resolved ID
|
||||
3. This ensures you're always working with the latest documentation rather than outdated information
|
||||
|
||||
### Example Usage Areas
|
||||
|
||||
- Implementing new documentation features in `apps/docs/`
|
||||
- Configuring Fumadocs settings in `source.config.ts`
|
||||
- Working with MDX components and layouts
|
||||
- Setting up search functionality
|
||||
- Implementing documentation navigation and structure
|
||||
|
||||
## GitHub Repository Search with Grep MCP
|
||||
|
||||
**IMPORTANT**: We have the Grep MCP server available for searching over a million public GitHub repositories to find real-world code examples and patterns.
|
||||
|
||||
### When to Use Grep MCP
|
||||
|
||||
Use the Grep MCP (`mcp__grep__searchGitHub`) when tackling complex problems that require:
|
||||
|
||||
- **Real-world implementation examples**: Finding how other developers solve similar problems
|
||||
- **Best practices and patterns**: Discovering production-ready code patterns
|
||||
- **Library usage examples**: Understanding how specific APIs or libraries are used in practice
|
||||
- **Complex integrations**: Seeing how different libraries work together
|
||||
- **Error handling patterns**: Learning from battle-tested error handling approaches
|
||||
|
||||
### How to Use Grep MCP
|
||||
|
||||
The Grep MCP searches for **literal code patterns**, not keywords. Use actual code syntax:
|
||||
|
||||
**Good examples**:
|
||||
|
||||
- `'useState('` - Find React hooks usage
|
||||
- `'import { tv } from "tailwind-variants"'` - Find tailwind-variants imports
|
||||
- `'forwardRef<'` - Find forwardRef usage patterns
|
||||
- `'(?s)useEffect\\(\\(\\) => {.*return.*}'` - Find useEffect with cleanup (regex)
|
||||
|
||||
**Bad examples**:
|
||||
|
||||
- `'react best practices'` - This is a keyword, not code
|
||||
- `'how to use tailwind'` - Use actual import statements instead
|
||||
|
||||
### Example Use Cases
|
||||
|
||||
1. **Complex Component Patterns**:
|
||||
- Search: `'compound.*component'` with language=['TypeScript', 'TSX']
|
||||
- Find how others implement compound component patterns
|
||||
|
||||
2. **Accessibility Implementations**:
|
||||
- Search: `'AriaProps'` or `'useAriaLabel'`
|
||||
- Discover accessibility patterns in React apps
|
||||
|
||||
3. **Monorepo Configurations**:
|
||||
- Search: `'pnpm-workspace.yaml'` with path='pnpm-workspace.yaml'
|
||||
- Study monorepo setups similar to HeroUI
|
||||
|
||||
4. **Tailwind CSS v4 Patterns**:
|
||||
- Search: `'@import "tailwindcss"'` with language=['CSS']
|
||||
- Find Tailwind CSS v4 usage patterns
|
||||
|
||||
5. **React Aria Components Usage**:
|
||||
- Search: `'from "react-aria-components"'`
|
||||
- See how others integrate React Aria Components
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Use language filters to narrow results (e.g., `language=['TypeScript', 'TSX']`)
|
||||
- Use regex patterns with `useRegexp=true` for flexible matching
|
||||
- Filter by well-known repositories for quality examples (e.g., `repo='vercel/'`)
|
||||
- Combine with file path filters for specific file types
|
||||
|
||||
## Agent-Specific Guidelines
|
||||
|
||||
### For style-migrator and tailwind-v4-css-expert Agents
|
||||
|
||||
When working with HeroUI CSS components, follow these critical patterns:
|
||||
|
||||
#### Default Size Implementation
|
||||
|
||||
**REQUIRED**: All CSS components MUST follow the default size pattern:
|
||||
|
||||
1. **Base classes** include default dimensions equivalent to `--md` variant
|
||||
2. **Medium variant** (`--md`) is an empty class with explanatory comment
|
||||
3. **Size variants** override the base defaults
|
||||
|
||||
**Template for CSS components with size variants:**
|
||||
|
||||
```css
|
||||
/* Base component styles */
|
||||
.component {
|
||||
/* Base styling */
|
||||
@apply [base-styles];
|
||||
|
||||
/* Default size - matches component--md variant */
|
||||
@apply [default-size-classes];
|
||||
}
|
||||
|
||||
/* Size variants */
|
||||
.component--sm {
|
||||
@apply [small-size-overrides];
|
||||
}
|
||||
|
||||
.component--md {
|
||||
/* No styles as this is the default size */
|
||||
}
|
||||
|
||||
.component--lg {
|
||||
@apply [large-size-overrides];
|
||||
}
|
||||
```
|
||||
|
||||
#### Pseudo-Class Fallback Pattern
|
||||
|
||||
**REQUIRED**: All interactive components MUST include both pseudo-class and data-attribute support:
|
||||
|
||||
```css
|
||||
/* Interactive states - both approaches */
|
||||
.component {
|
||||
/* Hover states */
|
||||
&:hover,
|
||||
&[data-hovered="true"] {
|
||||
@apply [hover-styles];
|
||||
}
|
||||
|
||||
/* Active/pressed states */
|
||||
&:active,
|
||||
&[data-pressed="true"] {
|
||||
@apply [active-styles];
|
||||
}
|
||||
|
||||
/* Focus states */
|
||||
&:focus-visible,
|
||||
&:focus:not(:focus-visible),
|
||||
&[data-focus-visible="true"] {
|
||||
outline: 2px solid var(--focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Component Examples
|
||||
|
||||
- **button.css**: Base has `h-10 md:h-9`, empty `.button--md` variant
|
||||
- **avatar.css**: Base has `size-10`, empty `.avatar--md` variant
|
||||
- **spinner.css**: Base has `size-6`, empty `.spinner--md` variant
|
||||
|
||||
These patterns ensure components never appear broken and maintain consistency across the design system.
|
||||
+3
-3
@@ -2,7 +2,7 @@
|
||||
|
||||
Hello!, I am very excited that you are interested in contributing with HeroUI. However, before submitting your contribution, be sure to take a moment and read the following guidelines.
|
||||
|
||||
- [Code of Conduct](https://github.com/frontio-ai/heroui/blob/canary/CODE_OF_CONDUCT.md)
|
||||
- [Code of Conduct](https://github.com/heroui-inc/heroui/blob/canary/CODE_OF_CONDUCT.md)
|
||||
- [Extraction request guidelines](#pull-request-guidelines)
|
||||
- [Development Setup](#development-setup)
|
||||
- [Tests](#tests)
|
||||
@@ -79,7 +79,7 @@ https://www.conventionalcommits.org/ or check out the
|
||||
commit type. `scope` is just a short id that describes the scope of work.
|
||||
|
||||
3. Make and commit your changes following the
|
||||
[commit convention](https://github.com/frontio-ai/heroui/blob/main/CONTRIBUTING.md#commit-convention).
|
||||
[commit convention](https://github.com/heroui-inc/heroui/blob/main/CONTRIBUTING.md#commit-convention).
|
||||
As you canary, you can run `pnpm build --filter=<module>` and
|
||||
`pnpm test packages/<module>/<pkg>` e.g. `pnpm build --filter=avatar & pnpm test packages/components/avatar` to make sure everything works as expected.
|
||||
|
||||
@@ -235,7 +235,7 @@ best to be proactive in reaching out to those that are already helping out.
|
||||
GitHub by default does not publicly state that you are a member of the
|
||||
organization. Please feel free to change that setting for yourself so others
|
||||
will know who's helping out. That can be configured on the [organization
|
||||
list](https://github.com/orgs/frontio-ai/people) page.
|
||||
list](https://github.com/orgs/heroui-inc/people) page.
|
||||
|
||||
Being a maintainer is not an obligation. You can help when you have time and be
|
||||
less active when you don't. If you get a new job and get busy, that's alright.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Next UI
|
||||
Copyright (c) 2025 NextUI Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
+8
-24
@@ -1,24 +1,8 @@
|
||||
## use cache for Github requests like "tags"
|
||||
USE_CACHE=true/false
|
||||
## Algolia client app id
|
||||
NEXT_PUBLIC_ALGOLIA_APP_ID=
|
||||
## Algolia client search only api key
|
||||
NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY=
|
||||
|
||||
|
||||
# Vercel Env (is used for skipping typescript check)
|
||||
IS_VERCEL_ENV=true/false
|
||||
IGNORE_BUILD_CHECKS=true/false
|
||||
IS_PREVIEW=true/false
|
||||
ANALYZE_BUNDLE=true/false
|
||||
|
||||
# Vercel preview env (is used for taking the docs directly from the project files)
|
||||
NEXT_PUBLIC_PREVIEW=true/false
|
||||
|
||||
## Featurebase
|
||||
NEXT_PUBLIC_FB_FEEDBACK_ORG=
|
||||
NEXT_PUBLIC_FB_FEEDBACK_URL=
|
||||
|
||||
# PostHog
|
||||
NEXT_PUBLIC_POSTHOG_KEY=your-posthog-key
|
||||
NEXT_PUBLIC_POSTHOG_HOST=your-posthog-host
|
||||
NEXT_PUBLIC_APP_ENV="production,preview,development"
|
||||
NEXT_PUBLIC_CDN_URL="<cdn-url>"
|
||||
LOOPS_API_ENDPOINT="<LOOPS_API_ENDPOINT>"
|
||||
LOOPS_API_KEY="<LOOPS_API_KEY>"
|
||||
FEATUREBASE_API_ENDPOINT="<FEATUREBASE_API_ENDPOINT>"
|
||||
FEATUREBASE_API_KEY="<FEATUREBASE_API_KEY>"
|
||||
NEXT_PUBLIC_POSTHOG_HOST="<POSTHOG_HOST>"
|
||||
NEXT_PUBLIC_POSTHOG_KEY="<POSTHOG_KEY>"
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"extends": ["../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"parserOptions": {
|
||||
"project": ["apps/docs/tsconfig(.*)?.json"],
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"react/no-unknown-property": [
|
||||
2,
|
||||
{
|
||||
"ignore": ["jsx", "global"]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
sitemap.xml
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env*.local
|
||||
.dev.vars
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# cloudflare
|
||||
.wrangler
|
||||
/.open-next/
|
||||
@@ -0,0 +1 @@
|
||||
enable-pre-post-scripts=true
|
||||
@@ -1,332 +0,0 @@
|
||||
[
|
||||
{
|
||||
"MemberId": 275354,
|
||||
"createdAt": "2022-02-18 11:57",
|
||||
"type": "USER",
|
||||
"role": "BACKER",
|
||||
"tier": "Backer 🖤",
|
||||
"isActive": true,
|
||||
"totalAmountDonated": 5,
|
||||
"currency": "USD",
|
||||
"lastTransactionAt": "2022-02-18 11:57",
|
||||
"lastTransactionAmount": 5,
|
||||
"profile": "https://opencollective.com/guest-7dc076fb",
|
||||
"name": "jorge",
|
||||
"company": null,
|
||||
"description": null,
|
||||
"image": "/sponsors/undefined",
|
||||
"email": null,
|
||||
"twitter": null,
|
||||
"github": null,
|
||||
"website": null
|
||||
},
|
||||
{
|
||||
"MemberId": 277819,
|
||||
"createdAt": "2022-03-01 09:13",
|
||||
"type": "USER",
|
||||
"role": "BACKER",
|
||||
"isActive": true,
|
||||
"totalAmountDonated": 10,
|
||||
"currency": "USD",
|
||||
"lastTransactionAt": "2022-03-01 09:13",
|
||||
"lastTransactionAmount": 10,
|
||||
"profile": "https://opencollective.com/dhananjay-senday",
|
||||
"name": "Dhananjay Senday",
|
||||
"company": null,
|
||||
"description": null,
|
||||
"image": "/sponsors/undefined",
|
||||
"email": null,
|
||||
"twitter": null,
|
||||
"github": null,
|
||||
"website": null
|
||||
},
|
||||
{
|
||||
"MemberId": 292380,
|
||||
"createdAt": "2022-04-22 07:47",
|
||||
"type": "USER",
|
||||
"role": "BACKER",
|
||||
"tier": "Backer 🖤",
|
||||
"isActive": true,
|
||||
"totalAmountDonated": 10,
|
||||
"currency": "USD",
|
||||
"lastTransactionAt": "2022-04-22 07:47",
|
||||
"lastTransactionAmount": 10,
|
||||
"profile": "https://opencollective.com/manuel5",
|
||||
"name": "manuel-rw",
|
||||
"company": null,
|
||||
"description": null,
|
||||
"image": "/sponsors/292380.jpg",
|
||||
"email": null,
|
||||
"twitter": null,
|
||||
"github": null,
|
||||
"website": null
|
||||
},
|
||||
{
|
||||
"MemberId": 327844,
|
||||
"createdAt": "2022-08-26 21:32",
|
||||
"type": "ORGANIZATION",
|
||||
"role": "BACKER",
|
||||
"tier": "Gold Sponsor 🥇",
|
||||
"isActive": true,
|
||||
"totalAmountDonated": 200,
|
||||
"currency": "USD",
|
||||
"lastTransactionAt": "2022-12-22 07:19",
|
||||
"lastTransactionAmount": 100,
|
||||
"profile": "https://opencollective.com/lowdefy",
|
||||
"name": "Lowdefy",
|
||||
"company": null,
|
||||
"description": "Create custom web apps in minutes",
|
||||
"image": "/sponsors/327844.jpg",
|
||||
"twitter": "https://twitter.com/lowdefy",
|
||||
"github": "https://github.com/lowdefy",
|
||||
"website": "https://lowdefy.com"
|
||||
},
|
||||
{
|
||||
"MemberId": 347048,
|
||||
"createdAt": "2022-10-30 06:25",
|
||||
"type": "USER",
|
||||
"role": "BACKER",
|
||||
"tier": "Backer 🖤",
|
||||
"isActive": true,
|
||||
"totalAmountDonated": 1,
|
||||
"currency": "USD",
|
||||
"lastTransactionAt": "2022-10-30 06:25",
|
||||
"lastTransactionAmount": 1,
|
||||
"profile": "https://opencollective.com/william-frank-monroy-mamani",
|
||||
"name": "William Frank Monroy Mamani",
|
||||
"company": null,
|
||||
"description": null,
|
||||
"image": "/sponsors/347048.jpg",
|
||||
"email": null,
|
||||
"twitter": null,
|
||||
"github": null,
|
||||
"website": null
|
||||
},
|
||||
{
|
||||
"MemberId": 374896,
|
||||
"createdAt": "2022-12-22 05:28",
|
||||
"type": "ORGANIZATION",
|
||||
"role": "BACKER",
|
||||
"tier": "bronze sponsor 🥉",
|
||||
"isActive": true,
|
||||
"totalAmountDonated": 70,
|
||||
"currency": "USD",
|
||||
"lastTransactionAt": "2023-07-02 09:04",
|
||||
"lastTransactionAmount": 10,
|
||||
"profile": "https://opencollective.com/chartbrew",
|
||||
"name": "Chartbrew",
|
||||
"company": null,
|
||||
"description": "Open source visualization and reporting platform",
|
||||
"image": "/sponsors/374896.jpg",
|
||||
"twitter": null,
|
||||
"github": null,
|
||||
"website": "https://chartbrew.com"
|
||||
},
|
||||
{
|
||||
"MemberId": 375034,
|
||||
"createdAt": "2022-12-22 13:16",
|
||||
"type": "USER",
|
||||
"role": "BACKER",
|
||||
"isActive": true,
|
||||
"totalAmountDonated": 50,
|
||||
"currency": "USD",
|
||||
"lastTransactionAt": "2022-12-22 13:16",
|
||||
"lastTransactionAmount": 50,
|
||||
"profile": "https://opencollective.com/jan-wagebach",
|
||||
"name": "PRISMA European Capacity Platform GmbH",
|
||||
"company": "PRISMA European Capacity Platform GmbH",
|
||||
"description": null,
|
||||
"image": "/sponsors/375034.jpg",
|
||||
"email": null,
|
||||
"twitter": null,
|
||||
"github": null,
|
||||
"website": null
|
||||
},
|
||||
{
|
||||
"MemberId": 395990,
|
||||
"createdAt": "2023-02-11 13:12",
|
||||
"type": "ORGANIZATION",
|
||||
"role": "BACKER",
|
||||
"tier": "Gold Sponsor 🥇",
|
||||
"isActive": true,
|
||||
"totalAmountDonated": 100,
|
||||
"currency": "USD",
|
||||
"lastTransactionAt": "2023-02-11 13:12",
|
||||
"lastTransactionAmount": 100,
|
||||
"profile": "https://opencollective.com/likn",
|
||||
"name": "LIKN",
|
||||
"company": null,
|
||||
"description": "LIKN is a powerful, but simple Web3 component and protocol. Connect Web2 content to Web3, and solely mint content NFTs by URL. Share and Trade in different platforms and marketplaces. OpenAI empowers LIKN which can automatically generate NFT metadata. ",
|
||||
"image": "/sponsors/395990.jpg",
|
||||
"twitter": "https://twitter.com/oxlikn",
|
||||
"github": "https://github.com/0xLIKN",
|
||||
"website": "https://www.likn.co/?ref=opencollective"
|
||||
},
|
||||
{
|
||||
"MemberId": 404415,
|
||||
"createdAt": "2023-03-01 16:22",
|
||||
"type": "USER",
|
||||
"role": "BACKER",
|
||||
"tier": "Backer 🖤",
|
||||
"isActive": true,
|
||||
"totalAmountDonated": 1,
|
||||
"currency": "USD",
|
||||
"lastTransactionAt": "2023-03-01 16:22",
|
||||
"lastTransactionAmount": 1,
|
||||
"profile": "https://opencollective.com/rharison-lucas-moreira-abreu",
|
||||
"name": "Rharison Lucas Moreira Abreu",
|
||||
"company": null,
|
||||
"description": null,
|
||||
"image": "/sponsors/404415.jpg",
|
||||
"email": null,
|
||||
"twitter": null,
|
||||
"github": null,
|
||||
"website": null
|
||||
},
|
||||
{
|
||||
"MemberId": 407510,
|
||||
"createdAt": "2023-03-07 22:27",
|
||||
"type": "USER",
|
||||
"role": "BACKER",
|
||||
"tier": "Backer 🖤",
|
||||
"isActive": true,
|
||||
"totalAmountDonated": 1,
|
||||
"currency": "USD",
|
||||
"lastTransactionAt": "2023-03-07 22:27",
|
||||
"lastTransactionAmount": 1,
|
||||
"profile": "https://opencollective.com/higherror",
|
||||
"name": "HighError",
|
||||
"company": null,
|
||||
"description": null,
|
||||
"image": "/sponsors/407510.jpg",
|
||||
"email": null,
|
||||
"twitter": null,
|
||||
"github": "https://github.com/HighError",
|
||||
"website": null
|
||||
},
|
||||
{
|
||||
"MemberId": 409558,
|
||||
"createdAt": "2023-03-12 21:41",
|
||||
"type": "USER",
|
||||
"role": "BACKER",
|
||||
"tier": "Backer 🖤",
|
||||
"isActive": true,
|
||||
"totalAmountDonated": 1,
|
||||
"currency": "USD",
|
||||
"lastTransactionAt": "2023-03-12 21:41",
|
||||
"lastTransactionAmount": 1,
|
||||
"profile": "https://opencollective.com/guest-6133a45c",
|
||||
"name": "Kristof",
|
||||
"company": null,
|
||||
"description": null,
|
||||
"image": "/sponsors/undefined",
|
||||
"email": null,
|
||||
"twitter": null,
|
||||
"github": null,
|
||||
"website": null
|
||||
},
|
||||
{
|
||||
"MemberId": 438158,
|
||||
"createdAt": "2023-05-31 22:44",
|
||||
"type": "USER",
|
||||
"role": "BACKER",
|
||||
"tier": "Backer 🖤",
|
||||
"isActive": true,
|
||||
"totalAmountDonated": 5,
|
||||
"currency": "USD",
|
||||
"lastTransactionAt": "2023-05-31 22:44",
|
||||
"lastTransactionAmount": 5,
|
||||
"profile": "https://opencollective.com/elektrnik",
|
||||
"name": "ELEKTRNIK",
|
||||
"company": null,
|
||||
"description": null,
|
||||
"image": "/sponsors/438158.jpg",
|
||||
"email": null,
|
||||
"twitter": null,
|
||||
"github": null,
|
||||
"website": null
|
||||
},
|
||||
{
|
||||
"MemberId": 438877,
|
||||
"createdAt": "2023-06-02 17:56",
|
||||
"type": "USER",
|
||||
"role": "BACKER",
|
||||
"tier": "Backer 🖤",
|
||||
"isActive": true,
|
||||
"totalAmountDonated": 1,
|
||||
"currency": "USD",
|
||||
"lastTransactionAt": "2023-06-02 17:56",
|
||||
"lastTransactionAmount": 1,
|
||||
"profile": "https://opencollective.com/guest-425370ea",
|
||||
"name": "Gabriel Jiménez",
|
||||
"company": null,
|
||||
"description": null,
|
||||
"image": "/sponsors/undefined",
|
||||
"email": null,
|
||||
"twitter": null,
|
||||
"github": null,
|
||||
"website": null
|
||||
},
|
||||
{
|
||||
"MemberId": 439182,
|
||||
"createdAt": "2023-06-03 15:15",
|
||||
"type": "USER",
|
||||
"role": "BACKER",
|
||||
"tier": "Backer 🖤",
|
||||
"isActive": true,
|
||||
"totalAmountDonated": 1,
|
||||
"currency": "USD",
|
||||
"lastTransactionAt": "2023-06-03 15:15",
|
||||
"lastTransactionAmount": 1,
|
||||
"profile": "https://opencollective.com/jay-thanawut1",
|
||||
"name": "Jay Thanawut",
|
||||
"company": null,
|
||||
"description": null,
|
||||
"image": "/sponsors/439182.jpg",
|
||||
"email": null,
|
||||
"twitter": null,
|
||||
"github": null,
|
||||
"website": null
|
||||
},
|
||||
{
|
||||
"MemberId": 442878,
|
||||
"createdAt": "2023-06-13 13:51",
|
||||
"type": "USER",
|
||||
"role": "BACKER",
|
||||
"tier": "Backer 🖤",
|
||||
"isActive": true,
|
||||
"totalAmountDonated": 5,
|
||||
"currency": "USD",
|
||||
"lastTransactionAt": "2023-06-13 13:51",
|
||||
"lastTransactionAmount": 5,
|
||||
"profile": "https://opencollective.com/encurme",
|
||||
"name": "EncurMe",
|
||||
"company": "EncurMe",
|
||||
"description": null,
|
||||
"image": "/sponsors/442878.jpg",
|
||||
"email": null,
|
||||
"twitter": null,
|
||||
"github": null,
|
||||
"website": "https://encur.me/"
|
||||
},
|
||||
{
|
||||
"MemberId": 571230,
|
||||
"createdAt": "2024-06-01 18:47",
|
||||
"type": "ORGANIZATION",
|
||||
"role": "BACKER",
|
||||
"tier": "Gold Sponsor 🥇",
|
||||
"isActive": true,
|
||||
"totalAmountDonated": 100,
|
||||
"currency": "USD",
|
||||
"lastTransactionAt": "2024-06-01 18:47",
|
||||
"lastTransactionAmount": 100,
|
||||
"profile": "https://opencollective.com/coderabbit",
|
||||
"name": "CodeRabbit",
|
||||
"company": null,
|
||||
"description": "CodeRabbit is an AI-driven context-aware code reviewer that provides line-by-line feedback and smart chat. ",
|
||||
"image": "/sponsors/571230.jpg",
|
||||
"twitter": null,
|
||||
"github": null,
|
||||
"website": "https://coderabbit.ai"
|
||||
}
|
||||
]
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Next UI
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,36 +0,0 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
[http://localhost:3000/api/hello](http://localhost:3000/api/hello) is an endpoint that uses [Route Handlers](https://beta.nextjs.org/docs/routing/route-handlers). This endpoint can be edited in `app/api/hello/route.ts`.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
@@ -1,132 +0,0 @@
|
||||
import type {Metadata} from "next";
|
||||
|
||||
import {notFound} from "next/navigation";
|
||||
import {allBlogPosts} from "contentlayer2/generated";
|
||||
import {Link, User} from "@heroui/react";
|
||||
import {format, parseISO} from "date-fns";
|
||||
import NextLink from "next/link";
|
||||
import {Balancer} from "react-wrap-balancer";
|
||||
|
||||
import {__DEV__, __PREVIEW__} from "@/utils";
|
||||
import {MDXContent} from "@/components/mdx-content";
|
||||
import {siteConfig} from "@/config/site";
|
||||
import {Route} from "@/libs/docs/page";
|
||||
import {ChevronRightLinearIcon} from "@/components/icons";
|
||||
|
||||
interface BlogPostProps {
|
||||
params: {
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
const isDraftVisible = __DEV__ || __PREVIEW__;
|
||||
|
||||
async function getBlogPostFromParams({params}: BlogPostProps) {
|
||||
const slug = params.slug || "";
|
||||
const post = allBlogPosts.find((post) => post.slugAsParams === slug);
|
||||
|
||||
if (!post) {
|
||||
null;
|
||||
}
|
||||
|
||||
const currentRoute: Route = {
|
||||
key: post?._id,
|
||||
title: post?.title,
|
||||
path: `/${post?._raw?.sourceFilePath}`,
|
||||
};
|
||||
|
||||
return {post, currentRoute};
|
||||
}
|
||||
|
||||
export async function generateMetadata({params}: BlogPostProps): Promise<Metadata> {
|
||||
const {post} = await getBlogPostFromParams({params});
|
||||
|
||||
if (!post) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
type: "article",
|
||||
url: post.url,
|
||||
images: [
|
||||
{
|
||||
url: post.imageAsParams || siteConfig.ogImage,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: post.title || siteConfig.name,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
images: [siteConfig.ogImage],
|
||||
creator: siteConfig.creator,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateStaticParams(): Promise<BlogPostProps["params"][]> {
|
||||
return allBlogPosts.map((doc) => ({
|
||||
slug: doc.slugAsParams,
|
||||
}));
|
||||
}
|
||||
|
||||
export default async function DocPage({params}: BlogPostProps) {
|
||||
const {post} = await getBlogPostFromParams({params});
|
||||
|
||||
if (!post || (post.draft && !isDraftVisible)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full mt-12 flex flex-col justify-start items-center prose prose-neutral">
|
||||
<div className="w-full max-w-4xl">
|
||||
<Link
|
||||
isBlock
|
||||
as={NextLink}
|
||||
className="mb-8 -ml-3 text-default-500 hover:text-default-900"
|
||||
color="foreground"
|
||||
href="/blog"
|
||||
size="sm"
|
||||
>
|
||||
<ChevronRightLinearIcon className="rotate-180 inline-block mr-1" size={15} />
|
||||
Back to blog
|
||||
</Link>
|
||||
|
||||
<time className="block text-small mb-2 text-default-500" dateTime={post.date}>
|
||||
{format(parseISO(post.date), "LLLL d, yyyy")}
|
||||
</time>
|
||||
<div className="mb-3 flex w-full flex-col items-start">
|
||||
<User
|
||||
isExternal
|
||||
as={Link}
|
||||
avatarProps={{
|
||||
className: "w-9 h-9 text-large",
|
||||
src: post.author?.avatar,
|
||||
}}
|
||||
className="hover:opacity-100"
|
||||
classNames={{
|
||||
base: "-ml-2 px-2 py-1.5 hover:bg-default-100 dark:hover:bg-default-50 cursor-pointer transition-colors",
|
||||
name: "text-foreground",
|
||||
}}
|
||||
description={post.author?.username}
|
||||
href={post.author?.link}
|
||||
name={post.author?.name}
|
||||
/>
|
||||
</div>
|
||||
<h1 className="mb-2 font-bold text-4xl">
|
||||
<Balancer>{post.title}</Balancer>
|
||||
<strong className="text-default-300">{post?.draft && " (Draft)"}</strong>
|
||||
</h1>
|
||||
<MDXContent code={post.body.code} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import {Image} from "@heroui/react";
|
||||
|
||||
import {ScriptProviders} from "@/components/scripts/script-providers";
|
||||
|
||||
interface DocsLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function DocsLayout({children}: DocsLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<main className="relative container mx-auto max-w-7xl z-10 px-6 min-h-[calc(100vh_-_64px_-_108px)] mb-12 flex-grow">
|
||||
{children}
|
||||
</main>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="fixed hidden dark:md:block dark:opacity-70 -bottom-[40%] -left-[20%] z-0"
|
||||
>
|
||||
<Image
|
||||
removeWrapper
|
||||
alt="docs left background"
|
||||
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/images/docs-left.png"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="fixed hidden dark:md:block dark:opacity-70 -top-[80%] -right-[60%] 2xl:-top-[60%] 2xl:-right-[45%] z-0 rotate-12"
|
||||
>
|
||||
<Image
|
||||
removeWrapper
|
||||
alt="docs right background"
|
||||
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/images/docs-right.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ScriptProviders />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import {allBlogPosts} from "contentlayer2/generated";
|
||||
import {compareDesc} from "date-fns";
|
||||
|
||||
import {BlogPostList} from "@/components/blog-post";
|
||||
import {__DEV__, __PREVIEW__} from "@/utils";
|
||||
|
||||
const isDraftVisible = __DEV__ || __PREVIEW__;
|
||||
|
||||
export default function Blog() {
|
||||
const posts = allBlogPosts
|
||||
.sort((a, b) => compareDesc(new Date(a.date), new Date(b.date)))
|
||||
?.filter((post) => {
|
||||
if (post.draft && !isDraftVisible) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full lg:px-16 mt-12">
|
||||
<div className="text-center">
|
||||
<h1 className="mb-2 font-bold text-4xl">HeroUI Latest Updates</h1>
|
||||
<h5 className="text-default-500 text-lg">All the latest news about HeroUI.</h5>
|
||||
</div>
|
||||
<BlogPostList posts={posts} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import type {Metadata} from "next";
|
||||
|
||||
import {notFound} from "next/navigation";
|
||||
import {allDocs} from "contentlayer2/generated";
|
||||
import {Link} from "@heroui/react";
|
||||
|
||||
import {MDXContent} from "@/components/mdx-content";
|
||||
import {siteConfig} from "@/config/site";
|
||||
import {DocsPager, DocsToc} from "@/components/docs";
|
||||
import {Route} from "@/libs/docs/page";
|
||||
import {GITHUB_URL, REPO_NAME} from "@/libs/github/constants";
|
||||
import {CONTENT_PATH, TAG} from "@/libs/docs/config";
|
||||
import {getHeadings} from "@/libs/docs/utils";
|
||||
|
||||
interface DocPageProps {
|
||||
params: {
|
||||
slug: string[];
|
||||
};
|
||||
}
|
||||
|
||||
async function getDocFromParams({params}: DocPageProps) {
|
||||
const slug = params.slug?.join("/") || "";
|
||||
const doc = allDocs.find((doc) => doc.slugAsParams === slug);
|
||||
|
||||
if (!doc) {
|
||||
null;
|
||||
}
|
||||
|
||||
const headings = getHeadings(doc?.body.raw);
|
||||
|
||||
const currentRoute: Route = {
|
||||
key: doc?._id,
|
||||
title: doc?.title,
|
||||
path: `/${doc?._raw?.sourceFilePath}`,
|
||||
};
|
||||
|
||||
return {doc, headings, currentRoute};
|
||||
}
|
||||
|
||||
export async function generateMetadata({params}: DocPageProps): Promise<Metadata> {
|
||||
const {doc} = await getDocFromParams({params});
|
||||
|
||||
if (!doc) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
title: doc.title,
|
||||
description: doc.description,
|
||||
openGraph: {
|
||||
title: doc.title,
|
||||
description: doc.description,
|
||||
type: "article",
|
||||
url: doc.url,
|
||||
images: [
|
||||
{
|
||||
url: siteConfig.ogImage,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: siteConfig.name,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: doc.title,
|
||||
description: doc.description,
|
||||
images: [siteConfig.ogImage],
|
||||
creator: siteConfig.creator,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateStaticParams(): Promise<DocPageProps["params"][]> {
|
||||
return allDocs.map((doc) => ({
|
||||
slug: doc.slugAsParams.split("/"),
|
||||
}));
|
||||
}
|
||||
|
||||
export default async function DocPage({params}: DocPageProps) {
|
||||
const {doc, headings, currentRoute} = await getDocFromParams({params});
|
||||
|
||||
if (!doc) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const editUrl = `${GITHUB_URL}/${REPO_NAME}/edit/${TAG}${CONTENT_PATH}${currentRoute?.path}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="col-span-12 lg:col-span-10 xl:col-span-8 lg:px-16 mt-10">
|
||||
<div className="w-full prose prose-neutral">
|
||||
<MDXContent code={doc.body.code} />
|
||||
</div>
|
||||
{currentRoute && <DocsPager currentRoute={currentRoute} />}
|
||||
<footer>
|
||||
<Link isExternal showAnchorIcon href={editUrl} size="sm">
|
||||
Edit this page on GitHub
|
||||
</Link>
|
||||
</footer>
|
||||
</div>
|
||||
{headings && headings.length > 0 && (
|
||||
<div className="hidden z-10 xl:flex xl:col-span-2 mt-8 pl-0">
|
||||
<DocsToc headings={headings} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import {Image} from "@heroui/react";
|
||||
|
||||
import manifest from "@/config/routes.json";
|
||||
import {DocsSidebar} from "@/components/docs/sidebar";
|
||||
import {ScriptProviders} from "@/components/scripts/script-providers";
|
||||
|
||||
interface DocsLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function DocsLayout({children}: DocsLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<main className="relative container mx-auto max-w-8xl z-10 px-6 min-h-[calc(100vh_-_64px_-_108px)] mb-12 flex-grow">
|
||||
<div className="grid grid-cols-12">
|
||||
<div className="hidden overflow-visible relative z-10 lg:block lg:col-span-2 mt-8 pr-4">
|
||||
<DocsSidebar routes={manifest.routes} />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="fixed hidden dark:md:block dark:opacity-100 -bottom-[30%] -left-[30%] z-0"
|
||||
>
|
||||
<Image
|
||||
removeWrapper
|
||||
alt="docs left background"
|
||||
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/images/docs-left.png"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="fixed hidden dark:md:block dark:opacity-70 -top-[50%] -right-[60%] 2xl:-top-[60%] 2xl:-right-[45%] z-0 rotate-12"
|
||||
>
|
||||
<Image
|
||||
removeWrapper
|
||||
alt="docs right background"
|
||||
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/images/docs-right.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ScriptProviders />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import {Autocomplete, AutocompleteItem} from "@heroui/react";
|
||||
import {useAsyncList} from "@react-stately/data";
|
||||
|
||||
type SWCharacter = {
|
||||
name: string;
|
||||
height: string;
|
||||
mass: string;
|
||||
birth_year: string;
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
let list = useAsyncList<SWCharacter>({
|
||||
async load({signal, filterText}) {
|
||||
let res = await fetch(`https://swapi.py4e.com/api/people/?search=${filterText}`, {signal});
|
||||
let json = await res.json();
|
||||
|
||||
return {
|
||||
items: json.results,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Autocomplete
|
||||
className="max-w-xs"
|
||||
inputValue={list.filterText}
|
||||
isLoading={list.isLoading}
|
||||
items={list.items}
|
||||
label="Select a character"
|
||||
placeholder="Type to search..."
|
||||
variant="bordered"
|
||||
onInputChange={list.setFilterText}
|
||||
>
|
||||
{(item) => (
|
||||
<AutocompleteItem key={item.name} className="capitalize">
|
||||
{item.name}
|
||||
</AutocompleteItem>
|
||||
)}
|
||||
</Autocomplete>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import {Autocomplete, AutocompleteItem} from "@heroui/react";
|
||||
import {useInfiniteScroll} from "@heroui/use-infinite-scroll";
|
||||
|
||||
type Pokemon = {
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type UsePokemonListProps = {
|
||||
/** Delay to wait before fetching more items */
|
||||
fetchDelay?: number;
|
||||
};
|
||||
|
||||
function usePokemonList({fetchDelay = 0}: UsePokemonListProps = {}) {
|
||||
const [items, setItems] = React.useState<Pokemon[]>([]);
|
||||
const [hasMore, setHasMore] = React.useState(true);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [offset, setOffset] = React.useState(0);
|
||||
const limit = 10; // Number of items per page, adjust as necessary
|
||||
|
||||
const loadPokemon = async (currentOffset: number) => {
|
||||
const controller = new AbortController();
|
||||
const {signal} = controller;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
if (offset > 0) {
|
||||
// Delay to simulate network latency
|
||||
await new Promise((resolve) => setTimeout(resolve, fetchDelay));
|
||||
}
|
||||
|
||||
let res = await fetch(
|
||||
`https://pokeapi.co/api/v2/pokemon?offset=${currentOffset}&limit=${limit}`,
|
||||
{signal},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
|
||||
let json = await res.json();
|
||||
|
||||
setHasMore(json.next !== null);
|
||||
// Append new results to existing ones
|
||||
setItems((prevItems) => [...prevItems, ...json.results]);
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
if (error.name === "AbortError") {
|
||||
console.log("Fetch aborted");
|
||||
} else {
|
||||
console.error("There was an error with the fetch operation:", error);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
loadPokemon(offset);
|
||||
}, []);
|
||||
|
||||
const onLoadMore = () => {
|
||||
const newOffset = offset + limit;
|
||||
|
||||
setOffset(newOffset);
|
||||
loadPokemon(newOffset);
|
||||
};
|
||||
|
||||
return {
|
||||
items,
|
||||
hasMore,
|
||||
isLoading,
|
||||
onLoadMore,
|
||||
};
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const {items, hasMore, isLoading, onLoadMore} = usePokemonList({fetchDelay: 1500});
|
||||
|
||||
const [, scrollerRef] = useInfiniteScroll({
|
||||
hasMore,
|
||||
isEnabled: isOpen,
|
||||
shouldUseLoader: false, // We don't want to show the loader at the bottom of the list
|
||||
onLoadMore,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Autocomplete
|
||||
className="max-w-xs"
|
||||
defaultItems={items}
|
||||
isLoading={isLoading}
|
||||
label="Pick a Pokemon"
|
||||
placeholder="Select a Pokemon"
|
||||
scrollRef={scrollerRef}
|
||||
variant="bordered"
|
||||
onOpenChange={setIsOpen}
|
||||
>
|
||||
{(item) => (
|
||||
<AutocompleteItem key={item.name} className="capitalize">
|
||||
{item.name}
|
||||
</AutocompleteItem>
|
||||
)}
|
||||
</Autocomplete>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import {Autocomplete, AutocompleteItem, MenuTriggerAction} from "@heroui/react";
|
||||
import {useFilter} from "@react-aria/i18n";
|
||||
|
||||
const animals = [
|
||||
{label: "Cat", value: "cat", description: "The second most popular pet in the world"},
|
||||
{label: "Dog", value: "dog", description: "The most popular pet in the world"},
|
||||
{label: "Elephant", value: "elephant", description: "The largest land animal"},
|
||||
{label: "Lion", value: "lion", description: "The king of the jungle"},
|
||||
{label: "Tiger", value: "tiger", description: "The largest cat species"},
|
||||
{label: "Giraffe", value: "giraffe", description: "The tallest land animal"},
|
||||
{
|
||||
label: "Dolphin",
|
||||
value: "dolphin",
|
||||
description: "A widely distributed and diverse group of aquatic mammals",
|
||||
},
|
||||
{label: "Penguin", value: "penguin", description: "A group of aquatic flightless birds"},
|
||||
{label: "Zebra", value: "zebra", description: "A several species of African equids"},
|
||||
{
|
||||
label: "Shark",
|
||||
value: "shark",
|
||||
description: "A group of elasmobranch fish characterized by a cartilaginous skeleton",
|
||||
},
|
||||
{
|
||||
label: "Whale",
|
||||
value: "whale",
|
||||
description: "Diverse group of fully aquatic placental marine mammals",
|
||||
},
|
||||
{label: "Otter", value: "otter", description: "A carnivorous mammal in the subfamily Lutrinae"},
|
||||
{label: "Crocodile", value: "crocodile", description: "A large semiaquatic reptile"},
|
||||
];
|
||||
|
||||
type FieldState = {
|
||||
selectedKey: React.Key | null;
|
||||
inputValue: string;
|
||||
items: typeof animals;
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
// Store ComboBox input value, selected option, open state, and items
|
||||
// in a state tracker
|
||||
const [fieldState, setFieldState] = React.useState<FieldState>({
|
||||
selectedKey: "",
|
||||
inputValue: "",
|
||||
items: animals,
|
||||
});
|
||||
|
||||
// Implement custom filtering logic and control what items are
|
||||
// available to the Autocomplete.
|
||||
const {startsWith} = useFilter({sensitivity: "base"});
|
||||
|
||||
// Specify how each of the Autocomplete values should change when an
|
||||
// option is selected from the list box
|
||||
const onSelectionChange = (key: React.Key | null) => {
|
||||
setFieldState((prevState) => {
|
||||
let selectedItem = prevState.items.find((option) => option.value === key);
|
||||
|
||||
return {
|
||||
inputValue: selectedItem?.label || "",
|
||||
selectedKey: key,
|
||||
items: animals.filter((item) => startsWith(item.label, selectedItem?.label || "")),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Specify how each of the Autocomplete values should change when the input
|
||||
// field is altered by the user
|
||||
const onInputChange = (value: string) => {
|
||||
setFieldState((prevState) => ({
|
||||
inputValue: value,
|
||||
selectedKey: value === "" ? null : prevState.selectedKey,
|
||||
items: animals.filter((item) => startsWith(item.label, value)),
|
||||
}));
|
||||
};
|
||||
|
||||
// Show entire list if user opens the menu manually
|
||||
const onOpenChange = (isOpen: boolean, menuTrigger: MenuTriggerAction) => {
|
||||
if (menuTrigger === "manual" && isOpen) {
|
||||
setFieldState((prevState) => ({
|
||||
inputValue: prevState.inputValue,
|
||||
selectedKey: prevState.selectedKey,
|
||||
items: animals,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Autocomplete
|
||||
className="max-w-xs"
|
||||
inputValue={fieldState.inputValue}
|
||||
items={fieldState.items}
|
||||
label="Favorite Animal"
|
||||
placeholder="Select an animal"
|
||||
selectedKey={fieldState.selectedKey}
|
||||
variant="bordered"
|
||||
onInputChange={onInputChange}
|
||||
onOpenChange={onOpenChange}
|
||||
onSelectionChange={onSelectionChange}
|
||||
>
|
||||
{(item) => <AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>}
|
||||
</Autocomplete>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {Card, CardBody, CircularProgress} from "@heroui/react";
|
||||
|
||||
export default function ButtonDemo() {
|
||||
return (
|
||||
<main className="dark bg-background text-foreground">
|
||||
<div className="flex w-screen h-screen items-center justify-center">
|
||||
<Card className="w-[240px] h-[240px] bg-default-200 dark:bg-default-50">
|
||||
<CardBody className="justify-center items-center py-0">
|
||||
<CircularProgress
|
||||
classNames={{
|
||||
svg: "w-36 h-36 drop-shadow-md",
|
||||
indicator: "stroke-foreground",
|
||||
track: "stroke-foreground/10",
|
||||
value: "text-3xl font-semibold text-foreground",
|
||||
}}
|
||||
showValueLabel={true}
|
||||
strokeWidth={4}
|
||||
value={70}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Button,
|
||||
useDisclosure,
|
||||
RadioGroup,
|
||||
Radio,
|
||||
ModalProps,
|
||||
} from "@heroui/react";
|
||||
import {useState} from "react";
|
||||
|
||||
export default function Page() {
|
||||
const {isOpen, onOpen, onOpenChange} = useDisclosure();
|
||||
const [modalPlacement, setModalPlacement] = useState("auto");
|
||||
|
||||
return (
|
||||
<div className="flex px-10 min-h-[80vh] justify-center items-center flex-col gap-4">
|
||||
<Button className="max-w-fit" onPress={onOpen}>
|
||||
Open Modal
|
||||
</Button>
|
||||
<RadioGroup
|
||||
label="Select modal placement"
|
||||
orientation="horizontal"
|
||||
value={modalPlacement}
|
||||
onValueChange={setModalPlacement}
|
||||
>
|
||||
<Radio value="auto">auto</Radio>
|
||||
<Radio value="top">top</Radio>
|
||||
<Radio value="bottom">bottom</Radio>
|
||||
<Radio value="center">center</Radio>
|
||||
<Radio value="top-center">top-center</Radio>
|
||||
<Radio value="bottom-center">bottom-center</Radio>
|
||||
</RadioGroup>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
placement={modalPlacement as ModalProps["placement"]}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">Modal Title</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non
|
||||
risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor
|
||||
quam.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non
|
||||
risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor
|
||||
quam.
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="light" onPress={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Button color="primary" onPress={onClose}>
|
||||
Action
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {Navbar, NavbarBrand, NavbarContent, NavbarItem, Link, Button} from "@heroui/react";
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Navbar isBordered>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
<Link href="#">Login</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
NavbarMenu,
|
||||
NavbarMenuItem,
|
||||
NavbarMenuToggle,
|
||||
Button,
|
||||
Link,
|
||||
} from "@heroui/react";
|
||||
import React from "react";
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
const [isMenuOpen, setIsMenuOpen] = React.useState<boolean | undefined>(false);
|
||||
|
||||
const menuItems = [
|
||||
"Profile",
|
||||
"Dashboard",
|
||||
"Activity",
|
||||
"Analytics",
|
||||
"System",
|
||||
"Deployments",
|
||||
"My Settings",
|
||||
"Team Settings",
|
||||
"Help & Feedback",
|
||||
"Log Out",
|
||||
];
|
||||
|
||||
return (
|
||||
<Navbar onMenuOpenChange={setIsMenuOpen}>
|
||||
<NavbarContent>
|
||||
<NavbarMenuToggle
|
||||
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
|
||||
className="sm:hidden"
|
||||
/>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
<Link href="#">Login</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarMenu>
|
||||
{menuItems.map((item, index) => (
|
||||
<NavbarMenuItem key={`${item}-${index}`}>
|
||||
<Link
|
||||
className="w-full"
|
||||
color={
|
||||
index === 2 ? "primary" : index === menuItems.length - 1 ? "danger" : "foreground"
|
||||
}
|
||||
href="#"
|
||||
size="lg"
|
||||
>
|
||||
{item}
|
||||
</Link>
|
||||
</NavbarMenuItem>
|
||||
))}
|
||||
</NavbarMenu>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {Navbar, NavbarBrand, NavbarContent, NavbarItem, Link, Button} from "@heroui/react";
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Navbar
|
||||
classNames={{
|
||||
item: [
|
||||
"flex",
|
||||
"relative",
|
||||
"h-full",
|
||||
"items-center",
|
||||
"data-[active=true]:after:content-['']",
|
||||
"data-[active=true]:after:absolute",
|
||||
"data-[active=true]:after:bottom-0",
|
||||
"data-[active=true]:after:left-0",
|
||||
"data-[active=true]:after:right-0",
|
||||
"data-[active=true]:after:h-[2px]",
|
||||
"data-[active=true]:after:rounded-[2px]",
|
||||
"data-[active=true]:after:bg-primary",
|
||||
],
|
||||
}}
|
||||
>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
<Link href="#">Login</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Button,
|
||||
Link,
|
||||
NavbarMenu,
|
||||
NavbarMenuItem,
|
||||
NavbarMenuToggle,
|
||||
} from "@heroui/react";
|
||||
import React from "react";
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
const [isMenuOpen, setIsMenuOpen] = React.useState<boolean | undefined>(false);
|
||||
|
||||
const menuItems = [
|
||||
"Profile",
|
||||
"Dashboard",
|
||||
"Activity",
|
||||
"Analytics",
|
||||
"System",
|
||||
"Deployments",
|
||||
"My Settings",
|
||||
"Team Settings",
|
||||
"Help & Feedback",
|
||||
"Log Out",
|
||||
];
|
||||
|
||||
return (
|
||||
<Navbar disableAnimation onMenuOpenChange={setIsMenuOpen}>
|
||||
<NavbarContent>
|
||||
<NavbarMenuToggle
|
||||
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
|
||||
className="sm:hidden"
|
||||
/>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
<Link href="#">Login</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarMenu>
|
||||
{menuItems.map((item, index) => (
|
||||
<NavbarMenuItem key={`${item}-${index}`}>
|
||||
<Link
|
||||
className="w-full"
|
||||
color={
|
||||
index === 2 ? "primary" : index === menuItems.length - 1 ? "danger" : "foreground"
|
||||
}
|
||||
href="#"
|
||||
size="lg"
|
||||
>
|
||||
{item}
|
||||
</Link>
|
||||
</NavbarMenuItem>
|
||||
))}
|
||||
</NavbarMenu>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {Navbar, NavbarBrand, NavbarContent, NavbarItem, Link, Button} from "@heroui/react";
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Navbar isBordered isBlurred={false}>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
<Link href="#">Login</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {Navbar, NavbarBrand, NavbarContent, NavbarItem, Link, Button} from "@heroui/react";
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Navbar shouldHideOnScroll>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
<Link href="#">Login</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
const Content = () => (
|
||||
<div className="px-6 flex gap-4 flex-col pb-16 flex-grow">
|
||||
<h1 className="mt-4 font-bold text-4xl">Lorem ipsum dolor sit amet</h1>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
|
||||
labore et dolore magna aliqua. Purus gravida quis blandit turpis. Augue neque gravida in
|
||||
fermentum et sollicitudin ac orci. Et sollicitudin ac orci phasellus egestas. Elementum tempus
|
||||
egestas sed sed risus pretium quam vulputate. Interdum velit euismod in pellentesque massa
|
||||
placerat duis ultricies.
|
||||
</p>
|
||||
<p>
|
||||
Rhoncus mattis rhoncus urna neque viverra justo nec ultrices dui. Praesent semper feugiat nibh
|
||||
sed pulvinar. Ultrices gravida dictum fusce ut placerat orci nulla pellentesque. Malesuada
|
||||
proin libero nunc consequat interdum varius sit amet. Lectus quam id leo in vitae. Sed viverra
|
||||
tellus in hac habitasse platea dictumst. Vivamus at augue eget arcu. Augue mauris augue neque
|
||||
gravida in.
|
||||
</p>
|
||||
<p>
|
||||
Tincidunt vitae semper quis lectus nulla at volutpat diam. Gravida dictum fusce ut placerat.
|
||||
Erat velit scelerisque in dictum non. Tempus quam pellentesque nec nam aliquam sem et tortor
|
||||
consequat. Eu nisl nunc mi ipsum faucibus. Cras fermentum odio eu feugiat pretium nibh. Vel
|
||||
pharetra vel turpis nunc eget lorem dolor sed viverra. Sollicitudin tempor id eu nisl nunc mi
|
||||
ipsum faucibus. Sed id semper risus in hendrerit gravida rutrum. Eget nulla facilisi etiam
|
||||
dignissim. Erat imperdiet sed euismod nisi. Risus in hendrerit gravida rutrum quisque non
|
||||
tellus orci ac.
|
||||
</p>
|
||||
<p>
|
||||
Tempor orci dapibus ultrices in iaculis nunc sed augue lacus. In pellentesque massa placerat
|
||||
duis ultricies. Sit amet massa vitae tortor condimentum. Morbi tincidunt augue interdum velit
|
||||
euismod. Aliquet enim tortor at auctor urna nunc id. A scelerisque purus semper eget. Vitae
|
||||
justo eget magna fermentum iaculis. Arcu non odio euismod lacinia at quis. Et leo duis ut diam
|
||||
quam nulla porttitor massa. Eget nunc scelerisque viverra mauris. Suscipit tellus mauris a
|
||||
diam maecenas sed enim. Cras sed felis eget velit aliquet. Est placerat in egestas erat
|
||||
imperdiet sed euismod nisi porta. In ante metus dictum at tempor commodo. In cursus turpis
|
||||
massa tincidunt dui ut ornare lectus. Tempus iaculis urna id volutpat. Iaculis eu non diam
|
||||
phasellus vestibulum lorem sed risus.
|
||||
</p>
|
||||
<p>
|
||||
Ridiculus mus mauris vitae ultricies leo integer malesuada nunc vel. Imperdiet massa tincidunt
|
||||
nunc pulvinar sapien et ligula ullamcorper malesuada. Faucibus pulvinar elementum integer enim
|
||||
neque volutpat. Gravida arcu ac tortor dignissim convallis aenean. Lectus quam id leo in
|
||||
vitae. Ultricies tristique nulla aliquet enim tortor. Nec tincidunt praesent semper feugiat
|
||||
nibh sed. Imperdiet proin fermentum leo vel orci porta non pulvinar neque. Praesent semper
|
||||
feugiat nibh sed pulvinar proin gravida. Dis parturient montes nascetur ridiculus mus mauris.
|
||||
Rhoncus dolor purus non enim praesent elementum facilisis leo vel. Ut lectus arcu bibendum at.
|
||||
Integer enim neque volutpat ac. Diam sit amet nisl suscipit. Eros donec ac odio tempor orci
|
||||
dapibus ultrices in iaculis. Ullamcorper a lacus vestibulum sed arcu non odio euismod. Quis
|
||||
lectus nulla at volutpat diam ut. Turpis egestas integer eget aliquet. Adipiscing tristique
|
||||
risus nec feugiat in fermentum posuere. Morbi tempus iaculis urna id. Amet commodo nulla
|
||||
facilisi nullam vehicula ipsum a arcu.
|
||||
</p>
|
||||
<p>
|
||||
Rhoncus mattis rhoncus urna neque viverra justo nec ultrices dui. Praesent semper feugiat nibh
|
||||
sed pulvinar. Ultrices gravida dictum fusce ut placerat orci nulla pellentesque. Malesuada
|
||||
proin libero nunc consequat interdum varius sit amet. Lectus quam id leo in vitae. Sed viverra
|
||||
tellus in hac habitasse platea dictumst. Vivamus at augue eget arcu. Augue mauris augue neque
|
||||
gravida in.
|
||||
</p>
|
||||
<p>
|
||||
Tincidunt vitae semper quis lectus nulla at volutpat diam. Gravida dictum fusce ut placerat.
|
||||
Erat velit scelerisque in dictum non. Tempus quam pellentesque nec nam aliquam sem et tortor
|
||||
consequat. Eu nisl nunc mi ipsum faucibus. Cras fermentum odio eu feugiat pretium nibh. Vel
|
||||
pharetra vel turpis nunc eget lorem dolor sed viverra. Sollicitudin tempor id eu nisl nunc mi
|
||||
ipsum faucibus. Sed id semper risus in hendrerit gravida rutrum. Eget nulla facilisi etiam
|
||||
dignissim. Erat imperdiet sed euismod nisi. Risus in hendrerit gravida rutrum quisque non
|
||||
tellus orci ac.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function NavbarExamplesLayout({children}: {children: React.ReactNode}) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{children}
|
||||
<Content />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {Navbar, NavbarBrand, NavbarContent, NavbarItem, Link, Button} from "@heroui/react";
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Navbar position="static">
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
<Link href="#">Login</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {Navbar, NavbarBrand, NavbarContent, NavbarItem, Link, Button} from "@heroui/react";
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Navbar>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
<Link href="#">Login</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link,
|
||||
DropdownItem,
|
||||
DropdownTrigger,
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
Avatar,
|
||||
} from "@heroui/react";
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Navbar>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" color="secondary" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent as="div" justify="end">
|
||||
<Dropdown placement="bottom-end">
|
||||
<DropdownTrigger>
|
||||
<Avatar
|
||||
isBordered
|
||||
as="button"
|
||||
className="transition-transform"
|
||||
color="secondary"
|
||||
name="Jason Hughes"
|
||||
size="sm"
|
||||
src="https://i.pravatar.cc/150?u=a042581f4e29026704d"
|
||||
/>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Profile Actions" variant="flat">
|
||||
<DropdownItem key="profile" className="h-14 gap-2">
|
||||
<p className="font-semibold">Signed in as</p>
|
||||
<p className="font-semibold">zoey@example.com</p>
|
||||
</DropdownItem>
|
||||
<DropdownItem key="settings">My Settings</DropdownItem>
|
||||
<DropdownItem key="team_settings">Team Settings</DropdownItem>
|
||||
<DropdownItem key="analytics">Analytics</DropdownItem>
|
||||
<DropdownItem key="system">System</DropdownItem>
|
||||
<DropdownItem key="configurations">Configurations</DropdownItem>
|
||||
<DropdownItem key="help_and_feedback">Help & Feedback</DropdownItem>
|
||||
<DropdownItem key="logout" color="danger">
|
||||
Log Out
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link,
|
||||
Button,
|
||||
DropdownItem,
|
||||
DropdownTrigger,
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
} from "@heroui/react";
|
||||
import {ChevronDown, Lock, Activity, Flash, Server, TagUser, Scale} from "@heroui/shared-icons";
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
const icons = {
|
||||
chevron: <ChevronDown fill="currentColor" size={16} />,
|
||||
scale: <Scale className="text-warning" fill="currentColor" size={30} />,
|
||||
lock: <Lock className="text-success" fill="currentColor" size={30} />,
|
||||
activity: <Activity className="text-secondary" fill="currentColor" size={30} />,
|
||||
flash: <Flash className="text-primary" fill="currentColor" size={30} />,
|
||||
server: <Server className="text-success" fill="currentColor" size={30} />,
|
||||
user: <TagUser className="text-danger" fill="currentColor" size={30} />,
|
||||
};
|
||||
|
||||
return (
|
||||
<Navbar>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<Dropdown>
|
||||
<NavbarItem>
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
disableRipple
|
||||
className="p-0 bg-transparent data-[hover=true]:bg-transparent"
|
||||
endContent={icons.chevron}
|
||||
radius="sm"
|
||||
variant="light"
|
||||
>
|
||||
Features
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
</NavbarItem>
|
||||
<DropdownMenu
|
||||
aria-label="ACME features"
|
||||
className="w-[340px]"
|
||||
itemClasses={{
|
||||
base: "gap-4",
|
||||
}}
|
||||
>
|
||||
<DropdownItem
|
||||
key="autoscaling"
|
||||
description="ACME scales apps to meet user demand, automagically, based on load."
|
||||
startContent={icons.scale}
|
||||
>
|
||||
Autoscaling
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="usage_metrics"
|
||||
description="Real-time metrics to debug issues. Slow query added? We’ll show you exactly where."
|
||||
startContent={icons.activity}
|
||||
>
|
||||
Usage Metrics
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="production_ready"
|
||||
description="ACME runs on ACME, join us and others serving requests at web scale."
|
||||
startContent={icons.flash}
|
||||
>
|
||||
Production Ready
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="99_uptime"
|
||||
description="Applications stay on the grid with high availability and high uptime guarantees."
|
||||
startContent={icons.server}
|
||||
>
|
||||
+99% Uptime
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="supreme_support"
|
||||
description="Overcome any challenge with a supporting team ready to respond."
|
||||
startContent={icons.user}
|
||||
>
|
||||
+Supreme Support
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
<Link href="#">Login</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link,
|
||||
Button,
|
||||
NavbarMenuToggle,
|
||||
NavbarMenu,
|
||||
NavbarMenuItem,
|
||||
} from "@heroui/react";
|
||||
import React from "react";
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
const [isMenuOpen, setIsMenuOpen] = React.useState<boolean | undefined>(false);
|
||||
|
||||
const menuItems = [
|
||||
"Profile",
|
||||
"Dashboard",
|
||||
"Activity",
|
||||
"Analytics",
|
||||
"System",
|
||||
"Deployments",
|
||||
"My Settings",
|
||||
"Team Settings",
|
||||
"Help & Feedback",
|
||||
"Log Out",
|
||||
];
|
||||
|
||||
return (
|
||||
<Navbar onMenuOpenChange={setIsMenuOpen}>
|
||||
<NavbarContent>
|
||||
<NavbarMenuToggle
|
||||
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
|
||||
className="sm:hidden"
|
||||
/>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
<Link href="#">Login</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarMenu>
|
||||
{menuItems.map((item, index) => (
|
||||
<NavbarMenuItem key={`${item}-${index}`}>
|
||||
<Link
|
||||
className="w-full"
|
||||
color={
|
||||
index === 2 ? "primary" : index === menuItems.length - 1 ? "danger" : "foreground"
|
||||
}
|
||||
href="#"
|
||||
size="lg"
|
||||
>
|
||||
{item}
|
||||
</Link>
|
||||
</NavbarMenuItem>
|
||||
))}
|
||||
</NavbarMenu>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link,
|
||||
Input,
|
||||
DropdownItem,
|
||||
DropdownTrigger,
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
Avatar,
|
||||
} from "@heroui/react";
|
||||
|
||||
import {SearchLinearIcon} from "@/components/icons";
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Navbar isBordered>
|
||||
<NavbarContent justify="start">
|
||||
<NavbarBrand className="mr-4">
|
||||
<AcmeLogo />
|
||||
<p className="hidden sm:block font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden sm:flex gap-3">
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" color="secondary" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</NavbarContent>
|
||||
<NavbarContent as="div" className="items-center" justify="end">
|
||||
<Input
|
||||
classNames={{
|
||||
base: "max-w-full sm:max-w-[10rem] h-10",
|
||||
input: "text-small",
|
||||
inputWrapper:
|
||||
"h-full font-normal text-default-500 bg-default-400/20 dark:bg-default-500/20",
|
||||
}}
|
||||
placeholder="Type to search..."
|
||||
startContent={<SearchLinearIcon size={18} />}
|
||||
type="search"
|
||||
/>
|
||||
<Dropdown placement="bottom-end">
|
||||
<NavbarItem>
|
||||
<DropdownTrigger>
|
||||
<Avatar
|
||||
isBordered
|
||||
as="button"
|
||||
className="transition-transform"
|
||||
color="secondary"
|
||||
name="Jason Hughes"
|
||||
size="sm"
|
||||
src="https://i.pravatar.cc/150?u=a042581f4e29026704d"
|
||||
/>
|
||||
</DropdownTrigger>
|
||||
</NavbarItem>
|
||||
<DropdownMenu aria-label="Profile Actions" variant="flat">
|
||||
<DropdownItem key="profile" className="h-14 gap-2">
|
||||
<p className="font-semibold">Signed in as</p>
|
||||
<p className="font-semibold">zoey@example.com</p>
|
||||
</DropdownItem>
|
||||
<DropdownItem key="settings">My Settings</DropdownItem>
|
||||
<DropdownItem key="team_settings">Team Settings</DropdownItem>
|
||||
<DropdownItem key="analytics">Analytics</DropdownItem>
|
||||
<DropdownItem key="system">System</DropdownItem>
|
||||
<DropdownItem key="configurations">Configurations</DropdownItem>
|
||||
<DropdownItem key="help_and_feedback">Help & Feedback</DropdownItem>
|
||||
<DropdownItem key="logout" color="danger">
|
||||
Log Out
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -1,561 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
RadioGroup,
|
||||
Radio,
|
||||
Button,
|
||||
Accordion,
|
||||
Tabs,
|
||||
Textarea,
|
||||
Input,
|
||||
Tab,
|
||||
Avatar,
|
||||
Select,
|
||||
SelectItem,
|
||||
AccordionItem,
|
||||
Pagination,
|
||||
extendVariants,
|
||||
PaginationItem,
|
||||
} from "@heroui/react";
|
||||
import {useFilter} from "@react-aria/i18n";
|
||||
import {useEffect, useMemo, useRef, useState} from "react";
|
||||
import {useSearchParams} from "next/navigation";
|
||||
|
||||
import {SearchLinearIcon} from "@/components/icons";
|
||||
|
||||
const MyRadioGroup = () => {
|
||||
const [radio, setRadio] = useState("1");
|
||||
|
||||
return (
|
||||
<RadioGroup value={radio} onValueChange={setRadio}>
|
||||
<Radio value="1">Radio 1</Radio>
|
||||
<Radio value="2">Radio 2</Radio>
|
||||
<Radio value="3">Radio 3</Radio>
|
||||
<Radio value="4">Radio 4</Radio>
|
||||
<Radio value="5">Radio 5</Radio>
|
||||
<Radio value="6">Radio 6</Radio>
|
||||
<Radio value="7">Radio 7</Radio>
|
||||
<Radio value="8">Radio 8</Radio>
|
||||
<Radio value="9">Radio 9</Radio>
|
||||
<Radio value="10">Radio 10</Radio>
|
||||
<Radio value="11">Radio 11</Radio>
|
||||
<Radio value="12">Radio 12</Radio>
|
||||
<Radio value="13">Radio 13</Radio>
|
||||
<Radio value="14">Radio 14</Radio>
|
||||
<Radio value="15">Radio 15</Radio>
|
||||
<Radio value="16">Radio 16</Radio>
|
||||
<Radio value="17">Radio 17</Radio>
|
||||
<Radio value="18">Radio 18</Radio>
|
||||
<Radio value="19">Radio 19</Radio>
|
||||
<Radio value="20">Radio 20</Radio>
|
||||
<Radio value="21">Radio 21</Radio>
|
||||
<Radio value="22">Radio 22</Radio>
|
||||
<Radio value="23">Radio 23</Radio>
|
||||
<Radio value="24">Radio 24</Radio>
|
||||
<Radio value="25">Radio 25</Radio>
|
||||
<Radio value="26">Radio 26</Radio>
|
||||
<Radio value="27">Radio 27</Radio>
|
||||
<Radio value="28">Radio 28</Radio>
|
||||
<Radio value="29">Radio 29</Radio>
|
||||
<Radio value="30">Radio 30</Radio>
|
||||
<Radio value="31">Radio 31</Radio>
|
||||
<Radio value="32">Radio 32</Radio>
|
||||
<Radio value="33">Radio 33</Radio>
|
||||
<Radio value="34">Radio 34</Radio>
|
||||
<Radio value="35">Radio 35</Radio>
|
||||
<Radio value="36">Radio 36</Radio>
|
||||
<Radio value="37">Radio 37</Radio>
|
||||
<Radio value="38">Radio 38</Radio>
|
||||
<Radio value="39">Radio 39</Radio>
|
||||
<Radio value="40">Radio 40</Radio>
|
||||
<Radio value="41">Radio 41</Radio>
|
||||
<Radio value="42">Radio 42</Radio>
|
||||
<Radio value="43">Radio 43</Radio>
|
||||
<Radio value="44">Radio 44</Radio>
|
||||
<Radio value="45">Radio 45</Radio>
|
||||
<Radio value="46">Radio 46</Radio>
|
||||
<Radio value="47">Radio 47</Radio>
|
||||
<Radio value="48">Radio 48</Radio>
|
||||
<Radio value="49">Radio 49</Radio>
|
||||
<Radio value="50">Radio 50</Radio>
|
||||
<Radio value="51">Radio 51</Radio>
|
||||
<Radio value="52">Radio 52</Radio>
|
||||
<Radio value="53">Radio 53</Radio>
|
||||
<Radio value="54">Radio 54</Radio>
|
||||
<Radio value="55">Radio 55</Radio>
|
||||
<Radio value="56">Radio 56</Radio>
|
||||
<Radio value="57">Radio 57</Radio>
|
||||
<Radio value="58">Radio 58</Radio>
|
||||
<Radio value="59">Radio 59</Radio>
|
||||
<Radio value="60">Radio 60</Radio>
|
||||
<Radio value="61">Radio 61</Radio>
|
||||
<Radio value="62">Radio 62</Radio>
|
||||
</RadioGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const MyInput = extendVariants(Input, {
|
||||
variants: {
|
||||
color: {
|
||||
stone: {
|
||||
inputWrapper: [
|
||||
"bg-zinc-100",
|
||||
"border",
|
||||
"shadow",
|
||||
"transition-colors",
|
||||
"focus-within:bg-zinc-100",
|
||||
"data-[hover=true]:border-zinc-600",
|
||||
"data-[hover=true]:bg-zinc-100",
|
||||
"group-data-[focus=true]:border-zinc-600",
|
||||
// dark theme
|
||||
"dark:bg-zinc-900",
|
||||
"dark:border-zinc-800",
|
||||
"dark:data-[hover=true]:bg-zinc-900",
|
||||
"dark:focus-within:bg-zinc-900",
|
||||
],
|
||||
input: [
|
||||
"text-zinc-800",
|
||||
"placeholder:text-zinc-600",
|
||||
// dark theme
|
||||
"dark:text-zinc-400",
|
||||
"dark:placeholder:text-zinc-600",
|
||||
],
|
||||
},
|
||||
},
|
||||
size: {
|
||||
xs: {
|
||||
inputWrapper: "h-6 min-h-6 px-1",
|
||||
input: "text-tiny",
|
||||
},
|
||||
md: {
|
||||
inputWrapper: "h-10 min-h-10",
|
||||
input: "text-small",
|
||||
},
|
||||
xl: {
|
||||
inputWrapper: "h-14 min-h-14",
|
||||
input: "text-medium",
|
||||
},
|
||||
},
|
||||
radius: {
|
||||
xs: {
|
||||
inputWrapper: "rounded",
|
||||
},
|
||||
sm: {
|
||||
inputWrapper: "rounded-[4px]",
|
||||
},
|
||||
},
|
||||
textSize: {
|
||||
base: {
|
||||
input: "text-base",
|
||||
},
|
||||
},
|
||||
removeLabel: {
|
||||
true: {
|
||||
label: "hidden",
|
||||
},
|
||||
false: {},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
color: "stone",
|
||||
textSize: "base",
|
||||
removeLabel: true,
|
||||
},
|
||||
});
|
||||
|
||||
const MyButton2 = extendVariants(Button, {
|
||||
variants: {
|
||||
color: {
|
||||
foreground:
|
||||
"bg-foreground text-background data-[hover=true]:bg-foreground/90 data-[pressed=true]:bg-foreground/80",
|
||||
},
|
||||
isScalable: {
|
||||
true: "scale-125",
|
||||
false: "",
|
||||
},
|
||||
size: {
|
||||
xl: "size--xl",
|
||||
"2xl": "size--2xl",
|
||||
},
|
||||
mySize: {
|
||||
lg: "px-12 py-6 text-lg",
|
||||
xl: "px-12 py-6 text-xl",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
color: "foreground",
|
||||
},
|
||||
});
|
||||
|
||||
const usersData = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Tony Reichert",
|
||||
role: "CEO",
|
||||
team: "Management",
|
||||
status: "active",
|
||||
age: "29",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/1.png",
|
||||
email: "tony.reichert@example.com",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Zoey Lang",
|
||||
role: "Tech Lead",
|
||||
team: "Development",
|
||||
status: "paused",
|
||||
age: "25",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/1.png",
|
||||
email: "zoey.lang@example.com",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Jane Fisher",
|
||||
role: "Sr. Dev",
|
||||
team: "Development",
|
||||
status: "active",
|
||||
age: "22",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/2.png",
|
||||
email: "jane.fisher@example.com",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "William Howard",
|
||||
role: "C.M.",
|
||||
team: "Marketing",
|
||||
status: "vacation",
|
||||
age: "28",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/2.png",
|
||||
email: "william.howard@example.com",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Kristen Copper",
|
||||
role: "S. Manager",
|
||||
team: "Sales",
|
||||
status: "active",
|
||||
age: "24",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/3.png",
|
||||
email: "kristen.cooper@example.com",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Brian Kim",
|
||||
role: "P. Manager",
|
||||
team: "Management",
|
||||
age: "29",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/3.png",
|
||||
email: "brian.kim@example.com",
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "Michael Hunt",
|
||||
role: "Designer",
|
||||
team: "Design",
|
||||
status: "paused",
|
||||
age: "27",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/4.png",
|
||||
email: "michael.hunt@example.com",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "Samantha Brooks",
|
||||
role: "HR Manager",
|
||||
team: "HR",
|
||||
status: "active",
|
||||
age: "31",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/4.png",
|
||||
email: "samantha.brooks@example.com",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "Frank Harrison",
|
||||
role: "F. Manager",
|
||||
team: "Finance",
|
||||
status: "vacation",
|
||||
age: "33",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/5.png",
|
||||
email: "frank.harrison@example.com",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "Emma Adams",
|
||||
role: "Ops Manager",
|
||||
team: "Operations",
|
||||
status: "active",
|
||||
age: "35",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/5.png",
|
||||
email: "emma.adams@example.com",
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "Brandon Stevens",
|
||||
role: "Jr. Dev",
|
||||
team: "Development",
|
||||
status: "active",
|
||||
age: "22",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/7.png",
|
||||
email: "brandon.stevens@example.com",
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "Megan Richards",
|
||||
role: "P. Manager",
|
||||
team: "Product",
|
||||
status: "paused",
|
||||
age: "28",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/7.png",
|
||||
email: "megan.richards@example.com",
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "Oliver Scott",
|
||||
role: "S. Manager",
|
||||
team: "Security",
|
||||
status: "active",
|
||||
age: "37",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/8.png",
|
||||
email: "oliver.scott@example.com",
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "Grace Allen",
|
||||
role: "M. Specialist",
|
||||
team: "Marketing",
|
||||
status: "active",
|
||||
age: "30",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/8.png",
|
||||
email: "grace.allen@example.com",
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: "Noah Carter",
|
||||
role: "IT Specialist",
|
||||
team: "I. Technology",
|
||||
status: "paused",
|
||||
age: "31",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/9.png",
|
||||
email: "noah.carter@example.com",
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: "Ava Perez",
|
||||
role: "Manager",
|
||||
team: "Sales",
|
||||
status: "active",
|
||||
age: "29",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/9.png",
|
||||
email: "ava.perez@example.com",
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
name: "Liam Johnson",
|
||||
role: "Data Analyst",
|
||||
team: "Analysis",
|
||||
status: "active",
|
||||
age: "28",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/11.png",
|
||||
email: "liam.johnson@example.com",
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: "Sophia Taylor",
|
||||
role: "QA Analyst",
|
||||
team: "Testing",
|
||||
status: "active",
|
||||
age: "27",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/11.png",
|
||||
email: "sophia.taylor@example.com",
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
name: "Lucas Harris",
|
||||
role: "Administrator",
|
||||
team: "Information Technology",
|
||||
status: "paused",
|
||||
age: "32",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/12.png",
|
||||
email: "lucas.harris@example.com",
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
name: "Mia Robinson",
|
||||
role: "Coordinator",
|
||||
team: "Operations",
|
||||
status: "active",
|
||||
age: "26",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/12.png",
|
||||
email: "mia.robinson@example.com",
|
||||
},
|
||||
];
|
||||
|
||||
export default function HeroUIPerf() {
|
||||
const [textA, setTextA] = useState<string>("");
|
||||
const [textB, setTextB] = useState<string>("");
|
||||
const [textC, setTextC] = useState<string>("");
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [inputValue, setInputValue] = useState<string>();
|
||||
const [selectedKey, setSelectedKey] = useState<string>("");
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const page = Number(searchParams.get("page"));
|
||||
|
||||
let {startsWith} = useFilter({sensitivity: "base"});
|
||||
|
||||
const filteredItems = inputValue
|
||||
? usersData.filter((item) => startsWith(item.name, inputValue))
|
||||
: usersData;
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
isOpen && inputRef?.current?.focus();
|
||||
}, [isOpen]);
|
||||
|
||||
const handleSelectionChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setSelectedKey(e.target.value);
|
||||
};
|
||||
|
||||
const topContent = useMemo(() => {
|
||||
return (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
isClearable
|
||||
aria-activedescendant={selectedKey}
|
||||
aria-expanded={isOpen}
|
||||
aria-label="Search user"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
className="z-10 sticky top-1"
|
||||
placeholder="Search..."
|
||||
spellCheck={false}
|
||||
startContent={<SearchLinearIcon className="text-default-400" size={18} strokeWidth="2" />}
|
||||
type="text"
|
||||
onValueChange={setInputValue}
|
||||
/>
|
||||
);
|
||||
}, [inputRef, selectedKey, isOpen]);
|
||||
|
||||
return (
|
||||
<div className="w-full p-24 gap-4 flex flex-col">
|
||||
<Select
|
||||
classNames={{
|
||||
base: "max-w-xs",
|
||||
listboxWrapper: "scroll-pb-6 scroll-pt-28",
|
||||
}}
|
||||
items={filteredItems}
|
||||
label="Assigned to"
|
||||
labelPlacement="outside"
|
||||
listboxProps={{
|
||||
topContent,
|
||||
variant: "flat",
|
||||
classNames: {
|
||||
base: [
|
||||
"before:content-[''] before:rounded-t-medium before:fixed before:w-full before:h-14 before:z-10",
|
||||
"before:top-0 before:left-0 before:bg-gradient-to-b before:from-default-50",
|
||||
],
|
||||
},
|
||||
}}
|
||||
placeholder="Select a user"
|
||||
selectedKeys={[selectedKey]}
|
||||
showScrollIndicators={false}
|
||||
variant="flat"
|
||||
onChange={handleSelectionChange}
|
||||
onOpenChange={setIsOpen}
|
||||
>
|
||||
{(item) => (
|
||||
<SelectItem key={item.id} textValue={item.name}>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Avatar alt={item.name} className="flex-shrink-0" size="sm" src={item.avatar} />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-small">{item.name}</span>
|
||||
<span className="text-tiny text-default-400">{item.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
|
||||
<Accordion>
|
||||
<AccordionItem key="1" aria-label="Accordion 1" title="Accordion 1">
|
||||
Non est aliqua tempor occaecat laborum. Lorem culpa minim irure mollit. Est qui
|
||||
reprehenderit commodo magna proident anim ipsum ex. Mollit id amet officia nisi excepteur
|
||||
eu. Commodo elit commodo nisi nisi aute eu aliquip aliquip voluptate exercitation ullamco
|
||||
ipsum eiusmod veniam. Magna in laborum anim amet anim ex elit aliqua nostrud mollit.
|
||||
Pariatur ullamco cillum proident aliqua nostrud. Labore ea veniam cillum duis veniam in
|
||||
cupidatat voluptate eu officia. Ut laborum sunt nostrud magna. Ex magna esse cillum enim
|
||||
incididunt pariatur qui veniam dolor. Exercitation id culpa et enim mollit duis duis
|
||||
aliquip. Magna ullamco est cupidatat laboris irure pariatur aliquip duis aute cillum.
|
||||
Officia irure do laboris ea nisi sunt reprehenderit laboris irure. Ex eiusmod in duis
|
||||
veniam excepteur. Sunt et et laboris culpa. Mollit excepteur occaecat elit anim officia.
|
||||
Laborum commodo proident cupidatat pariatur eu veniam id qui do culpa. Quis consectetur
|
||||
adipisicing anim ex ea velit excepteur. Deserunt laboris ex aute sunt laborum tempor ea
|
||||
enim dolore ut in. Id aliqua Lorem exercitation qui velit nostrud anim reprehenderit enim.
|
||||
Nisi elit fugiat deserunt elit. Sit excepteur ipsum enim excepteur irure irure sint veniam
|
||||
elit consequat ea id. Lorem ea qui sunt enim occaecat excepteur officia ex consequat
|
||||
nostrud. Tempor sint Lorem est culpa do.
|
||||
</AccordionItem>
|
||||
<AccordionItem key="2" aria-label="Accordion 2" title="Accordion 2">
|
||||
Non est aliqua tempor occaecat laborum. Lorem culpa minim irure mollit. Est qui
|
||||
reprehenderit commodo magna proident anim ipsum ex. Mollit id amet officia nisi excepteur
|
||||
eu. Commodo elit commodo nisi nisi aute eu aliquip aliquip voluptate exercitation ullamco
|
||||
ipsum eiusmod veniam. Magna in laborum anim amet anim ex elit aliqua nostrud mollit.
|
||||
Pariatur ullamco cillum proident aliqua nostrud. Labore ea veniam cillum duis veniam in
|
||||
cupidatat voluptate eu officia. Ut laborum sunt nostrud magna. Ex magna esse cillum enim
|
||||
</AccordionItem>
|
||||
<AccordionItem key="3" aria-label="Accordion 3" title="Accordion 3">
|
||||
Non est aliqua tempor occaecat laborum. Lorem culpa minim irure mollit. Est qui
|
||||
reprehenderit commodo magna proident anim ipsum ex. Mollit id amet officia nisi excepteur
|
||||
eu. Commodo elit commodo nisi nisi aute eu aliquip aliquip voluptate exercitation ullamco
|
||||
ipsum eiusmod veniam. Magna in laborum anim amet anim ex elit aliqua nostrud mollit.
|
||||
Pariatur ullamco cillum proident aliqua nostrud. Labore ea veniam cillum duis veniam in
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<Tabs classNames={{panel: "flex flex-col gap-5"}} variant="underlined">
|
||||
<Tab title="Test 1">
|
||||
<Textarea defaultValue="ASdasd" label="Default value (uncontrolled)" />
|
||||
<Input label="Text B Tab 1" value={textB} onValueChange={setTextB} />
|
||||
<Textarea label="Text C Tab 1" value={textC} onValueChange={setTextC} />
|
||||
</Tab>
|
||||
<Tab title="Test 2">
|
||||
<Textarea label="Text B Tab 2" value={textB} onValueChange={setTextB} />
|
||||
<Textarea label="Text C Tab 2" value={textC} onValueChange={setTextC} />
|
||||
</Tab>
|
||||
<Tab title="Test 3">
|
||||
<Textarea label="Text B Tab 3" value={textB} onValueChange={setTextB} />
|
||||
<Textarea label="Text C Tab 3" value={textC} onValueChange={setTextC} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<h2>Outside</h2>
|
||||
<Textarea label="Text A" placeholder="Text A" value={textA} onValueChange={setTextA} />
|
||||
<Textarea label="Text B" placeholder="Text B" value={textB} onValueChange={setTextB} />
|
||||
<Textarea label="Text C" placeholder="Text C" value={textC} onValueChange={setTextC} />
|
||||
|
||||
<MyRadioGroup />
|
||||
|
||||
<MyInput
|
||||
isClearable
|
||||
placeholder="Search..."
|
||||
radius="md"
|
||||
size="md"
|
||||
startContent={<SearchLinearIcon className="text-zinc-500" size={16} />}
|
||||
/>
|
||||
|
||||
<Button>Click Me!</Button>
|
||||
|
||||
<MyButton2 color="primary">Press Me!</MyButton2>
|
||||
|
||||
<Pagination
|
||||
showControls
|
||||
initialPage={page ?? 1}
|
||||
renderItem={({page, ...itemProps}) => {
|
||||
return <PaginationItem href={`/examples/perf?page=${page}`} {...itemProps} />;
|
||||
}}
|
||||
total={10}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import {Suspense} from "react";
|
||||
|
||||
import ClientPage from "./client-page";
|
||||
|
||||
export default function PerfPage() {
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<ClientPage />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
"use client";
|
||||
|
||||
import {Select, SelectItem} from "@heroui/react";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useInfiniteScroll} from "@heroui/use-infinite-scroll";
|
||||
|
||||
type Pokemon = {
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type UsePokemonListProps = {
|
||||
/** Delay to wait before fetching more items */
|
||||
fetchDelay?: number;
|
||||
};
|
||||
|
||||
function usePokemonList({fetchDelay = 0}: UsePokemonListProps = {}) {
|
||||
const [items, setItems] = useState<Pokemon[]>([]);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const limit = 10; // Number of items per page, adjust as necessary
|
||||
|
||||
const loadPokemon = async (currentOffset: number) => {
|
||||
const controller = new AbortController();
|
||||
const {signal} = controller;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
if (offset > 0) {
|
||||
// Delay to simulate network latency
|
||||
await new Promise((resolve) => setTimeout(resolve, fetchDelay));
|
||||
}
|
||||
|
||||
let res = await fetch(
|
||||
`https://pokeapi.co/api/v2/pokemon?offset=${currentOffset}&limit=${limit}`,
|
||||
{signal},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
|
||||
let json = await res.json();
|
||||
|
||||
setHasMore(json.next !== null);
|
||||
// Append new results to existing ones
|
||||
setItems((prevItems) => [...prevItems, ...json.results]);
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
if (error.name === "AbortError") {
|
||||
console.log("Fetch aborted");
|
||||
} else {
|
||||
console.error("There was an error with the fetch operation:", error);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadPokemon(offset);
|
||||
}, []);
|
||||
|
||||
const onLoadMore = () => {
|
||||
const newOffset = offset + limit;
|
||||
|
||||
setOffset(newOffset);
|
||||
loadPokemon(newOffset);
|
||||
};
|
||||
|
||||
return {
|
||||
items,
|
||||
hasMore,
|
||||
isLoading,
|
||||
onLoadMore,
|
||||
};
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const {items, hasMore, isLoading, onLoadMore} = usePokemonList({fetchDelay: 1500});
|
||||
|
||||
const [, scrollerRef] = useInfiniteScroll({
|
||||
hasMore,
|
||||
isEnabled: isOpen,
|
||||
shouldUseLoader: false, // We don't want to show the loader at the bottom of the list
|
||||
onLoadMore,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Select
|
||||
className="max-w-xs"
|
||||
isLoading={isLoading}
|
||||
items={items}
|
||||
label="Pick a Pokemon"
|
||||
placeholder="Select a Pokemon"
|
||||
scrollRef={scrollerRef}
|
||||
selectionMode="single"
|
||||
onOpenChange={setIsOpen}
|
||||
>
|
||||
{(item) => (
|
||||
<SelectItem key={item.name} className="capitalize">
|
||||
{item.name}
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableColumn,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
getKeyValue,
|
||||
Spinner,
|
||||
Pagination,
|
||||
} from "@heroui/react";
|
||||
import {useMemo, useState} from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
type SWCharacter = {
|
||||
name: string;
|
||||
height: string;
|
||||
mass: string;
|
||||
birth_year: string;
|
||||
};
|
||||
|
||||
const fetcher = (...args: Parameters<typeof fetch>) => fetch(...args).then((res) => res.json());
|
||||
|
||||
export default function Page() {
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const {data, isLoading} = useSWR<{
|
||||
count: number;
|
||||
results: SWCharacter[];
|
||||
}>(`https://swapi.py4e.com/api/people?page=${page}`, fetcher, {
|
||||
keepPreviousData: true,
|
||||
});
|
||||
|
||||
const rowsPerPage = 10;
|
||||
|
||||
const pages = useMemo(() => {
|
||||
return data?.count ? Math.ceil(data.count / rowsPerPage) : 0;
|
||||
}, [data?.count, rowsPerPage]);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Table
|
||||
aria-label="Example table with client async pagination"
|
||||
bottomContent={
|
||||
pages > 0 ? (
|
||||
<div className="flex w-full justify-center">
|
||||
<Pagination
|
||||
isCompact
|
||||
showControls
|
||||
showShadow
|
||||
color="primary"
|
||||
page={page}
|
||||
total={pages}
|
||||
onChange={(page) => setPage(page)}
|
||||
/>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
classNames={{
|
||||
table: "min-h-[400px]",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableColumn key="name">Name</TableColumn>
|
||||
<TableColumn key="height">Height</TableColumn>
|
||||
<TableColumn key="mass">Mass</TableColumn>
|
||||
<TableColumn key="birth_year">Birth year</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody
|
||||
isLoading={isLoading || data?.results.length === 0}
|
||||
items={data?.results ?? []}
|
||||
loadingContent={<Spinner />}
|
||||
>
|
||||
{(item) => (
|
||||
<TableRow key={item.name}>
|
||||
{(columnKey) => <TableCell>{getKeyValue(item, columnKey)}</TableCell>}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,579 +0,0 @@
|
||||
/* eslint-disable jsx-a11y/no-onchange */
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableColumn,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
Input,
|
||||
Button,
|
||||
DropdownTrigger,
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
Selection,
|
||||
Chip,
|
||||
User,
|
||||
ChipProps,
|
||||
Pagination,
|
||||
SortDescriptor,
|
||||
} from "@heroui/react";
|
||||
import {ChevronDownIcon, SearchIcon} from "@heroui/shared-icons";
|
||||
import {useCallback, useMemo, useState} from "react";
|
||||
import {capitalize} from "@heroui/shared-utils";
|
||||
|
||||
import {PlusLinearIcon} from "@/components/icons";
|
||||
import {VerticalDotsIcon} from "@/components/icons/vertical-dots";
|
||||
|
||||
const statusColorMap: Record<string, ChipProps["color"]> = {
|
||||
active: "success",
|
||||
paused: "danger",
|
||||
vacation: "warning",
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{name: "ID", uid: "id", sortable: true},
|
||||
{name: "NAME", uid: "name", sortable: true},
|
||||
{name: "AGE", uid: "age", sortable: true},
|
||||
{name: "ROLE", uid: "role", sortable: true},
|
||||
{name: "TEAM", uid: "team"},
|
||||
{name: "EMAIL", uid: "email"},
|
||||
{name: "STATUS", uid: "status", sortable: true},
|
||||
{name: "ACTIONS", uid: "actions"},
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
{name: "Active", uid: "active"},
|
||||
{name: "Paused", uid: "paused"},
|
||||
{name: "Vacation", uid: "vacation"},
|
||||
];
|
||||
|
||||
const INITIAL_VISIBLE_COLUMNS = ["name", "role", "status", "actions"];
|
||||
|
||||
const users = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Tony Reichert",
|
||||
role: "CEO",
|
||||
team: "Management",
|
||||
status: "active",
|
||||
age: "29",
|
||||
avatar: "https://i.pravatar.cc/150?u=a042581f4e29026024d",
|
||||
email: "tony.reichert@example.com",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Zoey Lang",
|
||||
role: "Tech Lead",
|
||||
team: "Development",
|
||||
status: "paused",
|
||||
age: "25",
|
||||
avatar: "https://i.pravatar.cc/150?u=a042581f4e29026704d",
|
||||
email: "zoey.lang@example.com",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Jane Fisher",
|
||||
role: "Sr. Dev",
|
||||
team: "Development",
|
||||
status: "active",
|
||||
age: "22",
|
||||
avatar: "https://i.pravatar.cc/150?u=a04258114e29026702d",
|
||||
email: "jane.fisher@example.com",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "William Howard",
|
||||
role: "C.M.",
|
||||
team: "Marketing",
|
||||
status: "vacation",
|
||||
age: "28",
|
||||
avatar: "https://i.pravatar.cc/150?u=a048581f4e29026701d",
|
||||
email: "william.howard@example.com",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Kristen Copper",
|
||||
role: "S. Manager",
|
||||
team: "Sales",
|
||||
status: "active",
|
||||
age: "24",
|
||||
avatar: "https://i.pravatar.cc/150?u=a092581d4ef9026700d",
|
||||
email: "kristen.cooper@example.com",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Brian Kim",
|
||||
role: "P. Manager",
|
||||
team: "Management",
|
||||
age: "29",
|
||||
avatar: "https://i.pravatar.cc/150?u=a042581f4e29026024d",
|
||||
email: "brian.kim@example.com",
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "Michael Hunt",
|
||||
role: "Designer",
|
||||
team: "Design",
|
||||
status: "paused",
|
||||
age: "27",
|
||||
avatar: "https://i.pravatar.cc/150?u=a042581f4e29027007d",
|
||||
email: "michael.hunt@example.com",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "Samantha Brooks",
|
||||
role: "HR Manager",
|
||||
team: "HR",
|
||||
status: "active",
|
||||
age: "31",
|
||||
avatar: "https://i.pravatar.cc/150?u=a042581f4e27027008d",
|
||||
email: "samantha.brooks@example.com",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "Frank Harrison",
|
||||
role: "F. Manager",
|
||||
team: "Finance",
|
||||
status: "vacation",
|
||||
age: "33",
|
||||
avatar: "https://i.pravatar.cc/150?img=4",
|
||||
email: "frank.harrison@example.com",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "Emma Adams",
|
||||
role: "Ops Manager",
|
||||
team: "Operations",
|
||||
status: "active",
|
||||
age: "35",
|
||||
avatar: "https://i.pravatar.cc/150?img=5",
|
||||
email: "emma.adams@example.com",
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "Brandon Stevens",
|
||||
role: "Jr. Dev",
|
||||
team: "Development",
|
||||
status: "active",
|
||||
age: "22",
|
||||
avatar: "https://i.pravatar.cc/150?img=8",
|
||||
email: "brandon.stevens@example.com",
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "Megan Richards",
|
||||
role: "P. Manager",
|
||||
team: "Product",
|
||||
status: "paused",
|
||||
age: "28",
|
||||
avatar: "https://i.pravatar.cc/150?img=10",
|
||||
email: "megan.richards@example.com",
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "Oliver Scott",
|
||||
role: "S. Manager",
|
||||
team: "Security",
|
||||
status: "active",
|
||||
age: "37",
|
||||
avatar: "https://i.pravatar.cc/150?img=12",
|
||||
email: "oliver.scott@example.com",
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "Grace Allen",
|
||||
role: "M. Specialist",
|
||||
team: "Marketing",
|
||||
status: "active",
|
||||
age: "30",
|
||||
avatar: "https://i.pravatar.cc/150?img=16",
|
||||
email: "grace.allen@example.com",
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: "Noah Carter",
|
||||
role: "IT Specialist",
|
||||
team: "I. Technology",
|
||||
status: "paused",
|
||||
age: "31",
|
||||
avatar: "https://i.pravatar.cc/150?img=15",
|
||||
email: "noah.carter@example.com",
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: "Ava Perez",
|
||||
role: "Manager",
|
||||
team: "Sales",
|
||||
status: "active",
|
||||
age: "29",
|
||||
avatar: "https://i.pravatar.cc/150?img=20",
|
||||
email: "ava.perez@example.com",
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
name: "Liam Johnson",
|
||||
role: "Data Analyst",
|
||||
team: "Analysis",
|
||||
status: "active",
|
||||
age: "28",
|
||||
avatar: "https://i.pravatar.cc/150?img=33",
|
||||
email: "liam.johnson@example.com",
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: "Sophia Taylor",
|
||||
role: "QA Analyst",
|
||||
team: "Testing",
|
||||
status: "active",
|
||||
age: "27",
|
||||
avatar: "https://i.pravatar.cc/150?img=29",
|
||||
email: "sophia.taylor@example.com",
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
name: "Lucas Harris",
|
||||
role: "Administrator",
|
||||
team: "Information Technology",
|
||||
status: "paused",
|
||||
age: "32",
|
||||
avatar: "https://i.pravatar.cc/150?img=50",
|
||||
email: "lucas.harris@example.com",
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
name: "Mia Robinson",
|
||||
role: "Coordinator",
|
||||
team: "Operations",
|
||||
status: "active",
|
||||
age: "26",
|
||||
avatar: "https://i.pravatar.cc/150?img=45",
|
||||
email: "mia.robinson@example.com",
|
||||
},
|
||||
];
|
||||
|
||||
type User = (typeof users)[number];
|
||||
|
||||
export default function Page() {
|
||||
const [filterValue, setFilterValue] = useState("");
|
||||
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set([]));
|
||||
const [visibleColumns, setVisibleColumns] = useState<Selection>(new Set(INITIAL_VISIBLE_COLUMNS));
|
||||
const [statusFilter, setStatusFilter] = useState<Selection>("all");
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
|
||||
column: "age",
|
||||
direction: "ascending",
|
||||
});
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const pages = Math.ceil(users.length / rowsPerPage);
|
||||
|
||||
const hasSearchFilter = Boolean(filterValue);
|
||||
|
||||
const headerColumns = useMemo(() => {
|
||||
if (visibleColumns === "all") return columns;
|
||||
|
||||
return columns.filter((column) => Array.from(visibleColumns).includes(column.uid));
|
||||
}, [visibleColumns]);
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
let filteredUsers = [...users];
|
||||
|
||||
if (hasSearchFilter) {
|
||||
filteredUsers = filteredUsers.filter((user) =>
|
||||
user.name.toLowerCase().includes(filterValue.toLowerCase()),
|
||||
);
|
||||
}
|
||||
if (statusFilter !== "all" && Array.from(statusFilter).length !== statusOptions.length) {
|
||||
filteredUsers = filteredUsers.filter((user) =>
|
||||
Array.from(statusFilter).includes(user.status),
|
||||
);
|
||||
}
|
||||
|
||||
return filteredUsers;
|
||||
}, [users, filterValue, statusFilter]);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const start = (page - 1) * rowsPerPage;
|
||||
const end = start + rowsPerPage;
|
||||
|
||||
return filteredItems.slice(start, end);
|
||||
}, [page, filteredItems, rowsPerPage]);
|
||||
|
||||
const sortedItems = useMemo(() => {
|
||||
return [...items].sort((a: User, b: User) => {
|
||||
const first = a[sortDescriptor.column as keyof User] as number;
|
||||
const second = b[sortDescriptor.column as keyof User] as number;
|
||||
const cmp = first < second ? -1 : first > second ? 1 : 0;
|
||||
|
||||
return sortDescriptor.direction === "descending" ? -cmp : cmp;
|
||||
});
|
||||
}, [sortDescriptor, items]);
|
||||
|
||||
const renderCell = useCallback((user: User, columnKey: React.Key) => {
|
||||
const cellValue = user[columnKey as keyof User];
|
||||
|
||||
switch (columnKey) {
|
||||
case "name":
|
||||
return (
|
||||
<User
|
||||
avatarProps={{radius: "full", size: "sm", src: user.avatar}}
|
||||
classNames={{
|
||||
description: "text-default-500",
|
||||
}}
|
||||
description={user.email}
|
||||
name={cellValue}
|
||||
>
|
||||
{user.email}
|
||||
</User>
|
||||
);
|
||||
case "role":
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<p className="text-bold text-small capitalize">{cellValue}</p>
|
||||
<p className="text-bold text-tiny capitalize text-default-500">{user.team}</p>
|
||||
</div>
|
||||
);
|
||||
case "status":
|
||||
return (
|
||||
<Chip
|
||||
className="capitalize border-none gap-1 text-default-600"
|
||||
color={statusColorMap[user.status]}
|
||||
size="sm"
|
||||
variant="dot"
|
||||
>
|
||||
{cellValue}
|
||||
</Chip>
|
||||
);
|
||||
case "actions":
|
||||
return (
|
||||
<div className="relative flex justify-end items-center gap-2">
|
||||
<Dropdown className="bg-background border-1 border-default-200">
|
||||
<DropdownTrigger>
|
||||
<Button isIconOnly radius="full" size="sm" variant="light">
|
||||
<VerticalDotsIcon className="text-default-400" />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu>
|
||||
<DropdownItem key="view">View</DropdownItem>
|
||||
<DropdownItem key="edit">Edit</DropdownItem>
|
||||
<DropdownItem key="delete">Delete</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return cellValue;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onRowsPerPageChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setRowsPerPage(Number(e.target.value));
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
const onSearchChange = useCallback((value?: string) => {
|
||||
if (value) {
|
||||
setFilterValue(value);
|
||||
setPage(1);
|
||||
} else {
|
||||
setFilterValue("");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const topContent = useMemo(() => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-between gap-3 items-end">
|
||||
<Input
|
||||
isClearable
|
||||
classNames={{
|
||||
base: "w-full sm:max-w-[44%]",
|
||||
inputWrapper: "border-1",
|
||||
}}
|
||||
placeholder="Search by name..."
|
||||
size="sm"
|
||||
startContent={<SearchIcon className="text-default-300" />}
|
||||
value={filterValue}
|
||||
variant="bordered"
|
||||
onClear={() => setFilterValue("")}
|
||||
onValueChange={onSearchChange}
|
||||
/>
|
||||
<div className="flex gap-3">
|
||||
<Dropdown>
|
||||
<DropdownTrigger className="hidden sm:flex">
|
||||
<Button
|
||||
endContent={<ChevronDownIcon className="text-small" />}
|
||||
size="sm"
|
||||
variant="flat"
|
||||
>
|
||||
Status
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
disallowEmptySelection
|
||||
aria-label="Table Columns"
|
||||
closeOnSelect={false}
|
||||
selectedKeys={statusFilter}
|
||||
selectionMode="multiple"
|
||||
onSelectionChange={setStatusFilter}
|
||||
>
|
||||
{statusOptions.map((status) => (
|
||||
<DropdownItem key={status.uid} className="capitalize">
|
||||
{capitalize(status.name)}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<Dropdown>
|
||||
<DropdownTrigger className="hidden sm:flex">
|
||||
<Button
|
||||
endContent={<ChevronDownIcon className="text-small" />}
|
||||
size="sm"
|
||||
variant="flat"
|
||||
>
|
||||
Columns
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
disallowEmptySelection
|
||||
aria-label="Table Columns"
|
||||
closeOnSelect={false}
|
||||
selectedKeys={visibleColumns}
|
||||
selectionMode="multiple"
|
||||
onSelectionChange={setVisibleColumns}
|
||||
>
|
||||
{columns.map((column) => (
|
||||
<DropdownItem key={column.uid} className="capitalize">
|
||||
{capitalize(column.name)}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<Button
|
||||
className="bg-foreground text-background"
|
||||
endContent={<PlusLinearIcon />}
|
||||
size="sm"
|
||||
>
|
||||
Add New
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-default-400 text-small">Total {users.length} users</span>
|
||||
<label className="flex items-center text-default-400 text-small">
|
||||
Rows per page:
|
||||
<select
|
||||
className="bg-transparent outline-none text-default-400 text-small"
|
||||
onChange={onRowsPerPageChange}
|
||||
>
|
||||
<option value="5">5</option>
|
||||
<option value="10">10</option>
|
||||
<option value="15">15</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [
|
||||
filterValue,
|
||||
statusFilter,
|
||||
visibleColumns,
|
||||
onSearchChange,
|
||||
onRowsPerPageChange,
|
||||
users.length,
|
||||
hasSearchFilter,
|
||||
]);
|
||||
|
||||
const bottomContent = useMemo(() => {
|
||||
return (
|
||||
<div className="py-2 px-2 flex justify-between items-center">
|
||||
<Pagination
|
||||
showControls
|
||||
classNames={{
|
||||
cursor: "bg-foreground text-background",
|
||||
}}
|
||||
color="default"
|
||||
isDisabled={hasSearchFilter}
|
||||
page={page}
|
||||
total={pages}
|
||||
variant="light"
|
||||
onChange={setPage}
|
||||
/>
|
||||
<span className="text-small text-default-400">
|
||||
{selectedKeys === "all"
|
||||
? "All items selected"
|
||||
: `${selectedKeys.size} of ${items.length} selected`}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}, [selectedKeys, items.length, page, pages, hasSearchFilter]);
|
||||
|
||||
const classNames = useMemo(
|
||||
() => ({
|
||||
wrapper: ["max-h-[382px]", "max-w-3xl"],
|
||||
th: ["bg-transparent", "text-default-500", "border-b", "border-divider"],
|
||||
td: [
|
||||
// changing the rows border radius
|
||||
// first
|
||||
"group-data-[first=true]:first:before:rounded-none",
|
||||
"group-data-[first=true]:last:before:rounded-none",
|
||||
// middle
|
||||
"group-data-[middle=true]:before:rounded-none",
|
||||
// last
|
||||
"group-data-[last=true]:first:before:rounded-none",
|
||||
"group-data-[last=true]:last:before:rounded-none",
|
||||
],
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Table
|
||||
isCompact
|
||||
removeWrapper
|
||||
aria-label="Example table with custom cells, pagination and sorting"
|
||||
bottomContent={bottomContent}
|
||||
bottomContentPlacement="outside"
|
||||
checkboxesProps={{
|
||||
classNames: {
|
||||
wrapper: "after:bg-foreground after:text-background text-background",
|
||||
},
|
||||
}}
|
||||
classNames={classNames}
|
||||
selectedKeys={selectedKeys}
|
||||
selectionMode="multiple"
|
||||
sortDescriptor={sortDescriptor}
|
||||
topContent={topContent}
|
||||
topContentPlacement="outside"
|
||||
onSelectionChange={setSelectedKeys}
|
||||
onSortChange={setSortDescriptor}
|
||||
>
|
||||
<TableHeader columns={headerColumns}>
|
||||
{(column) => (
|
||||
<TableColumn
|
||||
key={column.uid}
|
||||
align={column.uid === "actions" ? "center" : "start"}
|
||||
allowsSorting={column.sortable}
|
||||
>
|
||||
{column.name}
|
||||
</TableColumn>
|
||||
)}
|
||||
</TableHeader>
|
||||
<TableBody emptyContent={"No users found"} items={sortedItems}>
|
||||
{(item) => (
|
||||
<TableRow key={item.id}>
|
||||
{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableColumn,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
getKeyValue,
|
||||
Spinner,
|
||||
} from "@heroui/react";
|
||||
import {useAsyncList} from "@react-stately/data";
|
||||
import {useInfiniteScroll} from "@heroui/use-infinite-scroll";
|
||||
import {useState} from "react";
|
||||
|
||||
type SWCharacter = {
|
||||
name: string;
|
||||
height: string;
|
||||
mass: string;
|
||||
birth_year: string;
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [hasMore, setHasMore] = useState(false);
|
||||
|
||||
let list = useAsyncList<SWCharacter>({
|
||||
async load({signal, cursor}) {
|
||||
if (cursor) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
// If no cursor is available, then we're loading the first page.
|
||||
// Otherwise, the cursor is the next URL to load, as returned from the previous page.
|
||||
const res = await fetch(cursor || "https://swapi.py4e.com/api/people/?search=", {signal});
|
||||
let json = await res.json();
|
||||
|
||||
setHasMore(json.next !== null);
|
||||
|
||||
return {
|
||||
items: json.results,
|
||||
cursor: json.next,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const [loaderRef, scrollerRef] = useInfiniteScroll({hasMore, onLoadMore: list.loadMore});
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Table
|
||||
isHeaderSticky
|
||||
aria-label="Example table with infinite pagination"
|
||||
baseRef={scrollerRef}
|
||||
bottomContent={
|
||||
hasMore ? (
|
||||
<div className="flex w-full justify-center">
|
||||
<Spinner ref={loaderRef} color="white" />
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
classNames={{
|
||||
base: "max-h-[520px] overflow-scroll",
|
||||
table: "min-h-[400px]",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableColumn key="name">Name</TableColumn>
|
||||
<TableColumn key="height">Height</TableColumn>
|
||||
<TableColumn key="mass">Mass</TableColumn>
|
||||
<TableColumn key="birth_year">Birth year</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody
|
||||
isLoading={isLoading}
|
||||
items={list.items}
|
||||
loadingContent={<Spinner color="white" />}
|
||||
>
|
||||
{(item) => (
|
||||
<TableRow key={item.name}>
|
||||
{(columnKey) => <TableCell>{getKeyValue(item, columnKey)}</TableCell>}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableColumn,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
getKeyValue,
|
||||
Spinner,
|
||||
Button,
|
||||
} from "@heroui/react";
|
||||
import {useAsyncList} from "@react-stately/data";
|
||||
import {useState} from "react";
|
||||
|
||||
type SWCharacter = {
|
||||
name: string;
|
||||
height: string;
|
||||
mass: string;
|
||||
birth_year: string;
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
let list = useAsyncList<SWCharacter>({
|
||||
async load({signal, cursor}) {
|
||||
if (cursor) {
|
||||
setPage((prev) => prev + 1);
|
||||
}
|
||||
|
||||
// If no cursor is available, then we're loading the first page.
|
||||
// Otherwise, the cursor is the next URL to load, as returned from the previous page.
|
||||
const res = await fetch(cursor || "https://swapi.py4e.com/api/people/?search=", {signal});
|
||||
let json = await res.json();
|
||||
|
||||
if (!cursor) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
return {
|
||||
items: json.results,
|
||||
cursor: json.next,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const hasMore = page < 9;
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Table
|
||||
isHeaderSticky
|
||||
aria-label="Example table with client side sorting"
|
||||
bottomContent={
|
||||
hasMore && !isLoading ? (
|
||||
<div className="flex w-full justify-center">
|
||||
<Button isDisabled={list.isLoading} variant="flat" onPress={list.loadMore}>
|
||||
{list.isLoading && <Spinner color="white" size="sm" />}
|
||||
Load More
|
||||
</Button>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
classNames={{
|
||||
base: "max-h-[520px] overflow-scroll",
|
||||
table: "min-h-[420px]",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableColumn key="name">Name</TableColumn>
|
||||
<TableColumn key="height">Height</TableColumn>
|
||||
<TableColumn key="mass">Mass</TableColumn>
|
||||
<TableColumn key="birth_year">Birth year</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody
|
||||
isLoading={isLoading}
|
||||
items={list.items}
|
||||
loadingContent={<Spinner label="Loading..." />}
|
||||
>
|
||||
{(item) => (
|
||||
<TableRow key={item.name}>
|
||||
{(columnKey) => <TableCell>{getKeyValue(item, columnKey)}</TableCell>}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableColumn,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
getKeyValue,
|
||||
Spinner,
|
||||
} from "@heroui/react";
|
||||
import {useAsyncList} from "@react-stately/data";
|
||||
import {useState} from "react";
|
||||
|
||||
type SWCharacter = {
|
||||
name: string;
|
||||
height: string;
|
||||
mass: string;
|
||||
birth_year: string;
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
let list = useAsyncList<SWCharacter>({
|
||||
async load({signal}) {
|
||||
let res = await fetch(`https://swapi.py4e.com/api/people/?search`, {
|
||||
signal,
|
||||
});
|
||||
let json = await res.json();
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
return {
|
||||
items: json.results,
|
||||
};
|
||||
},
|
||||
async sort({items, sortDescriptor}) {
|
||||
return {
|
||||
items: items.sort((a, b) => {
|
||||
let first = a[sortDescriptor.column as keyof SWCharacter];
|
||||
let second = b[sortDescriptor.column as keyof SWCharacter];
|
||||
let cmp = (parseInt(first) || first) < (parseInt(second) || second) ? -1 : 1;
|
||||
|
||||
if (sortDescriptor.direction === "descending") {
|
||||
cmp *= -1;
|
||||
}
|
||||
|
||||
return cmp;
|
||||
}),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Table
|
||||
aria-label="Example table with client side sorting"
|
||||
classNames={{
|
||||
table: "min-h-[400px]",
|
||||
}}
|
||||
sortDescriptor={list.sortDescriptor}
|
||||
onSortChange={list.sort}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableColumn key="name" allowsSorting>
|
||||
Name
|
||||
</TableColumn>
|
||||
<TableColumn key="height" allowsSorting>
|
||||
Height
|
||||
</TableColumn>
|
||||
<TableColumn key="mass" allowsSorting>
|
||||
Mass
|
||||
</TableColumn>
|
||||
<TableColumn key="birth_year" allowsSorting>
|
||||
Birth year
|
||||
</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody
|
||||
isLoading={isLoading}
|
||||
items={list.items}
|
||||
loadingContent={<Spinner label="Loading..." />}
|
||||
>
|
||||
{(item) => (
|
||||
<TableRow key={item.name}>
|
||||
{(columnKey) => <TableCell>{getKeyValue(item, columnKey)}</TableCell>}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,553 +0,0 @@
|
||||
/* eslint-disable jsx-a11y/no-onchange */
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableColumn,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
Input,
|
||||
Button,
|
||||
DropdownTrigger,
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
Selection,
|
||||
Chip,
|
||||
User,
|
||||
ChipProps,
|
||||
Pagination,
|
||||
SortDescriptor,
|
||||
} from "@heroui/react";
|
||||
import {ChevronDownIcon, SearchIcon} from "@heroui/shared-icons";
|
||||
import {useCallback, useMemo, useState} from "react";
|
||||
import {capitalize} from "@heroui/shared-utils";
|
||||
|
||||
import {PlusLinearIcon} from "@/components/icons";
|
||||
import {VerticalDotsIcon} from "@/components/icons/vertical-dots";
|
||||
|
||||
const statusColorMap: Record<string, ChipProps["color"]> = {
|
||||
active: "success",
|
||||
paused: "danger",
|
||||
vacation: "warning",
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{name: "ID", uid: "id", sortable: true},
|
||||
{name: "NAME", uid: "name", sortable: true},
|
||||
{name: "AGE", uid: "age", sortable: true},
|
||||
{name: "ROLE", uid: "role", sortable: true},
|
||||
{name: "TEAM", uid: "team"},
|
||||
{name: "EMAIL", uid: "email"},
|
||||
{name: "STATUS", uid: "status", sortable: true},
|
||||
{name: "ACTIONS", uid: "actions"},
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
{name: "Active", uid: "active"},
|
||||
{name: "Paused", uid: "paused"},
|
||||
{name: "Vacation", uid: "vacation"},
|
||||
];
|
||||
|
||||
const INITIAL_VISIBLE_COLUMNS = ["name", "role", "status", "actions"];
|
||||
|
||||
const users = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Tony Reichert",
|
||||
role: "CEO",
|
||||
team: "Management",
|
||||
status: "active",
|
||||
age: "29",
|
||||
avatar: "https://i.pravatar.cc/150?u=a042581f4e29026024d",
|
||||
email: "tony.reichert@example.com",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Zoey Lang",
|
||||
role: "Tech Lead",
|
||||
team: "Development",
|
||||
status: "paused",
|
||||
age: "25",
|
||||
avatar: "https://i.pravatar.cc/150?u=a042581f4e29026704d",
|
||||
email: "zoey.lang@example.com",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Jane Fisher",
|
||||
role: "Sr. Dev",
|
||||
team: "Development",
|
||||
status: "active",
|
||||
age: "22",
|
||||
avatar: "https://i.pravatar.cc/150?u=a04258114e29026702d",
|
||||
email: "jane.fisher@example.com",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "William Howard",
|
||||
role: "C.M.",
|
||||
team: "Marketing",
|
||||
status: "vacation",
|
||||
age: "28",
|
||||
avatar: "https://i.pravatar.cc/150?u=a048581f4e29026701d",
|
||||
email: "william.howard@example.com",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Kristen Copper",
|
||||
role: "S. Manager",
|
||||
team: "Sales",
|
||||
status: "active",
|
||||
age: "24",
|
||||
avatar: "https://i.pravatar.cc/150?u=a092581d4ef9026700d",
|
||||
email: "kristen.cooper@example.com",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Brian Kim",
|
||||
role: "P. Manager",
|
||||
team: "Management",
|
||||
age: "29",
|
||||
avatar: "https://i.pravatar.cc/150?u=a042581f4e29026024d",
|
||||
email: "brian.kim@example.com",
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "Michael Hunt",
|
||||
role: "Designer",
|
||||
team: "Design",
|
||||
status: "paused",
|
||||
age: "27",
|
||||
avatar: "https://i.pravatar.cc/150?u=a042581f4e29027007d",
|
||||
email: "michael.hunt@example.com",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "Samantha Brooks",
|
||||
role: "HR Manager",
|
||||
team: "HR",
|
||||
status: "active",
|
||||
age: "31",
|
||||
avatar: "https://i.pravatar.cc/150?u=a042581f4e27027008d",
|
||||
email: "samantha.brooks@example.com",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "Frank Harrison",
|
||||
role: "F. Manager",
|
||||
team: "Finance",
|
||||
status: "vacation",
|
||||
age: "33",
|
||||
avatar: "https://i.pravatar.cc/150?img=4",
|
||||
email: "frank.harrison@example.com",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "Emma Adams",
|
||||
role: "Ops Manager",
|
||||
team: "Operations",
|
||||
status: "active",
|
||||
age: "35",
|
||||
avatar: "https://i.pravatar.cc/150?img=5",
|
||||
email: "emma.adams@example.com",
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "Brandon Stevens",
|
||||
role: "Jr. Dev",
|
||||
team: "Development",
|
||||
status: "active",
|
||||
age: "22",
|
||||
avatar: "https://i.pravatar.cc/150?img=8",
|
||||
email: "brandon.stevens@example.com",
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "Megan Richards",
|
||||
role: "P. Manager",
|
||||
team: "Product",
|
||||
status: "paused",
|
||||
age: "28",
|
||||
avatar: "https://i.pravatar.cc/150?img=10",
|
||||
email: "megan.richards@example.com",
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "Oliver Scott",
|
||||
role: "S. Manager",
|
||||
team: "Security",
|
||||
status: "active",
|
||||
age: "37",
|
||||
avatar: "https://i.pravatar.cc/150?img=12",
|
||||
email: "oliver.scott@example.com",
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "Grace Allen",
|
||||
role: "M. Specialist",
|
||||
team: "Marketing",
|
||||
status: "active",
|
||||
age: "30",
|
||||
avatar: "https://i.pravatar.cc/150?img=16",
|
||||
email: "grace.allen@example.com",
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: "Noah Carter",
|
||||
role: "IT Specialist",
|
||||
team: "I. Technology",
|
||||
status: "paused",
|
||||
age: "31",
|
||||
avatar: "https://i.pravatar.cc/150?img=15",
|
||||
email: "noah.carter@example.com",
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: "Ava Perez",
|
||||
role: "Manager",
|
||||
team: "Sales",
|
||||
status: "active",
|
||||
age: "29",
|
||||
avatar: "https://i.pravatar.cc/150?img=20",
|
||||
email: "ava.perez@example.com",
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
name: "Liam Johnson",
|
||||
role: "Data Analyst",
|
||||
team: "Analysis",
|
||||
status: "active",
|
||||
age: "28",
|
||||
avatar: "https://i.pravatar.cc/150?img=33",
|
||||
email: "liam.johnson@example.com",
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: "Sophia Taylor",
|
||||
role: "QA Analyst",
|
||||
team: "Testing",
|
||||
status: "active",
|
||||
age: "27",
|
||||
avatar: "https://i.pravatar.cc/150?img=29",
|
||||
email: "sophia.taylor@example.com",
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
name: "Lucas Harris",
|
||||
role: "Administrator",
|
||||
team: "Information Technology",
|
||||
status: "paused",
|
||||
age: "32",
|
||||
avatar: "https://i.pravatar.cc/150?img=50",
|
||||
email: "lucas.harris@example.com",
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
name: "Mia Robinson",
|
||||
role: "Coordinator",
|
||||
team: "Operations",
|
||||
status: "active",
|
||||
age: "26",
|
||||
avatar: "https://i.pravatar.cc/150?img=45",
|
||||
email: "mia.robinson@example.com",
|
||||
},
|
||||
];
|
||||
|
||||
type User = (typeof users)[number];
|
||||
|
||||
export default function Page() {
|
||||
const [filterValue, setFilterValue] = useState("");
|
||||
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set([]));
|
||||
const [visibleColumns, setVisibleColumns] = useState<Selection>(new Set(INITIAL_VISIBLE_COLUMNS));
|
||||
const [statusFilter, setStatusFilter] = useState<Selection>("all");
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
|
||||
column: "age",
|
||||
direction: "ascending",
|
||||
});
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const hasSearchFilter = Boolean(filterValue);
|
||||
|
||||
const headerColumns = useMemo(() => {
|
||||
if (visibleColumns === "all") return columns;
|
||||
|
||||
return columns.filter((column) => Array.from(visibleColumns).includes(column.uid));
|
||||
}, [visibleColumns]);
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
let filteredUsers = [...users];
|
||||
|
||||
if (hasSearchFilter) {
|
||||
filteredUsers = filteredUsers.filter((user) =>
|
||||
user.name.toLowerCase().includes(filterValue.toLowerCase()),
|
||||
);
|
||||
}
|
||||
if (statusFilter !== "all" && Array.from(statusFilter).length !== statusOptions.length) {
|
||||
filteredUsers = filteredUsers.filter((user) =>
|
||||
Array.from(statusFilter).includes(user.status),
|
||||
);
|
||||
}
|
||||
|
||||
return filteredUsers;
|
||||
}, [users, filterValue, statusFilter]);
|
||||
|
||||
const pages = Math.ceil(filteredItems.length / rowsPerPage);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const start = (page - 1) * rowsPerPage;
|
||||
const end = start + rowsPerPage;
|
||||
|
||||
return filteredItems.slice(start, end);
|
||||
}, [page, filteredItems, rowsPerPage]);
|
||||
|
||||
const sortedItems = useMemo(() => {
|
||||
return [...items].sort((a: User, b: User) => {
|
||||
const first = a[sortDescriptor.column as keyof User] as number;
|
||||
const second = b[sortDescriptor.column as keyof User] as number;
|
||||
const cmp = first < second ? -1 : first > second ? 1 : 0;
|
||||
|
||||
return sortDescriptor.direction === "descending" ? -cmp : cmp;
|
||||
});
|
||||
}, [sortDescriptor, items]);
|
||||
|
||||
const renderCell = useCallback((user: User, columnKey: React.Key) => {
|
||||
const cellValue = user[columnKey as keyof User];
|
||||
|
||||
switch (columnKey) {
|
||||
case "name":
|
||||
return (
|
||||
<User
|
||||
avatarProps={{radius: "lg", src: user.avatar}}
|
||||
description={user.email}
|
||||
name={cellValue}
|
||||
>
|
||||
{user.email}
|
||||
</User>
|
||||
);
|
||||
case "role":
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<p className="text-bold text-small capitalize">{cellValue}</p>
|
||||
<p className="text-bold text-tiny capitalize text-default-400">{user.team}</p>
|
||||
</div>
|
||||
);
|
||||
case "status":
|
||||
return (
|
||||
<Chip className="capitalize" color={statusColorMap[user.status]} size="sm" variant="flat">
|
||||
{cellValue}
|
||||
</Chip>
|
||||
);
|
||||
case "actions":
|
||||
return (
|
||||
<div className="relative flex justify-end items-center gap-2">
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button isIconOnly size="sm" variant="light">
|
||||
<VerticalDotsIcon className="text-default-300" />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu>
|
||||
<DropdownItem key="view">View</DropdownItem>
|
||||
<DropdownItem key="edit">Edit</DropdownItem>
|
||||
<DropdownItem key="delete">Delete</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return cellValue;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onNextPage = useCallback(() => {
|
||||
if (page < pages) {
|
||||
setPage(page + 1);
|
||||
}
|
||||
}, [page, pages]);
|
||||
|
||||
const onPreviousPage = useCallback(() => {
|
||||
if (page > 1) {
|
||||
setPage(page - 1);
|
||||
}
|
||||
}, [page]);
|
||||
|
||||
const onRowsPerPageChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setRowsPerPage(Number(e.target.value));
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
const onSearchChange = useCallback((value?: string) => {
|
||||
if (value) {
|
||||
setFilterValue(value);
|
||||
setPage(1);
|
||||
} else {
|
||||
setFilterValue("");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onClear = useCallback(() => {
|
||||
setFilterValue("");
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
const topContent = useMemo(() => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-between gap-3 items-end">
|
||||
<Input
|
||||
isClearable
|
||||
className="w-full sm:max-w-[44%]"
|
||||
placeholder="Search by name..."
|
||||
startContent={<SearchIcon />}
|
||||
value={filterValue}
|
||||
onClear={() => onClear()}
|
||||
onValueChange={onSearchChange}
|
||||
/>
|
||||
<div className="flex gap-3">
|
||||
<Dropdown>
|
||||
<DropdownTrigger className="hidden sm:flex">
|
||||
<Button endContent={<ChevronDownIcon className="text-small" />} variant="flat">
|
||||
Status
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
disallowEmptySelection
|
||||
aria-label="Table Columns"
|
||||
closeOnSelect={false}
|
||||
selectedKeys={statusFilter}
|
||||
selectionMode="multiple"
|
||||
onSelectionChange={setStatusFilter}
|
||||
>
|
||||
{statusOptions.map((status) => (
|
||||
<DropdownItem key={status.uid} className="capitalize">
|
||||
{capitalize(status.name)}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<Dropdown>
|
||||
<DropdownTrigger className="hidden sm:flex">
|
||||
<Button endContent={<ChevronDownIcon className="text-small" />} variant="flat">
|
||||
Columns
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
disallowEmptySelection
|
||||
aria-label="Table Columns"
|
||||
closeOnSelect={false}
|
||||
selectedKeys={visibleColumns}
|
||||
selectionMode="multiple"
|
||||
onSelectionChange={setVisibleColumns}
|
||||
>
|
||||
{columns.map((column) => (
|
||||
<DropdownItem key={column.uid} className="capitalize">
|
||||
{capitalize(column.name)}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<Button color="primary" endContent={<PlusLinearIcon />}>
|
||||
Add New
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-default-400 text-small">Total {users.length} users</span>
|
||||
<label className="flex items-center text-default-400 text-small">
|
||||
Rows per page:
|
||||
<select
|
||||
className="bg-transparent outline-none text-default-400 text-small"
|
||||
onChange={onRowsPerPageChange}
|
||||
>
|
||||
<option value="5">5</option>
|
||||
<option value="10">10</option>
|
||||
<option value="15">15</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [
|
||||
filterValue,
|
||||
statusFilter,
|
||||
visibleColumns,
|
||||
onSearchChange,
|
||||
onRowsPerPageChange,
|
||||
users.length,
|
||||
hasSearchFilter,
|
||||
]);
|
||||
|
||||
const bottomContent = useMemo(() => {
|
||||
return (
|
||||
<div className="py-2 px-2 flex justify-between items-center">
|
||||
<span className="w-[30%] text-small text-default-400">
|
||||
{selectedKeys === "all"
|
||||
? "All items selected"
|
||||
: `${selectedKeys.size} of ${filteredItems.length} selected`}
|
||||
</span>
|
||||
<Pagination
|
||||
isCompact
|
||||
showControls
|
||||
showShadow
|
||||
color="primary"
|
||||
page={page}
|
||||
total={pages}
|
||||
onChange={setPage}
|
||||
/>
|
||||
<div className="hidden sm:flex w-[30%] justify-end gap-2">
|
||||
<Button isDisabled={pages === 1} size="sm" variant="flat" onPress={onPreviousPage}>
|
||||
Previous
|
||||
</Button>
|
||||
<Button isDisabled={pages === 1} size="sm" variant="flat" onPress={onNextPage}>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [selectedKeys, items.length, page, pages, hasSearchFilter]);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Table
|
||||
isHeaderSticky
|
||||
aria-label="Example table with custom cells, pagination and sorting"
|
||||
bottomContent={bottomContent}
|
||||
bottomContentPlacement="outside"
|
||||
classNames={{
|
||||
wrapper: "max-h-[382px]",
|
||||
}}
|
||||
selectedKeys={selectedKeys}
|
||||
selectionMode="multiple"
|
||||
sortDescriptor={sortDescriptor}
|
||||
topContent={topContent}
|
||||
topContentPlacement="outside"
|
||||
onSelectionChange={setSelectedKeys}
|
||||
onSortChange={setSortDescriptor}
|
||||
>
|
||||
<TableHeader columns={headerColumns}>
|
||||
{(column) => (
|
||||
<TableColumn
|
||||
key={column.uid}
|
||||
align={column.uid === "actions" ? "center" : "start"}
|
||||
allowsSorting={column.sortable}
|
||||
>
|
||||
{column.name}
|
||||
</TableColumn>
|
||||
)}
|
||||
</TableHeader>
|
||||
<TableBody emptyContent={"No users found"} items={sortedItems}>
|
||||
{(item) => (
|
||||
<TableRow key={item.id}>
|
||||
{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import Rss from "rss";
|
||||
import {allBlogPosts} from "contentlayer2/generated";
|
||||
|
||||
import {siteConfig} from "@/config/site";
|
||||
import {allCoreContent} from "@/libs/contentlayer";
|
||||
|
||||
export async function GET() {
|
||||
const feed = new Rss({
|
||||
title: siteConfig.name,
|
||||
description: siteConfig.description,
|
||||
feed_url: `${siteConfig.siteUrl}/feed.xml`,
|
||||
site_url: siteConfig.siteUrl,
|
||||
webMaster: `${siteConfig.author} <${siteConfig.email}>`,
|
||||
managingEditor: `${siteConfig.author} <${siteConfig.email}>`,
|
||||
language: "en-US",
|
||||
});
|
||||
|
||||
allCoreContent(allBlogPosts).forEach((post) => {
|
||||
const author = post.author ? post.author : siteConfig.author;
|
||||
|
||||
feed.item({
|
||||
title: post.title,
|
||||
description: post.description ?? "",
|
||||
url: `${siteConfig.siteUrl}/blog/${post.slug}`,
|
||||
guid: `${siteConfig.siteUrl}/blog/${post.slug}`,
|
||||
date: post.date,
|
||||
// @ts-ignore - name does exist
|
||||
author: `${author.name} <${siteConfig.email}>`,
|
||||
categories: post.tags ?? [],
|
||||
});
|
||||
});
|
||||
|
||||
return new Response(feed.xml(), {
|
||||
headers: {
|
||||
"Content-Type": "application/xml",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import "@/styles/globals.css";
|
||||
import "@/styles/sandpack.css";
|
||||
import {Metadata, Viewport} from "next";
|
||||
import {clsx} from "@heroui/shared-utils";
|
||||
import {Analytics} from "@vercel/analytics/next";
|
||||
|
||||
import {Providers} from "./providers";
|
||||
|
||||
import {Cmdk} from "@/components/cmdk";
|
||||
import manifest from "@/config/routes.json";
|
||||
import {siteConfig} from "@/config/site";
|
||||
import {fonts} from "@/config/fonts";
|
||||
import {Navbar} from "@/components/navbar";
|
||||
import {Footer} from "@/components/footer";
|
||||
import {ProBanner} from "@/components/pro-banner";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: siteConfig.name,
|
||||
template: `%s | ${siteConfig.name}`,
|
||||
},
|
||||
description: siteConfig.description,
|
||||
keywords: [
|
||||
"React",
|
||||
"Next.js",
|
||||
"NextUI",
|
||||
"Tailwind CSS",
|
||||
"HeroUI",
|
||||
"React Aria",
|
||||
"Server Components",
|
||||
"React Components",
|
||||
"UI Components",
|
||||
"UI Kit",
|
||||
"UI Library",
|
||||
"UI Framework",
|
||||
"UI Design System",
|
||||
],
|
||||
icons: {
|
||||
icon: "/favicon.ico",
|
||||
shortcut: "/favicon-32x32.png",
|
||||
apple: "/apple-touch-icon.png",
|
||||
},
|
||||
manifest: "/manifest.json",
|
||||
twitter: siteConfig.twitter,
|
||||
openGraph: siteConfig.openGraph,
|
||||
authors: [
|
||||
{
|
||||
name: "hero_ui",
|
||||
url: "https://x.com/hero_ui",
|
||||
},
|
||||
],
|
||||
creator: "heroui-inc",
|
||||
alternates: {
|
||||
canonical: "https://heroui.com",
|
||||
types: {
|
||||
"application/rss+xml": [{url: "https://heroui.com/feed.xml", title: "HeroUI RSS Feed"}],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: [
|
||||
{color: "#f4f4f5", media: "(prefers-color-scheme: light)"},
|
||||
{color: "#111111", media: "(prefers-color-scheme: dark)"},
|
||||
],
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
userScalable: false,
|
||||
};
|
||||
|
||||
export default function RootLayout({children}: {children: React.ReactNode}) {
|
||||
return (
|
||||
<html suppressHydrationWarning dir="ltr" lang="en">
|
||||
<head />
|
||||
<body
|
||||
className={clsx(
|
||||
"min-h-screen bg-background font-sans antialiased",
|
||||
fonts.sans.variable,
|
||||
fonts.mono.variable,
|
||||
)}
|
||||
>
|
||||
<Providers themeProps={{attribute: "class", defaultTheme: "dark"}}>
|
||||
<div className="relative flex flex-col" id="app-container">
|
||||
<ProBanner />
|
||||
<Navbar mobileRoutes={manifest.mobileRoutes} routes={manifest.routes} />
|
||||
{children}
|
||||
<Analytics mode="production" />
|
||||
<Footer />
|
||||
</div>
|
||||
<Cmdk />
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import {Spacer} from "@heroui/spacer";
|
||||
import {Suspense} from "react";
|
||||
|
||||
import {Hero} from "@/components/marketing/hero";
|
||||
import {FeaturesGrid} from "@/components/marketing/features-grid";
|
||||
import {CustomThemes} from "@/components/marketing/custom-themes";
|
||||
import {A11yOtb} from "@/components/marketing/a11y-otb";
|
||||
import {DarkMode} from "@/components/marketing/dark-mode";
|
||||
import {Customization} from "@/components/marketing/customization";
|
||||
import {LastButNotLeast} from "@/components/marketing/last-but-not-least";
|
||||
import {InstallBanner} from "@/components/marketing/install-banner";
|
||||
import {Community} from "@/components/marketing/community";
|
||||
import Support from "@/components/marketing/support";
|
||||
import landingContent from "@/content/landing";
|
||||
import {Sponsors} from "@/components/marketing/sponsors";
|
||||
import {HeroUIProSection} from "@/components/marketing/heroui-pro-section";
|
||||
|
||||
export default async function Home() {
|
||||
return (
|
||||
<main className="container mx-auto max-w-7xl px-6 flex-grow">
|
||||
<section className="flex flex-col items-center justify-center">
|
||||
<Hero />
|
||||
<FeaturesGrid features={landingContent.topFeatures} />
|
||||
<Sponsors />
|
||||
<CustomThemes />
|
||||
<A11yOtb />
|
||||
<DarkMode />
|
||||
<Customization />
|
||||
<HeroUIProSection />
|
||||
<LastButNotLeast />
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<Support />
|
||||
</Suspense>
|
||||
<Spacer y={24} />
|
||||
<InstallBanner />
|
||||
<Community />
|
||||
<Spacer y={24} />
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import {Image} from "@heroui/react";
|
||||
|
||||
import PlaygroundTabs from "./playground-tabs";
|
||||
|
||||
export default function FigmaPage() {
|
||||
return (
|
||||
<>
|
||||
<main className="prose prose-neutral relative container mx-auto max-w-3xl z-10 px-6 min-h-[calc(100vh_-_64px_-_108px)] mb-12 flex-grow">
|
||||
<section className="w-full flex flex-col items-center mt-12 gap-6">
|
||||
<div className="text-center">
|
||||
<h1 className="mb-2">Playground</h1>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h3 className="text-medium text-default-500">Components</h3>
|
||||
<PlaygroundTabs />
|
||||
</section>
|
||||
</main>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="fixed hidden dark:md:block dark:opacity-70 -bottom-[40%] -left-[20%] z-0"
|
||||
>
|
||||
<Image removeWrapper alt="docs left background" src="/gradients/docs-left.png" />
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="fixed hidden dark:md:block dark:opacity-70 -top-[80%] -right-[60%] 2xl:-top-[60%] 2xl:-right-[45%] z-0 rotate-12"
|
||||
>
|
||||
<Image removeWrapper alt="docs right background" src="/gradients/docs-right.png" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {Tab, Tabs} from "@heroui/react";
|
||||
|
||||
export default function BlocksTabs() {
|
||||
return (
|
||||
<Tabs
|
||||
classNames={{
|
||||
cursor: "dark:bg-default-100 bg-default-200",
|
||||
}}
|
||||
radius="full"
|
||||
variant="light"
|
||||
>
|
||||
<Tab key="input" title="Inputs" />
|
||||
<Tab key="button" title="Buttons" />
|
||||
<Tab key="card" title="Cards" />
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import {HeroUIProvider} from "@heroui/react";
|
||||
import {ThemeProvider as NextThemesProvider} from "next-themes";
|
||||
import {ThemeProviderProps} from "next-themes";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useEffect} from "react";
|
||||
import posthog from "posthog-js";
|
||||
import {PostHogProvider} from "posthog-js/react";
|
||||
|
||||
import {__PROD__} from "@/utils";
|
||||
|
||||
export interface ProvidersProps {
|
||||
children: React.ReactNode;
|
||||
themeProps?: ThemeProviderProps;
|
||||
}
|
||||
|
||||
const ProviderWrapper = ({children}: {children: React.ReactElement}) => {
|
||||
useEffect(() => {
|
||||
// Initialize PostHog only once when the app starts
|
||||
if (typeof window !== "undefined" && __PROD__ && !posthog.isFeatureEnabled("capture")) {
|
||||
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
|
||||
api_host: "/ingest",
|
||||
person_profiles: "identified_only",
|
||||
ui_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
capture_pageview: false,
|
||||
capture_pageleave: false,
|
||||
capture_heatmaps: false,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (__PROD__) {
|
||||
return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export function Providers({children, themeProps}: ProvidersProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<ProviderWrapper>
|
||||
<HeroUIProvider navigate={router.push}>
|
||||
<NextThemesProvider {...themeProps}>{children}</NextThemesProvider>
|
||||
</HeroUIProvider>
|
||||
</ProviderWrapper>
|
||||
);
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, {useCallback, useEffect} from "react";
|
||||
import Script from "next/script";
|
||||
|
||||
import carbonOptimize from "./carbon-optimize";
|
||||
|
||||
import {loadScript} from "@/utils/scripts";
|
||||
import {useIsMounted} from "@/hooks/use-is-mounted";
|
||||
import {__PROD__, __ENABLE_ADS__} from "@/utils";
|
||||
|
||||
const EA_PROVIDER_RATIO = 0.85;
|
||||
|
||||
export const CarbonAd: React.FC<unknown> = () => {
|
||||
const carbonRef = React.useRef(null);
|
||||
const [showEthicalAds, setShowEthicalAds] = React.useState(false);
|
||||
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
const loadEthicalAds = useCallback(() => {
|
||||
return new Promise((resolve) => {
|
||||
const script = document.createElement("script");
|
||||
|
||||
script.src = "https://media.ethicalads.io/media/client/ethicalads.min.js";
|
||||
script.async = true;
|
||||
script.onload = () => {
|
||||
// @ts-ignore
|
||||
resolve(window.ethicalads);
|
||||
};
|
||||
|
||||
script.onerror = () => {
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
document.body.appendChild(script);
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(script);
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const shouldShowEthicalAds = Math.random() < EA_PROVIDER_RATIO;
|
||||
|
||||
let loadCarbon: any = null;
|
||||
|
||||
const loadCarbonAds = () => {
|
||||
setShowEthicalAds(false);
|
||||
// The isolation logic of carbonads is flawed.
|
||||
// Once the script starts loading, it will asynchronous resolve, with no way to stop it.
|
||||
// This leads to duplication of the ad. To solve the issue, we debounce the load action.
|
||||
loadCarbon =
|
||||
isMounted &&
|
||||
setTimeout(() => {
|
||||
const script = loadScript(
|
||||
"https://cdn.carbonads.com/carbon.js?serve=CESIC53Y&placement=nextuiorg",
|
||||
carbonRef.current,
|
||||
);
|
||||
|
||||
script.id = "_carbonads_js";
|
||||
carbonOptimize.init();
|
||||
});
|
||||
};
|
||||
|
||||
const loadAdProvider = async () => {
|
||||
if (shouldShowEthicalAds) {
|
||||
try {
|
||||
const ethicalads = await loadEthicalAds();
|
||||
|
||||
if (!ethicalads) {
|
||||
loadCarbonAds();
|
||||
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
ethicalads.wait.then((placements) => {
|
||||
if (!placements.length) {
|
||||
loadCarbonAds();
|
||||
} else {
|
||||
setShowEthicalAds(true);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
loadCarbonAds();
|
||||
}
|
||||
} else {
|
||||
loadCarbonAds();
|
||||
}
|
||||
};
|
||||
|
||||
loadAdProvider();
|
||||
|
||||
return () => {
|
||||
loadCarbon && clearTimeout(loadCarbon);
|
||||
};
|
||||
}, [isMounted]);
|
||||
|
||||
if (!__PROD__ || !__ENABLE_ADS__) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<>
|
||||
<Script async src="https://media.ethicalads.io/media/client/ethicalads.min.js" />
|
||||
<div
|
||||
className="ea-container horizontal"
|
||||
data-ea-campaign-types="paid|publisher-house|community"
|
||||
data-ea-publisher="nextuiorg"
|
||||
data-ea-type="image"
|
||||
style={{display: showEthicalAds ? "block" : "none"}}
|
||||
/>
|
||||
</>
|
||||
<div className="carbon-ad-container" style={{display: showEthicalAds ? "none" : "block"}}>
|
||||
<span ref={carbonRef} id="carbon-ad" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,64 +0,0 @@
|
||||
export default {
|
||||
isRefreshAble: function () {
|
||||
return !(
|
||||
typeof document.addEventListener === "undefined" || this.browserSupport().hidden === undefined
|
||||
);
|
||||
},
|
||||
browserSupport: function () {
|
||||
let hidden;
|
||||
let visibilityChange;
|
||||
|
||||
if (typeof document.hidden !== "undefined") {
|
||||
// Opera 12.10 and Firefox 18 and later support
|
||||
hidden = "hidden";
|
||||
visibilityChange = "visibilitychange";
|
||||
} else if (typeof document.msHidden !== "undefined") {
|
||||
hidden = "msHidden";
|
||||
visibilityChange = "msvisibilitychange";
|
||||
} else if (typeof document.webkitHidden !== "undefined") {
|
||||
hidden = "webkitHidden";
|
||||
visibilityChange = "webkitvisibilitychange";
|
||||
}
|
||||
|
||||
return {
|
||||
hidden: hidden,
|
||||
visibilityChange: visibilityChange,
|
||||
};
|
||||
},
|
||||
handleVisibilityChange: function () {
|
||||
const isElementInViewport = function (el) {
|
||||
let element = document.querySelector(el);
|
||||
let bounding = element.getBoundingClientRect();
|
||||
let isVisible;
|
||||
|
||||
if (
|
||||
bounding.top >= 0 &&
|
||||
bounding.left >= 0 &&
|
||||
bounding.right <= window.innerWidth &&
|
||||
bounding.bottom <= window.innerHeight
|
||||
) {
|
||||
isVisible = true;
|
||||
} else {
|
||||
isVisible = false;
|
||||
}
|
||||
|
||||
return isVisible;
|
||||
};
|
||||
|
||||
if (!document.hidden) {
|
||||
if (typeof _carbonads !== "undefined" && isElementInViewport("#carbonads")) {
|
||||
// eslint-disable-next-line no-undef
|
||||
_carbonads.refresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
init: function () {
|
||||
if (this.isRefreshAble()) {
|
||||
document.addEventListener(
|
||||
this.browserSupport().visibilityChange,
|
||||
this.handleVisibilityChange,
|
||||
false,
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./carbon-ad";
|
||||
@@ -1,55 +0,0 @@
|
||||
import {ReactNode, FC} from "react";
|
||||
import {clsx} from "@heroui/shared-utils";
|
||||
export interface BgGridContainerProps {
|
||||
showGradient?: boolean;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const BgGridContainer: FC<BgGridContainerProps> = ({
|
||||
// showGradient = true,
|
||||
children,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"relative overflow-y-hidden flex items-center border border-default-200 dark:border-default-100 px-2 py-4 rounded-lg",
|
||||
"overflow-hidden",
|
||||
// blur effect
|
||||
// "bg-transparent",
|
||||
// "before:w-full",
|
||||
// "before:bg-background/10",
|
||||
// "before:content-['']",
|
||||
// "before:block",
|
||||
// "before:z-[-1]",
|
||||
// "before:absolute",
|
||||
// "before:inset-0",
|
||||
// "before:backdrop-blur-md",
|
||||
// "before:backdrop-saturate-200",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="max-w-full py-4 px-2 w-full h-full scrollbar-hide overflow-x-scroll">
|
||||
{children}
|
||||
</div>
|
||||
{/* <div
|
||||
className={clsx(
|
||||
"hidden md:block absolute z-[-1] inset-0 bg-grid-zinc-300/25 [mask-image:linear-gradient(0deg,rgba(255,255,255,0.1),rgba(255,255,255,0.6))]",
|
||||
"dark:bg-grid-zinc-500/25 dark:[mask-image:linear-gradient(0deg,rgba(255,255,255,0.1),rgba(255,255,255,0.5))]",
|
||||
)}
|
||||
style={{backgroundPosition: "10px 10px"}}
|
||||
/>
|
||||
{showGradient && (
|
||||
<div className="hidden md:block absolute h-full w-full z-[-1] opacity-40 -top-8 -right-28">
|
||||
<Image
|
||||
removeWrapper
|
||||
alt="custom themes background"
|
||||
className="h-full w-full object-cover overflow-visible"
|
||||
src="/gradients/blue-purple-1.svg"
|
||||
/>
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,82 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {BlogPost} from "contentlayer2/generated";
|
||||
import {Card, CardFooter, CardBody, CardHeader, Link, Avatar, Image} from "@heroui/react";
|
||||
import Balancer from "react-wrap-balancer";
|
||||
import {format, parseISO} from "date-fns";
|
||||
import NextLink from "next/link";
|
||||
import {AnimatePresence, motion} from "framer-motion";
|
||||
import {usePostHog} from "posthog-js/react";
|
||||
|
||||
import {useIsMounted} from "@/hooks/use-is-mounted";
|
||||
|
||||
const BlogPostCard = (post: BlogPost) => {
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
const posthog = usePostHog();
|
||||
|
||||
const handlePress = () => {
|
||||
posthog.capture("BlogPostCard - Selection", {
|
||||
name: post.title,
|
||||
action: "click",
|
||||
category: "blog",
|
||||
data: post.url ?? "",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isMounted && (
|
||||
<motion.article
|
||||
animate={{opacity: 1, y: 0}}
|
||||
exit={{opacity: 0, y: 5}}
|
||||
initial={{opacity: 0, y: 5}}
|
||||
transition={{duration: 0.3}}
|
||||
>
|
||||
<Card
|
||||
disableRipple
|
||||
isBlurred
|
||||
as={NextLink}
|
||||
className="p-2 h-full border-transparent text-start bg-white/5 dark:bg-default-400/10 backdrop-blur-lg backdrop-saturate-[1.8]"
|
||||
href={post.url}
|
||||
isPressable={!!post.url}
|
||||
onPress={handlePress}
|
||||
>
|
||||
<CardHeader>
|
||||
<Link
|
||||
as={NextLink}
|
||||
className="font-semibold text-foreground"
|
||||
href={post.url}
|
||||
size="lg"
|
||||
underline="hover"
|
||||
onPress={handlePress}
|
||||
>
|
||||
<Balancer>{post.title}</Balancer>
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardBody className="pt-0 px-2 pb-1">
|
||||
<Image className="mb-4" src={post.image} />
|
||||
<p className="font-normal w-full text-default-600">{post.description}</p>
|
||||
</CardBody>
|
||||
<CardFooter className="flex justify-between items-center">
|
||||
<time className="block text-small text-default-500" dateTime={post.date}>
|
||||
{format(parseISO(post.date), "LLLL d, yyyy")} {post?.draft && " (Draft)"}
|
||||
</time>
|
||||
<Avatar size="sm" src={post.author?.avatar} />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</motion.article>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
export const BlogPostList = ({posts}: {posts: BlogPost[]}) => {
|
||||
return (
|
||||
<div className="mt-10 grid gap-4 grid-cols-[repeat(auto-fill,minmax(300px,1fr))]">
|
||||
{posts.map((post, idx) => (
|
||||
<BlogPostCard key={idx} {...post} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./nextjs-templates";
|
||||
export * from "./video-in-view";
|
||||
@@ -1,34 +0,0 @@
|
||||
import {NewNextJSIcon} from "@/components/icons";
|
||||
import {FeaturesGrid} from "@/components/marketing/features-grid";
|
||||
|
||||
const frameworks = [
|
||||
{
|
||||
title: "Next.js 13 (App) Template",
|
||||
isExternal: true,
|
||||
description:
|
||||
"A Next.js 13 with app directory template pre-configured with HeroUI (v2) and Tailwind CSS.",
|
||||
icon: <NewNextJSIcon height={40} width={40} />,
|
||||
href: "https://github.com/heroui-inc/next-app-template",
|
||||
},
|
||||
{
|
||||
title: "Next.js 13 (Pages) Template",
|
||||
isExternal: true,
|
||||
description:
|
||||
"A Next.js 13 with pages directory template pre-configured with HeroUI (v2) and Tailwind CSS.",
|
||||
icon: <NewNextJSIcon height={40} width={40} />,
|
||||
href: "https://github.com/heroui-inc/next-pages-template",
|
||||
},
|
||||
];
|
||||
|
||||
export const NextJsTemplates = () => {
|
||||
return (
|
||||
<FeaturesGrid
|
||||
classNames={{
|
||||
base: "mt-8 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-4",
|
||||
iconWrapper: "bg-default-300/20",
|
||||
body: "py-0",
|
||||
}}
|
||||
features={frameworks}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,151 +0,0 @@
|
||||
/* eslint-disable jsx-a11y/media-has-caption */
|
||||
"use client";
|
||||
|
||||
import {useInView} from "framer-motion";
|
||||
import {useRef, FC, useEffect, useState, useCallback} from "react";
|
||||
import {Button, cn, Spinner, Tooltip} from "@heroui/react";
|
||||
|
||||
import {PlayBoldIcon, PauseBoldIcon} from "@/components/icons";
|
||||
import {RotateLeftLinearIcon} from "@/components/icons";
|
||||
|
||||
interface VideoInViewProps {
|
||||
src: string;
|
||||
playMode?: "auto" | "manual";
|
||||
autoPlay?: boolean;
|
||||
poster?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
controls?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const VideoInView: FC<VideoInViewProps> = ({
|
||||
src,
|
||||
width,
|
||||
height,
|
||||
poster,
|
||||
autoPlay = true,
|
||||
playMode = "manual",
|
||||
controls = false,
|
||||
className,
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
const isVisible = useInView(videoRef);
|
||||
|
||||
// play video when it is visible and playMode is auto
|
||||
useEffect(() => {
|
||||
if (playMode !== "auto") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isVisible) {
|
||||
videoRef.current?.play();
|
||||
} else {
|
||||
videoRef.current?.pause();
|
||||
}
|
||||
}, [isVisible]);
|
||||
|
||||
const handleCanPlay = useCallback(() => {
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const videoEl = videoRef.current;
|
||||
|
||||
if (videoEl) {
|
||||
if (videoEl.readyState > 3) {
|
||||
// HAVE_FUTURE_DATA: enough data to start playing
|
||||
handleCanPlay();
|
||||
} else {
|
||||
videoEl.addEventListener("canplaythrough", handleCanPlay);
|
||||
}
|
||||
|
||||
// Cleanup the event listener
|
||||
return () => {
|
||||
videoEl.removeEventListener("canplaythrough", handleCanPlay);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onRestart = useCallback(() => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.currentTime = 0;
|
||||
videoRef.current.play();
|
||||
setIsPlaying(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onTogglePlay = useCallback(() => {
|
||||
if (videoRef.current) {
|
||||
if (!isPlaying) {
|
||||
videoRef.current.play();
|
||||
} else {
|
||||
videoRef.current.pause();
|
||||
}
|
||||
|
||||
setIsPlaying((v) => !v);
|
||||
}
|
||||
}, [isPlaying]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative data-[playing=true]:after:opacity-0 data-[playing=true]:after:z-[-1] after:content-[''] after:absolute after:inset-0 after:bg-black/30 after:z-20 after:transition-opacity"
|
||||
data-playing={isPlaying}
|
||||
>
|
||||
{isLoading && (
|
||||
<Spinner
|
||||
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
|
||||
color="secondary"
|
||||
size="lg"
|
||||
/>
|
||||
)}
|
||||
<Tooltip content={isPlaying ? "Pause" : "Play"} delay={1000}>
|
||||
<Button
|
||||
isIconOnly
|
||||
className="absolute z-50 right-12 top-3 border-1 border-transparent bg-transparent before:bg-white/10 before:content-[''] before:block before:z-[-1] before:absolute before:inset-0 before:backdrop-blur-md before:backdrop-saturate-100 before:rounded-lg"
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
onPress={onTogglePlay}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<PauseBoldIcon className="text-white" size={16} />
|
||||
) : (
|
||||
<PlayBoldIcon className="text-white" size={16} />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Restart" delay={1000}>
|
||||
<Button
|
||||
isIconOnly
|
||||
className="absolute z-50 right-3 top-3 border-1 border-transparent bg-transparent before:bg-white/10 before:content-[''] before:block before:z-[-1] before:absolute before:inset-0 before:backdrop-blur-md before:backdrop-saturate-100 before:rounded-lg"
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
onPress={onRestart}
|
||||
>
|
||||
<RotateLeftLinearIcon className="text-white" size={16} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<video
|
||||
ref={videoRef}
|
||||
loop
|
||||
muted
|
||||
autoPlay={autoPlay && playMode === "auto"}
|
||||
className={cn(
|
||||
"w-full border border-transparent dark:border-default-200/50 object-fit rounded-xl shadow-lg",
|
||||
className,
|
||||
)}
|
||||
controls={controls}
|
||||
height={height}
|
||||
poster={poster}
|
||||
src={src}
|
||||
width={width}
|
||||
onCanPlay={handleCanPlay}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,478 +0,0 @@
|
||||
/* eslint-disable jsx-a11y/no-autofocus */
|
||||
"use client";
|
||||
|
||||
import {Command} from "cmdk";
|
||||
import {useEffect, useState, FC, useMemo, useCallback, useRef} from "react";
|
||||
import {matchSorter} from "match-sorter";
|
||||
import {Button, ButtonProps, Kbd, Modal, ModalContent} from "@heroui/react";
|
||||
import {CloseIcon} from "@heroui/shared-icons";
|
||||
import {tv} from "tailwind-variants";
|
||||
import {usePathname, useRouter} from "next/navigation";
|
||||
import MultiRef from "react-multi-ref";
|
||||
import {clsx} from "@heroui/shared-utils";
|
||||
import scrollIntoView from "scroll-into-view-if-needed";
|
||||
import {isAppleDevice, isWebKit} from "@react-aria/utils";
|
||||
import {create} from "zustand";
|
||||
import {isEmpty, intersectionBy} from "@heroui/shared-utils";
|
||||
import {writeStorage, useLocalStorage} from "@rehooks/local-storage";
|
||||
import {usePostHog} from "posthog-js/react";
|
||||
|
||||
import {
|
||||
DocumentCodeBoldIcon,
|
||||
HashBoldIcon,
|
||||
ChevronRightLinearIcon,
|
||||
SearchLinearIcon,
|
||||
} from "./icons";
|
||||
|
||||
import searchData from "@/config/search-meta.json";
|
||||
import {useUpdateEffect} from "@/hooks/use-update-effect";
|
||||
|
||||
const hideOnPaths = ["examples"];
|
||||
|
||||
export interface CmdkStore {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onOpen: () => void;
|
||||
}
|
||||
|
||||
export const useCmdkStore = create<CmdkStore>((set) => ({
|
||||
isOpen: false,
|
||||
onClose: () => set({isOpen: false}),
|
||||
onOpen: () => set({isOpen: true}),
|
||||
}));
|
||||
|
||||
const cmdk = tv({
|
||||
slots: {
|
||||
base: "max-h-full overflow-y-auto",
|
||||
header: [
|
||||
"flex",
|
||||
"items-center",
|
||||
"w-full",
|
||||
"px-4",
|
||||
"border-b",
|
||||
"border-default-400/50",
|
||||
"dark:border-default-100",
|
||||
],
|
||||
searchIcon: "text-default-400 text-lg",
|
||||
input: [
|
||||
"w-full",
|
||||
"px-2",
|
||||
"h-14",
|
||||
"font-sans",
|
||||
"text-lg",
|
||||
"outline-none",
|
||||
"rounded-none",
|
||||
"bg-transparent",
|
||||
"text-default-700",
|
||||
"placeholder-default-500",
|
||||
"dark:text-default-500",
|
||||
"dark:placeholder:text-default-300",
|
||||
],
|
||||
list: ["px-4", "mt-2", "pb-4", "overflow-y-auto", "max-h-[50vh]"],
|
||||
itemWrapper: [
|
||||
"px-4",
|
||||
"mt-2",
|
||||
"group",
|
||||
"flex",
|
||||
"h-16",
|
||||
"justify-between",
|
||||
"items-center",
|
||||
"rounded-lg",
|
||||
"shadow",
|
||||
"bg-content2/50",
|
||||
"active:opacity-70",
|
||||
"cursor-pointer",
|
||||
"transition-opacity",
|
||||
"data-[active=true]:bg-primary",
|
||||
"data-[active=true]:text-primary-foreground",
|
||||
],
|
||||
leftWrapper: ["flex", "gap-3", "items-center", "w-full", "max-w-full"],
|
||||
leftIcon: [
|
||||
"text-default-500 dark:text-default-300",
|
||||
"group-data-[active=true]:text-primary-foreground",
|
||||
],
|
||||
itemContent: ["flex", "flex-col", "gap-0", "justify-center", "max-w-[80%]"],
|
||||
itemParentTitle: [
|
||||
"text-default-400",
|
||||
"text-xs",
|
||||
"group-data-[active=true]:text-primary-foreground",
|
||||
"select-none",
|
||||
],
|
||||
itemTitle: [
|
||||
"truncate",
|
||||
"text-default-500",
|
||||
"group-data-[active=true]:text-primary-foreground",
|
||||
"select-none",
|
||||
],
|
||||
emptyWrapper: ["flex", "flex-col", "text-center", "items-center", "justify-center", "h-32"],
|
||||
},
|
||||
});
|
||||
|
||||
interface SearchResultItem {
|
||||
content: string;
|
||||
objectID: string;
|
||||
url: string;
|
||||
type: "lvl1" | "lvl2" | "lvl3";
|
||||
hierarchy: {
|
||||
lvl1: string | null;
|
||||
lvl2?: string | null;
|
||||
lvl3?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
const MATCH_KEYS = ["hierarchy.lvl1", "hierarchy.lvl2", "hierarchy.lvl3", "content"];
|
||||
const RECENT_SEARCHES_KEY = "recent-searches";
|
||||
const MAX_RECENT_SEARCHES = 10;
|
||||
const MAX_RESULTS = 20;
|
||||
|
||||
export const Cmdk: FC<{}> = () => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [activeItem, setActiveItem] = useState(0);
|
||||
const [menuNodes] = useState(() => new MultiRef<number, HTMLElement>());
|
||||
const slots = useMemo(() => cmdk(), []);
|
||||
|
||||
const pathname = usePathname();
|
||||
|
||||
const eventRef = useRef<"mouse" | "keyboard">();
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
const router = useRouter();
|
||||
|
||||
const {isOpen, onClose, onOpen} = useCmdkStore();
|
||||
|
||||
const posthog = usePostHog();
|
||||
|
||||
const [recentSearches] = useLocalStorage<SearchResultItem[]>(RECENT_SEARCHES_KEY);
|
||||
|
||||
const addToRecentSearches = (item: SearchResultItem) => {
|
||||
let searches = recentSearches ?? [];
|
||||
|
||||
// Avoid adding the same search again
|
||||
if (!searches.find((i) => i.objectID === item.objectID)) {
|
||||
writeStorage(RECENT_SEARCHES_KEY, [item, ...searches].slice(0, MAX_RECENT_SEARCHES));
|
||||
} else {
|
||||
// Move the search to the top
|
||||
searches = searches.filter((i) => i.objectID !== item.objectID);
|
||||
writeStorage(RECENT_SEARCHES_KEY, [item, ...searches].slice(0, MAX_RECENT_SEARCHES));
|
||||
}
|
||||
};
|
||||
|
||||
const prioritizeFirstLevelItems = (a: SearchResultItem, b: SearchResultItem) => {
|
||||
if (a.type === "lvl1") {
|
||||
return -1;
|
||||
} else if (b.type === "lvl1") {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const results = useMemo<SearchResultItem[]>(
|
||||
function getResults() {
|
||||
if (query.length < 2) return [];
|
||||
|
||||
const data = searchData as SearchResultItem[];
|
||||
|
||||
const words = query.split(" ");
|
||||
|
||||
if (words.length === 1) {
|
||||
return matchSorter(data, query, {
|
||||
keys: MATCH_KEYS,
|
||||
sorter: (matches) => {
|
||||
matches.sort((a, b) => prioritizeFirstLevelItems(a.item, b.item));
|
||||
|
||||
return matches;
|
||||
},
|
||||
}).slice(0, MAX_RESULTS);
|
||||
}
|
||||
|
||||
const matchesForEachWord = words.map((word) =>
|
||||
matchSorter(data, word, {
|
||||
keys: MATCH_KEYS,
|
||||
sorter: (matches) => {
|
||||
matches.sort((a, b) => prioritizeFirstLevelItems(a.item, b.item));
|
||||
|
||||
return matches;
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const matches = intersectionBy(...matchesForEachWord, "objectID").slice(0, MAX_RESULTS);
|
||||
|
||||
posthog.capture("Cmdk - Search", {
|
||||
name: "cmdk - search",
|
||||
action: "search",
|
||||
category: "cmdk",
|
||||
data: {query, words, matches: matches?.map((match) => match.url).join(", ")},
|
||||
});
|
||||
|
||||
return matches;
|
||||
},
|
||||
[query],
|
||||
);
|
||||
|
||||
const items = !isEmpty(results) ? results : recentSearches ?? [];
|
||||
|
||||
// Toggle the menu when ⌘K / CTRL K is pressed
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
const hotkey = isAppleDevice() ? "metaKey" : "ctrlKey";
|
||||
|
||||
if (e?.key?.toLowerCase() === "k" && e[hotkey]) {
|
||||
e.preventDefault();
|
||||
isOpen ? onClose() : onOpen();
|
||||
|
||||
posthog.capture("Cmdk - Open/Close", {
|
||||
name: "cmdk - open/close",
|
||||
action: "keydown",
|
||||
category: "cmdk",
|
||||
data: isOpen ? "close" : "open",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const onItemSelect = useCallback(
|
||||
(item: SearchResultItem) => {
|
||||
onClose();
|
||||
router.push(item.url);
|
||||
addToRecentSearches(item);
|
||||
|
||||
posthog.capture("Cmdk - ItemSelect", {
|
||||
name: item.content,
|
||||
action: "click",
|
||||
category: "cmdk",
|
||||
data: item.url,
|
||||
});
|
||||
},
|
||||
[router, recentSearches],
|
||||
);
|
||||
|
||||
const onInputKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
eventRef.current = "keyboard";
|
||||
switch (e.key) {
|
||||
case "ArrowDown": {
|
||||
e.preventDefault();
|
||||
if (activeItem + 1 < items.length) {
|
||||
setActiveItem(activeItem + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ArrowUp": {
|
||||
e.preventDefault();
|
||||
if (activeItem - 1 >= 0) {
|
||||
setActiveItem(activeItem - 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Control":
|
||||
case "Alt":
|
||||
case "Shift": {
|
||||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
case "Enter": {
|
||||
if (items?.length <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
onItemSelect(items[activeItem]);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
[activeItem, items, router],
|
||||
);
|
||||
|
||||
useUpdateEffect(() => {
|
||||
setActiveItem(0);
|
||||
}, [query]);
|
||||
|
||||
useUpdateEffect(() => {
|
||||
if (!listRef.current || eventRef.current === "mouse") return;
|
||||
const node = menuNodes.map.get(activeItem);
|
||||
|
||||
if (!node) return;
|
||||
scrollIntoView(node, {
|
||||
scrollMode: "if-needed",
|
||||
behavior: "smooth",
|
||||
block: "end",
|
||||
inline: "end",
|
||||
boundary: listRef.current,
|
||||
});
|
||||
}, [activeItem]);
|
||||
|
||||
const CloseButton = useCallback(
|
||||
({
|
||||
onPress,
|
||||
className,
|
||||
}: {
|
||||
onPress?: ButtonProps["onPress"];
|
||||
className?: ButtonProps["className"];
|
||||
}) => {
|
||||
return (
|
||||
<Button
|
||||
isIconOnly
|
||||
className={clsx(
|
||||
"border data-[hover=true]:bg-content2 border-default-400 dark:border-default-100",
|
||||
className,
|
||||
)}
|
||||
radius="full"
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
onPress={onPress}
|
||||
>
|
||||
<CloseIcon />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const renderItem = useCallback(
|
||||
(item: SearchResultItem, index: number, isRecent = false) => {
|
||||
const isLvl1 = item.type === "lvl1";
|
||||
|
||||
const mainIcon = isRecent ? (
|
||||
<SearchLinearIcon className={slots.leftIcon()} size={20} strokeWidth={2} />
|
||||
) : isLvl1 ? (
|
||||
<DocumentCodeBoldIcon className={slots.leftIcon()} />
|
||||
) : (
|
||||
<HashBoldIcon className={slots.leftIcon()} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Command.Item
|
||||
key={item.objectID}
|
||||
ref={menuNodes.ref(index)}
|
||||
className={slots.itemWrapper()}
|
||||
data-active={index === activeItem}
|
||||
value={item.content}
|
||||
onMouseEnter={() => {
|
||||
eventRef.current = "mouse";
|
||||
|
||||
setActiveItem(index);
|
||||
}}
|
||||
onSelect={() => {
|
||||
if (eventRef.current === "keyboard") {
|
||||
return;
|
||||
}
|
||||
|
||||
onItemSelect(item);
|
||||
}}
|
||||
>
|
||||
<div className={slots.leftWrapper()}>
|
||||
{mainIcon}
|
||||
<div className={slots.itemContent()}>
|
||||
{!isLvl1 && <span className={slots.itemParentTitle()}>{item.hierarchy.lvl1}</span>}
|
||||
<p className={slots.itemTitle()}>{item.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ChevronRightLinearIcon size={14} />
|
||||
</Command.Item>
|
||||
);
|
||||
},
|
||||
[activeItem, onItemSelect, CloseButton, slots],
|
||||
);
|
||||
|
||||
const shouldOpen = !hideOnPaths.some((path) => pathname.includes(path));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
hideCloseButton
|
||||
backdrop="opaque"
|
||||
classNames={{
|
||||
base: [
|
||||
"mt-[20vh]",
|
||||
"border-small",
|
||||
"dark:border-default-100",
|
||||
"supports-[backdrop-filter]:bg-background/80",
|
||||
"dark:supports-[backdrop-filter]:bg-background/30",
|
||||
"supports-[backdrop-filter]:backdrop-blur-md",
|
||||
"supports-[backdrop-filter]:backdrop-saturate-150",
|
||||
],
|
||||
backdrop: ["bg-black/80"],
|
||||
}}
|
||||
isOpen={isOpen && shouldOpen}
|
||||
motionProps={{
|
||||
onAnimationComplete: () => {
|
||||
if (!isOpen) {
|
||||
setQuery("");
|
||||
}
|
||||
},
|
||||
}}
|
||||
placement="top-center"
|
||||
scrollBehavior="inside"
|
||||
size="xl"
|
||||
onClose={() => onClose()}
|
||||
>
|
||||
<ModalContent>
|
||||
<Command className={slots.base()} label="Quick search command" shouldFilter={false}>
|
||||
<div className={slots.header()}>
|
||||
<SearchLinearIcon className={slots.searchIcon()} strokeWidth={2} />
|
||||
<Command.Input
|
||||
autoFocus={!isWebKit()}
|
||||
className={slots.input()}
|
||||
placeholder="Search documentation"
|
||||
value={query}
|
||||
onKeyDown={onInputKeyDown}
|
||||
onValueChange={setQuery}
|
||||
/>
|
||||
{query.length > 0 && <CloseButton onPress={() => setQuery("")} />}
|
||||
<Kbd className="hidden md:block border-none px-2 py-1 ml-2 font-medium text-[0.6rem]">
|
||||
ESC
|
||||
</Kbd>
|
||||
</div>
|
||||
<Command.List ref={listRef} className={slots.list()} role="listbox">
|
||||
<Command.Empty>
|
||||
{query.length > 0 && (
|
||||
<div className={slots.emptyWrapper()}>
|
||||
<div>
|
||||
<p>No results for "{query}"</p>
|
||||
{query.length === 1 ? (
|
||||
<p className="text-default-400">
|
||||
Try adding more characters to your search term.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-default-400">Try searching for something else.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Command.Empty>
|
||||
|
||||
{isEmpty(query) &&
|
||||
(isEmpty(recentSearches) ? (
|
||||
<div className={slots.emptyWrapper()}>
|
||||
<p className="text-default-400">No recent searches</p>
|
||||
</div>
|
||||
) : (
|
||||
recentSearches &&
|
||||
recentSearches.length > 0 && (
|
||||
<Command.Group
|
||||
heading={
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-default-600">Recent</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{recentSearches.map((item, index) => renderItem(item, index, true))}
|
||||
</Command.Group>
|
||||
)
|
||||
))}
|
||||
|
||||
{results.map((item, index) => renderItem(item, index))}
|
||||
</Command.List>
|
||||
</Command>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,165 +0,0 @@
|
||||
// Inspired by https://github.dev/modulz/stitches-site code demo
|
||||
import React from "react";
|
||||
import refractor from "refractor/core";
|
||||
import js from "refractor/lang/javascript";
|
||||
import jsx from "refractor/lang/jsx";
|
||||
import bash from "refractor/lang/bash";
|
||||
import css from "refractor/lang/css";
|
||||
import diff from "refractor/lang/diff";
|
||||
import {toHtml} from "hast-util-to-html";
|
||||
import rangeParser from "parse-numeric-range";
|
||||
import {clsx} from "@heroui/shared-utils";
|
||||
|
||||
import {Pre} from "./pre";
|
||||
import {WindowActions} from "./window-actions";
|
||||
|
||||
import highlightLine from "@/libs/rehype-highlight-line";
|
||||
import highlightWord from "@/libs/rehype-highlight-word";
|
||||
|
||||
refractor.register(js);
|
||||
refractor.register(jsx);
|
||||
refractor.register(bash);
|
||||
refractor.register(css);
|
||||
refractor.register(diff);
|
||||
|
||||
type PreProps = Omit<React.ComponentProps<typeof Pre>, "css">;
|
||||
|
||||
export type CodeBlockProps = PreProps & {
|
||||
language: "js" | "jsx" | "bash" | "css" | "diff";
|
||||
title?: string;
|
||||
value?: string;
|
||||
highlightLines?: string;
|
||||
mode?: "static" | "typewriter";
|
||||
showLineNumbers?: boolean;
|
||||
showWindowIcons?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* recursively get all text nodes as an array for a given element
|
||||
*/
|
||||
function getTextNodes(node: any): any[] {
|
||||
let childTextNodes: React.ReactNode[] = [];
|
||||
|
||||
if (!node.hasChildNodes()) return [];
|
||||
|
||||
const childNodes = node.childNodes;
|
||||
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
if (childNodes[i].nodeType == Node.TEXT_NODE) {
|
||||
childTextNodes.push(childNodes[i]);
|
||||
} else if (childNodes[i].nodeType == Node.ELEMENT_NODE) {
|
||||
Array.prototype.push.apply(childTextNodes, getTextNodes(childNodes[i]));
|
||||
}
|
||||
}
|
||||
|
||||
return childTextNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* given a text node, wrap each character in the
|
||||
* given tag.
|
||||
*/
|
||||
function wrapEachCharacter(textNode: any, tag: string, count: number) {
|
||||
const text = textNode.nodeValue;
|
||||
const parent = textNode.parentNode;
|
||||
|
||||
const characters = text.split("");
|
||||
|
||||
characters.forEach(function (character: any, letterIndex: any) {
|
||||
const delay = (count + letterIndex) * 50;
|
||||
var element = document.createElement(tag);
|
||||
var characterNode = document.createTextNode(character);
|
||||
|
||||
element.appendChild(characterNode);
|
||||
element.style.opacity = "0";
|
||||
element.style.transition = `all ease 0ms ${delay}ms`;
|
||||
|
||||
parent.insertBefore(element, textNode);
|
||||
|
||||
// skip a couple of frames to trigger transition
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => (element.style.opacity = "1")));
|
||||
});
|
||||
|
||||
parent.removeChild(textNode);
|
||||
}
|
||||
|
||||
function CodeTypewriter({value, className, css, ...props}: any) {
|
||||
const wrapperRef = React.useRef(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const wrapper = wrapperRef.current as any;
|
||||
|
||||
if (wrapper) {
|
||||
var allTextNodes = getTextNodes(wrapper);
|
||||
|
||||
let count = 0;
|
||||
|
||||
allTextNodes?.forEach((textNode) => {
|
||||
wrapEachCharacter(textNode, "span", count);
|
||||
count = count + textNode.nodeValue.length;
|
||||
});
|
||||
wrapper.style.opacity = "1";
|
||||
}
|
||||
|
||||
return () => (wrapper.innerHTML = value);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Pre className={className} css={css} {...props}>
|
||||
<code
|
||||
dangerouslySetInnerHTML={{__html: value}}
|
||||
ref={wrapperRef}
|
||||
className={className}
|
||||
style={{opacity: 0}}
|
||||
/>
|
||||
</Pre>
|
||||
);
|
||||
}
|
||||
|
||||
const CodeBlock = React.forwardRef<HTMLPreElement, CodeBlockProps>((_props, forwardedRef) => {
|
||||
const {
|
||||
language,
|
||||
value,
|
||||
title,
|
||||
highlightLines = "0",
|
||||
className = "",
|
||||
mode,
|
||||
showLineNumbers,
|
||||
showWindowIcons,
|
||||
...props
|
||||
} = _props;
|
||||
|
||||
let result: any = refractor.highlight(value || "", language);
|
||||
|
||||
result = highlightLine(result, rangeParser(highlightLines));
|
||||
|
||||
result = highlightWord(result);
|
||||
|
||||
// convert to html
|
||||
result = toHtml(result);
|
||||
|
||||
// TODO reset theme
|
||||
const classes = `language-${language}`;
|
||||
const codeClasses = clsx("absolute w-full px-4 pb-6", showWindowIcons ? "top-10" : "top-0");
|
||||
|
||||
if (mode === "typewriter") {
|
||||
return <CodeTypewriter className={classes} css={css} value={result} {...props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Pre
|
||||
ref={forwardedRef}
|
||||
className={clsx("code-block", classes, className)}
|
||||
data-line-numbers={showLineNumbers}
|
||||
{...props}
|
||||
>
|
||||
{showWindowIcons && <WindowActions title={title} />}
|
||||
<code dangerouslySetInnerHTML={{__html: result}} className={clsx(classes, codeClasses)} />
|
||||
</Pre>
|
||||
);
|
||||
});
|
||||
|
||||
CodeBlock.displayName = "HeroUI - CodeBlock";
|
||||
|
||||
export default CodeBlock;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user