Compare commits

...

3 Commits

Author SHA1 Message Date
Elias Schneider
96aa2ce043 Merge branch 'breaking/v2' into v2/use-item-component 2025-11-29 18:42:28 +01:00
Kyle Mendell
ddff3a2975 fixes 2025-11-26 13:55:49 -06:00
Kyle Mendell
a738d9fe88 refactor: update forms and other areas to use new shadcn components 2025-11-26 12:58:44 -06:00
53 changed files with 1006 additions and 373 deletions

View File

@@ -469,5 +469,6 @@
"default_profile_picture": "Default Profile Picture", "default_profile_picture": "Default Profile Picture",
"light": "Light", "light": "Light",
"dark": "Dark", "dark": "Dark",
"system": "System" "system": "System",
"scopes": "Scopes"
} }

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Checkbox } from '$lib/components/ui/checkbox'; import { Checkbox } from '$lib/components/ui/checkbox';
import { Label } from '$lib/components/ui/label'; import * as Field from '$lib/components/ui/field';
let { let {
id, id,
@@ -26,14 +26,10 @@
onCheckedChange={(v) => onCheckedChange && onCheckedChange(v == true)} onCheckedChange={(v) => onCheckedChange && onCheckedChange(v == true)}
bind:checked bind:checked
/> />
<div class="grid gap-1.5 leading-none"> <Field.Field class="gap-0">
<Label for={id} class="mb-0 text-sm leading-none font-medium"> <Field.Label for={id}>{label}</Field.Label>
{label}
</Label>
{#if description} {#if description}
<p class="text-muted-foreground text-[0.8rem]"> <Field.Description>{description}</Field.Description>
{description}
</p>
{/if} {/if}
</div> </Field.Field>
</div> </div>

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import DatePicker from '$lib/components/form/date-picker.svelte'; import DatePicker from '$lib/components/form/date-picker.svelte';
import * as Field from '$lib/components/ui/field';
import { Input, type FormInputEvent } from '$lib/components/ui/input'; import { Input, type FormInputEvent } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import type { FormInput } from '$lib/utils/form-util'; import type { FormInput } from '$lib/utils/form-util';
import { LucideExternalLink } from '@lucide/svelte'; import { LucideExternalLink } from '@lucide/svelte';
@@ -34,12 +34,12 @@
const id = label?.toLowerCase().replace(/ /g, '-'); const id = label?.toLowerCase().replace(/ /g, '-');
</script> </script>
<div {...restProps}> <Field.Field data-disabled={disabled} {...restProps}>
{#if label} {#if label}
<Label required={input?.required} class="mb-0" for={id}>{label}</Label> <Field.Label required={input?.required} for={id}>{label}</Field.Label>
{/if} {/if}
{#if description} {#if description}
<p class="text-muted-foreground mt-1 text-xs"> <Field.Description>
{description} {description}
{#if docsLink} {#if docsLink}
<a <a
@@ -51,28 +51,26 @@
<LucideExternalLink class="inline size-3 align-text-top" /> <LucideExternalLink class="inline size-3 align-text-top" />
</a> </a>
{/if} {/if}
</p> </Field.Description>
{/if} {/if}
<div class={label || description ? 'mt-2' : ''}> {#if children}
{#if children} {@render children()}
{@render children()} {:else if input}
{:else if input} {#if type === 'date'}
{#if type === 'date'} <DatePicker {id} bind:value={input.value as Date} />
<DatePicker {id} bind:value={input.value as Date} /> {:else}
{:else} <Input
<Input aria-invalid={!!input.error}
aria-invalid={!!input.error} {id}
{id} {placeholder}
{placeholder} {type}
{type} bind:value={input.value}
bind:value={input.value} {disabled}
{disabled} oninput={(e) => onInput?.(e)}
oninput={(e) => onInput?.(e)} />
/>
{/if}
{/if} {/if}
{#if input?.error} {/if}
<p class="text-destructive mt-1 text-start text-xs">{input.error}</p> {#if input?.error}
{/if} <Field.Error>{input.error}</Field.Error>
</div> {/if}
</div> </Field.Field>

View File

@@ -5,7 +5,8 @@
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import appConfigStore from '$lib/stores/application-configuration-store'; import appConfigStore from '$lib/stores/application-configuration-store';
import { cachedProfilePicture } from '$lib/utils/cached-image-util'; import { cachedProfilePicture } from '$lib/utils/cached-image-util';
import { LucideLoader, LucideRefreshCw, LucideUpload } from '@lucide/svelte'; import { LucideRefreshCw, LucideUpload } from '@lucide/svelte';
import { Spinner } from '$lib/components/ui/spinner';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { openConfirmDialog } from '../confirm-dialog'; import { openConfirmDialog } from '../confirm-dialog';
@@ -88,7 +89,7 @@
</Avatar.Root> </Avatar.Root>
<div class="absolute inset-0 flex items-center justify-center"> <div class="absolute inset-0 flex items-center justify-center">
{#if isLoading} {#if isLoading}
<LucideLoader class="size-5 animate-spin" /> <Spinner class="size-5" />
{:else} {:else}
<LucideUpload class="size-5 opacity-0 transition-opacity group-hover:opacity-100" /> <LucideUpload class="size-5 opacity-0 transition-opacity group-hover:opacity-100" />
{/if} {/if}

View File

@@ -3,9 +3,10 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Command from '$lib/components/ui/command'; import * as Command from '$lib/components/ui/command';
import * as Popover from '$lib/components/ui/popover'; import * as Popover from '$lib/components/ui/popover';
import { Spinner } from '$lib/components/ui/spinner';
import { cn } from '$lib/utils/style'; import { cn } from '$lib/utils/style';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import { LoaderCircle, LucideCheck, LucideChevronDown } from '@lucide/svelte'; import { LucideCheck, LucideChevronDown } from '@lucide/svelte';
import type { FormEventHandler } from 'svelte/elements'; import type { FormEventHandler } from 'svelte/elements';
type Item = { type Item = {
@@ -108,7 +109,7 @@
<Command.Empty> <Command.Empty>
{#if isLoading} {#if isLoading}
<div class="flex w-full items-center justify-center py-2"> <div class="flex w-full items-center justify-center py-2">
<LoaderCircle class="size-4 animate-spin" /> <Spinner />
</div> </div>
{:else} {:else}
{m.no_items_found()} {m.no_items_found()}

View File

@@ -2,9 +2,10 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Command from '$lib/components/ui/command'; import * as Command from '$lib/components/ui/command';
import * as Popover from '$lib/components/ui/popover'; import * as Popover from '$lib/components/ui/popover';
import { Spinner } from '$lib/components/ui/spinner';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import { cn } from '$lib/utils/style'; import { cn } from '$lib/utils/style';
import { LoaderCircle, LucideCheck, LucideChevronDown } from '@lucide/svelte'; import { LucideCheck, LucideChevronDown } from '@lucide/svelte';
import { tick } from 'svelte'; import { tick } from 'svelte';
import type { FormEventHandler, HTMLAttributes } from 'svelte/elements'; import type { FormEventHandler, HTMLAttributes } from 'svelte/elements';
@@ -90,7 +91,7 @@
<Command.Empty> <Command.Empty>
{#if isLoading} {#if isLoading}
<div class="flex w-full justify-center"> <div class="flex w-full justify-center">
<LoaderCircle class="size-4 animate-spin" /> <Spinner />
</div> </div>
{:else} {:else}
{m.no_items_found()} {m.no_items_found()}

View File

@@ -4,7 +4,7 @@
import Qrcode from '$lib/components/qrcode/qrcode.svelte'; import Qrcode from '$lib/components/qrcode/qrcode.svelte';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog'; import * as Dialog from '$lib/components/ui/dialog';
import Label from '$lib/components/ui/label/label.svelte'; import * as Field from '$lib/components/ui/field';
import * as Select from '$lib/components/ui/select/index.js'; import * as Select from '$lib/components/ui/select/index.js';
import { Separator } from '$lib/components/ui/separator'; import { Separator } from '$lib/components/ui/separator';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
@@ -36,7 +36,10 @@
async function createLoginCode() { async function createLoginCode() {
try { try {
code = await userService.createOneTimeAccessToken(userId!, availableExpirations[selectedExpiration]); code = await userService.createOneTimeAccessToken(
userId!,
availableExpirations[selectedExpiration]
);
oneTimeLink = `${page.url.origin}/lc/${code}`; oneTimeLink = `${page.url.origin}/lc/${code}`;
} catch (e) { } catch (e) {
axiosErrorToast(e); axiosErrorToast(e);
@@ -45,7 +48,10 @@
async function sendLoginCodeEmail() { async function sendLoginCodeEmail() {
try { try {
await userService.requestOneTimeAccessEmailAsAdmin(userId!, availableExpirations[selectedExpiration]); await userService.requestOneTimeAccessEmailAsAdmin(
userId!,
availableExpirations[selectedExpiration]
);
toast.success(m.login_code_email_success()); toast.success(m.login_code_email_success());
onOpenChange(false); onOpenChange(false);
} catch (e) { } catch (e) {
@@ -72,14 +78,14 @@
</Dialog.Header> </Dialog.Header>
{#if oneTimeLink === null} {#if oneTimeLink === null}
<div> <Field.Field>
<Label for="expiration">{m.expiration()}</Label> <Field.Label for="expiration">{m.expiration()}</Field.Label>
<Select.Root <Select.Root
type="single" type="single"
value={Object.keys(availableExpirations)[0]} value={Object.keys(availableExpirations)[0]}
onValueChange={(v) => (selectedExpiration = v! as keyof typeof availableExpirations)} onValueChange={(v) => (selectedExpiration = v! as keyof typeof availableExpirations)}
> >
<Select.Trigger id="expiration" class="w-full h-9"> <Select.Trigger id="expiration" class="h-9 w-full">
{selectedExpiration} {selectedExpiration}
</Select.Trigger> </Select.Trigger>
<Select.Content> <Select.Content>
@@ -88,7 +94,7 @@
{/each} {/each}
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
</div> </Field.Field>
<Dialog.Footer class="mt-2"> <Dialog.Footer class="mt-2">
{#if $appConfigStore.emailOneTimeAccessAsAdminEnabled} {#if $appConfigStore.emailOneTimeAccessAsAdminEnabled}
<Button <Button
@@ -106,10 +112,10 @@
{:else} {:else}
<div class="flex flex-col items-center gap-2"> <div class="flex flex-col items-center gap-2">
<CopyToClipboard value={code!}> <CopyToClipboard value={code!}>
<p class="text-3xl font-code">{code}</p> <p class="font-code text-3xl">{code}</p>
</CopyToClipboard> </CopyToClipboard>
<div class="flex items-center justify-center gap-3 my-2 text-muted-foreground"> <div class="text-muted-foreground my-2 flex items-center justify-center gap-3">
<Separator /> <Separator />
<p class="text-xs text-nowrap">{m.or_visit()}</p> <p class="text-xs text-nowrap">{m.or_visit()}</p>
<Separator /> <Separator />

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Item from '$lib/components/ui/item/index.js';
import * as Tooltip from '$lib/components/ui/tooltip/index.js'; import * as Tooltip from '$lib/components/ui/tooltip/index.js';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import { LucideCalendar, LucidePencil, LucideTrash, type Icon as IconType } from '@lucide/svelte'; import { LucideCalendar, LucidePencil, LucideTrash, type Icon as IconType } from '@lucide/svelte';
@@ -19,61 +20,54 @@
} = $props(); } = $props();
</script> </script>
<div class="bg-card hover:bg-muted/50 group rounded-lg p-3 transition-colors"> <Item.Root variant="muted" class="hover:bg-muted transition-colors">
<div class="flex items-center justify-between"> <Item.Media class="bg-primary/10 text-primary rounded-lg p-2">
<div class="flex items-start gap-3"> {#if icon}{@const Icon = icon}
<div class="bg-primary/10 text-primary mt-1 rounded-lg p-2"> <Icon class="size-5" />
{#if icon}{@const Icon = icon} {/if}
<Icon class="size-5" /> </Item.Media>
{/if} <Item.Content>
</div> <Item.Title>{label}</Item.Title>
<div> {#if description}
<div class="flex items-center gap-2"> <Item.Description class="flex items-center">
<p class="font-medium">{label}</p> <LucideCalendar class="mr-1 size-3" />
</div> {description}
{#if description} </Item.Description>
<div class="text-muted-foreground mt-1 flex items-center text-xs"> {/if}
<LucideCalendar class="mr-1 size-3" /> </Item.Content>
{description} <Item.Actions>
</div> <Tooltip.Provider>
{/if} <Tooltip.Root>
</div> <Tooltip.Trigger>
</div> <Button
onclick={onRename}
size="icon"
variant="ghost"
class="size-8"
aria-label={m.rename()}
>
<LucidePencil class="size-4" />
</Button>
</Tooltip.Trigger>
<Tooltip.Content>{m.rename()}</Tooltip.Content>
</Tooltip.Root>
</Tooltip.Provider>
<div class="flex items-center gap-2"> <Tooltip.Provider>
<Tooltip.Provider> <Tooltip.Root>
<Tooltip.Root> <Tooltip.Trigger>
<Tooltip.Trigger> <Button
<Button onclick={onDelete}
onclick={onRename} size="icon"
size="icon" variant="ghost"
variant="ghost" class="hover:bg-destructive/10 hover:text-destructive size-8"
class="size-8" aria-label={m.delete()}
aria-label={m.rename()} >
> <LucideTrash class="size-4" />
<LucidePencil class="size-4" /> </Button>
</Button> </Tooltip.Trigger>
</Tooltip.Trigger> <Tooltip.Content>{m.delete()}</Tooltip.Content>
<Tooltip.Content>{m.rename()}</Tooltip.Content> </Tooltip.Root>
</Tooltip.Root></Tooltip.Provider </Tooltip.Provider>
> </Item.Actions>
</Item.Root>
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger>
<Button
onclick={onDelete}
size="icon"
variant="ghost"
class="hover:bg-destructive/10 hover:text-destructive size-8"
aria-label={m.delete()}
>
<LucideTrash class="size-4" />
</Button>
</Tooltip.Trigger>
<Tooltip.Content>{m.delete()}</Tooltip.Content>
</Tooltip.Root></Tooltip.Provider
>
</div>
</div>
</div>

View File

@@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
import * as Item from '$lib/components/ui/item/index.js';
import type { Icon as IconType } from '@lucide/svelte'; import type { Icon as IconType } from '@lucide/svelte';
interface Props { interface Props {
icon: typeof IconType; icon: typeof IconType;
name: string; name: string;
@@ -11,10 +13,12 @@
const SvelteComponent = $derived(icon); const SvelteComponent = $derived(icon);
</script> </script>
<div class="flex items-center"> <Item.Root size="sm">
<div class="bg-muted mr-5 rounded-lg p-2"><SvelteComponent /></div> <Item.Media variant="icon">
<div class="text-start"> <SvelteComponent class="size-4" />
<h3 class="font-semibold">{name}</h3> </Item.Media>
<p class="text-muted-foreground text-sm">{description}</p> <Item.Content>
</div> <Item.Title class="font-medium">{name}</Item.Title>
</div> <Item.Description class="text-xs">{description}</Item.Description>
</Item.Content>
</Item.Root>

View File

@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import * as Item from '$lib/components/ui/item/index.js';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import { LucideMail, LucideUser, LucideUsers } from '@lucide/svelte'; import { LucideMail, LucideUser, LucideUsers } from '@lucide/svelte';
import ScopeItem from './scope-item.svelte'; import ScopeItem from './scope-item.svelte';
@@ -6,7 +7,7 @@
let { scope }: { scope: string } = $props(); let { scope }: { scope: string } = $props();
</script> </script>
<div class="flex flex-col gap-3" data-testid="scopes"> <Item.Group data-testid="scopes">
{#if scope!.includes('email')} {#if scope!.includes('email')}
<ScopeItem icon={LucideMail} name={m.email()} description={m.view_your_email_address()} /> <ScopeItem icon={LucideMail} name={m.email()} description={m.view_your_email_address()} />
{/if} {/if}
@@ -24,4 +25,4 @@
description={m.view_the_groups_you_are_a_member_of()} description={m.view_the_groups_you_are_a_member_of()}
/> />
{/if} {/if}
</div> </Item.Group>

View File

@@ -4,8 +4,8 @@
import Qrcode from '$lib/components/qrcode/qrcode.svelte'; import Qrcode from '$lib/components/qrcode/qrcode.svelte';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog'; import * as Dialog from '$lib/components/ui/dialog';
import * as Field from '$lib/components/ui/field';
import { Input } from '$lib/components/ui/input'; import { Input } from '$lib/components/ui/input';
import Label from '$lib/components/ui/label/label.svelte';
import * as Select from '$lib/components/ui/select/index.js'; import * as Select from '$lib/components/ui/select/index.js';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import UserService from '$lib/services/user-service'; import UserService from '$lib/services/user-service';
@@ -67,8 +67,8 @@
{#if signupToken === null} {#if signupToken === null}
<div class="space-y-4"> <div class="space-y-4">
<div> <Field.Field>
<Label for="expiration">{m.expiration()}</Label> <Field.Label for="expiration">{m.expiration()}</Field.Label>
<Select.Root <Select.Root
type="single" type="single"
value={Object.keys(availableExpirations)[0]} value={Object.keys(availableExpirations)[0]}
@@ -83,13 +83,13 @@
{/each} {/each}
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
</div> </Field.Field>
<div> <Field.Field>
<Label class="mb-0" for="usage-limit">{m.usage_limit()}</Label> <Field.Label for="usage-limit">{m.usage_limit()}</Field.Label>
<p class="text-muted-foreground mt-1 mb-2 text-xs"> <Field.Description>
{m.number_of_times_token_can_be_used()} {m.number_of_times_token_can_be_used()}
</p> </Field.Description>
<Input <Input
id="usage-limit" id="usage-limit"
type="number" type="number"
@@ -98,7 +98,7 @@
bind:value={usageLimit} bind:value={usageLimit}
class="h-9" class="h-9"
/> />
</div> </Field.Field>
</div> </div>
<Dialog.Footer class="mt-4"> <Dialog.Footer class="mt-4">

View File

@@ -42,7 +42,7 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import LoaderCircle from '@lucide/svelte/icons/loader-circle'; import { Spinner } from '$lib/components/ui/spinner';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
let { let {
@@ -97,7 +97,7 @@
{...restProps} {...restProps}
> >
{#if isLoading} {#if isLoading}
<LoaderCircle class="size-4 animate-spin" /> <Spinner />
{/if} {/if}
{@render children?.()} {@render children?.()}
</a> </a>
@@ -112,7 +112,7 @@
{...restProps} {...restProps}
> >
{#if isLoading} {#if isLoading}
<LoaderCircle class="size-4 animate-spin" /> <Spinner />
{/if} {/if}
{@render children?.()} {@render children?.()}
</button> </button>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils/style.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="field-content"
class={cn("group/field-content flex flex-1 flex-col gap-1.5 leading-snug", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils/style.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
</script>
<p
bind:this={ref}
data-slot="field-description"
class={cn(
'text-muted-foreground -mt-1 mb-0 text-xs leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance',
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
className
)}
{...restProps}
>
{@render children?.()}
</p>

View File

@@ -0,0 +1,58 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils/style.js";
import type { HTMLAttributes } from "svelte/elements";
import type { Snippet } from "svelte";
let {
ref = $bindable(null),
class: className,
children,
errors,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
children?: Snippet;
errors?: { message?: string }[];
} = $props();
const hasContent = $derived.by(() => {
// has slotted error
if (children) return true;
// no errors
if (!errors) return false;
// has an error but no message
if (errors.length === 1 && !errors[0]?.message) {
return false;
}
return true;
});
const isMultipleErrors = $derived(errors && errors.length > 1);
const singleErrorMessage = $derived(errors && errors.length === 1 && errors[0]?.message);
</script>
{#if hasContent}
<div
bind:this={ref}
role="alert"
data-slot="field-error"
class={cn("text-destructive text-sm font-normal", className)}
{...restProps}
>
{#if children}
{@render children()}
{:else if singleErrorMessage}
{singleErrorMessage}
{:else if isMultipleErrors}
<ul class="ms-4 flex list-disc flex-col gap-1">
{#each errors ?? [] as error, index (index)}
{#if error?.message}
<li>{error.message}</li>
{/if}
{/each}
</ul>
{/if}
</div>
{/if}

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils/style.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="field-group"
class={cn(
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
className
)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,30 @@
<script lang="ts">
import { Label } from '$lib/components/ui/label/index.js';
import { cn } from '$lib/utils/style.js';
import type { ComponentProps } from 'svelte';
let {
ref = $bindable(null),
class: className,
required = false,
children,
...restProps
}: ComponentProps<typeof Label> & {
required?: boolean;
} = $props();
</script>
<Label
bind:ref
data-slot="field-label"
{required}
class={cn(
'group/field-label peer/field-label mt-1 mb-0 flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4',
'has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10',
className
)}
{...restProps}
>
{@render children?.()}
</Label>

View File

@@ -0,0 +1,29 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils/style.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
variant = "legend",
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLLegendElement>> & {
variant?: "legend" | "label";
} = $props();
</script>
<legend
bind:this={ref}
data-slot="field-legend"
data-variant={variant}
class={cn(
"mb-3 font-medium",
"data-[variant=legend]:text-base",
"data-[variant=label]:text-sm",
className
)}
{...restProps}
>
{@render children?.()}
</legend>

View File

@@ -0,0 +1,38 @@
<script lang="ts">
import { Separator } from "$lib/components/ui/separator/index.js";
import { cn, type WithElementRef } from "$lib/utils/style.js";
import type { HTMLAttributes } from "svelte/elements";
import type { Snippet } from "svelte";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
children?: Snippet;
} = $props();
const hasContent = $derived(!!children);
</script>
<div
bind:this={ref}
data-slot="field-separator"
data-content={hasContent}
class={cn(
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
className
)}
{...restProps}
>
<Separator class="absolute inset-0 top-1/2" />
{#if children}
<span
class="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
data-slot="field-separator-content"
>
{@render children()}
</span>
{/if}
</div>

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils/style.js";
import type { HTMLFieldsetAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLFieldsetAttributes> = $props();
</script>
<fieldset
bind:this={ref}
data-slot="field-set"
class={cn(
"flex flex-col gap-6",
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
className
)}
{...restProps}
>
{@render children?.()}
</fieldset>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils/style.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="field-title"
class={cn(
"flex w-fit items-center gap-2 text-sm font-medium leading-snug group-data-[disabled=true]/field:opacity-50",
className
)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,53 @@
<script lang="ts" module>
import { tv, type VariantProps } from "tailwind-variants";
export const fieldVariants = tv({
base: "group/field data-[invalid=true]:text-destructive flex w-full gap-3",
variants: {
orientation: {
vertical: "flex-col [&>*]:w-full [&>.sr-only]:w-auto",
horizontal: [
"flex-row items-center",
"[&>[data-slot=field-label]]:flex-auto",
"has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px has-[>[data-slot=field-content]]:items-start",
],
responsive: [
"@md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto flex-col [&>*]:w-full [&>.sr-only]:w-auto",
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
],
},
},
defaultVariants: {
orientation: "vertical",
},
});
export type FieldOrientation = VariantProps<typeof fieldVariants>["orientation"];
</script>
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils/style.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
orientation = "vertical",
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
orientation?: FieldOrientation;
} = $props();
</script>
<div
bind:this={ref}
role="group"
data-slot="field"
data-orientation={orientation}
class={cn(fieldVariants({ orientation }), className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,33 @@
import Field from "./field.svelte";
import Set from "./field-set.svelte";
import Legend from "./field-legend.svelte";
import Group from "./field-group.svelte";
import Content from "./field-content.svelte";
import Label from "./field-label.svelte";
import Title from "./field-title.svelte";
import Description from "./field-description.svelte";
import Separator from "./field-separator.svelte";
import Error from "./field-error.svelte";
export {
Field,
Set,
Legend,
Group,
Content,
Label,
Title,
Description,
Separator,
Error,
//
Set as FieldSet,
Legend as FieldLegend,
Group as FieldGroup,
Content as FieldContent,
Label as FieldLabel,
Title as FieldTitle,
Description as FieldDescription,
Separator as FieldSeparator,
Error as FieldError,
};

View File

@@ -0,0 +1,34 @@
import Root from "./item.svelte";
import Group from "./item-group.svelte";
import Separator from "./item-separator.svelte";
import Header from "./item-header.svelte";
import Footer from "./item-footer.svelte";
import Content from "./item-content.svelte";
import Title from "./item-title.svelte";
import Description from "./item-description.svelte";
import Actions from "./item-actions.svelte";
import Media from "./item-media.svelte";
export {
Root,
Group,
Separator,
Header,
Footer,
Content,
Title,
Description,
Actions,
Media,
//
Root as Item,
Group as ItemGroup,
Separator as ItemSeparator,
Header as ItemHeader,
Footer as ItemFooter,
Content as ItemContent,
Title as ItemTitle,
Description as ItemDescription,
Actions as ItemActions,
Media as ItemMedia,
};

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils/style.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="item-actions"
class={cn("flex items-center gap-2", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils/style.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="item-content"
class={cn("flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils/style.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
</script>
<p
bind:this={ref}
data-slot="item-description"
class={cn(
"text-muted-foreground line-clamp-2 text-balance text-sm font-normal leading-normal",
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
className
)}
{...restProps}
>
{@render children?.()}
</p>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils/style.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="item-footer"
class={cn("flex basis-full items-center justify-between gap-2", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,21 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils/style.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
role="list"
data-slot="item-group"
class={cn("group/item-group flex flex-col", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils/style.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="item-header"
class={cn("flex basis-full items-center justify-between gap-2", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,42 @@
<script lang="ts" module>
import { tv, type VariantProps } from "tailwind-variants";
export const itemMediaVariants = tv({
base: "flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:translate-y-0.5 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none",
variants: {
variant: {
default: "bg-transparent",
icon: "bg-muted size-8 rounded-sm border [&_svg:not([class*='size-'])]:size-4",
image: "size-10 overflow-hidden rounded-sm [&_img]:size-full [&_img]:object-cover",
},
},
defaultVariants: {
variant: "default",
},
});
export type ItemMediaVariant = VariantProps<typeof itemMediaVariants>["variant"];
</script>
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils/style.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
variant = "default",
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { variant?: ItemMediaVariant } = $props();
</script>
<div
bind:this={ref}
data-slot="item-media"
data-variant={variant}
class={cn(itemMediaVariants({ variant }), className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { Separator } from "$lib/components/ui/separator/index.js";
import { cn } from "$lib/utils/style.js";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
class: className,
...restProps
}: ComponentProps<typeof Separator> = $props();
</script>
<Separator
bind:ref
data-slot="item-separator"
orientation="horizontal"
class={cn("my-0", className)}
{...restProps}
/>

View File

@@ -0,0 +1,22 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils/style.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
role="heading"
aria-level="3"
data-slot="item-title"
class={cn("flex w-fit items-center gap-2 text-sm font-medium leading-snug", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,60 @@
<script lang="ts" module>
import { tv, type VariantProps } from "tailwind-variants";
export const itemVariants = tv({
base: "group/item [a]:hover:bg-accent/50 [a]:transition-colors focus-visible:border-ring focus-visible:ring-ring/50 flex flex-wrap items-center rounded-md border border-transparent text-sm outline-none transition-colors duration-100 focus-visible:ring-[3px]",
variants: {
variant: {
default: "bg-transparent",
outline: "border-border",
muted: "bg-muted/50",
},
size: {
default: "gap-4 p-4",
sm: "gap-2.5 px-4 py-3",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export type ItemSize = VariantProps<typeof itemVariants>["size"];
export type ItemVariant = VariantProps<typeof itemVariants>["variant"];
</script>
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils/style.js";
import type { HTMLAttributes } from "svelte/elements";
import type { Snippet } from "svelte";
let {
ref = $bindable(null),
class: className,
child,
variant,
size,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
child?: Snippet<[{ props: Record<string, unknown> }]>;
variant?: ItemVariant;
size?: ItemSize;
} = $props();
const mergedProps = $derived({
class: cn(itemVariants({ variant, size }), className),
"data-slot": "item",
"data-variant": variant,
"data-size": size,
...restProps,
});
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<div bind:this={ref} {...mergedProps}>
{@render mergedProps.children?.()}
</div>
{/if}

View File

@@ -0,0 +1 @@
export { default as Spinner } from "./spinner.svelte";

View File

@@ -0,0 +1,14 @@
<script lang="ts">
import { cn } from "$lib/utils/style.js";
import Loader2Icon from "@lucide/svelte/icons/loader-2";
import type { ComponentProps } from "svelte";
let { class: className, ...restProps }: ComponentProps<typeof Loader2Icon> = $props();
</script>
<Loader2Icon
role="status"
aria-label="Loading"
class={cn("size-4 animate-spin", className)}
{...restProps}
/>

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import FormattedMessage from '$lib/components/formatted-message.svelte'; import FormattedMessage from '$lib/components/formatted-message.svelte';
import SignInWrapper from '$lib/components/login-wrapper.svelte'; import SignInWrapper from '$lib/components/login-wrapper.svelte';
import ScopeItem from '$lib/components/scope-item.svelte'; import ScopeList from '$lib/components/scope-list.svelte';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card'; import * as Card from '$lib/components/ui/card';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
@@ -10,7 +10,6 @@
import appConfigStore from '$lib/stores/application-configuration-store'; import appConfigStore from '$lib/stores/application-configuration-store';
import userStore from '$lib/stores/user-store'; import userStore from '$lib/stores/user-store';
import { getWebauthnErrorMessage } from '$lib/utils/error-util'; import { getWebauthnErrorMessage } from '$lib/utils/error-util';
import { LucideMail, LucideUser, LucideUsers } from '@lucide/svelte';
import { startAuthentication, type AuthenticationResponseJSON } from '@simplewebauthn/browser'; import { startAuthentication, type AuthenticationResponseJSON } from '@simplewebauthn/browser';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
@@ -140,30 +139,8 @@
/> />
</p> </p>
</Card.Header> </Card.Header>
<Card.Content data-testid="scopes"> <Card.Content>
<div class="flex flex-col gap-3"> <ScopeList {scope} />
{#if scope!.includes('email')}
<ScopeItem
icon={LucideMail}
name={m.email()}
description={m.view_your_email_address()}
/>
{/if}
{#if scope!.includes('profile')}
<ScopeItem
icon={LucideUser}
name={m.profile()}
description={m.view_your_profile_information()}
/>
{/if}
{#if scope!.includes('groups')}
<ScopeItem
icon={LucideUsers}
name={m.groups()}
description={m.view_the_groups_you_are_a_member_of()}
/>
{/if}
</div>
</Card.Content> </Card.Content>
</Card.Root> </Card.Root>
</div> </div>

View File

@@ -2,8 +2,7 @@
import { page } from '$app/state'; import { page } from '$app/state';
import SignInWrapper from '$lib/components/login-wrapper.svelte'; import SignInWrapper from '$lib/components/login-wrapper.svelte';
import Logo from '$lib/components/logo.svelte'; import Logo from '$lib/components/logo.svelte';
import { Button } from '$lib/components/ui/button'; import * as Item from '$lib/components/ui/item/index.js';
import * as Card from '$lib/components/ui/card';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import appConfigStore from '$lib/stores/application-configuration-store'; import appConfigStore from '$lib/stores/application-configuration-store';
import { LucideChevronRight, LucideMail, LucideRectangleEllipsis } from '@lucide/svelte'; import { LucideChevronRight, LucideMail, LucideRectangleEllipsis } from '@lucide/svelte';
@@ -40,24 +39,26 @@
<p class="text-muted-foreground mt-3"> <p class="text-muted-foreground mt-3">
{m.if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods()} {m.if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods()}
</p> </p>
<div class="mt-5 flex flex-col gap-3"> <Item.Group class="mt-5 gap-3">
{#each methods as method} {#each methods as method}
<a href={method.href + page.url.search}> <Item.Root variant="outline">
<Card.Root> {#snippet child({ props })}
<Card.Content class="flex items-center justify-between px-4"> <a href={method.href + page.url.search} {...props}>
<div class="flex gap-3"> <Item.Media class="text-primary">
<method.icon class="text-primary size-7" /> <method.icon class="size-7" />
<div class="text-start"> </Item.Media>
<h3 class="text-lg font-semibold">{method.title}</h3> <Item.Content>
<p class="text-muted-foreground text-sm">{method.description}</p> <Item.Title class="text-lg">{method.title}</Item.Title>
</div> <Item.Description>{method.description}</Item.Description>
</div> </Item.Content>
<Button variant="ghost"><LucideChevronRight class="size-5" /></Button> <Item.Actions>
</Card.Content> <LucideChevronRight class="size-5" />
</Card.Root> </Item.Actions>
</a> </a>
{/snippet}
</Item.Root>
{/each} {/each}
</div> </Item.Group>
<a class="text-muted-foreground mt-5 text-xs" href={'/login' + page.url.search} <a class="text-muted-foreground mt-5 text-xs" href={'/login' + page.url.search}
>{m.use_your_passkey_instead()}</a >{m.use_your_passkey_instead()}</a

View File

@@ -3,6 +3,7 @@
import * as Alert from '$lib/components/ui/alert'; import * as Alert from '$lib/components/ui/alert';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card'; import * as Card from '$lib/components/ui/card';
import * as Item from '$lib/components/ui/item/index.js';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import UserService from '$lib/services/user-service'; import UserService from '$lib/services/user-service';
import WebAuthnService from '$lib/services/webauthn-service'; import WebAuthnService from '$lib/services/webauthn-service';
@@ -100,25 +101,22 @@
<!-- Login code card mobile --> <!-- Login code card mobile -->
<div class="block sm:hidden"> <div class="block sm:hidden">
<Card.Root> <Item.Root variant="outline">
<Card.Header> <Item.Media class="text-primary/80">
<div class="flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center"> <RectangleEllipsis class="size-5" />
<div> </Item.Media>
<Card.Title> <Item.Content>
<RectangleEllipsis class="text-primary/80 size-5" /> <Item.Title>{m.login_code()}</Item.Title>
{m.login_code()} <Item.Description>
</Card.Title> {m.create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey()}
<Card.Description> </Item.Description>
{m.create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey()} </Item.Content>
</Card.Description> <Item.Actions class="w-full sm:w-auto">
</div> <Button variant="outline" class="w-full" onclick={() => (showLoginCodeModal = true)}>
{m.create()}
<Button variant="outline" class="w-full" onclick={() => (showLoginCodeModal = true)}> </Button>
{m.create()} </Item.Actions>
</Button> </Item.Root>
</div>
</Card.Header>
</Card.Root>
</div> </div>
<!-- Account details card --> <!-- Account details card -->
@@ -140,75 +138,68 @@
</Card.Content> </Card.Content>
</Card.Root> </Card.Root>
<!-- Passkey management card --> <!-- Passkey management -->
<div> <Item.Group class="bg-muted/50 rounded-xl border p-4">
<Card.Root class="gap-3"> <Item.Root class="border-none bg-transparent p-0">
<Card.Header> <Item.Media class="text-primary/80">
<div class="flex items-center justify-between"> <KeyRound class="size-5" />
<div> </Item.Media>
<Card.Title> <Item.Content>
<KeyRound class="text-primary/80 size-5" /> <Item.Title class="text-xl font-semibold">{m.passkeys()}</Item.Title>
{m.passkeys()} <Item.Description>
</Card.Title> {m.manage_your_passkeys_that_you_can_use_to_authenticate_yourself()}
<Card.Description> </Item.Description>
{m.manage_your_passkeys_that_you_can_use_to_authenticate_yourself()} </Item.Content>
</Card.Description> <Item.Actions>
</div> <Button variant="outline" onclick={createPasskey}>
<Button variant="outline" class="ml-3" onclick={createPasskey}> {m.add_passkey()}
{m.add_passkey()} </Button>
</Button> </Item.Actions>
</div> </Item.Root>
</Card.Header> {#if passkeys.length != 0}
{#if passkeys.length != 0} <Item.Separator class="my-4" />
<Card.Content> <PasskeyList bind:passkeys />
<PasskeyList bind:passkeys /> {/if}
</Card.Content> </Item.Group>
{/if}
</Card.Root>
</div>
<!-- Login code card --> <!-- Login code card -->
<div class="hidden sm:block"> <div class="hidden sm:block">
<Card.Root> <Item.Root variant="muted">
<Card.Header> <Item.Media class="text-primary/80">
<div class="flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center"> <RectangleEllipsis class="size-5" />
<div> </Item.Media>
<Card.Title> <Item.Content>
<RectangleEllipsis class="text-primary/80 size-5" /> <Item.Title>{m.login_code()}</Item.Title>
{m.login_code()} <Item.Description>
</Card.Title> {m.create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey()}
<Card.Description> </Item.Description>
{m.create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey()} </Item.Content>
</Card.Description> <Item.Actions>
</div> <Button variant="outline" onclick={() => (showLoginCodeModal = true)}>
<Button variant="outline" onclick={() => (showLoginCodeModal = true)}> {m.create()}
{m.create()} </Button>
</Button> </Item.Actions>
</div> </Item.Root>
</Card.Header>
</Card.Root>
</div> </div>
<!-- Language selection card --> <!-- Language selection card -->
<div> <div>
<Card.Root> <Item.Root variant="muted">
<Card.Header> <Item.Media class="text-primary/80">
<div class="flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center"> <Languages class="size-5" />
<div> </Item.Media>
<Card.Title> <Item.Content>
<Languages class="text-primary/80 size-5" /> <Item.Title>{m.language()}</Item.Title>
{m.language()} <Item.Description>
</Card.Title> {m.select_the_language_you_want_to_use()}
<Card.Description> <br />
{m.select_the_language_you_want_to_use()} <FormattedMessage m={m.contribute_to_translation()} />
<br /> </Item.Description>
<FormattedMessage m={m.contribute_to_translation()} /> </Item.Content>
</Card.Description> <Item.Actions>
</div> <LocalePicker />
<LocalePicker /> </Item.Actions>
</div> </Item.Root>
</Card.Header>
</Card.Root>
</div> </div>
<RenamePasskeyModal <RenamePasskeyModal

View File

@@ -2,6 +2,7 @@
import FormInput from '$lib/components/form/form-input.svelte'; import FormInput from '$lib/components/form/form-input.svelte';
import ProfilePictureSettings from '$lib/components/form/profile-picture-settings.svelte'; import ProfilePictureSettings from '$lib/components/form/profile-picture-settings.svelte';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Field from '$lib/components/ui/field/index.js';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import UserService from '$lib/services/user-service'; import UserService from '$lib/services/user-service';
import appConfigStore from '$lib/stores/application-configuration-store'; import appConfigStore from '$lib/stores/application-configuration-store';
@@ -86,30 +87,20 @@
resetCallback={resetProfilePicture} resetCallback={resetProfilePicture}
/> />
<hr class="border-border" /> <Field.Separator />
<fieldset disabled={userInfoInputDisabled}> <fieldset disabled={userInfoInputDisabled}>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2"> <Field.Group class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div> <FormInput label={m.first_name()} bind:input={$inputs.firstName} onInput={onNameInput} />
<FormInput label={m.first_name()} bind:input={$inputs.firstName} onInput={onNameInput} /> <FormInput label={m.last_name()} bind:input={$inputs.lastName} onInput={onNameInput} />
</div> <FormInput
<div> label={m.display_name()}
<FormInput label={m.last_name()} bind:input={$inputs.lastName} onInput={onNameInput} /> bind:input={$inputs.displayName}
</div> onInput={() => (hasManualDisplayNameEdit = true)}
<div> />
<FormInput <FormInput label={m.username()} bind:input={$inputs.username} />
label={m.display_name()} <FormInput label={m.email()} type="email" bind:input={$inputs.email} />
bind:input={$inputs.displayName} </Field.Group>
onInput={() => (hasManualDisplayNameEdit = true)}
/>
</div>
<div>
<FormInput label={m.username()} bind:input={$inputs.username} />
</div>
<div>
<FormInput label={m.email()} bind:input={$inputs.email} />
</div>
</div>
<div class="flex justify-end pt-4"> <div class="flex justify-end pt-4">
<Button {isLoading} type="submit">{m.save()}</Button> <Button {isLoading} type="submit">{m.save()}</Button>

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { openConfirmDialog } from '$lib/components/confirm-dialog/'; import { openConfirmDialog } from '$lib/components/confirm-dialog/';
import GlassRowItem from '$lib/components/passkey-row.svelte'; import GlassRowItem from '$lib/components/passkey-row.svelte';
import * as Item from '$lib/components/ui/item/index.js';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import WebauthnService from '$lib/services/webauthn-service'; import WebauthnService from '$lib/services/webauthn-service';
import type { Passkey } from '$lib/types/passkey.type'; import type { Passkey } from '$lib/types/passkey.type';
@@ -36,7 +37,7 @@
} }
</script> </script>
<div class="space-y-3"> <Item.Group class="gap-3">
{#each passkeys as passkey} {#each passkeys as passkey}
<GlassRowItem <GlassRowItem
label={passkey.name} label={passkey.name}
@@ -46,7 +47,7 @@
onDelete={() => deletePasskey(passkey)} onDelete={() => deletePasskey(passkey)}
/> />
{/each} {/each}
</div> </Item.Group>
<RenamePasskeyModal <RenamePasskeyModal
bind:passkey={passkeyToRename} bind:passkey={passkeyToRename}

View File

@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog'; import * as Dialog from '$lib/components/ui/dialog';
import * as Field from '$lib/components/ui/field';
import { Input } from '$lib/components/ui/input'; import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import WebAuthnService from '$lib/services/webauthn-service'; import WebAuthnService from '$lib/services/webauthn-service';
import type { Passkey } from '$lib/types/passkey.type'; import type { Passkey } from '$lib/types/passkey.type';
@@ -52,7 +52,7 @@
</Dialog.Header> </Dialog.Header>
<form onsubmit={preventDefault(onSubmit)}> <form onsubmit={preventDefault(onSubmit)}>
<div class="grid items-center gap-4 sm:grid-cols-4"> <div class="grid items-center gap-4 sm:grid-cols-4">
<Label for="name" class="sm:text-right">{m.name()}</Label> <Field.Label for="name" class="sm:text-right">{m.name()}</Field.Label>
<Input id="name" bind:value={name} class="col-span-3" /> <Input id="name" bind:value={name} class="col-span-3" />
</div> </div>
<Dialog.Footer class="mt-4"> <Dialog.Footer class="mt-4">

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import FileInput from '$lib/components/form/file-input.svelte'; import FileInput from '$lib/components/form/file-input.svelte';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { Label } from '$lib/components/ui/label'; import * as Field from '$lib/components/ui/field';
import { cn } from '$lib/utils/style'; import { cn } from '$lib/utils/style';
import { LucideImageOff, LucideUpload, LucideX } from '@lucide/svelte'; import { LucideImageOff, LucideUpload, LucideX } from '@lucide/svelte';
import type { HTMLAttributes } from 'svelte/elements'; import type { HTMLAttributes } from 'svelte/elements';
@@ -53,7 +53,7 @@
</script> </script>
<div class="flex flex-col items-start md:flex-row md:items-center" {...restProps}> <div class="flex flex-col items-start md:flex-row md:items-center" {...restProps}>
<Label class="w-52" for={id}>{label}</Label> <Field.Label class="w-52" for={id}>{label}</Field.Label>
<FileInput {id} variant="secondary" {accept} onchange={onImageChange}> <FileInput {id} variant="secondary" {accept} onchange={onImageChange}>
<div <div
class={cn('group/image relative flex items-center rounded transition-colors', { class={cn('group/image relative flex items-center rounded transition-colors', {

View File

@@ -3,7 +3,7 @@
import FormInput from '$lib/components/form/form-input.svelte'; import FormInput from '$lib/components/form/form-input.svelte';
import SwitchWithLabel from '$lib/components/form/switch-with-label.svelte'; import SwitchWithLabel from '$lib/components/form/switch-with-label.svelte';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import Label from '$lib/components/ui/label/label.svelte'; import * as Field from '$lib/components/ui/field';
import * as Select from '$lib/components/ui/select'; import * as Select from '$lib/components/ui/select';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import AppConfigService from '$lib/services/app-config-service'; import AppConfigService from '$lib/services/app-config-service';
@@ -112,8 +112,8 @@
<FormInput label={m.smtp_user()} bind:input={$inputs.smtpUser} /> <FormInput label={m.smtp_user()} bind:input={$inputs.smtpUser} />
<FormInput label={m.smtp_password()} type="password" bind:input={$inputs.smtpPassword} /> <FormInput label={m.smtp_password()} type="password" bind:input={$inputs.smtpPassword} />
<FormInput label={m.smtp_from()} bind:input={$inputs.smtpFrom} /> <FormInput label={m.smtp_from()} bind:input={$inputs.smtpFrom} />
<div class="grid gap-2"> <Field.Field>
<Label class="mb-0" for="smtp-tls">{m.smtp_tls_option()}</Label> <Field.Label for="smtp-tls">{m.smtp_tls_option()}</Field.Label>
<Select.Root <Select.Root
type="single" type="single"
value={$inputs.smtpTls.value} value={$inputs.smtpTls.value}
@@ -128,7 +128,7 @@
<Select.Item value="tls" label="TLS" /> <Select.Item value="tls" label="TLS" />
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
</div> </Field.Field>
<SwitchWithLabel <SwitchWithLabel
id="skip-cert-verify" id="skip-cert-verify"
label={m.skip_certificate_verification()} label={m.skip_certificate_verification()}

View File

@@ -2,8 +2,7 @@
import FormInput from '$lib/components/form/form-input.svelte'; import FormInput from '$lib/components/form/form-input.svelte';
import SwitchWithLabel from '$lib/components/form/switch-with-label.svelte'; import SwitchWithLabel from '$lib/components/form/switch-with-label.svelte';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { Label } from '$lib/components/ui/label/index.js'; import * as Field from '$lib/components/ui/field';
import * as Select from '$lib/components/ui/select';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import appConfigStore from '$lib/stores/application-configuration-store'; import appConfigStore from '$lib/stores/application-configuration-store';
import type { AllAppConfig } from '$lib/types/application-configuration'; import type { AllAppConfig } from '$lib/types/application-configuration';
@@ -82,21 +81,21 @@
bind:checked={$inputs.disableAnimations.value} bind:checked={$inputs.disableAnimations.value}
/> />
<div class="space-y-5"> <Field.Field class="space-y-5">
<div> <div>
<Label class="mb-0 text-sm font-medium"> <Field.Label>
{m.accent_color()} {m.accent_color()}
</Label> </Field.Label>
<p class="text-muted-foreground text-[0.8rem]"> <Field.Description>
{m.select_an_accent_color_to_customize_the_appearance_of_pocket_id()} {m.select_an_accent_color_to_customize_the_appearance_of_pocket_id()}
</p> </Field.Description>
</div> </div>
<AccentColorPicker <AccentColorPicker
previousColor={appConfig.accentColor} previousColor={appConfig.accentColor}
bind:selectedColor={$inputs.accentColor.value} bind:selectedColor={$inputs.accentColor.value}
disabled={$appConfigStore.uiConfigDisabled} disabled={$appConfigStore.uiConfigDisabled}
/> />
</div> </Field.Field>
</div> </div>
<div class="mt-5 flex justify-end"> <div class="mt-5 flex justify-end">
<Button {isLoading} type="submit">{m.save()}</Button> <Button {isLoading} type="submit">{m.save()}</Button>

View File

@@ -2,7 +2,7 @@
import CustomClaimsInput from '$lib/components/form/custom-claims-input.svelte'; import CustomClaimsInput from '$lib/components/form/custom-claims-input.svelte';
import SearchableMultiSelect from '$lib/components/form/searchable-multi-select.svelte'; import SearchableMultiSelect from '$lib/components/form/searchable-multi-select.svelte';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { Label } from '$lib/components/ui/label'; import * as Field from '$lib/components/ui/field';
import * as Select from '$lib/components/ui/select'; import * as Select from '$lib/components/ui/select';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import UserGroupService from '$lib/services/user-group-service'; import UserGroupService from '$lib/services/user-group-service';
@@ -98,13 +98,11 @@
<form onsubmit={preventDefault(onSubmit)}> <form onsubmit={preventDefault(onSubmit)}>
<fieldset class="flex flex-col gap-5" disabled={$appConfigStore.uiConfigDisabled}> <fieldset class="flex flex-col gap-5" disabled={$appConfigStore.uiConfigDisabled}>
<div class="grid gap-2"> <Field.Field>
<div> <Field.Label for="enable-user-signup">{m.enable_user_signups()}</Field.Label>
<Label class="mb-0" for="enable-user-signup">{m.enable_user_signups()}</Label> <Field.Description>
<p class="text-muted-foreground text-[0.8rem]"> {m.enable_user_signups_description()}
{m.enable_user_signups_description()} </Field.Description>
</p>
</div>
<Select.Root <Select.Root
type="single" type="single"
value={allowUserSignups} value={allowUserSignups}
@@ -145,13 +143,13 @@
</Select.Item> </Select.Item>
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
</div> </Field.Field>
<div> <Field.Field>
<Label for="default-groups" class="mb-0">{m.user_groups()}</Label> <Field.Label for="default-groups">{m.user_groups()}</Field.Label>
<p class="text-muted-foreground mt-1 mb-2 text-xs"> <Field.Description>
{m.user_creation_groups_description()} {m.user_creation_groups_description()}
</p> </Field.Description>
<SearchableMultiSelect <SearchableMultiSelect
id="default-groups" id="default-groups"
items={userGroups} items={userGroups}
@@ -163,14 +161,14 @@
isLoading={isUserSearchLoading} isLoading={isUserSearchLoading}
disableInternalSearch disableInternalSearch
/> />
</div> </Field.Field>
<div> <Field.Field>
<Label class="mb-0">{m.custom_claims()}</Label> <Field.Label>{m.custom_claims()}</Field.Label>
<p class="text-muted-foreground mt-1 mb-2 text-xs"> <Field.Description>
{m.user_creation_claims_description()} {m.user_creation_claims_description()}
</p> </Field.Description>
<CustomClaimsInput bind:customClaims /> <CustomClaimsInput bind:customClaims />
</div> </Field.Field>
<div class="flex justify-end pt-2"> <div class="flex justify-end pt-2">
<Button {isLoading} type="submit">{m.save()}</Button> <Button {isLoading} type="submit">{m.save()}</Button>

View File

@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog'; import * as Dialog from '$lib/components/ui/dialog';
import * as Field from '$lib/components/ui/field';
import { Input } from '$lib/components/ui/input'; import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label/index.js';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import { preventDefault } from '$lib/utils/event-util'; import { preventDefault } from '$lib/utils/event-util';
@@ -48,8 +48,8 @@
<form onsubmit={preventDefault(applyCustomColor)}> <form onsubmit={preventDefault(applyCustomColor)}>
<div class="space-y-4"> <div class="space-y-4">
<div> <Field.Field>
<Label for="custom-color-input" class="text-sm font-medium">{m.color_value()}</Label> <Field.Label for="custom-color-input">{m.color_value()}</Field.Label>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="w-full transition"> <div class="w-full transition">
<Input <Input
@@ -68,7 +68,7 @@
style="background-color: {customColorInput}" style="background-color: {customColorInput}"
></div> ></div>
</div> </div>
</div> </Field.Field>
</div> </div>
<Dialog.Footer class="mt-6"> <Dialog.Footer class="mt-6">

View File

@@ -6,7 +6,7 @@
import CopyToClipboard from '$lib/components/copy-to-clipboard.svelte'; import CopyToClipboard from '$lib/components/copy-to-clipboard.svelte';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card'; import * as Card from '$lib/components/ui/card';
import Label from '$lib/components/ui/label/label.svelte'; import * as Field from '$lib/components/ui/field';
import UserGroupSelection from '$lib/components/user-group-selection.svelte'; import UserGroupSelection from '$lib/components/user-group-selection.svelte';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import OidcService from '$lib/services/oidc-service'; import OidcService from '$lib/services/oidc-service';
@@ -132,14 +132,14 @@
<Card.Content> <Card.Content>
<div class="flex flex-col"> <div class="flex flex-col">
<div class="mb-2 flex flex-col sm:flex-row sm:items-center"> <div class="mb-2 flex flex-col sm:flex-row sm:items-center">
<Label class="mb-0 w-50">{m.client_id()}</Label> <Field.Label class="w-50">{m.client_id()}</Field.Label>
<CopyToClipboard value={client.id}> <CopyToClipboard value={client.id}>
<span class="text-muted-foreground text-sm" data-testid="client-id"> {client.id}</span> <span class="text-muted-foreground text-sm" data-testid="client-id"> {client.id}</span>
</CopyToClipboard> </CopyToClipboard>
</div> </div>
{#if !client.isPublic} {#if !client.isPublic}
<div class="mt-1 mb-2 flex flex-col sm:flex-row sm:items-center"> <div class="mt-1 mb-2 flex flex-col sm:flex-row sm:items-center">
<Label class="mb-0 w-50">{m.client_secret()}</Label> <Field.Label class="w-50">{m.client_secret()}</Field.Label>
{#if $clientSecretStore} {#if $clientSecretStore}
<CopyToClipboard value={$clientSecretStore}> <CopyToClipboard value={$clientSecretStore}>
<span class="text-muted-foreground text-sm" data-testid="client-secret"> <span class="text-muted-foreground text-sm" data-testid="client-secret">
@@ -166,7 +166,7 @@
<div transition:slide> <div transition:slide>
{#each Object.entries(setupDetails) as [key, value]} {#each Object.entries(setupDetails) as [key, value]}
<div class="mb-5 flex flex-col sm:flex-row sm:items-center"> <div class="mb-5 flex flex-col sm:flex-row sm:items-center">
<Label class="mb-0 w-50">{key}</Label> <Field.Label class="w-50">{key}</Field.Label>
<CopyToClipboard {value}> <CopyToClipboard {value}>
<span class="text-muted-foreground text-sm">{value}</span> <span class="text-muted-foreground text-sm">{value}</span>
</CopyToClipboard> </CopyToClipboard>

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import * as Dialog from '$lib/components/ui/dialog'; import * as Dialog from '$lib/components/ui/dialog';
import * as Field from '$lib/components/ui/field';
import Input from '$lib/components/ui/input/input.svelte'; import Input from '$lib/components/ui/input/input.svelte';
import Label from '$lib/components/ui/label/label.svelte';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
let { let {
@@ -23,7 +23,7 @@
<Dialog.Title>{m.one_time_link()}</Dialog.Title> <Dialog.Title>{m.one_time_link()}</Dialog.Title>
<Dialog.Description>{m.use_this_link_to_sign_in_once()}</Dialog.Description> <Dialog.Description>{m.use_this_link_to_sign_in_once()}</Dialog.Description>
</Dialog.Header> </Dialog.Header>
<Label for="one-time-link">{m.one_time_link()}</Label> <Field.Label for="one-time-link">{m.one_time_link()}</Field.Label>
<Input id="one-time-link" value={oneTimeLink} readonly /> <Input id="one-time-link" value={oneTimeLink} readonly />
</Dialog.Content> </Dialog.Content>
</Dialog.Root> </Dialog.Root>

View File

@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import FormInput from '$lib/components/form/form-input.svelte'; import FormInput from '$lib/components/form/form-input.svelte';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Field from '$lib/components/ui/field';
import { Input } from '$lib/components/ui/input'; import { Input } from '$lib/components/ui/input';
import Label from '$lib/components/ui/label/label.svelte';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import type { OidcClient, OidcClientFederatedIdentity } from '$lib/types/oidc.type'; import type { OidcClient, OidcClientFederatedIdentity } from '$lib/types/oidc.type';
import { LucideMinus, LucidePlus } from '@lucide/svelte'; import { LucideMinus, LucidePlus } from '@lucide/svelte';
@@ -67,7 +67,7 @@
{#each federatedIdentities as identity, i} {#each federatedIdentities as identity, i}
<div class="space-y-3 rounded-lg border p-4"> <div class="space-y-3 rounded-lg border p-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<Label class="text-sm font-medium">Identity {i + 1}</Label> <Field.Label>Identity {i + 1}</Field.Label>
{#if federatedIdentities.length > 0} {#if federatedIdentities.length > 0}
<Button <Button
variant="outline" variant="outline"
@@ -81,8 +81,8 @@
</div> </div>
<div class="grid grid-cols-1 gap-3 md:grid-cols-2"> <div class="grid grid-cols-1 gap-3 md:grid-cols-2">
<div> <Field.Field>
<Label required for="issuer-{i}" class="text-xs">Issuer</Label> <Field.Label required for="issuer-{i}">Issuer</Field.Label>
<Input <Input
id="issuer-{i}" id="issuer-{i}"
placeholder="https://example.com/" placeholder="https://example.com/"
@@ -91,12 +91,12 @@
aria-invalid={!!getFieldError(i, 'issuer')} aria-invalid={!!getFieldError(i, 'issuer')}
/> />
{#if getFieldError(i, 'issuer')} {#if getFieldError(i, 'issuer')}
<p class="text-destructive mt-1 text-xs">{getFieldError(i, 'issuer')}</p> <Field.Error>{getFieldError(i, 'issuer')}</Field.Error>
{/if} {/if}
</div> </Field.Field>
<div> <Field.Field>
<Label for="subject-{i}" class="text-xs">Subject</Label> <Field.Label for="subject-{i}">Subject</Field.Label>
<Input <Input
id="subject-{i}" id="subject-{i}"
placeholder="Defaults to the client ID" placeholder="Defaults to the client ID"
@@ -105,12 +105,12 @@
aria-invalid={!!getFieldError(i, 'subject')} aria-invalid={!!getFieldError(i, 'subject')}
/> />
{#if getFieldError(i, 'subject')} {#if getFieldError(i, 'subject')}
<p class="text-destructive mt-1 text-xs">{getFieldError(i, 'subject')}</p> <Field.Error>{getFieldError(i, 'subject')}</Field.Error>
{/if} {/if}
</div> </Field.Field>
<div> <Field.Field>
<Label for="audience-{i}" class="text-xs">Audience</Label> <Field.Label for="audience-{i}">Audience</Field.Label>
<Input <Input
id="audience-{i}" id="audience-{i}"
placeholder="Defaults to the Pocket ID URL" placeholder="Defaults to the Pocket ID URL"
@@ -119,12 +119,12 @@
aria-invalid={!!getFieldError(i, 'audience')} aria-invalid={!!getFieldError(i, 'audience')}
/> />
{#if getFieldError(i, 'audience')} {#if getFieldError(i, 'audience')}
<p class="text-destructive mt-1 text-xs">{getFieldError(i, 'audience')}</p> <Field.Error>{getFieldError(i, 'audience')}</Field.Error>
{/if} {/if}
</div> </Field.Field>
<div> <Field.Field>
<Label for="jwks-{i}" class="text-xs">JWKS URL</Label> <Field.Label for="jwks-{i}">JWKS URL</Field.Label>
<Input <Input
id="jwks-{i}" id="jwks-{i}"
placeholder="Defaults to {identity.issuer || '<issuer>'}/.well-known/jwks.json" placeholder="Defaults to {identity.issuer || '<issuer>'}/.well-known/jwks.json"
@@ -133,9 +133,9 @@
aria-invalid={!!getFieldError(i, 'jwks')} aria-invalid={!!getFieldError(i, 'jwks')}
/> />
{#if getFieldError(i, 'jwks')} {#if getFieldError(i, 'jwks')}
<p class="text-destructive mt-1 text-xs">{getFieldError(i, 'jwks')}</p> <Field.Error>{getFieldError(i, 'jwks')}</Field.Error>
{/if} {/if}
</div> </Field.Field>
</div> </div>
</div> </div>
{/each} {/each}

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import FormInput from '$lib/components/form/form-input.svelte'; import FormInput from '$lib/components/form/form-input.svelte';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Field from '$lib/components/ui/field';
import { Input } from '$lib/components/ui/input'; import { Input } from '$lib/components/ui/input';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import { LucideMinus, LucidePlus } from '@lucide/svelte'; import { LucideMinus, LucidePlus } from '@lucide/svelte';
@@ -44,7 +45,7 @@
</div> </div>
</FormInput> </FormInput>
{#if error} {#if error}
<p class="text-destructive mt-1 text-xs">{error}</p> <Field.Error>{error}</Field.Error>
{/if} {/if}
<Button <Button
class="mt-2" class="mt-2"

View File

@@ -2,7 +2,7 @@
import UrlFileInput from '$lib/components/form/url-file-input.svelte'; import UrlFileInput from '$lib/components/form/url-file-input.svelte';
import ImageBox from '$lib/components/image-box.svelte'; import ImageBox from '$lib/components/image-box.svelte';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { Label } from '$lib/components/ui/label'; import * as Field from '$lib/components/ui/field';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import { LucideX } from '@lucide/svelte'; import { LucideX } from '@lucide/svelte';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
@@ -26,7 +26,7 @@
let id = `oidc-client-logo-${light ? 'light' : 'dark'}`; let id = `oidc-client-logo-${light ? 'light' : 'dark'}`;
</script> </script>
<Label for={id}>{m.logo()}</Label> <Field.Label for={id}>{m.logo()}</Field.Label>
<div class="flex h-24 items-end gap-4"> <div class="flex h-24 items-end gap-4">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
{#if tabTriggers} {#if tabTriggers}

View File

@@ -5,7 +5,7 @@
import * as Alert from '$lib/components/ui/alert'; import * as Alert from '$lib/components/ui/alert';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog'; import * as Dialog from '$lib/components/ui/dialog';
import Label from '$lib/components/ui/label/label.svelte'; import * as Field from '$lib/components/ui/field';
import * as Tabs from '$lib/components/ui/tabs'; import * as Tabs from '$lib/components/ui/tabs';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import OidcService from '$lib/services/oidc-service'; import OidcService from '$lib/services/oidc-service';
@@ -14,6 +14,7 @@
import { debounced } from '$lib/utils/debounce-util'; import { debounced } from '$lib/utils/debounce-util';
import { getAxiosErrorMessage } from '$lib/utils/error-util'; import { getAxiosErrorMessage } from '$lib/utils/error-util';
import { LucideAlertTriangle } from '@lucide/svelte'; import { LucideAlertTriangle } from '@lucide/svelte';
import { Spinner } from '$lib/components/ui/spinner';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
let { let {
@@ -110,33 +111,31 @@
<div class="overflow-auto px-4"> <div class="overflow-auto px-4">
{#if loadingPreview} {#if loadingPreview}
<div class="flex items-center justify-center py-12"> <div class="flex items-center justify-center py-12">
<div class="h-8 w-8 animate-spin rounded-full border-b-2 border-gray-900"></div> <Spinner class="size-8" />
</div> </div>
{/if} {/if}
<div class="flex justify-start gap-3"> <div class="flex justify-start gap-3">
<div> <Field.Field>
<Label class="text-sm font-medium">{m.users()}</Label> <Field.Label>{m.users()}</Field.Label>
<div> <SearchableSelect
<SearchableSelect class="w-48"
class="w-48" selectText={m.select_user()}
selectText={m.select_user()} isLoading={isUserSearchLoading}
isLoading={isUserSearchLoading} items={Object.values(users).map((user) => ({
items={Object.values(users).map((user) => ({ value: user.id,
value: user.id, label: user.username
label: user.username }))}
}))} value={user?.id || ''}
value={user?.id || ''} oninput={(e) => onUserSearch(e.currentTarget.value)}
oninput={(e) => onUserSearch(e.currentTarget.value)} onSelect={(value) => {
onSelect={(value) => { user = users.find((u) => u.id === value) || null;
user = users.find((u) => u.id === value) || null; loadPreviewData();
loadPreviewData(); }}
}} />
/> </Field.Field>
</div> <Field.Field>
</div> <Field.Label>{m.scopes()}</Field.Label>
<div>
<Label class="text-sm font-medium">Scopes</Label>
<MultiSelect <MultiSelect
items={[ items={[
{ value: 'openid', label: 'openid' }, { value: 'openid', label: 'openid' },
@@ -146,7 +145,7 @@
]} ]}
bind:selectedItems={scopes} bind:selectedItems={scopes}
/> />
</div> </Field.Field>
</div> </div>
{#if errorMessage && !loadingPreview} {#if errorMessage && !loadingPreview}
@@ -186,7 +185,7 @@
{#snippet tabContent(data: any, title: string)} {#snippet tabContent(data: any, title: string)}
<div class="space-y-4"> <div class="space-y-4">
<div class="mb-6 flex items-center justify-between"> <div class="mb-6 flex items-center justify-between">
<Label class="text-lg font-semibold">{title}</Label> <span class="text-lg font-semibold">{title}</span>
<CopyToClipboard value={JSON.stringify(data, null, 2)}> <CopyToClipboard value={JSON.stringify(data, null, 2)}>
<Button size="sm" variant="outline">{m.copy_all()}</Button> <Button size="sm" variant="outline">{m.copy_all()}</Button>
</CopyToClipboard> </CopyToClipboard>
@@ -194,7 +193,7 @@
<div class="space-y-3"> <div class="space-y-3">
{#each Object.entries(data || {}) as [key, value]} {#each Object.entries(data || {}) as [key, value]}
<div class="grid grid-cols-1 items-start gap-4 border-b pb-3 md:grid-cols-[200px_1fr]"> <div class="grid grid-cols-1 items-start gap-4 border-b pb-3 md:grid-cols-[200px_1fr]">
<Label class="pt-1 text-sm font-medium">{key}</Label> <Field.Label class="pt-1">{key}</Field.Label>
<div class="min-w-0"> <div class="min-w-0">
<CopyToClipboard value={typeof value === 'string' ? value : JSON.stringify(value)}> <CopyToClipboard value={typeof value === 'string' ? value : JSON.stringify(value)}>
<div <div