Compare commits

...

3 Commits

Author SHA1 Message Date
d32d88b56c feat: 实现Button组件/样式
- 相关属性对应DaisyUI的样式
2025-12-23 12:06:15 +08:00
71df72f843 fix: 去除无用css导入 2025-12-23 11:06:13 +08:00
6069fd29ae style: 调整Select动画样式
- 动画采用Svelte的原生transition语法
2025-12-23 10:59:34 +08:00
6 changed files with 150 additions and 49 deletions

View File

@ -0,0 +1,52 @@
<script lang="ts">
import { Button as BitsButton, type WithoutChildren } from 'bits-ui';
import type { Snippet } from 'svelte';
import { COLOR_MAP, VARIANT_MAP, SIZE_MAP } from './button.variants';
import type { Color, Variant, Size } from './button.variants';
type Props = WithoutChildren<BitsButton.RootProps> & {
color?: Color;
variant?: Variant;
size?: Size;
wide?: boolean;
block?: boolean;
square?: boolean;
circle?: boolean;
class?: string;
children?: Snippet;
};
let {
color,
variant,
size,
wide,
block,
square,
circle,
class: className = '',
children,
...restProps
}: Props = $props();
const classes = $derived(
[
'btn',
color && COLOR_MAP[color],
variant && VARIANT_MAP[variant],
size && SIZE_MAP[size],
wide && 'btn-wide',
block && 'btn-block',
square && 'btn-square',
circle && 'btn-circle',
className,
]
.filter(Boolean)
.join(' ')
);
</script>
<BitsButton.Root class={classes.trim()} {...restProps}>
{@render children?.()}
</BitsButton.Root>

View File

@ -0,0 +1,30 @@
export const COLOR_MAP = {
neutral: 'btn-neutral',
primary: 'btn-primary',
secondary: 'btn-secondary',
accent: 'btn-accent',
info: 'btn-info',
success: 'btn-success',
warning: 'btn-warning',
error: 'btn-error',
} as const;
export const VARIANT_MAP = {
outline: 'btn-outline',
dash: 'btn-dash',
soft: 'btn-soft',
ghost: 'btn-ghost',
link: 'btn-link',
} as const;
export const SIZE_MAP = {
xs: 'btn-xs',
sm: 'btn-sm',
md: 'btn-md',
lg: 'btn-lg',
xl: 'btn-xl',
} as const;
export type Color = keyof typeof COLOR_MAP;
export type Variant = keyof typeof VARIANT_MAP;
export type Size = keyof typeof SIZE_MAP;

View File

@ -0,0 +1 @@
export { default as Button } from './Button.svelte';

View File

@ -1,5 +1,6 @@
<script lang="ts">
import { Select as BitsSelect, type WithoutChildren } from 'bits-ui';
import { elasticOut, cubicOut } from 'svelte/easing';
type Props = WithoutChildren<BitsSelect.ContentProps> & {
items: { value: string; label: string; disabled?: boolean }[];
@ -7,48 +8,79 @@
};
let { items, class: className = '', ...restProps }: Props = $props();
function selectTransition(
node: HTMLElement,
params: {
delay?: number;
duration?: number;
easing?: (t: number) => number;
y?: number;
start?: number;
} = {}
) {
const {
delay = 0,
duration = 1000,
easing = cubicOut,
y = -6,
start = 0.95,
} = params;
const existingTransform = getComputedStyle(node).transform.replace(
'none',
''
);
return {
delay,
duration,
easing,
css: (t: number, u: number) => {
const translate = `translateY(${u * y}px)`;
const scale = `scale(${start + t * (1 - start)})`;
return `
opacity: ${t};
transform: ${existingTransform} ${translate} ${scale};
`;
},
};
}
</script>
<BitsSelect.Portal>
<BitsSelect.Content
forceMount
sideOffset={4}
class={`data=[state=closed]:fade-out-0 select-motion w-(--bits-select-anchor-width) min-w-(--bits-select-anchor-width) rounded-xl border-[1.5px] border-base-content/20 bg-base-100 select-none ${className}`}
class={`w-(--bits-select-anchor-width) min-w-(--bits-select-anchor-width) rounded-xl border-[1.5px] border-base-content/20 bg-base-100 select-none ${className}`}
{...restProps}
>
<BitsSelect.ScrollUpButton>up</BitsSelect.ScrollUpButton>
<BitsSelect.Viewport class="p-1">
{#each items as { value, label, disabled } (value)}
<BitsSelect.Item
{value}
{label}
{disabled}
class="outlined-hidden flex h-10 w-full items-center rounded-lg p-2 text-sm capitalize select-none data-disabled:opacity-50 data-highlighted:bg-gray-200"
>
{label}
</BitsSelect.Item>
{/each}
</BitsSelect.Viewport>
<BitsSelect.ScrollDownButton>down</BitsSelect.ScrollDownButton>
{#snippet child({ wrapperProps, props, open })}
{#if open}
<div {...wrapperProps}>
<div
{...props}
in:selectTransition={{ easing: elasticOut }}
out:selectTransition={{ duration: 200 }}
>
<BitsSelect.ScrollUpButton>up</BitsSelect.ScrollUpButton>
<BitsSelect.Viewport class="p-1">
{#each items as { value, label, disabled } (value)}
<BitsSelect.Item
{value}
{label}
{disabled}
class="outlined-hidden flex h-10 w-full items-center rounded-lg p-2 text-sm capitalize select-none data-disabled:opacity-50 data-highlighted:bg-gray-200"
>
{label}
</BitsSelect.Item>
{/each}
</BitsSelect.Viewport>
<BitsSelect.ScrollDownButton>down</BitsSelect.ScrollDownButton>
</div>
</div>
{/if}
{/snippet}
</BitsSelect.Content>
</BitsSelect.Portal>
<style lang="postcss">
@reference "tailwindcss";
:global(.select-content-animate) {
@apply transition-[opacity,transform] duration-200 ease-out will-change-transform;
}
:global(.select-content-animate[data-state='open']) {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
:global(.select-content-animate[data-state='closed']) {
opacity: 0;
transform: translateY(-10px);
pointer-events: none;
}
</style>

View File

@ -1,5 +1,4 @@
@import "tailwindcss";
@import "../styles/select-motion.css";
@plugin "@tailwindcss/forms";
@plugin "daisyui" {
logs: false;

View File

@ -1,13 +0,0 @@
@layer components {
.select-motion {
@apply transition-all duration-200 ease-out will-change-transform;
}
.select-motion[data-state="open"] {
@apply pointer-events-auto translate-y-0 scale-100 opacity-100;
}
.select-motion[data-state="closed"] {
@apply pointer-events-none -translate-y-2 scale-95 opacity-0;
}
}