mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-02-10 14:54:19 +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:
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import * as Field from '$lib/components/ui/field';
|
||||
|
||||
let {
|
||||
id,
|
||||
@@ -26,14 +26,10 @@
|
||||
onCheckedChange={(v) => onCheckedChange && onCheckedChange(v == true)}
|
||||
bind:checked
|
||||
/>
|
||||
<div class="grid gap-1.5 leading-none">
|
||||
<Label for={id} class="mb-0 text-sm leading-none font-medium">
|
||||
{label}
|
||||
</Label>
|
||||
<Field.Field class="gap-0">
|
||||
<Field.Label for={id}>{label}</Field.Label>
|
||||
{#if description}
|
||||
<p class="text-muted-foreground text-[0.8rem]">
|
||||
{description}
|
||||
</p>
|
||||
<Field.Description>{description}</Field.Description>
|
||||
{/if}
|
||||
</div>
|
||||
</Field.Field>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
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 { Label } from '$lib/components/ui/label';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import type { FormInput } from '$lib/utils/form-util';
|
||||
import { LucideExternalLink } from '@lucide/svelte';
|
||||
@@ -46,12 +46,12 @@
|
||||
const id = label?.toLowerCase().replace(/ /g, '-');
|
||||
</script>
|
||||
|
||||
<div {...restProps}>
|
||||
<Field.Field data-disabled={disabled} {...restProps}>
|
||||
{#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 description}
|
||||
<p class="text-muted-foreground mt-1 text-xs">
|
||||
<Field.Description>
|
||||
<FormattedMessage m={description} />
|
||||
{#if docsLink}
|
||||
<a
|
||||
@@ -63,28 +63,26 @@
|
||||
<LucideExternalLink class="inline size-3 align-text-top" />
|
||||
</a>
|
||||
{/if}
|
||||
</p>
|
||||
</Field.Description>
|
||||
{/if}
|
||||
<div class={label || description ? 'mt-2' : ''}>
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else if input}
|
||||
{#if type === 'date'}
|
||||
<DatePicker {id} bind:value={input.value as Date} />
|
||||
{:else}
|
||||
<Input
|
||||
aria-invalid={!!input.error}
|
||||
{id}
|
||||
{placeholder}
|
||||
{type}
|
||||
bind:value={input.value}
|
||||
{disabled}
|
||||
oninput={(e) => onInput?.(e)}
|
||||
/>
|
||||
{/if}
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else if input}
|
||||
{#if type === 'date'}
|
||||
<DatePicker {id} bind:value={input.value as Date} />
|
||||
{:else}
|
||||
<Input
|
||||
aria-invalid={!!input.error}
|
||||
{id}
|
||||
{placeholder}
|
||||
{type}
|
||||
bind:value={input.value}
|
||||
{disabled}
|
||||
oninput={(e) => onInput?.(e)}
|
||||
/>
|
||||
{/if}
|
||||
{#if input?.error}
|
||||
<p class="text-destructive mt-1 text-start text-xs">{input.error}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if input?.error}
|
||||
<Field.Error>{input.error}</Field.Error>
|
||||
{/if}
|
||||
</Field.Field>
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
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 { openConfirmDialog } from '../confirm-dialog';
|
||||
|
||||
@@ -88,7 +89,7 @@
|
||||
</Avatar.Root>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
{#if isLoading}
|
||||
<LucideLoader class="size-5 animate-spin" />
|
||||
<Spinner class="size-5" />
|
||||
{:else}
|
||||
<LucideUpload class="size-5 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
{/if}
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Command from '$lib/components/ui/command';
|
||||
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 { LoaderCircle, LucideCheck, LucideChevronDown } from '@lucide/svelte';
|
||||
import { cn } from '$lib/utils/style';
|
||||
import { LucideCheck, LucideChevronDown } from '@lucide/svelte';
|
||||
import type { FormEventHandler } from 'svelte/elements';
|
||||
|
||||
type Item = {
|
||||
@@ -108,7 +109,7 @@
|
||||
<Command.Empty>
|
||||
{#if isLoading}
|
||||
<div class="flex w-full items-center justify-center py-2">
|
||||
<LoaderCircle class="size-4 animate-spin" />
|
||||
<Spinner />
|
||||
</div>
|
||||
{:else}
|
||||
{m.no_items_found()}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Command from '$lib/components/ui/command';
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
import { Spinner } from '$lib/components/ui/spinner';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { cn } from '$lib/utils/style';
|
||||
import { LoaderCircle, LucideCheck, LucideChevronDown } from '@lucide/svelte';
|
||||
import { LucideCheck, LucideChevronDown } from '@lucide/svelte';
|
||||
import { tick } from 'svelte';
|
||||
import type { FormEventHandler, HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
@@ -90,7 +91,7 @@
|
||||
<Command.Empty>
|
||||
{#if isLoading}
|
||||
<div class="flex w-full justify-center">
|
||||
<LoaderCircle class="size-4 animate-spin" />
|
||||
<Spinner />
|
||||
</div>
|
||||
{:else}
|
||||
{m.no_items_found()}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import Qrcode from '$lib/components/qrcode/qrcode.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
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 { Separator } from '$lib/components/ui/separator';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
@@ -78,14 +78,14 @@
|
||||
</Dialog.Header>
|
||||
|
||||
{#if oneTimeLink === null}
|
||||
<div>
|
||||
<Label for="expiration">{m.expiration()}</Label>
|
||||
<Field.Field>
|
||||
<Field.Label for="expiration">{m.expiration()}</Field.Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={Object.keys(availableExpirations)[0]}
|
||||
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}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
@@ -94,7 +94,7 @@
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
</Field.Field>
|
||||
<Dialog.Footer class="mt-2">
|
||||
{#if $appConfigStore.emailOneTimeAccessAsAdminEnabled}
|
||||
<Button
|
||||
@@ -115,7 +115,7 @@
|
||||
<p class="text-3xl font-bold">{code}</p>
|
||||
</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 />
|
||||
<p class="text-xs text-nowrap">{m.or_visit()}</p>
|
||||
<Separator />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
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 { m } from '$lib/paraglide/messages';
|
||||
import { LucideCalendar, LucidePencil, LucideTrash, type Icon as IconType } from '@lucide/svelte';
|
||||
@@ -19,61 +20,54 @@
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="bg-card hover:bg-muted/50 group rounded-lg p-3 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<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}
|
||||
<Icon class="size-5" />
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="font-medium">{label}</p>
|
||||
</div>
|
||||
{#if description}
|
||||
<div class="text-muted-foreground mt-1 flex items-center text-xs">
|
||||
<LucideCalendar class="mr-1 size-3" />
|
||||
{description}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<Item.Root variant="transparent" class="hover:bg-muted transition-colors py-3 px-0 sm:px-4">
|
||||
<Item.Media class="bg-primary/10 text-primary rounded-lg p-2">
|
||||
{#if icon}{@const Icon = icon}
|
||||
<Icon class="size-5" />
|
||||
{/if}
|
||||
</Item.Media>
|
||||
<Item.Content class="gap-0.5">
|
||||
<Item.Title>{label}</Item.Title>
|
||||
{#if description}
|
||||
<Item.Description class="flex items-center">
|
||||
<LucideCalendar class="mr-1 size-3" />
|
||||
{description}
|
||||
</Item.Description>
|
||||
{/if}
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
onclick={onRename}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="size-8"
|
||||
aria-label={m.rename()}
|
||||
>
|
||||
<LucidePencil class="size-4" />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>{m.rename()}</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
onclick={onRename}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="size-8"
|
||||
aria-label={m.rename()}
|
||||
>
|
||||
<LucidePencil class="size-4" />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>{m.rename()}</Tooltip.Content>
|
||||
</Tooltip.Root></Tooltip.Provider
|
||||
>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
onclick={onDelete}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="hover:bg-destructive/10 hover:text-destructive size-8"
|
||||
aria-label={m.delete()}
|
||||
>
|
||||
<LucideTrash class="size-4" />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>{m.delete()}</Tooltip.Content>
|
||||
</Tooltip.Root></Tooltip.Provider
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
onclick={onDelete}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="hover:bg-destructive/10 hover:text-destructive size-8"
|
||||
aria-label={m.delete()}
|
||||
>
|
||||
<LucideTrash class="size-4" />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>{m.delete()}</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
</Item.Actions>
|
||||
</Item.Root>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import * as Item from '$lib/components/ui/item/index.js';
|
||||
import type { Icon as IconType } from '@lucide/svelte';
|
||||
|
||||
interface Props {
|
||||
icon: typeof IconType;
|
||||
name: string;
|
||||
@@ -11,10 +13,12 @@
|
||||
const SvelteComponent = $derived(icon);
|
||||
</script>
|
||||
|
||||
<div class="flex items-center">
|
||||
<div class="bg-muted mr-5 rounded-lg p-2"><SvelteComponent /></div>
|
||||
<div class="text-start">
|
||||
<h3 class="font-semibold">{name}</h3>
|
||||
<p class="text-muted-foreground text-sm">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Item.Root class="py-1.5 px-0">
|
||||
<Item.Media class="bg-muted !self-center rounded-lg p-2 !translate-y-0 h-full">
|
||||
<SvelteComponent class="size-5" />
|
||||
</Item.Media>
|
||||
<Item.Content class="text-start">
|
||||
<Item.Title class="font-semibold">{name}</Item.Title>
|
||||
<Item.Description>{description}</Item.Description>
|
||||
</Item.Content>
|
||||
</Item.Root>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import * as Item from '$lib/components/ui/item/index.js';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { LucideMail, LucideUser, LucideUsers } from '@lucide/svelte';
|
||||
import ScopeItem from './scope-item.svelte';
|
||||
@@ -6,7 +7,7 @@
|
||||
let { scope }: { scope: string } = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-3" data-testid="scopes">
|
||||
<Item.Group data-testid="scopes">
|
||||
{#if scope!.includes('email')}
|
||||
<ScopeItem icon={LucideMail} name={m.email()} description={m.view_your_email_address()} />
|
||||
{/if}
|
||||
@@ -24,4 +25,4 @@
|
||||
description={m.view_the_groups_you_are_a_member_of()}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</Item.Group>
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
value={$inputs.ttl.value.toString()}
|
||||
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)}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
|
||||
@@ -331,7 +331,7 @@
|
||||
value={items?.pagination.itemsPerPage.toString()}
|
||||
onValueChange={(v) => onPageSizeChange(Number(v))}
|
||||
>
|
||||
<Select.Trigger class="h-9 w-[80px]">
|
||||
<Select.Trigger class="w-20">
|
||||
{items?.pagination.itemsPerPage}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
size: {
|
||||
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',
|
||||
icon: 'h-10 w-10'
|
||||
}
|
||||
@@ -42,7 +42,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import LoaderCircle from '@lucide/svelte/icons/loader-circle';
|
||||
import { Spinner } from '$lib/components/ui/spinner';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let {
|
||||
@@ -97,7 +97,7 @@
|
||||
{...restProps}
|
||||
>
|
||||
{#if isLoading}
|
||||
<LoaderCircle class="size-4 animate-spin" />
|
||||
<Spinner />
|
||||
{/if}
|
||||
{@render children?.()}
|
||||
</a>
|
||||
@@ -112,7 +112,7 @@
|
||||
{...restProps}
|
||||
>
|
||||
{#if isLoading}
|
||||
<LoaderCircle class="size-4 animate-spin" />
|
||||
<Spinner />
|
||||
{/if}
|
||||
{@render children?.()}
|
||||
</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}
|
||||
data-slot="input"
|
||||
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]',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
className
|
||||
@@ -39,7 +39,7 @@
|
||||
bind:this={ref}
|
||||
data-slot="input"
|
||||
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]',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
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}
|
||||
/>
|
||||
Reference in New Issue
Block a user