WIP. A little custom music server
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add sidebar

+1095
+59
bun.lock
··· 51 51 "dependencies": { 52 52 "@boombox/shared": "workspace:*", 53 53 "@elysiajs/eden": "^1.4.1", 54 + "@radix-ui/react-dialog": "^1.1.15", 54 55 "@radix-ui/react-progress": "^1.1.7", 56 + "@radix-ui/react-separator": "^1.1.7", 55 57 "@radix-ui/react-slider": "^1.3.6", 56 58 "@radix-ui/react-slot": "^1.2.3", 59 + "@radix-ui/react-tooltip": "^1.2.8", 57 60 "class-variance-authority": "^0.7.1", 58 61 "clsx": "^2.1.1", 59 62 "date-fns": "^4.1.0", ··· 220 223 221 224 "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.9", "", { "os": "win32", "cpu": "x64" }, "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ=="], 222 225 226 + "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], 227 + 228 + "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], 229 + 230 + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="], 231 + 232 + "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], 233 + 223 234 "@hono/node-server": ["@hono/node-server@1.19.1", "", { "peerDependencies": { "hono": "^4" } }, "sha512-h44e5s+ByUriaRIbeS/C74O8v90m0A95luyYQGMF7KEn96KkYMXO7bZAwombzTpjQTU4e0TkU8U1WBIXlwuwtA=="], 224 235 225 236 "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], ··· 386 397 387 398 "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], 388 399 400 + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], 401 + 389 402 "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], 390 403 391 404 "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], 392 405 393 406 "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], 407 + 408 + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], 394 409 395 410 "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], 396 411 412 + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], 413 + 414 + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], 415 + 416 + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], 417 + 418 + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], 419 + 420 + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], 421 + 422 + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], 423 + 424 + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], 425 + 397 426 "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], 398 427 399 428 "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="], 400 429 430 + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="], 431 + 401 432 "@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw=="], 402 433 403 434 "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], 404 435 436 + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], 437 + 438 + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], 439 + 405 440 "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], 406 441 407 442 "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], 443 + 444 + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], 408 445 409 446 "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], 410 447 411 448 "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], 412 449 450 + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], 451 + 413 452 "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], 453 + 454 + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], 455 + 456 + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], 414 457 415 458 "@remix-run/node-fetch-server": ["@remix-run/node-fetch-server@0.8.1", "", {}, "sha512-J1dev372wtJqmqn9U/qbpbZxbJSQrogNN2+Qv1lKlpATpe/WQ9aCZfl/xSb9d2Rgh1IyLSvNxZAXPZxruO6Xig=="], 416 459 ··· 617 660 "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], 618 661 619 662 "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], 663 + 664 + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], 620 665 621 666 "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], 622 667 ··· 674 719 675 720 "detect-libc": ["detect-libc@2.1.0", "", {}, "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg=="], 676 721 722 + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], 723 + 677 724 "dotenv": ["dotenv@17.2.2", "", {}, "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q=="], 678 725 679 726 "drizzle-kit": ["drizzle-kit@0.31.4", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA=="], ··· 733 780 "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 734 781 735 782 "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], 783 + 784 + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], 736 785 737 786 "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], 738 787 ··· 898 947 899 948 "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], 900 949 950 + "react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="], 951 + 952 + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], 953 + 901 954 "react-server-dom-webpack": ["react-server-dom-webpack@19.1.1", "", { "dependencies": { "acorn-loose": "^8.3.0", "neo-async": "^2.6.1", "webpack-sources": "^3.2.0" }, "peerDependencies": { "react": "^19.1.1", "react-dom": "^19.1.1", "webpack": "^5.59.0" } }, "sha512-6MJwCAgQKcNxzRQebeyDspWzcncJMxe7JfmA8OFHWCMtp6L++HyJMgScM492iIeQZln3C834HyG4/UeMMZeRMw=="], 955 + 956 + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], 902 957 903 958 "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], 904 959 ··· 989 1044 "undici-types": ["undici-types@7.11.0", "", {}, "sha512-kt1ZriHTi7MU+Z/r9DOdAI3ONdaR3M3csEaRc6ewa4f4dTvX4cQCbJ4NkEn0ohE4hHtq85+PhPSTY+pO/1PwgA=="], 990 1045 991 1046 "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], 1047 + 1048 + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], 1049 + 1050 + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], 992 1051 993 1052 "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], 994 1053
+3
web/package.json
··· 12 12 "dependencies": { 13 13 "@boombox/shared": "workspace:*", 14 14 "@elysiajs/eden": "^1.4.1", 15 + "@radix-ui/react-dialog": "^1.1.15", 15 16 "@radix-ui/react-progress": "^1.1.7", 17 + "@radix-ui/react-separator": "^1.1.7", 16 18 "@radix-ui/react-slider": "^1.3.6", 17 19 "@radix-ui/react-slot": "^1.2.3", 20 + "@radix-ui/react-tooltip": "^1.2.8", 18 21 "class-variance-authority": "^0.7.1", 19 22 "clsx": "^2.1.1", 20 23 "date-fns": "^4.1.0",
+28
web/src/components/ui/separator.tsx
··· 1 + "use client" 2 + 3 + import * as React from "react" 4 + import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 + 6 + import { cn } from "@/lib/utils" 7 + 8 + function Separator({ 9 + className, 10 + orientation = "horizontal", 11 + decorative = true, 12 + ...props 13 + }: React.ComponentProps<typeof SeparatorPrimitive.Root>) { 14 + return ( 15 + <SeparatorPrimitive.Root 16 + data-slot="separator" 17 + decorative={decorative} 18 + orientation={orientation} 19 + className={cn( 20 + "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", 21 + className 22 + )} 23 + {...props} 24 + /> 25 + ) 26 + } 27 + 28 + export { Separator }
+139
web/src/components/ui/sheet.tsx
··· 1 + "use client" 2 + 3 + import * as React from "react" 4 + import * as SheetPrimitive from "@radix-ui/react-dialog" 5 + import { XIcon } from "lucide-react" 6 + 7 + import { cn } from "@/lib/utils" 8 + 9 + function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) { 10 + return <SheetPrimitive.Root data-slot="sheet" {...props} /> 11 + } 12 + 13 + function SheetTrigger({ 14 + ...props 15 + }: React.ComponentProps<typeof SheetPrimitive.Trigger>) { 16 + return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} /> 17 + } 18 + 19 + function SheetClose({ 20 + ...props 21 + }: React.ComponentProps<typeof SheetPrimitive.Close>) { 22 + return <SheetPrimitive.Close data-slot="sheet-close" {...props} /> 23 + } 24 + 25 + function SheetPortal({ 26 + ...props 27 + }: React.ComponentProps<typeof SheetPrimitive.Portal>) { 28 + return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} /> 29 + } 30 + 31 + function SheetOverlay({ 32 + className, 33 + ...props 34 + }: React.ComponentProps<typeof SheetPrimitive.Overlay>) { 35 + return ( 36 + <SheetPrimitive.Overlay 37 + data-slot="sheet-overlay" 38 + className={cn( 39 + "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", 40 + className 41 + )} 42 + {...props} 43 + /> 44 + ) 45 + } 46 + 47 + function SheetContent({ 48 + className, 49 + children, 50 + side = "right", 51 + ...props 52 + }: React.ComponentProps<typeof SheetPrimitive.Content> & { 53 + side?: "top" | "right" | "bottom" | "left" 54 + }) { 55 + return ( 56 + <SheetPortal> 57 + <SheetOverlay /> 58 + <SheetPrimitive.Content 59 + data-slot="sheet-content" 60 + className={cn( 61 + "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500", 62 + side === "right" && 63 + "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm", 64 + side === "left" && 65 + "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm", 66 + side === "top" && 67 + "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b", 68 + side === "bottom" && 69 + "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t", 70 + className 71 + )} 72 + {...props} 73 + > 74 + {children} 75 + <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"> 76 + <XIcon className="size-4" /> 77 + <span className="sr-only">Close</span> 78 + </SheetPrimitive.Close> 79 + </SheetPrimitive.Content> 80 + </SheetPortal> 81 + ) 82 + } 83 + 84 + function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { 85 + return ( 86 + <div 87 + data-slot="sheet-header" 88 + className={cn("flex flex-col gap-1.5 p-4", className)} 89 + {...props} 90 + /> 91 + ) 92 + } 93 + 94 + function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { 95 + return ( 96 + <div 97 + data-slot="sheet-footer" 98 + className={cn("mt-auto flex flex-col gap-2 p-4", className)} 99 + {...props} 100 + /> 101 + ) 102 + } 103 + 104 + function SheetTitle({ 105 + className, 106 + ...props 107 + }: React.ComponentProps<typeof SheetPrimitive.Title>) { 108 + return ( 109 + <SheetPrimitive.Title 110 + data-slot="sheet-title" 111 + className={cn("text-foreground font-semibold", className)} 112 + {...props} 113 + /> 114 + ) 115 + } 116 + 117 + function SheetDescription({ 118 + className, 119 + ...props 120 + }: React.ComponentProps<typeof SheetPrimitive.Description>) { 121 + return ( 122 + <SheetPrimitive.Description 123 + data-slot="sheet-description" 124 + className={cn("text-muted-foreground text-sm", className)} 125 + {...props} 126 + /> 127 + ) 128 + } 129 + 130 + export { 131 + Sheet, 132 + SheetTrigger, 133 + SheetClose, 134 + SheetContent, 135 + SheetHeader, 136 + SheetFooter, 137 + SheetTitle, 138 + SheetDescription, 139 + }
+773
web/src/components/ui/sidebar.tsx
··· 1 + "use client" 2 + 3 + import * as React from "react" 4 + import { Slot } from "@radix-ui/react-slot" 5 + import { VariantProps, cva } from "class-variance-authority" 6 + import { PanelLeft } from "lucide-react" 7 + 8 + import { useIsMobile } from "@/hooks/use-mobile" 9 + import { cn } from "@/lib/utils" 10 + import { Button } from "@/components/ui/button" 11 + import { Input } from "@/components/ui/input" 12 + import { Separator } from "@/components/ui/separator" 13 + import { 14 + Sheet, 15 + SheetContent, 16 + SheetDescription, 17 + SheetHeader, 18 + SheetTitle, 19 + } from "@/components/ui/sheet" 20 + import { Skeleton } from "@/components/ui/skeleton" 21 + import { 22 + Tooltip, 23 + TooltipContent, 24 + TooltipProvider, 25 + TooltipTrigger, 26 + } from "@/components/ui/tooltip" 27 + 28 + const SIDEBAR_COOKIE_NAME = "sidebar_state" 29 + const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 30 + const SIDEBAR_WIDTH = "16rem" 31 + const SIDEBAR_WIDTH_MOBILE = "18rem" 32 + const SIDEBAR_WIDTH_ICON = "3rem" 33 + const SIDEBAR_KEYBOARD_SHORTCUT = "b" 34 + 35 + type SidebarContextProps = { 36 + state: "expanded" | "collapsed" 37 + open: boolean 38 + setOpen: (open: boolean) => void 39 + openMobile: boolean 40 + setOpenMobile: (open: boolean) => void 41 + isMobile: boolean 42 + toggleSidebar: () => void 43 + } 44 + 45 + const SidebarContext = React.createContext<SidebarContextProps | null>(null) 46 + 47 + function useSidebar() { 48 + const context = React.useContext(SidebarContext) 49 + if (!context) { 50 + throw new Error("useSidebar must be used within a SidebarProvider.") 51 + } 52 + 53 + return context 54 + } 55 + 56 + const SidebarProvider = React.forwardRef< 57 + HTMLDivElement, 58 + React.ComponentProps<"div"> & { 59 + defaultOpen?: boolean 60 + open?: boolean 61 + onOpenChange?: (open: boolean) => void 62 + } 63 + >( 64 + ( 65 + { 66 + defaultOpen = true, 67 + open: openProp, 68 + onOpenChange: setOpenProp, 69 + className, 70 + style, 71 + children, 72 + ...props 73 + }, 74 + ref 75 + ) => { 76 + const isMobile = useIsMobile() 77 + const [openMobile, setOpenMobile] = React.useState(false) 78 + 79 + // This is the internal state of the sidebar. 80 + // We use openProp and setOpenProp for control from outside the component. 81 + const [_open, _setOpen] = React.useState(defaultOpen) 82 + const open = openProp ?? _open 83 + const setOpen = React.useCallback( 84 + (value: boolean | ((value: boolean) => boolean)) => { 85 + const openState = typeof value === "function" ? value(open) : value 86 + if (setOpenProp) { 87 + setOpenProp(openState) 88 + } else { 89 + _setOpen(openState) 90 + } 91 + 92 + // This sets the cookie to keep the sidebar state. 93 + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` 94 + }, 95 + [setOpenProp, open] 96 + ) 97 + 98 + // Helper to toggle the sidebar. 99 + const toggleSidebar = React.useCallback(() => { 100 + return isMobile 101 + ? setOpenMobile((open) => !open) 102 + : setOpen((open) => !open) 103 + }, [isMobile, setOpen, setOpenMobile]) 104 + 105 + // Adds a keyboard shortcut to toggle the sidebar. 106 + React.useEffect(() => { 107 + const handleKeyDown = (event: KeyboardEvent) => { 108 + if ( 109 + event.key === SIDEBAR_KEYBOARD_SHORTCUT && 110 + (event.metaKey || event.ctrlKey) 111 + ) { 112 + event.preventDefault() 113 + toggleSidebar() 114 + } 115 + } 116 + 117 + window.addEventListener("keydown", handleKeyDown) 118 + return () => window.removeEventListener("keydown", handleKeyDown) 119 + }, [toggleSidebar]) 120 + 121 + // We add a state so that we can do data-state="expanded" or "collapsed". 122 + // This makes it easier to style the sidebar with Tailwind classes. 123 + const state = open ? "expanded" : "collapsed" 124 + 125 + const contextValue = React.useMemo<SidebarContextProps>( 126 + () => ({ 127 + state, 128 + open, 129 + setOpen, 130 + isMobile, 131 + openMobile, 132 + setOpenMobile, 133 + toggleSidebar, 134 + }), 135 + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] 136 + ) 137 + 138 + return ( 139 + <SidebarContext.Provider value={contextValue}> 140 + <TooltipProvider delayDuration={0}> 141 + <div 142 + style={ 143 + { 144 + "--sidebar-width": SIDEBAR_WIDTH, 145 + "--sidebar-width-icon": SIDEBAR_WIDTH_ICON, 146 + ...style, 147 + } as React.CSSProperties 148 + } 149 + className={cn( 150 + "group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar", 151 + className 152 + )} 153 + ref={ref} 154 + {...props} 155 + > 156 + {children} 157 + </div> 158 + </TooltipProvider> 159 + </SidebarContext.Provider> 160 + ) 161 + } 162 + ) 163 + SidebarProvider.displayName = "SidebarProvider" 164 + 165 + const Sidebar = React.forwardRef< 166 + HTMLDivElement, 167 + React.ComponentProps<"div"> & { 168 + side?: "left" | "right" 169 + variant?: "sidebar" | "floating" | "inset" 170 + collapsible?: "offcanvas" | "icon" | "none" 171 + } 172 + >( 173 + ( 174 + { 175 + side = "left", 176 + variant = "sidebar", 177 + collapsible = "offcanvas", 178 + className, 179 + children, 180 + ...props 181 + }, 182 + ref 183 + ) => { 184 + const { isMobile, state, openMobile, setOpenMobile } = useSidebar() 185 + 186 + if (collapsible === "none") { 187 + return ( 188 + <div 189 + className={cn( 190 + "flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground", 191 + className 192 + )} 193 + ref={ref} 194 + {...props} 195 + > 196 + {children} 197 + </div> 198 + ) 199 + } 200 + 201 + if (isMobile) { 202 + return ( 203 + <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}> 204 + <SheetContent 205 + data-sidebar="sidebar" 206 + data-mobile="true" 207 + className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden" 208 + style={ 209 + { 210 + "--sidebar-width": SIDEBAR_WIDTH_MOBILE, 211 + } as React.CSSProperties 212 + } 213 + side={side} 214 + > 215 + <SheetHeader className="sr-only"> 216 + <SheetTitle>Sidebar</SheetTitle> 217 + <SheetDescription>Displays the mobile sidebar.</SheetDescription> 218 + </SheetHeader> 219 + <div className="flex h-full w-full flex-col">{children}</div> 220 + </SheetContent> 221 + </Sheet> 222 + ) 223 + } 224 + 225 + return ( 226 + <div 227 + ref={ref} 228 + className="group peer hidden text-sidebar-foreground md:block" 229 + data-state={state} 230 + data-collapsible={state === "collapsed" ? collapsible : ""} 231 + data-variant={variant} 232 + data-side={side} 233 + > 234 + {/* This is what handles the sidebar gap on desktop */} 235 + <div 236 + className={cn( 237 + "relative w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear", 238 + "group-data-[collapsible=offcanvas]:w-0", 239 + "group-data-[side=right]:rotate-180", 240 + variant === "floating" || variant === "inset" 241 + ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]" 242 + : "group-data-[collapsible=icon]:w-[--sidebar-width-icon]" 243 + )} 244 + /> 245 + <div 246 + className={cn( 247 + "fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex", 248 + side === "left" 249 + ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]" 250 + : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]", 251 + // Adjust the padding for floating and inset variants. 252 + variant === "floating" || variant === "inset" 253 + ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]" 254 + : "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l", 255 + className 256 + )} 257 + {...props} 258 + > 259 + <div 260 + data-sidebar="sidebar" 261 + className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow" 262 + > 263 + {children} 264 + </div> 265 + </div> 266 + </div> 267 + ) 268 + } 269 + ) 270 + Sidebar.displayName = "Sidebar" 271 + 272 + const SidebarTrigger = React.forwardRef< 273 + React.ElementRef<typeof Button>, 274 + React.ComponentProps<typeof Button> 275 + >(({ className, onClick, ...props }, ref) => { 276 + const { toggleSidebar } = useSidebar() 277 + 278 + return ( 279 + <Button 280 + ref={ref} 281 + data-sidebar="trigger" 282 + variant="ghost" 283 + size="icon" 284 + className={cn("h-7 w-7", className)} 285 + onClick={(event) => { 286 + onClick?.(event) 287 + toggleSidebar() 288 + }} 289 + {...props} 290 + > 291 + <PanelLeft /> 292 + <span className="sr-only">Toggle Sidebar</span> 293 + </Button> 294 + ) 295 + }) 296 + SidebarTrigger.displayName = "SidebarTrigger" 297 + 298 + const SidebarRail = React.forwardRef< 299 + HTMLButtonElement, 300 + React.ComponentProps<"button"> 301 + >(({ className, ...props }, ref) => { 302 + const { toggleSidebar } = useSidebar() 303 + 304 + return ( 305 + <button 306 + ref={ref} 307 + data-sidebar="rail" 308 + aria-label="Toggle Sidebar" 309 + tabIndex={-1} 310 + onClick={toggleSidebar} 311 + title="Toggle Sidebar" 312 + className={cn( 313 + "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex", 314 + "[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize", 315 + "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", 316 + "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar", 317 + "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", 318 + "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", 319 + className 320 + )} 321 + {...props} 322 + /> 323 + ) 324 + }) 325 + SidebarRail.displayName = "SidebarRail" 326 + 327 + const SidebarInset = React.forwardRef< 328 + HTMLDivElement, 329 + React.ComponentProps<"main"> 330 + >(({ className, ...props }, ref) => { 331 + return ( 332 + <main 333 + ref={ref} 334 + className={cn( 335 + "relative flex w-full flex-1 flex-col bg-background", 336 + "md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow", 337 + className 338 + )} 339 + {...props} 340 + /> 341 + ) 342 + }) 343 + SidebarInset.displayName = "SidebarInset" 344 + 345 + const SidebarInput = React.forwardRef< 346 + React.ElementRef<typeof Input>, 347 + React.ComponentProps<typeof Input> 348 + >(({ className, ...props }, ref) => { 349 + return ( 350 + <Input 351 + ref={ref} 352 + data-sidebar="input" 353 + className={cn( 354 + "h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring", 355 + className 356 + )} 357 + {...props} 358 + /> 359 + ) 360 + }) 361 + SidebarInput.displayName = "SidebarInput" 362 + 363 + const SidebarHeader = React.forwardRef< 364 + HTMLDivElement, 365 + React.ComponentProps<"div"> 366 + >(({ className, ...props }, ref) => { 367 + return ( 368 + <div 369 + ref={ref} 370 + data-sidebar="header" 371 + className={cn("flex flex-col gap-2 p-2", className)} 372 + {...props} 373 + /> 374 + ) 375 + }) 376 + SidebarHeader.displayName = "SidebarHeader" 377 + 378 + const SidebarFooter = React.forwardRef< 379 + HTMLDivElement, 380 + React.ComponentProps<"div"> 381 + >(({ className, ...props }, ref) => { 382 + return ( 383 + <div 384 + ref={ref} 385 + data-sidebar="footer" 386 + className={cn("flex flex-col gap-2 p-2", className)} 387 + {...props} 388 + /> 389 + ) 390 + }) 391 + SidebarFooter.displayName = "SidebarFooter" 392 + 393 + const SidebarSeparator = React.forwardRef< 394 + React.ElementRef<typeof Separator>, 395 + React.ComponentProps<typeof Separator> 396 + >(({ className, ...props }, ref) => { 397 + return ( 398 + <Separator 399 + ref={ref} 400 + data-sidebar="separator" 401 + className={cn("mx-2 w-auto bg-sidebar-border", className)} 402 + {...props} 403 + /> 404 + ) 405 + }) 406 + SidebarSeparator.displayName = "SidebarSeparator" 407 + 408 + const SidebarContent = React.forwardRef< 409 + HTMLDivElement, 410 + React.ComponentProps<"div"> 411 + >(({ className, ...props }, ref) => { 412 + return ( 413 + <div 414 + ref={ref} 415 + data-sidebar="content" 416 + className={cn( 417 + "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden", 418 + className 419 + )} 420 + {...props} 421 + /> 422 + ) 423 + }) 424 + SidebarContent.displayName = "SidebarContent" 425 + 426 + const SidebarGroup = React.forwardRef< 427 + HTMLDivElement, 428 + React.ComponentProps<"div"> 429 + >(({ className, ...props }, ref) => { 430 + return ( 431 + <div 432 + ref={ref} 433 + data-sidebar="group" 434 + className={cn("relative flex w-full min-w-0 flex-col p-2", className)} 435 + {...props} 436 + /> 437 + ) 438 + }) 439 + SidebarGroup.displayName = "SidebarGroup" 440 + 441 + const SidebarGroupLabel = React.forwardRef< 442 + HTMLDivElement, 443 + React.ComponentProps<"div"> & { asChild?: boolean } 444 + >(({ className, asChild = false, ...props }, ref) => { 445 + const Comp = asChild ? Slot : "div" 446 + 447 + return ( 448 + <Comp 449 + ref={ref} 450 + data-sidebar="group-label" 451 + className={cn( 452 + "flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", 453 + "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", 454 + className 455 + )} 456 + {...props} 457 + /> 458 + ) 459 + }) 460 + SidebarGroupLabel.displayName = "SidebarGroupLabel" 461 + 462 + const SidebarGroupAction = React.forwardRef< 463 + HTMLButtonElement, 464 + React.ComponentProps<"button"> & { asChild?: boolean } 465 + >(({ className, asChild = false, ...props }, ref) => { 466 + const Comp = asChild ? Slot : "button" 467 + 468 + return ( 469 + <Comp 470 + ref={ref} 471 + data-sidebar="group-action" 472 + className={cn( 473 + "absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", 474 + // Increases the hit area of the button on mobile. 475 + "after:absolute after:-inset-2 after:md:hidden", 476 + "group-data-[collapsible=icon]:hidden", 477 + className 478 + )} 479 + {...props} 480 + /> 481 + ) 482 + }) 483 + SidebarGroupAction.displayName = "SidebarGroupAction" 484 + 485 + const SidebarGroupContent = React.forwardRef< 486 + HTMLDivElement, 487 + React.ComponentProps<"div"> 488 + >(({ className, ...props }, ref) => ( 489 + <div 490 + ref={ref} 491 + data-sidebar="group-content" 492 + className={cn("w-full text-sm", className)} 493 + {...props} 494 + /> 495 + )) 496 + SidebarGroupContent.displayName = "SidebarGroupContent" 497 + 498 + const SidebarMenu = React.forwardRef< 499 + HTMLUListElement, 500 + React.ComponentProps<"ul"> 501 + >(({ className, ...props }, ref) => ( 502 + <ul 503 + ref={ref} 504 + data-sidebar="menu" 505 + className={cn("flex w-full min-w-0 flex-col gap-1", className)} 506 + {...props} 507 + /> 508 + )) 509 + SidebarMenu.displayName = "SidebarMenu" 510 + 511 + const SidebarMenuItem = React.forwardRef< 512 + HTMLLIElement, 513 + React.ComponentProps<"li"> 514 + >(({ className, ...props }, ref) => ( 515 + <li 516 + ref={ref} 517 + data-sidebar="menu-item" 518 + className={cn("group/menu-item relative", className)} 519 + {...props} 520 + /> 521 + )) 522 + SidebarMenuItem.displayName = "SidebarMenuItem" 523 + 524 + const sidebarMenuButtonVariants = cva( 525 + "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", 526 + { 527 + variants: { 528 + variant: { 529 + default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", 530 + outline: 531 + "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", 532 + }, 533 + size: { 534 + default: "h-8 text-sm", 535 + sm: "h-7 text-xs", 536 + lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0", 537 + }, 538 + }, 539 + defaultVariants: { 540 + variant: "default", 541 + size: "default", 542 + }, 543 + } 544 + ) 545 + 546 + const SidebarMenuButton = React.forwardRef< 547 + HTMLButtonElement, 548 + React.ComponentProps<"button"> & { 549 + asChild?: boolean 550 + isActive?: boolean 551 + tooltip?: string | React.ComponentProps<typeof TooltipContent> 552 + } & VariantProps<typeof sidebarMenuButtonVariants> 553 + >( 554 + ( 555 + { 556 + asChild = false, 557 + isActive = false, 558 + variant = "default", 559 + size = "default", 560 + tooltip, 561 + className, 562 + ...props 563 + }, 564 + ref 565 + ) => { 566 + const Comp = asChild ? Slot : "button" 567 + const { isMobile, state } = useSidebar() 568 + 569 + const button = ( 570 + <Comp 571 + ref={ref} 572 + data-sidebar="menu-button" 573 + data-size={size} 574 + data-active={isActive} 575 + className={cn(sidebarMenuButtonVariants({ variant, size }), className)} 576 + {...props} 577 + /> 578 + ) 579 + 580 + if (!tooltip) { 581 + return button 582 + } 583 + 584 + if (typeof tooltip === "string") { 585 + tooltip = { 586 + children: tooltip, 587 + } 588 + } 589 + 590 + return ( 591 + <Tooltip> 592 + <TooltipTrigger asChild>{button}</TooltipTrigger> 593 + <TooltipContent 594 + side="right" 595 + align="center" 596 + hidden={state !== "collapsed" || isMobile} 597 + {...tooltip} 598 + /> 599 + </Tooltip> 600 + ) 601 + } 602 + ) 603 + SidebarMenuButton.displayName = "SidebarMenuButton" 604 + 605 + const SidebarMenuAction = React.forwardRef< 606 + HTMLButtonElement, 607 + React.ComponentProps<"button"> & { 608 + asChild?: boolean 609 + showOnHover?: boolean 610 + } 611 + >(({ className, asChild = false, showOnHover = false, ...props }, ref) => { 612 + const Comp = asChild ? Slot : "button" 613 + 614 + return ( 615 + <Comp 616 + ref={ref} 617 + data-sidebar="menu-action" 618 + className={cn( 619 + "absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0", 620 + // Increases the hit area of the button on mobile. 621 + "after:absolute after:-inset-2 after:md:hidden", 622 + "peer-data-[size=sm]/menu-button:top-1", 623 + "peer-data-[size=default]/menu-button:top-1.5", 624 + "peer-data-[size=lg]/menu-button:top-2.5", 625 + "group-data-[collapsible=icon]:hidden", 626 + showOnHover && 627 + "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0", 628 + className 629 + )} 630 + {...props} 631 + /> 632 + ) 633 + }) 634 + SidebarMenuAction.displayName = "SidebarMenuAction" 635 + 636 + const SidebarMenuBadge = React.forwardRef< 637 + HTMLDivElement, 638 + React.ComponentProps<"div"> 639 + >(({ className, ...props }, ref) => ( 640 + <div 641 + ref={ref} 642 + data-sidebar="menu-badge" 643 + className={cn( 644 + "pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground", 645 + "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground", 646 + "peer-data-[size=sm]/menu-button:top-1", 647 + "peer-data-[size=default]/menu-button:top-1.5", 648 + "peer-data-[size=lg]/menu-button:top-2.5", 649 + "group-data-[collapsible=icon]:hidden", 650 + className 651 + )} 652 + {...props} 653 + /> 654 + )) 655 + SidebarMenuBadge.displayName = "SidebarMenuBadge" 656 + 657 + const SidebarMenuSkeleton = React.forwardRef< 658 + HTMLDivElement, 659 + React.ComponentProps<"div"> & { 660 + showIcon?: boolean 661 + } 662 + >(({ className, showIcon = false, ...props }, ref) => { 663 + // Random width between 50 to 90%. 664 + const width = React.useMemo(() => { 665 + return `${Math.floor(Math.random() * 40) + 50}%` 666 + }, []) 667 + 668 + return ( 669 + <div 670 + ref={ref} 671 + data-sidebar="menu-skeleton" 672 + className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)} 673 + {...props} 674 + > 675 + {showIcon && ( 676 + <Skeleton 677 + className="size-4 rounded-md" 678 + data-sidebar="menu-skeleton-icon" 679 + /> 680 + )} 681 + <Skeleton 682 + className="h-4 max-w-[--skeleton-width] flex-1" 683 + data-sidebar="menu-skeleton-text" 684 + style={ 685 + { 686 + "--skeleton-width": width, 687 + } as React.CSSProperties 688 + } 689 + /> 690 + </div> 691 + ) 692 + }) 693 + SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton" 694 + 695 + const SidebarMenuSub = React.forwardRef< 696 + HTMLUListElement, 697 + React.ComponentProps<"ul"> 698 + >(({ className, ...props }, ref) => ( 699 + <ul 700 + ref={ref} 701 + data-sidebar="menu-sub" 702 + className={cn( 703 + "mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5", 704 + "group-data-[collapsible=icon]:hidden", 705 + className 706 + )} 707 + {...props} 708 + /> 709 + )) 710 + SidebarMenuSub.displayName = "SidebarMenuSub" 711 + 712 + const SidebarMenuSubItem = React.forwardRef< 713 + HTMLLIElement, 714 + React.ComponentProps<"li"> 715 + >(({ ...props }, ref) => <li ref={ref} {...props} />) 716 + SidebarMenuSubItem.displayName = "SidebarMenuSubItem" 717 + 718 + const SidebarMenuSubButton = React.forwardRef< 719 + HTMLAnchorElement, 720 + React.ComponentProps<"a"> & { 721 + asChild?: boolean 722 + size?: "sm" | "md" 723 + isActive?: boolean 724 + } 725 + >(({ asChild = false, size = "md", isActive, className, ...props }, ref) => { 726 + const Comp = asChild ? Slot : "a" 727 + 728 + return ( 729 + <Comp 730 + ref={ref} 731 + data-sidebar="menu-sub-button" 732 + data-size={size} 733 + data-active={isActive} 734 + className={cn( 735 + "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground", 736 + "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", 737 + size === "sm" && "text-xs", 738 + size === "md" && "text-sm", 739 + "group-data-[collapsible=icon]:hidden", 740 + className 741 + )} 742 + {...props} 743 + /> 744 + ) 745 + }) 746 + SidebarMenuSubButton.displayName = "SidebarMenuSubButton" 747 + 748 + export { 749 + Sidebar, 750 + SidebarContent, 751 + SidebarFooter, 752 + SidebarGroup, 753 + SidebarGroupAction, 754 + SidebarGroupContent, 755 + SidebarGroupLabel, 756 + SidebarHeader, 757 + SidebarInput, 758 + SidebarInset, 759 + SidebarMenu, 760 + SidebarMenuAction, 761 + SidebarMenuBadge, 762 + SidebarMenuButton, 763 + SidebarMenuItem, 764 + SidebarMenuSkeleton, 765 + SidebarMenuSub, 766 + SidebarMenuSubButton, 767 + SidebarMenuSubItem, 768 + SidebarProvider, 769 + SidebarRail, 770 + SidebarSeparator, 771 + SidebarTrigger, 772 + useSidebar, 773 + }
+13
web/src/components/ui/skeleton.tsx
··· 1 + import { cn } from "@/lib/utils" 2 + 3 + function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 + return ( 5 + <div 6 + data-slot="skeleton" 7 + className={cn("bg-accent animate-pulse rounded-md", className)} 8 + {...props} 9 + /> 10 + ) 11 + } 12 + 13 + export { Skeleton }
+61
web/src/components/ui/tooltip.tsx
··· 1 + "use client" 2 + 3 + import * as React from "react" 4 + import * as TooltipPrimitive from "@radix-ui/react-tooltip" 5 + 6 + import { cn } from "@/lib/utils" 7 + 8 + function TooltipProvider({ 9 + delayDuration = 0, 10 + ...props 11 + }: React.ComponentProps<typeof TooltipPrimitive.Provider>) { 12 + return ( 13 + <TooltipPrimitive.Provider 14 + data-slot="tooltip-provider" 15 + delayDuration={delayDuration} 16 + {...props} 17 + /> 18 + ) 19 + } 20 + 21 + function Tooltip({ 22 + ...props 23 + }: React.ComponentProps<typeof TooltipPrimitive.Root>) { 24 + return ( 25 + <TooltipProvider> 26 + <TooltipPrimitive.Root data-slot="tooltip" {...props} /> 27 + </TooltipProvider> 28 + ) 29 + } 30 + 31 + function TooltipTrigger({ 32 + ...props 33 + }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { 34 + return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} /> 35 + } 36 + 37 + function TooltipContent({ 38 + className, 39 + sideOffset = 0, 40 + children, 41 + ...props 42 + }: React.ComponentProps<typeof TooltipPrimitive.Content>) { 43 + return ( 44 + <TooltipPrimitive.Portal> 45 + <TooltipPrimitive.Content 46 + data-slot="tooltip-content" 47 + sideOffset={sideOffset} 48 + className={cn( 49 + "bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance", 50 + className 51 + )} 52 + {...props} 53 + > 54 + {children} 55 + <TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" /> 56 + </TooltipPrimitive.Content> 57 + </TooltipPrimitive.Portal> 58 + ) 59 + } 60 + 61 + export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
+19
web/src/hooks/use-mobile.ts
··· 1 + import * as React from "react" 2 + 3 + const MOBILE_BREAKPOINT = 768 4 + 5 + export function useIsMobile() { 6 + const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined) 7 + 8 + React.useEffect(() => { 9 + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 + const onChange = () => { 11 + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 + } 13 + mql.addEventListener("change", onChange) 14 + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 + return () => mql.removeEventListener("change", onChange) 16 + }, []) 17 + 18 + return !!isMobile 19 + }