mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-02-17 09:14:11 +00:00
refactor: update forms and other areas to use new shadcn components (#1115)
Co-authored-by: Elias Schneider <login@eliasschneider.com> Co-authored-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -483,5 +483,6 @@
|
|||||||
"allowed_oidc_clients_updated_successfully": "Allowed OIDC clients updated successfully",
|
"allowed_oidc_clients_updated_successfully": "Allowed OIDC clients updated successfully",
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
"restricted": "Restricted"
|
"restricted": "Restricted",
|
||||||
|
"scopes": "Scopes"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -46,12 +46,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={labelFor ?? id}>{label}</Label>
|
<Field.Label required={input?.required} class="mb-0" for={labelFor ?? id}>{label}</Field.Label>
|
||||||
{/if}
|
{/if}
|
||||||
{#if description}
|
{#if description}
|
||||||
<p class="text-muted-foreground mt-1 text-xs">
|
<Field.Description>
|
||||||
<FormattedMessage m={description} />
|
<FormattedMessage m={description} />
|
||||||
{#if docsLink}
|
{#if docsLink}
|
||||||
<a
|
<a
|
||||||
@@ -63,9 +63,8 @@
|
|||||||
<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}
|
||||||
@@ -84,7 +83,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if input?.error}
|
{#if input?.error}
|
||||||
<p class="text-destructive mt-1 text-start text-xs">{input.error}</p>
|
<Field.Error>{input.error}</Field.Error>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</Field.Field>
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 { cn } from '$lib/utils/style';
|
import { Spinner } from '$lib/components/ui/spinner';
|
||||||
import { m } from '$lib/paraglide/messages';
|
import { m } from '$lib/paraglide/messages';
|
||||||
import { LoaderCircle, LucideCheck, LucideChevronDown } from '@lucide/svelte';
|
import { cn } from '$lib/utils/style';
|
||||||
|
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()}
|
||||||
|
|||||||
@@ -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()}
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -78,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="w-full">
|
||||||
{selectedExpiration}
|
{selectedExpiration}
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
@@ -94,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
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
<p class="text-3xl font-bold">{code}</p>
|
<p class="text-3xl font-bold">{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 />
|
||||||
|
|||||||
@@ -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,28 +20,22 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="bg-card hover:bg-muted/50 group rounded-lg p-3 transition-colors">
|
<Item.Root variant="transparent" class="hover:bg-muted transition-colors py-3 px-0 sm:px-4">
|
||||||
<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">
|
|
||||||
<div class="bg-primary/10 text-primary mt-1 rounded-lg p-2">
|
|
||||||
{#if icon}{@const Icon = icon}
|
{#if icon}{@const Icon = icon}
|
||||||
<Icon class="size-5" />
|
<Icon class="size-5" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</Item.Media>
|
||||||
<div>
|
<Item.Content class="gap-0.5">
|
||||||
<div class="flex items-center gap-2">
|
<Item.Title>{label}</Item.Title>
|
||||||
<p class="font-medium">{label}</p>
|
|
||||||
</div>
|
|
||||||
{#if description}
|
{#if description}
|
||||||
<div class="text-muted-foreground mt-1 flex items-center text-xs">
|
<Item.Description class="flex items-center">
|
||||||
<LucideCalendar class="mr-1 size-3" />
|
<LucideCalendar class="mr-1 size-3" />
|
||||||
{description}
|
{description}
|
||||||
</div>
|
</Item.Description>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</Item.Content>
|
||||||
</div>
|
<Item.Actions>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Tooltip.Provider>
|
<Tooltip.Provider>
|
||||||
<Tooltip.Root>
|
<Tooltip.Root>
|
||||||
<Tooltip.Trigger>
|
<Tooltip.Trigger>
|
||||||
@@ -55,8 +50,8 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Content>{m.rename()}</Tooltip.Content>
|
<Tooltip.Content>{m.rename()}</Tooltip.Content>
|
||||||
</Tooltip.Root></Tooltip.Provider
|
</Tooltip.Root>
|
||||||
>
|
</Tooltip.Provider>
|
||||||
|
|
||||||
<Tooltip.Provider>
|
<Tooltip.Provider>
|
||||||
<Tooltip.Root>
|
<Tooltip.Root>
|
||||||
@@ -72,8 +67,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Content>{m.delete()}</Tooltip.Content>
|
<Tooltip.Content>{m.delete()}</Tooltip.Content>
|
||||||
</Tooltip.Root></Tooltip.Provider
|
</Tooltip.Root>
|
||||||
>
|
</Tooltip.Provider>
|
||||||
</div>
|
</Item.Actions>
|
||||||
</div>
|
</Item.Root>
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -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 class="py-1.5 px-0">
|
||||||
<div class="bg-muted mr-5 rounded-lg p-2"><SvelteComponent /></div>
|
<Item.Media class="bg-muted !self-center rounded-lg p-2 !translate-y-0 h-full">
|
||||||
<div class="text-start">
|
<SvelteComponent class="size-5" />
|
||||||
<h3 class="font-semibold">{name}</h3>
|
</Item.Media>
|
||||||
<p class="text-muted-foreground text-sm">{description}</p>
|
<Item.Content class="text-start">
|
||||||
</div>
|
<Item.Title class="font-semibold">{name}</Item.Title>
|
||||||
</div>
|
<Item.Description>{description}</Item.Description>
|
||||||
|
</Item.Content>
|
||||||
|
</Item.Root>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -136,7 +136,7 @@
|
|||||||
value={$inputs.ttl.value.toString()}
|
value={$inputs.ttl.value.toString()}
|
||||||
onValueChange={(v) => v && form.setValue('ttl', Number(v))}
|
onValueChange={(v) => v && form.setValue('ttl', Number(v))}
|
||||||
>
|
>
|
||||||
<Select.Trigger id="expiration" class="h-9 w-full">
|
<Select.Trigger id="expiration" class="w-full">
|
||||||
{getExpirationLabel($inputs.ttl.value)}
|
{getExpirationLabel($inputs.ttl.value)}
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
|
|||||||
@@ -331,7 +331,7 @@
|
|||||||
value={items?.pagination.itemsPerPage.toString()}
|
value={items?.pagination.itemsPerPage.toString()}
|
||||||
onValueChange={(v) => onPageSizeChange(Number(v))}
|
onValueChange={(v) => onPageSizeChange(Number(v))}
|
||||||
>
|
>
|
||||||
<Select.Trigger class="h-9 w-[80px]">
|
<Select.Trigger class="w-20">
|
||||||
{items?.pagination.itemsPerPage}
|
{items?.pagination.itemsPerPage}
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: 'h-10 px-4 py-2',
|
default: 'h-10 px-4 py-2',
|
||||||
sm: 'h-9 rounded-md px-3',
|
sm: 'h-8 rounded-md px-3',
|
||||||
lg: 'h-11 rounded-md px-8',
|
lg: 'h-11 rounded-md px-8',
|
||||||
icon: 'h-10 w-10'
|
icon: 'h-10 w-10'
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
20
frontend/src/lib/components/ui/field/field-content.svelte
Normal file
20
frontend/src/lib/components/ui/field/field-content.svelte
Normal 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>
|
||||||
@@ -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>
|
||||||
58
frontend/src/lib/components/ui/field/field-error.svelte
Normal file
58
frontend/src/lib/components/ui/field/field-error.svelte
Normal 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}
|
||||||
23
frontend/src/lib/components/ui/field/field-group.svelte
Normal file
23
frontend/src/lib/components/ui/field/field-group.svelte
Normal 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-4 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
30
frontend/src/lib/components/ui/field/field-label.svelte
Normal file
30
frontend/src/lib/components/ui/field/field-label.svelte
Normal 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 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>
|
||||||
29
frontend/src/lib/components/ui/field/field-legend.svelte
Normal file
29
frontend/src/lib/components/ui/field/field-legend.svelte
Normal 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>
|
||||||
38
frontend/src/lib/components/ui/field/field-separator.svelte
Normal file
38
frontend/src/lib/components/ui/field/field-separator.svelte
Normal 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>
|
||||||
24
frontend/src/lib/components/ui/field/field-set.svelte
Normal file
24
frontend/src/lib/components/ui/field/field-set.svelte
Normal 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-4',
|
||||||
|
'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</fieldset>
|
||||||
23
frontend/src/lib/components/ui/field/field-title.svelte
Normal file
23
frontend/src/lib/components/ui/field/field-title.svelte
Normal 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>
|
||||||
53
frontend/src/lib/components/ui/field/field.svelte
Normal file
53
frontend/src/lib/components/ui/field/field.svelte
Normal 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-1.5',
|
||||||
|
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>
|
||||||
33
frontend/src/lib/components/ui/field/index.ts
Normal file
33
frontend/src/lib/components/ui/field/index.ts
Normal 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,
|
||||||
|
};
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="input"
|
data-slot="input"
|
||||||
class={cn(
|
class={cn(
|
||||||
'selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-sm font-medium shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
'selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground flex h-8 w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-sm font-medium shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||||
className
|
className
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="input"
|
data-slot="input"
|
||||||
class={cn(
|
class={cn(
|
||||||
'border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
'border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground flex h-8 w-full min-w-0 rounded-md border px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||||
className
|
className
|
||||||
|
|||||||
34
frontend/src/lib/components/ui/item/index.ts
Normal file
34
frontend/src/lib/components/ui/item/index.ts
Normal 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,
|
||||||
|
};
|
||||||
23
frontend/src/lib/components/ui/item/item-actions.svelte
Normal file
23
frontend/src/lib/components/ui/item/item-actions.svelte
Normal 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,
|
||||||
|
wrapOnMobile,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
wrapOnMobile?: boolean;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="item-actions"
|
||||||
|
class={cn('flex items-center gap-2', wrapOnMobile && 'w-full pl-8 sm:w-auto sm:pl-0 pt-1', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
20
frontend/src/lib/components/ui/item/item-content.svelte
Normal file
20
frontend/src/lib/components/ui/item/item-content.svelte
Normal 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>
|
||||||
24
frontend/src/lib/components/ui/item/item-description.svelte
Normal file
24
frontend/src/lib/components/ui/item/item-description.svelte
Normal 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 text-balance text-sm font-normal leading-normal",
|
||||||
|
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</p>
|
||||||
20
frontend/src/lib/components/ui/item/item-footer.svelte
Normal file
20
frontend/src/lib/components/ui/item/item-footer.svelte
Normal 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>
|
||||||
21
frontend/src/lib/components/ui/item/item-group.svelte
Normal file
21
frontend/src/lib/components/ui/item/item-group.svelte
Normal 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>
|
||||||
20
frontend/src/lib/components/ui/item/item-header.svelte
Normal file
20
frontend/src/lib/components/ui/item/item-header.svelte
Normal 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>
|
||||||
43
frontend/src/lib/components/ui/item/item-media.svelte
Normal file
43
frontend/src/lib/components/ui/item/item-media.svelte
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<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',
|
||||||
|
transparent: '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>
|
||||||
19
frontend/src/lib/components/ui/item/item-separator.svelte
Normal file
19
frontend/src/lib/components/ui/item/item-separator.svelte
Normal 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}
|
||||||
|
/>
|
||||||
22
frontend/src/lib/components/ui/item/item-title.svelte
Normal file
22
frontend/src/lib/components/ui/item/item-title.svelte
Normal 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 font-semibold leading-snug", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
62
frontend/src/lib/components/ui/item/item.svelte
Normal file
62
frontend/src/lib/components/ui/item/item.svelte
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { tv, type VariantProps } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const itemVariants = tv({
|
||||||
|
base: 'group/item focus-visible:border-ring focus-visible:ring-ring/50 flex flex-wrap items-center rounded-xl border border-transparent text-sm outline-none transition-colors duration-100 focus-visible:ring-[3px]',
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-transparent [a&]:hover:bg-accent/50 [a&]:transition-colors',
|
||||||
|
outline: 'border-border [a&]:hover:bg-accent/50 [a&]:transition-colors',
|
||||||
|
muted: 'bg-muted/50 [a&]:hover:bg-accent/50 [a&]:transition-colors',
|
||||||
|
card: 'bg-card shadow-sm [a&]:hover:bg-accent/50 [a&]:transition-colors',
|
||||||
|
transparent: 'bg-transparent'
|
||||||
|
},
|
||||||
|
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}
|
||||||
1
frontend/src/lib/components/ui/spinner/index.ts
Normal file
1
frontend/src/lib/components/ui/spinner/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as Spinner } from "./spinner.svelte";
|
||||||
14
frontend/src/lib/components/ui/spinner/spinner.svelte
Normal file
14
frontend/src/lib/components/ui/spinner/spinner.svelte
Normal 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}
|
||||||
|
/>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" class="gap-5">
|
||||||
<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 !self-center !translate-y-0">
|
||||||
<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 class="text-start">
|
||||||
<p class="text-muted-foreground text-sm">{method.description}</p>
|
<Item.Title class="text-lg font-semibold">{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
|
||||||
|
|||||||
@@ -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,28 +101,24 @@
|
|||||||
|
|
||||||
<!-- 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>
|
|
||||||
<Card.Description>
|
|
||||||
{m.create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey()}
|
{m.create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey()}
|
||||||
</Card.Description>
|
</Item.Description>
|
||||||
</div>
|
</Item.Content>
|
||||||
|
<Item.Actions class="w-full sm:w-auto">
|
||||||
<Button variant="outline" class="w-full" onclick={() => (showLoginCodeModal = true)}>
|
<Button variant="outline" class="w-full" onclick={() => (showLoginCodeModal = true)}>
|
||||||
{m.create()}
|
{m.create()}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Item.Actions>
|
||||||
</Card.Header>
|
</Item.Root>
|
||||||
</Card.Root>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Account details card -->
|
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Card.Title>
|
<Card.Title>
|
||||||
@@ -140,76 +137,63 @@
|
|||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|
||||||
<!-- Passkey management card -->
|
<Item.Group class="bg-card rounded-xl border p-4 shadow-sm">
|
||||||
<div>
|
<Item.Root class="border-none bg-transparent p-0">
|
||||||
<Card.Root class="gap-3">
|
<Item.Media class="text-primary/80">
|
||||||
<Card.Header>
|
<KeyRound class="size-5" />
|
||||||
<div class="flex items-center justify-between">
|
</Item.Media>
|
||||||
<div>
|
<Item.Content class="min-w-52">
|
||||||
<Card.Title>
|
<Item.Title class="text-xl font-semibold">{m.passkeys()}</Item.Title>
|
||||||
<KeyRound class="text-primary/80 size-5" />
|
<Item.Description>
|
||||||
{m.passkeys()}
|
|
||||||
</Card.Title>
|
|
||||||
<Card.Description>
|
|
||||||
{m.manage_your_passkeys_that_you_can_use_to_authenticate_yourself()}
|
{m.manage_your_passkeys_that_you_can_use_to_authenticate_yourself()}
|
||||||
</Card.Description>
|
</Item.Description>
|
||||||
</div>
|
</Item.Content>
|
||||||
<Button variant="outline" class="ml-3" onclick={createPasskey}>
|
<Item.Actions>
|
||||||
|
<Button variant="outline" onclick={createPasskey}>
|
||||||
{m.add_passkey()}
|
{m.add_passkey()}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Item.Actions>
|
||||||
</Card.Header>
|
</Item.Root>
|
||||||
{#if passkeys.length != 0}
|
{#if passkeys.length != 0}
|
||||||
<Card.Content>
|
|
||||||
<PasskeyList bind:passkeys />
|
<PasskeyList bind:passkeys />
|
||||||
</Card.Content>
|
|
||||||
{/if}
|
{/if}
|
||||||
</Card.Root>
|
</Item.Group>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Login code card -->
|
|
||||||
<div class="hidden sm:block">
|
<div class="hidden sm:block">
|
||||||
<Card.Root>
|
<Item.Root variant="card" class="border-border">
|
||||||
<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>
|
|
||||||
<Card.Description>
|
|
||||||
{m.create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey()}
|
{m.create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey()}
|
||||||
</Card.Description>
|
</Item.Description>
|
||||||
</div>
|
</Item.Content>
|
||||||
|
<Item.Actions>
|
||||||
<Button variant="outline" onclick={() => (showLoginCodeModal = true)}>
|
<Button variant="outline" onclick={() => (showLoginCodeModal = true)}>
|
||||||
{m.create()}
|
{m.create()}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Item.Actions>
|
||||||
</Card.Header>
|
</Item.Root>
|
||||||
</Card.Root>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Language selection card -->
|
<Item.Root variant="card" class="border-border mb-2">
|
||||||
<div>
|
<Item.Media class="text-primary/80">
|
||||||
<Card.Root>
|
<Languages class="size-5" />
|
||||||
<Card.Header>
|
</Item.Media>
|
||||||
<div class="flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center">
|
<Item.Content class="min-w-52">
|
||||||
<div>
|
<Item.Title>{m.language()}</Item.Title>
|
||||||
<Card.Title>
|
<Item.Description>
|
||||||
<Languages class="text-primary/80 size-5" />
|
|
||||||
{m.language()}
|
|
||||||
</Card.Title>
|
|
||||||
<Card.Description>
|
|
||||||
{m.select_the_language_you_want_to_use()}
|
{m.select_the_language_you_want_to_use()}
|
||||||
<br />
|
<br />
|
||||||
<FormattedMessage m={m.contribute_to_translation()} />
|
<FormattedMessage m={m.contribute_to_translation()} />
|
||||||
</Card.Description>
|
</Item.Description>
|
||||||
</div>
|
</Item.Content>
|
||||||
|
<Item.Actions>
|
||||||
<LocalePicker />
|
<LocalePicker />
|
||||||
</div>
|
</Item.Actions>
|
||||||
</Card.Header>
|
</Item.Root>
|
||||||
</Card.Root>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<RenamePasskeyModal
|
<RenamePasskeyModal
|
||||||
bind:passkey={passkeyToRename}
|
bind:passkey={passkeyToRename}
|
||||||
|
|||||||
@@ -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 class="m-2" />
|
||||||
|
|
||||||
<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} />
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FormInput label={m.last_name()} bind:input={$inputs.lastName} onInput={onNameInput} />
|
<FormInput label={m.last_name()} bind:input={$inputs.lastName} onInput={onNameInput} />
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FormInput
|
<FormInput
|
||||||
label={m.display_name()}
|
label={m.display_name()}
|
||||||
bind:input={$inputs.displayName}
|
bind:input={$inputs.displayName}
|
||||||
onInput={() => (hasManualDisplayNameEdit = true)}
|
onInput={() => (hasManualDisplayNameEdit = true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FormInput label={m.username()} bind:input={$inputs.username} />
|
<FormInput label={m.username()} bind:input={$inputs.username} />
|
||||||
</div>
|
<FormInput label={m.email()} type="email" bind:input={$inputs.email} />
|
||||||
<div>
|
</Field.Group>
|
||||||
<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>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Select.Root type="single" value={currentLocale} onValueChange={(v) => updateLocale(v as Locale)}>
|
<Select.Root type="single" value={currentLocale} onValueChange={(v) => updateLocale(v as Locale)}>
|
||||||
<Select.Trigger class="h-9 max-w-[200px]" aria-label="Select locale">
|
<Select.Trigger class="max-w-[200px]" aria-label="Select locale">
|
||||||
{locales[currentLocale]}
|
{locales[currentLocale]}
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
|
|||||||
@@ -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="mt-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}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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', {
|
||||||
|
|||||||
@@ -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()}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 UserGroupInput from '$lib/components/form/user-group-input.svelte';
|
import UserGroupInput from '$lib/components/form/user-group-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 * 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 appConfigStore from '$lib/stores/application-configuration-store';
|
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||||
@@ -58,12 +58,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">
|
<div class="grid gap-2">
|
||||||
<div>
|
<Field.Field>
|
||||||
<Label class="mb-0" for="enable-user-signup">{m.enable_user_signups()}</Label>
|
<Field.Label for="enable-user-signup">{m.enable_user_signups()}</Field.Label>
|
||||||
<p class="text-muted-foreground text-[0.8rem]">
|
<Field.Description>
|
||||||
{m.enable_user_signups_description()}
|
{m.enable_user_signups_description()}
|
||||||
</p>
|
</Field.Description>
|
||||||
</div>
|
|
||||||
<Select.Root
|
<Select.Root
|
||||||
type="single"
|
type="single"
|
||||||
value={allowUserSignups}
|
value={allowUserSignups}
|
||||||
@@ -104,22 +103,23 @@
|
|||||||
</Select.Item>
|
</Select.Item>
|
||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select.Root>
|
</Select.Root>
|
||||||
|
</Field.Field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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>
|
||||||
<UserGroupInput bind:selectedGroupIds />
|
<UserGroupInput bind:selectedGroupIds />
|
||||||
</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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -177,14 +177,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">
|
||||||
@@ -211,7 +211,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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -233,7 +233,7 @@
|
|||||||
onLogoChange={(input) => onLogoChange(input, true)}
|
onLogoChange={(input) => onLogoChange(input, true)}
|
||||||
>
|
>
|
||||||
{#snippet tabTriggers()}
|
{#snippet tabTriggers()}
|
||||||
<Tabs.List class="grid h-9 w-full grid-cols-2">
|
<Tabs.List class="grid h-8 w-full grid-cols-2">
|
||||||
<Tabs.Trigger value="light-logo" class="px-3">
|
<Tabs.Trigger value="light-logo" class="px-3">
|
||||||
<LucideSun class="size-4" />
|
<LucideSun class="size-4" />
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
@@ -253,7 +253,7 @@
|
|||||||
onLogoChange={(input) => onLogoChange(input, false)}
|
onLogoChange={(input) => onLogoChange(input, false)}
|
||||||
>
|
>
|
||||||
{#snippet tabTriggers()}
|
{#snippet tabTriggers()}
|
||||||
<Tabs.List class="grid h-9 w-full grid-cols-2">
|
<Tabs.List class="grid h-8 w-full grid-cols-2">
|
||||||
<Tabs.Trigger value="light-logo" class="px-3">
|
<Tabs.Trigger value="light-logo" class="px-3">
|
||||||
<LucideSun class="size-4" />
|
<LucideSun class="size-4" />
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
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 { Spinner } from '$lib/components/ui/spinner';
|
||||||
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';
|
||||||
@@ -110,16 +111,14 @@
|
|||||||
<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 class="w-auto min-w-48">
|
||||||
<Label class="text-sm font-medium">{m.users()}</Label>
|
<Field.Label>{m.users()}</Field.Label>
|
||||||
<div>
|
|
||||||
<SearchableSelect
|
<SearchableSelect
|
||||||
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) => ({
|
||||||
@@ -133,10 +132,9 @@
|
|||||||
loadPreviewData();
|
loadPreviewData();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Field.Field>
|
||||||
</div>
|
<Field.Field class="w-auto">
|
||||||
<div>
|
<Field.Label>{m.scopes()}</Field.Label>
|
||||||
<Label class="text-sm font-medium">Scopes</Label>
|
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
items={[
|
items={[
|
||||||
{ value: 'openid', label: 'openid' },
|
{ value: 'openid', label: 'openid' },
|
||||||
@@ -146,7 +144,7 @@
|
|||||||
]}
|
]}
|
||||||
bind:selectedItems={scopes}
|
bind:selectedItems={scopes}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Field.Field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if errorMessage && !loadingPreview}
|
{#if errorMessage && !loadingPreview}
|
||||||
@@ -186,7 +184,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 +192,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
|
||||||
|
|||||||
759
pnpm-lock.yaml
generated
759
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user