diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e1a1ff2..2cfd88a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,10 +8,12 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@tailwindcss/vite": "^4.1.16", "openapi-fetch": "^0.13.5", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-router": "^7.4.1" + "react-router": "^7.4.1", + "tailwindcss": "^4.1.16" }, "devDependencies": { "@eslint/js": "^9.21.0", @@ -62,7 +64,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -79,7 +80,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -96,7 +96,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -113,7 +112,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -130,7 +128,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -147,7 +144,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -164,7 +160,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -181,7 +176,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -198,7 +192,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -215,7 +208,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -232,7 +224,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -249,7 +240,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -266,7 +256,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -283,7 +272,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -300,7 +288,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -317,7 +304,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -334,7 +320,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -351,7 +336,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -368,7 +352,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -385,7 +368,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -402,7 +384,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -419,7 +400,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -436,7 +416,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -453,7 +432,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -470,7 +448,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -700,6 +677,51 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -821,7 +843,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -835,7 +856,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -849,7 +869,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -863,7 +882,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -877,7 +895,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -891,7 +908,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -905,7 +921,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -919,7 +934,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -933,7 +947,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -947,7 +960,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -961,7 +973,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -975,7 +986,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -989,7 +999,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1003,7 +1012,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1017,7 +1025,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1031,7 +1038,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1045,7 +1051,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1059,7 +1064,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1073,7 +1077,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1087,7 +1090,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1320,11 +1322,321 @@ "@swc/counter": "^0.1.3" } }, + "node_modules/@tailwindcss/node": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz", + "integrity": "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.19", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.16" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz", + "integrity": "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.16", + "@tailwindcss/oxide-darwin-arm64": "4.1.16", + "@tailwindcss/oxide-darwin-x64": "4.1.16", + "@tailwindcss/oxide-freebsd-x64": "4.1.16", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.16", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.16", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.16", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.16", + "@tailwindcss/oxide-linux-x64-musl": "4.1.16", + "@tailwindcss/oxide-wasm32-wasi": "4.1.16", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.16", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.16" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.16.tgz", + "integrity": "sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.16.tgz", + "integrity": "sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.16.tgz", + "integrity": "sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.16.tgz", + "integrity": "sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.16.tgz", + "integrity": "sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.16.tgz", + "integrity": "sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.16.tgz", + "integrity": "sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.16.tgz", + "integrity": "sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.16.tgz", + "integrity": "sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.16.tgz", + "integrity": "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.7", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.0.7", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "inBundle": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.16.tgz", + "integrity": "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.16.tgz", + "integrity": "sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.16.tgz", + "integrity": "sha512-bbguNBcDxsRmi9nnlWJxhfDWamY3lmcyACHcdO1crxfzuLpOhHLLtEIN/nCbbAtj5rchUgQD17QVAKi1f7IsKg==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.16", + "@tailwindcss/oxide": "4.1.16", + "tailwindcss": "4.1.16" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { @@ -1811,11 +2123,32 @@ "dev": true, "license": "MIT" }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/esbuild": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -2185,7 +2518,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -2222,6 +2554,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2343,6 +2681,15 @@ "dev": true, "license": "ISC" }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-levenshtein": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", @@ -2418,6 +2765,255 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2441,6 +3037,15 @@ "dev": true, "license": "MIT" }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2489,7 +3094,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -2665,7 +3269,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -2695,7 +3298,6 @@ "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "dev": true, "funding": [ { "type": "opencollective", @@ -2855,7 +3457,6 @@ "version": "4.38.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.38.0.tgz", "integrity": "sha512-5SsIRtJy9bf1ErAOiFMFzl64Ex9X5V7bnJ+WlFMb+zmP459OSWCEG7b0ERZ+PEU7xPt4OG3RHbrp1LJlXxYTrw==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.7" @@ -2967,7 +3568,6 @@ "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" @@ -2999,11 +3599,29 @@ "node": ">=8" } }, + "node_modules/tailwindcss": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz", + "integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.4.4", @@ -3020,7 +3638,6 @@ "version": "6.4.5", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", "integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==", - "dev": true, "license": "MIT", "peerDependencies": { "picomatch": "^3 || ^4" @@ -3035,7 +3652,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -3154,7 +3770,6 @@ "version": "6.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", - "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", @@ -3229,7 +3844,6 @@ "version": "6.4.5", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", "integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==", - "dev": true, "license": "MIT", "peerDependencies": { "picomatch": "^3 || ^4" @@ -3244,7 +3858,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" diff --git a/frontend/package.json b/frontend/package.json index 6306aff..2a502ae 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,10 +12,12 @@ "preview": "vite preview" }, "dependencies": { + "@tailwindcss/vite": "^4.1.16", "openapi-fetch": "^0.13.5", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-router": "^7.4.1" + "react-router": "^7.4.1", + "tailwindcss": "^4.1.16" }, "devDependencies": { "@eslint/js": "^9.21.0", diff --git a/frontend/src/Studentportalen.tsx b/frontend/src/Studentportalen.tsx index 90b9f40..11ad6ed 100644 --- a/frontend/src/Studentportalen.tsx +++ b/frontend/src/Studentportalen.tsx @@ -1,5 +1,5 @@ import { BrowserRouter, Route, Routes } from "react-router"; -import Home from "./studentportalen/Home.tsx"; +import ComponentLibrary from "./studentportalen/ComponentLibrary.tsx"; import Layout from "./studentportalen/Layout.tsx"; export default function Studentportalen() { @@ -8,7 +8,8 @@ export default function Studentportalen() { }> - } /> + } /> + } /> diff --git a/frontend/src/assets/TheSansB-W2ExtraLight.woff2 b/frontend/src/assets/TheSansB-W2ExtraLight.woff2 new file mode 100644 index 0000000..7d57f87 Binary files /dev/null and b/frontend/src/assets/TheSansB-W2ExtraLight.woff2 differ diff --git a/frontend/src/assets/TheSansB-W2ExtraLightItalic.woff2 b/frontend/src/assets/TheSansB-W2ExtraLightItalic.woff2 new file mode 100644 index 0000000..45b8404 Binary files /dev/null and b/frontend/src/assets/TheSansB-W2ExtraLightItalic.woff2 differ diff --git a/frontend/src/assets/TheSansB-W3Light.woff2 b/frontend/src/assets/TheSansB-W3Light.woff2 new file mode 100644 index 0000000..a4e1967 Binary files /dev/null and b/frontend/src/assets/TheSansB-W3Light.woff2 differ diff --git a/frontend/src/assets/TheSansB-W3LightItalic.woff2 b/frontend/src/assets/TheSansB-W3LightItalic.woff2 new file mode 100644 index 0000000..080800d Binary files /dev/null and b/frontend/src/assets/TheSansB-W3LightItalic.woff2 differ diff --git a/frontend/src/assets/TheSansB-W4SemiLight.woff2 b/frontend/src/assets/TheSansB-W4SemiLight.woff2 new file mode 100644 index 0000000..49e1f1f Binary files /dev/null and b/frontend/src/assets/TheSansB-W4SemiLight.woff2 differ diff --git a/frontend/src/assets/TheSansB-W4SemiLightItalic.woff2 b/frontend/src/assets/TheSansB-W4SemiLightItalic.woff2 new file mode 100644 index 0000000..e86a9d8 Binary files /dev/null and b/frontend/src/assets/TheSansB-W4SemiLightItalic.woff2 differ diff --git a/frontend/src/assets/TheSansB-W5Plain.woff2 b/frontend/src/assets/TheSansB-W5Plain.woff2 new file mode 100644 index 0000000..3e6c31c Binary files /dev/null and b/frontend/src/assets/TheSansB-W5Plain.woff2 differ diff --git a/frontend/src/assets/TheSansB-W5PlainItalic.woff2 b/frontend/src/assets/TheSansB-W5PlainItalic.woff2 new file mode 100644 index 0000000..aa2cdad Binary files /dev/null and b/frontend/src/assets/TheSansB-W5PlainItalic.woff2 differ diff --git a/frontend/src/assets/TheSansB-W6SemiBold.woff2 b/frontend/src/assets/TheSansB-W6SemiBold.woff2 new file mode 100644 index 0000000..b8e2df7 Binary files /dev/null and b/frontend/src/assets/TheSansB-W6SemiBold.woff2 differ diff --git a/frontend/src/assets/TheSansB-W6SemiBoldItalic.woff2 b/frontend/src/assets/TheSansB-W6SemiBoldItalic.woff2 new file mode 100644 index 0000000..807948f Binary files /dev/null and b/frontend/src/assets/TheSansB-W6SemiBoldItalic.woff2 differ diff --git a/frontend/src/assets/TheSansB-W7Bold.woff2 b/frontend/src/assets/TheSansB-W7Bold.woff2 new file mode 100644 index 0000000..aba7374 Binary files /dev/null and b/frontend/src/assets/TheSansB-W7Bold.woff2 differ diff --git a/frontend/src/assets/TheSansB-W7BoldItalic.woff2 b/frontend/src/assets/TheSansB-W7BoldItalic.woff2 new file mode 100644 index 0000000..1954c0e Binary files /dev/null and b/frontend/src/assets/TheSansB-W7BoldItalic.woff2 differ diff --git a/frontend/src/assets/TheSansB-W8ExtraBold.woff2 b/frontend/src/assets/TheSansB-W8ExtraBold.woff2 new file mode 100644 index 0000000..701b7ad Binary files /dev/null and b/frontend/src/assets/TheSansB-W8ExtraBold.woff2 differ diff --git a/frontend/src/assets/TheSansB-W8ExtraBoldItalic.woff2 b/frontend/src/assets/TheSansB-W8ExtraBoldItalic.woff2 new file mode 100644 index 0000000..08b5178 Binary files /dev/null and b/frontend/src/assets/TheSansB-W8ExtraBoldItalic.woff2 differ diff --git a/frontend/src/assets/TheSansB-W9Black.woff2 b/frontend/src/assets/TheSansB-W9Black.woff2 new file mode 100644 index 0000000..3296bcc Binary files /dev/null and b/frontend/src/assets/TheSansB-W9Black.woff2 differ diff --git a/frontend/src/assets/TheSansB-W9BlackItalic.woff2 b/frontend/src/assets/TheSansB-W9BlackItalic.woff2 new file mode 100644 index 0000000..2bc7573 Binary files /dev/null and b/frontend/src/assets/TheSansB-W9BlackItalic.woff2 differ diff --git a/frontend/src/components/Button/Button.tsx b/frontend/src/components/Button/Button.tsx new file mode 100644 index 0000000..5ab2de4 --- /dev/null +++ b/frontend/src/components/Button/Button.tsx @@ -0,0 +1,57 @@ +import type { ButtonHTMLAttributes, ReactNode } from "react"; + +export type ButtonVariant = "primary" | "secondary" | "red" | "green"; +export type ButtonSize = "sm" | "md" | "lg"; + +export interface ButtonProps extends ButtonHTMLAttributes { + variant?: ButtonVariant; + size?: ButtonSize; + children: ReactNode; +} + +const variantClasses: Record = { + primary: + "bg-primary text-base-canvas border border-primary hover:bg-secondary hover:text-base-canvas hover:border hover:border-primary focus-visible:bg-base-canvas focus-visible:text-base-ink-strong focus-visible:border focus-visible:border-primary focus-visible:outline focus-visible:outline-[length:var(--border-width-lg)] focus-visible:outline-sky-100", + secondary: + "bg-base-canvas text-base-ink-strong border-solid [border-width:var(--border-width-sm)] border-base-ink-soft hover:bg-base-canvas hover:text-base-ink-strong hover:border-base-ink-medium focus-visible:bg-base-canvas focus-visible:text-base-ink-strong focus-visible:border-primary focus-visible:outline focus-visible:outline-[length:var(--border-width-lg)] focus-visible:outline-sky-100", + red: "bg-other-red-100 text-su-white focus-visible:bg-base-canvas focus-visible:text-base-ink-strong focus-visible:border-primary focus-visible:border focus-visible:outline focus-visible:outline-[length:var(--border-width-lg)] focus-visible:outline-sky-100", + green: + "bg-other-green text-su-white focus-visible:bg-base-canvas focus-visible:text-base-ink-strong focus-visible:border-primary focus-visible:border focus-visible:outline focus-visible:outline-[length:var(--border-width-lg)] focus-visible:outline-sky-100", +}; + +const sizeClasses: Record = { + sm: "h-(--control-height-sm) min-w-(--button-min-width-sm) px-(--button-padding-x-sm) body-bold-md rounded-(--border-radius-sm)", + md: "h-(--control-height-md) min-w-(--button-min-width-md) px-(--button-padding-x-md) body-bold-md rounded-(--border-radius-sm)", + lg: "h-(--control-height-lg) min-w-(--button-min-width-lg) px-(--button-padding-x-lg) body-bold-lg rounded-(--border-radius-md)", +}; + +const textPaddingClasses: Record = { + sm: "px-(--button-text-padding-x-sm)", + md: "px-(--button-text-padding-x-md)", + lg: "px-(--button-text-padding-x-lg)", +}; + +export default function Button({ + variant = "primary", + size = "md", + className = "", + children, + ...props +}: ButtonProps) { + const baseClasses = "inline-flex items-center justify-center cursor-pointer"; + + const classes = [ + baseClasses, + variantClasses[variant], + sizeClasses[size], + className, + ] + .filter(Boolean) + .join(" "); + + return ( + + ); +} diff --git a/frontend/src/components/Combobox/Combobox.tsx b/frontend/src/components/Combobox/Combobox.tsx new file mode 100644 index 0000000..897f454 --- /dev/null +++ b/frontend/src/components/Combobox/Combobox.tsx @@ -0,0 +1,243 @@ +import { useState, useRef, useEffect } from 'react'; +import TextInput from '../TextInput/TextInput'; +import SearchResultList, { type SearchResultOption } from '../SearchResultList/SearchResultList'; + +export type ComboboxOption = SearchResultOption; + +export type ComboboxSize = 'sm' | 'md' | 'lg'; + +export interface ComboboxProps { + options: ComboboxOption[]; + placeholder?: string; + label?: string; + size?: ComboboxSize; + fullWidth?: boolean; + customWidth?: string; + dropdownHeight?: number; + noResultsText?: string; + multiple?: boolean; + value?: string | string[]; + onChange?: (value: string | string[]) => void; + /** Called when search term changes. When provided, local filtering is disabled (assumes API filtering). */ + onSearchChange?: (term: string) => void; +} + +const widthClasses: Record = { + sm: 'w-(--text-input-default-width-md)', + md: 'w-(--text-input-default-width-md)', + lg: 'w-(--text-input-default-width-lg)', +}; + +const dropdownWrapperClasses = 'absolute top-full left-0 z-50 w-full mt-(--spacing-sm)'; + +function SearchIcon({ style }: { style?: React.CSSProperties }) { + return ( + + + + + ); +} + +function useClickOutside(ref: React.RefObject, onClickOutside: () => void) { + useEffect(() => { + const handleClick = (event: MouseEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + onClickOutside(); + } + }; + + document.addEventListener('mousedown', handleClick); + return () => document.removeEventListener('mousedown', handleClick); + }, [ref, onClickOutside]); +} + +function useScrollIntoView(index: number, refs: React.RefObject<(HTMLDivElement | null)[]>) { + useEffect(() => { + if (index >= 0 && refs.current[index]) { + refs.current[index]?.scrollIntoView({ block: 'nearest' }); + } + }, [index, refs]); +} + +function filterOptions(options: ComboboxOption[], searchTerm: string): ComboboxOption[] { + const term = searchTerm.toLowerCase(); + return options.filter( + (opt) => opt.label.toLowerCase().includes(term) || opt.subtitle?.toLowerCase().includes(term), + ); +} + +function getNextIndex(currentIndex: number, direction: 'up' | 'down', maxIndex: number): number { + const next = currentIndex + (direction === 'down' ? 1 : -1); + return Math.max(0, Math.min(next, maxIndex)); +} + +function getDisplayValue( + options: ComboboxOption[], + selectedValues: string[], + multiple: boolean, + searchTerm: string, +): string { + if (searchTerm) return searchTerm; + + if (multiple) { + return ''; + } + + if (selectedValues.length === 0) return ''; + + return options.find((opt) => opt.value === selectedValues[0])?.label || ''; +} + +export default function Combobox({ + options, + placeholder = 'Search...', + label, + size = 'md', + fullWidth = false, + customWidth, + dropdownHeight = 300, + noResultsText = 'No results found', + multiple = false, + value, + onChange, + onSearchChange, +}: ComboboxProps) { + // Normalize value to array for internal use + const selectedValues: string[] = value === undefined ? [] : Array.isArray(value) ? value : [value]; + + // State + const [isOpen, setIsOpen] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const [focusedIndex, setFocusedIndex] = useState(-1); + + const containerRef = useRef(null); + const itemRefs = useRef<(HTMLDivElement | null)[]>([]); + + // Derived state - skip local filtering when onSearchChange is provided (API handles filtering) + const filteredOptions = onSearchChange ? options : filterOptions(options, searchTerm); + const displayValue = getDisplayValue(options, selectedValues, multiple, searchTerm); + + const closeDropdown = () => setIsOpen(false); + + // Hooks + useClickOutside(containerRef, closeDropdown); + useScrollIntoView(focusedIndex, itemRefs); + + // Event handlers + const handleInputChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setSearchTerm(value); + onSearchChange?.(value); + + if (value === '') { + // Clear selection when user empties the field in single-select mode + if (!multiple && selectedValues.length > 0) { + onChange?.(''); + } + closeDropdown(); + } else { + setIsOpen(true); + setFocusedIndex(-1); + } + }; + + const handleSelect = (option: ComboboxOption) => { + if (multiple) { + // Toggle selection in multi-select mode + const newValues = selectedValues.includes(option.value) + ? selectedValues.filter((v) => v !== option.value) + : [...selectedValues, option.value]; + onChange?.(newValues); + } else { + // Replace selection in single-select mode + onChange?.(option.value); + } + setSearchTerm(''); // Clear search so selected label shows + closeDropdown(); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + // Open dropdown on arrow keys when closed (only if we have options to show) + if (!isOpen) { + if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { + if (filteredOptions.length > 0) { + setIsOpen(true); + } + e.preventDefault(); + } + return; + } + + // Handle navigation when open + switch (e.key) { + case 'ArrowDown': + e.preventDefault(); + setFocusedIndex((prev) => getNextIndex(prev, 'down', filteredOptions.length - 1)); + break; + + case 'ArrowUp': + e.preventDefault(); + setFocusedIndex((prev) => getNextIndex(prev, 'up', filteredOptions.length - 1)); + break; + + case 'Enter': + e.preventDefault(); + if (focusedIndex >= 0 && focusedIndex < filteredOptions.length) { + handleSelect(filteredOptions[focusedIndex]); + } + break; + + case 'Escape': + case 'Tab': + closeDropdown(); + break; + } + }; + + const containerClasses = fullWidth + ? 'relative w-full' + : customWidth + ? 'relative' + : `relative ${widthClasses[size]}`; + const containerStyle = customWidth ? { width: customWidth } : undefined; + + return ( +
+ } + /> + + {isOpen && ( +
+ +
+ )} +
+ ); +} diff --git a/frontend/src/components/ListCard/ListCard.tsx b/frontend/src/components/ListCard/ListCard.tsx new file mode 100644 index 0000000..12f4da3 --- /dev/null +++ b/frontend/src/components/ListCard/ListCard.tsx @@ -0,0 +1,66 @@ +import type { HTMLAttributes, CSSProperties } from 'react'; + +export interface ListCardProps extends Omit, 'title'> { + title: string; + subtitle?: string; + onRemove?: () => void; +} + +const baseClasses = [ + 'px-(--padding-md) py-(--padding-md)', + 'bg-sky-35 border border-sky-100 rounded-(--border-radius-md)', + 'flex items-center justify-between', + 'group text-base-ink-strong hover:bg-sky-70 hover:text-base-ink-max focus-visible:text-base-ink-max', + 'focus-visible:border-primary focus-visible:border-[length:var(--border-width-sm)] focus-visible:outline focus-visible:outline-sky-100 focus-visible:outline-[length:var(--border-width-lg)]', +].join(' '); + +const iconStyles: CSSProperties = { + width: 'var(--font-size-body-md)', + height: 'var(--font-size-body-md)', +}; + +function RemoveIcon({ style }: { style?: CSSProperties }) { + return ( + + + + + ); +} + +export default function ListCard({ title, subtitle = '', onRemove, className = '', ...props }: ListCardProps) { + const classes = [baseClasses, className].filter(Boolean).join(' '); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && onRemove) { + onRemove(); + } + }; + + return ( +
+
+
{title}
+ {subtitle &&
{subtitle}
} +
+ {onRemove && ( + + )} +
+ ); +} diff --git a/frontend/src/components/ListItem/ListItem.tsx b/frontend/src/components/ListItem/ListItem.tsx new file mode 100644 index 0000000..8c4f92d --- /dev/null +++ b/frontend/src/components/ListItem/ListItem.tsx @@ -0,0 +1,72 @@ +import type { HTMLAttributes, CSSProperties } from 'react'; + +export interface ListItemProps extends HTMLAttributes { + title: string; + subtitle?: string; + selected?: boolean; + focused?: boolean; +} + +const iconStyles: CSSProperties = { + width: 'var(--font-size-body-md)', + height: 'var(--font-size-body-md)', + marginLeft: 'var(--spacing-sm)', +}; + +function CheckmarkIcon() { + return ( + + + + ); +} + +const baseClasses = 'w-full px-(--padding-md) py-(--padding-md) cursor-pointer flex items-center justify-between'; + +const defaultStateClasses = + 'bg-base-canvas text-base-ink-strong hover:bg-sky-100 hover:text-base-ink-max focus-visible:bg-sky-100 focus-visible:text-primary focus-visible:outline-none'; + +const selectedStateClasses = + 'bg-base-canvas text-base-ink-placeholder hover:bg-sky-100 hover:text-base-ink-max focus-visible:bg-sky-100 focus-visible:text-primary focus-visible:outline-none'; + +const focusedStateClasses = 'bg-sky-100 text-base-ink-max'; + +export default function ListItem({ + title, + subtitle, + selected = false, + focused = false, + className = '', + ...props +}: ListItemProps) { + const getStateClasses = () => { + if (selected && focused) return focusedStateClasses; + if (selected) return selectedStateClasses; + if (focused) return focusedStateClasses; + return defaultStateClasses; + }; + + const classes = [baseClasses, getStateClasses(), className].filter(Boolean).join(' '); + + return ( +
+
+
{title}
+ {subtitle &&
{subtitle}
} +
+ {selected && ( +
+ +
+ )} +
+ ); +} diff --git a/frontend/src/components/ParticipantPicker/ParticipantPicker.tsx b/frontend/src/components/ParticipantPicker/ParticipantPicker.tsx new file mode 100644 index 0000000..08b77b0 --- /dev/null +++ b/frontend/src/components/ParticipantPicker/ParticipantPicker.tsx @@ -0,0 +1,77 @@ +import Combobox, { type ComboboxOption, type ComboboxSize } from '../Combobox/Combobox'; +import ListCard from '../ListCard/ListCard'; + +export interface ParticipantPickerProps { + options: ComboboxOption[]; + value: string[]; + onChange: (value: string[]) => void; + placeholder?: string; + label?: string; + noResultsText?: string; + size?: ComboboxSize; + fullWidth?: boolean; + customWidth?: string; + /** Called when search term changes. When provided, local filtering is disabled (assumes API filtering). */ + onSearchChange?: (term: string) => void; +} + +const widthClasses: Record = { + sm: 'w-(--text-input-default-width-md)', + md: 'w-(--text-input-default-width-md)', + lg: 'w-(--text-input-default-width-lg)', +}; + +export default function ParticipantPicker({ + options, + value, + onChange, + placeholder = 'Sök...', + label, + noResultsText = 'Inga resultat', + size = 'md', + fullWidth = false, + customWidth, + onSearchChange, +}: ParticipantPickerProps) { + const handleRemove = (valueToRemove: string) => { + onChange(value.filter((v) => v !== valueToRemove)); + }; + + const selectedOptions = options.filter((opt) => value.includes(opt.value)); + + const containerClasses = fullWidth + ? 'flex flex-col gap-(--spacing-sm) w-full' + : customWidth + ? 'flex flex-col gap-(--spacing-sm)' + : `flex flex-col gap-(--spacing-sm) ${widthClasses[size]}`; + const containerStyle = customWidth ? { width: customWidth } : undefined; + + return ( +
+ onChange(v as string[])} + placeholder={placeholder} + label={label} + noResultsText={noResultsText} + size={size} + fullWidth + multiple + onSearchChange={onSearchChange} + /> + {selectedOptions.length > 0 && ( +
+ {selectedOptions.map((option) => ( + handleRemove(option.value)} + /> + ))} +
+ )} +
+ ); +} diff --git a/frontend/src/components/SearchResultList/SearchResultList.tsx b/frontend/src/components/SearchResultList/SearchResultList.tsx new file mode 100644 index 0000000..bd670dd --- /dev/null +++ b/frontend/src/components/SearchResultList/SearchResultList.tsx @@ -0,0 +1,66 @@ +import ListItem from '../ListItem/ListItem'; + +export interface SearchResultOption { + value: string; + label: string; + subtitle?: string; +} + +export interface SearchResultListProps { + options: SearchResultOption[]; + selectedValues?: string[]; + focusedIndex?: number; + maxHeight?: number; + noResultsText?: string; + onSelect?: (option: SearchResultOption) => void; + itemRefs?: React.RefObject<(HTMLDivElement | null)[]>; +} + +const containerClasses = [ + 'w-full bg-base-canvas border border-base-ink-medium rounded-(--border-radius-md)', + 'overflow-y-auto', +].join(' '); + +const noResultsClasses = 'px-(--padding-md) py-(--padding-md) body-normal-md text-base-ink-placeholder text-center'; + +export default function SearchResultList({ + options, + selectedValues = [], + focusedIndex = -1, + maxHeight = 300, + noResultsText = 'No results found', + onSelect, + itemRefs, +}: SearchResultListProps) { + const isSelected = (value: string) => selectedValues.includes(value); + + return ( +
e.preventDefault()}> + {options.length > 0 ? ( + options.map((option, index) => ( +
{ + if (itemRefs?.current) { + itemRefs.current[index] = el; + } + }} + className={ + index > 0 ? 'border-t border-base-ink-soft [border-top-width:var(--border-width-sm)]' : '' + } + > + onSelect?.(option)} + /> +
+ )) + ) : ( +
{noResultsText}
+ )} +
+ ); +} diff --git a/frontend/src/components/TextInput/TextInput.tsx b/frontend/src/components/TextInput/TextInput.tsx new file mode 100644 index 0000000..2066769 --- /dev/null +++ b/frontend/src/components/TextInput/TextInput.tsx @@ -0,0 +1,113 @@ +import type { InputHTMLAttributes, ReactNode, CSSProperties } from 'react'; +// isValidElement: checks if something is a React element (e.g., , ) +// cloneElement: creates a copy of a React element with modified/additional props +import { cloneElement, isValidElement } from 'react'; + +export type TextInputSize = 'sm' | 'md' | 'lg'; + +// Omit<... 'size'> removes the native 'size' attribute from input elements so we can use our own +export interface TextInputProps extends Omit, 'size'> { + size?: TextInputSize; + icon?: ReactNode; + error?: boolean; + fullWidth?: boolean; + customWidth?: string; + label?: string; + message?: string; +} + +const heightClasses: Record = { + sm: 'h-(--control-height-sm)', + md: 'h-(--control-height-md)', + lg: 'h-(--control-height-lg)', +}; + +const widthClasses: Record = { + sm: 'w-(--text-input-default-width-md)', + md: 'w-(--text-input-default-width-md)', + lg: 'w-(--text-input-default-width-lg)', +}; + +const radiusClasses: Record = { + sm: 'rounded-(--border-radius-sm)', + md: 'rounded-(--border-radius-sm)', + lg: 'rounded-(--border-radius-md)', +}; + +const textClasses: Record = { + sm: 'body-normal-md', + md: 'body-normal-md', + lg: 'body-normal-lg', +}; + +const iconContainerStyles: Record = { + sm: { width: 'var(--control-height-sm)', height: 'var(--control-height-sm)' }, + md: { width: 'var(--control-height-md)', height: 'var(--control-height-md)' }, + lg: { width: 'var(--control-height-lg)', height: 'var(--control-height-lg)' }, +}; + +const iconStyles: Record = { + sm: { width: 'var(--font-size-body-md)', height: 'var(--font-size-body-md)' }, + md: { width: 'var(--font-size-body-md)', height: 'var(--font-size-body-md)' }, + lg: { width: 'var(--font-size-body-lg)', height: 'var(--font-size-body-lg)' }, +}; + +const baseClasses = 'bg-base-canvas border-[length:var(--border-width-sm)] border-base-ink-medium'; + +// focus-within: applies styles when any child element (the input) has focus +const defaultStateClasses = + 'hover:border-base-ink-placeholder focus-within:border-primary focus-within:outline focus-within:outline-sky-100 focus-within:outline-[length:var(--border-width-lg)]'; + +const errorStateClasses = + 'border-fire-100 outline outline-fire-100 outline-[length:var(--border-width-sm)] focus-within:border-primary focus-within:outline focus-within:outline-[length:var(--border-width-lg)] focus-within:outline-sky-100'; + +export default function TextInput({ + size = 'md', + icon, + error = false, + fullWidth = false, + customWidth, + label, + message, + className = '', + ...props +}: TextInputProps) { + const widthClass = fullWidth ? 'w-full' : widthClasses[size]; + const widthStyle = customWidth ? { width: customWidth } : undefined; + const stateClasses = error ? errorStateClasses : defaultStateClasses; + const inputPadding = icon ? 'pr-(--padding-md)' : 'px-(--padding-md)'; + + const inputField = ( +
+ {icon && ( +
+ {isValidElement<{ style?: CSSProperties }>(icon) + ? cloneElement(icon, { style: iconStyles[size] }) + : icon} +
+ )} + +
+ ); + + if (!label && !message) { + return inputField; + } + + return ( +
+ {label && } + {inputField} + {message && {message}} +
+ ); +} diff --git a/frontend/src/index.css b/frontend/src/index.css index 0c91973..fc0ce33 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,11 +1,222 @@ +@import "tailwindcss"; + +/* TheSans Font Family */ +@font-face { + font-family: "TheSans"; + src: url("./assets/TheSansB-W5Plain.woff2") format("woff2"); + font-style: normal; +} + +@font-face { + font-family: "TheSans"; + src: url("./assets/TheSansB-W5PlainItalic.woff2") format("woff2"); + font-style: italic; +} + +@font-face { + font-family: "TheSans Light"; + src: url("./assets/TheSansB-W3Light.woff2") format("woff2"); + font-style: normal; +} + +@font-face { + font-family: "TheSans Light"; + src: url("./assets/TheSansB-W3LightItalic.woff2") format("woff2"); + font-style: italic; +} + +@font-face { + font-family: "TheSans SemiLight"; + src: url("./assets/TheSansB-W4SemiLight.woff2") format("woff2"); + font-style: normal; +} + +@font-face { + font-family: "TheSans SemiLight"; + src: url("./assets/TheSansB-W4SemiLightItalic.woff2") format("woff2"); + font-style: italic; +} + +@font-face { + font-family: "TheSans Plain"; + src: url("./assets/TheSansB-W5Plain.woff2") format("woff2"); + font-style: normal; +} + +@font-face { + font-family: "TheSans Plain"; + src: url("./assets/TheSansB-W5PlainItalic.woff2") format("woff2"); + font-style: italic; +} + +@font-face { + font-family: "TheSans SemiBold"; + src: url("./assets/TheSansB-W6SemiBold.woff2") format("woff2"); + font-style: normal; +} + +@font-face { + font-family: "TheSans SemiBold"; + src: url("./assets/TheSansB-W6SemiBoldItalic.woff2") format("woff2"); + font-style: italic; +} + +@theme { + /* Colors */ + --color-primary: #05305d; + --color-base-canvas: #ffffff; + --color-secondary: #34587f; + --color-sky-100: #b0dee4; + --color-sky-70: #c7e8ed; + --color-sky-35: #e4f4f7; + --color-sky-20: #eff9fa; + --color-base-ink-max: #000000; + --color-base-ink-strong: #4b4b4b; + --color-base-ink-medium: #bababa; + --color-base-ink-soft: #dadada; + --color-base-ink-placeholder: #757575; + --color-other-red-100: #aa1227; + --color-other-red-10: #f6e6e8; + --color-other-green: #539848; + --color-su-white: #ffffff; + --color-fire-100: #eb7124; + --color-fire-70: #f19b66; + --color-fire-35: #f8cdb4; + --color-fire-20: #fbe2d3; + + /* Font sizes */ + --font-size-body-md: 16px; + --font-size-body-lg: 18px; + + /* Border radius */ + --border-radius-sm: 3px; + --border-radius-md: 4px; + --border-radius-lg: 6px; + --border-radius-xl: 8px; + + /* Border width */ + --border-width-sm: 1px; + --border-width-lg: 3px; + + /* Padding */ + --padding-md: 12px; + --padding-lg: 24px; + --padding-xl: 48px; + + /* Spacing */ + --spacing-sm: 8px; + --spacing-md: 12px; + --spacing-lg: 24px; + --spacing-xl: 32px; + + /* Control heights */ + --control-height-sm: 32px; + --control-height-md: 40px; + --control-height-lg: 48px; + + /* Button padding x */ + --button-padding-x-sm: 6px; + --button-padding-x-md: 10px; + --button-padding-x-lg: 14px; + + /* Button min width */ + --button-min-width-sm: 72px; + --button-min-width-md: 72px; + --button-min-width-lg: 84px; + + /* Button text padding x */ + --button-text-padding-x-sm: 6px; + --button-text-padding-x-md: 6px; + --button-text-padding-x-lg: 6px; + + /* Text input default width */ + --text-input-default-width-md: 194px; + --text-input-default-width-lg: 218px; +} + +.dark { + --color-primary: #ffffff; + --color-base-canvas: #000000; + --color-secondary: #d9d6d6; + --color-sky-100: #403d3d; + --color-sky-70: #2d2b2b; + --color-sky-35: #1f1e1e; + --color-sky-20: #141414; + --color-base-ink-max: #ffffff; + --color-base-ink-strong: #ffffff; + --color-base-ink-medium: #636363; + --color-base-ink-soft: #555555; + --color-base-ink-placeholder: #959595; + --color-other-red-100: #aa1227; + --color-other-red-10: #f6e6e8; + --color-other-green: #539848; + --color-su-white: #ffffff; + --color-fire-100: #eb7124; + --color-fire-70: #f19b66; + --color-fire-35: #f8cdb4; + --color-fire-20: #fbe2d3; +} + :root { --color-su-primary: #002f5f; --color-su-primary-80: #33587f; - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + --bottom-nav-height: 4.5rem; + font-family: 'TheSans', system-ui, Avenir, Helvetica, Arial, sans-serif; + background-color: #ffffff; + color: #000000; } + +/* Text styles - Body */ +.body-light-sm { + font-family: "TheSans Light", "TheSans", system-ui, sans-serif; + font-size: 14px; +} + +.body-normal-md { + font-family: "TheSans SemiLight", "TheSans", system-ui, sans-serif; + font-size: 16px; +} + +.body-normal-lg { + font-family: 'TheSans SemiLight', 'TheSans', system-ui, sans-serif; + font-size: 18px; +} + +.body-semibold-md { + font-family: 'TheSans Plain', 'TheSans', system-ui, sans-serif; + font-size: 16px; +} + +.body-semibold-lg { + font-family: 'TheSans Plain', 'TheSans', system-ui, sans-serif; + font-size: 18px; +} + +.body-bold-md { + font-family: 'TheSans SemiBold', 'TheSans', system-ui, sans-serif; + font-size: 16px; +} + +.body-bold-lg { + font-family: 'TheSans SemiBold', 'TheSans', system-ui, sans-serif; + font-size: 18px; +} + +/* Text styles - Heading */ +.heading-semibold-lg { + font-family: 'TheSans SemiBold', 'TheSans', system-ui, sans-serif; + font-size: 32px; +} + +.dark { + background-color: #141414; + color: #ffffff; +} + * { box-sizing: border-box; } body { margin: 0; + min-height: 100vh; } diff --git a/frontend/src/studentportalen/ComponentLibrary.tsx b/frontend/src/studentportalen/ComponentLibrary.tsx new file mode 100644 index 0000000..a94817a --- /dev/null +++ b/frontend/src/studentportalen/ComponentLibrary.tsx @@ -0,0 +1,359 @@ +import { useState, useEffect } from 'react'; +import Button from '../components/Button/Button'; +import TextInput from '../components/TextInput/TextInput'; +import ListItem from '../components/ListItem/ListItem'; +import SearchResultList from '../components/SearchResultList/SearchResultList'; +import Combobox from '../components/Combobox/Combobox'; +import ListCard from '../components/ListCard/ListCard'; +import ParticipantPicker from '../components/ParticipantPicker/ParticipantPicker'; + +const peopleOptions = [ + { value: '1', label: 'Lennart Johansson', subtitle: 'lejo1891' }, + { value: '2', label: 'Mats Rubarth', subtitle: 'matsrub1891' }, + { value: '3', label: 'Daniel Tjernström', subtitle: 'datj1891' }, + { value: '4', label: 'Johan Mjällby', subtitle: 'jomj1891' }, + { value: '5', label: 'Krister Nordin', subtitle: 'krno1891' }, + { value: '6', label: 'Kurre Hamrin', subtitle: 'kuha1891' }, +]; + +export default function ComponentLibrary() { + const [darkMode, setDarkMode] = useState(() => { + return document.documentElement.classList.contains('dark'); + }); + const [selectedPerson, setSelectedPerson] = useState(''); + const [selectedPeople, setSelectedPeople] = useState([]); + const [participants, setParticipants] = useState([]); + + useEffect(() => { + if (darkMode) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + }, [darkMode]); + + return ( + <> +

Component Library

+ +
+

Dark Mode

+ +
+ +
+

Button Variants

+
+ + + + +
+
+ +
+

Button Sizes

+
+ + + +
+
+ +
+

Text Input Sizes

+
+ + + +
+
+ +
+

Text Input with Icon

+
+ + + + } + /> + + + + } + /> + + + + } + /> +
+
+ +
+

