Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place

SPA/Domain Routes work

+85
bun.lock
··· 12 12 "@elysiajs/eden": "^1.4.3", 13 13 "@elysiajs/openapi": "^1.4.11", 14 14 "@elysiajs/static": "^1.4.2", 15 + "@radix-ui/react-dialog": "^1.1.15", 16 + "@radix-ui/react-label": "^2.1.7", 17 + "@radix-ui/react-radio-group": "^1.3.8", 18 + "@radix-ui/react-slot": "^1.2.3", 19 + "@radix-ui/react-tabs": "^1.1.13", 15 20 "@tanstack/react-query": "^5.90.2", 21 + "class-variance-authority": "^0.7.1", 16 22 "clsx": "^2.1.1", 17 23 "elysia": "latest", 18 24 "iron-session": "^8.0.4", 25 + "lucide-react": "^0.546.0", 19 26 "react": "^19.2.0", 20 27 "react-dom": "^19.2.0", 28 + "tailwind-merge": "^3.3.1", 21 29 "tailwindcss": "4", 30 + "tw-animate-css": "^1.4.0", 22 31 }, 23 32 "devDependencies": { 24 33 "@types/react": "^19.2.2", ··· 129 138 130 139 "@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/jVZ8eYjpYHLDFNoT86cP+AjuWvpkzFY+0R0a1bdeu0sQ6ILuy1FV6hz1hUAP390E09VCo5oP76fnx29giHTtA=="], 131 140 141 + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], 142 + 143 + "@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=="], 144 + 145 + "@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=="], 146 + 147 + "@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=="], 148 + 149 + "@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=="], 150 + 151 + "@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=="], 152 + 153 + "@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=="], 154 + 155 + "@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=="], 156 + 157 + "@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=="], 158 + 159 + "@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=="], 160 + 161 + "@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=="], 162 + 163 + "@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=="], 164 + 165 + "@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=="], 166 + 167 + "@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=="], 168 + 169 + "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.3.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-direction": "1.1.1", "@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-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-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ=="], 170 + 171 + "@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=="], 172 + 173 + "@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=="], 174 + 175 + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@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-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-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], 176 + 177 + "@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=="], 178 + 179 + "@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=="], 180 + 181 + "@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=="], 182 + 183 + "@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=="], 184 + 185 + "@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=="], 186 + 187 + "@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=="], 188 + 189 + "@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=="], 190 + 132 191 "@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="], 133 192 134 193 "@tanstack/query-core": ["@tanstack/query-core@5.90.2", "", {}, "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ=="], ··· 153 212 154 213 "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], 155 214 215 + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], 216 + 156 217 "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], 157 218 158 219 "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], ··· 189 250 190 251 "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], 191 252 253 + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], 254 + 192 255 "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], 193 256 194 257 "code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="], ··· 219 282 220 283 "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], 221 284 285 + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], 286 + 222 287 "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], 223 288 224 289 "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], ··· 264 329 "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], 265 330 266 331 "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], 332 + 333 + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], 267 334 268 335 "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], 269 336 ··· 297 364 298 365 "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], 299 366 367 + "lucide-react": ["lucide-react@0.546.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ=="], 368 + 300 369 "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], 301 370 302 371 "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], ··· 364 433 "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], 365 434 366 435 "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], 436 + 437 + "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=="], 438 + 439 + "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=="], 440 + 441 + "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=="], 367 442 368 443 "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], 369 444 ··· 403 478 404 479 "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], 405 480 481 + "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], 482 + 406 483 "tailwindcss": ["tailwindcss@4.1.14", "", {}, "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA=="], 407 484 408 485 "thread-stream": ["thread-stream@2.7.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw=="], ··· 417 494 418 495 "ts-morph": ["ts-morph@24.0.0", "", { "dependencies": { "@ts-morph/common": "~0.25.0", "code-block-writer": "^13.0.3" } }, "sha512-2OAOg/Ob5yx9Et7ZX4CvTCc0UFoZHwLEJ+dpDPSUi5TgwwlTlX47w+iFRrEwzUZwYACjq83cgjS/Da50Ga37uw=="], 419 496 497 + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 498 + 499 + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], 500 + 420 501 "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], 421 502 422 503 "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], ··· 430 511 "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], 431 512 432 513 "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], 514 + 515 + "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=="], 516 + 517 + "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=="], 433 518 434 519 "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], 435 520
+21
components.json
··· 1 + { 2 + "$schema": "https://ui.shadcn.com/schema.json", 3 + "style": "new-york", 4 + "rsc": false, 5 + "tsx": true, 6 + "tailwind": { 7 + "config": "", 8 + "css": "src/styles/globals.css", 9 + "baseColor": "neutral", 10 + "cssVariables": true, 11 + "prefix": "" 12 + }, 13 + "aliases": { 14 + "components": "@public/components", 15 + "utils": "@public/lib/utils", 16 + "ui": "@public/components/ui", 17 + "lib": "@public/libs", 18 + "hooks": "@public/hooks" 19 + }, 20 + "iconLibrary": "lucide" 21 + }
+10 -1
package.json
··· 15 15 "@elysiajs/eden": "^1.4.3", 16 16 "@elysiajs/openapi": "^1.4.11", 17 17 "@elysiajs/static": "^1.4.2", 18 + "@radix-ui/react-dialog": "^1.1.15", 19 + "@radix-ui/react-label": "^2.1.7", 20 + "@radix-ui/react-radio-group": "^1.3.8", 21 + "@radix-ui/react-slot": "^1.2.3", 22 + "@radix-ui/react-tabs": "^1.1.13", 18 23 "@tanstack/react-query": "^5.90.2", 24 + "class-variance-authority": "^0.7.1", 19 25 "clsx": "^2.1.1", 20 26 "elysia": "latest", 21 27 "iron-session": "^8.0.4", 28 + "lucide-react": "^0.546.0", 22 29 "react": "^19.2.0", 23 30 "react-dom": "^19.2.0", 24 - "tailwindcss": "4" 31 + "tailwind-merge": "^3.3.1", 32 + "tailwindcss": "4", 33 + "tw-animate-css": "^1.4.0" 25 34 }, 26 35 "devDependencies": { 27 36 "@types/react": "^19.2.2",
+46
public/components/ui/badge.tsx
··· 1 + import * as React from "react" 2 + import { Slot } from "@radix-ui/react-slot" 3 + import { cva, type VariantProps } from "class-variance-authority" 4 + 5 + import { cn } from "@public/lib/utils" 6 + 7 + const badgeVariants = cva( 8 + "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", 9 + { 10 + variants: { 11 + variant: { 12 + default: 13 + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", 14 + secondary: 15 + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", 16 + destructive: 17 + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 18 + outline: 19 + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", 20 + }, 21 + }, 22 + defaultVariants: { 23 + variant: "default", 24 + }, 25 + } 26 + ) 27 + 28 + function Badge({ 29 + className, 30 + variant, 31 + asChild = false, 32 + ...props 33 + }: React.ComponentProps<"span"> & 34 + VariantProps<typeof badgeVariants> & { asChild?: boolean }) { 35 + const Comp = asChild ? Slot : "span" 36 + 37 + return ( 38 + <Comp 39 + data-slot="badge" 40 + className={cn(badgeVariants({ variant }), className)} 41 + {...props} 42 + /> 43 + ) 44 + } 45 + 46 + export { Badge, badgeVariants }
+60
public/components/ui/button.tsx
··· 1 + import * as React from "react" 2 + import { Slot } from "@radix-ui/react-slot" 3 + import { cva, type VariantProps } from "class-variance-authority" 4 + 5 + import { cn } from "@public/lib/utils" 6 + 7 + const buttonVariants = cva( 8 + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 + { 10 + variants: { 11 + variant: { 12 + default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 + destructive: 14 + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 15 + outline: 16 + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 17 + secondary: 18 + "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 + ghost: 20 + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 21 + link: "text-primary underline-offset-4 hover:underline", 22 + }, 23 + size: { 24 + default: "h-9 px-4 py-2 has-[>svg]:px-3", 25 + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 26 + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 27 + icon: "size-9", 28 + "icon-sm": "size-8", 29 + "icon-lg": "size-10", 30 + }, 31 + }, 32 + defaultVariants: { 33 + variant: "default", 34 + size: "default", 35 + }, 36 + } 37 + ) 38 + 39 + function Button({ 40 + className, 41 + variant, 42 + size, 43 + asChild = false, 44 + ...props 45 + }: React.ComponentProps<"button"> & 46 + VariantProps<typeof buttonVariants> & { 47 + asChild?: boolean 48 + }) { 49 + const Comp = asChild ? Slot : "button" 50 + 51 + return ( 52 + <Comp 53 + data-slot="button" 54 + className={cn(buttonVariants({ variant, size, className }))} 55 + {...props} 56 + /> 57 + ) 58 + } 59 + 60 + export { Button, buttonVariants }
+92
public/components/ui/card.tsx
··· 1 + import * as React from "react" 2 + 3 + import { cn } from "@public/lib/utils" 4 + 5 + function Card({ className, ...props }: React.ComponentProps<"div">) { 6 + return ( 7 + <div 8 + data-slot="card" 9 + className={cn( 10 + "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", 11 + className 12 + )} 13 + {...props} 14 + /> 15 + ) 16 + } 17 + 18 + function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 19 + return ( 20 + <div 21 + data-slot="card-header" 22 + className={cn( 23 + "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", 24 + className 25 + )} 26 + {...props} 27 + /> 28 + ) 29 + } 30 + 31 + function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 32 + return ( 33 + <div 34 + data-slot="card-title" 35 + className={cn("leading-none font-semibold", className)} 36 + {...props} 37 + /> 38 + ) 39 + } 40 + 41 + function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 42 + return ( 43 + <div 44 + data-slot="card-description" 45 + className={cn("text-muted-foreground text-sm", className)} 46 + {...props} 47 + /> 48 + ) 49 + } 50 + 51 + function CardAction({ className, ...props }: React.ComponentProps<"div">) { 52 + return ( 53 + <div 54 + data-slot="card-action" 55 + className={cn( 56 + "col-start-2 row-span-2 row-start-1 self-start justify-self-end", 57 + className 58 + )} 59 + {...props} 60 + /> 61 + ) 62 + } 63 + 64 + function CardContent({ className, ...props }: React.ComponentProps<"div">) { 65 + return ( 66 + <div 67 + data-slot="card-content" 68 + className={cn("px-6", className)} 69 + {...props} 70 + /> 71 + ) 72 + } 73 + 74 + function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 75 + return ( 76 + <div 77 + data-slot="card-footer" 78 + className={cn("flex items-center px-6 [.border-t]:pt-6", className)} 79 + {...props} 80 + /> 81 + ) 82 + } 83 + 84 + export { 85 + Card, 86 + CardHeader, 87 + CardFooter, 88 + CardTitle, 89 + CardAction, 90 + CardDescription, 91 + CardContent, 92 + }
+141
public/components/ui/dialog.tsx
··· 1 + import * as React from "react" 2 + import * as DialogPrimitive from "@radix-ui/react-dialog" 3 + import { XIcon } from "lucide-react" 4 + 5 + import { cn } from "@public/lib/utils" 6 + 7 + function Dialog({ 8 + ...props 9 + }: React.ComponentProps<typeof DialogPrimitive.Root>) { 10 + return <DialogPrimitive.Root data-slot="dialog" {...props} /> 11 + } 12 + 13 + function DialogTrigger({ 14 + ...props 15 + }: React.ComponentProps<typeof DialogPrimitive.Trigger>) { 16 + return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} /> 17 + } 18 + 19 + function DialogPortal({ 20 + ...props 21 + }: React.ComponentProps<typeof DialogPrimitive.Portal>) { 22 + return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} /> 23 + } 24 + 25 + function DialogClose({ 26 + ...props 27 + }: React.ComponentProps<typeof DialogPrimitive.Close>) { 28 + return <DialogPrimitive.Close data-slot="dialog-close" {...props} /> 29 + } 30 + 31 + function DialogOverlay({ 32 + className, 33 + ...props 34 + }: React.ComponentProps<typeof DialogPrimitive.Overlay>) { 35 + return ( 36 + <DialogPrimitive.Overlay 37 + data-slot="dialog-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 DialogContent({ 48 + className, 49 + children, 50 + showCloseButton = true, 51 + ...props 52 + }: React.ComponentProps<typeof DialogPrimitive.Content> & { 53 + showCloseButton?: boolean 54 + }) { 55 + return ( 56 + <DialogPortal data-slot="dialog-portal"> 57 + <DialogOverlay /> 58 + <DialogPrimitive.Content 59 + data-slot="dialog-content" 60 + className={cn( 61 + "bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg", 62 + className 63 + )} 64 + {...props} 65 + > 66 + {children} 67 + {showCloseButton && ( 68 + <DialogPrimitive.Close 69 + data-slot="dialog-close" 70 + className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground 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 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" 71 + > 72 + <XIcon /> 73 + <span className="sr-only">Close</span> 74 + </DialogPrimitive.Close> 75 + )} 76 + </DialogPrimitive.Content> 77 + </DialogPortal> 78 + ) 79 + } 80 + 81 + function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { 82 + return ( 83 + <div 84 + data-slot="dialog-header" 85 + className={cn("flex flex-col gap-2 text-center sm:text-left", className)} 86 + {...props} 87 + /> 88 + ) 89 + } 90 + 91 + function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { 92 + return ( 93 + <div 94 + data-slot="dialog-footer" 95 + className={cn( 96 + "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", 97 + className 98 + )} 99 + {...props} 100 + /> 101 + ) 102 + } 103 + 104 + function DialogTitle({ 105 + className, 106 + ...props 107 + }: React.ComponentProps<typeof DialogPrimitive.Title>) { 108 + return ( 109 + <DialogPrimitive.Title 110 + data-slot="dialog-title" 111 + className={cn("text-lg leading-none font-semibold", className)} 112 + {...props} 113 + /> 114 + ) 115 + } 116 + 117 + function DialogDescription({ 118 + className, 119 + ...props 120 + }: React.ComponentProps<typeof DialogPrimitive.Description>) { 121 + return ( 122 + <DialogPrimitive.Description 123 + data-slot="dialog-description" 124 + className={cn("text-muted-foreground text-sm", className)} 125 + {...props} 126 + /> 127 + ) 128 + } 129 + 130 + export { 131 + Dialog, 132 + DialogClose, 133 + DialogContent, 134 + DialogDescription, 135 + DialogFooter, 136 + DialogHeader, 137 + DialogOverlay, 138 + DialogPortal, 139 + DialogTitle, 140 + DialogTrigger, 141 + }
+21
public/components/ui/input.tsx
··· 1 + import * as React from "react" 2 + 3 + import { cn } from "@public/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 }
+22
public/components/ui/label.tsx
··· 1 + import * as React from "react" 2 + import * as LabelPrimitive from "@radix-ui/react-label" 3 + 4 + import { cn } from "@public/lib/utils" 5 + 6 + function Label({ 7 + className, 8 + ...props 9 + }: React.ComponentProps<typeof LabelPrimitive.Root>) { 10 + return ( 11 + <LabelPrimitive.Root 12 + data-slot="label" 13 + className={cn( 14 + "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", 15 + className 16 + )} 17 + {...props} 18 + /> 19 + ) 20 + } 21 + 22 + export { Label }
+45
public/components/ui/radio-group.tsx
··· 1 + "use client" 2 + 3 + import * as React from "react" 4 + import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" 5 + import { CircleIcon } from "lucide-react" 6 + 7 + import { cn } from "@public/lib/utils" 8 + 9 + function RadioGroup({ 10 + className, 11 + ...props 12 + }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) { 13 + return ( 14 + <RadioGroupPrimitive.Root 15 + data-slot="radio-group" 16 + className={cn("grid gap-3", className)} 17 + {...props} 18 + /> 19 + ) 20 + } 21 + 22 + function RadioGroupItem({ 23 + className, 24 + ...props 25 + }: React.ComponentProps<typeof RadioGroupPrimitive.Item>) { 26 + return ( 27 + <RadioGroupPrimitive.Item 28 + data-slot="radio-group-item" 29 + className={cn( 30 + "border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", 31 + className 32 + )} 33 + {...props} 34 + > 35 + <RadioGroupPrimitive.Indicator 36 + data-slot="radio-group-indicator" 37 + className="relative flex items-center justify-center" 38 + > 39 + <CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" /> 40 + </RadioGroupPrimitive.Indicator> 41 + </RadioGroupPrimitive.Item> 42 + ) 43 + } 44 + 45 + export { RadioGroup, RadioGroupItem }
+64
public/components/ui/tabs.tsx
··· 1 + import * as React from "react" 2 + import * as TabsPrimitive from "@radix-ui/react-tabs" 3 + 4 + import { cn } from "@public/lib/utils" 5 + 6 + function Tabs({ 7 + className, 8 + ...props 9 + }: React.ComponentProps<typeof TabsPrimitive.Root>) { 10 + return ( 11 + <TabsPrimitive.Root 12 + data-slot="tabs" 13 + className={cn("flex flex-col gap-2", className)} 14 + {...props} 15 + /> 16 + ) 17 + } 18 + 19 + function TabsList({ 20 + className, 21 + ...props 22 + }: React.ComponentProps<typeof TabsPrimitive.List>) { 23 + return ( 24 + <TabsPrimitive.List 25 + data-slot="tabs-list" 26 + className={cn( 27 + "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]", 28 + className 29 + )} 30 + {...props} 31 + /> 32 + ) 33 + } 34 + 35 + function TabsTrigger({ 36 + className, 37 + ...props 38 + }: React.ComponentProps<typeof TabsPrimitive.Trigger>) { 39 + return ( 40 + <TabsPrimitive.Trigger 41 + data-slot="tabs-trigger" 42 + className={cn( 43 + "data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 44 + className 45 + )} 46 + {...props} 47 + /> 48 + ) 49 + } 50 + 51 + function TabsContent({ 52 + className, 53 + ...props 54 + }: React.ComponentProps<typeof TabsPrimitive.Content>) { 55 + return ( 56 + <TabsPrimitive.Content 57 + data-slot="tabs-content" 58 + className={cn("outline-none", className)} 59 + {...props} 60 + /> 61 + ) 62 + } 63 + 64 + export { Tabs, TabsList, TabsTrigger, TabsContent }
+503 -92
public/editor/editor.tsx
··· 1 - import { useState, useRef } from 'react' 1 + import { useState } from 'react' 2 2 import { createRoot } from 'react-dom/client' 3 + import { Button } from '@public/components/ui/button' 4 + import { 5 + Card, 6 + CardContent, 7 + CardDescription, 8 + CardHeader, 9 + CardTitle 10 + } from '@public/components/ui/card' 11 + import { Input } from '@public/components/ui/input' 12 + import { Label } from '@public/components/ui/label' 13 + import { 14 + Tabs, 15 + TabsContent, 16 + TabsList, 17 + TabsTrigger 18 + } from '@public/components/ui/tabs' 19 + import { Badge } from '@public/components/ui/badge' 20 + import { 21 + Dialog, 22 + DialogContent, 23 + DialogDescription, 24 + DialogHeader, 25 + DialogTitle, 26 + DialogFooter 27 + } from '@public/components/ui/dialog' 28 + import { RadioGroup, RadioGroupItem } from '@public/components/ui/radio-group' 29 + import { 30 + Globe, 31 + Upload, 32 + Settings, 33 + ExternalLink, 34 + CheckCircle2, 35 + XCircle, 36 + AlertCircle 37 + } from 'lucide-react' 3 38 4 39 import Layout from '@public/layouts' 5 40 6 - function Editor() { 7 - const [uploading, setUploading] = useState(false) 8 - const [result, setResult] = useState<any>(null) 9 - const [error, setError] = useState<string | null>(null) 10 - const folderInputRef = useRef<HTMLInputElement>(null) 11 - const siteNameRef = useRef<HTMLInputElement>(null) 41 + // Mock user data - replace with actual auth 42 + const mockUser = { 43 + did: 'did:plc:abc123xyz', 44 + handle: 'alice.bsky.social', 45 + wispSubdomain: 'alice' 46 + } 12 47 13 - const handleFileUpload = async (e: React.FormEvent) => { 14 - e.preventDefault() 15 - setError(null) 16 - setResult(null) 48 + function Dashboard() { 49 + const [customDomain, setCustomDomain] = useState('') 50 + const [verificationStatus, setVerificationStatus] = useState< 51 + 'idle' | 'verifying' | 'success' | 'error' 52 + >('idle') 53 + const [selectedSite, setSelectedSite] = useState('') 17 54 18 - const files = folderInputRef.current?.files 19 - const siteName = siteNameRef.current?.value 55 + const [configureModalOpen, setConfigureModalOpen] = useState(false) 56 + const [addDomainModalOpen, setAddDomainModalOpen] = useState(false) 57 + const [currentSite, setCurrentSite] = useState<{ 58 + id: string 59 + name: string 60 + domain: string | null 61 + } | null>(null) 62 + const [selectedDomain, setSelectedDomain] = useState<string>('') 20 63 21 - if (!files || files.length === 0) { 22 - setError('Please select a folder to upload') 23 - return 64 + // Mock sites data 65 + const [sites] = useState([ 66 + { 67 + id: '1', 68 + name: 'my-blog', 69 + domain: 'alice.wisp.place', 70 + status: 'active' 71 + }, 72 + { id: '2', name: 'portfolio', domain: null, status: 'active' }, 73 + { 74 + id: '3', 75 + name: 'docs-site', 76 + domain: 'docs.example.com', 77 + status: 'active' 24 78 } 79 + ]) 25 80 26 - if (!siteName) { 27 - setError('Please enter a site name') 28 - return 29 - } 81 + const availableDomains = [ 82 + { value: 'alice.wisp.place', label: 'alice.wisp.place', type: 'wisp' }, 83 + { 84 + value: 'docs.example.com', 85 + label: 'docs.example.com', 86 + type: 'custom' 87 + }, 88 + { value: 'none', label: 'No domain (use default URL)', type: 'none' } 89 + ] 30 90 31 - setUploading(true) 32 - 33 - try { 34 - const formData = new FormData() 35 - formData.append('siteName', siteName) 36 - 37 - for (let i = 0; i < files.length; i++) { 38 - formData.append('files', files[i]) 39 - } 91 + const handleVerifyDNS = async () => { 92 + setVerificationStatus('verifying') 93 + // Simulate DNS verification 94 + setTimeout(() => { 95 + setVerificationStatus('success') 96 + }, 2000) 97 + } 40 98 41 - const response = await fetch('/wisp/upload-files', { 42 - method: 'POST', 43 - body: formData 44 - }) 99 + const handleConfigureSite = (site: { 100 + id: string 101 + name: string 102 + domain: string | null 103 + }) => { 104 + setCurrentSite(site) 105 + setSelectedDomain(site.domain || 'none') 106 + setConfigureModalOpen(true) 107 + } 45 108 46 - if (!response.ok) { 47 - throw new Error(`Upload failed: ${response.statusText}`) 48 - } 109 + const handleSaveConfiguration = () => { 110 + console.log( 111 + '[v0] Saving configuration for site:', 112 + currentSite?.name, 113 + 'with domain:', 114 + selectedDomain 115 + ) 116 + // TODO: Implement actual save logic 117 + setConfigureModalOpen(false) 118 + } 49 119 50 - const data = await response.json() 51 - setResult(data) 52 - } catch (err) { 53 - setError(err instanceof Error ? err.message : 'Upload failed') 54 - } finally { 55 - setUploading(false) 120 + const getSiteUrl = (site: { name: string; domain: string | null }) => { 121 + if (site.domain) { 122 + return `https://${site.domain}` 56 123 } 124 + return `https://sites.wisp.place/${mockUser.did}/${site.name}` 57 125 } 58 126 59 127 return ( 60 - <div className="w-full max-w-2xl mx-auto p-6"> 61 - <h1 className="text-3xl font-bold mb-6 text-center">Upload Folder</h1> 62 - 63 - <form onSubmit={handleFileUpload} className="space-y-4"> 64 - <div> 65 - <label htmlFor="siteName" className="block text-sm font-medium mb-2"> 66 - Site Name 67 - </label> 68 - <input 69 - ref={siteNameRef} 70 - type="text" 71 - id="siteName" 72 - placeholder="Enter site name" 73 - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" 74 - /> 128 + <div className="w-full min-h-screen bg-background"> 129 + {/* Header */} 130 + <header className="border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50"> 131 + <div className="container mx-auto px-4 py-4 flex items-center justify-between"> 132 + <div className="flex items-center gap-2"> 133 + <div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center"> 134 + <Globe className="w-5 h-5 text-primary-foreground" /> 135 + </div> 136 + <span className="text-xl font-semibold text-foreground"> 137 + wisp.place 138 + </span> 139 + </div> 140 + <div className="flex items-center gap-3"> 141 + <span className="text-sm text-muted-foreground"> 142 + {mockUser.handle} 143 + </span> 144 + </div> 75 145 </div> 146 + </header> 76 147 77 - <div> 78 - <label htmlFor="folder" className="block text-sm font-medium mb-2"> 79 - Select Folder 80 - </label> 81 - <input 82 - ref={folderInputRef} 83 - type="file" 84 - id="folder" 85 - {...({ webkitdirectory: '', directory: '' } as any)} 86 - multiple 87 - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" 88 - /> 148 + <div className="container mx-auto px-4 py-8 max-w-6xl w-full"> 149 + <div className="mb-8"> 150 + <h1 className="text-3xl font-bold mb-2">Dashboard</h1> 151 + <p className="text-muted-foreground"> 152 + Manage your sites and domains 153 + </p> 89 154 </div> 90 155 91 - <button 92 - type="submit" 93 - disabled={uploading} 94 - className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-semibold py-2 px-4 rounded-md transition-colors" 95 - > 96 - {uploading ? 'Uploading...' : 'Upload Folder'} 97 - </button> 98 - </form> 156 + <Tabs defaultValue="sites" className="space-y-6 w-full"> 157 + <TabsList className="grid w-full grid-cols-3 max-w-md"> 158 + <TabsTrigger value="sites">Sites</TabsTrigger> 159 + <TabsTrigger value="domains">Domains</TabsTrigger> 160 + <TabsTrigger value="upload">Upload</TabsTrigger> 161 + </TabsList> 162 + 163 + {/* Sites Tab */} 164 + <TabsContent value="sites" className="space-y-4 min-h-[400px]"> 165 + <Card> 166 + <CardHeader> 167 + <CardTitle>Your Sites</CardTitle> 168 + <CardDescription> 169 + View and manage all your deployed sites 170 + </CardDescription> 171 + </CardHeader> 172 + <CardContent className="space-y-4"> 173 + {sites.map((site) => ( 174 + <div 175 + key={site.id} 176 + className="flex items-center justify-between p-4 border border-border rounded-lg hover:bg-muted/50 transition-colors" 177 + > 178 + <div className="flex-1"> 179 + <div className="flex items-center gap-3 mb-2"> 180 + <h3 className="font-semibold text-lg"> 181 + {site.name} 182 + </h3> 183 + <Badge 184 + variant="secondary" 185 + className="text-xs" 186 + > 187 + {site.status} 188 + </Badge> 189 + </div> 190 + <a 191 + href={getSiteUrl(site)} 192 + target="_blank" 193 + rel="noopener noreferrer" 194 + className="text-sm text-accent hover:text-accent/80 flex items-center gap-1" 195 + > 196 + {site.domain || 197 + `sites.wisp.place/${mockUser.did}/${site.name}`} 198 + <ExternalLink className="w-3 h-3" /> 199 + </a> 200 + </div> 201 + <Button 202 + variant="outline" 203 + size="sm" 204 + onClick={() => 205 + handleConfigureSite(site) 206 + } 207 + > 208 + <Settings className="w-4 h-4 mr-2" /> 209 + Configure 210 + </Button> 211 + </div> 212 + ))} 213 + </CardContent> 214 + </Card> 215 + </TabsContent> 216 + 217 + {/* Domains Tab */} 218 + <TabsContent value="domains" className="space-y-4 min-h-[400px]"> 219 + <Card> 220 + <CardHeader> 221 + <CardTitle>wisp.place Subdomain</CardTitle> 222 + <CardDescription> 223 + Your free subdomain on the wisp.place 224 + network 225 + </CardDescription> 226 + </CardHeader> 227 + <CardContent> 228 + <div className="flex items-center gap-2 p-4 bg-muted/50 rounded-lg"> 229 + <CheckCircle2 className="w-5 h-5 text-green-500" /> 230 + <span className="font-mono text-lg"> 231 + {mockUser.wispSubdomain}.wisp.place 232 + </span> 233 + </div> 234 + <p className="text-sm text-muted-foreground mt-3"> 235 + Configure which site uses this domain in the 236 + Sites tab 237 + </p> 238 + </CardContent> 239 + </Card> 240 + 241 + <Card> 242 + <CardHeader> 243 + <CardTitle>Custom Domains</CardTitle> 244 + <CardDescription> 245 + Bring your own domain with DNS verification 246 + </CardDescription> 247 + </CardHeader> 248 + <CardContent className="space-y-4"> 249 + <Button 250 + onClick={() => setAddDomainModalOpen(true)} 251 + className="w-full" 252 + > 253 + Add Custom Domain 254 + </Button> 255 + 256 + <div className="space-y-2"> 257 + <div className="flex items-center justify-between p-3 border border-border rounded-lg"> 258 + <div className="flex items-center gap-2"> 259 + <CheckCircle2 className="w-4 h-4 text-green-500" /> 260 + <span className="font-mono"> 261 + docs.example.com 262 + </span> 263 + </div> 264 + <Badge variant="secondary"> 265 + Verified 266 + </Badge> 267 + </div> 268 + </div> 269 + </CardContent> 270 + </Card> 271 + </TabsContent> 272 + 273 + {/* Upload Tab */} 274 + <TabsContent value="upload" className="space-y-4 min-h-[400px]"> 275 + <Card> 276 + <CardHeader> 277 + <CardTitle>Upload Site</CardTitle> 278 + <CardDescription> 279 + Deploy a new site from a folder or Git 280 + repository 281 + </CardDescription> 282 + </CardHeader> 283 + <CardContent className="space-y-6"> 284 + <div className="space-y-2"> 285 + <Label htmlFor="site-name">Site Name</Label> 286 + <Input 287 + id="site-name" 288 + placeholder="my-awesome-site" 289 + /> 290 + </div> 291 + 292 + <div className="grid md:grid-cols-2 gap-4"> 293 + <Card className="border-2 border-dashed hover:border-accent transition-colors cursor-pointer"> 294 + <CardContent className="flex flex-col items-center justify-center p-8 text-center"> 295 + <Upload className="w-12 h-12 text-muted-foreground mb-4" /> 296 + <h3 className="font-semibold mb-2"> 297 + Upload Folder 298 + </h3> 299 + <p className="text-sm text-muted-foreground mb-4"> 300 + Drag and drop or click to upload 301 + your static site files 302 + </p> 303 + <Button variant="outline"> 304 + Choose Folder 305 + </Button> 306 + </CardContent> 307 + </Card> 308 + 309 + <Card className="border-2 border-dashed hover:border-accent transition-colors"> 310 + <CardContent className="flex flex-col items-center justify-center p-8 text-center"> 311 + <Globe className="w-12 h-12 text-muted-foreground mb-4" /> 312 + <h3 className="font-semibold mb-2"> 313 + Connect Git Repository 314 + </h3> 315 + <p className="text-sm text-muted-foreground mb-4"> 316 + Link your GitHub, GitLab, or any 317 + Git repository 318 + </p> 319 + <Button variant="outline"> 320 + Connect Git 321 + </Button> 322 + </CardContent> 323 + </Card> 324 + </div> 325 + </CardContent> 326 + </Card> 327 + </TabsContent> 328 + </Tabs> 329 + </div> 330 + 331 + <Dialog 332 + open={configureModalOpen} 333 + onOpenChange={setConfigureModalOpen} 334 + > 335 + <DialogContent className="sm:max-w-md"> 336 + <DialogHeader> 337 + <DialogTitle>Configure Site Domain</DialogTitle> 338 + <DialogDescription> 339 + Choose which domain {currentSite?.name} should use 340 + </DialogDescription> 341 + </DialogHeader> 342 + <div className="space-y-4 py-4"> 343 + <RadioGroup 344 + value={selectedDomain} 345 + onValueChange={setSelectedDomain} 346 + > 347 + {availableDomains.map((domain) => ( 348 + <div 349 + key={domain.value} 350 + className="flex items-center space-x-2" 351 + > 352 + <RadioGroupItem 353 + value={domain.value} 354 + id={domain.value} 355 + /> 356 + <Label 357 + htmlFor={domain.value} 358 + className="flex-1 cursor-pointer" 359 + > 360 + <div className="flex items-center justify-between"> 361 + <span className="font-mono text-sm"> 362 + {domain.label} 363 + </span> 364 + {domain.type === 'wisp' && ( 365 + <Badge 366 + variant="secondary" 367 + className="text-xs" 368 + > 369 + Free 370 + </Badge> 371 + )} 372 + {domain.type === 'custom' && ( 373 + <Badge 374 + variant="outline" 375 + className="text-xs" 376 + > 377 + Custom 378 + </Badge> 379 + )} 380 + </div> 381 + </Label> 382 + </div> 383 + ))} 384 + </RadioGroup> 385 + </div> 386 + <DialogFooter> 387 + <Button 388 + variant="outline" 389 + onClick={() => setConfigureModalOpen(false)} 390 + > 391 + Cancel 392 + </Button> 393 + <Button onClick={handleSaveConfiguration}> 394 + Save Configuration 395 + </Button> 396 + </DialogFooter> 397 + </DialogContent> 398 + </Dialog> 399 + 400 + <Dialog 401 + open={addDomainModalOpen} 402 + onOpenChange={setAddDomainModalOpen} 403 + > 404 + <DialogContent className="sm:max-w-lg"> 405 + <DialogHeader> 406 + <DialogTitle>Add Custom Domain</DialogTitle> 407 + <DialogDescription> 408 + Configure DNS records to verify your domain 409 + ownership 410 + </DialogDescription> 411 + </DialogHeader> 412 + <div className="space-y-4 py-4"> 413 + <div className="space-y-2"> 414 + <Label htmlFor="new-domain">Domain Name</Label> 415 + <Input 416 + id="new-domain" 417 + placeholder="example.com" 418 + value={customDomain} 419 + onChange={(e) => 420 + setCustomDomain(e.target.value) 421 + } 422 + /> 423 + </div> 99 424 100 - {error && ( 101 - <div className="mt-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded-md"> 102 - {error} 103 - </div> 104 - )} 425 + {customDomain && ( 426 + <div className="space-y-4 p-4 bg-muted/30 rounded-lg border border-border"> 427 + <div> 428 + <h4 className="font-semibold mb-2 flex items-center gap-2"> 429 + <AlertCircle className="w-4 h-4 text-accent" /> 430 + DNS Configuration Required 431 + </h4> 432 + <p className="text-sm text-muted-foreground mb-4"> 433 + Add these DNS records to your domain 434 + provider: 435 + </p> 436 + </div> 105 437 106 - {result && ( 107 - <div className="mt-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded-md"> 108 - <h3 className="font-semibold mb-2">Upload Successful!</h3> 109 - <p>Files uploaded: {result.fileCount}</p> 110 - <p>Site name: {result.siteName}</p> 111 - <p>URI: {result.uri}</p> 112 - </div> 113 - )} 438 + <div className="space-y-3"> 439 + <div className="p-3 bg-background rounded border border-border"> 440 + <div className="flex justify-between items-start mb-1"> 441 + <span className="text-xs font-semibold text-muted-foreground"> 442 + TXT Record 443 + </span> 444 + </div> 445 + <div className="font-mono text-sm space-y-1"> 446 + <div> 447 + <span className="text-muted-foreground"> 448 + Name: 449 + </span>{' '} 450 + _wisp 451 + </div> 452 + <div> 453 + <span className="text-muted-foreground"> 454 + Value: 455 + </span>{' '} 456 + {mockUser.did} 457 + </div> 458 + </div> 459 + </div> 460 + 461 + <div className="p-3 bg-background rounded border border-border"> 462 + <div className="flex justify-between items-start mb-1"> 463 + <span className="text-xs font-semibold text-muted-foreground"> 464 + CNAME Record 465 + </span> 466 + </div> 467 + <div className="font-mono text-sm space-y-1"> 468 + <div> 469 + <span className="text-muted-foreground"> 470 + Name: 471 + </span>{' '} 472 + @ or {customDomain} 473 + </div> 474 + <div> 475 + <span className="text-muted-foreground"> 476 + Value: 477 + </span>{' '} 478 + abc123.dns.wisp.place 479 + </div> 480 + </div> 481 + </div> 482 + </div> 483 + </div> 484 + )} 485 + </div> 486 + <DialogFooter className="flex-col sm:flex-row gap-2"> 487 + <Button 488 + variant="outline" 489 + onClick={() => { 490 + setAddDomainModalOpen(false) 491 + setCustomDomain('') 492 + setVerificationStatus('idle') 493 + }} 494 + className="w-full sm:w-auto" 495 + > 496 + Cancel 497 + </Button> 498 + <Button 499 + onClick={handleVerifyDNS} 500 + disabled={ 501 + !customDomain || 502 + verificationStatus === 'verifying' 503 + } 504 + className="w-full sm:w-auto" 505 + > 506 + {verificationStatus === 'verifying' ? ( 507 + <>Verifying DNS...</> 508 + ) : verificationStatus === 'success' ? ( 509 + <> 510 + <CheckCircle2 className="w-4 h-4 mr-2" /> 511 + Verified 512 + </> 513 + ) : verificationStatus === 'error' ? ( 514 + <> 515 + <XCircle className="w-4 h-4 mr-2" /> 516 + Verification Failed 517 + </> 518 + ) : ( 519 + <>Verify DNS Records</> 520 + )} 521 + </Button> 522 + </DialogFooter> 523 + </DialogContent> 524 + </Dialog> 114 525 </div> 115 526 ) 116 527 } ··· 118 529 const root = createRoot(document.getElementById('elysia')!) 119 530 root.render( 120 531 <Layout className="gap-6"> 121 - <Editor /> 532 + <Dashboard /> 122 533 </Layout> 123 - ) 534 + )
public/images/maddelena-1.webp

This is a binary file and will not be displayed.

public/images/maddelena-2.webp

This is a binary file and will not be displayed.

+281 -127
public/index.tsx
··· 1 1 import { useState, useRef, useEffect } from 'react' 2 2 import { createRoot } from 'react-dom/client' 3 + import { 4 + ArrowRight, 5 + Shield, 6 + Zap, 7 + Globe, 8 + Lock, 9 + Code, 10 + Server 11 + } from 'lucide-react' 3 12 4 13 import Layout from '@public/layouts' 14 + import { Button } from '@public/components/ui/button' 15 + import { Card } from '@public/components/ui/card' 5 16 6 17 function App() { 7 18 const [showForm, setShowForm] = useState(false) ··· 14 25 }, [showForm]) 15 26 16 27 return ( 17 - <> 18 - <section id="header" className="py-24 px-6"> 19 - <div className="text-center space-y-8"> 20 - <div className="space-y-4"> 21 - <h1 className="text-6xl md:text-8xl font-bold text-balance leading-tight"> 22 - The complete platform to{' '} 23 - <span className="gradient-text"> 24 - publish the web. 25 - </span> 26 - </h1> 27 - <p className="text-xl md:text-2xl text-muted-foreground max-w-3xl mx-auto text-balance"> 28 - Your decentralized toolkit to stop configuring and 29 - start publishing. Securely build, deploy, and own 30 - your web presence with AT Protocol. 31 - </p> 28 + <div className="min-h-screen"> 29 + {/* Header */} 30 + <header className="border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50"> 31 + <div className="container mx-auto px-4 py-4 flex items-center justify-between"> 32 + <div className="flex items-center gap-2"> 33 + <div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center"> 34 + <Globe className="w-5 h-5 text-primary-foreground" /> 35 + </div> 36 + <span className="text-xl font-semibold text-foreground"> 37 + wisp.place 38 + </span> 32 39 </div> 33 - 34 - <div className="inline-flex items-center gap-2 bg-accent/10 border border-accent/20 rounded-full px-4 py-2"> 35 - <svg 36 - xmlns="http://www.w3.org/2000/svg" 37 - className="h-5 w-5 text-accent" 38 - viewBox="0 0 20 20" 39 - fill="currentColor" 40 + <div className="flex items-center gap-3"> 41 + <Button 42 + variant="ghost" 43 + size="sm" 44 + onClick={() => setShowForm(true)} 40 45 > 41 - <path 42 - fillRule="evenodd" 43 - d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.414-1.414L11 9.586V6z" 44 - clipRule="evenodd" 45 - /> 46 - </svg> 47 - <span className="text-sm font-medium text-accent"> 48 - Publish once, own forever 46 + Sign In 47 + </Button> 48 + <Button 49 + size="sm" 50 + className="bg-accent text-accent-foreground hover:bg-accent/90" 51 + > 52 + Get Started 53 + </Button> 54 + </div> 55 + </div> 56 + </header> 57 + 58 + {/* Hero Section */} 59 + <section className="container mx-auto px-4 py-20 md:py-32"> 60 + <div className="max-w-4xl mx-auto text-center"> 61 + <div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-accent/10 border border-accent/20 mb-8"> 62 + <span className="w-2 h-2 bg-accent rounded-full animate-pulse"></span> 63 + <span className="text-sm text-accent-foreground"> 64 + Built on AT Protocol 49 65 </span> 50 66 </div> 51 67 52 - <div className="max-w-md mx-auto space-y-4 mt-8"> 53 - <div className="relative h-16"> 54 - <div 55 - className={`transition-all duration-500 ease-in-out absolute inset-0 ${ 56 - showForm 57 - ? 'opacity-0 -translate-y-5 pointer-events-none' 58 - : 'opacity-100 translate-y-0' 59 - }`} 60 - > 61 - <button 62 - onClick={() => setShowForm(true)} 63 - className="w-full bg-primary hover:bg-primary/90 text-primary-foreground font-semibold py-4 px-6 text-lg rounded-lg inline-flex items-center justify-center transition-colors" 64 - > 65 - Log in with AT Proto 66 - <svg 67 - xmlns="http://www.w3.org/2000/svg" 68 - className="ml-2 w-5 h-5" 69 - viewBox="0 0 24 24" 70 - fill="none" 71 - stroke="currentColor" 72 - strokeWidth="2" 73 - strokeLinecap="round" 74 - strokeLinejoin="round" 75 - > 76 - <path d="M5 12h14M12 5l7 7-7 7" /> 77 - </svg> 78 - </button> 79 - </div> 68 + <h1 className="text-5xl md:text-7xl font-bold text-balance mb-6 leading-tight"> 69 + Host your sites on the{' '} 70 + <span className="text-primary">decentralized</span> web 71 + </h1> 80 72 81 - <div 82 - className={`transition-all duration-500 ease-in-out absolute inset-0 ${ 83 - showForm 84 - ? 'opacity-100 translate-y-0' 85 - : 'opacity-0 translate-y-5 pointer-events-none' 86 - }`} 73 + <p className="text-xl md:text-2xl text-muted-foreground text-balance mb-10 leading-relaxed max-w-3xl mx-auto"> 74 + Deploy static sites to a truly open network. Your 75 + content, your control, your identity. No platform 76 + lock-in, ever. 77 + </p> 78 + 79 + <div className="max-w-md mx-auto relative"> 80 + <div 81 + className={`transition-all duration-500 ease-in-out ${ 82 + showForm 83 + ? 'opacity-0 -translate-y-5 pointer-events-none' 84 + : 'opacity-100 translate-y-0' 85 + }`} 86 + > 87 + <Button 88 + size="lg" 89 + className="bg-primary text-primary-foreground hover:bg-primary/90 text-lg px-8 py-6 w-full" 90 + onClick={() => setShowForm(true)} 87 91 > 88 - <form 89 - onSubmit={async (e) => { 90 - e.preventDefault() 91 - try { 92 - const handle = 93 - inputRef.current?.value 94 - const res = await fetch( 95 - '/api/auth/signin', 96 - { 97 - method: 'POST', 98 - headers: { 99 - 'Content-Type': 100 - 'application/json' 101 - }, 102 - body: JSON.stringify({ 103 - handle 104 - }) 105 - } 106 - ) 107 - if (!res.ok) 108 - throw new Error( 109 - 'Request failed' 110 - ) 111 - const data = await res.json() 112 - if (data.url) { 113 - window.location.href = data.url 114 - } else { 115 - alert('Unexpected response') 92 + Log in with AT Proto 93 + <ArrowRight className="ml-2 w-5 h-5" /> 94 + </Button> 95 + </div> 96 + 97 + <div 98 + className={`transition-all duration-500 ease-in-out absolute inset-0 ${ 99 + showForm 100 + ? 'opacity-100 translate-y-0' 101 + : 'opacity-0 translate-y-5 pointer-events-none' 102 + }`} 103 + > 104 + <form 105 + onSubmit={async (e) => { 106 + e.preventDefault() 107 + try { 108 + const handle = inputRef.current?.value 109 + const res = await fetch( 110 + '/api/auth/signin', 111 + { 112 + method: 'POST', 113 + headers: { 114 + 'Content-Type': 115 + 'application/json' 116 + }, 117 + body: JSON.stringify({ handle }) 116 118 } 117 - } catch (error) { 118 - console.error( 119 - 'Login failed:', 120 - error 121 - ) 122 - alert('Authentication failed') 119 + ) 120 + if (!res.ok) 121 + throw new Error('Request failed') 122 + const data = await res.json() 123 + if (data.url) { 124 + window.location.href = data.url 125 + } else { 126 + alert('Unexpected response') 123 127 } 124 - }} 125 - className="space-y-3" 128 + } catch (error) { 129 + console.error('Login failed:', error) 130 + alert('Authentication failed') 131 + } 132 + }} 133 + className="space-y-3" 134 + > 135 + <input 136 + ref={inputRef} 137 + type="text" 138 + name="handle" 139 + placeholder="Enter your handle (e.g., alice.bsky.social)" 140 + className="w-full py-4 px-4 text-lg bg-input border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent" 141 + /> 142 + <button 143 + type="submit" 144 + className="w-full bg-accent hover:bg-accent/90 text-accent-foreground font-semibold py-4 px-6 text-lg rounded-lg inline-flex items-center justify-center transition-colors" 126 145 > 127 - <input 128 - ref={inputRef} 129 - type="text" 130 - name="handle" 131 - placeholder="Enter your handle (e.g., alice.bsky.social)" 132 - className="w-full py-4 px-4 text-lg bg-input border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent" 133 - /> 134 - <button 135 - type="submit" 136 - className="w-full bg-accent hover:bg-accent/90 text-accent-foreground font-semibold py-4 px-6 text-lg rounded-lg inline-flex items-center justify-center transition-colors" 137 - > 138 - Continue 139 - <svg 140 - xmlns="http://www.w3.org/2000/svg" 141 - className="ml-2 w-5 h-5" 142 - viewBox="0 0 24 24" 143 - fill="none" 144 - stroke="currentColor" 145 - strokeWidth="2" 146 - strokeLinecap="round" 147 - strokeLinejoin="round" 148 - > 149 - <path d="M5 12h14M12 5l7 7-7 7" /> 150 - </svg> 151 - </button> 152 - </form> 146 + Continue 147 + <ArrowRight className="ml-2 w-5 h-5" /> 148 + </button> 149 + </form> 150 + </div> 151 + </div> 152 + </div> 153 + </section> 154 + 155 + {/* Stats Section */} 156 + <section className="container mx-auto px-4 py-16"> 157 + <div className="grid grid-cols-2 md:grid-cols-4 gap-8 max-w-5xl mx-auto"> 158 + {[ 159 + { value: '100%', label: 'Decentralized' }, 160 + { value: '0ms', label: 'Cold Start' }, 161 + { value: '∞', label: 'Scalability' }, 162 + { value: 'You', label: 'Own Your Data' } 163 + ].map((stat, i) => ( 164 + <div key={i} className="text-center"> 165 + <div className="text-4xl md:text-5xl font-bold text-primary mb-2"> 166 + {stat.value} 167 + </div> 168 + <div className="text-sm text-muted-foreground"> 169 + {stat.label} 153 170 </div> 154 171 </div> 172 + ))} 173 + </div> 174 + </section> 175 + 176 + {/* Features Grid */} 177 + <section id="features" className="container mx-auto px-4 py-20"> 178 + <div className="text-center mb-16"> 179 + <h2 className="text-4xl md:text-5xl font-bold mb-4 text-balance"> 180 + Built for the open web 181 + </h2> 182 + <p className="text-xl text-muted-foreground text-balance max-w-2xl mx-auto"> 183 + Everything you need to deploy and manage static sites on 184 + a decentralized network 185 + </p> 186 + </div> 187 + 188 + <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto"> 189 + {[ 190 + { 191 + icon: Shield, 192 + title: 'True Ownership', 193 + description: 194 + 'Your content lives on the AT Protocol network. No single company can take it down or lock you out.' 195 + }, 196 + { 197 + icon: Zap, 198 + title: 'Lightning Fast', 199 + description: 200 + 'Distributed edge network ensures your sites load instantly from anywhere in the world.' 201 + }, 202 + { 203 + icon: Lock, 204 + title: 'Cryptographic Security', 205 + description: 206 + 'Content-addressed storage and cryptographic verification ensure integrity and authenticity.' 207 + }, 208 + { 209 + icon: Code, 210 + title: 'Developer Friendly', 211 + description: 212 + 'Simple CLI, Git integration, and familiar workflows. Deploy with a single command.' 213 + }, 214 + { 215 + icon: Server, 216 + title: 'Zero Vendor Lock-in', 217 + description: 218 + 'Built on open protocols. Migrate your sites anywhere, anytime. Your data is portable.' 219 + }, 220 + { 221 + icon: Globe, 222 + title: 'Global Network', 223 + description: 224 + 'Leverage the power of decentralized infrastructure for unmatched reliability and uptime.' 225 + } 226 + ].map((feature, i) => ( 227 + <Card 228 + key={i} 229 + className="p-6 hover:shadow-lg transition-shadow border-2 bg-card" 230 + > 231 + <div className="w-12 h-12 rounded-lg bg-accent/10 flex items-center justify-center mb-4"> 232 + <feature.icon className="w-6 h-6 text-accent" /> 233 + </div> 234 + <h3 className="text-xl font-semibold mb-2 text-card-foreground"> 235 + {feature.title} 236 + </h3> 237 + <p className="text-muted-foreground leading-relaxed"> 238 + {feature.description} 239 + </p> 240 + </Card> 241 + ))} 242 + </div> 243 + </section> 244 + 245 + {/* How It Works */} 246 + <section 247 + id="how-it-works" 248 + className="container mx-auto px-4 py-20 bg-muted/30" 249 + > 250 + <div className="max-w-4xl mx-auto"> 251 + <h2 className="text-4xl md:text-5xl font-bold text-center mb-16 text-balance"> 252 + Deploy in three steps 253 + </h2> 254 + 255 + <div className="space-y-12"> 256 + {[ 257 + { 258 + step: '01', 259 + title: 'Upload your site', 260 + description: 261 + 'Link your Git repository or upload a folder containing your static site directly.' 262 + }, 263 + { 264 + step: '02', 265 + title: 'Name and set domain', 266 + description: 267 + 'Name your site and set domain routing to it. You can bring your own domain too.' 268 + }, 269 + { 270 + step: '03', 271 + title: 'Deploy to AT Protocol', 272 + description: 273 + 'Your site is published to the decentralized network with a permanent, verifiable identity.' 274 + } 275 + ].map((step, i) => ( 276 + <div key={i} className="flex gap-6 items-start"> 277 + <div className="text-6xl font-bold text-accent/20 min-w-[80px]"> 278 + {step.step} 279 + </div> 280 + <div className="flex-1 pt-2"> 281 + <h3 className="text-2xl font-semibold mb-3"> 282 + {step.title} 283 + </h3> 284 + <p className="text-lg text-muted-foreground leading-relaxed"> 285 + {step.description} 286 + </p> 287 + </div> 288 + </div> 289 + ))} 155 290 </div> 156 291 </div> 157 292 </section> 158 - </> 293 + 294 + {/* Footer */} 295 + <footer className="border-t border-border/40 bg-muted/20"> 296 + <div className="container mx-auto px-4 py-8"> 297 + <div className="text-center text-sm text-muted-foreground"> 298 + <p> 299 + Built by{' '} 300 + <a 301 + href="https://bsky.app/profile/nekomimi.pet" 302 + target="_blank" 303 + rel="noopener noreferrer" 304 + className="text-accent hover:text-accent/80 transition-colors font-medium" 305 + > 306 + @nekomimi.pet 307 + </a> 308 + </p> 309 + </div> 310 + </div> 311 + </footer> 312 + </div> 159 313 ) 160 314 } 161 315
+1 -1
public/layouts/index.tsx
··· 16 16 <QueryClientProvider client={client}> 17 17 <div 18 18 className={clsx( 19 - 'flex flex-col justify-center items-center w-full min-h-screen', 19 + 'flex flex-col items-center w-full min-h-screen', 20 20 className 21 21 )} 22 22 >
+6
public/lib/utils.ts
··· 1 + import { clsx, type ClassValue } from "clsx" 2 + import { twMerge } from "tailwind-merge" 3 + 4 + export function cn(...inputs: ClassValue[]) { 5 + return twMerge(clsx(inputs)) 6 + }
public/libs/api.ts public/lib/api.ts
-13
public/other/index.html
··· 1 - <!doctype html> 2 - <html lang="en"> 3 - <head> 4 - <meta charset="UTF-8" /> 5 - 6 - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 - <title>Elysia Static</title> 8 - </head> 9 - <body> 10 - <div id="elysia"></div> 11 - <script type="module" src="./index.tsx"></script> 12 - </body> 13 - </html>
-27
public/other/index.tsx
··· 1 - import { createRoot } from 'react-dom/client' 2 - import { useQuery } from '@tanstack/react-query' 3 - 4 - import Layout from '../layouts' 5 - import { api } from '../libs/api' 6 - 7 - function App() { 8 - const { data: response, isLoading } = useQuery({ 9 - queryKey: ['version'], 10 - queryFn: () => api.message.get() 11 - }) 12 - 13 - return ( 14 - <> 15 - <img src="/images/maddelena-2.webp" className="max-w-40" /> 16 - <h1 className="text-3xl">API call!</h1> 17 - <h2 className="text-6xl">{response?.data?.message}</h2> 18 - </> 19 - ) 20 - } 21 - 22 - const root = createRoot(document.getElementById('elysia')!) 23 - root.render( 24 - <Layout className="gap-6"> 25 - <App /> 26 - </Layout> 27 - )
+90 -145
public/styles/global.css
··· 1 1 @import "tailwindcss"; 2 + @import "tw-animate-css"; 2 3 3 - .gradient-text { 4 - background: linear-gradient(135deg, 5 - #FFAAD2 0%, /* lavender pink */ 6 - #348AA7 25%, /* blue munsell */ 7 - #413C58 50%, /* english violet */ 8 - #CCD7C5 75%, /* ash gray */ 9 - #F2E7C9 100% /* parchment */ 10 - ); 11 - background-size: 200% 200%; 12 - -webkit-background-clip: text; 13 - -webkit-text-fill-color: transparent; 14 - background-clip: text; 15 - animation: gradient-shift 4s ease-in-out infinite; 16 - } 4 + @custom-variant dark (&:is(.dark *)); 5 + 6 + :root { 7 + /* #F2E7C9 - parchment background */ 8 + --background: oklch(0.93 0.03 85); 9 + /* #413C58 - violet for text */ 10 + --foreground: oklch(0.32 0.04 285); 11 + 12 + --card: oklch(0.98 0.01 85); 13 + --card-foreground: oklch(0.32 0.04 285); 14 + 15 + --popover: oklch(0.98 0.01 85); 16 + --popover-foreground: oklch(0.32 0.04 285); 17 + 18 + /* #413C58 - violet primary */ 19 + --primary: oklch(0.32 0.04 285); 20 + --primary-foreground: oklch(0.98 0.01 85); 21 + 22 + /* #FFAAD2 - pink accent */ 23 + --accent: oklch(0.78 0.15 345); 24 + --accent-foreground: oklch(0.32 0.04 285); 25 + 26 + /* #348AA7 - blue secondary */ 27 + --secondary: oklch(0.56 0.08 220); 28 + --secondary-foreground: oklch(0.98 0.01 85); 29 + 30 + /* #CCD7C5 - ash muted */ 31 + --muted: oklch(0.85 0.02 130); 32 + --muted-foreground: oklch(0.45 0.03 285); 33 + 34 + --border: oklch(0.75 0.02 285); 35 + --input: oklch(0.75 0.02 285); 36 + --ring: oklch(0.78 0.15 345); 37 + 38 + --destructive: oklch(0.577 0.245 27.325); 39 + --destructive-foreground: oklch(0.985 0 0); 17 40 18 - @keyframes gradient-shift { 19 - 0%, 20 - 100% { 21 - background-position: 0% 50%; 22 - } 23 - 50% { 24 - background-position: 100% 50%; 25 - } 41 + --chart-1: oklch(0.78 0.15 345); 42 + --chart-2: oklch(0.32 0.04 285); 43 + --chart-3: oklch(0.56 0.08 220); 44 + --chart-4: oklch(0.85 0.02 130); 45 + --chart-5: oklch(0.93 0.03 85); 46 + 47 + --radius: 0.75rem; 48 + --sidebar: oklch(0.985 0 0); 49 + --sidebar-foreground: oklch(0.145 0 0); 50 + --sidebar-primary: oklch(0.205 0 0); 51 + --sidebar-primary-foreground: oklch(0.985 0 0); 52 + --sidebar-accent: oklch(0.97 0 0); 53 + --sidebar-accent-foreground: oklch(0.205 0 0); 54 + --sidebar-border: oklch(0.922 0 0); 55 + --sidebar-ring: oklch(0.708 0 0); 26 56 } 27 57 28 - /* 29 - WISPY / GHOSTY THEME (Dark baseline) 30 - Philosophy: elegant violet depths with pink accents 31 - Palette: #FFAAD2 (pink), #413C58 (violet), #348AA7 (blue), #CCD7C5 (ash), #F2E7C9 (parchment) 32 - */ 33 - :root { 34 - /* Core surfaces - english violet base */ 35 - --background: #413C58; /* english violet */ 36 - --foreground: #F2E7C9; /* parchment */ 37 - --card: #4d4763; /* slightly lighter violet */ 38 - --card-foreground: var(--foreground); 39 - --popover: #48445f; 40 - --popover-foreground: var(--foreground); 58 + .dark { 59 + /* #413C58 - violet background for dark mode */ 60 + --background: oklch(0.28 0.04 285); 61 + /* #F2E7C9 - parchment text */ 62 + --foreground: oklch(0.93 0.03 85); 63 + 64 + --card: oklch(0.32 0.04 285); 65 + --card-foreground: oklch(0.93 0.03 85); 41 66 42 - /* Brand spectral axis (pink accent!) */ 43 - --primary: #FFAAD2; /* lavender pink - main accent */ 44 - --primary-foreground: #413C58; /* violet */ 45 - --secondary: #348AA7; /* blue munsell */ 46 - --secondary-foreground: #F2E7C9; /* parchment */ 47 - --accent: #FFAAD2; /* lavender pink - keeping the pink! */ 48 - --accent-foreground: #413C58; /* violet */ 49 - --muted: #5a5570; /* muted violet */ 50 - --muted-foreground: #CCD7C5; 67 + --popover: oklch(0.32 0.04 285); 68 + --popover-foreground: oklch(0.93 0.03 85); 51 69 52 - /* Feedback / semantic */ 53 - --destructive: #ff5588; /* brighter pink for warnings */ 54 - --destructive-foreground: #fff; 70 + /* #FFAAD2 - pink primary in dark mode */ 71 + --primary: oklch(0.78 0.15 345); 72 + --primary-foreground: oklch(0.32 0.04 285); 55 73 56 - /* Interaction frame tokens */ 57 - --border: #5a5570; /* muted violet */ 58 - --input: #4d4763; 59 - --ring: #FFAAD2; /* pink focus ring */ 74 + --accent: oklch(0.78 0.15 345); 75 + --accent-foreground: oklch(0.32 0.04 285); 60 76 61 - /* Data viz (ordered spectral gentle ramp) */ 62 - --chart-1: #FFAAD2; /* lavender pink */ 63 - --chart-2: #348AA7; /* blue munsell */ 64 - --chart-3: #CCD7C5; /* ash gray */ 65 - --chart-4: #F2E7C9; /* parchment */ 66 - --chart-5: #413C58; /* english violet */ 77 + --secondary: oklch(0.56 0.08 220); 78 + --secondary-foreground: oklch(0.93 0.03 85); 67 79 68 - --radius: 0.75rem; 80 + --muted: oklch(0.38 0.03 285); 81 + --muted-foreground: oklch(0.75 0.02 85); 69 82 70 - /* Sidebar palette reuses base tokens for cohesion */ 71 - --sidebar: #38344a; /* darker violet */ 72 - --sidebar-foreground: var(--foreground); 73 - --sidebar-primary: var(--primary); 74 - --sidebar-primary-foreground: var(--primary-foreground); 75 - --sidebar-accent: var(--accent); 76 - --sidebar-accent-foreground: var(--accent-foreground); 77 - --sidebar-border: var(--border); 78 - --sidebar-ring: var(--ring); 79 - } 83 + --border: oklch(0.42 0.03 285); 84 + --input: oklch(0.42 0.03 285); 85 + --ring: oklch(0.78 0.15 345); 80 86 81 - /* Light (parchment) variant */ 82 - [data-theme="light"] { 83 - --background: #F2E7C9; /* parchment */ 84 - --foreground: #413C58; /* english violet */ 85 - --card: #faf5e6; /* lighter parchment */ 86 - --card-foreground: var(--foreground); 87 - --popover: #fff; 88 - --popover-foreground: var(--foreground); 89 - --primary: #FFAAD2; /* lavender pink - keep the pink! */ 90 - --primary-foreground: #413C58; 91 - --secondary: #348AA7; /* blue munsell */ 92 - --secondary-foreground: #fff; 93 - --accent: #FFAAD2; /* lavender pink accent */ 94 - --accent-foreground: #413C58; 95 - --muted: #e8dfc0; 96 - --muted-foreground: #5a5570; 97 - --destructive: #d8006d; 98 - --destructive-foreground: #fff; 99 - --border: #CCD7C5; /* ash gray */ 100 - --input: #faf5e6; 101 - --ring: #FFAAD2; /* pink ring */ 102 - --chart-1: #FFAAD2; 103 - --chart-2: #348AA7; 104 - --chart-3: #413C58; 105 - --chart-4: #CCD7C5; 106 - --chart-5: #5a5570; 107 - --sidebar: #f7f0da; 108 - --sidebar-foreground: var(--foreground); 109 - --sidebar-primary: var(--primary); 110 - --sidebar-primary-foreground: var(--primary-foreground); 111 - --sidebar-accent: var(--accent); 112 - --sidebar-accent-foreground: var(--accent-foreground); 113 - --sidebar-border: var(--border); 114 - --sidebar-ring: var(--ring); 115 - } 87 + --destructive: oklch(0.577 0.245 27.325); 88 + --destructive-foreground: oklch(0.985 0 0); 116 89 117 - @media (prefers-color-scheme: light) { 118 - :root:not([data-theme="dark"]) { 119 - --background: #F2E7C9; /* parchment */ 120 - --foreground: #413C58; /* english violet */ 121 - --card: #faf5e6; /* lighter parchment */ 122 - --card-foreground: var(--foreground); 123 - --popover: #fff; 124 - --popover-foreground: var(--foreground); 125 - --primary: #FFAAD2; /* lavender pink */ 126 - --primary-foreground: #413C58; 127 - --secondary: #348AA7; /* blue munsell */ 128 - --secondary-foreground: #fff; 129 - --accent: #FFAAD2; /* lavender pink */ 130 - --accent-foreground: #413C58; 131 - --muted: #e8dfc0; 132 - --muted-foreground: #5a5570; 133 - --destructive: #d8006d; 134 - --destructive-foreground: #fff; 135 - --border: #CCD7C5; /* ash gray */ 136 - --input: #faf5e6; 137 - --ring: #FFAAD2; 138 - --chart-1: #FFAAD2; 139 - --chart-2: #348AA7; 140 - --chart-3: #413C58; 141 - --chart-4: #CCD7C5; 142 - --chart-5: #5a5570; 143 - --sidebar: #f7f0da; 144 - --sidebar-foreground: var(--foreground); 145 - --sidebar-primary: var(--primary); 146 - --sidebar-primary-foreground: var(--primary-foreground); 147 - --sidebar-accent: var(--accent); 148 - --sidebar-accent-foreground: var(--accent-foreground); 149 - --sidebar-border: var(--border); 150 - --sidebar-ring: var(--ring); 151 - } 90 + --chart-1: oklch(0.78 0.15 345); 91 + --chart-2: oklch(0.93 0.03 85); 92 + --chart-3: oklch(0.56 0.08 220); 93 + --chart-4: oklch(0.85 0.02 130); 94 + --chart-5: oklch(0.32 0.04 285); 95 + --sidebar: oklch(0.205 0 0); 96 + --sidebar-foreground: oklch(0.985 0 0); 97 + --sidebar-primary: oklch(0.488 0.243 264.376); 98 + --sidebar-primary-foreground: oklch(0.985 0 0); 99 + --sidebar-accent: oklch(0.269 0 0); 100 + --sidebar-accent-foreground: oklch(0.985 0 0); 101 + --sidebar-border: oklch(0.269 0 0); 102 + --sidebar-ring: oklch(0.439 0 0); 152 103 } 153 104 154 105 @theme inline { 155 - --font-sans: var(--font-geist-sans); 156 - --font-mono: var(--font-geist-mono); 106 + /* optional: --font-sans, --font-serif, --font-mono if they are applied in the layout.tsx */ 157 107 --color-background: var(--background); 158 108 --color-foreground: var(--foreground); 159 109 --color-card: var(--card); ··· 200 150 @apply bg-background text-foreground; 201 151 } 202 152 } 203 - 204 - /* Reduced motion respect: disable animated shimmer if user prefers */ 205 - @media (prefers-reduced-motion: reduce) { 206 - .gradient-text { animation: none; } 207 - }
+2
src/index.ts
··· 12 12 } from './lib/oauth-client' 13 13 import { authRoutes } from './routes/auth' 14 14 import { wispRoutes } from './routes/wisp' 15 + import { domainRoutes } from './routes/domain' 15 16 16 17 const config: Config = { 17 18 domain: (Bun.env.DOMAIN ?? `https://${BASE_HOST}`) as `https://${string}`, ··· 33 34 ) 34 35 .use(authRoutes(client)) 35 36 .use(wispRoutes(client)) 37 + .use(domainRoutes(client)) 36 38 .get('/client-metadata.json', (c) => { 37 39 return createClientMetadata(config) 38 40 })
+129
src/routes/domain.ts
··· 1 + import { Elysia } from 'elysia' 2 + import { requireAuth, type AuthenticatedContext } from '../lib/wisp-auth' 3 + import { NodeOAuthClient } from '@atproto/oauth-client-node' 4 + import { Agent } from '@atproto/api' 5 + import { 6 + claimDomain, 7 + getDomainByDid, 8 + isDomainAvailable, 9 + isValidHandle, 10 + toDomain, 11 + updateDomain, 12 + } from '../lib/db' 13 + 14 + export const domainRoutes = (client: NodeOAuthClient) => 15 + new Elysia({ prefix: '/api/domain' }) 16 + .derive(async ({ cookie }) => { 17 + const auth = await requireAuth(client, cookie) 18 + return { auth } 19 + }) 20 + .get('/check', async ({ query }) => { 21 + try { 22 + const handle = (query.handle || "") 23 + .trim() 24 + .toLowerCase(); 25 + 26 + if (!isValidHandle(handle)) { 27 + return { 28 + available: false, 29 + reason: "invalid" 30 + }; 31 + } 32 + 33 + const available = await isDomainAvailable(handle); 34 + return { 35 + available, 36 + domain: toDomain(handle) 37 + }; 38 + } catch (err) { 39 + console.error("domain/check error", err); 40 + return { 41 + available: false 42 + }; 43 + } 44 + }) 45 + .post('/claim', async ({ body, auth }) => { 46 + try { 47 + const { handle } = body as { handle?: string }; 48 + const normalizedHandle = (handle || "").trim().toLowerCase(); 49 + 50 + if (!isValidHandle(normalizedHandle)) { 51 + throw new Error("Invalid handle"); 52 + } 53 + 54 + // ensure user hasn't already claimed 55 + const existing = await getDomainByDid(auth.did); 56 + if (existing) { 57 + throw new Error("Already claimed"); 58 + } 59 + 60 + // claim in DB 61 + let domain: string; 62 + try { 63 + domain = await claimDomain(auth.did, normalizedHandle); 64 + } catch (err) { 65 + throw new Error("Handle taken"); 66 + } 67 + 68 + // write place.wisp.domain record rkey = self 69 + const agent = new Agent((url, init) => auth.session.fetchHandler(url, init)); 70 + await agent.com.atproto.repo.putRecord({ 71 + repo: auth.did, 72 + collection: "place.wisp.domain", 73 + rkey: "self", 74 + record: { 75 + $type: "place.wisp.domain", 76 + domain, 77 + createdAt: new Date().toISOString(), 78 + } as any, 79 + validate: false, 80 + }); 81 + 82 + return { success: true, domain }; 83 + } catch (err) { 84 + console.error("domain/claim error", err); 85 + throw new Error(`Failed to claim: ${err instanceof Error ? err.message : 'Unknown error'}`); 86 + } 87 + }) 88 + .post('/update', async ({ body, auth }) => { 89 + try { 90 + const { handle } = body as { handle?: string }; 91 + const normalizedHandle = (handle || "").trim().toLowerCase(); 92 + 93 + if (!isValidHandle(normalizedHandle)) { 94 + throw new Error("Invalid handle"); 95 + } 96 + 97 + const desiredDomain = toDomain(normalizedHandle); 98 + const current = await getDomainByDid(auth.did); 99 + 100 + if (current === desiredDomain) { 101 + return { success: true, domain: current }; 102 + } 103 + 104 + let domain: string; 105 + try { 106 + domain = await updateDomain(auth.did, normalizedHandle); 107 + } catch (err) { 108 + throw new Error("Handle taken"); 109 + } 110 + 111 + const agent = new Agent((url, init) => auth.session.fetchHandler(url, init)); 112 + await agent.com.atproto.repo.putRecord({ 113 + repo: auth.did, 114 + collection: "place.wisp.domain", 115 + rkey: "self", 116 + record: { 117 + $type: "place.wisp.domain", 118 + domain, 119 + createdAt: new Date().toISOString(), 120 + } as any, 121 + validate: false, 122 + }); 123 + 124 + return { success: true, domain }; 125 + } catch (err) { 126 + console.error("domain/update error", err); 127 + throw new Error(`Failed to update: ${err instanceof Error ? err.message : 'Unknown error'}`); 128 + } 129 + });
+1 -1
tsconfig.json
··· 105 105 "paths": { 106 106 "@server": ["./src/index.ts"], 107 107 "@server/*": ["./src/*"], 108 - "@public/*": ["./public/*"] 108 + "@public/*": ["./public/*"], 109 109 } 110 110 } 111 111 }