chore: added dashboard components d7cdfcd3
Steve · 2025-10-27 16:22 25 file(s) · +2590 −0
bun.lock +105 −0
7 7
        "@evolu/common": "^6.0.1-preview.19",
8 8
        "@evolu/react": "^9.0.1-preview.4",
9 9
        "@evolu/react-web": "^1.0.1-preview.3",
10 +
        "@radix-ui/react-avatar": "^1.1.10",
11 +
        "@radix-ui/react-collapsible": "^1.1.12",
12 +
        "@radix-ui/react-dialog": "^1.1.15",
13 +
        "@radix-ui/react-dropdown-menu": "^2.1.16",
14 +
        "@radix-ui/react-label": "^2.1.7",
15 +
        "@radix-ui/react-popover": "^1.1.15",
16 +
        "@radix-ui/react-separator": "^1.1.7",
10 17
        "@radix-ui/react-slot": "^1.2.3",
18 +
        "@radix-ui/react-switch": "^1.2.6",
19 +
        "@radix-ui/react-tooltip": "^1.2.8",
11 20
        "@tailwindcss/vite": "^4.1.16",
12 21
        "class-variance-authority": "^0.7.1",
13 22
        "clsx": "^2.1.1",
108 117
109 118
    "@evolu/web": ["@evolu/web@1.0.1-preview.5", "", { "dependencies": { "@sqlite.org/sqlite-wasm": "3.50.4-build1" }, "peerDependencies": { "@evolu/common": "^6.0.1-preview.18" } }, "sha512-3MHTpw7Dm7xgKjp1s9lymVNA+FbobhcsBabs3vohUv26sqF5bl4/tZeVrdORYL+mT9K3TPiGolzi19nZF2N+hQ=="],
110 119
120 +
    "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
121 +
122 +
    "@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=="],
123 +
124 +
    "@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=="],
125 +
126 +
    "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
127 +
111 128
    "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
112 129
113 130
    "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
153 170
    "@oxc-project/runtime": ["@oxc-project/runtime@0.92.0", "", {}, "sha512-Z7x2dZOmznihvdvCvLKMl+nswtOSVxS2H2ocar+U9xx6iMfTp0VGIrX6a4xB1v80IwOPC7dT1LXIJrY70Xu3Jw=="],
154 171
155 172
    "@oxc-project/types": ["@oxc-project/types@0.93.0", "", {}, "sha512-yNtwmWZIBtJsMr5TEfoZFDxIWV6OdScOpza/f5YxbqUMJk+j6QX3Cf3jgZShGEFYWQJ5j9mJ6jM0tZHu2J9Yrg=="],
173 +
174 +
    "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
175 +
176 +
    "@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=="],
177 +
178 +
    "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@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-is-hydrated": "0.1.0", "@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-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="],
179 +
180 +
    "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "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-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@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" }, "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-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="],
181 +
182 +
    "@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=="],
156 183
157 184
    "@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=="],
158 185
186 +
    "@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=="],
187 +
188 +
    "@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=="],
189 +
190 +
    "@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=="],
191 +
192 +
    "@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=="],
193 +
194 +
    "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "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-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "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-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="],
195 +
196 +
    "@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=="],
197 +
198 +
    "@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=="],
199 +
200 +
    "@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=="],
201 +
202 +
    "@radix-ui/react-label": ["@radix-ui/react-label@2.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-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
203 +
204 +
    "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@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-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-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-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "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-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
205 +
206 +
    "@radix-ui/react-popover": ["@radix-ui/react-popover@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-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", "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-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
207 +
208 +
    "@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=="],
209 +
210 +
    "@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=="],
211 +
212 +
    "@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=="],
213 +
214 +
    "@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=="],
215 +
216 +
    "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@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-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "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-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
217 +
218 +
    "@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=="],
219 +
159 220
    "@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=="],
160 221
222 +
    "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "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-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@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-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
223 +
224 +
    "@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=="],
225 +
226 +
    "@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=="],
227 +
228 +
    "@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=="],
229 +
230 +
    "@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=="],
231 +
232 +
    "@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=="],
233 +
234 +
    "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.0", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA=="],
235 +
236 +
    "@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=="],
237 +
238 +
    "@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=="],
239 +
240 +
    "@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=="],
241 +
242 +
    "@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=="],
243 +
244 +
    "@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=="],
245 +
246 +
    "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
247 +
161 248
    "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.41", "", { "os": "android", "cpu": "arm64" }, "sha512-Edflndd9lU7JVhVIvJlZhdCj5DkhYDJPIRn4Dx0RUdfc8asP9xHOI5gMd8MesDDx+BJpdIT/uAmVTearteU/mQ=="],
162 249
163 250
    "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.41", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XGCzqfjdk7550PlyZRTBKbypXrB7ATtXhw/+bjtxnklLQs0mKP/XkQVOKyn9qGKSlvH8I56JLYryVxl0PCvSNw=="],
278 365
279 366
    "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
280 367
368 +
    "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
369 +
281 370
    "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
282 371
283 372
    "baseline-browser-mapping": ["baseline-browser-mapping@2.8.20", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ=="],
315 404
    "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
316 405
317 406
    "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
407 +
408 +
    "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
318 409
319 410
    "electron-to-chromium": ["electron-to-chromium@1.5.240", "", {}, "sha512-OBwbZjWgrCOH+g6uJsA2/7Twpas2OlepS9uvByJjR2datRDuKGYeD+nP8lBBks2qnB7bGJNHDUx7c/YLaT3QMQ=="],
320 411
370 461
371 462
    "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
372 463
464 +
    "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
465 +
373 466
    "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
374 467
375 468
    "globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="],
502 595
503 596
    "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
504 597
598 +
    "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=="],
599 +
600 +
    "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=="],
601 +
602 +
    "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=="],
603 +
505 604
    "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
506 605
507 606
    "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
551 650
    "update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="],
552 651
553 652
    "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
653 +
654 +
    "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=="],
655 +
656 +
    "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=="],
657 +
658 +
    "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
