refactor: migrate shadcn-components to Svelte 5 and TW4 (#551)

Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
Kyle Mendell
2025-05-21 12:15:27 -05:00
committed by Elias Schneider
parent 05b443d984
commit 28c85990ba
197 changed files with 8142 additions and 7471 deletions

View File

@@ -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"

View File

@@ -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}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}

View File

@@ -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}