fix: replace native color picker and confirm dialog with custom components (fixes #12)
- commit
- 12c9f08c223aa1b1e7ad75a636da05faab122f3c
- author
- brice · · 2025-12-23 20:15 UTC
- committer
- brice · · 2025-12-24 16:11 UTC
- parent
- f8b9839
- signature
- unsigned
17 files · +381 -19
M webui/package-lock.json +32 -0
| @@ -26,6 +26,7 @@ | |||
| 26 | 26 | "eslint": "^8.57.0", | |
| 27 | 27 | "postcss": "^8.4.47", | |
| 28 | 28 | "svelte": "^5.0.0", | |
| 29 | + | "svelte-awesome-color-picker": "^4.1.0", | |
| 29 | 30 | "svelte-check": "^4.0.0", | |
| 30 | 31 | "tailwind-variants": "^3.2.2", | |
| 31 | 32 | "tailwindcss": "^4.0.0", | |
| @@ -2284,6 +2285,13 @@ | |||
| 2284 | 2285 | "dev": true, | |
| 2285 | 2286 | "license": "MIT" | |
| 2286 | 2287 | }, | |
| 2288 | + | "node_modules/colord": { | |
| 2289 | + | "version": "2.9.3", | |
| 2290 | + | "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", | |
| 2291 | + | "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", | |
| 2292 | + | "dev": true, | |
| 2293 | + | "license": "MIT" | |
| 2294 | + | }, | |
| 2287 | 2295 | "node_modules/concat-map": { | |
| 2288 | 2296 | "version": "0.0.1", | |
| 2289 | 2297 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", | |
| @@ -4078,6 +4086,30 @@ | |||
| 4078 | 4086 | "node": ">=18" | |
| 4079 | 4087 | } | |
| 4080 | 4088 | }, | |
| 4089 | + | "node_modules/svelte-awesome-color-picker": { | |
| 4090 | + | "version": "4.1.0", | |
| 4091 | + | "resolved": "https://registry.npmjs.org/svelte-awesome-color-picker/-/svelte-awesome-color-picker-4.1.0.tgz", | |
| 4092 | + | "integrity": "sha512-afiSB3eTBlqu96f4+rjBvqG3eCaLwuneNYHe587Wr4Ien6yQWeztGZunPT0FmiI7wFFBVNUlJQLYutII8LfQUg==", | |
| 4093 | + | "dev": true, | |
| 4094 | + | "license": "MIT", | |
| 4095 | + | "dependencies": { | |
| 4096 | + | "colord": "^2.9.3", | |
| 4097 | + | "svelte-awesome-slider": "2.0.0" | |
| 4098 | + | }, | |
| 4099 | + | "peerDependencies": { | |
| 4100 | + | "svelte": "^5.0.0" | |
| 4101 | + | } | |
| 4102 | + | }, | |
| 4103 | + | "node_modules/svelte-awesome-slider": { | |
| 4104 | + | "version": "2.0.0", | |
| 4105 | + | "resolved": "https://registry.npmjs.org/svelte-awesome-slider/-/svelte-awesome-slider-2.0.0.tgz", | |
| 4106 | + | "integrity": "sha512-YBkOdYm1Feaqsn2JkJBRs+Kc/X3Qy/3GuVmI7GmoYDjBaHkjx9uH4khTuED22z57Hg3gGWeDhp/clIjWDdLNaw==", | |
| 4107 | + | "dev": true, | |
| 4108 | + | "license": "MIT", | |
| 4109 | + | "peerDependencies": { | |
| 4110 | + | "svelte": "^5.0.0" | |
| 4111 | + | } | |
| 4112 | + | }, | |
| 4081 | 4113 | "node_modules/svelte-check": { | |
| 4082 | 4114 | "version": "4.3.4", | |
| 4083 | 4115 | "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.4.tgz", | |
| @@ -26,6 +26,7 @@ | |||||
| 26 | "eslint": "^8.57.0", | 26 | "eslint": "^8.57.0", | ||
| 27 | "postcss": "^8.4.47", | 27 | "postcss": "^8.4.47", | ||
| 28 | "svelte": "^5.0.0", | 28 | "svelte": "^5.0.0", | ||
| 29 | + | "svelte-awesome-color-picker": "^4.1.0", | |||
| 29 | "svelte-check": "^4.0.0", | 30 | "svelte-check": "^4.0.0", | ||
| 30 | "tailwind-variants": "^3.2.2", | 31 | "tailwind-variants": "^3.2.2", | ||
| 31 | "tailwindcss": "^4.0.0", | 32 | "tailwindcss": "^4.0.0", | ||
| @@ -2284,6 +2285,13 @@ | |||||
| 2284 | "dev": true, | 2285 | "dev": true, | ||
| 2285 | "license": "MIT" | 2286 | "license": "MIT" | ||
| 2286 | }, | 2287 | }, | ||
| 2288 | + | "node_modules/colord": { | |||
| 2289 | + | "version": "2.9.3", | |||
| 2290 | + | "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", | |||
| 2291 | + | "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", | |||
| 2292 | + | "dev": true, | |||
| 2293 | + | "license": "MIT" | |||
| 2294 | + | }, | |||
| 2287 | "node_modules/concat-map": { | 2295 | "node_modules/concat-map": { | ||
| 2288 | "version": "0.0.1", | 2296 | "version": "0.0.1", | ||
| 2289 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", | 2297 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", | ||
| @@ -4078,6 +4086,30 @@ | |||||
| 4078 | "node": ">=18" | 4086 | "node": ">=18" | ||
| 4079 | } | 4087 | } | ||
| 4080 | }, | 4088 | }, | ||
| 4089 | + | "node_modules/svelte-awesome-color-picker": { | |||
| 4090 | + | "version": "4.1.0", | |||
| 4091 | + | "resolved": "https://registry.npmjs.org/svelte-awesome-color-picker/-/svelte-awesome-color-picker-4.1.0.tgz", | |||
| 4092 | + | "integrity": "sha512-afiSB3eTBlqu96f4+rjBvqG3eCaLwuneNYHe587Wr4Ien6yQWeztGZunPT0FmiI7wFFBVNUlJQLYutII8LfQUg==", | |||
| 4093 | + | "dev": true, | |||
| 4094 | + | "license": "MIT", | |||
| 4095 | + | "dependencies": { | |||
| 4096 | + | "colord": "^2.9.3", | |||
| 4097 | + | "svelte-awesome-slider": "2.0.0" | |||
| 4098 | + | }, | |||
| 4099 | + | "peerDependencies": { | |||
| 4100 | + | "svelte": "^5.0.0" | |||
| 4101 | + | } | |||
| 4102 | + | }, | |||
| 4103 | + | "node_modules/svelte-awesome-slider": { | |||
| 4104 | + | "version": "2.0.0", | |||
| 4105 | + | "resolved": "https://registry.npmjs.org/svelte-awesome-slider/-/svelte-awesome-slider-2.0.0.tgz", | |||
| 4106 | + | "integrity": "sha512-YBkOdYm1Feaqsn2JkJBRs+Kc/X3Qy/3GuVmI7GmoYDjBaHkjx9uH4khTuED22z57Hg3gGWeDhp/clIjWDdLNaw==", | |||
| 4107 | + | "dev": true, | |||
| 4108 | + | "license": "MIT", | |||
| 4109 | + | "peerDependencies": { | |||
| 4110 | + | "svelte": "^5.0.0" | |||
| 4111 | + | } | |||
| 4112 | + | }, | |||
| 4081 | "node_modules/svelte-check": { | 4113 | "node_modules/svelte-check": { | ||
| 4082 | "version": "4.3.4", | 4114 | "version": "4.3.4", | ||
| 4083 | "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.4.tgz", | 4115 | "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.4.tgz", | ||
M webui/package.json +1 -0
| @@ -29,6 +29,7 @@ | |||
| 29 | 29 | "eslint": "^8.57.0", | |
| 30 | 30 | "postcss": "^8.4.47", | |
| 31 | 31 | "svelte": "^5.0.0", | |
| 32 | + | "svelte-awesome-color-picker": "^4.1.0", | |
| 32 | 33 | "svelte-check": "^4.0.0", | |
| 33 | 34 | "tailwind-variants": "^3.2.2", | |
| 34 | 35 | "tailwindcss": "^4.0.0", | |
| @@ -29,6 +29,7 @@ | |||||
| 29 | "eslint": "^8.57.0", | 29 | "eslint": "^8.57.0", | ||
| 30 | "postcss": "^8.4.47", | 30 | "postcss": "^8.4.47", | ||
| 31 | "svelte": "^5.0.0", | 31 | "svelte": "^5.0.0", | ||
| 32 | + | "svelte-awesome-color-picker": "^4.1.0", | |||
| 32 | "svelte-check": "^4.0.0", | 33 | "svelte-check": "^4.0.0", | ||
| 33 | "tailwind-variants": "^3.2.2", | 34 | "tailwind-variants": "^3.2.2", | ||
| 34 | "tailwindcss": "^4.0.0", | 35 | "tailwindcss": "^4.0.0", | ||
A webui/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte +17 -0
| @@ -0,0 +1,17 @@ | |||
| 1 | + | <script lang="ts"> | |
| 2 | + | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |
| 3 | + | import { buttonVariants } from "$lib/components/ui/button/index.js"; | |
| 4 | + | import { cn } from "$lib/utils.js"; | |
| 5 | + | ||
| 6 | + | let { | |
| 7 | + | ref = $bindable(null), | |
| 8 | + | class: className, | |
| 9 | + | ...restProps | |
| 10 | + | }: AlertDialogPrimitive.ActionProps = $props(); | |
| 11 | + | </script> | |
| 12 | + | ||
| 13 | + | <AlertDialogPrimitive.Action | |
| 14 | + | bind:ref | |
| 15 | + | class={cn(buttonVariants(), className)} | |
| 16 | + | {...restProps} | |
| 17 | + | /> | |
| @@ -0,0 +1,17 @@ | |||||
| 1 | + | <script lang="ts"> | |||
| 2 | + | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |||
| 3 | + | import { buttonVariants } from "$lib/components/ui/button/index.js"; | |||
| 4 | + | import { cn } from "$lib/utils.js"; | |||
| 5 | + | ||||
| 6 | + | let { | |||
| 7 | + | ref = $bindable(null), | |||
| 8 | + | class: className, | |||
| 9 | + | ...restProps | |||
| 10 | + | }: AlertDialogPrimitive.ActionProps = $props(); | |||
| 11 | + | </script> | |||
| 12 | + | ||||
| 13 | + | <AlertDialogPrimitive.Action | |||
| 14 | + | bind:ref | |||
| 15 | + | class={cn(buttonVariants(), className)} | |||
| 16 | + | {...restProps} | |||
| 17 | + | /> | |||
A webui/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte +17 -0
| @@ -0,0 +1,17 @@ | |||
| 1 | + | <script lang="ts"> | |
| 2 | + | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |
| 3 | + | import { buttonVariants } from "$lib/components/ui/button/index.js"; | |
| 4 | + | import { cn } from "$lib/utils.js"; | |
| 5 | + | ||
| 6 | + | let { | |
| 7 | + | ref = $bindable(null), | |
| 8 | + | class: className, | |
| 9 | + | ...restProps | |
| 10 | + | }: AlertDialogPrimitive.CancelProps = $props(); | |
| 11 | + | </script> | |
| 12 | + | ||
| 13 | + | <AlertDialogPrimitive.Cancel | |
| 14 | + | bind:ref | |
| 15 | + | class={cn(buttonVariants({ variant: "outline" }), className)} | |
| 16 | + | {...restProps} | |
| 17 | + | /> | |
| @@ -0,0 +1,17 @@ | |||||
| 1 | + | <script lang="ts"> | |||
| 2 | + | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |||
| 3 | + | import { buttonVariants } from "$lib/components/ui/button/index.js"; | |||
| 4 | + | import { cn } from "$lib/utils.js"; | |||
| 5 | + | ||||
| 6 | + | let { | |||
| 7 | + | ref = $bindable(null), | |||
| 8 | + | class: className, | |||
| 9 | + | ...restProps | |||
| 10 | + | }: AlertDialogPrimitive.CancelProps = $props(); | |||
| 11 | + | </script> | |||
| 12 | + | ||||
| 13 | + | <AlertDialogPrimitive.Cancel | |||
| 14 | + | bind:ref | |||
| 15 | + | class={cn(buttonVariants({ variant: "outline" }), className)} | |||
| 16 | + | {...restProps} | |||
| 17 | + | /> | |||
A webui/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte +24 -0
| @@ -0,0 +1,24 @@ | |||
| 1 | + | <script lang="ts"> | |
| 2 | + | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |
| 3 | + | import { cn } from "$lib/utils.js"; | |
| 4 | + | import AlertDialogOverlay from "./alert-dialog-overlay.svelte"; | |
| 5 | + | import AlertDialogPortal from "./alert-dialog-portal.svelte"; | |
| 6 | + | ||
| 7 | + | let { | |
| 8 | + | ref = $bindable(null), | |
| 9 | + | class: className, | |
| 10 | + | ...restProps | |
| 11 | + | }: AlertDialogPrimitive.ContentProps = $props(); | |
| 12 | + | </script> | |
| 13 | + | ||
| 14 | + | <AlertDialogPortal> | |
| 15 | + | <AlertDialogOverlay /> | |
| 16 | + | <AlertDialogPrimitive.Content | |
| 17 | + | bind:ref | |
| 18 | + | class={cn( | |
| 19 | + | "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border border-border p-6 shadow-lg duration-200 sm:rounded-lg", | |
| 20 | + | className | |
| 21 | + | )} | |
| 22 | + | {...restProps} | |
| 23 | + | /> | |
| 24 | + | </AlertDialogPortal> | |
| @@ -0,0 +1,24 @@ | |||||
| 1 | + | <script lang="ts"> | |||
| 2 | + | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |||
| 3 | + | import { cn } from "$lib/utils.js"; | |||
| 4 | + | import AlertDialogOverlay from "./alert-dialog-overlay.svelte"; | |||
| 5 | + | import AlertDialogPortal from "./alert-dialog-portal.svelte"; | |||
| 6 | + | ||||
| 7 | + | let { | |||
| 8 | + | ref = $bindable(null), | |||
| 9 | + | class: className, | |||
| 10 | + | ...restProps | |||
| 11 | + | }: AlertDialogPrimitive.ContentProps = $props(); | |||
| 12 | + | </script> | |||
| 13 | + | ||||
| 14 | + | <AlertDialogPortal> | |||
| 15 | + | <AlertDialogOverlay /> | |||
| 16 | + | <AlertDialogPrimitive.Content | |||
| 17 | + | bind:ref | |||
| 18 | + | class={cn( | |||
| 19 | + | "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border border-border p-6 shadow-lg duration-200 sm:rounded-lg", | |||
| 20 | + | className | |||
| 21 | + | )} | |||
| 22 | + | {...restProps} | |||
| 23 | + | /> | |||
| 24 | + | </AlertDialogPortal> | |||
A webui/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte +16 -0
| @@ -0,0 +1,16 @@ | |||
| 1 | + | <script lang="ts"> | |
| 2 | + | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |
| 3 | + | import { cn } from "$lib/utils.js"; | |
| 4 | + | ||
| 5 | + | let { | |
| 6 | + | ref = $bindable(null), | |
| 7 | + | class: className, | |
| 8 | + | ...restProps | |
| 9 | + | }: AlertDialogPrimitive.DescriptionProps = $props(); | |
| 10 | + | </script> | |
| 11 | + | ||
| 12 | + | <AlertDialogPrimitive.Description | |
| 13 | + | bind:ref | |
| 14 | + | class={cn("text-sm text-muted-foreground", className)} | |
| 15 | + | {...restProps} | |
| 16 | + | /> | |
| @@ -0,0 +1,16 @@ | |||||
| 1 | + | <script lang="ts"> | |||
| 2 | + | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |||
| 3 | + | import { cn } from "$lib/utils.js"; | |||
| 4 | + | ||||
| 5 | + | let { | |||
| 6 | + | ref = $bindable(null), | |||
| 7 | + | class: className, | |||
| 8 | + | ...restProps | |||
| 9 | + | }: AlertDialogPrimitive.DescriptionProps = $props(); | |||
| 10 | + | </script> | |||
| 11 | + | ||||
| 12 | + | <AlertDialogPrimitive.Description | |||
| 13 | + | bind:ref | |||
| 14 | + | class={cn("text-sm text-muted-foreground", className)} | |||
| 15 | + | {...restProps} | |||
| 16 | + | /> | |||
A webui/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte +19 -0
| @@ -0,0 +1,19 @@ | |||
| 1 | + | <script lang="ts"> | |
| 2 | + | import type { HTMLAttributes } from "svelte/elements"; | |
| 3 | + | import { cn, type WithElementRef } from "$lib/utils.js"; | |
| 4 | + | ||
| 5 | + | let { | |
| 6 | + | ref = $bindable(null), | |
| 7 | + | class: className, | |
| 8 | + | children, | |
| 9 | + | ...restProps | |
| 10 | + | }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); | |
| 11 | + | </script> | |
| 12 | + | ||
| 13 | + | <div | |
| 14 | + | bind:this={ref} | |
| 15 | + | class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)} | |
| 16 | + | {...restProps} | |
| 17 | + | > | |
| 18 | + | {@render children?.()} | |
| 19 | + | </div> | |
| @@ -0,0 +1,19 @@ | |||||
| 1 | + | <script lang="ts"> | |||
| 2 | + | import type { HTMLAttributes } from "svelte/elements"; | |||
| 3 | + | import { cn, type WithElementRef } from "$lib/utils.js"; | |||
| 4 | + | ||||
| 5 | + | let { | |||
| 6 | + | ref = $bindable(null), | |||
| 7 | + | class: className, | |||
| 8 | + | children, | |||
| 9 | + | ...restProps | |||
| 10 | + | }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); | |||
| 11 | + | </script> | |||
| 12 | + | ||||
| 13 | + | <div | |||
| 14 | + | bind:this={ref} | |||
| 15 | + | class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)} | |||
| 16 | + | {...restProps} | |||
| 17 | + | > | |||
| 18 | + | {@render children?.()} | |||
| 19 | + | </div> | |||
A webui/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte +19 -0
| @@ -0,0 +1,19 @@ | |||
| 1 | + | <script lang="ts"> | |
| 2 | + | import type { HTMLAttributes } from "svelte/elements"; | |
| 3 | + | import { cn, type WithElementRef } from "$lib/utils.js"; | |
| 4 | + | ||
| 5 | + | let { | |
| 6 | + | ref = $bindable(null), | |
| 7 | + | class: className, | |
| 8 | + | children, | |
| 9 | + | ...restProps | |
| 10 | + | }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); | |
| 11 | + | </script> | |
| 12 | + | ||
| 13 | + | <div | |
| 14 | + | bind:this={ref} | |
| 15 | + | class={cn("flex flex-col gap-2 text-center sm:text-left", className)} | |
| 16 | + | {...restProps} | |
| 17 | + | > | |
| 18 | + | {@render children?.()} | |
| 19 | + | </div> | |
| @@ -0,0 +1,19 @@ | |||||
| 1 | + | <script lang="ts"> | |||
| 2 | + | import type { HTMLAttributes } from "svelte/elements"; | |||
| 3 | + | import { cn, type WithElementRef } from "$lib/utils.js"; | |||
| 4 | + | ||||
| 5 | + | let { | |||
| 6 | + | ref = $bindable(null), | |||
| 7 | + | class: className, | |||
| 8 | + | children, | |||
| 9 | + | ...restProps | |||
| 10 | + | }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); | |||
| 11 | + | </script> | |||
| 12 | + | ||||
| 13 | + | <div | |||
| 14 | + | bind:this={ref} | |||
| 15 | + | class={cn("flex flex-col gap-2 text-center sm:text-left", className)} | |||
| 16 | + | {...restProps} | |||
| 17 | + | > | |||
| 18 | + | {@render children?.()} | |||
| 19 | + | </div> | |||
A webui/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte +19 -0
| @@ -0,0 +1,19 @@ | |||
| 1 | + | <script lang="ts"> | |
| 2 | + | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |
| 3 | + | import { cn } from "$lib/utils.js"; | |
| 4 | + | ||
| 5 | + | let { | |
| 6 | + | ref = $bindable(null), | |
| 7 | + | class: className, | |
| 8 | + | ...restProps | |
| 9 | + | }: AlertDialogPrimitive.OverlayProps = $props(); | |
| 10 | + | </script> | |
| 11 | + | ||
| 12 | + | <AlertDialogPrimitive.Overlay | |
| 13 | + | bind:ref | |
| 14 | + | class={cn( | |
| 15 | + | "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80", | |
| 16 | + | className | |
| 17 | + | )} | |
| 18 | + | {...restProps} | |
| 19 | + | /> | |
| @@ -0,0 +1,19 @@ | |||||
| 1 | + | <script lang="ts"> | |||
| 2 | + | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |||
| 3 | + | import { cn } from "$lib/utils.js"; | |||
| 4 | + | ||||
| 5 | + | let { | |||
| 6 | + | ref = $bindable(null), | |||
| 7 | + | class: className, | |||
| 8 | + | ...restProps | |||
| 9 | + | }: AlertDialogPrimitive.OverlayProps = $props(); | |||
| 10 | + | </script> | |||
| 11 | + | ||||
| 12 | + | <AlertDialogPrimitive.Overlay | |||
| 13 | + | bind:ref | |||
| 14 | + | class={cn( | |||
| 15 | + | "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80", | |||
| 16 | + | className | |||
| 17 | + | )} | |||
| 18 | + | {...restProps} | |||
| 19 | + | /> | |||
A webui/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | <script lang="ts"> | |
| 2 | + | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |
| 3 | + | ||
| 4 | + | let { ...restProps }: AlertDialogPrimitive.PortalProps = $props(); | |
| 5 | + | </script> | |
| 6 | + | ||
| 7 | + | <AlertDialogPrimitive.Portal {...restProps} /> | |
| @@ -0,0 +1,7 @@ | |||||
| 1 | + | <script lang="ts"> | |||
| 2 | + | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |||
| 3 | + | ||||
| 4 | + | let { ...restProps }: AlertDialogPrimitive.PortalProps = $props(); | |||
| 5 | + | </script> | |||
| 6 | + | ||||
| 7 | + | <AlertDialogPrimitive.Portal {...restProps} /> | |||
A webui/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte +16 -0
| @@ -0,0 +1,16 @@ | |||
| 1 | + | <script lang="ts"> | |
| 2 | + | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |
| 3 | + | import { cn } from "$lib/utils.js"; | |
| 4 | + | ||
| 5 | + | let { | |
| 6 | + | ref = $bindable(null), | |
| 7 | + | class: className, | |
| 8 | + | ...restProps | |
| 9 | + | }: AlertDialogPrimitive.TitleProps = $props(); | |
| 10 | + | </script> | |
| 11 | + | ||
| 12 | + | <AlertDialogPrimitive.Title | |
| 13 | + | bind:ref | |
| 14 | + | class={cn("text-lg font-semibold", className)} | |
| 15 | + | {...restProps} | |
| 16 | + | /> | |
| @@ -0,0 +1,16 @@ | |||||
| 1 | + | <script lang="ts"> | |||
| 2 | + | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |||
| 3 | + | import { cn } from "$lib/utils.js"; | |||
| 4 | + | ||||
| 5 | + | let { | |||
| 6 | + | ref = $bindable(null), | |||
| 7 | + | class: className, | |||
| 8 | + | ...restProps | |||
| 9 | + | }: AlertDialogPrimitive.TitleProps = $props(); | |||
| 10 | + | </script> | |||
| 11 | + | ||||
| 12 | + | <AlertDialogPrimitive.Title | |||
| 13 | + | bind:ref | |||
| 14 | + | class={cn("text-lg font-semibold", className)} | |||
| 15 | + | {...restProps} | |||
| 16 | + | /> | |||
A webui/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte +16 -0
| @@ -0,0 +1,16 @@ | |||
| 1 | + | <script lang="ts"> | |
| 2 | + | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |
| 3 | + | import { cn } from "$lib/utils.js"; | |
| 4 | + | ||
| 5 | + | let { | |
| 6 | + | ref = $bindable(null), | |
| 7 | + | class: className, | |
| 8 | + | ...restProps | |
| 9 | + | }: AlertDialogPrimitive.TriggerProps = $props(); | |
| 10 | + | </script> | |
| 11 | + | ||
| 12 | + | <AlertDialogPrimitive.Trigger | |
| 13 | + | bind:ref | |
| 14 | + | class={cn(className)} | |
| 15 | + | {...restProps} | |
| 16 | + | /> | |
| @@ -0,0 +1,16 @@ | |||||
| 1 | + | <script lang="ts"> | |||
| 2 | + | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |||
| 3 | + | import { cn } from "$lib/utils.js"; | |||
| 4 | + | ||||
| 5 | + | let { | |||
| 6 | + | ref = $bindable(null), | |||
| 7 | + | class: className, | |||
| 8 | + | ...restProps | |||
| 9 | + | }: AlertDialogPrimitive.TriggerProps = $props(); | |||
| 10 | + | </script> | |||
| 11 | + | ||||
| 12 | + | <AlertDialogPrimitive.Trigger | |||
| 13 | + | bind:ref | |||
| 14 | + | class={cn(className)} | |||
| 15 | + | {...restProps} | |||
| 16 | + | /> | |||
A webui/src/lib/components/ui/alert-dialog/alert-dialog.svelte +10 -0
| @@ -0,0 +1,10 @@ | |||
| 1 | + | <script lang="ts"> | |
| 2 | + | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |
| 3 | + | ||
| 4 | + | let { | |
| 5 | + | open = $bindable(false), | |
| 6 | + | ...restProps | |
| 7 | + | }: AlertDialogPrimitive.RootProps = $props(); | |
| 8 | + | </script> | |
| 9 | + | ||
| 10 | + | <AlertDialogPrimitive.Root bind:open {...restProps} /> | |
| @@ -0,0 +1,10 @@ | |||||
| 1 | + | <script lang="ts"> | |||
| 2 | + | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |||
| 3 | + | ||||
| 4 | + | let { | |||
| 5 | + | open = $bindable(false), | |||
| 6 | + | ...restProps | |||
| 7 | + | }: AlertDialogPrimitive.RootProps = $props(); | |||
| 8 | + | </script> | |||
| 9 | + | ||||
| 10 | + | <AlertDialogPrimitive.Root bind:open {...restProps} /> | |||
A webui/src/lib/components/ui/alert-dialog/index.ts +37 -0
| @@ -0,0 +1,37 @@ | |||
| 1 | + | import Root from "./alert-dialog.svelte"; | |
| 2 | + | import Trigger from "./alert-dialog-trigger.svelte"; | |
| 3 | + | import Portal from "./alert-dialog-portal.svelte"; | |
| 4 | + | import Overlay from "./alert-dialog-overlay.svelte"; | |
| 5 | + | import Content from "./alert-dialog-content.svelte"; | |
| 6 | + | import Header from "./alert-dialog-header.svelte"; | |
| 7 | + | import Footer from "./alert-dialog-footer.svelte"; | |
| 8 | + | import Title from "./alert-dialog-title.svelte"; | |
| 9 | + | import Description from "./alert-dialog-description.svelte"; | |
| 10 | + | import Action from "./alert-dialog-action.svelte"; | |
| 11 | + | import Cancel from "./alert-dialog-cancel.svelte"; | |
| 12 | + | ||
| 13 | + | export { | |
| 14 | + | Root, | |
| 15 | + | Trigger, | |
| 16 | + | Portal, | |
| 17 | + | Overlay, | |
| 18 | + | Content, | |
| 19 | + | Header, | |
| 20 | + | Footer, | |
| 21 | + | Title, | |
| 22 | + | Description, | |
| 23 | + | Action, | |
| 24 | + | Cancel, | |
| 25 | + | // | |
| 26 | + | Root as AlertDialog, | |
| 27 | + | Trigger as AlertDialogTrigger, | |
| 28 | + | Portal as AlertDialogPortal, | |
| 29 | + | Overlay as AlertDialogOverlay, | |
| 30 | + | Content as AlertDialogContent, | |
| 31 | + | Header as AlertDialogHeader, | |
| 32 | + | Footer as AlertDialogFooter, | |
| 33 | + | Title as AlertDialogTitle, | |
| 34 | + | Description as AlertDialogDescription, | |
| 35 | + | Action as AlertDialogAction, | |
| 36 | + | Cancel as AlertDialogCancel, | |
| 37 | + | }; | |
| @@ -0,0 +1,37 @@ | |||||
| 1 | + | import Root from "./alert-dialog.svelte"; | |||
| 2 | + | import Trigger from "./alert-dialog-trigger.svelte"; | |||
| 3 | + | import Portal from "./alert-dialog-portal.svelte"; | |||
| 4 | + | import Overlay from "./alert-dialog-overlay.svelte"; | |||
| 5 | + | import Content from "./alert-dialog-content.svelte"; | |||
| 6 | + | import Header from "./alert-dialog-header.svelte"; | |||
| 7 | + | import Footer from "./alert-dialog-footer.svelte"; | |||
| 8 | + | import Title from "./alert-dialog-title.svelte"; | |||
| 9 | + | import Description from "./alert-dialog-description.svelte"; | |||
| 10 | + | import Action from "./alert-dialog-action.svelte"; | |||
| 11 | + | import Cancel from "./alert-dialog-cancel.svelte"; | |||
| 12 | + | ||||
| 13 | + | export { | |||
| 14 | + | Root, | |||
| 15 | + | Trigger, | |||
| 16 | + | Portal, | |||
| 17 | + | Overlay, | |||
| 18 | + | Content, | |||
| 19 | + | Header, | |||
| 20 | + | Footer, | |||
| 21 | + | Title, | |||
| 22 | + | Description, | |||
| 23 | + | Action, | |||
| 24 | + | Cancel, | |||
| 25 | + | // | |||
| 26 | + | Root as AlertDialog, | |||
| 27 | + | Trigger as AlertDialogTrigger, | |||
| 28 | + | Portal as AlertDialogPortal, | |||
| 29 | + | Overlay as AlertDialogOverlay, | |||
| 30 | + | Content as AlertDialogContent, | |||
| 31 | + | Header as AlertDialogHeader, | |||
| 32 | + | Footer as AlertDialogFooter, | |||
| 33 | + | Title as AlertDialogTitle, | |||
| 34 | + | Description as AlertDialogDescription, | |||
| 35 | + | Action as AlertDialogAction, | |||
| 36 | + | Cancel as AlertDialogCancel, | |||
| 37 | + | }; | |||
A webui/src/lib/components/ui/color-picker/color-picker.svelte +89 -0
| @@ -0,0 +1,89 @@ | |||
| 1 | + | <script lang="ts" module> | |
| 2 | + | export type ColorPickerProps = { | |
| 3 | + | value: string; | |
| 4 | + | onValueChange?: (value: string) => void; | |
| 5 | + | id?: string; | |
| 6 | + | class?: string; | |
| 7 | + | }; | |
| 8 | + | </script> | |
| 9 | + | ||
| 10 | + | <script lang="ts"> | |
| 11 | + | import ColorPicker from "svelte-awesome-color-picker"; | |
| 12 | + | import { Popover } from "bits-ui"; | |
| 13 | + | import { cn } from "$lib/utils.js"; | |
| 14 | + | ||
| 15 | + | let { | |
| 16 | + | class: className, | |
| 17 | + | value = $bindable("#000000"), | |
| 18 | + | onValueChange, | |
| 19 | + | id, | |
| 20 | + | }: ColorPickerProps = $props(); | |
| 21 | + | ||
| 22 | + | let open = $state(false); | |
| 23 | + | ||
| 24 | + | // svelte-awesome-color-picker uses hex binding | |
| 25 | + | let hex = $state(value); | |
| 26 | + | ||
| 27 | + | // Sync hex when value changes externally | |
| 28 | + | $effect(() => { | |
| 29 | + | hex = value; | |
| 30 | + | }); | |
| 31 | + | ||
| 32 | + | function handleInput(event: { hex: string | null }) { | |
| 33 | + | if (event.hex) { | |
| 34 | + | hex = event.hex; | |
| 35 | + | value = event.hex; | |
| 36 | + | onValueChange?.(event.hex); | |
| 37 | + | } | |
| 38 | + | } | |
| 39 | + | </script> | |
| 40 | + | ||
| 41 | + | <Popover.Root bind:open> | |
| 42 | + | <Popover.Trigger | |
| 43 | + | {id} | |
| 44 | + | class={cn( | |
| 45 | + | "inline-flex h-10 w-12 cursor-pointer items-center justify-center border border-border bg-background transition-colors hover:border-foreground/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", | |
| 46 | + | className | |
| 47 | + | )} | |
| 48 | + | > | |
| 49 | + | <span | |
| 50 | + | class="block h-6 w-8 border border-border/50" | |
| 51 | + | style="background-color: {value};" | |
| 52 | + | aria-hidden="true" | |
| 53 | + | ></span> | |
| 54 | + | <span class="sr-only">Choose color: {value}</span> | |
| 55 | + | </Popover.Trigger> | |
| 56 | + | ||
| 57 | + | <Popover.Portal> | |
| 58 | + | <Popover.Content | |
| 59 | + | class="color-picker-popover z-50 border border-border bg-background p-3 shadow-lg" | |
| 60 | + | sideOffset={4} | |
| 61 | + | > | |
| 62 | + | <ColorPicker | |
| 63 | + | bind:hex | |
| 64 | + | onInput={handleInput} | |
| 65 | + | isDialog={false} | |
| 66 | + | /> | |
| 67 | + | <!-- Hex value display --> | |
| 68 | + | <div class="mt-2 flex items-center gap-2"> | |
| 69 | + | <span | |
| 70 | + | class="block h-6 w-6 shrink-0 border border-border" | |
| 71 | + | style="background-color: {hex};" | |
| 72 | + | aria-hidden="true" | |
| 73 | + | ></span> | |
| 74 | + | <span class="font-mono text-sm text-foreground">{hex}</span> | |
| 75 | + | </div> | |
| 76 | + | </Popover.Content> | |
| 77 | + | </Popover.Portal> | |
| 78 | + | </Popover.Root> | |
| 79 | + | ||
| 80 | + | <style> | |
| 81 | + | /* Override svelte-awesome-color-picker styles to match our theme */ | |
| 82 | + | :global(.color-picker-popover) { | |
| 83 | + | --cp-bg-color: var(--background); | |
| 84 | + | --cp-border-color: var(--border); | |
| 85 | + | --cp-text-color: var(--foreground); | |
| 86 | + | --cp-input-color: var(--background); | |
| 87 | + | --cp-button-hover-color: var(--accent); | |
| 88 | + | } | |
| 89 | + | </style> | |
| @@ -0,0 +1,89 @@ | |||||
| 1 | + | <script lang="ts" module> | |||
| 2 | + | export type ColorPickerProps = { | |||
| 3 | + | value: string; | |||
| 4 | + | onValueChange?: (value: string) => void; | |||
| 5 | + | id?: string; | |||
| 6 | + | class?: string; | |||
| 7 | + | }; | |||
| 8 | + | </script> | |||
| 9 | + | ||||
| 10 | + | <script lang="ts"> | |||
| 11 | + | import ColorPicker from "svelte-awesome-color-picker"; | |||
| 12 | + | import { Popover } from "bits-ui"; | |||
| 13 | + | import { cn } from "$lib/utils.js"; | |||
| 14 | + | ||||
| 15 | + | let { | |||
| 16 | + | class: className, | |||
| 17 | + | value = $bindable("#000000"), | |||
| 18 | + | onValueChange, | |||
| 19 | + | id, | |||
| 20 | + | }: ColorPickerProps = $props(); | |||
| 21 | + | ||||
| 22 | + | let open = $state(false); | |||
| 23 | + | ||||
| 24 | + | // svelte-awesome-color-picker uses hex binding | |||
| 25 | + | let hex = $state(value); | |||
| 26 | + | ||||
| 27 | + | // Sync hex when value changes externally | |||
| 28 | + | $effect(() => { | |||
| 29 | + | hex = value; | |||
| 30 | + | }); | |||
| 31 | + | ||||
| 32 | + | function handleInput(event: { hex: string | null }) { | |||
| 33 | + | if (event.hex) { | |||
| 34 | + | hex = event.hex; | |||
| 35 | + | value = event.hex; | |||
| 36 | + | onValueChange?.(event.hex); | |||
| 37 | + | } | |||
| 38 | + | } | |||
| 39 | + | </script> | |||
| 40 | + | ||||
| 41 | + | <Popover.Root bind:open> | |||
| 42 | + | <Popover.Trigger | |||
| 43 | + | {id} | |||
| 44 | + | class={cn( | |||
| 45 | + | "inline-flex h-10 w-12 cursor-pointer items-center justify-center border border-border bg-background transition-colors hover:border-foreground/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", | |||
| 46 | + | className | |||
| 47 | + | )} | |||
| 48 | + | > | |||
| 49 | + | <span | |||
| 50 | + | class="block h-6 w-8 border border-border/50" | |||
| 51 | + | style="background-color: {value};" | |||
| 52 | + | aria-hidden="true" | |||
| 53 | + | ></span> | |||
| 54 | + | <span class="sr-only">Choose color: {value}</span> | |||
| 55 | + | </Popover.Trigger> | |||
| 56 | + | ||||
| 57 | + | <Popover.Portal> | |||
| 58 | + | <Popover.Content | |||
| 59 | + | class="color-picker-popover z-50 border border-border bg-background p-3 shadow-lg" | |||
| 60 | + | sideOffset={4} | |||
| 61 | + | > | |||
| 62 | + | <ColorPicker | |||
| 63 | + | bind:hex | |||
| 64 | + | onInput={handleInput} | |||
| 65 | + | isDialog={false} | |||
| 66 | + | /> | |||
| 67 | + | <!-- Hex value display --> | |||
| 68 | + | <div class="mt-2 flex items-center gap-2"> | |||
| 69 | + | <span | |||
| 70 | + | class="block h-6 w-6 shrink-0 border border-border" | |||
| 71 | + | style="background-color: {hex};" | |||
| 72 | + | aria-hidden="true" | |||
| 73 | + | ></span> | |||
| 74 | + | <span class="font-mono text-sm text-foreground">{hex}</span> | |||
| 75 | + | </div> | |||
| 76 | + | </Popover.Content> | |||
| 77 | + | </Popover.Portal> | |||
| 78 | + | </Popover.Root> | |||
| 79 | + | ||||
| 80 | + | <style> | |||
| 81 | + | /* Override svelte-awesome-color-picker styles to match our theme */ | |||
| 82 | + | :global(.color-picker-popover) { | |||
| 83 | + | --cp-bg-color: var(--background); | |||
| 84 | + | --cp-border-color: var(--border); | |||
| 85 | + | --cp-text-color: var(--foreground); | |||
| 86 | + | --cp-input-color: var(--background); | |||
| 87 | + | --cp-button-hover-color: var(--accent); | |||
| 88 | + | } | |||
| 89 | + | </style> | |||
A webui/src/lib/components/ui/color-picker/index.ts +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | import Root, { type ColorPickerProps } from "./color-picker.svelte"; | |
| 2 | + | ||
| 3 | + | export { | |
| 4 | + | Root, | |
| 5 | + | Root as ColorPicker, | |
| 6 | + | type ColorPickerProps, | |
| 7 | + | type ColorPickerProps as Props, | |
| 8 | + | }; | |
| @@ -0,0 +1,8 @@ | |||||
| 1 | + | import Root, { type ColorPickerProps } from "./color-picker.svelte"; | |||
| 2 | + | ||||
| 3 | + | export { | |||
| 4 | + | Root, | |||
| 5 | + | Root as ColorPicker, | |||
| 6 | + | type ColorPickerProps, | |||
| 7 | + | type ColorPickerProps as Props, | |||
| 8 | + | }; | |||
M webui/src/pages/ConfigPage.svelte +34 -19
| @@ -2,7 +2,9 @@ | |||
| 2 | 2 | import { onMount } from "svelte"; | |
| 3 | 3 | import ConfigShell from "./config/ConfigShell.svelte"; | |
| 4 | 4 | import ShortcutsTable from "./config/ShortcutsTable.svelte"; | |
| 5 | + | import * as AlertDialog from "$lib/components/ui/alert-dialog"; | |
| 5 | 6 | import { Button } from "$lib/components/ui/button"; | |
| 7 | + | import { ColorPicker } from "$lib/components/ui/color-picker"; | |
| 6 | 8 | import { Input } from "$lib/components/ui/input"; | |
| 7 | 9 | import { Label } from "$lib/components/ui/label"; | |
| 8 | 10 | import { Spinner } from "$lib/components/ui/spinner"; | |
| @@ -84,13 +86,10 @@ | |||
| 84 | 86 | loadConfig(); | |
| 85 | 87 | } | |
| 86 | 88 | ||
| 87 | - | async function resetToDefaults() { | |
| 88 | - | saveSuccess = false; | |
| 89 | - | const confirmed = window.confirm( | |
| 90 | - | "Reset this page to default settings? (You still need to click Save to persist.)", | |
| 91 | - | ); | |
| 92 | - | if (!confirmed) return; | |
| 89 | + | let resetDialogOpen = $state(false); | |
| 93 | 90 | ||
| 91 | + | async function doResetToDefaults() { | |
| 92 | + | saveSuccess = false; | |
| 94 | 93 | try { | |
| 95 | 94 | const response = await fetch("/api/config/default"); | |
| 96 | 95 | if (!response.ok) throw new Error("Failed to fetch default config"); | |
| @@ -98,6 +97,7 @@ | |||
| 98 | 97 | } catch (e: any) { | |
| 99 | 98 | console.error("[config] reset defaults failed", e); | |
| 100 | 99 | } | |
| 100 | + | resetDialogOpen = false; | |
| 101 | 101 | } | |
| 102 | 102 | ||
| 103 | 103 | function getWebKitBridge(): { postMessage: (msg: unknown) => void } | null { | |
| @@ -305,15 +305,12 @@ | |||
| 305 | 305 | <div class="space-y-2"> | |
| 306 | 306 | <Label for={`light_${field.key}`}>{field.label}</Label> | |
| 307 | 307 | <div class="flex items-center gap-3"> | |
| 308 | - | <input | |
| 308 | + | <ColorPicker | |
| 309 | 309 | id={`light_${field.key}`} | |
| 310 | - | type="color" | |
| 311 | 310 | value={config.appearance.light_palette[field.key]} | |
| 312 | - | oninput={(e) => { | |
| 313 | - | const v = (e.currentTarget as HTMLInputElement).value; | |
| 311 | + | onValueChange={(v) => { | |
| 314 | 312 | config!.appearance.light_palette[field.key] = v; | |
| 315 | 313 | }} | |
| 316 | - | class="h-10 w-12 border border-border bg-background" | |
| 317 | 314 | /> | |
| 318 | 315 | <Input bind:value={config.appearance.light_palette[field.key]} /> | |
| 319 | 316 | </div> | |
| @@ -334,15 +331,12 @@ | |||
| 334 | 331 | <div class="space-y-2"> | |
| 335 | 332 | <Label for={`dark_${field.key}`}>{field.label}</Label> | |
| 336 | 333 | <div class="flex items-center gap-3"> | |
| 337 | - | <input | |
| 334 | + | <ColorPicker | |
| 338 | 335 | id={`dark_${field.key}`} | |
| 339 | - | type="color" | |
| 340 | 336 | value={config.appearance.dark_palette[field.key]} | |
| 341 | - | oninput={(e) => { | |
| 342 | - | const v = (e.currentTarget as HTMLInputElement).value; | |
| 337 | + | onValueChange={(v) => { | |
| 343 | 338 | config!.appearance.dark_palette[field.key] = v; | |
| 344 | 339 | }} | |
| 345 | - | class="h-10 w-12 border border-border bg-background" | |
| 346 | 340 | /> | |
| 347 | 341 | <Input bind:value={config.appearance.dark_palette[field.key]} /> | |
| 348 | 342 | </div> | |
| @@ -394,9 +388,30 @@ | |||
| 394 | 388 | {/if} | |
| 395 | 389 | </div> | |
| 396 | 390 | <div class="flex items-center gap-3"> | |
| 397 | - | <Button variant="outline" onclick={resetToDefaults} disabled={saving} type="button"> | |
| 398 | - | Reset Defaults | |
| 399 | - | </Button> | |
| 391 | + | <AlertDialog.Root bind:open={resetDialogOpen}> | |
| 392 | + | <AlertDialog.Trigger disabled={saving}> | |
| 393 | + | {#snippet child({ props })} | |
| 394 | + | <Button variant="outline" {...props} type="button"> | |
| 395 | + | Reset Defaults | |
| 396 | + | </Button> | |
| 397 | + | {/snippet} | |
| 398 | + | </AlertDialog.Trigger> | |
| 399 | + | <AlertDialog.Content> | |
| 400 | + | <AlertDialog.Header> | |
| 401 | + | <AlertDialog.Title>Reset to defaults?</AlertDialog.Title> | |
| 402 | + | <AlertDialog.Description> | |
| 403 | + | This will reset all settings on this page to their default values. | |
| 404 | + | You will still need to click Save to persist the changes. | |
| 405 | + | </AlertDialog.Description> | |
| 406 | + | </AlertDialog.Header> | |
| 407 | + | <AlertDialog.Footer> | |
| 408 | + | <AlertDialog.Cancel>Cancel</AlertDialog.Cancel> | |
| 409 | + | <AlertDialog.Action onclick={doResetToDefaults}> | |
| 410 | + | Reset Defaults | |
| 411 | + | </AlertDialog.Action> | |
| 412 | + | </AlertDialog.Footer> | |
| 413 | + | </AlertDialog.Content> | |
| 414 | + | </AlertDialog.Root> | |
| 400 | 415 | <Button onclick={saveConfig} disabled={saving} type="button"> | |
| 401 | 416 | {saving ? "Saving…" : "Save"} | |
| 402 | 417 | </Button> | |
| @@ -2,7 +2,9 @@ | |||||
| 2 | import { onMount } from "svelte"; | 2 | import { onMount } from "svelte"; | ||
| 3 | import ConfigShell from "./config/ConfigShell.svelte"; | 3 | import ConfigShell from "./config/ConfigShell.svelte"; | ||
| 4 | import ShortcutsTable from "./config/ShortcutsTable.svelte"; | 4 | import ShortcutsTable from "./config/ShortcutsTable.svelte"; | ||
| 5 | + | import * as AlertDialog from "$lib/components/ui/alert-dialog"; | |||
| 5 | import { Button } from "$lib/components/ui/button"; | 6 | import { Button } from "$lib/components/ui/button"; | ||
| 7 | + | import { ColorPicker } from "$lib/components/ui/color-picker"; | |||
| 6 | import { Input } from "$lib/components/ui/input"; | 8 | import { Input } from "$lib/components/ui/input"; | ||
| 7 | import { Label } from "$lib/components/ui/label"; | 9 | import { Label } from "$lib/components/ui/label"; | ||
| 8 | import { Spinner } from "$lib/components/ui/spinner"; | 10 | import { Spinner } from "$lib/components/ui/spinner"; | ||
| @@ -84,13 +86,10 @@ | |||||
| 84 | loadConfig(); | 86 | loadConfig(); | ||
| 85 | } | 87 | } | ||
| 86 | 88 | ||||
| 87 | - | async function resetToDefaults() { | 89 | + | let resetDialogOpen = $state(false); |
| 88 | - | saveSuccess = false; | |||
| 89 | - | const confirmed = window.confirm( | |||
| 90 | - | "Reset this page to default settings? (You still need to click Save to persist.)", | |||
| 91 | - | ); | |||
| 92 | - | if (!confirmed) return; | |||
| 93 | 90 | ||||
| 91 | + | async function doResetToDefaults() { | |||
| 92 | + | saveSuccess = false; | |||
| 94 | try { | 93 | try { | ||
| 95 | const response = await fetch("/api/config/default"); | 94 | const response = await fetch("/api/config/default"); | ||
| 96 | if (!response.ok) throw new Error("Failed to fetch default config"); | 95 | if (!response.ok) throw new Error("Failed to fetch default config"); | ||
| @@ -98,6 +97,7 @@ | |||||
| 98 | } catch (e: any) { | 97 | } catch (e: any) { | ||
| 99 | console.error("[config] reset defaults failed", e); | 98 | console.error("[config] reset defaults failed", e); | ||
| 100 | } | 99 | } | ||
| 100 | + | resetDialogOpen = false; | |||
| 101 | } | 101 | } | ||
| 102 | 102 | ||||
| 103 | function getWebKitBridge(): { postMessage: (msg: unknown) => void } | null { | 103 | function getWebKitBridge(): { postMessage: (msg: unknown) => void } | null { | ||
| @@ -305,15 +305,12 @@ | |||||
| 305 | <div class="space-y-2"> | 305 | <div class="space-y-2"> | ||
| 306 | <Label for={`light_${field.key}`}>{field.label}</Label> | 306 | <Label for={`light_${field.key}`}>{field.label}</Label> | ||
| 307 | <div class="flex items-center gap-3"> | 307 | <div class="flex items-center gap-3"> | ||
| 308 | - | <input | 308 | + | <ColorPicker |
| 309 | id={`light_${field.key}`} | 309 | id={`light_${field.key}`} | ||
| 310 | - | type="color" | |||
| 311 | value={config.appearance.light_palette[field.key]} | 310 | value={config.appearance.light_palette[field.key]} | ||
| 312 | - | oninput={(e) => { | 311 | + | onValueChange={(v) => { |
| 313 | - | const v = (e.currentTarget as HTMLInputElement).value; | |||
| 314 | config!.appearance.light_palette[field.key] = v; | 312 | config!.appearance.light_palette[field.key] = v; | ||
| 315 | }} | 313 | }} | ||
| 316 | - | class="h-10 w-12 border border-border bg-background" | |||
| 317 | /> | 314 | /> | ||
| 318 | <Input bind:value={config.appearance.light_palette[field.key]} /> | 315 | <Input bind:value={config.appearance.light_palette[field.key]} /> | ||
| 319 | </div> | 316 | </div> | ||
| @@ -334,15 +331,12 @@ | |||||
| 334 | <div class="space-y-2"> | 331 | <div class="space-y-2"> | ||
| 335 | <Label for={`dark_${field.key}`}>{field.label}</Label> | 332 | <Label for={`dark_${field.key}`}>{field.label}</Label> | ||
| 336 | <div class="flex items-center gap-3"> | 333 | <div class="flex items-center gap-3"> | ||
| 337 | - | <input | 334 | + | <ColorPicker |
| 338 | id={`dark_${field.key}`} | 335 | id={`dark_${field.key}`} | ||
| 339 | - | type="color" | |||
| 340 | value={config.appearance.dark_palette[field.key]} | 336 | value={config.appearance.dark_palette[field.key]} | ||
| 341 | - | oninput={(e) => { | 337 | + | onValueChange={(v) => { |
| 342 | - | const v = (e.currentTarget as HTMLInputElement).value; | |||
| 343 | config!.appearance.dark_palette[field.key] = v; | 338 | config!.appearance.dark_palette[field.key] = v; | ||
| 344 | }} | 339 | }} | ||
| 345 | - | class="h-10 w-12 border border-border bg-background" | |||
| 346 | /> | 340 | /> | ||
| 347 | <Input bind:value={config.appearance.dark_palette[field.key]} /> | 341 | <Input bind:value={config.appearance.dark_palette[field.key]} /> | ||
| 348 | </div> | 342 | </div> | ||
| @@ -394,9 +388,30 @@ | |||||
| 394 | {/if} | 388 | {/if} | ||
| 395 | </div> | 389 | </div> | ||
| 396 | <div class="flex items-center gap-3"> | 390 | <div class="flex items-center gap-3"> | ||
| 397 | - | <Button variant="outline" onclick={resetToDefaults} disabled={saving} type="button"> | 391 | + | <AlertDialog.Root bind:open={resetDialogOpen}> |
| 398 | - | Reset Defaults | 392 | + | <AlertDialog.Trigger disabled={saving}> |
| 399 | - | </Button> | 393 | + | {#snippet child({ props })} |
| 394 | + | <Button variant="outline" {...props} type="button"> | |||
| 395 | + | Reset Defaults | |||
| 396 | + | </Button> | |||
| 397 | + | {/snippet} | |||
| 398 | + | </AlertDialog.Trigger> | |||
| 399 | + | <AlertDialog.Content> | |||
| 400 | + | <AlertDialog.Header> | |||
| 401 | + | <AlertDialog.Title>Reset to defaults?</AlertDialog.Title> | |||
| 402 | + | <AlertDialog.Description> | |||
| 403 | + | This will reset all settings on this page to their default values. | |||
| 404 | + | You will still need to click Save to persist the changes. | |||
| 405 | + | </AlertDialog.Description> | |||
| 406 | + | </AlertDialog.Header> | |||
| 407 | + | <AlertDialog.Footer> | |||
| 408 | + | <AlertDialog.Cancel>Cancel</AlertDialog.Cancel> | |||
| 409 | + | <AlertDialog.Action onclick={doResetToDefaults}> | |||
| 410 | + | Reset Defaults | |||
| 411 | + | </AlertDialog.Action> | |||
| 412 | + | </AlertDialog.Footer> | |||
| 413 | + | </AlertDialog.Content> | |||
| 414 | + | </AlertDialog.Root> | |||
| 400 | <Button onclick={saveConfig} disabled={saving} type="button"> | 415 | <Button onclick={saveConfig} disabled={saving} type="button"> | ||
| 401 | {saving ? "Saving…" : "Save"} | 416 | {saving ? "Saving…" : "Save"} | ||
| 402 | </Button> | 417 | </Button> | ||