diff --git a/backend/internal/dto/app_config_dto.go b/backend/internal/dto/app_config_dto.go index dccb446a..25d82eda 100644 --- a/backend/internal/dto/app_config_dto.go +++ b/backend/internal/dto/app_config_dto.go @@ -17,6 +17,7 @@ type AppConfigUpdateDto struct { EmailsVerified string `json:"emailsVerified" binding:"required"` DisableAnimations string `json:"disableAnimations" binding:"required"` AllowOwnAccountEdit string `json:"allowOwnAccountEdit" binding:"required"` + AccentColor string `json:"accentColor"` SmtpHost string `json:"smtpHost"` SmtpPort string `json:"smtpPort"` SmtpFrom string `json:"smtpFrom" binding:"omitempty,email"` diff --git a/backend/internal/model/app_config.go b/backend/internal/model/app_config.go index ca525adf..a2d20169 100644 --- a/backend/internal/model/app_config.go +++ b/backend/internal/model/app_config.go @@ -35,6 +35,7 @@ type AppConfig struct { AppName AppConfigVariable `key:"appName,public"` // Public SessionDuration AppConfigVariable `key:"sessionDuration"` EmailsVerified AppConfigVariable `key:"emailsVerified"` + AccentColor AppConfigVariable `key:"accentColor,public"` // Public DisableAnimations AppConfigVariable `key:"disableAnimations,public"` // Public AllowOwnAccountEdit AppConfigVariable `key:"allowOwnAccountEdit,public"` // Public // Internal diff --git a/backend/internal/service/app_config_service.go b/backend/internal/service/app_config_service.go index c900b6de..73f25fbc 100644 --- a/backend/internal/service/app_config_service.go +++ b/backend/internal/service/app_config_service.go @@ -68,6 +68,7 @@ func (s *AppConfigService) getDefaultDbConfig() *model.AppConfig { EmailsVerified: model.AppConfigVariable{Value: "false"}, DisableAnimations: model.AppConfigVariable{Value: "false"}, AllowOwnAccountEdit: model.AppConfigVariable{Value: "true"}, + AccentColor: model.AppConfigVariable{Value: "default"}, // Internal BackgroundImageType: model.AppConfigVariable{Value: "jpg"}, LogoLightImageType: model.AppConfigVariable{Value: "svg"}, diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 07e94b7b..132e7c0b 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -372,5 +372,11 @@ "show": "Show", "select_an_option": "Select an option", "select_user": "Select User", - "error": "Error" + "error": "Error", + "select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Select an accent color to customize the appearance of Pocket ID.", + "accent_color": "Accent Color", + "custom_accent_color": "Custom Accent Color", + "custom_accent_color_description": "Enter a custom color using valid CSS color formats (e.g., hex, rgb, hsl).", + "color_value": "Color Value", + "apply": "Apply" } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 58438ffd..4de25ba9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,14 +1,13 @@ { "name": "pocket-id-frontend", - "version": "1.2.0", + "version": "1.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pocket-id-frontend", - "version": "1.2.0", + "version": "1.3.1", "dependencies": { - "@lucide/svelte": "^0.511.0", "@simplewebauthn/browser": "^13.1.0", "@tailwindcss/vite": "^4.1.7", "axios": "^1.8.2", @@ -16,7 +15,6 @@ "crypto": "^1.0.1", "jose": "^5.9.6", "qrcode": "^1.5.4", - "rollup-plugin-visualizer": "^6.0.1", "sveltekit-superforms": "^2.23.1", "tailwind-merge": "^3.3.0", "zod": "^3.25.55" @@ -25,7 +23,8 @@ "@inlang/paraglide-js": "^2.0.13", "@inlang/plugin-m-function-matcher": "^2.0.10", "@inlang/plugin-message-format": "^4.0.0", - "@internationalized/date": "^3.7.0", + "@internationalized/date": "^3.8.2", + "@lucide/svelte": "^0.513.0", "@playwright/test": "^1.50.0", "@sveltejs/adapter-static": "^3.0.8", "@sveltejs/kit": "^2.20.7", @@ -33,7 +32,7 @@ "@types/eslint": "^9.6.1", "@types/node": "^22.10.10", "@types/qrcode": "^1.5.5", - "bits-ui": "^1.5.3", + "bits-ui": "^2.5.0", "eslint": "^9.19.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-svelte": "^2.46.1", @@ -633,21 +632,23 @@ "optional": true }, "node_modules/@floating-ui/core": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", - "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.1.tgz", + "integrity": "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==", "dev": true, + "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "node_modules/@floating-ui/dom": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", - "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.1.tgz", + "integrity": "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==", "dev": true, + "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.6.0", + "@floating-ui/core": "^1.7.1", "@floating-ui/utils": "^0.2.9" } }, @@ -655,7 +656,8 @@ "version": "0.2.9", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@gcornut/valibot-json-schema": { "version": "0.31.0", @@ -828,10 +830,11 @@ "license": "MIT" }, "node_modules/@internationalized/date": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.7.0.tgz", - "integrity": "sha512-VJ5WS3fcVx0bejE/YHfbDKR/yawZgKqn/if+oEeLqNwBtPzVB06olkfcnojTmEMX+gTpH+FlQ69SHNitJ8/erQ==", + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.8.2.tgz", + "integrity": "sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@swc/helpers": "^0.5.0" } @@ -918,9 +921,10 @@ "license": "Apache-2.0" }, "node_modules/@lucide/svelte": { - "version": "0.511.0", - "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.511.0.tgz", - "integrity": "sha512-aLCSPMUJmHlCuLXzXENXa4Z1NV2mN1iAZAFKk4bEbey+/MdsNlu+/DqwVkgW3Yvj6p8y8Vn5xZ2v9CLmPlA6Vw==", + "version": "0.513.0", + "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.513.0.tgz", + "integrity": "sha512-XwBQMQkMlr9qp9yVg+epx5MzhBBrqul8atO00y/ZfhlKRJuQZVmq3ELibApqyBtj9ys0Ai4FH/SZcODTUFYXig==", + "dev": true, "license": "ISC", "peerDependencies": { "svelte": "^5" @@ -2075,40 +2079,42 @@ "dev": true }, "node_modules/bits-ui": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.5.3.tgz", - "integrity": "sha512-BTZ9/GU11DaEGyQp+AY+sXCMLZO0gbDC5J8l7+Ngj4Vf6hNOwrpMmoh5iuKktA6cphXYolVkUDgBWmkh415I+w==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.5.0.tgz", + "integrity": "sha512-PbjylA1UWd4A/c5AYqie/EVxQ1/8uugmJKLg9whLoBBHbfPEBGhK09dCPrahK9kA6DRHhMmij0XXIUGIfrmNow==", "dev": true, "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.6.4", - "@floating-ui/dom": "^1.6.7", - "@internationalized/date": "^3.5.6", + "@floating-ui/core": "^1.7.0", + "@floating-ui/dom": "^1.7.0", + "css.escape": "^1.5.1", "esm-env": "^1.1.2", - "runed": "^0.23.2", - "svelte-toolbelt": "^0.7.1", + "runed": "^0.28.0", + "svelte-toolbelt": "^0.9.1", "tabbable": "^6.2.0" }, "engines": { - "node": ">=18", + "node": ">=20", "pnpm": ">=8.7.0" }, "funding": { "url": "https://github.com/sponsors/huntabyte" }, "peerDependencies": { - "svelte": "^5.11.0" + "@internationalized/date": "^3.8.1", + "svelte": "^5.33.0" } }, "node_modules/bits-ui/node_modules/runed": { - "version": "0.23.4", - "resolved": "https://registry.npmjs.org/runed/-/runed-0.23.4.tgz", - "integrity": "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.28.0.tgz", + "integrity": "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==", "dev": true, "funding": [ "https://github.com/sponsors/huntabyte", "https://github.com/sponsors/tglide" ], + "license": "MIT", "dependencies": { "esm-env": "^1.0.0" }, @@ -2116,6 +2122,27 @@ "svelte": "^5.7.0" } }, + "node_modules/bits-ui/node_modules/svelte-toolbelt": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.9.1.tgz", + "integrity": "sha512-wBX6MtYw/kpht80j5zLpxJyR9soLizXPIAIWEVd9llAi17SR44ZdG291bldjB7r/K5duC0opDFcuhk2cA1hb8g==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte" + ], + "dependencies": { + "clsx": "^2.1.1", + "runed": "^0.28.0", + "style-to-object": "^1.0.8" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "peerDependencies": { + "svelte": "^5.30.2" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2341,6 +2368,13 @@ "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in." }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2413,15 +2447,6 @@ "node": ">=0.10.0" } }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2546,15 +2571,6 @@ "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", "optional": true }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2764,9 +2780,9 @@ } }, "node_modules/esrap": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.6.tgz", - "integrity": "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==", + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.9.tgz", + "integrity": "sha512-3OMlcd0a03UGuZpPeUC1HxR3nA23l+HEyCiZw3b3FumJIN9KphoGzDJKMXI1S72jVS1dsenDyQC0kJlO1U9E1g==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -3175,21 +3191,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3229,18 +3230,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3837,23 +3826,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "license": "MIT", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4419,112 +4391,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/rollup-plugin-visualizer": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-6.0.1.tgz", - "integrity": "sha512-NjlGElvLXCSZSAi3gNRZbfX3qlQbQcJ9TW97c5JpqfVwMhttj9YwEdPwcvbKj91RnMX2PWAjonvSEv6UEYtnRQ==", - "license": "MIT", - "dependencies": { - "open": "^8.0.0", - "picomatch": "^4.0.2", - "source-map": "^0.7.4", - "yargs": "^17.5.1" - }, - "bin": { - "rollup-plugin-visualizer": "dist/bin/cli.js" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "rolldown": "1.x", - "rollup": "2.x || 3.x || 4.x" - }, - "peerDependenciesMeta": { - "rolldown": { - "optional": true - }, - "rollup": { - "optional": true - } - } - }, - "node_modules/rollup-plugin-visualizer/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/rollup-plugin-visualizer/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 8" - } - }, - "node_modules/rollup-plugin-visualizer/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/rollup-plugin-visualizer/node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/rollup-plugin-visualizer/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/rollup-plugin-visualizer/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4742,9 +4608,9 @@ } }, "node_modules/svelte": { - "version": "5.31.1", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.31.1.tgz", - "integrity": "sha512-09fup3U7NQobUCUJnLhed6pxG6MzUS8rPsALB5Jr8m8u3pVKITs0ejYiKS/wsVjfkXHvKc2g260KA8o7dWypHA==", + "version": "5.33.18", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.33.18.tgz", + "integrity": "sha512-GVAhi8vi8pGne/wlEdnfWIJvSR9eKvEknxjfL5Sr8gQALiyk8Ey+H0lhUYLpjW+MrqgH9h4dgh2NF6/BTFprRg==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", @@ -4756,7 +4622,7 @@ "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", - "esrap": "^1.4.6", + "esrap": "^1.4.8", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", diff --git a/frontend/package.json b/frontend/package.json index c62aa09f..5ac68079 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,6 @@ "format": "prettier --write ." }, "dependencies": { - "@lucide/svelte": "^0.511.0", "@simplewebauthn/browser": "^13.1.0", "@tailwindcss/vite": "^4.1.7", "axios": "^1.8.2", @@ -29,7 +28,8 @@ "@inlang/paraglide-js": "^2.0.13", "@inlang/plugin-m-function-matcher": "^2.0.10", "@inlang/plugin-message-format": "^4.0.0", - "@internationalized/date": "^3.7.0", + "@internationalized/date": "^3.8.2", + "@lucide/svelte": "^0.513.0", "@playwright/test": "^1.50.0", "@sveltejs/adapter-static": "^3.0.8", "@sveltejs/kit": "^2.20.7", @@ -37,7 +37,7 @@ "@types/eslint": "^9.6.1", "@types/node": "^22.10.10", "@types/qrcode": "^1.5.5", - "bits-ui": "^1.5.3", + "bits-ui": "^2.5.0", "eslint": "^9.19.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-svelte": "^2.46.1", diff --git a/frontend/src/lib/components/form/switch-with-label.svelte b/frontend/src/lib/components/form/switch-with-label.svelte new file mode 100644 index 00000000..cc7cfc5c --- /dev/null +++ b/frontend/src/lib/components/form/switch-with-label.svelte @@ -0,0 +1,39 @@ + + +
+ onCheckedChange && onCheckedChange(v == true)} + bind:checked + /> +
+ + {#if description} +

+ {description} +

+ {/if} +
+
diff --git a/frontend/src/lib/components/ui/radio-group/index.ts b/frontend/src/lib/components/ui/radio-group/index.ts new file mode 100644 index 00000000..a8bb80f9 --- /dev/null +++ b/frontend/src/lib/components/ui/radio-group/index.ts @@ -0,0 +1,10 @@ +import Root from './radio-group.svelte'; +import Item from './radio-group-item.svelte'; + +export { + Root, + Item, + // + Root as RadioGroup, + Item as RadioGroupItem +}; diff --git a/frontend/src/lib/components/ui/radio-group/radio-group-item.svelte b/frontend/src/lib/components/ui/radio-group/radio-group-item.svelte new file mode 100644 index 00000000..43add264 --- /dev/null +++ b/frontend/src/lib/components/ui/radio-group/radio-group-item.svelte @@ -0,0 +1,31 @@ + + + + {#snippet children({ checked })} +
+ {#if checked} + + {/if} +
+ {/snippet} +
diff --git a/frontend/src/lib/components/ui/radio-group/radio-group.svelte b/frontend/src/lib/components/ui/radio-group/radio-group.svelte new file mode 100644 index 00000000..9ac7cab1 --- /dev/null +++ b/frontend/src/lib/components/ui/radio-group/radio-group.svelte @@ -0,0 +1,19 @@ + + + diff --git a/frontend/src/lib/components/ui/switch/index.ts b/frontend/src/lib/components/ui/switch/index.ts new file mode 100644 index 00000000..129f8f5c --- /dev/null +++ b/frontend/src/lib/components/ui/switch/index.ts @@ -0,0 +1,7 @@ +import Root from './switch.svelte'; + +export { + Root, + // + Root as Switch +}; diff --git a/frontend/src/lib/components/ui/switch/switch.svelte b/frontend/src/lib/components/ui/switch/switch.svelte new file mode 100644 index 00000000..aead7cba --- /dev/null +++ b/frontend/src/lib/components/ui/switch/switch.svelte @@ -0,0 +1,29 @@ + + + + + diff --git a/frontend/src/lib/stores/application-configuration-store.ts b/frontend/src/lib/stores/application-configuration-store.ts index dbeb4174..ff278004 100644 --- a/frontend/src/lib/stores/application-configuration-store.ts +++ b/frontend/src/lib/stores/application-configuration-store.ts @@ -1,5 +1,6 @@ import AppConfigService from '$lib/services/app-config-service'; import type { AppConfig } from '$lib/types/application-configuration'; +import { applyAccentColor } from '$lib/utils/accent-color-util'; import { writable } from 'svelte/store'; const appConfigStore = writable(); @@ -8,10 +9,11 @@ const appConfigService = new AppConfigService(); const reload = async () => { const appConfig = await appConfigService.list(); - appConfigStore.set(appConfig); + set(appConfig); }; const set = (appConfig: AppConfig) => { + applyAccentColor(appConfig.accentColor); appConfigStore.set(appConfig); }; diff --git a/frontend/src/lib/types/application-configuration.ts b/frontend/src/lib/types/application-configuration.ts index cc2fc699..cdd8154e 100644 --- a/frontend/src/lib/types/application-configuration.ts +++ b/frontend/src/lib/types/application-configuration.ts @@ -6,6 +6,7 @@ export type AppConfig = { ldapEnabled: boolean; disableAnimations: boolean; uiConfigDisabled: boolean; + accentColor: string; }; export type AllAppConfig = AppConfig & { diff --git a/frontend/src/lib/utils/accent-color-util.ts b/frontend/src/lib/utils/accent-color-util.ts new file mode 100644 index 00000000..775618f8 --- /dev/null +++ b/frontend/src/lib/utils/accent-color-util.ts @@ -0,0 +1,58 @@ +export function applyAccentColor(accentValue: string) { + if (accentValue === 'default') { + document.documentElement.style.removeProperty('--primary'); + document.documentElement.style.removeProperty('--primary-foreground'); + document.documentElement.style.removeProperty('--ring'); + document.documentElement.style.removeProperty('--sidebar-ring'); + return; + } + + document.documentElement.style.setProperty('--primary', accentValue); + + // Smart foreground color selection based on brightness + const foregroundColor = getContrastingForeground(accentValue); + document.documentElement.style.setProperty('--primary-foreground', foregroundColor); + + // Create proper ring colors based on input format + const ringColor = `color-mix(in srgb, ${accentValue} 50%, transparent)`; + document.documentElement.style.setProperty('--ring', ringColor); + document.documentElement.style.setProperty('--sidebar-ring', ringColor); +} + +function getContrastingForeground(color: string): string { + const brightness = getColorBrightness(color); + + // Use white text for dark colors, black text for light colors + return brightness < 0.55 ? 'oklch(0.98 0 0)' : 'oklch(0.09 0 0)'; +} + +function getColorBrightness(color: string): number { + // Create a temporary element to get computed color + const tempElement = document.createElement('div'); + tempElement.style.color = color; + document.body.appendChild(tempElement); + + const computedColor = window.getComputedStyle(tempElement).color; + document.body.removeChild(tempElement); + + // Parse RGB values from computed color + const rgbMatch = computedColor.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); + if (!rgbMatch) { + // Fallback: assume medium brightness + return 0.5; + } + + const [, r, g, b] = rgbMatch.map(Number); + + // Calculate relative luminance using the standard formula + // https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html + const sR = r / 255; + const sG = g / 255; + const sB = b / 255; + + const rLinear = sR <= 0.03928 ? sR / 12.92 : Math.pow((sR + 0.055) / 1.055, 2.4); + const gLinear = sG <= 0.03928 ? sG / 12.92 : Math.pow((sG + 0.055) / 1.055, 2.4); + const bLinear = sB <= 0.03928 ? sB / 12.92 : Math.pow((sB + 0.055) / 1.055, 2.4); + + return 0.2126 * rLinear + 0.7152 * gLinear + 0.0722 * bLinear; +} diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 5b9dffbe..e9e47dab 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -44,6 +44,17 @@
{@render children()} {/if} - + diff --git a/frontend/src/routes/settings/admin/application-configuration/forms/accent-color-picker.svelte b/frontend/src/routes/settings/admin/application-configuration/forms/accent-color-picker.svelte new file mode 100644 index 00000000..3e3c1839 --- /dev/null +++ b/frontend/src/routes/settings/admin/application-configuration/forms/accent-color-picker.svelte @@ -0,0 +1,103 @@ + + + { + if (value != 'custom') { + handleAccentColorChange(value); + } + }} +> + {#each accentColors as accent} + {@render colorOption(accent.label, accent.color, selectedColor === accent.color)} + {/each} + {#if isCustomColor || isPreviousColorCustom} + {@render colorOption('Custom', isCustomColor ? selectedColor : previousColor, isCustomColor)} + {/if} + {@render colorOption('Custom', 'custom', false, true)} + + + + +{#snippet colorOption( + label: string, + color: string, + isSelected: boolean, + isCustomColorSelection = false +)} +
+ + +
+{/snippet} diff --git a/frontend/src/routes/settings/admin/application-configuration/forms/app-config-email-form.svelte b/frontend/src/routes/settings/admin/application-configuration/forms/app-config-email-form.svelte index f574d8b2..e79e233f 100644 --- a/frontend/src/routes/settings/admin/application-configuration/forms/app-config-email-form.svelte +++ b/frontend/src/routes/settings/admin/application-configuration/forms/app-config-email-form.svelte @@ -1,6 +1,6 @@ + + + + + {m.custom_accent_color()} + + {m.custom_accent_color_description()} + + + +
+
+
+ +
+
+ +
+
+
+
+
+ + + + + +
+
+
diff --git a/frontend/src/routes/settings/admin/oidc-clients/oidc-client-form.svelte b/frontend/src/routes/settings/admin/oidc-clients/oidc-client-form.svelte index a7c53a38..d67e7f86 100644 --- a/frontend/src/routes/settings/admin/oidc-clients/oidc-client-form.svelte +++ b/frontend/src/routes/settings/admin/oidc-clients/oidc-client-form.svelte @@ -1,5 +1,5 @@