554 659
555 660
    "vite": ["rolldown-vite@7.1.14", "", { "dependencies": { "@oxc-project/runtime": "0.92.0", "fdir": "^6.5.0", "lightningcss": "^1.30.1", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rolldown": "1.0.0-beta.41", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "esbuild": "^0.25.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-eSiiRJmovt8qDJkGyZuLnbxAOAdie6NCmmd0NkTC0RJI9duiSBTfr8X2mBYJOUFzxQa2USaHmL99J9uMxkjCyw=="],
556 661
package.json +9 −0
13 13
    "@evolu/common": "^6.0.1-preview.19",
14 14
    "@evolu/react": "^9.0.1-preview.4",
15 15
    "@evolu/react-web": "^1.0.1-preview.3",
16 +
    "@radix-ui/react-avatar": "^1.1.10",
17 +
    "@radix-ui/react-collapsible": "^1.1.12",
18 +
    "@radix-ui/react-dialog": "^1.1.15",
19 +
    "@radix-ui/react-dropdown-menu": "^2.1.16",
20 +
    "@radix-ui/react-label": "^2.1.7",
21 +
    "@radix-ui/react-popover": "^1.1.15",
22 +
    "@radix-ui/react-separator": "^1.1.7",
16 23
    "@radix-ui/react-slot": "^1.2.3",
24 +
    "@radix-ui/react-switch": "^1.2.6",
25 +
    "@radix-ui/react-tooltip": "^1.2.8",
17 26
    "@tailwindcss/vite": "^4.1.16",
18 27
    "class-variance-authority": "^0.7.1",
19 28
    "clsx": "^2.1.1",
src/components/app-sidebar.tsx (added) +259 −0
1 +
"use client";
2 +
3 +
import * as React from "react";
4 +
import {
5 +
	ArchiveX,
6 +
	Circle,
7 +
	Command,
8 +
	File,
9 +
	Inbox,
10 +
	Send,
11 +
	Star,
12 +
	Trash2,
13 +
} from "lucide-react";
14 +
15 +
import { NavUser } from "@/components/nav-user";
16 +
import { Label } from "@/components/ui/label";
17 +
import {
18 +
	Sidebar,
19 +
	SidebarContent,
20 +
	SidebarFooter,
21 +
	SidebarGroup,
22 +
	SidebarGroupContent,
23 +
	SidebarHeader,
24 +
	SidebarInput,
25 +
	SidebarMenu,
26 +
	SidebarMenuButton,
27 +
	SidebarMenuItem,
28 +
	useSidebar,
29 +
} from "@/components/ui/sidebar";
30 +
import { Switch } from "@/components/ui/switch";
31 +
32 +
// This is sample data
33 +
const data = {
34 +
	user: {
35 +
		name: "shadcn",
36 +
		email: "m@example.com",
37 +
		avatar: "/avatars/shadcn.jpg",
38 +
	},
39 +
	navMain: [
40 +
		{
41 +
			title: "Today",
42 +
			url: "#",
43 +
			icon: Inbox,
44 +
			isActive: true,
45 +
		},
46 +
		{
47 +
			title: "Unread",
48 +
			url: "#",
49 +
			icon: Circle,
50 +
			isActive: false,
51 +
		},
52 +
		{
53 +
			title: "Starred",
54 +
			url: "#",
55 +
			icon: Star,
56 +
			isActive: false,
57 +
		},
58 +
	],
59 +
	mails: [
60 +
		{
61 +
			name: "William Smith",
62 +
			email: "williamsmith@example.com",
63 +
			subject: "Meeting Tomorrow",
64 +
			date: "09:34 AM",
65 +
			teaser:
66 +
				"Hi team, just a reminder about our meeting tomorrow at 10 AM.\nPlease come prepared with your project updates.",
67 +
		},
68 +
		{
69 +
			name: "Alice Smith",
70 +
			email: "alicesmith@example.com",
71 +
			subject: "Re: Project Update",
72 +
			date: "Yesterday",
73 +
			teaser:
74 +
				"Thanks for the update. The progress looks great so far.\nLet's schedule a call to discuss the next steps.",
75 +
		},
76 +
		{
77 +
			name: "Bob Johnson",
78 +
			email: "bobjohnson@example.com",
79 +
			subject: "Weekend Plans",
80 +
			date: "2 days ago",
81 +
			teaser:
82 +
				"Hey everyone! I'm thinking of organizing a team outing this weekend.\nWould you be interested in a hiking trip or a beach day?",
83 +
		},
84 +
		{
85 +
			name: "Emily Davis",
86 +
			email: "emilydavis@example.com",
87 +
			subject: "Re: Question about Budget",
88 +
			date: "2 days ago",
89 +
			teaser:
90 +
				"I've reviewed the budget numbers you sent over.\nCan we set up a quick call to discuss some potential adjustments?",
91 +
		},
92 +
		{
93 +
			name: "Michael Wilson",
94 +
			email: "michaelwilson@example.com",
95 +
			subject: "Important Announcement",
96 +
			date: "1 week ago",
97 +
			teaser:
98 +
				"Please join us for an all-hands meeting this Friday at 3 PM.\nWe have some exciting news to share about the company's future.",
99 +
		},
100 +
		{
101 +
			name: "Sarah Brown",
102 +
			email: "sarahbrown@example.com",
103 +
			subject: "Re: Feedback on Proposal",
104 +
			date: "1 week ago",
105 +
			teaser:
106 +
				"Thank you for sending over the proposal. I've reviewed it and have some thoughts.\nCould we schedule a meeting to discuss my feedback in detail?",
107 +
		},
108 +
		{
109 +
			name: "David Lee",
110 +
			email: "davidlee@example.com",
111 +
			subject: "New Project Idea",
112 +
			date: "1 week ago",
113 +
			teaser:
114 +
				"I've been brainstorming and came up with an interesting project concept.\nDo you have time this week to discuss its potential impact and feasibility?",
115 +
		},
116 +
		{
117 +
			name: "Olivia Wilson",
118 +
			email: "oliviawilson@example.com",
119 +
			subject: "Vacation Plans",
120 +
			date: "1 week ago",
121 +
			teaser:
122 +
				"Just a heads up that I'll be taking a two-week vacation next month.\nI'll make sure all my projects are up to date before I leave.",
123 +
		},
124 +
		{
125 +
			name: "James Martin",
126 +
			email: "jamesmartin@example.com",
127 +
			subject: "Re: Conference Registration",
128 +
			date: "1 week ago",
129 +
			teaser:
130 +
				"I've completed the registration for the upcoming tech conference.\nLet me know if you need any additional information from my end.",
131 +
		},
132 +
		{
133 +
			name: "Sophia White",
134 +
			email: "sophiawhite@example.com",
135 +
			subject: "Team Dinner",
136 +
			date: "1 week ago",
137 +
			teaser:
138 +
				"To celebrate our recent project success, I'd like to organize a team dinner.\nAre you available next Friday evening? Please let me know your preferences.",
139 +
		},
140 +
	],
141 +
};
142 +
143 +
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
144 +
	// Note: I'm using state to show active item.
145 +
	// IRL you should use the url/router.
146 +
	const [activeItem, setActiveItem] = React.useState(data.navMain[0]);
147 +
	const [mails, setMails] = React.useState(data.mails);
148 +
	const { setOpen } = useSidebar();
149 +
150 +
	return (
151 +
		<Sidebar
152 +
			collapsible="icon"
153 +
			className="overflow-hidden *:data-[sidebar=sidebar]:flex-row"
154 +
			{...props}
155 +
		>
156 +
			{/* This is the first sidebar */}
157 +
			{/* We disable collapsible and adjust width to icon. */}
158 +
			{/* This will make the sidebar appear as icons. */}
159 +
			<Sidebar
160 +
				collapsible="none"
161 +
				className="w-[calc(var(--sidebar-width-icon)+1px)]! border-r"
162 +
			>
163 +
				<SidebarHeader>
164 +
					<SidebarMenu>
165 +
						<SidebarMenuItem>
166 +
							<SidebarMenuButton size="lg" asChild className="md:h-8 md:p-0">
167 +
								<a href="#">
168 +
									<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
169 +
										<Command className="size-4" />
170 +
									</div>
171 +
									<div className="grid flex-1 text-left text-sm leading-tight">
172 +
										<span className="truncate font-medium">Acme Inc</span>
173 +
										<span className="truncate text-xs">Enterprise</span>
174 +
									</div>
175 +
								</a>
176 +
							</SidebarMenuButton>
177 +
						</SidebarMenuItem>
178 +
					</SidebarMenu>
179 +
				</SidebarHeader>
180 +
				<SidebarContent>
181 +
					<SidebarGroup>
182 +
						<SidebarGroupContent className="px-1.5 md:px-0">
183 +
							<SidebarMenu>
184 +
								{data.navMain.map((item) => (
185 +
									<SidebarMenuItem key={item.title}>
186 +
										<SidebarMenuButton
187 +
											tooltip={{
188 +
												children: item.title,
189 +
												hidden: false,
190 +
											}}
191 +
											onClick={() => {
192 +
												setActiveItem(item);
193 +
												const mail = data.mails.sort(() => Math.random() - 0.5);
194 +
												setMails(
195 +
													mail.slice(
196 +
														0,
197 +
														Math.max(5, Math.floor(Math.random() * 10) + 1),
198 +
													),
199 +
												);
200 +
												setOpen(true);
201 +
											}}
202 +
											isActive={activeItem?.title === item.title}
203 +
											className="px-2.5 md:px-2"
204 +
										>
205 +
											<item.icon />
206 +
											<span>{item.title}</span>
207 +
										</SidebarMenuButton>
208 +
									</SidebarMenuItem>
209 +
								))}
210 +
							</SidebarMenu>
211 +
						</SidebarGroupContent>
212 +
					</SidebarGroup>
213 +
				</SidebarContent>
214 +
				<SidebarFooter>
215 +
					<NavUser user={data.user} />
216 +
				</SidebarFooter>
217 +
			</Sidebar>
218 +
219 +
			{/* This is the second sidebar */}
220 +
			{/* We disable collapsible and let it fill remaining space */}
221 +
			<Sidebar collapsible="none" className="hidden flex-1 md:flex">
222 +
				<SidebarHeader className="gap-3.5 border-b p-4">
223 +
					<div className="flex w-full items-center justify-between">
224 +
						<div className="text-foreground text-base font-medium">
225 +
							{activeItem?.title}
226 +
						</div>
227 +
						<Label className="flex items-center gap-2 text-sm">
228 +
							<span>Unreads</span>
229 +
							<Switch className="shadow-none" />
230 +
						</Label>
231 +
					</div>
232 +
					<SidebarInput placeholder="Type to search..." />
233 +
				</SidebarHeader>
234 +
				<SidebarContent>
235 +
					<SidebarGroup className="px-0">
236 +
						<SidebarGroupContent>
237 +
							{mails.map((mail) => (
238 +
								<a
239 +
									href="#"
240 +
									key={mail.email}
241 +
									className="hover:bg-sidebar-accent hover:text-sidebar-accent-foreground flex flex-col items-start gap-2 border-b p-4 text-sm leading-tight whitespace-nowrap last:border-b-0"
242 +
								>
243 +
									<div className="flex w-full items-center gap-2">
244 +
										<span>{mail.name}</span>{" "}
245 +
										<span className="ml-auto text-xs">{mail.date}</span>
246 +
									</div>
247 +
									<span className="font-medium">{mail.subject}</span>
248 +
									<span className="line-clamp-2 w-[260px] text-xs whitespace-break-spaces">
249 +
										{mail.teaser}
250 +
									</span>
251 +
								</a>
252 +
							))}
253 +
						</SidebarGroupContent>
254 +
					</SidebarGroup>
255 +
				</SidebarContent>
256 +
			</Sidebar>
257 +
		</Sidebar>
258 +
	);
259 +
}
src/components/dashboard.tsx (added) +64 −0
1 +
import { useEvolu } from "../main";
2 +
import { AppSidebar } from "@/components/app-sidebar";
3 +
import {
4 +
	Breadcrumb,
5 +
	BreadcrumbItem,
6 +
	BreadcrumbLink,
7 +
	BreadcrumbList,
8 +
	BreadcrumbPage,
9 +
	BreadcrumbSeparator,
10 +
} from "@/components/ui/breadcrumb";
11 +
import { Separator } from "@/components/ui/separator";
12 +
import {
13 +
	SidebarInset,
14 +
	SidebarProvider,
15 +
	SidebarTrigger,
16 +
} from "@/components/ui/sidebar";
17 +
18 +
function Dashboard() {
19 +
	const { insert, update } = useEvolu();
20 +
21 +
	return (
22 +
		<main className="min-h-screen w-full items-center justify-center flex-col flex gap-2">
23 +
			<SidebarProvider
24 +
				style={
25 +
					{
26 +
						"--sidebar-width": "350px",
27 +
					} as React.CSSProperties
28 +
				}
29 +
			>
30 +
				<AppSidebar />
31 +
				<SidebarInset>
32 +
					<header className="bg-background sticky top-0 flex shrink-0 items-center gap-2 border-b p-4">
33 +
						<SidebarTrigger className="-ml-1" />
34 +
						<Separator
35 +
							orientation="vertical"
36 +
							className="mr-2 data-[orientation=vertical]:h-4"
37 +
						/>
38 +
						<Breadcrumb>
39 +
							<BreadcrumbList>
40 +
								<BreadcrumbItem className="hidden md:block">
41 +
									<BreadcrumbLink href="#">All Inboxes</BreadcrumbLink>
42 +
								</BreadcrumbItem>
43 +
								<BreadcrumbSeparator className="hidden md:block" />
44 +
								<BreadcrumbItem>
45 +
									<BreadcrumbPage>Inbox</BreadcrumbPage>
46 +
								</BreadcrumbItem>
47 +
							</BreadcrumbList>
48 +
						</Breadcrumb>
49 +
					</header>
50 +
					<div className="flex flex-1 flex-col gap-4 p-4">
51 +
						{Array.from({ length: 24 }).map((_, index) => (
52 +
							<div
53 +
								key={index}
54 +
								className="bg-muted/50 aspect-video h-12 w-full rounded-lg"
55 +
							/>
56 +
						))}
57 +
					</div>
58 +
				</SidebarInset>
59 +
			</SidebarProvider>
60 +
		</main>
61 +
	);
62 +
}
63 +
64 +
export default Dashboard;
src/components/nav-actions.tsx (added) +151 −0
1 +
import * as React from "react"
2 +
import {
3 +
  ArrowDown,
4 +
  ArrowUp,
5 +
  Bell,
6 +
  Copy,
7 +
  CornerUpLeft,
8 +
  CornerUpRight,
9 +
  FileText,
10 +
  GalleryVerticalEnd,
11 +
  LineChart,
12 +
  Link,
13 +
  MoreHorizontal,
14 +
  Settings2,
15 +
  Star,
16 +
  Trash,
17 +
  Trash2,
18 +
} from "lucide-react"
19 +
20 +
import { Button } from "@/components/ui/button"
21 +
import {
22 +
  Popover,
23 +
  PopoverContent,
24 +
  PopoverTrigger,
25 +
} from "@/components/ui/popover"
26 +
import {
27 +
  Sidebar,
28 +
  SidebarContent,
29 +
  SidebarGroup,
30 +
  SidebarGroupContent,
31 +
  SidebarMenu,
32 +
  SidebarMenuButton,
33 +
  SidebarMenuItem,
34 +
} from "@/components/ui/sidebar"
35 +
36 +
const data = [
37 +
  [
38 +
    {
39 +
      label: "Customize Page",
40 +
      icon: Settings2,
41 +
    },
42 +
    {
43 +
      label: "Turn into wiki",
44 +
      icon: FileText,
45 +
    },
46 +
  ],
47 +
  [
48 +
    {
49 +
      label: "Copy Link",
50 +
      icon: Link,
51 +
    },
52 +
    {
53 +
      label: "Duplicate",
54 +
      icon: Copy,
55 +
    },
56 +
    {
57 +
      label: "Move to",
58 +
      icon: CornerUpRight,
59 +
    },
60 +
    {
61 +
      label: "Move to Trash",
62 +
      icon: Trash2,
63 +
    },
64 +
  ],
65 +
  [
66 +
    {
67 +
      label: "Undo",
68 +
      icon: CornerUpLeft,
69 +
    },
70 +
    {
71 +
      label: "View analytics",
72 +
      icon: LineChart,
73 +
    },
74 +
    {
75 +
      label: "Version History",
76 +
      icon: GalleryVerticalEnd,
77 +
    },
78 +
    {
79 +
      label: "Show delete pages",
80 +
      icon: Trash,
81 +
    },
82 +
    {
83 +
      label: "Notifications",
84 +
      icon: Bell,
85 +
    },
86 +
  ],
87 +
  [
88 +
    {
89 +
      label: "Import",
90 +
      icon: ArrowUp,
91 +
    },
92 +
    {
93 +
      label: "Export",
94 +
      icon: ArrowDown,
95 +
    },
96 +
  ],
97 +
]
98 +
99 +
export function NavActions() {
100 +
  const [isOpen, setIsOpen] = React.useState(false)
101 +
102 +
  React.useEffect(() => {
103 +
    setIsOpen(true)
104 +
  }, [])
105 +
106 +
  return (
107 +
    <div className="flex items-center gap-2 text-sm">
108 +
      <div className="text-muted-foreground hidden font-medium md:inline-block">
109 +
        Edit Oct 08
110 +
      </div>
111 +
      <Button variant="ghost" size="icon" className="h-7 w-7">
112 +
        <Star />
113 +
      </Button>
114 +
      <Popover open={isOpen} onOpenChange={setIsOpen}>
115 +
        <PopoverTrigger asChild>
116 +
          <Button
117 +
            variant="ghost"
118 +
            size="icon"
119 +
            className="data-[state=open]:bg-accent h-7 w-7"
120 +
          >
121 +
            <MoreHorizontal />
122 +
          </Button>
123 +
        </PopoverTrigger>
124 +
        <PopoverContent
125 +
          className="w-56 overflow-hidden rounded-lg p-0"
126 +
          align="end"
127 +
        >
128 +
          <Sidebar collapsible="none" className="bg-transparent">
129 +
            <SidebarContent>
130 +
              {data.map((group, index) => (
131 +
                <SidebarGroup key={index} className="border-b last:border-none">
132 +
                  <SidebarGroupContent className="gap-0">
133 +
                    <SidebarMenu>
134 +
                      {group.map((item, index) => (
135 +
                        <SidebarMenuItem key={index}>
136 +
                          <SidebarMenuButton>
137 +
                            <item.icon /> <span>{item.label}</span>
138 +
                          </SidebarMenuButton>
139 +
                        </SidebarMenuItem>
140 +
                      ))}
141 +
                    </SidebarMenu>
142 +
                  </SidebarGroupContent>
143 +
                </SidebarGroup>
144 +
              ))}
145 +
            </SidebarContent>
146 +
          </Sidebar>
147 +
        </PopoverContent>
148 +
      </Popover>
149 +
    </div>
150 +
  )
151 +
}
src/components/nav-favorites.tsx (added) +94 −0
1 +
"use client"
2 +
3 +
import {
4 +
  ArrowUpRight,
5 +
  Link,
6 +
  MoreHorizontal,
7 +
  StarOff,
8 +
  Trash2,
9 +
} from "lucide-react"
10 +
11 +
import {
12 +
  DropdownMenu,
13 +
  DropdownMenuContent,
14 +
  DropdownMenuItem,
15 +
  DropdownMenuSeparator,
16 +
  DropdownMenuTrigger,
17 +
} from "@/components/ui/dropdown-menu"
18 +
import {
19 +
  SidebarGroup,
20 +
  SidebarGroupLabel,
21 +
  SidebarMenu,
22 +
  SidebarMenuAction,
23 +
  SidebarMenuButton,
24 +
  SidebarMenuItem,
25 +
  useSidebar,
26 +
} from "@/components/ui/sidebar"
27 +
28 +
export function NavFavorites({
29 +
  favorites,
30 +
}: {
31 +
  favorites: {
32 +
    name: string
33 +
    url: string
34 +
    emoji: string
35 +
  }[]
36 +
}) {
37 +
  const { isMobile } = useSidebar()
38 +
39 +
  return (
40 +
    <SidebarGroup className="group-data-[collapsible=icon]:hidden">
41 +
      <SidebarGroupLabel>Favorites</SidebarGroupLabel>
42 +
      <SidebarMenu>
43 +
        {favorites.map((item) => (
44 +
          <SidebarMenuItem key={item.name}>
45 +
            <SidebarMenuButton asChild>
46 +
              <a href={item.url} title={item.name}>
47 +
                <span>{item.emoji}</span>
48 +
                <span>{item.name}</span>
49 +
              </a>
50 +
            </SidebarMenuButton>
51 +
            <DropdownMenu>
52 +
              <DropdownMenuTrigger asChild>
53 +
                <SidebarMenuAction showOnHover>
54 +
                  <MoreHorizontal />
55 +
                  <span className="sr-only">More</span>
56 +
                </SidebarMenuAction>
57 +
              </DropdownMenuTrigger>
58 +
              <DropdownMenuContent
59 +
                className="w-56 rounded-lg"
60 +
                side={isMobile ? "bottom" : "right"}
61 +
                align={isMobile ? "end" : "start"}
62 +
              >
63 +
                <DropdownMenuItem>
64 +
                  <StarOff className="text-muted-foreground" />
65 +
                  <span>Remove from Favorites</span>
66 +
                </DropdownMenuItem>
67 +
                <DropdownMenuSeparator />
68 +
                <DropdownMenuItem>
69 +
                  <Link className="text-muted-foreground" />
70 +
                  <span>Copy Link</span>
71 +
                </DropdownMenuItem>
72 +
                <DropdownMenuItem>
73 +
                  <ArrowUpRight className="text-muted-foreground" />
74 +
                  <span>Open in New Tab</span>
75 +
                </DropdownMenuItem>
76 +
                <DropdownMenuSeparator />
77 +
                <DropdownMenuItem>
78 +
                  <Trash2 className="text-muted-foreground" />
79 +
                  <span>Delete</span>
80 +
                </DropdownMenuItem>
81 +
              </DropdownMenuContent>
82 +
            </DropdownMenu>
83 +
          </SidebarMenuItem>
84 +
        ))}
85 +
        <SidebarMenuItem>
86 +
          <SidebarMenuButton className="text-sidebar-foreground/70">
87 +
            <MoreHorizontal />
88 +
            <span>More</span>
89 +
          </SidebarMenuButton>
90 +
        </SidebarMenuItem>
91 +
      </SidebarMenu>
92 +
    </SidebarGroup>
93 +
  )
94 +
}
src/components/nav-main.tsx (added) +33 −0
1 +
import { type LucideIcon } from "lucide-react"
2 +
3 +
import {
4 +
  SidebarMenu,
5 +
  SidebarMenuButton,
6 +
  SidebarMenuItem,
7 +
} from "@/components/ui/sidebar"
8 +
9 +
export function NavMain({
10 +
  items,
11 +
}: {
12 +
  items: {
13 +
    title: string
14 +
    url: string
15 +
    icon: LucideIcon
16 +
    isActive?: boolean
17 +
  }[]
18 +
}) {
19 +
  return (
20 +
    <SidebarMenu>
21 +
      {items.map((item) => (
22 +
        <SidebarMenuItem key={item.title}>
23 +
          <SidebarMenuButton asChild isActive={item.isActive}>
24 +
            <a href={item.url}>
25 +
              <item.icon />
26 +
              <span>{item.title}</span>
27 +
            </a>
28 +
          </SidebarMenuButton>
29 +
        </SidebarMenuItem>
30 +
      ))}
31 +
    </SidebarMenu>
32 +
  )
33 +
}
src/components/nav-secondary.tsx (added) +43 −0
1 +
import React from "react"
2 +
import { type LucideIcon } from "lucide-react"
3 +
4 +
import {
5 +
  SidebarGroup,
6 +
  SidebarGroupContent,
7 +
  SidebarMenu,
8 +
  SidebarMenuBadge,
9 +
  SidebarMenuButton,
10 +
  SidebarMenuItem,
11 +
} from "@/components/ui/sidebar"
12 +
13 +
export function NavSecondary({
14 +
  items,
15 +
  ...props
16 +
}: {
17 +
  items: {
18 +
    title: string
19 +
    url: string
20 +
    icon: LucideIcon
21 +
    badge?: React.ReactNode
22 +
  }[]
23 +
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
24 +
  return (
25 +
    <SidebarGroup {...props}>
26 +
      <SidebarGroupContent>
27 +
        <SidebarMenu>
28 +
          {items.map((item) => (
29 +
            <SidebarMenuItem key={item.title}>
30 +
              <SidebarMenuButton asChild>
31 +
                <a href={item.url}>
32 +
                  <item.icon />
33 +
                  <span>{item.title}</span>
34 +
                </a>
35 +
              </SidebarMenuButton>
36 +
              {item.badge && <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>}
37 +
            </SidebarMenuItem>
38 +
          ))}
39 +
        </SidebarMenu>
40 +
      </SidebarGroupContent>
41 +
    </SidebarGroup>
42 +
  )
43 +
}
src/components/nav-user.tsx (added) +112 −0
1 +
import {
2 +
  BadgeCheck,
3 +
  Bell,
4 +
  ChevronsUpDown,
5 +
  CreditCard,
6 +
  LogOut,
7 +
  Sparkles,
8 +
} from "lucide-react"
9 +
10 +
import {
11 +
  Avatar,
12 +
  AvatarFallback,
13 +
  AvatarImage,
14 +
} from "@/components/ui/avatar"
15 +
import {
16 +
  DropdownMenu,
17 +
  DropdownMenuContent,
18 +
  DropdownMenuGroup,
19 +
  DropdownMenuItem,
20 +
  DropdownMenuLabel,
21 +
  DropdownMenuSeparator,
22 +
  DropdownMenuTrigger,
23 +
} from "@/components/ui/dropdown-menu"
24 +
import {
25 +
  SidebarMenu,
26 +
  SidebarMenuButton,
27 +
  SidebarMenuItem,
28 +
  useSidebar,
29 +
} from "@/components/ui/sidebar"
30 +
31 +
export function NavUser({
32 +
  user,
33 +
}: {
34 +
  user: {
35 +
    name: string
36 +
    email: string
37 +
    avatar: string
38 +
  }
39 +
}) {
40 +
  const { isMobile } = useSidebar()
41 +
42 +
  return (
43 +
    <SidebarMenu>
44 +
      <SidebarMenuItem>
45 +
        <DropdownMenu>
46 +
          <DropdownMenuTrigger asChild>
47 +
            <SidebarMenuButton
48 +
              size="lg"
49 +
              className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground md:h-8 md:p-0"
50 +
            >
51 +
              <Avatar className="h-8 w-8 rounded-lg">
52 +
                <AvatarImage src={user.avatar} alt={user.name} />
53 +
                <AvatarFallback className="rounded-lg">CN</AvatarFallback>
54 +
              </Avatar>
55 +
              <div className="grid flex-1 text-left text-sm leading-tight">
56 +
                <span className="truncate font-medium">{user.name}</span>
57 +
                <span className="truncate text-xs">{user.email}</span>
58 +
              </div>
59 +
              <ChevronsUpDown className="ml-auto size-4" />
60 +
            </SidebarMenuButton>
61 +
          </DropdownMenuTrigger>
62 +
          <DropdownMenuContent
63 +
            className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
64 +
            side={isMobile ? "bottom" : "right"}
65 +
            align="end"
66 +
            sideOffset={4}
67 +
          >
68 +
            <DropdownMenuLabel className="p-0 font-normal">
69 +
              <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
70 +
                <Avatar className="h-8 w-8 rounded-lg">
71 +
                  <AvatarImage src={user.avatar} alt={user.name} />
72 +
                  <AvatarFallback className="rounded-lg">CN</AvatarFallback>
73 +
                </Avatar>
74 +
                <div className="grid flex-1 text-left text-sm leading-tight">
75 +
                  <span className="truncate font-medium">{user.name}</span>
76 +
                  <span className="truncate text-xs">{user.email}</span>
77 +
                </div>
78 +
              </div>
79 +
            </DropdownMenuLabel>
80 +
            <DropdownMenuSeparator />
81 +
            <DropdownMenuGroup>
82 +
              <DropdownMenuItem>
83 +
                <Sparkles />
84 +
                Upgrade to Pro
85 +
              </DropdownMenuItem>
86 +
            </DropdownMenuGroup>
87 +
            <DropdownMenuSeparator />
88 +
            <DropdownMenuGroup>
89 +
              <DropdownMenuItem>
90 +
                <BadgeCheck />
91 +
                Account
92 +
              </DropdownMenuItem>
93 +
              <DropdownMenuItem>
94 +
                <CreditCard />
95 +
                Billing
96 +
              </DropdownMenuItem>
97 +
              <DropdownMenuItem>
98 +
                <Bell />
99 +
                Notifications
100 +
              </DropdownMenuItem>
101 +
            </DropdownMenuGroup>
102 +
            <DropdownMenuSeparator />
103 +
            <DropdownMenuItem>
104 +
              <LogOut />
105 +
              Log out
106 +
            </DropdownMenuItem>
107 +
          </DropdownMenuContent>
108 +
        </DropdownMenu>
109 +
      </SidebarMenuItem>
110 +
    </SidebarMenu>
111 +
  )
112 +
}
src/components/nav-workspaces.tsx (added) +85 −0
1 +
import { ChevronRight, MoreHorizontal, Plus } from "lucide-react"
2 +
3 +
import {
4 +
  Collapsible,
5 +
  CollapsibleContent,
6 +
  CollapsibleTrigger,
7 +
} from "@/components/ui/collapsible"
8 +
import {
9 +
  SidebarGroup,
10 +
  SidebarGroupContent,
11 +
  SidebarGroupLabel,
12 +
  SidebarMenu,
13 +
  SidebarMenuAction,
14 +
  SidebarMenuButton,
15 +
  SidebarMenuItem,
16 +
  SidebarMenuSub,
17 +
  SidebarMenuSubButton,
18 +
  SidebarMenuSubItem,
19 +
} from "@/components/ui/sidebar"
20 +
21 +
export function NavWorkspaces({
22 +
  workspaces,
23 +
}: {
24 +
  workspaces: {
25 +
    name: string
26 +
    emoji: React.ReactNode
27 +
    pages: {
28 +
      name: string
29 +
      emoji: React.ReactNode
30 +
    }[]
31 +
  }[]
32 +
}) {
33 +
  return (
34 +
    <SidebarGroup>
35 +
      <SidebarGroupLabel>Workspaces</SidebarGroupLabel>
36 +
      <SidebarGroupContent>
37 +
        <SidebarMenu>
38 +
          {workspaces.map((workspace) => (
39 +
            <Collapsible key={workspace.name}>
40 +
              <SidebarMenuItem>
41 +
                <SidebarMenuButton asChild>
42 +
                  <a href="#">
43 +
                    <span>{workspace.emoji}</span>
44 +
                    <span>{workspace.name}</span>
45 +
                  </a>
46 +
                </SidebarMenuButton>
47 +
                <CollapsibleTrigger asChild>
48 +
                  <SidebarMenuAction
49 +
                    className="bg-sidebar-accent text-sidebar-accent-foreground left-2 data-[state=open]:rotate-90"
50 +
                    showOnHover
51 +
                  >
52 +
                    <ChevronRight />
53 +
                  </SidebarMenuAction>
54 +
                </CollapsibleTrigger>
55 +
                <SidebarMenuAction showOnHover>
56 +
                  <Plus />
57 +
                </SidebarMenuAction>
58 +
                <CollapsibleContent>
59 +
                  <SidebarMenuSub>
60 +
                    {workspace.pages.map((page) => (
61 +
                      <SidebarMenuSubItem key={page.name}>
62 +
                        <SidebarMenuSubButton asChild>
63 +
                          <a href="#">
64 +
                            <span>{page.emoji}</span>
65 +
                            <span>{page.name}</span>
66 +
                          </a>
67 +
                        </SidebarMenuSubButton>
68 +
                      </SidebarMenuSubItem>
69 +
                    ))}
70 +
                  </SidebarMenuSub>
71 +
                </CollapsibleContent>
72 +
              </SidebarMenuItem>
73 +
            </Collapsible>
74 +
          ))}
75 +
          <SidebarMenuItem>
76 +
            <SidebarMenuButton className="text-sidebar-foreground/70">
77 +
              <MoreHorizontal />
78 +
              <span>More</span>
79 +
            </SidebarMenuButton>
80 +
          </SidebarMenuItem>
81 +
        </SidebarMenu>
82 +
      </SidebarGroupContent>
83 +
    </SidebarGroup>
84 +
  )
85 +
}
src/components/team-switcher.tsx (added) +83 −0
1 +
"use client"
2 +
3 +
import * as React from "react"
4 +
import { ChevronDown, Plus } from "lucide-react"
5 +
6 +
import {
7 +
  DropdownMenu,
8 +
  DropdownMenuContent,
9 +
  DropdownMenuItem,
10 +
  DropdownMenuLabel,
11 +
  DropdownMenuSeparator,
12 +
  DropdownMenuShortcut,
13 +
  DropdownMenuTrigger,
14 +
} from "@/components/ui/dropdown-menu"
15 +
import {
16 +
  SidebarMenu,
17 +
  SidebarMenuButton,
18 +
  SidebarMenuItem,
19 +
} from "@/components/ui/sidebar"
20 +
21 +
export function TeamSwitcher({
22 +
  teams,
23 +
}: {
24 +
  teams: {
25 +
    name: string
26 +
    logo: React.ElementType
27 +
    plan: string
28 +
  }[]
29 +
}) {
30 +
  const [activeTeam, setActiveTeam] = React.useState(teams[0])
31 +
32 +
  if (!activeTeam) {
33 +
    return null
34 +
  }
35 +
36 +
  return (
37 +
    <SidebarMenu>
38 +
      <SidebarMenuItem>
39 +
        <DropdownMenu>
40 +
          <DropdownMenuTrigger asChild>
41 +
            <SidebarMenuButton className="w-fit px-1.5">
42 +
              <div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-5 items-center justify-center rounded-md">
43 +
                <activeTeam.logo className="size-3" />
44 +
              </div>
45 +
              <span className="truncate font-medium">{activeTeam.name}</span>
46 +
              <ChevronDown className="opacity-50" />
47 +
            </SidebarMenuButton>
48 +
          </DropdownMenuTrigger>
49 +
          <DropdownMenuContent
50 +
            className="w-64 rounded-lg"
51 +
            align="start"
52 +
            side="bottom"
53 +
            sideOffset={4}
54 +
          >
55 +
            <DropdownMenuLabel className="text-muted-foreground text-xs">
56 +
              Teams
57 +
            </DropdownMenuLabel>
58 +
            {teams.map((team, index) => (
59 +
              <DropdownMenuItem
60 +
                key={team.name}
61 +
                onClick={() => setActiveTeam(team)}
62 +
                className="gap-2 p-2"
63 +
              >
64 +
                <div className="flex size-6 items-center justify-center rounded-xs border">
65 +
                  <team.logo className="size-4 shrink-0" />
66 +
                </div>
67 +
                {team.name}
68 +
                <DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
69 +
              </DropdownMenuItem>
70 +
            ))}
71 +
            <DropdownMenuSeparator />
72 +
            <DropdownMenuItem className="gap-2 p-2">
73 +
              <div className="bg-background flex size-6 items-center justify-center rounded-md border">
74 +
                <Plus className="size-4" />
75 +
              </div>
76 +
              <div className="text-muted-foreground font-medium">Add team</div>
77 +
            </DropdownMenuItem>
78 +
          </DropdownMenuContent>
79 +
        </DropdownMenu>
80 +
      </SidebarMenuItem>
81 +
    </SidebarMenu>
82 +
  )
83 +
}
src/components/ui/avatar.tsx (added) +53 −0
1 +
"use client"
2 +
3 +
import * as React from "react"
4 +
import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 +
6 +
import { cn } from "@/lib/utils"
7 +
8 +
function Avatar({
9 +
  className,
10 +
  ...props
11 +
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
12 +
  return (
13 +
    <AvatarPrimitive.Root
14 +
      data-slot="avatar"
15 +
      className={cn(
16 +
        "relative flex size-8 shrink-0 overflow-hidden rounded-full",
17 +
        className
18 +
      )}
19 +
      {...props}
20 +
    />
21 +
  )
22 +
}
23 +
24 +
function AvatarImage({
25 +
  className,
26 +
  ...props
27 +
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
28 +
  return (
29 +
    <AvatarPrimitive.Image
30 +
      data-slot="avatar-image"
31 +
      className={cn("aspect-square size-full", className)}
32 +
      {...props}
33 +
    />
34 +
  )
35 +
}
36 +
37 +
function AvatarFallback({
38 +
  className,
39 +
  ...props
40 +
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
41 +
  return (
42 +
    <AvatarPrimitive.Fallback
43 +
      data-slot="avatar-fallback"
44 +
      className={cn(
45 +
        "bg-muted flex size-full items-center justify-center rounded-full",
46 +
        className
47 +
      )}
48 +
      {...props}
49 +
    />
50 +
  )
51 +
}
52 +
53 +
export { Avatar, AvatarImage, AvatarFallback }
src/components/ui/breadcrumb.tsx (added) +109 −0
1 +
import * as React from "react"
2 +
import { Slot } from "@radix-ui/react-slot"
3 +
import { ChevronRight, MoreHorizontal } from "lucide-react"
4 +
5 +
import { cn } from "@/lib/utils"
6 +
7 +
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
8 +
  return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
9 +
}
10 +
11 +
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
12 +
  return (
13 +
    <ol
14 +
      data-slot="breadcrumb-list"
15 +
      className={cn(
16 +
        "text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
17 +
        className
18 +
      )}
19 +
      {...props}
20 +
    />
21 +
  )
22 +
}
23 +
24 +
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
25 +
  return (
26 +
    <li
27 +
      data-slot="breadcrumb-item"
28 +
      className={cn("inline-flex items-center gap-1.5", className)}
29 +
      {...props}
30 +
    />
31 +
  )
32 +
}
33 +
34 +
function BreadcrumbLink({
35 +
  asChild,
36 +
  className,
37 +
  ...props
38 +
}: React.ComponentProps<"a"> & {
39 +
  asChild?: boolean
40 +
}) {
41 +
  const Comp = asChild ? Slot : "a"
42 +
43 +
  return (
44 +
    <Comp
45 +
      data-slot="breadcrumb-link"
46 +
      className={cn("hover:text-foreground transition-colors", className)}
47 +
      {...props}
48 +
    />
49 +
  )
50 +
}
51 +
52 +
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
53 +
  return (
54 +
    <span
55 +
      data-slot="breadcrumb-page"
56 +
      role="link"
57 +
      aria-disabled="true"
58 +
      aria-current="page"
59 +
      className={cn("text-foreground font-normal", className)}
60 +
      {...props}
61 +
    />
62 +
  )
63 +
}
64 +
65 +
function BreadcrumbSeparator({
66 +
  children,
67 +
  className,
68 +
  ...props
69 +
}: React.ComponentProps<"li">) {
70 +
  return (
71 +
    <li
72 +
      data-slot="breadcrumb-separator"
73 +
      role="presentation"
74 +
      aria-hidden="true"
75 +
      className={cn("[&>svg]:size-3.5", className)}
76 +
      {...props}
77 +
    >
78 +
      {children ?? <ChevronRight />}
79 +
    </li>
80 +
  )
81 +
}
82 +
83 +
function BreadcrumbEllipsis({
84 +
  className,
85 +
  ...props
86 +
}: React.ComponentProps<"span">) {
87 +
  return (
88 +
    <span
89 +
      data-slot="breadcrumb-ellipsis"
90 +
      role="presentation"
91 +
      aria-hidden="true"
92 +
      className={cn("flex size-9 items-center justify-center", className)}
93 +
      {...props}
94 +
    >
95 +
      <MoreHorizontal className="size-4" />
96 +
      <span className="sr-only">More</span>
97 +
    </span>
98 +
  )
99 +
}
100 +
101 +
export {
102 +
  Breadcrumb,
103 +
  BreadcrumbList,
104 +
  BreadcrumbItem,
105 +
  BreadcrumbLink,
106 +
  BreadcrumbPage,
107 +
  BreadcrumbSeparator,
108 +
  BreadcrumbEllipsis,
109 +
}
src/components/ui/collapsible.tsx (added) +33 −0
1 +
"use client"
2 +
3 +
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4 +
5 +
function Collapsible({
6 +
  ...props
7 +
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
8 +
  return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
9 +
}
10 +
11 +
function CollapsibleTrigger({
12 +
  ...props
13 +
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
14 +
  return (
15 +
    <CollapsiblePrimitive.CollapsibleTrigger
16 +
      data-slot="collapsible-trigger"
17 +
      {...props}
18 +
    />
19 +
  )
20 +
}
21 +
22 +
function CollapsibleContent({
23 +
  ...props
24 +
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
25 +
  return (
26 +
    <CollapsiblePrimitive.CollapsibleContent
27 +
      data-slot="collapsible-content"
28 +
      {...props}
29 +
    />
30 +
  )
31 +
}
32 +
33 +
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
src/components/ui/dropdown-menu.tsx (added) +255 −0
1 +
import * as React from "react"
2 +
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
3 +
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
4 +
5 +
import { cn } from "@/lib/utils"
6 +
7 +
function DropdownMenu({
8 +
  ...props
9 +
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
10 +
  return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
11 +
}
12 +
13 +
function DropdownMenuPortal({
14 +
  ...props
15 +
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
16 +
  return (
17 +
    <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
18 +
  )
19 +
}
20 +
21 +
function DropdownMenuTrigger({
22 +
  ...props
23 +
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
24 +
  return (
25 +
    <DropdownMenuPrimitive.Trigger
26 +
      data-slot="dropdown-menu-trigger"
27 +
      {...props}
28 +
    />
29 +
  )
30 +
}
31 +
32 +
function DropdownMenuContent({
33 +
  className,
34 +
  sideOffset = 4,
35 +
  ...props
36 +
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
37 +
  return (
38 +
    <DropdownMenuPrimitive.Portal>
39 +
      <DropdownMenuPrimitive.Content
40 +
        data-slot="dropdown-menu-content"
41 +
        sideOffset={sideOffset}
42 +
        className={cn(
43 +
          "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
44 +
          className
45 +
        )}
46 +
        {...props}
47 +
      />
48 +
    </DropdownMenuPrimitive.Portal>
49 +
  )
50 +
}
51 +
52 +
function DropdownMenuGroup({
53 +
  ...props
54 +
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
55 +
  return (
56 +
    <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
57 +
  )
58 +
}
59 +
60 +
function DropdownMenuItem({
61 +
  className,
62 +
  inset,
63 +
  variant = "default",
64 +
  ...props
65 +
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
66 +
  inset?: boolean
67 +
  variant?: "default" | "destructive"
68 +
}) {
69 +
  return (
70 +
    <DropdownMenuPrimitive.Item
71 +
      data-slot="dropdown-menu-item"
72 +
      data-inset={inset}
73 +
      data-variant={variant}
74 +
      className={cn(
75 +
        "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
76 +
        className
77 +
      )}
78 +
      {...props}
79 +
    />
80 +
  )
81 +
}
82 +
83 +
function DropdownMenuCheckboxItem({
84 +
  className,
85 +
  children,
86 +
  checked,
87 +
  ...props
88 +
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
89 +
  return (
90 +
    <DropdownMenuPrimitive.CheckboxItem
91 +
      data-slot="dropdown-menu-checkbox-item"
92 +
      className={cn(
93 +
        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
94 +
        className
95 +
      )}
96 +
      checked={checked}
97 +
      {...props}
98 +
    >
99 +
      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
100 +
        <DropdownMenuPrimitive.ItemIndicator>
101 +
          <CheckIcon className="size-4" />
102 +
        </DropdownMenuPrimitive.ItemIndicator>
103 +
      </span>
104 +
      {children}
105 +
    </DropdownMenuPrimitive.CheckboxItem>
106 +
  )
107 +
}
108 +
109 +
function DropdownMenuRadioGroup({
110 +
  ...props
111 +
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
112 +
  return (
113 +
    <DropdownMenuPrimitive.RadioGroup
114 +
      data-slot="dropdown-menu-radio-group"
115 +
      {...props}
116 +
    />
117 +
  )
118 +
}
119 +
120 +
function DropdownMenuRadioItem({
121 +
  className,
122 +
  children,
123 +
  ...props
124 +
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
125 +
  return (
126 +
    <DropdownMenuPrimitive.RadioItem
127 +
      data-slot="dropdown-menu-radio-item"
128 +
      className={cn(
129 +
        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
130 +
        className
131 +
      )}
132 +
      {...props}
133 +
    >
134 +
      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
135 +
        <DropdownMenuPrimitive.ItemIndicator>
136 +
          <CircleIcon className="size-2 fill-current" />
137 +
        </DropdownMenuPrimitive.ItemIndicator>
138 +
      </span>
139 +
      {children}
140 +
    </DropdownMenuPrimitive.RadioItem>
141 +
  )
142 +
}
143 +
144 +
function DropdownMenuLabel({
145 +
  className,
146 +
  inset,
147 +
  ...props
148 +
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
149 +
  inset?: boolean
150 +
}) {
151 +
  return (
152 +
    <DropdownMenuPrimitive.Label
153 +
      data-slot="dropdown-menu-label"
154 +
      data-inset={inset}
155 +
      className={cn(
156 +
        "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
157 +
        className
158 +
      )}
159 +
      {...props}
160 +
    />
161 +
  )
162 +
}
163 +
164 +
function DropdownMenuSeparator({
165 +
  className,
166 +
  ...props
167 +
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
168 +
  return (
169 +
    <DropdownMenuPrimitive.Separator
170 +
      data-slot="dropdown-menu-separator"
171 +
      className={cn("bg-border -mx-1 my-1 h-px", className)}
172 +
      {...props}
173 +
    />
174 +
  )
175 +
}
176 +
177 +
function DropdownMenuShortcut({
178 +
  className,
179 +
  ...props
180 +
}: React.ComponentProps<"span">) {
181 +
  return (
182 +
    <span
183 +
      data-slot="dropdown-menu-shortcut"
184 +
      className={cn(
185 +
        "text-muted-foreground ml-auto text-xs tracking-widest",
186 +
        className
187 +
      )}
188 +
      {...props}
189 +
    />
190 +
  )
191 +
}
192 +
193 +
function DropdownMenuSub({
194 +
  ...props
195 +
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
196 +
  return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
197 +
}
198 +
199 +
function DropdownMenuSubTrigger({
200 +
  className,
201 +
  inset,
202 +
  children,
203 +
  ...props
204 +
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
205 +
  inset?: boolean
206 +
}) {
207 +
  return (
208 +
    <DropdownMenuPrimitive.SubTrigger
209 +
      data-slot="dropdown-menu-sub-trigger"
210 +
      data-inset={inset}
211 +
      className={cn(
212 +
        "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
213 +
        className
214 +
      )}
215 +
      {...props}
216 +
    >
217 +
      {children}
218 +
      <ChevronRightIcon className="ml-auto size-4" />
219 +
    </DropdownMenuPrimitive.SubTrigger>
220 +
  )
221 +
}
222 +
223 +
function DropdownMenuSubContent({
224 +
  className,
225 +
  ...props
226 +
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
227 +
  return (
228 +
    <DropdownMenuPrimitive.SubContent
229 +
      data-slot="dropdown-menu-sub-content"
230 +
      className={cn(
231 +
        "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
232 +
        className
233 +
      )}
234 +
      {...props}
235 +
    />
236 +
  )
237 +
}
238 +
239 +
export {
240 +
  DropdownMenu,
241 +
  DropdownMenuPortal,
242 +
  DropdownMenuTrigger,
243 +
  DropdownMenuContent,
244 +
  DropdownMenuGroup,
245 +
  DropdownMenuLabel,
246 +
  DropdownMenuItem,
247 +
  DropdownMenuCheckboxItem,
248 +
  DropdownMenuRadioGroup,
249 +
  DropdownMenuRadioItem,
250 +
  DropdownMenuSeparator,
251 +
  DropdownMenuShortcut,
252 +
  DropdownMenuSub,
253 +
  DropdownMenuSubTrigger,
254 +
  DropdownMenuSubContent,
255 +
}
src/components/ui/input.tsx (added) +21 −0
1 +
import * as React from "react"
2 +
3 +
import { cn } from "@/lib/utils"
4 +
5 +
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6 +
  return (
7 +
    <input
8 +
      type={type}
9 +
      data-slot="input"
10 +
      className={cn(
11 +
        "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12 +
        "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
13 +
        "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
14 +
        className
15 +
      )}
16 +
      {...props}
17 +
    />
18 +
  )
19 +
}
20 +
21 +
export { Input }
src/components/ui/label.tsx (added) +24 −0
1 +
"use client"
2 +
3 +
import * as React from "react"
4 +
import * as LabelPrimitive from "@radix-ui/react-label"
5 +
6 +
import { cn } from "@/lib/utils"
7 +
8 +
function Label({
9 +
  className,
10 +
  ...props
11 +
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
12 +
  return (
13 +
    <LabelPrimitive.Root
14 +
      data-slot="label"
15 +
      className={cn(
16 +
        "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
17 +
        className
18 +
      )}
19 +
      {...props}
20 +
    />
21 +
  )
22 +
}
23 +
24 +
export { Label }
src/components/ui/popover.tsx (added) +48 −0
1 +
"use client"
2 +
3 +
import * as React from "react"
4 +
import * as PopoverPrimitive from "@radix-ui/react-popover"
5 +
6 +
import { cn } from "@/lib/utils"
7 +
8 +
function Popover({
9 +
  ...props
10 +
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
11 +
  return <PopoverPrimitive.Root data-slot="popover" {...props} />
12 +
}
13 +
14 +
function PopoverTrigger({
15 +
  ...props
16 +
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
17 +
  return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
18 +
}
19 +
20 +
function PopoverContent({
21 +
  className,
22 +
  align = "center",
23 +
  sideOffset = 4,
24 +
  ...props
25 +
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
26 +
  return (
27 +
    <PopoverPrimitive.Portal>
28 +
      <PopoverPrimitive.Content
29 +
        data-slot="popover-content"
30 +
        align={align}
31 +
        sideOffset={sideOffset}
32 +
        className={cn(
33 +
          "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
34 +
          className
35 +
        )}
36 +
        {...props}
37 +
      />
38 +
    </PopoverPrimitive.Portal>
39 +
  )
40 +
}
41 +
42 +
function PopoverAnchor({
43 +
  ...props
44 +
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
45 +
  return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
46 +
}
47 +
48 +
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
src/components/ui/separator.tsx (added) +26 −0
1 +
import * as React from "react"
2 +
import * as SeparatorPrimitive from "@radix-ui/react-separator"
3 +
4 +
import { cn } from "@/lib/utils"
5 +
6 +
function Separator({
7 +
  className,
8 +
  orientation = "horizontal",
9 +
  decorative = true,
10 +
  ...props
11 +
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
12 +
  return (
13 +
    <SeparatorPrimitive.Root
14 +
      data-slot="separator"
15 +
      decorative={decorative}
16 +
      orientation={orientation}
17 +
      className={cn(
18 +
        "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
19 +
        className
20 +
      )}
21 +
      {...props}
22 +
    />
23 +
  )
24 +
}
25 +
26 +
export { Separator }
src/components/ui/sheet.tsx (added) +139 −0
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 +
}
src/components/ui/sidebar.tsx (added) +724 −0
1 +
import * as React from "react"
2 +
import { Slot } from "@radix-ui/react-slot"
3 +
import { cva, type VariantProps } from "class-variance-authority"
4 +
import { PanelLeftIcon } from "lucide-react"
5 +
6 +
import { useIsMobile } from "@/hooks/use-mobile"
7 +
import { cn } from "@/lib/utils"
8 +
import { Button } from "@/components/ui/button"
9 +
import { Input } from "@/components/ui/input"
10 +
import { Separator } from "@/components/ui/separator"
11 +
import {
12 +
  Sheet,
13 +
  SheetContent,
14 +
  SheetDescription,
15 +
  SheetHeader,
16 +
  SheetTitle,
17 +
} from "@/components/ui/sheet"
18 +
import { Skeleton } from "@/components/ui/skeleton"
19 +
import {
20 +
  Tooltip,
21 +
  TooltipContent,
22 +
  TooltipProvider,
23 +
  TooltipTrigger,
24 +
} from "@/components/ui/tooltip"
25 +
26 +
const SIDEBAR_COOKIE_NAME = "sidebar_state"
27 +
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
28 +
const SIDEBAR_WIDTH = "16rem"
29 +
const SIDEBAR_WIDTH_MOBILE = "18rem"
30 +
const SIDEBAR_WIDTH_ICON = "3rem"
31 +
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
32 +
33 +
type SidebarContextProps = {
34 +
  state: "expanded" | "collapsed"
35 +
  open: boolean
36 +
  setOpen: (open: boolean) => void
37 +
  openMobile: boolean
38 +
  setOpenMobile: (open: boolean) => void
39 +
  isMobile: boolean
40 +
  toggleSidebar: () => void
41 +
}
42 +
43 +
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
44 +
45 +
function useSidebar() {
46 +
  const context = React.useContext(SidebarContext)
47 +
  if (!context) {
48 +
    throw new Error("useSidebar must be used within a SidebarProvider.")
49 +
  }
50 +
51 +
  return context
52 +
}
53 +
54 +
function SidebarProvider({
55 +
  defaultOpen = true,
56 +
  open: openProp,
57 +
  onOpenChange: setOpenProp,
58 +
  className,
59 +
  style,
60 +
  children,
61 +
  ...props
62 +
}: React.ComponentProps<"div"> & {
63 +
  defaultOpen?: boolean
64 +
  open?: boolean
65 +
  onOpenChange?: (open: boolean) => void
66 +
}) {
67 +
  const isMobile = useIsMobile()
68 +
  const [openMobile, setOpenMobile] = React.useState(false)
69 +
70 +
  // This is the internal state of the sidebar.
71 +
  // We use openProp and setOpenProp for control from outside the component.
72 +
  const [_open, _setOpen] = React.useState(defaultOpen)
73 +
  const open = openProp ?? _open
74 +
  const setOpen = React.useCallback(
75 +
    (value: boolean | ((value: boolean) => boolean)) => {
76 +
      const openState = typeof value === "function" ? value(open) : value
77 +
      if (setOpenProp) {
78 +
        setOpenProp(openState)
79 +
      } else {
80 +
        _setOpen(openState)
81 +
      }
82 +
83 +
      // This sets the cookie to keep the sidebar state.
84 +
      document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
85 +
    },
86 +
    [setOpenProp, open]
87 +
  )
88 +
89 +
  // Helper to toggle the sidebar.
90 +
  const toggleSidebar = React.useCallback(() => {
91 +
    return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
92 +
  }, [isMobile, setOpen, setOpenMobile])
93 +
94 +
  // Adds a keyboard shortcut to toggle the sidebar.
95 +
  React.useEffect(() => {
96 +
    const handleKeyDown = (event: KeyboardEvent) => {
97 +
      if (
98 +
        event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
99 +
        (event.metaKey || event.ctrlKey)
100 +
      ) {
101 +
        event.preventDefault()
102 +
        toggleSidebar()
103 +
      }
104 +
    }
105 +
106 +
    window.addEventListener("keydown", handleKeyDown)
107 +
    return () => window.removeEventListener("keydown", handleKeyDown)
108 +
  }, [toggleSidebar])
109 +
110 +
  // We add a state so that we can do data-state="expanded" or "collapsed".
111 +
  // This makes it easier to style the sidebar with Tailwind classes.
112 +
  const state = open ? "expanded" : "collapsed"
113 +
114 +
  const contextValue = React.useMemo<SidebarContextProps>(
115 +
    () => ({
116 +
      state,
117 +
      open,
118 +
      setOpen,
119 +
      isMobile,
120 +
      openMobile,
121 +
      setOpenMobile,
122 +
      toggleSidebar,
123 +
    }),
124 +
    [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
125 +
  )
126 +
127 +
  return (
128 +
    <SidebarContext.Provider value={contextValue}>
129 +
      <TooltipProvider delayDuration={0}>
130 +
        <div
131 +
          data-slot="sidebar-wrapper"
132 +
          style={
133 +
            {
134 +
              "--sidebar-width": SIDEBAR_WIDTH,
135 +
              "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
136 +
              ...style,
137 +
            } as React.CSSProperties
138 +
          }
139 +
          className={cn(
140 +
            "group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
141 +
            className
142 +
          )}
143 +
          {...props}
144 +
        >
145 +
          {children}
146 +
        </div>
147 +
      </TooltipProvider>
148 +
    </SidebarContext.Provider>
149 +
  )
150 +
}
151 +
152 +
function Sidebar({
153 +
  side = "left",
154 +
  variant = "sidebar",
155 +
  collapsible = "offcanvas",
156 +
  className,
157 +
  children,
158 +
  ...props
159 +
}: React.ComponentProps<"div"> & {
160 +
  side?: "left" | "right"
161 +
  variant?: "sidebar" | "floating" | "inset"
162 +
  collapsible?: "offcanvas" | "icon" | "none"
163 +
}) {
164 +
  const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
165 +
166 +
  if (collapsible === "none") {
167 +
    return (
168 +
      <div
169 +
        data-slot="sidebar"
170 +
        className={cn(
171 +
          "bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
172 +
          className
173 +
        )}
174 +
        {...props}
175 +
      >
176 +
        {children}
177 +
      </div>
178 +
    )
179 +
  }
180 +
181 +
  if (isMobile) {
182 +
    return (
183 +
      <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
184 +
        <SheetContent
185 +
          data-sidebar="sidebar"
186 +
          data-slot="sidebar"
187 +
          data-mobile="true"
188 +
          className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
189 +
          style={
190 +
            {
191 +
              "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
192 +
            } as React.CSSProperties
193 +
          }
194 +
          side={side}
195 +
        >
196 +
          <SheetHeader className="sr-only">
197 +
            <SheetTitle>Sidebar</SheetTitle>
198 +
            <SheetDescription>Displays the mobile sidebar.</SheetDescription>
199 +
          </SheetHeader>
200 +
          <div className="flex h-full w-full flex-col">{children}</div>
201 +
        </SheetContent>
202 +
      </Sheet>
203 +
    )
204 +
  }
205 +
206 +
  return (
207 +
    <div
208 +
      className="group peer text-sidebar-foreground hidden md:block"
209 +
      data-state={state}
210 +
      data-collapsible={state === "collapsed" ? collapsible : ""}
211 +
      data-variant={variant}
212 +
      data-side={side}
213 +
      data-slot="sidebar"
214 +
    >
215 +
      {/* This is what handles the sidebar gap on desktop */}
216 +
      <div
217 +
        data-slot="sidebar-gap"
218 +
        className={cn(
219 +
          "relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
220 +
          "group-data-[collapsible=offcanvas]:w-0",
221 +
          "group-data-[side=right]:rotate-180",
222 +
          variant === "floating" || variant === "inset"
223 +
            ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
224 +
            : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
225 +
        )}
226 +
      />
227 +
      <div
228 +
        data-slot="sidebar-container"
229 +
        className={cn(
230 +
          "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
231 +
          side === "left"
232 +
            ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
233 +
            : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
234 +
          // Adjust the padding for floating and inset variants.
235 +
          variant === "floating" || variant === "inset"
236 +
            ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
237 +
            : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
238 +
          className
239 +
        )}
240 +
        {...props}
241 +
      >
242 +
        <div
243 +
          data-sidebar="sidebar"
244 +
          data-slot="sidebar-inner"
245 +
          className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
246 +
        >
247 +
          {children}
248 +
        </div>
249 +
      </div>
250 +
    </div>
251 +
  )
252 +
}
253 +
254 +
function SidebarTrigger({
255 +
  className,
256 +
  onClick,
257 +
  ...props
258 +
}: React.ComponentProps<typeof Button>) {
259 +
  const { toggleSidebar } = useSidebar()
260 +
261 +
  return (
262 +
    <Button
263 +
      data-sidebar="trigger"
264 +
      data-slot="sidebar-trigger"
265 +
      variant="ghost"
266 +
      size="icon"
267 +
      className={cn("size-7", className)}
268 +
      onClick={(event) => {
269 +
        onClick?.(event)
270 +
        toggleSidebar()
271 +
      }}
272 +
      {...props}
273 +
    >
274 +
      <PanelLeftIcon />
275 +
      <span className="sr-only">Toggle Sidebar</span>
276 +
    </Button>
277 +
  )
278 +
}
279 +
280 +
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
281 +
  const { toggleSidebar } = useSidebar()
282 +
283 +
  return (
284 +
    <button
285 +
      data-sidebar="rail"
286 +
      data-slot="sidebar-rail"
287 +
      aria-label="Toggle Sidebar"
288 +
      tabIndex={-1}
289 +
      onClick={toggleSidebar}
290 +
      title="Toggle Sidebar"
291 +
      className={cn(
292 +
        "hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
293 +
        "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
294 +
        "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
295 +
        "hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
296 +
        "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
297 +
        "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
298 +
        className
299 +
      )}
300 +
      {...props}
301 +
    />
302 +
  )
303 +
}
304 +
305 +
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
306 +
  return (
307 +
    <main
308 +
      data-slot="sidebar-inset"
309 +
      className={cn(
310 +
        "bg-background relative flex w-full flex-1 flex-col",
311 +
        "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
312 +
        className
313 +
      )}
314 +
      {...props}
315 +
    />
316 +
  )
317 +
}
318 +
319 +
function SidebarInput({
320 +
  className,
321 +
  ...props
322 +
}: React.ComponentProps<typeof Input>) {
323 +
  return (
324 +
    <Input
325 +
      data-slot="sidebar-input"
326 +
      data-sidebar="input"
327 +
      className={cn("bg-background h-8 w-full shadow-none", className)}
328 +
      {...props}
329 +
    />
330 +
  )
331 +
}
332 +
333 +
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
334 +
  return (
335 +
    <div
336 +
      data-slot="sidebar-header"
337 +
      data-sidebar="header"
338 +
      className={cn("flex flex-col gap-2 p-2", className)}
339 +
      {...props}
340 +
    />
341 +
  )
342 +
}
343 +
344 +
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
345 +
  return (
346 +
    <div
347 +
      data-slot="sidebar-footer"
348 +
      data-sidebar="footer"
349 +
      className={cn("flex flex-col gap-2 p-2", className)}
350 +
      {...props}
351 +
    />
352 +
  )
353 +
}
354 +
355 +
function SidebarSeparator({
356 +
  className,
357 +
  ...props
358 +
}: React.ComponentProps<typeof Separator>) {
359 +
  return (
360 +
    <Separator
361 +
      data-slot="sidebar-separator"
362 +
      data-sidebar="separator"
363 +
      className={cn("bg-sidebar-border mx-2 w-auto", className)}
364 +
      {...props}
365 +
    />
366 +
  )
367 +
}
368 +
369 +
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
370 +
  return (
371 +
    <div
372 +
      data-slot="sidebar-content"
373 +
      data-sidebar="content"
374 +
      className={cn(
375 +
        "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
376 +
        className
377 +
      )}
378 +
      {...props}
379 +
    />
380 +
  )
381 +
}
382 +
383 +
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
384 +
  return (
385 +
    <div
386 +
      data-slot="sidebar-group"
387 +
      data-sidebar="group"
388 +
      className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
389 +
      {...props}
390 +
    />
391 +
  )
392 +
}
393 +
394 +
function SidebarGroupLabel({
395 +
  className,
396 +
  asChild = false,
397 +
  ...props
398 +
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
399 +
  const Comp = asChild ? Slot : "div"
400 +
401 +
  return (
402 +
    <Comp
403 +
      data-slot="sidebar-group-label"
404 +
      data-sidebar="group-label"
405 +
      className={cn(
406 +
        "text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
407 +
        "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
408 +
        className
409 +
      )}
410 +
      {...props}
411 +
    />
412 +
  )
413 +
}
414 +
415 +
function SidebarGroupAction({
416 +
  className,
417 +
  asChild = false,
418 +
  ...props
419 +
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
420 +
  const Comp = asChild ? Slot : "button"
421 +
422 +
  return (
423 +
    <Comp
424 +
      data-slot="sidebar-group-action"
425 +
      data-sidebar="group-action"
426 +
      className={cn(
427 +
        "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
428 +
        // Increases the hit area of the button on mobile.
429 +
        "after:absolute after:-inset-2 md:after:hidden",
430 +
        "group-data-[collapsible=icon]:hidden",
431 +
        className
432 +
      )}
433 +
      {...props}
434 +
    />
435 +
  )
436 +
}
437 +
438 +
function SidebarGroupContent({
439 +
  className,
440 +
  ...props
441 +
}: React.ComponentProps<"div">) {
442 +
  return (
443 +
    <div
444 +
      data-slot="sidebar-group-content"
445 +
      data-sidebar="group-content"
446 +
      className={cn("w-full text-sm", className)}
447 +
      {...props}
448 +
    />
449 +
  )
450 +
}
451 +
452 +
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
453 +
  return (
454 +
    <ul
455 +
      data-slot="sidebar-menu"
456 +
      data-sidebar="menu"
457 +
      className={cn("flex w-full min-w-0 flex-col gap-1", className)}
458 +
      {...props}
459 +
    />
460 +
  )
461 +
}
462 +
463 +
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
464 +
  return (
465 +
    <li
466 +
      data-slot="sidebar-menu-item"
467 +
      data-sidebar="menu-item"
468 +
      className={cn("group/menu-item relative", className)}
469 +
      {...props}
470 +
    />
471 +
  )
472 +
}
473 +
474 +
const sidebarMenuButtonVariants = cva(
475 +
  "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden 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",
476 +
  {
477 +
    variants: {
478 +
      variant: {
479 +
        default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
480 +
        outline:
481 +
          "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))]",
482 +
      },
483 +
      size: {
484 +
        default: "h-8 text-sm",
485 +
        sm: "h-7 text-xs",
486 +
        lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
487 +
      },
488 +
    },
489 +
    defaultVariants: {
490 +
      variant: "default",
491 +
      size: "default",
492 +
    },
493 +
  }
494 +
)
495 +
496 +
function SidebarMenuButton({
497 +
  asChild = false,
498 +
  isActive = false,
499 +
  variant = "default",
500 +
  size = "default",
501 +
  tooltip,
502 +
  className,
503 +
  ...props
504 +
}: React.ComponentProps<"button"> & {
505 +
  asChild?: boolean
506 +
  isActive?: boolean
507 +
  tooltip?: string | React.ComponentProps<typeof TooltipContent>
508 +
} & VariantProps<typeof sidebarMenuButtonVariants>) {
509 +
  const Comp = asChild ? Slot : "button"
510 +
  const { isMobile, state } = useSidebar()
511 +
512 +
  const button = (
513 +
    <Comp
514 +
      data-slot="sidebar-menu-button"
515 +
      data-sidebar="menu-button"
516 +
      data-size={size}
517 +
      data-active={isActive}
518 +
      className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
519 +
      {...props}
520 +
    />
521 +
  )
522 +
523 +
  if (!tooltip) {
524 +
    return button
525 +
  }
526 +
527 +
  if (typeof tooltip === "string") {
528 +
    tooltip = {
529 +
      children: tooltip,
530 +
    }
531 +
  }
532 +
533 +
  return (
534 +
    <Tooltip>
535 +
      <TooltipTrigger asChild>{button}</TooltipTrigger>
536 +
      <TooltipContent
537 +
        side="right"
538 +
        align="center"
539 +
        hidden={state !== "collapsed" || isMobile}
540 +
        {...tooltip}
541 +
      />
542 +
    </Tooltip>
543 +
  )
544 +
}
545 +
546 +
function SidebarMenuAction({
547 +
  className,
548 +
  asChild = false,
549 +
  showOnHover = false,
550 +
  ...props
551 +
}: React.ComponentProps<"button"> & {
552 +
  asChild?: boolean
553 +
  showOnHover?: boolean
554 +
}) {
555 +
  const Comp = asChild ? Slot : "button"
556 +
557 +
  return (
558 +
    <Comp
559 +
      data-slot="sidebar-menu-action"
560 +
      data-sidebar="menu-action"
561 +
      className={cn(
562 +
        "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
563 +
        // Increases the hit area of the button on mobile.
564 +
        "after:absolute after:-inset-2 md:after:hidden",
565 +
        "peer-data-[size=sm]/menu-button:top-1",
566 +
        "peer-data-[size=default]/menu-button:top-1.5",
567 +
        "peer-data-[size=lg]/menu-button:top-2.5",
568 +
        "group-data-[collapsible=icon]:hidden",
569 +
        showOnHover &&
570 +
          "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
571 +
        className
572 +
      )}
573 +
      {...props}
574 +
    />
575 +
  )
576 +
}
577 +
578 +
function SidebarMenuBadge({
579 +
  className,
580 +
  ...props
581 +
}: React.ComponentProps<"div">) {
582 +
  return (
583 +
    <div
584 +
      data-slot="sidebar-menu-badge"
585 +
      data-sidebar="menu-badge"
586 +
      className={cn(
587 +
        "text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
588 +
        "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
589 +
        "peer-data-[size=sm]/menu-button:top-1",
590 +
        "peer-data-[size=default]/menu-button:top-1.5",
591 +
        "peer-data-[size=lg]/menu-button:top-2.5",
592 +
        "group-data-[collapsible=icon]:hidden",
593 +
        className
594 +
      )}
595 +
      {...props}
596 +
    />
597 +
  )
598 +
}
599 +
600 +
function SidebarMenuSkeleton({
601 +
  className,
602 +
  showIcon = false,
603 +
  ...props
604 +
}: React.ComponentProps<"div"> & {
605 +
  showIcon?: boolean
606 +
}) {
607 +
  // Random width between 50 to 90%.
608 +
  const width = React.useMemo(() => {
609 +
    return `${Math.floor(Math.random() * 40) + 50}%`
610 +
  }, [])
611 +
612 +
  return (
613 +
    <div
614 +
      data-slot="sidebar-menu-skeleton"
615 +
      data-sidebar="menu-skeleton"
616 +
      className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
617 +
      {...props}
618 +
    >
619 +
      {showIcon && (
620 +
        <Skeleton
621 +
          className="size-4 rounded-md"
622 +
          data-sidebar="menu-skeleton-icon"
623 +
        />
624 +
      )}
625 +
      <Skeleton
626 +
        className="h-4 max-w-(--skeleton-width) flex-1"
627 +
        data-sidebar="menu-skeleton-text"
628 +
        style={
629 +
          {
630 +
            "--skeleton-width": width,
631 +
          } as React.CSSProperties
632 +
        }
633 +
      />
634 +
    </div>
635 +
  )
636 +
}
637 +
638 +
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
639 +
  return (
640 +
    <ul
641 +
      data-slot="sidebar-menu-sub"
642 +
      data-sidebar="menu-sub"
643 +
      className={cn(
644 +
        "border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
645 +
        "group-data-[collapsible=icon]:hidden",
646 +
        className
647 +
      )}
648 +
      {...props}
649 +
    />
650 +
  )
651 +
}
652 +
653 +
function SidebarMenuSubItem({
654 +
  className,
655 +
  ...props
656 +
}: React.ComponentProps<"li">) {
657 +
  return (
658 +
    <li
659 +
      data-slot="sidebar-menu-sub-item"
660 +
      data-sidebar="menu-sub-item"
661 +
      className={cn("group/menu-sub-item relative", className)}
662 +
      {...props}
663 +
    />
664 +
  )
665 +
}
666 +
667 +
function SidebarMenuSubButton({
668 +
  asChild = false,
669 +
  size = "md",
670 +
  isActive = false,
671 +
  className,
672 +
  ...props
673 +
}: React.ComponentProps<"a"> & {
674 +
  asChild?: boolean
675 +
  size?: "sm" | "md"
676 +
  isActive?: boolean
677 +
}) {
678 +
  const Comp = asChild ? Slot : "a"
679 +
680 +
  return (
681 +
    <Comp
682 +
      data-slot="sidebar-menu-sub-button"
683 +
      data-sidebar="menu-sub-button"
684 +
      data-size={size}
685 +
      data-active={isActive}
686 +
      className={cn(
687 +
        "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 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",
688 +
        "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
689 +
        size === "sm" && "text-xs",
690 +
        size === "md" && "text-sm",
691 +
        "group-data-[collapsible=icon]:hidden",
692 +
        className
693 +
      )}
694 +
      {...props}
695 +
    />
696 +
  )
697 +
}
698 +
699 +
export {
700 +
  Sidebar,
701 +
  SidebarContent,
702 +
  SidebarFooter,
703 +
  SidebarGroup,
704 +
  SidebarGroupAction,
705 +
  SidebarGroupContent,
706 +
  SidebarGroupLabel,
707 +
  SidebarHeader,
708 +
  SidebarInput,
709 +
  SidebarInset,
710 +
  SidebarMenu,
711 +
  SidebarMenuAction,
712 +
  SidebarMenuBadge,
713 +
  SidebarMenuButton,
714 +
  SidebarMenuItem,
715 +
  SidebarMenuSkeleton,
716 +
  SidebarMenuSub,
717 +
  SidebarMenuSubButton,
718 +
  SidebarMenuSubItem,
719 +
  SidebarProvider,
720 +
  SidebarRail,
721 +
  SidebarSeparator,
722 +
  SidebarTrigger,
723 +
  useSidebar,
724 +
}
src/components/ui/skeleton.tsx (added) +13 −0
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 }
src/components/ui/switch.tsx (added) +29 −0
1 +
import * as React from "react"
2 +
import * as SwitchPrimitive from "@radix-ui/react-switch"
3 +
4 +
import { cn } from "@/lib/utils"
5 +
6 +
function Switch({
7 +
  className,
8 +
  ...props
9 +
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
10 +
  return (
11 +
    <SwitchPrimitive.Root
12 +
      data-slot="switch"
13 +
      className={cn(
14 +
        "peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
15 +
        className
16 +
      )}
17 +
      {...props}
18 +
    >
19 +
      <SwitchPrimitive.Thumb
20 +
        data-slot="switch-thumb"
21 +
        className={cn(
22 +
          "bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
23 +
        )}
24 +
      />
25 +
    </SwitchPrimitive.Root>
26 +
  )
27 +
}
28 +
29 +
export { Switch }
src/components/ui/tooltip.tsx (added) +59 −0
1 +
import * as React from "react"
2 +
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
3 +
4 +
import { cn } from "@/lib/utils"
5 +
6 +
function TooltipProvider({
7 +
  delayDuration = 0,
8 +
  ...props
9 +
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
10 +
  return (
11 +
    <TooltipPrimitive.Provider
12 +
      data-slot="tooltip-provider"
13 +
      delayDuration={delayDuration}
14 +
      {...props}
15 +
    />
16 +
  )
17 +
}
18 +
19 +
function Tooltip({
20 +
  ...props
21 +
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
22 +
  return (
23 +
    <TooltipProvider>
24 +
      <TooltipPrimitive.Root data-slot="tooltip" {...props} />
25 +
    </TooltipProvider>
26 +
  )
27 +
}
28 +
29 +
function TooltipTrigger({
30 +
  ...props
31 +
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
32 +
  return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
33 +
}
34 +
35 +
function TooltipContent({
36 +
  className,
37 +
  sideOffset = 0,
38 +
  children,
39 +
  ...props
40 +
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
41 +
  return (
42 +
    <TooltipPrimitive.Portal>
43 +
      <TooltipPrimitive.Content
44 +
        data-slot="tooltip-content"
45 +
        sideOffset={sideOffset}
46 +
        className={cn(
47 +
          "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",
48 +
          className
49 +
        )}
50 +
        {...props}
51 +
      >
52 +
        {children}
53 +
        <TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
54 +
      </TooltipPrimitive.Content>
55 +
    </TooltipPrimitive.Portal>
56 +
  )
57 +
}
58 +
59 +
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
src/hooks/use-mobile.ts (added) +19 −0
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 +
}