List components #31

Merged
stne3960 merged 68 commits from list_item into main 2025-12-18 12:41:13 +01:00
17 changed files with 775 additions and 457 deletions
Showing only changes of commit 30e4920da3 - Show all commits

6
.gitattributes vendored
View File

@ -8,3 +8,9 @@
*.jpg binary
*.png binary
*.jar binary
*.woff binary
*.woff2 binary
*.ttf binary
*.otf binary
*.eot binary

View File

@ -8,7 +8,7 @@
"name": "frontend",
"version": "0.0.0",
"dependencies": {
"@tailwindcss/vite": "^4.1.16",
"clsx": "^2.1.1",
"openapi-fetch": "^0.13.5",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@ -17,6 +17,7 @@
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@tailwindcss/vite": "^4.1.16",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react-swc": "^3.8.0",
@ -27,6 +28,7 @@
"globals": "^15.15.0",
"openapi-typescript": "^7.6.1",
"prettier": "3.5.3",
"tailwindcss": "^4.1.16",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.0"
@ -64,6 +66,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -80,6 +83,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -96,6 +100,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -112,6 +117,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -128,6 +134,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -144,6 +151,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -160,6 +168,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -176,6 +185,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -192,6 +202,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -208,6 +219,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -224,6 +236,7 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -240,6 +253,7 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -256,6 +270,7 @@
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -272,6 +287,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -288,6 +304,7 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -304,6 +321,7 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -320,6 +338,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -336,6 +355,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -352,6 +372,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -368,6 +389,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -384,6 +406,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -400,6 +423,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -416,6 +440,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -432,6 +457,7 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -448,6 +474,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -681,6 +708,7 @@
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@ -691,6 +719,7 @@
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@ -701,6 +730,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@ -710,12 +740,14 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@ -843,6 +875,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -856,6 +889,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -869,6 +903,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -882,6 +917,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -895,6 +931,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -908,6 +945,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -921,6 +959,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -934,6 +973,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -947,6 +987,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -960,6 +1001,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -973,6 +1015,7 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -986,6 +1029,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -999,6 +1043,7 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1012,6 +1057,7 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1025,6 +1071,7 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1038,6 +1085,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1051,6 +1099,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1064,6 +1113,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1077,6 +1127,7 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1090,6 +1141,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1326,6 +1378,7 @@
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz",
"integrity": "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
@ -1341,6 +1394,7 @@
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz",
"integrity": "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10"
@ -1367,6 +1421,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1383,6 +1438,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1399,6 +1455,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1415,6 +1472,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1431,6 +1489,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1447,6 +1506,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1463,6 +1523,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1479,6 +1540,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1495,6 +1557,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1519,6 +1582,7 @@
"cpu": [
"wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@ -1535,6 +1599,7 @@
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.5.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
@ -1545,6 +1610,7 @@
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.5.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
@ -1554,6 +1620,7 @@
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.1.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
@ -1563,6 +1630,7 @@
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "1.0.7",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
@ -1574,6 +1642,7 @@
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
@ -1583,6 +1652,7 @@
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
"version": "2.8.1",
"dev": true,
"inBundle": true,
"license": "0BSD",
"optional": true
@ -1594,6 +1664,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1610,6 +1681,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1623,6 +1695,7 @@
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.16.tgz",
"integrity": "sha512-bbguNBcDxsRmi9nnlWJxhfDWamY3lmcyACHcdO1crxfzuLpOhHLLtEIN/nCbbAtj5rchUgQD17QVAKi1f7IsKg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@tailwindcss/node": "4.1.16",
@ -1637,6 +1710,7 @@
"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": {
@ -2033,6 +2107,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -2127,6 +2210,7 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
@ -2136,6 +2220,7 @@
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
@ -2149,6 +2234,7 @@
"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": {
@ -2518,6 +2604,7 @@
"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,
@ -2558,6 +2645,7 @@
"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==",
"dev": true,
"license": "ISC"
},
"node_modules/graphemer": {
@ -2685,6 +2773,7 @@
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"dev": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
@ -2769,6 +2858,7 @@
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
@ -2801,6 +2891,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@ -2821,6 +2912,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@ -2841,6 +2933,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@ -2861,6 +2954,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@ -2881,6 +2975,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@ -2901,6 +2996,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@ -2921,6 +3017,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@ -2941,6 +3038,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@ -2961,6 +3059,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@ -2981,6 +3080,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@ -3001,6 +3101,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@ -3041,6 +3142,7 @@
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
@ -3094,6 +3196,7 @@
"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",
@ -3269,6 +3372,7 @@
"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": {
@ -3298,6 +3402,7 @@
"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",
@ -3457,6 +3562,7 @@
"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"
@ -3568,6 +3674,7 @@
"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"
@ -3603,12 +3710,14 @@
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz",
"integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==",
"dev": true,
"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==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@ -3622,6 +3731,7 @@
"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",
@ -3638,6 +3748,7 @@
"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"
@ -3652,6 +3763,7 @@
"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"
@ -3770,6 +3882,7 @@
"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",
@ -3844,6 +3957,7 @@
"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"
@ -3858,6 +3972,7 @@
"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"

View File

@ -12,7 +12,7 @@
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.16",
"clsx": "^2.1.1",
"openapi-fetch": "^0.13.5",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@ -21,6 +21,7 @@
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@tailwindcss/vite": "^4.1.16",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react-swc": "^3.8.0",
@ -31,6 +32,7 @@
"globals": "^15.15.0",
"openapi-typescript": "^7.6.1",
"prettier": "3.5.3",
"tailwindcss": "^4.1.16",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.0"

View File

@ -1,4 +1,5 @@
import type { ButtonHTMLAttributes, ReactNode } from "react";
import clsx from "clsx";
export type ButtonVariant = "primary" | "secondary" | "red" | "green";
export type ButtonSize = "sm" | "md" | "lg";
@ -10,13 +11,36 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
}
const variantClasses: Record<ButtonVariant, string> = {
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",
primary: clsx(
"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: clsx(
"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: clsx(
"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: clsx(
"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<ButtonSize, string> = {
@ -40,14 +64,12 @@ export default function Button({
}: ButtonProps) {
const baseClasses = "inline-flex items-center justify-center cursor-pointer";
const classes = [
const classes = clsx(
baseClasses,
variantClasses[variant],
sizeClasses[size],
className,
]
.filter(Boolean)
.join(" ");
);
return (
<button className={classes} {...props}>

View File

@ -0,0 +1,96 @@
import type { SVGAttributes } from "react";
import clsx from "clsx";
/**
* Icon sizes matching the design system control sizes.
* "inherit" (default) uses 1em to scale with the parent's font-size.
*/
export type IconSize = "inherit" | "sm" | "md" | "lg";
/**
* Base props for all icon components.
* Extends SVG attributes but replaces `size` with our design system size.
*/
export interface IconProps extends Omit<SVGAttributes<SVGSVGElement>, "size"> {
size?: IconSize;
}
/**
* Type for icon components that can be passed to controls like TextInput.
* Usage: `<TextInput Icon={SearchIcon} />`
*/
export type IconComponent = React.ComponentType<IconProps>;
/**
* Size classes using design system font-size tokens.
* "inherit" uses 1em so icons scale with the parent's font-size.
*/
const iconSizeClasses: Record<IconSize, string> = {
inherit: "w-[1em] h-[1em]",
sm: "w-(--font-size-body-md) h-(--font-size-body-md)",
md: "w-(--font-size-body-md) h-(--font-size-body-md)",
lg: "w-(--font-size-body-lg) h-(--font-size-body-lg)",
};
/**
* Shared SVG attributes applied to all icons.
* Uses stroke (not fill) so icons inherit text color via currentColor.
*/
const baseSvgProps: SVGAttributes<SVGSVGElement> = {
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: 2,
strokeLinecap: "round",
strokeLinejoin: "round",
};
export function SearchIcon({
size = "inherit",
className,
...props
}: IconProps) {
return (
<svg
className={clsx(iconSizeClasses[size], className)}
{...baseSvgProps}
{...props}
>
<circle cx="11" cy="11" r="8" />
<path d="M21 21l-4.35-4.35" />
</svg>
);
}
export function RemoveIcon({
size = "inherit",
className,
...props
}: IconProps) {
return (
<svg
className={clsx(iconSizeClasses[size], className)}
{...baseSvgProps}
{...props}
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
);
}
export function CheckmarkIcon({
size = "inherit",
className,
...props
}: IconProps) {
return (
<svg
className={clsx(iconSizeClasses[size], className)}
{...baseSvgProps}
{...props}
>
<polyline points="20 6 9 17 4 12" />
</svg>
);
}

View File

@ -1,113 +1,138 @@
import type { InputHTMLAttributes, ReactNode, CSSProperties } from 'react';
// isValidElement: checks if something is a React element (e.g., <svg>, <MyComponent />)
// cloneElement: creates a copy of a React element with modified/additional props
import { cloneElement, isValidElement } from 'react';
import { useId, type InputHTMLAttributes } from "react";
import clsx from "clsx";
import type { IconComponent } from "../Icon/Icon";
export type TextInputSize = 'sm' | 'md' | 'lg';
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<InputHTMLAttributes<HTMLInputElement>, 'size'> {
size?: TextInputSize;
icon?: ReactNode;
error?: boolean;
fullWidth?: boolean;
customWidth?: string;
label?: string;
message?: string;
export interface TextInputProps
extends Omit<InputHTMLAttributes<HTMLInputElement>, "size"> {
size?: TextInputSize;
Icon?: IconComponent;
error?: boolean;
fullWidth?: boolean;
customWidth?: string;
label: string;
hideLabel?: boolean;
message?: string;
}
const heightClasses: Record<TextInputSize, string> = {
sm: 'h-(--control-height-sm)',
md: 'h-(--control-height-md)',
lg: 'h-(--control-height-lg)',
const wrapperSizeClasses: Record<TextInputSize, string> = {
sm: clsx(
"h-(--control-height-sm)",
"rounded-(--border-radius-sm)",
"w-(--text-input-default-width-md)",
),
md: clsx(
"h-(--control-height-md)",
"rounded-(--border-radius-sm)",
"w-(--text-input-default-width-md)",
),
lg: clsx(
"h-(--control-height-lg)",
"rounded-(--border-radius-md)",
"w-(--text-input-default-width-lg)",
),
};
const widthClasses: Record<TextInputSize, string> = {
sm: 'w-(--text-input-default-width-md)',
md: 'w-(--text-input-default-width-md)',
lg: 'w-(--text-input-default-width-lg)',
const inputSizeClasses: Record<TextInputSize, string> = {
sm: "body-normal-md",
md: "body-normal-md",
lg: "body-normal-lg",
};
const radiusClasses: Record<TextInputSize, string> = {
sm: 'rounded-(--border-radius-sm)',
md: 'rounded-(--border-radius-sm)',
lg: 'rounded-(--border-radius-md)',
const iconContainerSizeClasses: Record<TextInputSize, string> = {
sm: clsx("w-(--control-height-sm) h-(--control-height-sm)"),
md: clsx("w-(--control-height-md) h-(--control-height-md)"),
lg: clsx("w-(--control-height-lg) h-(--control-height-lg)"),
};
const textClasses: Record<TextInputSize, string> = {
sm: 'body-normal-md',
md: 'body-normal-md',
lg: 'body-normal-lg',
};
const iconContainerStyles: Record<TextInputSize, CSSProperties> = {
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<TextInputSize, CSSProperties> = {
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';
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 defaultStateClasses = clsx(
"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';
const errorStateClasses = clsx(
"border-fire-100",
"outline outline-fire-100 outline-[length:var(--border-width-sm)]",
"focus-within:border-primary",
"focus-within:outline focus-within:outline-sky-100 focus-within:outline-[length:var(--border-width-lg)]",
);
export default function TextInput({
size = 'md',
icon,
error = false,
fullWidth = false,
customWidth,
label,
message,
className = '',
...props
size = "md",
Icon,
error = false,
fullWidth = false,
customWidth,
label,
hideLabel = false,
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 inputId = useId();
const widthStyle = customWidth ? { width: customWidth } : undefined;
const stateClasses = error ? errorStateClasses : defaultStateClasses;
const inputPadding = Icon ? "pr-(--padding-md)" : "px-(--padding-md)";
const showVisibleLabel = !hideLabel;
const inputField = (
const inputField = (
<div
className={clsx(
"flex items-center",
baseClasses,
wrapperSizeClasses[size],
stateClasses,
fullWidth && "w-full",
className,
)}
style={widthStyle}
>
{Icon && (
<div
className={`flex items-center ${baseClasses} ${heightClasses[size]} ${widthClass} ${radiusClasses[size]} ${stateClasses} ${className}`}
style={widthStyle}
className={clsx(
"flex items-center justify-center shrink-0 text-base-ink-placeholder",
iconContainerSizeClasses[size],
)}
>
{icon && (
<div
className="flex items-center justify-center shrink-0 text-base-ink-placeholder"
style={iconContainerStyles[size]}
>
{isValidElement<{ style?: CSSProperties }>(icon)
? cloneElement(icon, { style: iconStyles[size] })
: icon}
</div>
)}
<input
className={`flex-1 min-w-0 h-full bg-transparent border-none outline-none text-base-ink-max placeholder:text-base-ink-placeholder ${inputPadding} ${textClasses[size]}`}
{...props}
/>
<Icon size={size} />
</div>
);
)}
<input
id={inputId}
aria-label={hideLabel ? label : undefined}
className={clsx(
"flex-1 min-w-0 h-full bg-transparent border-none outline-none",
"text-base-ink-max placeholder:text-base-ink-placeholder",
inputPadding,
inputSizeClasses[size],
)}
{...props}
/>
</div>
);
if (!label && !message) {
return inputField;
}
if (!showVisibleLabel && !message) {
return inputField;
}
return (
<div className="flex flex-col gap-(--spacing-sm)">
{label && <label className="body-bold-md text-base-ink-strong">{label}</label>}
{inputField}
{message && <span className="body-light-sm text-base-ink-strong">{message}</span>}
</div>
);
return (
<div className="flex flex-col gap-(--spacing-sm)">
{showVisibleLabel && (
<label htmlFor={inputId} className="body-bold-md text-base-ink-strong">
{label}
</label>
)}
{inputField}
{message && (
<span className="body-light-sm text-base-ink-strong">{message}</span>
)}
</div>
);
}

View File

@ -2,65 +2,101 @@
/* TheSans Font Family */
@font-face {
font-family: "TheSans";
src: url("./assets/TheSansB-W5Plain.woff2") format("woff2");
font-family: "TheSansB W2 ExtraLight";
src: url("./assets/TheSansB-W2ExtraLight.woff2") format("woff2");
font-style: normal;
}
@font-face {
font-family: "TheSans";
src: url("./assets/TheSansB-W5PlainItalic.woff2") format("woff2");
font-family: "TheSansB W2 ExtraLight";
src: url("./assets/TheSansB-W2ExtraLightItalic.woff2") format("woff2");
font-style: italic;
}
@font-face {
font-family: "TheSans Light";
font-family: "TheSansB W3 Light";
src: url("./assets/TheSansB-W3Light.woff2") format("woff2");
font-style: normal;
}
@font-face {
font-family: "TheSans Light";
font-family: "TheSansB W3 Light";
src: url("./assets/TheSansB-W3LightItalic.woff2") format("woff2");
font-style: italic;
}
@font-face {
font-family: "TheSans SemiLight";
font-family: "TheSansB W4 SemiLight";
src: url("./assets/TheSansB-W4SemiLight.woff2") format("woff2");
font-style: normal;
}
@font-face {
font-family: "TheSans SemiLight";
font-family: "TheSansB W4 SemiLight";
src: url("./assets/TheSansB-W4SemiLightItalic.woff2") format("woff2");
font-style: italic;
}
@font-face {
font-family: "TheSans Plain";
font-family: "TheSansB W5 Plain";
src: url("./assets/TheSansB-W5Plain.woff2") format("woff2");
font-style: normal;
}
@font-face {
font-family: "TheSans Plain";
font-family: "TheSansB W5 Plain";
src: url("./assets/TheSansB-W5PlainItalic.woff2") format("woff2");
font-style: italic;
}
@font-face {
font-family: "TheSans SemiBold";
font-family: "TheSansB W6 SemiBold";
src: url("./assets/TheSansB-W6SemiBold.woff2") format("woff2");
font-style: normal;
}
@font-face {
font-family: "TheSans SemiBold";
font-family: "TheSansB W6 SemiBold";
src: url("./assets/TheSansB-W6SemiBoldItalic.woff2") format("woff2");
font-style: italic;
}
@font-face {
font-family: "TheSansB W7 Bold";
src: url("./assets/TheSansB-W7Bold.woff2") format("woff2");
font-style: normal;
}
@font-face {
font-family: "TheSansB W7 Bold";
src: url("./assets/TheSansB-W7BoldItalic.woff2") format("woff2");
font-style: italic;
}
@font-face {
font-family: "TheSansB W8 ExtraBold";
src: url("./assets/TheSansB-W8ExtraBold.woff2") format("woff2");
font-style: normal;
}
@font-face {
font-family: "TheSansB W8 ExtraBold";
src: url("./assets/TheSansB-W8ExtraBoldItalic.woff2") format("woff2");
font-style: italic;
}
@font-face {
font-family: "TheSansB W9 Black";
src: url("./assets/TheSansB-W9Black.woff2") format("woff2");
font-style: normal;
}
@font-face {
font-family: "TheSansB W9 Black";
src: url("./assets/TheSansB-W9BlackItalic.woff2") format("woff2");
font-style: italic;
}
@theme {
/* Colors */
--color-primary: #05305d;
@ -161,50 +197,50 @@
--color-su-primary: #002f5f;
--color-su-primary-80: #33587f;
--bottom-nav-height: 4.5rem;
font-family: 'TheSans', system-ui, Avenir, Helvetica, Arial, sans-serif;
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-family: "TheSansB W3 Light", system-ui, sans-serif;
font-size: 14px;
}
.body-normal-md {
font-family: "TheSans SemiLight", "TheSans", system-ui, sans-serif;
font-family: "TheSansB W4 SemiLight", system-ui, sans-serif;
font-size: 16px;
}
.body-normal-lg {
font-family: 'TheSans SemiLight', 'TheSans', system-ui, sans-serif;
font-family: "TheSansB W4 SemiLight", system-ui, sans-serif;
font-size: 18px;
}
.body-semibold-md {
font-family: 'TheSans Plain', 'TheSans', system-ui, sans-serif;
font-family: "TheSansB W5 Plain", system-ui, sans-serif;
font-size: 16px;
}
.body-semibold-lg {
font-family: 'TheSans Plain', 'TheSans', system-ui, sans-serif;
font-family: "TheSansB W5 Plain", system-ui, sans-serif;
font-size: 18px;
}
.body-bold-md {
font-family: 'TheSans SemiBold', 'TheSans', system-ui, sans-serif;
font-family: "TheSansB W6 SemiBold", system-ui, sans-serif;
font-size: 16px;
}
.body-bold-lg {
font-family: 'TheSans SemiBold', 'TheSans', system-ui, sans-serif;
font-family: "TheSansB W6 SemiBold", system-ui, sans-serif;
font-size: 18px;
}
/* Text styles - Heading */
.heading-semibold-lg {
font-family: 'TheSans SemiBold', 'TheSans', system-ui, sans-serif;
font-family: "TheSansB W6 SemiBold", system-ui, sans-serif;
font-size: 32px;
}

View File

@ -1,359 +1,375 @@
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';
import { useState, useEffect } from "react";
import Button from "../components/Button/Button";
import TextInput from "../components/TextInput/TextInput";
import { SearchIcon } from "../components/Icon/Icon";
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' },
{ 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<string>('');
const [selectedPeople, setSelectedPeople] = useState<string[]>([]);
const [participants, setParticipants] = useState<string[]>([]);
const [darkMode, setDarkMode] = useState(() => {
return document.documentElement.classList.contains("dark");
});
const [selectedPerson, setSelectedPerson] = useState<string>("");
const [selectedPeople, setSelectedPeople] = useState<string[]>([]);
const [participants, setParticipants] = useState<string[]>([]);
useEffect(() => {
if (darkMode) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, [darkMode]);
useEffect(() => {
if (darkMode) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}, [darkMode]);
return (
<>
<h1>Component Library</h1>
return (
<>
<h1>Component Library</h1>
<section className="mt-lg">
<h2 className="mb-md">Dark Mode</h2>
<Button variant="primary" onClick={() => setDarkMode(!darkMode)}>
{darkMode ? 'Light Mode' : 'Dark Mode'}
</Button>
</section>
<section className="mt-lg">
<h2 className="mb-md">Dark Mode</h2>
<Button variant="primary" onClick={() => setDarkMode(!darkMode)}>
{darkMode ? "Light Mode" : "Dark Mode"}
</Button>
</section>
<section className="mt-lg">
<h2 className="mb-md">Button Variants</h2>
<div className="flex flex-wrap gap-md">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="red">Red</Button>
<Button variant="green">Green</Button>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Button Variants</h2>
<div className="flex flex-wrap gap-md">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="red">Red</Button>
<Button variant="green">Green</Button>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Button Sizes</h2>
<div className="flex flex-wrap items-center gap-md">
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Button Sizes</h2>
<div className="flex flex-wrap items-center gap-md">
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Text Input Sizes</h2>
<div className="flex flex-wrap items-center gap-md">
<TextInput
size="sm"
placeholder="Small"
label="Small input"
hideLabel
/>
<TextInput
size="md"
placeholder="Medium"
label="Medium input"
hideLabel
/>
<TextInput
size="lg"
placeholder="Large"
label="Large input"
hideLabel
/>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Text Input Sizes</h2>
<div className="flex flex-wrap items-center gap-md">
<TextInput size="sm" placeholder="Small" />
<TextInput size="md" placeholder="Medium" />
<TextInput size="lg" placeholder="Large" />
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Text Input with Icon</h2>
<div className="flex flex-wrap items-center gap-md">
<TextInput
size="sm"
placeholder="Small with icon"
Icon={SearchIcon}
label="Small search"
hideLabel
/>
<TextInput
size="md"
placeholder="Medium with icon"
Icon={SearchIcon}
label="Medium search"
hideLabel
/>
<TextInput
size="lg"
placeholder="Large with icon"
Icon={SearchIcon}
label="Large search"
hideLabel
/>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Text Input with Icon</h2>
<div className="flex flex-wrap items-center gap-md">
<TextInput
size="sm"
placeholder="Small with icon"
icon={
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
}
/>
<TextInput
size="md"
placeholder="Medium with icon"
icon={
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
}
/>
<TextInput
size="lg"
placeholder="Large with icon"
icon={
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
}
/>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Text Input States</h2>
<div className="flex flex-wrap items-center gap-md">
<TextInput placeholder="Default" label="Default state" hideLabel />
<TextInput
placeholder="Error state"
error
label="Error state"
hideLabel
/>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Text Input States</h2>
<div className="flex flex-wrap items-center gap-md">
<TextInput placeholder="Default" />
<TextInput placeholder="Error state" error />
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Text Input With/Without Placeholder</h2>
<div className="flex flex-wrap items-center gap-md">
<TextInput
placeholder="Placeholder"
label="With placeholder"
hideLabel
/>
<TextInput label="Without placeholder" hideLabel />
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Text Input With/Without Placeholder</h2>
<div className="flex flex-wrap items-center gap-md">
<TextInput placeholder="Placeholder" />
<TextInput />
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Text Input Width Options</h2>
<div className="flex flex-col gap-md">
<TextInput
placeholder="Full width"
fullWidth
label="Full width input"
hideLabel
/>
<TextInput
placeholder="Custom width"
customWidth="300px"
label="Custom width input"
hideLabel
/>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Text Input Width Options</h2>
<div className="flex flex-col gap-md">
<TextInput placeholder="Full width" fullWidth />
<TextInput placeholder="Custom width" customWidth="300px" />
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Text Input with Label</h2>
<div className="flex flex-wrap items-start gap-md">
<TextInput label="Email" placeholder="Enter your email" />
<TextInput label="Password" placeholder="Enter password" error />
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Text Input with Label</h2>
<div className="flex flex-wrap items-start gap-md">
<TextInput label="Email" placeholder="Enter your email" />
<TextInput label="Password" placeholder="Enter password" error />
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Text Input with Label and Message</h2>
<div className="flex flex-wrap items-start gap-md">
<TextInput
label="Email"
placeholder="Enter your email"
error
message="This field is required"
/>
<TextInput
label="Username"
placeholder="Choose a username"
error
message="Must be at least 3 characters"
/>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Text Input with Label and Message</h2>
<div className="flex flex-wrap items-start gap-md">
<TextInput label="Email" placeholder="Enter your email" error message="This field is required" />
<TextInput label="Username" placeholder="Choose a username" error message="Must be at least 3 characters" />
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">List Item</h2>
<div className="max-w-96 border border-base-ink-soft rounded-(--border-radius-md) overflow-hidden">
<ListItem title="Lennart Johansson" subtitle="lejo1891" />
<ListItem title="Mats Rubarth" subtitle="matsrub1891" />
<ListItem title="Daniel Tjernström" subtitle="datj1891" selected />
<ListItem title="Johan Mjällby" subtitle="jomj1891" />
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">List Item</h2>
<div className="max-w-96 border border-base-ink-soft rounded-(--border-radius-md) overflow-hidden">
<ListItem title="Lennart Johansson" subtitle="lejo1891" />
<ListItem title="Mats Rubarth" subtitle="matsrub1891" />
<ListItem title="Daniel Tjernström" subtitle="datj1891" selected />
<ListItem title="Johan Mjällby" subtitle="jomj1891" />
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">List Item - Title Only</h2>
<div className="max-w-96 border border-base-ink-soft rounded-(--border-radius-md) overflow-hidden">
<ListItem title="Krister Nordin" />
<ListItem title="Kurre Hamrin" selected />
<ListItem title="Per Karlsson" />
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">List Item - Title Only</h2>
<div className="max-w-96 border border-base-ink-soft rounded-(--border-radius-md) overflow-hidden">
<ListItem title="Krister Nordin" />
<ListItem title="Kurre Hamrin" selected />
<ListItem title="Per Karlsson" />
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">SearchResultList</h2>
<div className="max-w-96">
<SearchResultList
options={peopleOptions}
selectedValues={["3"]}
focusedIndex={1}
noResultsText="Inga resultat"
/>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">SearchResultList</h2>
<div className="max-w-96">
<SearchResultList
options={peopleOptions}
selectedValues={['3']}
focusedIndex={1}
noResultsText="Inga resultat"
/>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">SearchResultList - Empty</h2>
<div className="max-w-96">
<SearchResultList options={[]} noResultsText="Inga resultat" />
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">SearchResultList - Empty</h2>
<div className="max-w-96">
<SearchResultList
options={[]}
noResultsText="Inga resultat"
/>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Combobox - Single Select</h2>
<div className="flex flex-col gap-md">
<Combobox
options={peopleOptions}
value={selectedPerson}
onChange={(v) => setSelectedPerson(v as string)}
placeholder="Sök..."
label="Välj person"
/>
<p className="body-light-sm text-base-ink-placeholder">
Selected:{" "}
{selectedPerson
? peopleOptions.find((p) => p.value === selectedPerson)?.label
: "None"}
</p>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Combobox - Single Select</h2>
<div className="flex flex-col gap-md">
<Combobox
options={peopleOptions}
value={selectedPerson}
onChange={(v) => setSelectedPerson(v as string)}
placeholder="Sök..."
label="Välj person"
/>
<p className="body-light-sm text-base-ink-placeholder">
Selected: {selectedPerson ? peopleOptions.find((p) => p.value === selectedPerson)?.label : 'None'}
</p>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Combobox - Multi Select</h2>
<div className="flex flex-col gap-md">
<Combobox
options={peopleOptions}
value={selectedPeople}
onChange={(v) => setSelectedPeople(v as string[])}
placeholder="Sök..."
label="Välj personer"
multiple
/>
<p className="body-light-sm text-base-ink-placeholder">
Selected:{" "}
{selectedPeople.length > 0
? selectedPeople
.map((v) => peopleOptions.find((p) => p.value === v)?.label)
.join(", ")
: "None"}
</p>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Combobox - Multi Select</h2>
<div className="flex flex-col gap-md">
<Combobox
options={peopleOptions}
value={selectedPeople}
onChange={(v) => setSelectedPeople(v as string[])}
placeholder="Sök..."
label="Välj personer"
multiple
/>
<p className="body-light-sm text-base-ink-placeholder">
Selected: {selectedPeople.length > 0 ? selectedPeople.map((v) => peopleOptions.find((p) => p.value === v)?.label).join(', ') : 'None'}
</p>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Combobox - Sizes</h2>
<div className="flex flex-wrap items-start gap-md">
<Combobox options={peopleOptions} placeholder="Small" size="sm" />
<Combobox options={peopleOptions} placeholder="Medium" size="md" />
<Combobox options={peopleOptions} placeholder="Large" size="lg" />
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Combobox - Sizes</h2>
<div className="flex flex-wrap items-start gap-md">
<Combobox
options={peopleOptions}
placeholder="Small"
size="sm"
/>
<Combobox
options={peopleOptions}
placeholder="Medium"
size="md"
/>
<Combobox
options={peopleOptions}
placeholder="Large"
size="lg"
/>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Combobox - Custom Width</h2>
<div className="flex flex-col gap-md">
<Combobox
options={peopleOptions}
placeholder="Sök..."
customWidth="350px"
/>
<Combobox
options={peopleOptions}
placeholder="Sök..."
label="Full width"
fullWidth
/>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Combobox - Custom Width</h2>
<div className="flex flex-col gap-md">
<Combobox
options={peopleOptions}
placeholder="Sök..."
customWidth="350px"
/>
<Combobox
options={peopleOptions}
placeholder="Sök..."
label="Full width"
fullWidth
/>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">ListCard</h2>
<div className="flex flex-col gap-md max-w-96">
<ListCard title="Lennart Johansson" onRemove={() => {}} />
<ListCard title="Mats Rubarth" onRemove={() => {}} />
<ListCard title="Daniel Tjernström" />
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">ListCard</h2>
<div className="flex flex-col gap-md max-w-96">
<ListCard title="Lennart Johansson" onRemove={() => {}} />
<ListCard title="Mats Rubarth" onRemove={() => {}} />
<ListCard title="Daniel Tjernström" />
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">ParticipantPicker</h2>
<ParticipantPicker
options={peopleOptions}
value={participants}
onChange={setParticipants}
placeholder="Sök deltagare..."
label="Välj deltagare"
/>
</section>
<section className="mt-lg">
<h2 className="mb-md">ParticipantPicker</h2>
<ParticipantPicker
options={peopleOptions}
value={participants}
onChange={setParticipants}
placeholder="Sök deltagare..."
label="Välj deltagare"
/>
</section>
<section className="mt-lg">
<h2 className="mb-md">ParticipantPicker - Sizes</h2>
<div className="flex flex-wrap items-start gap-md">
<ParticipantPicker
options={peopleOptions}
value={participants}
onChange={setParticipants}
placeholder="Small"
size="sm"
/>
<ParticipantPicker
options={peopleOptions}
value={participants}
onChange={setParticipants}
placeholder="Medium"
size="md"
/>
<ParticipantPicker
options={peopleOptions}
value={participants}
onChange={setParticipants}
placeholder="Large"
size="lg"
/>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">ParticipantPicker - Sizes</h2>
<div className="flex flex-wrap items-start gap-md">
<ParticipantPicker
options={peopleOptions}
value={participants}
onChange={setParticipants}
placeholder="Small"
size="sm"
/>
<ParticipantPicker
options={peopleOptions}
value={participants}
onChange={setParticipants}
placeholder="Medium"
size="md"
/>
<ParticipantPicker
options={peopleOptions}
value={participants}
onChange={setParticipants}
placeholder="Large"
size="lg"
/>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">ParticipantPicker - Custom Width</h2>
<div className="flex flex-col gap-md">
<ParticipantPicker
options={peopleOptions}
value={participants}
onChange={setParticipants}
placeholder="Sök..."
customWidth="350px"
/>
<ParticipantPicker
options={peopleOptions}
value={participants}
onChange={setParticipants}
placeholder="Sök..."
label="Full width"
fullWidth
/>
</div>
</section>
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
</>
);
<section className="mt-lg">
<h2 className="mb-md">ParticipantPicker - Custom Width</h2>
<div className="flex flex-col gap-md">
<ParticipantPicker
options={peopleOptions}
value={participants}
onChange={setParticipants}
placeholder="Sök..."
customWidth="350px"
/>
<ParticipantPicker
options={peopleOptions}
value={participants}
onChange={setParticipants}
placeholder="Sök..."
label="Full width"
fullWidth
/>
</div>
</section>
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
</>
);
}

View File

@ -1,8 +1,8 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import tailwindcss from '@tailwindcss/vite';
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(), tailwindcss()],
plugins: [react(), tailwindcss()],
});