Text Input States

+
+ + +
+
+ +
+

Text Input With/Without Placeholder

+
+ + +
+
+ +
+

Text Input Width Options

+
+ + +
+
+ +
+

Text Input with Label

+
+ + +
+
+ +
+

Text Input with Label and Message

+
+ + +
+
+ +
+

List Item

+
+ + + + +
+
+ +
+

List Item - Title Only

+
+ + + +
+
+ +
+

SearchResultList

+
+ +
+
+ +
+

SearchResultList - Empty

+
+ +
+
+ +
+

Combobox - Single Select

+
+ setSelectedPerson(v as string)} + placeholder="Sök..." + label="Välj person" + /> +

+ Selected: {selectedPerson ? peopleOptions.find((p) => p.value === selectedPerson)?.label : 'None'} +

+
+
+ +
+

Combobox - Multi Select

+
+ setSelectedPeople(v as string[])} + placeholder="Sök..." + label="Välj personer" + multiple + /> +

+ Selected: {selectedPeople.length > 0 ? selectedPeople.map((v) => peopleOptions.find((p) => p.value === v)?.label).join(', ') : 'None'} +

+
+
+ +
+

Combobox - Sizes

+
+ + + +
+
+ +
+

Combobox - Custom Width

+
+ + +
+
+ +
+

ListCard

+
+ {}} /> + {}} /> + +
+
+ +
+

ParticipantPicker

+ +
+ +
+

ParticipantPicker - Sizes

+
+ + + +
+
+ +
+

ParticipantPicker - Custom Width

+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + ); +} diff --git a/frontend/src/studentportalen/menu.css b/frontend/src/studentportalen/menu.css index bcbeffc..96d4ea4 100644 --- a/frontend/src/studentportalen/menu.css +++ b/frontend/src/studentportalen/menu.css @@ -7,6 +7,7 @@ menu.main { left: 0; right: 0; display: flex; + z-index: 40; li { list-style: none; margin: 0; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 1f378e7..d6b1edb 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,7 +1,8 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react-swc"; +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react-swc'; +import tailwindcss from '@tailwindcss/vite'; // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react(), tailwindcss()], });