diff --git a/docs/screenshots/home.png b/docs/screenshots/home.png new file mode 100644 index 0000000..fe50498 Binary files /dev/null and b/docs/screenshots/home.png differ diff --git a/docs/screenshots/login.png b/docs/screenshots/login.png new file mode 100644 index 0000000..85a8305 Binary files /dev/null and b/docs/screenshots/login.png differ diff --git a/package-lock.json b/package-lock.json index d86aed2..daeb76f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,12 +18,26 @@ "devDependencies": { "jest": "^30.4.2", "jest-environment-jsdom": "^30.4.1", - "supertest": "^7.0.0" + "supertest": "^7.0.0", + "tailwindcss": "^3.4.19" }, "engines": { "node": ">=20" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", @@ -1201,6 +1215,44 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@paralleldrive/cuid2": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", @@ -1859,6 +1911,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -1886,6 +1945,13 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2055,6 +2121,19 @@ "node": ">=6.0.0" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -2089,6 +2168,19 @@ "balanced-match": "^1.0.0" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", @@ -2198,6 +2290,16 @@ "node": ">=6" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001793", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", @@ -2255,6 +2357,44 @@ "is-regex": "^1.0.3" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ci-info": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", @@ -2413,6 +2553,16 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/component-emitter": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", @@ -2509,6 +2659,19 @@ "node": ">= 8" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/cssstyle": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", @@ -2635,6 +2798,20 @@ "wrappy": "1" } }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, "node_modules/doctypes": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", @@ -2976,6 +3153,36 @@ "integrity": "sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw==", "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2989,6 +3196,16 @@ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "license": "MIT" }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -2999,6 +3216,37 @@ "bser": "2.1.1" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -3237,6 +3485,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -3469,6 +3730,19 @@ "dev": true, "license": "MIT" }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-core-module": { "version": "2.16.2", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", @@ -3494,6 +3768,16 @@ "object-assign": "^4.1.1" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -3514,6 +3798,29 @@ "node": ">=6" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -4294,6 +4601,16 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -4423,6 +4740,19 @@ "node": ">=6" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -4529,6 +4859,16 @@ "dev": true, "license": "MIT" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -4539,6 +4879,33 @@ "node": ">= 0.6" } }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -4613,6 +4980,37 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/napi-postinstall": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", @@ -4701,6 +5099,16 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -4952,6 +5360,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pino": { "version": "9.14.0", "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", @@ -5045,6 +5463,169 @@ "node": ">=8" } }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/pretty-format": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", @@ -5288,6 +5869,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/quick-format-unescaped": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", @@ -5334,6 +5936,42 @@ "dev": true, "license": "MIT" }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/real-require": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", @@ -5397,6 +6035,17 @@ "node": ">=8" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -5426,6 +6075,30 @@ "dev": true, "license": "MIT" }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-stable-stringify": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", @@ -5693,6 +6366,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", @@ -5915,6 +6598,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/superagent": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", @@ -6012,6 +6718,44 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -6073,6 +6817,29 @@ "node": "*" } }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/thread-stream": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", @@ -6082,6 +6849,23 @@ "real-require": "^0.2.0" } }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/tldts": { "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", @@ -6109,6 +6893,19 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -6150,6 +6947,13 @@ "node": ">=18" } }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -6322,6 +7126,13 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", diff --git a/package.json b/package.json index defdb08..cf4b810 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "scripts": { "start": "node app.js", "dev": "node --watch app.js", + "build:css": "tailwindcss -i ./src/tailwind.css -o ./static/styles.css --minify", + "watch:css": "tailwindcss -i ./src/tailwind.css -o ./static/styles.css --watch", "test": "jest" }, "dependencies": { @@ -27,6 +29,7 @@ "devDependencies": { "jest": "^30.4.2", "jest-environment-jsdom": "^30.4.1", - "supertest": "^7.0.0" + "supertest": "^7.0.0", + "tailwindcss": "^3.4.19" } } diff --git a/readme.md b/readme.md index 145603b..b677247 100644 --- a/readme.md +++ b/readme.md @@ -13,6 +13,12 @@ This project demonstrates key components of several essential Castle workflows. The browser SDK is also used to track page views (`Castle.page()`) and send an ad-hoc custom event (`Castle.custom()`). +## Screenshots + +| Home | Login | +| ---- | ----- | +| ![Home](docs/screenshots/home.png) | ![Login](docs/screenshots/login.png) | + ## Prerequisites You'll need a Castle tenant to run this app against. If you don't already have one, you can start a free trial at https://castle.io. @@ -66,6 +72,21 @@ For development with auto-reload: npm run dev ``` +## Styling (Tailwind CSS) + +The UI is styled with [Tailwind CSS](https://tailwindcss.com). The source lives in +`src/tailwind.css` (design tokens are configured in `tailwind.config.js`) and is +compiled to `static/styles.css`, which is committed so `npm start` and the Docker +image work without a build step. + +If you change the templates (`views/`) or `src/tailwind.css`, regenerate the +stylesheet: + +```bash +npm run build:css # one-off, minified build +npm run watch:css # rebuild on change during development +``` + ## Running the tests The app is covered by a Jest + Supertest suite (no network access or API secret diff --git a/src/tailwind.css b/src/tailwind.css new file mode 100644 index 0000000..960200d --- /dev/null +++ b/src/tailwind.css @@ -0,0 +1,238 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + body { + @apply min-h-screen bg-bg font-sans text-[15px] leading-relaxed text-ink antialiased; + background-image: radial-gradient( + 1200px 600px at 80% -10%, + rgba(124, 92, 255, 0.12), + transparent 60% + ); + } + + a { + @apply text-accent no-underline hover:underline; + } + + h1, + h2, + h3, + h4 { + @apply font-semibold leading-tight; + } + + p { + @apply mb-3; + } + + code { + @apply rounded border border-border bg-surface-2 px-1.5 py-0.5 font-mono text-[0.86em]; + } +} + +/* + * Component classes. Authored outside @layer so they are always emitted even + * when the selector only appears in JS-generated markup (verdict/chip/json). + */ + +.navbar { + @apply sticky top-0 z-50 flex flex-wrap items-center gap-6 border-b border-border px-6 py-3.5; + background: rgba(13, 16, 22, 0.8); + backdrop-filter: blur(10px); +} + +.brand { + @apply flex items-center gap-2 text-[1.05rem] font-bold text-ink hover:no-underline; +} + +.brand-dot { + @apply h-2.5 w-2.5 rounded-full bg-accent; + box-shadow: 0 0 12px #7c5cff; +} + +.nav-links { + @apply ml-auto flex flex-wrap items-center gap-5; +} + +.nav-links a { + @apply text-[0.92rem] text-muted hover:text-ink hover:no-underline; +} + +.tag { + @apply rounded-full border border-accent/40 bg-accent/10 px-2 py-0.5 text-xs font-semibold text-accent; +} + +.container-page { + @apply mx-auto max-w-[1120px] px-6 pb-16 pt-8; +} + +.card { + @apply rounded-xl border border-border bg-surface p-6 shadow-card; +} + +.eyebrow { + @apply mb-1.5 text-xs font-bold uppercase tracking-wider text-muted; +} + +.hero { + @apply px-4 py-12 text-center; +} + +.feature { + @apply block rounded-xl border border-border bg-surface p-5 text-left transition hover:-translate-y-0.5 hover:border-accent hover:no-underline; +} + +.feature h3 { + @apply mb-1 text-ink; +} + +.feature p { + @apply m-0 text-sm text-muted; +} + +.field { + @apply mb-3.5; +} + +.field label { + @apply mb-1.5 block text-[0.82rem] font-semibold text-muted; +} + +.input { + @apply w-full rounded-lg border border-border bg-bg-soft px-3 py-2.5 font-sans text-[0.95rem] text-ink transition; +} + +.input:focus { + @apply border-accent outline-none; + box-shadow: 0 0 0 3px rgba(124, 92, 255, 0.14); +} + +.checkbox { + @apply mt-1 flex items-center gap-2 text-[0.85rem] text-muted; +} + +.checkbox input { + @apply m-0 w-auto; +} + +.form-links { + @apply mt-4 flex justify-between gap-4 text-[0.85rem]; +} + +.btn { + @apply cursor-pointer rounded-lg border border-border bg-surface-2 px-4 py-2.5 font-sans text-[0.92rem] font-semibold text-ink transition hover:border-accent active:translate-y-px; +} + +.btn-primary { + @apply border-accent bg-accent text-white hover:bg-accent-hover; +} + +.btn-ghost { + @apply bg-transparent; +} + +.btn-row { + @apply mt-4 flex flex-wrap gap-2.5; +} + +.meta-list { + @apply m-0 list-none p-0; +} + +.meta-list li { + @apply flex justify-between gap-4 border-b border-border-soft py-2 text-[0.9rem] last:border-b-0; +} + +.meta-list .k { + @apply text-muted; +} + +.meta-list .v { + @apply break-all text-right font-mono text-ink; +} + +.result-block { + @apply mt-4; +} + +.result-block .label { + @apply mb-1.5 text-[0.78rem] font-bold uppercase tracking-wide text-muted; +} + +pre.json { + @apply m-0 overflow-auto rounded-lg border border-border bg-bg-soft p-4 font-mono text-[0.85rem] leading-normal; +} + +.json .k { + color: #7ee0c8; +} + +.json .s { + color: #ffd479; +} + +.json .n { + color: #84c1ff; +} + +.json .b { + @apply text-accent; +} + +.json .z { + @apply text-muted; +} + +.badge { + @apply inline-block rounded-full border border-border px-2 py-0.5 text-xs font-semibold; +} + +.badge.endpoint { + @apply border-accent/40 bg-accent/10 font-mono text-accent; +} + +.verdict { + @apply flex items-center gap-3.5 rounded-lg border border-border bg-surface-2 px-4 py-2.5; +} + +.verdict-action { + @apply rounded-full px-2.5 py-1 text-[0.85rem] font-bold uppercase tracking-wider; +} + +.verdict-score { + @apply text-[0.9rem] text-muted; +} + +.verdict-allow { + @apply border-success/40 bg-success/10; +} + +.verdict-allow .verdict-action { + @apply bg-success text-bg; +} + +.verdict-challenge { + @apply border-challenge/40 bg-challenge/10; +} + +.verdict-challenge .verdict-action { + @apply bg-challenge text-bg; +} + +.verdict-deny { + @apply border-danger/40 bg-danger/10; +} + +.verdict-deny .verdict-action { + @apply bg-danger text-white; +} + +.signals { + @apply mt-2.5 flex flex-wrap gap-1.5; +} + +.signals .chip { + @apply rounded-full border border-border bg-bg-soft px-2 py-0.5 font-mono text-xs text-muted; +} diff --git a/static/styles.css b/static/styles.css index 59d815e..886ebbe 100644 --- a/static/styles.css +++ b/static/styles.css @@ -1,506 +1 @@ -:root { - --bg: #0b0e14; - --bg-soft: #11151f; - --surface: #151a23; - --surface-2: #1b2230; - --border: #232b39; - --border-soft: #1c2330; - --text: #e6e9ef; - --text-muted: #9aa4b2; - --accent: #7c5cff; - --accent-soft: rgba(124, 92, 255, 0.14); - --success: #2ecc71; - --danger: #ff5c7c; - --radius: 14px; - --radius-sm: 9px; - --shadow: 0 10px 30px rgba(0, 0, 0, 0.35); - --font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; -} - -* { - box-sizing: border-box; -} - -html, -body { - margin: 0; - padding: 0; -} - -body { - background: - radial-gradient(1200px 600px at 80% -10%, rgba(124, 92, 255, 0.12), transparent 60%), - var(--bg); - color: var(--text); - font-family: var(--font); - font-size: 15px; - line-height: 1.6; - -webkit-font-smoothing: antialiased; - min-height: 100vh; -} - -a { - color: var(--accent); - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -h1, -h2, -h3, -h4 { - line-height: 1.25; - margin: 0 0 0.6rem; - font-weight: 650; -} - -p { - margin: 0 0 0.8rem; -} - -code { - font-family: var(--mono); - font-size: 0.86em; - background: var(--surface-2); - border: 1px solid var(--border); - border-radius: 6px; - padding: 0.1rem 0.36rem; -} - -/* ---- Navbar ---- */ -.navbar { - position: sticky; - top: 0; - z-index: 50; - display: flex; - align-items: center; - gap: 1.5rem; - padding: 0.85rem 1.5rem; - background: rgba(13, 16, 22, 0.8); - backdrop-filter: blur(10px); - border-bottom: 1px solid var(--border); -} - -.brand { - display: flex; - align-items: center; - gap: 0.55rem; - font-weight: 700; - font-size: 1.05rem; - color: var(--text); -} - -.brand:hover { - text-decoration: none; -} - -.brand .dot { - width: 11px; - height: 11px; - border-radius: 50%; - background: var(--accent); - box-shadow: 0 0 12px var(--accent); -} - -.nav-links { - display: flex; - align-items: center; - gap: 1.2rem; - margin-left: auto; - flex-wrap: wrap; -} - -.nav-links a { - color: var(--text-muted); - font-size: 0.92rem; -} - -.nav-links a:hover { - color: var(--text); - text-decoration: none; -} - -.tag { - font-size: 0.72rem; - font-weight: 600; - color: var(--accent); - background: var(--accent-soft); - border: 1px solid rgba(124, 92, 255, 0.35); - padding: 0.1rem 0.5rem; - border-radius: 999px; -} - -/* ---- Layout ---- */ -.container { - max-width: 1120px; - margin: 0 auto; - padding: 2rem 1.5rem 4rem; -} - -.grid { - display: grid; - grid-template-columns: 1.3fr 1fr; - gap: 1.5rem; - align-items: start; -} - -@media (max-width: 860px) { - .grid { - grid-template-columns: 1fr; - } -} - -.card { - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 1.5rem; - box-shadow: var(--shadow); -} - -.card + .card { - margin-top: 1.5rem; -} - -.card h2 { - font-size: 1.15rem; -} - -.eyebrow { - text-transform: uppercase; - letter-spacing: 0.08em; - font-size: 0.72rem; - font-weight: 700; - color: var(--text-muted); - margin-bottom: 0.4rem; -} - -.muted { - color: var(--text-muted); -} - -.lead { - font-size: 1.15rem; - color: var(--text); -} - -hr { - border: none; - border-top: 1px solid var(--border); - margin: 1.2rem 0; -} - -/* ---- Hero (home) ---- */ -.hero { - text-align: center; - padding: 3rem 1rem; -} - -.hero h1 { - font-size: 2.2rem; -} - -.feature-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); - gap: 1rem; - margin-top: 2rem; -} - -.feature { - display: block; - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 1.2rem; - text-align: left; - transition: border-color 0.15s ease, transform 0.15s ease; -} - -.feature:hover { - border-color: var(--accent); - transform: translateY(-2px); - text-decoration: none; -} - -.feature h3 { - color: var(--text); - margin-bottom: 0.3rem; -} - -.feature p { - color: var(--text-muted); - font-size: 0.9rem; - margin: 0; -} - -/* ---- Forms ---- */ -.field { - margin-bottom: 0.9rem; -} - -.field label { - display: block; - font-size: 0.82rem; - font-weight: 600; - color: var(--text-muted); - margin-bottom: 0.35rem; -} - -input[type="text"], -input[type="password"] { - width: 100%; - background: var(--bg-soft); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - color: var(--text); - font-family: inherit; - font-size: 0.95rem; - padding: 0.6rem 0.75rem; - transition: border-color 0.15s ease, box-shadow 0.15s ease; -} - -input:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-soft); -} - -.btn-row { - display: flex; - flex-wrap: wrap; - gap: 0.6rem; - margin-top: 1rem; -} - -button { - font-family: inherit; - font-size: 0.92rem; - font-weight: 600; - cursor: pointer; - border-radius: var(--radius-sm); - padding: 0.6rem 1.1rem; - border: 1px solid var(--border); - background: var(--surface-2); - color: var(--text); - transition: background 0.15s ease, border-color 0.15s ease, transform 0.05s ease; -} - -button:hover { - border-color: var(--accent); -} - -button:active { - transform: translateY(1px); -} - -button.primary { - background: var(--accent); - border-color: var(--accent); - color: #fff; -} - -button.primary:hover { - background: #6b4cf0; -} - -button.ghost { - background: transparent; -} - -/* ---- Metadata list ---- */ -.meta-list { - list-style: none; - margin: 0; - padding: 0; -} - -.meta-list li { - display: flex; - justify-content: space-between; - gap: 1rem; - padding: 0.5rem 0; - border-bottom: 1px solid var(--border-soft); - font-size: 0.9rem; -} - -.meta-list li:last-child { - border-bottom: none; -} - -.meta-list .k { - color: var(--text-muted); -} - -.meta-list .v { - font-family: var(--mono); - color: var(--text); - text-align: right; - word-break: break-all; -} - -/* ---- Results / JSON ---- */ -.result-block { - margin-top: 1rem; -} - -.result-block .label { - font-size: 0.78rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--text-muted); - margin-bottom: 0.4rem; -} - -pre.json { - background: var(--bg-soft); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - padding: 1rem; - overflow: auto; - font-family: var(--mono); - font-size: 0.85rem; - line-height: 1.5; - margin: 0; -} - -.json .k { - color: #7ee0c8; -} - -.json .s { - color: #ffd479; -} - -.json .n { - color: #84c1ff; -} - -.json .b { - color: var(--accent); -} - -.json .z { - color: var(--text-muted); -} - -/* ---- Badges ---- */ -.badge { - display: inline-block; - font-size: 0.75rem; - font-weight: 600; - padding: 0.15rem 0.55rem; - border-radius: 999px; - border: 1px solid var(--border); -} - -.badge.endpoint { - font-family: var(--mono); - color: var(--accent); - background: var(--accent-soft); - border-color: rgba(124, 92, 255, 0.35); -} - -/* ---- Verdict banner ---- */ -.verdict { - display: flex; - align-items: center; - gap: 0.9rem; - padding: 0.7rem 1rem; - border-radius: var(--radius-sm); - border: 1px solid var(--border); - background: var(--surface-2); -} - -.verdict-action { - text-transform: uppercase; - letter-spacing: 0.08em; - font-weight: 700; - font-size: 0.85rem; - padding: 0.2rem 0.7rem; - border-radius: 999px; -} - -.verdict-score { - color: var(--text-muted); - font-size: 0.9rem; -} - -.verdict-allow { - border-color: rgba(46, 204, 113, 0.4); - background: rgba(46, 204, 113, 0.1); -} - -.verdict-allow .verdict-action { - color: #0b0e14; - background: var(--success); -} - -.verdict-challenge { - border-color: rgba(255, 191, 71, 0.45); - background: rgba(255, 191, 71, 0.1); -} - -.verdict-challenge .verdict-action { - color: #0b0e14; - background: #ffbf47; -} - -.verdict-deny { - border-color: rgba(255, 92, 124, 0.45); - background: rgba(255, 92, 124, 0.1); -} - -.verdict-deny .verdict-action { - color: #fff; - background: var(--danger); -} - -.signals { - display: flex; - flex-wrap: wrap; - gap: 0.4rem; - margin-top: 0.7rem; -} - -.signals .chip { - font-family: var(--mono); - font-size: 0.75rem; - color: var(--text-muted); - background: var(--bg-soft); - border: 1px solid var(--border); - border-radius: 999px; - padding: 0.15rem 0.55rem; -} - -/* ---- Auxiliary form controls ---- */ -.checkbox { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.85rem; - color: var(--text-muted); - margin-top: 0.2rem; -} - -.checkbox input { - width: auto; - margin: 0; -} - -.form-links { - display: flex; - justify-content: space-between; - gap: 1rem; - margin-top: 1rem; - font-size: 0.85rem; -} - -.hidden { - display: none !important; -} +*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter,ui-sans-serif,system-ui,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}body{min-height:100vh;--tw-bg-opacity:1;background-color:rgb(11 14 20/var(--tw-bg-opacity,1));font-family:Inter,ui-sans-serif,system-ui,sans-serif;font-size:15px;line-height:1.625;color:rgb(230 233 239/var(--tw-text-opacity,1));-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background-image:radial-gradient(1200px 600px at 80% -10%,rgba(124,92,255,.12),transparent 60%)}a,body{--tw-text-opacity:1}a{color:rgb(124 92 255/var(--tw-text-opacity,1));text-decoration-line:none}a:hover{text-decoration-line:underline}h1,h2,h3,h4{font-weight:600;line-height:1.25}p{margin-bottom:.75rem}code{border-radius:.25rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(35 43 57/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(27 34 48/var(--tw-bg-opacity,1));padding:.125rem .375rem;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.86em}.my-5{margin-top:1.25rem;margin-bottom:1.25rem}.mt-3{margin-top:.75rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.grid{display:grid}.hidden{display:none}.list-decimal{list-style-type:decimal}.items-start{align-items:flex-start}.justify-center{justify-content:center}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.border-border{--tw-border-opacity:1;border-color:rgb(35 43 57/var(--tw-border-opacity,1))}.pl-5{padding-left:1.25rem}.text-\[1\.15rem\]{font-size:1.15rem}.text-\[2\.2rem\]{font-size:2.2rem}.text-muted{--tw-text-opacity:1;color:rgb(154 164 178/var(--tw-text-opacity,1))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.navbar{position:sticky;top:0;z-index:50;flex-wrap:wrap;gap:1.5rem;border-bottom-width:1px;--tw-border-opacity:1;border-color:rgb(35 43 57/var(--tw-border-opacity,1));padding:.875rem 1.5rem;background:rgba(13,16,22,.8);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.brand,.navbar{display:flex;align-items:center}.brand{gap:.5rem;font-size:1.05rem;font-weight:700;--tw-text-opacity:1;color:rgb(230 233 239/var(--tw-text-opacity,1))}.brand:hover{text-decoration-line:none}.brand-dot{height:.625rem;width:.625rem;border-radius:9999px;--tw-bg-opacity:1;background-color:rgb(124 92 255/var(--tw-bg-opacity,1));box-shadow:0 0 12px #7c5cff}.nav-links{margin-left:auto;display:flex;flex-wrap:wrap;align-items:center;gap:1.25rem}.nav-links a{font-size:.92rem;--tw-text-opacity:1;color:rgb(154 164 178/var(--tw-text-opacity,1))}.nav-links a:hover{--tw-text-opacity:1;color:rgb(230 233 239/var(--tw-text-opacity,1));text-decoration-line:none}.tag{border-radius:9999px;border-width:1px;border-color:rgba(124,92,255,.4);background-color:rgba(124,92,255,.1);padding:.125rem .5rem;font-size:.75rem;line-height:1rem;font-weight:600;--tw-text-opacity:1;color:rgb(124 92 255/var(--tw-text-opacity,1))}.container-page{margin-left:auto;margin-right:auto;max-width:1120px;padding:2rem 1.5rem 4rem}.card{border-radius:14px;border-width:1px;--tw-border-opacity:1;border-color:rgb(35 43 57/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(21 26 35/var(--tw-bg-opacity,1));padding:1.5rem;--tw-shadow:0 10px 30px rgba(0,0,0,.35);--tw-shadow-colored:0 10px 30px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.eyebrow{margin-bottom:.375rem;font-size:.75rem;line-height:1rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;--tw-text-opacity:1;color:rgb(154 164 178/var(--tw-text-opacity,1))}.hero{padding:3rem 1rem;text-align:center}.feature{display:block;border-radius:14px;border-width:1px;--tw-border-opacity:1;border-color:rgb(35 43 57/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(21 26 35/var(--tw-bg-opacity,1));padding:1.25rem;text-align:left;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.feature:hover{--tw-translate-y:-0.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));--tw-border-opacity:1;border-color:rgb(124 92 255/var(--tw-border-opacity,1));text-decoration-line:none}.feature h3{margin-bottom:.25rem;--tw-text-opacity:1;color:rgb(230 233 239/var(--tw-text-opacity,1))}.feature p{margin:0;font-size:.875rem;line-height:1.25rem;--tw-text-opacity:1;color:rgb(154 164 178/var(--tw-text-opacity,1))}.field{margin-bottom:.875rem}.field label{margin-bottom:.375rem;display:block;font-size:.82rem;font-weight:600;--tw-text-opacity:1;color:rgb(154 164 178/var(--tw-text-opacity,1))}.input{width:100%;border-radius:9px;border-width:1px;--tw-border-opacity:1;border-color:rgb(35 43 57/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(17 21 31/var(--tw-bg-opacity,1));padding:.625rem .75rem;font-family:Inter,ui-sans-serif,system-ui,sans-serif;font-size:.95rem;--tw-text-opacity:1;color:rgb(230 233 239/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.input:focus{--tw-border-opacity:1;border-color:rgb(124 92 255/var(--tw-border-opacity,1));outline:2px solid transparent;outline-offset:2px;box-shadow:0 0 0 3px rgba(124,92,255,.14)}.checkbox{margin-top:.25rem;display:flex;align-items:center;gap:.5rem;font-size:.85rem;--tw-text-opacity:1;color:rgb(154 164 178/var(--tw-text-opacity,1))}.checkbox input{margin:0;width:auto}.form-links{margin-top:1rem;display:flex;justify-content:space-between;gap:1rem;font-size:.85rem}.btn{cursor:pointer;border-radius:9px;border-width:1px;--tw-border-opacity:1;border-color:rgb(35 43 57/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(27 34 48/var(--tw-bg-opacity,1));padding:.625rem 1rem;font-family:Inter,ui-sans-serif,system-ui,sans-serif;font-size:.92rem;font-weight:600;--tw-text-opacity:1;color:rgb(230 233 239/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.btn:hover{--tw-border-opacity:1;border-color:rgb(124 92 255/var(--tw-border-opacity,1))}.btn:active{--tw-translate-y:1px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.btn-primary{--tw-border-opacity:1;border-color:rgb(124 92 255/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(124 92 255/var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.btn-primary:hover{--tw-bg-opacity:1;background-color:rgb(107 76 240/var(--tw-bg-opacity,1))}.btn-ghost{background-color:transparent}.btn-row{margin-top:1rem;display:flex;flex-wrap:wrap;gap:.625rem}.meta-list{margin:0;list-style-type:none;padding:0}.meta-list li{display:flex;justify-content:space-between;gap:1rem;border-bottom-width:1px;--tw-border-opacity:1;border-color:rgb(28 35 48/var(--tw-border-opacity,1));padding-top:.5rem;padding-bottom:.5rem;font-size:.9rem}.meta-list li:last-child{border-bottom-width:0}.meta-list .k{--tw-text-opacity:1;color:rgb(154 164 178/var(--tw-text-opacity,1))}.meta-list .v{word-break:break-all;text-align:right;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--tw-text-opacity:1;color:rgb(230 233 239/var(--tw-text-opacity,1))}.result-block{margin-top:1rem}.result-block .label{margin-bottom:.375rem;font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.025em;--tw-text-opacity:1;color:rgb(154 164 178/var(--tw-text-opacity,1))}pre.json{margin:0;overflow:auto;border-radius:9px;border-width:1px;--tw-border-opacity:1;border-color:rgb(35 43 57/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(17 21 31/var(--tw-bg-opacity,1));padding:1rem;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.85rem;line-height:1.5}.json .k{color:#7ee0c8}.json .s{color:#ffd479}.json .n{color:#84c1ff}.json .b{color:rgb(124 92 255/var(--tw-text-opacity,1))}.json .b,.json .z{--tw-text-opacity:1}.json .z{color:rgb(154 164 178/var(--tw-text-opacity,1))}.badge{display:inline-block;border-radius:9999px;border-width:1px;--tw-border-opacity:1;border-color:rgb(35 43 57/var(--tw-border-opacity,1));padding:.125rem .5rem;font-size:.75rem;line-height:1rem;font-weight:600}.badge.endpoint{border-color:rgba(124,92,255,.4);background-color:rgba(124,92,255,.1);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--tw-text-opacity:1;color:rgb(124 92 255/var(--tw-text-opacity,1))}.verdict{display:flex;align-items:center;gap:.875rem;border-radius:9px;border-width:1px;--tw-border-opacity:1;border-color:rgb(35 43 57/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(27 34 48/var(--tw-bg-opacity,1));padding:.625rem 1rem}.verdict-action{border-radius:9999px;padding:.25rem .625rem;font-size:.85rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em}.verdict-score{font-size:.9rem;--tw-text-opacity:1;color:rgb(154 164 178/var(--tw-text-opacity,1))}.verdict-allow{border-color:rgba(46,204,113,.4);background-color:rgba(46,204,113,.1)}.verdict-allow .verdict-action{--tw-bg-opacity:1;background-color:rgb(46 204 113/var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(11 14 20/var(--tw-text-opacity,1))}.verdict-challenge{border-color:rgba(255,191,71,.4);background-color:rgba(255,191,71,.1)}.verdict-challenge .verdict-action{--tw-bg-opacity:1;background-color:rgb(255 191 71/var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(11 14 20/var(--tw-text-opacity,1))}.verdict-deny{border-color:rgba(255,92,124,.4);background-color:rgba(255,92,124,.1)}.verdict-deny .verdict-action{--tw-bg-opacity:1;background-color:rgb(255 92 124/var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.signals{margin-top:.625rem;display:flex;flex-wrap:wrap;gap:.375rem}.signals .chip{border-radius:9999px;border-width:1px;--tw-border-opacity:1;border-color:rgb(35 43 57/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(17 21 31/var(--tw-bg-opacity,1));padding:.125rem .5rem;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.75rem;line-height:1rem;--tw-text-opacity:1;color:rgb(154 164 178/var(--tw-text-opacity,1))}@media (min-width:768px){.md\:grid-cols-\[1\.3fr_1fr\]{grid-template-columns:1.3fr 1fr}} \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..ec5cf91 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,37 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + // Scan the templates and the browser helpers (which build result markup) so + // every utility used in markup or JS strings is generated. + content: ['./views/**/*.pug', './static/**/*.js'], + theme: { + extend: { + colors: { + bg: '#0b0e14', + 'bg-soft': '#11151f', + surface: '#151a23', + 'surface-2': '#1b2230', + border: '#232b39', + 'border-soft': '#1c2330', + ink: '#e6e9ef', + muted: '#9aa4b2', + accent: '#7c5cff', + 'accent-hover': '#6b4cf0', + success: '#2ecc71', + challenge: '#ffbf47', + danger: '#ff5c7c', + }, + fontFamily: { + sans: ['Inter', 'ui-sans-serif', 'system-ui', 'sans-serif'], + mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Consolas', 'monospace'], + }, + borderRadius: { + xl: '14px', + lg: '9px', + }, + boxShadow: { + card: '0 10px 30px rgba(0, 0, 0, 0.35)', + }, + }, + }, + plugins: [], +}; diff --git a/views/base.pug b/views/base.pug index ba8dbc6..82ab8d6 100644 --- a/views/base.pug +++ b/views/base.pug @@ -24,17 +24,17 @@ html(lang="en") body nav.navbar a.brand(href="/") - span.dot + span.brand-dot | Castle | - span.muted(style="font-weight:400") demo + span.text-muted(style="font-weight:400") demo .nav-links each demo in demo_list a(href="/" + demo.url)= demo.friendly_name a(href="https://github.com/castle/castle-node-example" target="_blank" rel="noopener") GitHub a(href="https://docs.castle.io" target="_blank" rel="noopener") Docs - main.container + main.container-page block content block scripts diff --git a/views/demo.pug b/views/demo.pug index c844b94..ba27c72 100644 --- a/views/demo.pug +++ b/views/demo.pug @@ -4,32 +4,32 @@ block content if home section.hero span.tag castle node sdk 3.0 - h1 Castle workflows demo - p.lead.muted A small Express app showing how to integrate the Castle Node SDK. + h1(class="text-[2.2rem] mt-3") Castle workflows demo + p(class="text-[1.15rem] text-muted") A small Express app showing how to integrate the Castle Node SDK. - .feature-grid + .grid.gap-4.mt-8(style="grid-template-columns:repeat(auto-fit,minmax(210px,1fr));") each demo in demo_list a.feature(href="/" + demo.url) h3= demo.friendly_name p= demo.blurb else - .grid + .grid.gap-6.items-start(class="md:grid-cols-[1.3fr_1fr]") div .card .eyebrow workflow - h2= friendly_name + h2(class="text-[1.15rem]")= friendly_name #ui block ui - #desc.muted(style="margin-top:1.2rem;") + #desc.text-muted(style="margin-top:1.2rem;") block desc if wsd - hr + hr.my-5.border-border a(href=wsd target="_blank" rel="noopener") View the web sequence diagram → - #results-card.card.hidden + #results-card.card.hidden.mt-6 .eyebrow result #results diff --git a/views/error.pug b/views/error.pug index 5ac361c..59d36af 100644 --- a/views/error.pug +++ b/views/error.pug @@ -3,7 +3,7 @@ extends base block content section.hero span.tag 404 - h1 Page not found - p.lead.muted Sorry, we couldn't load that URL. - .btn-row(style="justify-content:center;") - a.feature(href="/" style="display:inline-block;") Back to demos + h1(class="text-[2.2rem] mt-3") Page not found + p(class="text-[1.15rem] text-muted") Sorry, we couldn't load that URL. + .btn-row.justify-center + a.feature.inline-block(href="/") Back to demos diff --git a/views/events.pug b/views/events.pug index c978bbe..05aa2e2 100644 --- a/views/events.pug +++ b/views/events.pug @@ -2,21 +2,21 @@ extends demo block ui .btn-row(style="margin-top:0;") - button(onclick="eventsSchema()") Fetch schema + button.btn(onclick="eventsSchema()") Fetch schema - hr + hr.my-5.border-border .field label(for="field") field - input(type="text" id="field" value="name") + input.input(type="text" id="field" value="name") .field label(for="op") op - input(type="text" id="op" value="$eq") + input.input(type="text" id="op" value="$eq") .field label(for="value") value - input(type="text" id="value" value="$login") + input.input(type="text" id="value" value="$login") .btn-row - button.primary(onclick="queryEvents()") Query events + button.btn.btn-primary(onclick="queryEvents()") Query events block desc p The diff --git a/views/lists.pug b/views/lists.pug index 0ebfe90..3dd3a3b 100644 --- a/views/lists.pug +++ b/views/lists.pug @@ -3,15 +3,15 @@ extends demo block ui .field label(for="name") name - input(type="text" id="name" value="demo-blocklist") + input.input(type="text" id="name" value="demo-blocklist") .field label(for="color") color - input(type="text" id="color" value="$red") + input.input(type="text" id="color" value="$red") .field label(for="primary_field") primary field - input(type="text" id="primary_field" value="user.email") + input.input(type="text" id="primary_field" value="user.email") .btn-row - button.primary(onclick="createList()") Create list + button.btn.btn-primary(onclick="createList()") Create list block desc p The diff --git a/views/login.pug b/views/login.pug index 432df16..db0bfb1 100644 --- a/views/login.pug +++ b/views/login.pug @@ -2,29 +2,29 @@ extends demo block ui .btn-row(style="margin-top:0;") - button.ghost(onclick="fillForm('valid')") valid user + pw - button.ghost(onclick="fillForm('bad_pw')") valid user, bad pw - button.ghost(onclick="fillForm('bad_user')") invalid username + button.btn.btn-ghost(onclick="fillForm('valid')") valid user + pw + button.btn.btn-ghost(onclick="fillForm('bad_pw')") valid user, bad pw + button.btn.btn-ghost(onclick="fillForm('bad_user')") invalid username div(style="margin-top:1.2rem;") .field label(for="email") email - input(type="text" id="email" autocomplete="off") + input.input(type="text" id="email" autocomplete="off") .field label(for="password") password - input(type="password" id="password") + input.input(type="password" id="password") label.checkbox input(type="checkbox" id="remember") | Remember me .btn-row - button.primary(onclick="login()") Log in + button.btn.btn-primary(onclick="login()") Log in .form-links a(href="/password_reset") Forgot your password? a(href="#" onclick="trackCustomEvent('$custom'); return false;") Send a custom event block desc p A login attempt has three common outcomes, and each maps to a different Castle endpoint: - ol + ol.list-decimal.pl-5.space-y-1 li strong valid username + valid password | → diff --git a/views/password_reset.pug b/views/password_reset.pug index b1784e5..f731ab9 100644 --- a/views/password_reset.pug +++ b/views/password_reset.pug @@ -3,12 +3,12 @@ extends demo block ui .field label email - input(type="text" value=username disabled) + input.input(type="text" value=username disabled) .field label(for="password") new password - input(type="password" id="password") + input.input(type="password" id="password") .btn-row - button.primary(onclick="resetPassword()") Submit + button.btn.btn-primary(onclick="resetPassword()") Submit block desc p This demo records the password-reset event with the non-blocking diff --git a/views/privacy.pug b/views/privacy.pug index ece7fe6..9fca257 100644 --- a/views/privacy.pug +++ b/views/privacy.pug @@ -3,13 +3,13 @@ extends demo block ui .field label(for="identifier") identifier - input(type="text" id="identifier" value=valid_username) + input.input(type="text" id="identifier" value=valid_username) .field label(for="identifier_type") identifier type - input(type="text" id="identifier_type" value="$email") + input.input(type="text" id="identifier_type" value="$email") .btn-row - button.primary(onclick="privacy('request')") Request user data - button(onclick="privacy('delete')") Delete user data + button.btn.btn-primary(onclick="privacy('request')") Request user data + button.btn(onclick="privacy('delete')") Delete user data block desc p The