refactor: Refactor Button and GroupedListItem components

Improve component typing with more specific HTML attribute types and
implement conditional rendering based on properties. Use Svelte snippets
to avoid code duplication and enhance component flexibility.
This commit is contained in:
Shibo Lyu 2025-04-17 02:28:08 +08:00
parent 9e79b29c69
commit c379933787
3 changed files with 67 additions and 47 deletions

9
.zed/settings.json Normal file
View file

@ -0,0 +1,9 @@
{
"lsp": {
"tailwindcss-language-server": {
"settings": {
"classFunctions": ["tw"]
}
}
}
}

View file

@ -1,45 +1,40 @@
<script lang="ts"> <script lang="ts">
import { tw } from '$lib/tw'; import { tw } from '$lib/tw';
import type { HTMLAttributes } from 'svelte/elements'; import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
type Props = { type Props = {
variant?: 'primary' | 'secondary'; variant?: 'primary' | 'secondary';
class?: string; class?: string;
children?: import('svelte').Snippet; children?: import('svelte').Snippet;
[key: string]: unknown; } & Omit<HTMLAnchorAttributes | HTMLButtonAttributes, 'class'>;
} & (
| ({
href: string;
} & HTMLAttributes<HTMLAnchorElement>)
| ({
href?: undefined;
} & HTMLAttributes<HTMLButtonElement>)
);
let { variant = 'secondary', class: className = '', href, children, ...rest }: Props = $props(); let { variant = 'secondary', class: externalClass, children, ...rest }: Props = $props();
const className = $derived(
tw(
'text-sf-secondary bg-sb-primary ring-ss-secondary inline-flex cursor-default items-center justify-center rounded-md px-2.5 py-1 shadow-xs ring-1',
'hover:ring-ss-primary font-normal transition-shadow duration-200 active:shadow-inner',
variant === 'primary' && [
'relative text-slate-50 ring-0 duration-200',
'before:absolute before:-inset-px before:rounded-[7px]',
'before:from-accent-400 before:to-accent-500 before:bg-linear-to-b before:from-40% before:ring-1 before:ring-black/10 before:ring-inset',
'dark:before:from-accent-500 dark:before:to-accent-600 before:transition-shadow active:before:shadow-inner'
],
externalClass
)
);
</script> </script>
<svelte:element {#snippet content()}
this={href ? 'a' : 'button'}
{href}
class={tw(
'text-sf-secondary bg-sb-primary ring-ss-secondary inline-flex cursor-default items-center justify-center rounded-md px-2.5 py-1 shadow-xs ring-1',
'hover:ring-ss-primary font-normal transition-shadow duration-200 active:shadow-inner',
variant === 'primary' && [
'relative text-slate-50 ring-0 duration-200',
'before:absolute before:-inset-px before:rounded-[7px]',
'before:from-accent-400 before:to-accent-500 before:bg-linear-to-b before:from-40% before:ring-1 before:ring-black/10 before:ring-inset',
'dark:before:from-accent-500 dark:before:to-accent-600 before:transition-shadow active:before:shadow-inner'
],
className
)}
role="button"
tabindex="0"
{...rest}
>
{#if variant === 'primary'} {#if variant === 'primary'}
<div class="z-10 drop-shadow-[0_-1px_0_--theme(--color-black/0.2)]">{@render children?.()}</div> <div class="z-10 drop-shadow-[0_-1px_0_--theme(--color-black/0.2)]">{@render children?.()}</div>
{:else} {:else}
{@render children?.()} {@render children?.()}
{/if} {/if}
</svelte:element> {/snippet}
{#if 'href' in rest}
<a class={className} {...rest}>{@render content()}</a>
{:else}
<button class={className} {...rest}>{@render content()}</button>
{/if}

View file

@ -1,29 +1,37 @@
<script lang="ts"> <script lang="ts">
import { tw } from '$lib/tw'; import { tw } from '$lib/tw';
import { Icon, type IconSource } from 'svelte-hero-icons'; import { Icon, type IconSource } from 'svelte-hero-icons';
import type { HTMLAnchorAttributes, HTMLAttributes, HTMLButtonAttributes } from 'svelte/elements';
interface Props { type Props = {
href?: string | undefined;
icon?: IconSource | undefined; icon?: IconSource | undefined;
selected?: boolean; selected?: boolean;
children?: import('svelte').Snippet; children?: import('svelte').Snippet;
onclick?: (e: MouseEvent) => void; class?: string;
} } & (
| ({ href: string } & Omit<HTMLAnchorAttributes, 'class' | 'href'>)
| ({ onclick: Required<HTMLButtonAttributes['onclick']> } & Omit<HTMLButtonAttributes, 'class'>)
| Omit<HTMLAttributes<HTMLDivElement>, 'onclick'>
);
let { href = undefined, icon = undefined, selected = false, children, onclick }: Props = $props(); let {
icon = undefined,
selected = false,
children,
class: externalClass,
...rest
}: Props = $props();
const className = $derived(
tw(
'text-sf-primary flex w-full cursor-default items-center gap-2 px-4 py-3 font-medium first:rounded-t-lg last:rounded-b-lg',
selected && 'bg-accent-500 dark:bg-accent-900 dark:text-sf-primary text-white shadow-inner',
externalClass
)
);
</script> </script>
<svelte:element {#snippet content()}
this={href ? 'a' : 'button'}
{href}
class={tw(
'text-sf-primary flex w-full cursor-default items-center gap-2 px-4 py-3 font-medium first:rounded-t-lg last:rounded-b-lg',
selected && 'bg-accent-500 dark:bg-accent-900 dark:text-sf-primary text-white shadow-inner'
)}
tabindex="0"
role="button"
{onclick}
>
{#if icon} {#if icon}
<Icon <Icon
src={icon} src={icon}
@ -34,4 +42,12 @@
<div class="min-w-0 truncate text-start"> <div class="min-w-0 truncate text-start">
{@render children?.()} {@render children?.()}
</div> </div>
</svelte:element> {/snippet}
{#if 'href' in rest}
<a class={className} {...rest}>{@render content()}</a>
{:else if 'onclick' in rest}
<button class={className} {...rest}>{@render content()}</button>
{:else}
<div class={className} {...rest}>{@render content()}</div>
{/if}