mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-03-30 11:16:35 +00:00
refactor: migrate shadcn-components to Svelte 5 and TW4 (#551)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
committed by
Elias Schneider
parent
05b443d984
commit
28c85990ba
@@ -75,15 +75,16 @@
|
||||
onfocus={() => (isInputFocused = true)}
|
||||
onblur={() => (isInputFocused = false)}
|
||||
/>
|
||||
<Popover.Root
|
||||
open={isOpen}
|
||||
disableFocusTrap
|
||||
openFocus={() => {}}
|
||||
closeOnOutsideClick={false}
|
||||
closeOnEscape={false}
|
||||
>
|
||||
<Popover.Root open={isOpen}>
|
||||
<Popover.Trigger tabindex={-1} class="h-0 w-full" aria-hidden />
|
||||
<Popover.Content class="p-0" sideOffset={5} sameWidth>
|
||||
<Popover.Content
|
||||
sameWidth
|
||||
class="p-0"
|
||||
sideOffset={5}
|
||||
trapFocus={false}
|
||||
interactOutsideBehavior="ignore"
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
{#each filteredSuggestions as suggestion, index}
|
||||
<div
|
||||
role="button"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import CustomClaimService from '$lib/services/custom-claim-service';
|
||||
import type { CustomClaim } from '$lib/types/custom-claim.type';
|
||||
import { LucideMinus, LucidePlus } from 'lucide-svelte';
|
||||
import { LucideMinus, LucidePlus } from '@lucide/svelte';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import AutoCompleteInput from './auto-complete-input.svelte';
|
||||
@@ -51,25 +51,25 @@
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-label={m.remove_custom_claim()}
|
||||
on:click={() => (customClaims = customClaims.filter((_, index) => index !== i))}
|
||||
onclick={() => (customClaims = customClaims.filter((_, index) => index !== i))}
|
||||
>
|
||||
<LucideMinus class="h-4 w-4" />
|
||||
<LucideMinus class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</FormInput>
|
||||
{#if error}
|
||||
<p class="mt-1 text-sm text-red-500">{error}</p>
|
||||
<p class="text-destructive mt-1 text-xs">{error}</p>
|
||||
{/if}
|
||||
{#if customClaims.length < limit}
|
||||
<Button
|
||||
class="mt-2"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
on:click={() => (customClaims = [...customClaims, { key: '', value: '' }])}
|
||||
onclick={() => (customClaims = [...customClaims, { key: '', value: '' }])}
|
||||
>
|
||||
<LucidePlus class="mr-1 h-4 w-4" />
|
||||
<LucidePlus class="mr-1 size-4" />
|
||||
{customClaims.length === 0 ? m.add_custom_claim() : m.add_another()}
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
@@ -11,24 +11,53 @@
|
||||
getLocalTimeZone,
|
||||
type DateValue
|
||||
} from '@internationalized/date';
|
||||
import CalendarIcon from 'lucide-svelte/icons/calendar';
|
||||
import CalendarIcon from '@lucide/svelte/icons/calendar';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let { value = $bindable(), ...restProps }: HTMLAttributes<HTMLButtonElement> & { value: Date } =
|
||||
$props();
|
||||
type Props = {
|
||||
value?: Date;
|
||||
id?: string;
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let { value = $bindable(undefined), id, ...restProps }: Props = $props();
|
||||
|
||||
let calendarDisplayDate: CalendarDate | undefined = $state(
|
||||
value ? dateToCalendarDate(value) : undefined
|
||||
);
|
||||
|
||||
let date: CalendarDate = $state(dateToCalendarDate(value));
|
||||
let open = $state(false);
|
||||
|
||||
function dateToCalendarDate(date: Date) {
|
||||
return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
||||
function dateToCalendarDate(d: Date): CalendarDate {
|
||||
return new CalendarDate(d.getFullYear(), d.getMonth() + 1, d.getDate());
|
||||
}
|
||||
|
||||
function onValueChange(newDate?: DateValue) {
|
||||
if (!newDate) return;
|
||||
$effect(() => {
|
||||
if (calendarDisplayDate) {
|
||||
const newExternalDate = calendarDisplayDate.toDate(getLocalTimeZone());
|
||||
if (!value || value.getTime() !== newExternalDate.getTime()) {
|
||||
value = newExternalDate;
|
||||
}
|
||||
} else {
|
||||
if (value !== undefined) {
|
||||
value = undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
value = newDate.toDate(getLocalTimeZone());
|
||||
date = newDate as CalendarDate;
|
||||
$effect(() => {
|
||||
if (value) {
|
||||
const newInternalCalendarDate = dateToCalendarDate(value);
|
||||
if (!calendarDisplayDate || calendarDisplayDate.compare(newInternalCalendarDate) !== 0) {
|
||||
calendarDisplayDate = newInternalCalendarDate;
|
||||
}
|
||||
} else {
|
||||
if (calendarDisplayDate !== undefined) {
|
||||
calendarDisplayDate = undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function handleCalendarInteraction(newDateValue?: DateValue) {
|
||||
open = false;
|
||||
}
|
||||
|
||||
@@ -37,19 +66,28 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<Popover.Root openFocus {open} onOpenChange={(o) => (open = o)}>
|
||||
<Popover.Trigger asChild let:builder>
|
||||
<Button
|
||||
{...restProps}
|
||||
variant="outline"
|
||||
class={cn('w-full justify-start text-left font-normal', !value && 'text-muted-foreground')}
|
||||
builders={[builder]}
|
||||
>
|
||||
<CalendarIcon class="mr-2 h-4 w-4" />
|
||||
{date ? df.format(date.toDate(getLocalTimeZone())) : m.select_a_date()}
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="w-auto p-0" align="start">
|
||||
<Calendar bind:value={date} initialFocus {onValueChange} />
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
<div class="w-full" {...restProps}>
|
||||
<Popover.Root bind:open>
|
||||
<Popover.Trigger class="w-full">
|
||||
<Button
|
||||
{id}
|
||||
variant="outline"
|
||||
class={cn('w-full justify-start text-left font-normal', !value && 'text-muted-foreground')}
|
||||
aria-label={m.select_a_date()}
|
||||
>
|
||||
<CalendarIcon class="mr-2 size-4" />
|
||||
{calendarDisplayDate
|
||||
? df.format(calendarDisplayDate.toDate(getLocalTimeZone()))
|
||||
: m.select_a_date()}
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
type="single"
|
||||
bind:value={calendarDisplayDate}
|
||||
onValueChange={handleCalendarInteraction}
|
||||
initialFocus
|
||||
/>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</div>
|
||||
|
||||
@@ -45,17 +45,18 @@
|
||||
<DatePicker {id} bind:value={input.value as Date} />
|
||||
{:else}
|
||||
<Input
|
||||
aria-invalid={!!input.error}
|
||||
{id}
|
||||
{placeholder}
|
||||
{type}
|
||||
bind:value={input.value}
|
||||
{disabled}
|
||||
on:input={(e) => onInput?.(e)}
|
||||
oninput={(e) => onInput?.(e)}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if input?.error}
|
||||
<p class="mt-1 text-sm text-red-500">{input.error}</p>
|
||||
<p class="text-destructive mt-1 text-xs">{input.error}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { getProfilePictureUrl } from '$lib/utils/profile-picture-util';
|
||||
import { LucideLoader, LucideRefreshCw, LucideUpload } from 'lucide-svelte';
|
||||
import { LucideLoader, LucideRefreshCw, LucideUpload } from '@lucide/svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { openConfirmDialog } from '../confirm-dialog';
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
<div class="flex flex-col items-center gap-6 sm:flex-row">
|
||||
<div class="shrink-0">
|
||||
{#if isLdapUser}
|
||||
<Avatar.Root class="h-24 w-24">
|
||||
<Avatar.Root class="size-24">
|
||||
<Avatar.Image class="object-cover" src={imageDataURL} />
|
||||
</Avatar.Root>
|
||||
{:else}
|
||||
@@ -75,7 +75,7 @@
|
||||
accept="image/png, image/jpeg"
|
||||
onchange={onImageChange}
|
||||
>
|
||||
<div class="group relative h-24 w-24 rounded-full">
|
||||
<div class="group relative size-24 rounded-full">
|
||||
<Avatar.Root class="h-full w-full transition-opacity duration-200">
|
||||
<Avatar.Image
|
||||
class="object-cover group-hover:opacity-30 {isLoading ? 'opacity-30' : ''}"
|
||||
@@ -84,9 +84,9 @@
|
||||
</Avatar.Root>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
{#if isLoading}
|
||||
<LucideLoader class="h-5 w-5 animate-spin" />
|
||||
<LucideLoader class="size-5 animate-spin" />
|
||||
{:else}
|
||||
<LucideUpload class="h-5 w-5 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
<LucideUpload class="size-5 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -105,8 +105,8 @@
|
||||
{m.click_profile_picture_to_upload_custom()}
|
||||
</p>
|
||||
<p class="text-muted-foreground mb-2 text-sm">{m.image_should_be_in_format()}</p>
|
||||
<Button variant="outline" size="sm" on:click={onReset} disabled={isLoading || isLdapUser}>
|
||||
<LucideRefreshCw class="mr-2 h-4 w-4" />
|
||||
<Button variant="outline" size="sm" onclick={onReset} disabled={isLoading || isLdapUser}>
|
||||
<LucideRefreshCw class="mr-2 size-4" />
|
||||
{m.reset_to_default()}
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import * as Command from '$lib/components/ui/command';
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
import { cn } from '$lib/utils/style';
|
||||
import { LucideCheck, LucideChevronDown } from 'lucide-svelte';
|
||||
import { LucideCheck, LucideChevronDown } from '@lucide/svelte';
|
||||
import { tick } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
@@ -52,21 +52,19 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<Popover.Root bind:open let:ids>
|
||||
<Popover.Trigger asChild let:builder>
|
||||
<Popover.Root bind:open {...restProps}>
|
||||
<Popover.Trigger class="w-full">
|
||||
<Button
|
||||
{...restProps}
|
||||
builders={[builder]}
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
class={cn('justify-between', restProps.class)}
|
||||
>
|
||||
{items.find((item) => item.value === value)?.label || 'Select an option'}
|
||||
<LucideChevronDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<LucideChevronDown class="ml-2 size-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="p-0" sameWidth>
|
||||
<Popover.Content class="p-0">
|
||||
<Command.Root shouldFilter={false}>
|
||||
<Command.Input placeholder="Search..." oninput={(e: any) => filterItems(e.target.value)} />
|
||||
<Command.Empty>No results found.</Command.Empty>
|
||||
@@ -77,10 +75,11 @@
|
||||
onSelect={() => {
|
||||
value = item.value;
|
||||
onSelect?.(item.value);
|
||||
closeAndFocusTrigger(ids.trigger);
|
||||
// If you need to focus the trigger, you may need to refactor to get the trigger id another way
|
||||
closeAndFocusTrigger('popover-trigger');
|
||||
}}
|
||||
>
|
||||
<LucideCheck class={cn('mr-2 h-4 w-4', value !== item.value && 'text-transparent')} />
|
||||
<LucideCheck class={cn('mr-2 size-4', value !== item.value && 'text-transparent')} />
|
||||
{item.label}
|
||||
</Command.Item>
|
||||
{/each}
|
||||
|
||||
Reference in New Issue
Block a user