feat: redesign routed ZarinPal merchant dashboard
This commit is contained in:
50
src/lib/components/ui/badge/badge.svelte
Normal file
50
src/lib/components/ui/badge/badge.svelte
Normal file
@@ -0,0 +1,50 @@
|
||||
<script lang="ts" module>
|
||||
import { type VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const badgeVariants = tv({
|
||||
base: 'h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap transition-colors focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none',
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
|
||||
secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
|
||||
destructive:
|
||||
'bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20',
|
||||
outline: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
|
||||
ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
});
|
||||
|
||||
export type BadgeVariant = VariantProps<typeof badgeVariants>['variant'];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
href,
|
||||
class: className,
|
||||
variant = 'default',
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: BadgeVariant;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={href ? 'a' : 'span'}
|
||||
bind:this={ref}
|
||||
data-slot="badge"
|
||||
{href}
|
||||
class={cn(badgeVariants({ variant }), className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</svelte:element>
|
||||
2
src/lib/components/ui/badge/index.ts
Normal file
2
src/lib/components/ui/badge/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as Badge } from './badge.svelte';
|
||||
export { badgeVariants, type BadgeVariant } from './badge.svelte';
|
||||
87
src/lib/components/ui/button/button.svelte
Normal file
87
src/lib/components/ui/button/button.svelte
Normal file
@@ -0,0 +1,87 @@
|
||||
<script lang="ts" module>
|
||||
import { type VariantProps, tv } from 'tailwind-variants';
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base: "rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
|
||||
outline:
|
||||
'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
|
||||
ghost:
|
||||
'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
|
||||
destructive:
|
||||
'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
||||
icon: 'size-8',
|
||||
'icon-xs':
|
||||
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||
'icon-sm': 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
|
||||
'icon-lg': 'size-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
});
|
||||
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
|
||||
|
||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
let {
|
||||
class: className,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
type = 'button',
|
||||
disabled,
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
href={disabled ? undefined : href}
|
||||
aria-disabled={disabled}
|
||||
role={disabled ? 'link' : undefined}
|
||||
tabindex={disabled ? -1 : undefined}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{type}
|
||||
{disabled}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
12
src/lib/components/ui/button/index.ts
Normal file
12
src/lib/components/ui/button/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import Root, { type ButtonProps, type ButtonSize, type ButtonVariant, buttonVariants } from './button.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
type ButtonProps as Props,
|
||||
//
|
||||
Root as Button,
|
||||
buttonVariants,
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
};
|
||||
20
src/lib/components/ui/card/card-action.svelte
Normal file
20
src/lib/components/ui/card/card-action.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.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="card-action"
|
||||
class={cn('cn-card-action col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
15
src/lib/components/ui/card/card-content.svelte
Normal file
15
src/lib/components/ui/card/card-content.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.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="card-content" class={cn('px-(--card-spacing)', className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/card/card-description.svelte
Normal file
20
src/lib/components/ui/card/card-description.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.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="card-description"
|
||||
class={cn('text-muted-foreground text-sm', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</p>
|
||||
20
src/lib/components/ui/card/card-footer.svelte
Normal file
20
src/lib/components/ui/card/card-footer.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.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="card-footer"
|
||||
class={cn('bg-muted/50 flex items-center rounded-b-xl border-t p-(--card-spacing)', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
23
src/lib/components/ui/card/card-header.svelte
Normal file
23
src/lib/components/ui/card/card-header.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.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="card-header"
|
||||
class={cn(
|
||||
'group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/card/card-title.svelte
Normal file
20
src/lib/components/ui/card/card-title.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.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="card-title"
|
||||
class={cn('text-base leading-snug font-medium group-data-[size=sm]/card:text-sm', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
25
src/lib/components/ui/card/card.svelte
Normal file
25
src/lib/components/ui/card/card.svelte
Normal file
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
size = 'default',
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { size?: 'default' | 'sm' } = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card"
|
||||
data-size={size}
|
||||
class={cn(
|
||||
'bg-card text-card-foreground ring-foreground/10 group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl py-(--card-spacing) text-sm ring-1 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
25
src/lib/components/ui/card/index.ts
Normal file
25
src/lib/components/ui/card/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import Action from './card-action.svelte';
|
||||
import Content from './card-content.svelte';
|
||||
import Description from './card-description.svelte';
|
||||
import Footer from './card-footer.svelte';
|
||||
import Header from './card-header.svelte';
|
||||
import Title from './card-title.svelte';
|
||||
import Root from './card.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Description,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
Action,
|
||||
//
|
||||
Root as Card,
|
||||
Content as CardContent,
|
||||
Description as CardDescription,
|
||||
Footer as CardFooter,
|
||||
Header as CardHeader,
|
||||
Title as CardTitle,
|
||||
Action as CardAction,
|
||||
};
|
||||
80
src/lib/components/ui/chart/chart-container.svelte
Normal file
80
src/lib/components/ui/chart/chart-container.svelte
Normal file
@@ -0,0 +1,80 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import ChartStyle from './chart-style.svelte';
|
||||
import { setChartContext, type ChartConfig } from './chart-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
id = uid,
|
||||
class: className,
|
||||
children,
|
||||
config,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
|
||||
config: ChartConfig;
|
||||
} = $props();
|
||||
|
||||
const chartId = $derived(`chart-${id || uid.replace(/:/g, '')}`);
|
||||
|
||||
setChartContext({
|
||||
get config() {
|
||||
return config;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-chart={chartId}
|
||||
data-slot="chart"
|
||||
class={cn(
|
||||
'flex aspect-video justify-center overflow-visible text-xs',
|
||||
// Overrides
|
||||
//
|
||||
// Stroke around dots/marks when hovering
|
||||
'[&_.lc-highlight-point]:stroke-transparent',
|
||||
// override the default stroke color of lines
|
||||
'[&_.lc-line]:stroke-border/50',
|
||||
|
||||
// by default, layerchart shows a line intersecting the point when hovering, this hides that
|
||||
'[&_.lc-highlight-line]:stroke-0',
|
||||
|
||||
// by default, when you hover a point on a stacked series chart, it will drop the opacity
|
||||
// of the other series, this overrides that
|
||||
'[&_.lc-area-path]:opacity-100 [&_.lc-highlight-line]:opacity-100 [&_.lc-highlight-point]:opacity-100 [&_.lc-spline-path]:opacity-100 [&_.lc-text]:text-xs [&_.lc-text-svg]:overflow-visible',
|
||||
|
||||
// We don't want the little tick lines between the axis labels and the chart, so we remove
|
||||
// the stroke. The alternative is to manually disable `tickMarks` on the x/y axis of every
|
||||
// chart.
|
||||
'[&_.lc-axis-tick]:stroke-0',
|
||||
|
||||
// We don't want to display the rule on the x/y axis, as there is already going to be
|
||||
// a grid line there and rule ends up overlapping the marks because it is rendered after
|
||||
// the marks
|
||||
'[&_.lc-rule-x-line:not(.lc-grid-x-rule)]:stroke-0 [&_.lc-rule-y-line:not(.lc-grid-y-rule)]:stroke-0',
|
||||
'[&_.lc-grid-x-radial-circle]:stroke-border [&_.lc-grid-x-radial-line]:stroke-border',
|
||||
'[&_.lc-grid-y-radial-circle]:stroke-border [&_.lc-grid-y-radial-line]:stroke-border',
|
||||
|
||||
// Legend adjustments
|
||||
'[&_.lc-legend-swatch-button]:items-center [&_.lc-legend-swatch-button]:gap-1.5',
|
||||
'[&_.lc-legend-swatch-group]:items-center [&_.lc-legend-swatch-group]:gap-4',
|
||||
'[&_.lc-legend-swatch]:size-2.5 [&_.lc-legend-swatch]:rounded-[2px]',
|
||||
|
||||
// Labels
|
||||
'[&_.lc-labels-text:not([fill])]:fill-foreground [&_text]:stroke-transparent',
|
||||
|
||||
// Tick labels on th x/y axes
|
||||
'[&_.lc-axis-tick-label]:fill-muted-foreground [&_.lc-axis-tick-label]:font-normal',
|
||||
'[&_.lc-tooltip-rects-g]:fill-transparent',
|
||||
'[&_.lc-layout-svg-g]:fill-transparent',
|
||||
'[&_.lc-root-container]:w-full',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChartStyle id={chartId} {config} />
|
||||
{@render children?.()}
|
||||
</div>
|
||||
37
src/lib/components/ui/chart/chart-style.svelte
Normal file
37
src/lib/components/ui/chart/chart-style.svelte
Normal file
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { THEMES, type ChartConfig } from './chart-utils.js';
|
||||
|
||||
let { id, config }: { id: string; config: ChartConfig } = $props();
|
||||
|
||||
const colorConfig = $derived(
|
||||
config ? Object.entries(config).filter(([, config]) => config.theme || config.color) : null,
|
||||
);
|
||||
|
||||
const themeContents = $derived.by(() => {
|
||||
if (!colorConfig || !colorConfig.length) return;
|
||||
|
||||
const themeContents = [];
|
||||
for (const [_theme, prefix] of Object.entries(THEMES)) {
|
||||
let content = `${prefix} [data-chart=${id}] {\n`;
|
||||
const color = colorConfig.map(([key, itemConfig]) => {
|
||||
const theme = _theme as keyof typeof itemConfig.theme;
|
||||
const color = itemConfig.theme?.[theme] || itemConfig.color;
|
||||
return color ? `\t--color-${key}: ${color};` : null;
|
||||
});
|
||||
|
||||
content += color.join('\n') + '\n}';
|
||||
|
||||
themeContents.push(content);
|
||||
}
|
||||
|
||||
return themeContents.join('\n');
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if themeContents}
|
||||
{#key id}
|
||||
<svelte:element this={'style'}>
|
||||
{themeContents}
|
||||
</svelte:element>
|
||||
{/key}
|
||||
{/if}
|
||||
175
src/lib/components/ui/chart/chart-tooltip.svelte
Normal file
175
src/lib/components/ui/chart/chart-tooltip.svelte
Normal file
@@ -0,0 +1,175 @@
|
||||
<script lang="ts">
|
||||
import { getChartContext, Tooltip as TooltipPrimitive } from 'layerchart';
|
||||
import { cn, type WithElementRef, type WithoutChildren } from '$lib/utils.js';
|
||||
import { getPayloadConfigFromPayload, useChart, type TooltipPayload } from './chart-utils.js';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function defaultFormatter(value: any, _payload: TooltipPayload[]) {
|
||||
return `${value}`;
|
||||
}
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
hideLabel = false,
|
||||
indicator = 'dot',
|
||||
hideIndicator = false,
|
||||
labelKey,
|
||||
label,
|
||||
labelFormatter = defaultFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
nameKey,
|
||||
color,
|
||||
...restProps
|
||||
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> & {
|
||||
hideLabel?: boolean;
|
||||
label?: string;
|
||||
indicator?: 'line' | 'dot' | 'dashed';
|
||||
nameKey?: string;
|
||||
labelKey?: string;
|
||||
hideIndicator?: boolean;
|
||||
labelClassName?: string;
|
||||
labelFormatter?: // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
((value: any, payload: TooltipPayload[]) => string | number | Snippet) | null;
|
||||
formatter?: Snippet<
|
||||
[
|
||||
{
|
||||
value: unknown;
|
||||
name: string;
|
||||
item: TooltipPayload;
|
||||
index: number;
|
||||
payload: TooltipPayload[];
|
||||
},
|
||||
]
|
||||
>;
|
||||
} = $props();
|
||||
|
||||
const chart = useChart();
|
||||
const chartCtx = getChartContext();
|
||||
|
||||
// Filter to series with defined values (important for item-based charts like Pie/Arc
|
||||
// where only the hovered item has a value)
|
||||
const visibleSeries = $derived(
|
||||
chartCtx.tooltip.series.filter((s: TooltipPayload) => s.value !== undefined),
|
||||
);
|
||||
|
||||
const formattedLabel = $derived.by(() => {
|
||||
if (hideLabel || !visibleSeries?.length) return null;
|
||||
|
||||
const [item] = visibleSeries;
|
||||
const tooltipData = chartCtx.tooltip.data;
|
||||
|
||||
// Get the x-axis label value from the raw tooltip data (e.g. a Date or month string)
|
||||
const dataLabel = tooltipData != null ? chartCtx.x(tooltipData) : undefined;
|
||||
|
||||
const key = labelKey ?? item?.label ?? item?.key ?? 'value';
|
||||
const itemConfig = getPayloadConfigFromPayload(
|
||||
chart.config,
|
||||
item,
|
||||
key,
|
||||
tooltipData as Record<string, unknown> | null,
|
||||
);
|
||||
|
||||
let value: unknown;
|
||||
if (!labelKey && typeof label === 'string') {
|
||||
value = chart.config[label as keyof typeof chart.config]?.label ?? label;
|
||||
} else if (labelKey) {
|
||||
value = itemConfig?.label ?? dataLabel;
|
||||
} else {
|
||||
value = dataLabel;
|
||||
}
|
||||
|
||||
if (value === undefined) return null;
|
||||
if (!labelFormatter) return value;
|
||||
return labelFormatter(value, visibleSeries);
|
||||
});
|
||||
|
||||
const nestLabel = $derived(visibleSeries.length === 1 && indicator !== 'dot');
|
||||
</script>
|
||||
|
||||
{#snippet TooltipLabel()}
|
||||
{#if formattedLabel}
|
||||
<div class={cn('font-medium', labelClassName)}>
|
||||
{#if typeof formattedLabel === 'function'}
|
||||
{@render formattedLabel()}
|
||||
{:else}
|
||||
{formattedLabel}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<TooltipPrimitive.Root variant="none">
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn(
|
||||
'border-border/50 bg-background grid min-w-[9rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#if !nestLabel}
|
||||
{@render TooltipLabel()}
|
||||
{/if}
|
||||
<div class="grid gap-1.5">
|
||||
{#each visibleSeries as item, i (item.key + i)}
|
||||
{@const key = `${nameKey || item.key || item.label || 'value'}`}
|
||||
{@const itemConfig = getPayloadConfigFromPayload(chart.config, item, key, chartCtx.tooltip.data)}
|
||||
{@const indicatorColor = color || item.config?.color || item.color}
|
||||
<div
|
||||
class={cn(
|
||||
'[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5',
|
||||
indicator === 'dot' && 'items-center',
|
||||
)}
|
||||
>
|
||||
{#if formatter && item.value !== undefined && item.label}
|
||||
{@render formatter({
|
||||
value: item.value,
|
||||
name: item.label,
|
||||
item,
|
||||
index: i,
|
||||
payload: visibleSeries,
|
||||
})}
|
||||
{:else}
|
||||
{#if itemConfig?.icon}
|
||||
<itemConfig.icon />
|
||||
{:else if !hideIndicator}
|
||||
<div
|
||||
style="--color-bg: {indicatorColor}; --color-border: {indicatorColor};"
|
||||
class={cn('shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)', {
|
||||
'size-2.5': indicator === 'dot',
|
||||
'h-full w-1': indicator === 'line',
|
||||
'w-0 border-[1.5px] border-dashed bg-transparent': indicator === 'dashed',
|
||||
'my-0.5': nestLabel && indicator === 'dashed',
|
||||
})}
|
||||
></div>
|
||||
{/if}
|
||||
<div
|
||||
class={cn(
|
||||
'flex flex-1 shrink-0 justify-between leading-none',
|
||||
nestLabel ? 'items-end' : 'items-center',
|
||||
)}
|
||||
>
|
||||
<div class="grid gap-1.5">
|
||||
{#if nestLabel}
|
||||
{@render TooltipLabel()}
|
||||
{/if}
|
||||
<span class="text-muted-foreground">
|
||||
{itemConfig?.label || item.label}
|
||||
</span>
|
||||
</div>
|
||||
{#if item.value !== undefined}
|
||||
<span class="text-foreground font-mono font-medium tabular-nums">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</TooltipPrimitive.Root>
|
||||
65
src/lib/components/ui/chart/chart-utils.ts
Normal file
65
src/lib/components/ui/chart/chart-utils.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { getContext, setContext, type Component, type Snippet } from 'svelte';
|
||||
import type { Tooltip } from 'layerchart';
|
||||
|
||||
export const THEMES = { light: '', dark: '.dark' } as const;
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: string;
|
||||
icon?: Component;
|
||||
} & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> });
|
||||
};
|
||||
|
||||
export type ExtractSnippetParams<T> = T extends Snippet<[infer P]> ? P : never;
|
||||
|
||||
export type TooltipPayload = Tooltip.TooltipSeries;
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
export function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: TooltipPayload,
|
||||
key: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data?: Record<string, any> | null,
|
||||
) {
|
||||
if (typeof payload !== 'object' || payload === null) return undefined;
|
||||
|
||||
const payloadConfig =
|
||||
'config' in payload && typeof payload.config === 'object' && payload.config !== null
|
||||
? payload.config
|
||||
: undefined;
|
||||
|
||||
let configLabelKey: string = key;
|
||||
|
||||
if (payload.key === key) {
|
||||
configLabelKey = payload.key;
|
||||
} else if (payload.label === key) {
|
||||
configLabelKey = payload.label;
|
||||
} else if (key in payload && typeof payload[key as keyof typeof payload] === 'string') {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||
} else if (
|
||||
payloadConfig !== undefined &&
|
||||
key in payloadConfig &&
|
||||
typeof payloadConfig[key as keyof typeof payloadConfig] === 'string'
|
||||
) {
|
||||
configLabelKey = payloadConfig[key as keyof typeof payloadConfig] as string;
|
||||
} else if (data != null && key in data && typeof data[key] === 'string') {
|
||||
configLabelKey = data[key] as string;
|
||||
}
|
||||
|
||||
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
|
||||
}
|
||||
|
||||
type ChartContextValue = {
|
||||
config: ChartConfig;
|
||||
};
|
||||
|
||||
const chartContextKey = Symbol('chart-context');
|
||||
|
||||
export function setChartContext(value: ChartContextValue) {
|
||||
return setContext(chartContextKey, value);
|
||||
}
|
||||
|
||||
export function useChart() {
|
||||
return getContext<ChartContextValue>(chartContextKey);
|
||||
}
|
||||
6
src/lib/components/ui/chart/index.ts
Normal file
6
src/lib/components/ui/chart/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import ChartContainer from './chart-container.svelte';
|
||||
import ChartTooltip from './chart-tooltip.svelte';
|
||||
|
||||
export { getPayloadConfigFromPayload, type ChartConfig } from './chart-utils.js';
|
||||
|
||||
export { ChartContainer, ChartTooltip, ChartContainer as Container, ChartTooltip as Tooltip };
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Collapsible as CollapsiblePrimitive } from 'bits-ui';
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: CollapsiblePrimitive.ContentProps = $props();
|
||||
</script>
|
||||
|
||||
<CollapsiblePrimitive.Content bind:ref data-slot="collapsible-content" {...restProps} />
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Collapsible as CollapsiblePrimitive } from 'bits-ui';
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: CollapsiblePrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<CollapsiblePrimitive.Trigger bind:ref data-slot="collapsible-trigger" {...restProps} />
|
||||
11
src/lib/components/ui/collapsible/collapsible.svelte
Normal file
11
src/lib/components/ui/collapsible/collapsible.svelte
Normal file
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Collapsible as CollapsiblePrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
open = $bindable(false),
|
||||
...restProps
|
||||
}: CollapsiblePrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<CollapsiblePrimitive.Root bind:ref bind:open data-slot="collapsible" {...restProps} />
|
||||
13
src/lib/components/ui/collapsible/index.ts
Normal file
13
src/lib/components/ui/collapsible/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import Content from './collapsible-content.svelte';
|
||||
import Trigger from './collapsible-trigger.svelte';
|
||||
import Root from './collapsible.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Trigger,
|
||||
//
|
||||
Root as Collapsible,
|
||||
Content as CollapsibleContent,
|
||||
Trigger as CollapsibleTrigger,
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable([]),
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.CheckboxGroupProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.CheckboxGroup
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="dropdown-menu-checkbox-group"
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import MinusIcon from '@lucide/svelte/icons/minus';
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
checked = $bindable(false),
|
||||
indeterminate = $bindable(false),
|
||||
class: className,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
|
||||
children?: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
bind:ref
|
||||
bind:checked
|
||||
bind:indeterminate
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-inset:pl-7 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked, indeterminate })}
|
||||
<span
|
||||
class="pointer-events-none absolute right-2 flex items-center justify-center"
|
||||
data-slot="dropdown-menu-checkbox-item-indicator"
|
||||
>
|
||||
{#if indeterminate}
|
||||
<MinusIcon />
|
||||
{:else if checked}
|
||||
<CheckIcon />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.()}
|
||||
{/snippet}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import DropdownMenuPortal from './dropdown-menu-portal.svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
sideOffset = 4,
|
||||
align = 'start',
|
||||
portalProps,
|
||||
class: className,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.ContentProps & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DropdownMenuPortal>>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPortal {...portalProps}>
|
||||
<DropdownMenuPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-content"
|
||||
{sideOffset}
|
||||
{align}
|
||||
class={cn(
|
||||
'bg-popover text-popover-foreground ring-foreground/10 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 z-50 w-(--bits-dropdown-menu-anchor-width) min-w-32 overflow-x-hidden overflow-y-auto rounded-lg p-1 shadow-md ring-1 duration-100 outline-none data-closed:overflow-hidden',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</DropdownMenuPortal>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
...restProps
|
||||
}: ComponentProps<typeof DropdownMenuPrimitive.GroupHeading> & {
|
||||
inset?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.GroupHeading
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-group-heading"
|
||||
data-inset={inset}
|
||||
class={cn('px-2 py-1.5 text-sm font-semibold data-[inset]:ps-8', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.GroupProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
variant = 'default',
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.ItemProps & {
|
||||
inset?: boolean;
|
||||
variant?: 'default' | 'destructive';
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Item
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:*:[svg]:text-destructive group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
inset?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
class={cn(
|
||||
'text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7 data-[inset]:pl-8',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
|
||||
let { ...restProps }: DropdownMenuPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Portal {...restProps} />
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.RadioGroupProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.RadioGroup bind:ref bind:value data-slot="dropdown-menu-radio-group" {...restProps} />
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
import { cn, type WithoutChild } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-inset:pl-7 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked })}
|
||||
<span
|
||||
class="pointer-events-none absolute right-2 flex items-center justify-center"
|
||||
data-slot="dropdown-menu-radio-item-indicator"
|
||||
>
|
||||
{#if checked}
|
||||
<CheckIcon />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.({ checked })}
|
||||
{/snippet}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.SeparatorProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Separator
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-separator"
|
||||
class={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
class={cn(
|
||||
'text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</span>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.SubContentProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
class={cn(
|
||||
'bg-popover text-popover-foreground ring-foreground/10 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 w-auto min-w-[96px] rounded-lg p-1 shadow-lg ring-1 duration-100',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
children,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.SubTriggerProps & {
|
||||
inset?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-inset:pl-7 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronRightIcon class="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
|
||||
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.SubProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Sub bind:open {...restProps} />
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} />
|
||||
7
src/lib/components/ui/dropdown-menu/dropdown-menu.svelte
Normal file
7
src/lib/components/ui/dropdown-menu/dropdown-menu.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
|
||||
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Root bind:open {...restProps} />
|
||||
54
src/lib/components/ui/dropdown-menu/index.ts
Normal file
54
src/lib/components/ui/dropdown-menu/index.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import CheckboxGroup from './dropdown-menu-checkbox-group.svelte';
|
||||
import CheckboxItem from './dropdown-menu-checkbox-item.svelte';
|
||||
import Content from './dropdown-menu-content.svelte';
|
||||
import GroupHeading from './dropdown-menu-group-heading.svelte';
|
||||
import Group from './dropdown-menu-group.svelte';
|
||||
import Item from './dropdown-menu-item.svelte';
|
||||
import Label from './dropdown-menu-label.svelte';
|
||||
import Portal from './dropdown-menu-portal.svelte';
|
||||
import RadioGroup from './dropdown-menu-radio-group.svelte';
|
||||
import RadioItem from './dropdown-menu-radio-item.svelte';
|
||||
import Separator from './dropdown-menu-separator.svelte';
|
||||
import Shortcut from './dropdown-menu-shortcut.svelte';
|
||||
import SubContent from './dropdown-menu-sub-content.svelte';
|
||||
import SubTrigger from './dropdown-menu-sub-trigger.svelte';
|
||||
import Sub from './dropdown-menu-sub.svelte';
|
||||
import Trigger from './dropdown-menu-trigger.svelte';
|
||||
import Root from './dropdown-menu.svelte';
|
||||
|
||||
export {
|
||||
CheckboxGroup,
|
||||
CheckboxItem,
|
||||
Content,
|
||||
Portal,
|
||||
Root as DropdownMenu,
|
||||
CheckboxGroup as DropdownMenuCheckboxGroup,
|
||||
CheckboxItem as DropdownMenuCheckboxItem,
|
||||
Content as DropdownMenuContent,
|
||||
Portal as DropdownMenuPortal,
|
||||
Group as DropdownMenuGroup,
|
||||
Item as DropdownMenuItem,
|
||||
Label as DropdownMenuLabel,
|
||||
RadioGroup as DropdownMenuRadioGroup,
|
||||
RadioItem as DropdownMenuRadioItem,
|
||||
Separator as DropdownMenuSeparator,
|
||||
Shortcut as DropdownMenuShortcut,
|
||||
Sub as DropdownMenuSub,
|
||||
SubContent as DropdownMenuSubContent,
|
||||
SubTrigger as DropdownMenuSubTrigger,
|
||||
Trigger as DropdownMenuTrigger,
|
||||
GroupHeading as DropdownMenuGroupHeading,
|
||||
Group,
|
||||
GroupHeading,
|
||||
Item,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioItem,
|
||||
Root,
|
||||
Separator,
|
||||
Shortcut,
|
||||
Sub,
|
||||
SubContent,
|
||||
SubTrigger,
|
||||
Trigger,
|
||||
};
|
||||
20
src/lib/components/ui/empty/empty-content.svelte
Normal file
20
src/lib/components/ui/empty/empty-content.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.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="empty-content"
|
||||
class={cn('flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-sm text-balance', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
23
src/lib/components/ui/empty/empty-description.svelte
Normal file
23
src/lib/components/ui/empty/empty-description.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.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="empty-description"
|
||||
class={cn(
|
||||
'text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/empty/empty-header.svelte
Normal file
20
src/lib/components/ui/empty/empty-header.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.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="empty-header"
|
||||
class={cn('flex max-w-sm flex-col items-center gap-2', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
41
src/lib/components/ui/empty/empty-media.svelte
Normal file
41
src/lib/components/ui/empty/empty-media.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<script lang="ts" module>
|
||||
import { tv, type VariantProps } from 'tailwind-variants';
|
||||
|
||||
export const emptyMediaVariants = tv({
|
||||
base: 'mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0',
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-transparent',
|
||||
icon: "flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-4",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
});
|
||||
|
||||
export type EmptyMediaVariant = VariantProps<typeof emptyMediaVariants>['variant'];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
variant = 'default',
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { variant?: EmptyMediaVariant } = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="empty-icon"
|
||||
data-variant={variant}
|
||||
class={cn(emptyMediaVariants({ variant }), className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/empty/empty-title.svelte
Normal file
20
src/lib/components/ui/empty/empty-title.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.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="empty-title"
|
||||
class={cn('text-sm font-medium tracking-tight', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
23
src/lib/components/ui/empty/empty.svelte
Normal file
23
src/lib/components/ui/empty/empty.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.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="empty"
|
||||
class={cn(
|
||||
'flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-xl border-dashed p-6 text-center text-balance',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
22
src/lib/components/ui/empty/index.ts
Normal file
22
src/lib/components/ui/empty/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import Content from './empty-content.svelte';
|
||||
import Description from './empty-description.svelte';
|
||||
import Header from './empty-header.svelte';
|
||||
import Media from './empty-media.svelte';
|
||||
import Title from './empty-title.svelte';
|
||||
import Root from './empty.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Header,
|
||||
Media,
|
||||
Title,
|
||||
Description,
|
||||
Content,
|
||||
//
|
||||
Root as Empty,
|
||||
Header as EmptyHeader,
|
||||
Media as EmptyMedia,
|
||||
Title as EmptyTitle,
|
||||
Description as EmptyDescription,
|
||||
Content as EmptyContent,
|
||||
};
|
||||
7
src/lib/components/ui/input/index.ts
Normal file
7
src/lib/components/ui/input/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from './input.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
};
|
||||
48
src/lib/components/ui/input/input.svelte
Normal file
48
src/lib/components/ui/input/input.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from 'svelte/elements';
|
||||
|
||||
type InputType = Exclude<HTMLInputTypeAttribute, 'file'>;
|
||||
|
||||
type Props = WithElementRef<
|
||||
Omit<HTMLInputAttributes, 'type'> &
|
||||
({ type: 'file'; files?: FileList } | { type?: InputType; files?: undefined })
|
||||
>;
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
type,
|
||||
files = $bindable(),
|
||||
class: className,
|
||||
'data-slot': dataSlot = 'input',
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if type === 'file'}
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot={dataSlot}
|
||||
class={cn(
|
||||
'border-input focus-visible:border-ring focus-visible:ring-ring/50 disabled:bg-input/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 file:text-foreground placeholder:text-muted-foreground h-8 w-full min-w-0 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-3 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 md:text-sm',
|
||||
className,
|
||||
)}
|
||||
type="file"
|
||||
bind:files
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot={dataSlot}
|
||||
class={cn(
|
||||
'border-input focus-visible:border-ring focus-visible:ring-ring/50 disabled:bg-input/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 file:text-foreground placeholder:text-muted-foreground h-8 w-full min-w-0 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-3 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 md:text-sm',
|
||||
className,
|
||||
)}
|
||||
{type}
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{/if}
|
||||
31
src/lib/components/ui/pagination/index.ts
Normal file
31
src/lib/components/ui/pagination/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import Content from './pagination-content.svelte';
|
||||
import Ellipsis from './pagination-ellipsis.svelte';
|
||||
import Item from './pagination-item.svelte';
|
||||
import Link from './pagination-link.svelte';
|
||||
import NextButton from './pagination-next-button.svelte';
|
||||
import Next from './pagination-next.svelte';
|
||||
import PrevButton from './pagination-prev-button.svelte';
|
||||
import Previous from './pagination-previous.svelte';
|
||||
import Root from './pagination.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Item,
|
||||
Link,
|
||||
PrevButton, // old
|
||||
NextButton, // old
|
||||
Ellipsis,
|
||||
Previous,
|
||||
Next,
|
||||
//
|
||||
Root as Pagination,
|
||||
Content as PaginationContent,
|
||||
Item as PaginationItem,
|
||||
Link as PaginationLink,
|
||||
PrevButton as PaginationPrevButton, // old
|
||||
NextButton as PaginationNextButton, // old
|
||||
Ellipsis as PaginationEllipsis,
|
||||
Previous as PaginationPrevious,
|
||||
Next as PaginationNext,
|
||||
};
|
||||
20
src/lib/components/ui/pagination/pagination-content.svelte
Normal file
20
src/lib/components/ui/pagination/pagination-content.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLUListElement>> = $props();
|
||||
</script>
|
||||
|
||||
<ul
|
||||
bind:this={ref}
|
||||
data-slot="pagination-content"
|
||||
class={cn('flex items-center gap-0.5', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</ul>
|
||||
25
src/lib/components/ui/pagination/pagination-ellipsis.svelte
Normal file
25
src/lib/components/ui/pagination/pagination-ellipsis.svelte
Normal file
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import MoreHorizontalIcon from '@lucide/svelte/icons/more-horizontal';
|
||||
import { cn, type WithElementRef, type WithoutChildren } from '$lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLSpanElement>>> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
aria-hidden="true"
|
||||
data-slot="pagination-ellipsis"
|
||||
class={cn(
|
||||
"flex size-8 items-center items-center justify-center justify-center [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<MoreHorizontalIcon />
|
||||
<span class="sr-only">More pages</span>
|
||||
</span>
|
||||
10
src/lib/components/ui/pagination/pagination-item.svelte
Normal file
10
src/lib/components/ui/pagination/pagination-item.svelte
Normal file
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from '$lib/utils.js';
|
||||
import type { HTMLLiAttributes } from 'svelte/elements';
|
||||
|
||||
let { ref = $bindable(null), children, ...restProps }: WithElementRef<HTMLLiAttributes> = $props();
|
||||
</script>
|
||||
|
||||
<li bind:this={ref} data-slot="pagination-item" {...restProps}>
|
||||
{@render children?.()}
|
||||
</li>
|
||||
42
src/lib/components/ui/pagination/pagination-link.svelte
Normal file
42
src/lib/components/ui/pagination/pagination-link.svelte
Normal file
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { Pagination as PaginationPrimitive } from 'bits-ui';
|
||||
import { buttonVariants, type ButtonSize } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
size = 'icon',
|
||||
isActive,
|
||||
page,
|
||||
children,
|
||||
...restProps
|
||||
}: PaginationPrimitive.PageProps & {
|
||||
size?: ButtonSize;
|
||||
isActive: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#snippet Fallback()}
|
||||
{page.value}
|
||||
{/snippet}
|
||||
|
||||
<PaginationPrimitive.Page
|
||||
bind:ref
|
||||
{page}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
data-slot="pagination-link"
|
||||
data-active={isActive}
|
||||
data-size={size}
|
||||
class={cn(
|
||||
buttonVariants({ size, variant: isActive ? 'outline' : 'ghost' }),
|
||||
'cn-pagination-link',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#if children}
|
||||
{@render children?.()}
|
||||
{:else}
|
||||
{@render Fallback()}
|
||||
{/if}
|
||||
</PaginationPrimitive.Page>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { Pagination as PaginationPrimitive } from 'bits-ui';
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { buttonVariants } from '../button/index.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: PaginationPrimitive.NextButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#snippet Fallback()}
|
||||
<span>Next</span>
|
||||
<ChevronRightIcon class={cn('size-4', className)} />
|
||||
{/snippet}
|
||||
|
||||
<PaginationPrimitive.NextButton
|
||||
bind:ref
|
||||
aria-label="Go to next page"
|
||||
class={cn(buttonVariants({ variant: 'ghost' }), 'pr-1.5!', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{#if children}
|
||||
{@render children?.()}
|
||||
{:else}
|
||||
{@render Fallback()}
|
||||
{/if}
|
||||
</PaginationPrimitive.NextButton>
|
||||
22
src/lib/components/ui/pagination/pagination-next.svelte
Normal file
22
src/lib/components/ui/pagination/pagination-next.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { Pagination as PaginationPrimitive } from 'bits-ui';
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||
import { buttonVariants } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: PaginationPrimitive.NextButtonProps = $props();
|
||||
</script>
|
||||
|
||||
<PaginationPrimitive.NextButton
|
||||
bind:ref
|
||||
aria-label="Go to next page"
|
||||
class={cn(buttonVariants({ variant: 'ghost', size: 'default' }), 'pr-1.5!', className)}
|
||||
{...restProps}
|
||||
>
|
||||
<span class="cn-pagination-next-text hidden sm:block">Next</span>
|
||||
<ChevronRightIcon data-icon="inline-end" />
|
||||
</PaginationPrimitive.NextButton>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { Pagination as PaginationPrimitive } from 'bits-ui';
|
||||
import ChevronLeftIcon from '@lucide/svelte/icons/chevron-left';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { buttonVariants } from '../button/index.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: PaginationPrimitive.PrevButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#snippet Fallback()}
|
||||
<ChevronLeftIcon class={cn('size-4', className)} />
|
||||
<span>Previous</span>
|
||||
{/snippet}
|
||||
|
||||
<PaginationPrimitive.PrevButton
|
||||
bind:ref
|
||||
aria-label="Go to previous page"
|
||||
class={cn(buttonVariants({ variant: 'ghost' }), 'pl-1.5!', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{#if children}
|
||||
{@render children?.()}
|
||||
{:else}
|
||||
{@render Fallback()}
|
||||
{/if}
|
||||
</PaginationPrimitive.PrevButton>
|
||||
22
src/lib/components/ui/pagination/pagination-previous.svelte
Normal file
22
src/lib/components/ui/pagination/pagination-previous.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { Pagination as PaginationPrimitive } from 'bits-ui';
|
||||
import ChevronLeftIcon from '@lucide/svelte/icons/chevron-left';
|
||||
import { buttonVariants } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: PaginationPrimitive.PrevButtonProps = $props();
|
||||
</script>
|
||||
|
||||
<PaginationPrimitive.PrevButton
|
||||
bind:ref
|
||||
aria-label="Go to previous page"
|
||||
class={cn(buttonVariants({ variant: 'ghost', size: 'default' }), 'pl-1.5!', className)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronLeftIcon data-icon="inline-start" />
|
||||
<span class="cn-pagination-previous-text hidden sm:block">Previous</span>
|
||||
</PaginationPrimitive.PrevButton>
|
||||
27
src/lib/components/ui/pagination/pagination.svelte
Normal file
27
src/lib/components/ui/pagination/pagination.svelte
Normal file
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { Pagination as PaginationPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
count = 0,
|
||||
perPage = 10,
|
||||
page = $bindable(1),
|
||||
siblingCount = 1,
|
||||
...restProps
|
||||
}: PaginationPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<PaginationPrimitive.Root
|
||||
bind:ref
|
||||
bind:page
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
data-slot="pagination"
|
||||
{count}
|
||||
{perPage}
|
||||
{siblingCount}
|
||||
class={cn('cn-pagination mx-auto flex w-full justify-center', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
28
src/lib/components/ui/popover/index.ts
Normal file
28
src/lib/components/ui/popover/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import Close from './popover-close.svelte';
|
||||
import Content from './popover-content.svelte';
|
||||
import Description from './popover-description.svelte';
|
||||
import Header from './popover-header.svelte';
|
||||
import Portal from './popover-portal.svelte';
|
||||
import Title from './popover-title.svelte';
|
||||
import Trigger from './popover-trigger.svelte';
|
||||
import Root from './popover.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Description,
|
||||
Header,
|
||||
Title,
|
||||
Trigger,
|
||||
Close,
|
||||
Portal,
|
||||
//
|
||||
Root as Popover,
|
||||
Content as PopoverContent,
|
||||
Description as PopoverDescription,
|
||||
Header as PopoverHeader,
|
||||
Title as PopoverTitle,
|
||||
Trigger as PopoverTrigger,
|
||||
Close as PopoverClose,
|
||||
Portal as PopoverPortal,
|
||||
};
|
||||
7
src/lib/components/ui/popover/popover-close.svelte
Normal file
7
src/lib/components/ui/popover/popover-close.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: PopoverPrimitive.CloseProps = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Close bind:ref data-slot="popover-close" {...restProps} />
|
||||
31
src/lib/components/ui/popover/popover-content.svelte
Normal file
31
src/lib/components/ui/popover/popover-content.svelte
Normal file
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import PopoverPortal from './popover-portal.svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 4,
|
||||
align = 'center',
|
||||
portalProps,
|
||||
...restProps
|
||||
}: PopoverPrimitive.ContentProps & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof PopoverPortal>>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPortal {...portalProps}>
|
||||
<PopoverPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="popover-content"
|
||||
{sideOffset}
|
||||
{align}
|
||||
class={cn(
|
||||
'bg-popover text-popover-foreground ring-foreground/10 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 z-50 flex w-72 origin-(--transform-origin) flex-col gap-2.5 rounded-lg p-2.5 text-sm shadow-md ring-1 outline-hidden duration-100',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</PopoverPortal>
|
||||
20
src/lib/components/ui/popover/popover-description.svelte
Normal file
20
src/lib/components/ui/popover/popover-description.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.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="popover-description"
|
||||
class={cn('text-muted-foreground', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/popover/popover-header.svelte
Normal file
20
src/lib/components/ui/popover/popover-header.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.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="popover-header"
|
||||
class={cn('flex flex-col gap-0.5 text-sm', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
7
src/lib/components/ui/popover/popover-portal.svelte
Normal file
7
src/lib/components/ui/popover/popover-portal.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||
|
||||
let { ...restProps }: PopoverPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Portal {...restProps} />
|
||||
15
src/lib/components/ui/popover/popover-title.svelte
Normal file
15
src/lib/components/ui/popover/popover-title.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.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="popover-title" class={cn('font-medium', className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
8
src/lib/components/ui/popover/popover-trigger.svelte
Normal file
8
src/lib/components/ui/popover/popover-trigger.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let { ref = $bindable(null), class: className, ...restProps }: PopoverPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Trigger bind:ref data-slot="popover-trigger" class={cn('', className)} {...restProps} />
|
||||
7
src/lib/components/ui/popover/popover.svelte
Normal file
7
src/lib/components/ui/popover/popover.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||
|
||||
let { open = $bindable(false), ...restProps }: PopoverPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Root bind:open {...restProps} />
|
||||
37
src/lib/components/ui/select/index.ts
Normal file
37
src/lib/components/ui/select/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import Content from './select-content.svelte';
|
||||
import GroupHeading from './select-group-heading.svelte';
|
||||
import Group from './select-group.svelte';
|
||||
import Item from './select-item.svelte';
|
||||
import Label from './select-label.svelte';
|
||||
import Portal from './select-portal.svelte';
|
||||
import ScrollDownButton from './select-scroll-down-button.svelte';
|
||||
import ScrollUpButton from './select-scroll-up-button.svelte';
|
||||
import Separator from './select-separator.svelte';
|
||||
import Trigger from './select-trigger.svelte';
|
||||
import Root from './select.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Group,
|
||||
Label,
|
||||
Item,
|
||||
Content,
|
||||
Trigger,
|
||||
Separator,
|
||||
ScrollDownButton,
|
||||
ScrollUpButton,
|
||||
GroupHeading,
|
||||
Portal,
|
||||
//
|
||||
Root as Select,
|
||||
Group as SelectGroup,
|
||||
Label as SelectLabel,
|
||||
Item as SelectItem,
|
||||
Content as SelectContent,
|
||||
Trigger as SelectTrigger,
|
||||
Separator as SelectSeparator,
|
||||
ScrollDownButton as SelectScrollDownButton,
|
||||
ScrollUpButton as SelectScrollUpButton,
|
||||
GroupHeading as SelectGroupHeading,
|
||||
Portal as SelectPortal,
|
||||
};
|
||||
43
src/lib/components/ui/select/select-content.svelte
Normal file
43
src/lib/components/ui/select/select-content.svelte
Normal file
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from 'bits-ui';
|
||||
import { cn, type WithoutChild } from '$lib/utils.js';
|
||||
import type { WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import SelectPortal from './select-portal.svelte';
|
||||
import SelectScrollDownButton from './select-scroll-down-button.svelte';
|
||||
import SelectScrollUpButton from './select-scroll-up-button.svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 4,
|
||||
portalProps,
|
||||
children,
|
||||
preventScroll = true,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.ContentProps> & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SelectPortal>>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SelectPortal {...portalProps}>
|
||||
<SelectPrimitive.Content
|
||||
bind:ref
|
||||
{sideOffset}
|
||||
{preventScroll}
|
||||
data-slot="select-content"
|
||||
class={cn(
|
||||
'bg-popover text-popover-foreground ring-foreground/10 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 relative isolate z-50 min-w-36 overflow-x-hidden overflow-y-auto rounded-lg shadow-md ring-1 duration-100',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
class={cn('h-(--bits-select-anchor-height) w-full min-w-(--bits-select-anchor-width) scroll-my-1')}
|
||||
>
|
||||
{@render children?.()}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPortal>
|
||||
21
src/lib/components/ui/select/select-group-heading.svelte
Normal file
21
src/lib/components/ui/select/select-group-heading.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.GroupHeading
|
||||
bind:ref
|
||||
data-slot="select-group-heading"
|
||||
class={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</SelectPrimitive.GroupHeading>
|
||||
13
src/lib/components/ui/select/select-group.svelte
Normal file
13
src/lib/components/ui/select/select-group.svelte
Normal file
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let { ref = $bindable(null), class: className, ...restProps }: SelectPrimitive.GroupProps = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Group
|
||||
bind:ref
|
||||
data-slot="select-group"
|
||||
class={cn('scroll-my-1 p-1', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
40
src/lib/components/ui/select/select-item.svelte
Normal file
40
src/lib/components/ui/select/select-item.svelte
Normal file
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from 'bits-ui';
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
import { cn, type WithoutChild } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value,
|
||||
label,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.ItemProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Item
|
||||
bind:ref
|
||||
{value}
|
||||
data-slot="select-item"
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ selected, highlighted })}
|
||||
<span class="absolute end-2 flex size-3.5 items-center justify-center">
|
||||
{#if selected}
|
||||
<CheckIcon class="cn-select-item-indicator-icon" />
|
||||
{/if}
|
||||
</span>
|
||||
<span class="flex flex-1 shrink-0 gap-2 whitespace-nowrap">
|
||||
{#if childrenProp}
|
||||
{@render childrenProp({ selected, highlighted })}
|
||||
{:else}
|
||||
{label || value}
|
||||
{/if}
|
||||
</span>
|
||||
{/snippet}
|
||||
</SelectPrimitive.Item>
|
||||
20
src/lib/components/ui/select/select-label.svelte
Normal file
20
src/lib/components/ui/select/select-label.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.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="select-label"
|
||||
class={cn('text-muted-foreground px-1.5 py-1 text-xs', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
7
src/lib/components/ui/select/select-portal.svelte
Normal file
7
src/lib/components/ui/select/select-portal.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from 'bits-ui';
|
||||
|
||||
let { ...restProps }: SelectPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Portal {...restProps} />
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from 'bits-ui';
|
||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
bind:ref
|
||||
data-slot="select-scroll-down-button"
|
||||
class={cn(
|
||||
"bg-popover bottom-0 z-10 flex w-full cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronDownIcon />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
23
src/lib/components/ui/select/select-scroll-up-button.svelte
Normal file
23
src/lib/components/ui/select/select-scroll-up-button.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from 'bits-ui';
|
||||
import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
bind:ref
|
||||
data-slot="select-scroll-up-button"
|
||||
class={cn(
|
||||
"bg-popover top-0 z-10 flex w-full cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronUpIcon />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
14
src/lib/components/ui/select/select-separator.svelte
Normal file
14
src/lib/components/ui/select/select-separator.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { Separator } from '$lib/components/ui/separator/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { Separator as SeparatorPrimitive } from 'bits-ui';
|
||||
|
||||
let { ref = $bindable(null), class: className, ...restProps }: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<Separator
|
||||
bind:ref
|
||||
data-slot="select-separator"
|
||||
class={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
29
src/lib/components/ui/select/select-trigger.svelte
Normal file
29
src/lib/components/ui/select/select-trigger.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from 'bits-ui';
|
||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||
import { cn, type WithoutChild } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
size = 'default',
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.TriggerProps> & {
|
||||
size?: 'sm' | 'default';
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
class={cn(
|
||||
"border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 flex w-fit items-center justify-between gap-1.5 rounded-lg border bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronDownIcon class="text-muted-foreground pointer-events-none size-4" />
|
||||
</SelectPrimitive.Trigger>
|
||||
7
src/lib/components/ui/select/select.svelte
Normal file
7
src/lib/components/ui/select/select.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from 'bits-ui';
|
||||
|
||||
let { open = $bindable(false), value = $bindable(), ...restProps }: SelectPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Root bind:open bind:value={value as never} {...restProps} />
|
||||
7
src/lib/components/ui/separator/index.ts
Normal file
7
src/lib/components/ui/separator/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from './separator.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Separator,
|
||||
};
|
||||
23
src/lib/components/ui/separator/separator.svelte
Normal file
23
src/lib/components/ui/separator/separator.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { Separator as SeparatorPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
'data-slot': dataSlot = 'separator',
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SeparatorPrimitive.Root
|
||||
bind:ref
|
||||
data-slot={dataSlot}
|
||||
class={cn(
|
||||
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px',
|
||||
// this is different in shadcn/ui but self-stretch breaks things for us
|
||||
'data-[orientation=vertical]:h-full',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
34
src/lib/components/ui/sheet/index.ts
Normal file
34
src/lib/components/ui/sheet/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import Close from './sheet-close.svelte';
|
||||
import Content from './sheet-content.svelte';
|
||||
import Description from './sheet-description.svelte';
|
||||
import Footer from './sheet-footer.svelte';
|
||||
import Header from './sheet-header.svelte';
|
||||
import Overlay from './sheet-overlay.svelte';
|
||||
import Portal from './sheet-portal.svelte';
|
||||
import Title from './sheet-title.svelte';
|
||||
import Trigger from './sheet-trigger.svelte';
|
||||
import Root from './sheet.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Close,
|
||||
Trigger,
|
||||
Portal,
|
||||
Overlay,
|
||||
Content,
|
||||
Header,
|
||||
Footer,
|
||||
Title,
|
||||
Description,
|
||||
//
|
||||
Root as Sheet,
|
||||
Close as SheetClose,
|
||||
Trigger as SheetTrigger,
|
||||
Portal as SheetPortal,
|
||||
Overlay as SheetOverlay,
|
||||
Content as SheetContent,
|
||||
Header as SheetHeader,
|
||||
Footer as SheetFooter,
|
||||
Title as SheetTitle,
|
||||
Description as SheetDescription,
|
||||
};
|
||||
7
src/lib/components/ui/sheet/sheet-close.svelte
Normal file
7
src/lib/components/ui/sheet/sheet-close.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: SheetPrimitive.CloseProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Close bind:ref data-slot="sheet-close" {...restProps} />
|
||||
55
src/lib/components/ui/sheet/sheet-content.svelte
Normal file
55
src/lib/components/ui/sheet/sheet-content.svelte
Normal file
@@ -0,0 +1,55 @@
|
||||
<script lang="ts" module>
|
||||
export type Side = 'top' | 'right' | 'bottom' | 'left';
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
import XIcon from '@lucide/svelte/icons/x';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import SheetOverlay from './sheet-overlay.svelte';
|
||||
import SheetPortal from './sheet-portal.svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
side = 'right',
|
||||
showCloseButton = true,
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SheetPrimitive.ContentProps> & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SheetPortal>>;
|
||||
side?: Side;
|
||||
showCloseButton?: boolean;
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SheetPortal {...portalProps}>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="sheet-content"
|
||||
data-side={side}
|
||||
class={cn(
|
||||
'bg-popover text-popover-foreground data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10 fixed z-50 flex flex-col gap-4 bg-clip-padding text-sm shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{#if showCloseButton}
|
||||
<SheetPrimitive.Close data-slot="sheet-close">
|
||||
{#snippet child({ props })}
|
||||
<Button variant="ghost" class="absolute top-3 right-3" size="icon-sm" {...props}>
|
||||
<XIcon />
|
||||
<span class="sr-only">Close</span>
|
||||
</Button>
|
||||
{/snippet}
|
||||
</SheetPrimitive.Close>
|
||||
{/if}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
13
src/lib/components/ui/sheet/sheet-description.svelte
Normal file
13
src/lib/components/ui/sheet/sheet-description.svelte
Normal file
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let { ref = $bindable(null), class: className, ...restProps }: SheetPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Description
|
||||
bind:ref
|
||||
data-slot="sheet-description"
|
||||
class={cn('text-muted-foreground text-sm', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
20
src/lib/components/ui/sheet/sheet-footer.svelte
Normal file
20
src/lib/components/ui/sheet/sheet-footer.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.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="sheet-footer"
|
||||
class={cn('mt-auto flex flex-col gap-2 p-4', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/sheet/sheet-header.svelte
Normal file
20
src/lib/components/ui/sheet/sheet-header.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.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="sheet-header"
|
||||
class={cn('flex flex-col gap-0.5 p-4', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
13
src/lib/components/ui/sheet/sheet-overlay.svelte
Normal file
13
src/lib/components/ui/sheet/sheet-overlay.svelte
Normal file
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let { ref = $bindable(null), class: className, ...restProps }: SheetPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Overlay
|
||||
bind:ref
|
||||
data-slot="sheet-overlay"
|
||||
class={cn('fixed inset-0 z-50 bg-black/10 supports-backdrop-filter:backdrop-blur-xs', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
7
src/lib/components/ui/sheet/sheet-portal.svelte
Normal file
7
src/lib/components/ui/sheet/sheet-portal.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
|
||||
let { ...restProps }: SheetPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Portal {...restProps} />
|
||||
13
src/lib/components/ui/sheet/sheet-title.svelte
Normal file
13
src/lib/components/ui/sheet/sheet-title.svelte
Normal file
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let { ref = $bindable(null), class: className, ...restProps }: SheetPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Title
|
||||
bind:ref
|
||||
data-slot="sheet-title"
|
||||
class={cn('text-foreground text-base font-medium', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
7
src/lib/components/ui/sheet/sheet-trigger.svelte
Normal file
7
src/lib/components/ui/sheet/sheet-trigger.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: SheetPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Trigger bind:ref data-slot="sheet-trigger" {...restProps} />
|
||||
7
src/lib/components/ui/sheet/sheet.svelte
Normal file
7
src/lib/components/ui/sheet/sheet.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
|
||||
let { open = $bindable(false), ...restProps }: SheetPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Root bind:open {...restProps} />
|
||||
6
src/lib/components/ui/sidebar/constants.ts
Normal file
6
src/lib/components/ui/sidebar/constants.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const SIDEBAR_COOKIE_NAME = 'sidebar_state';
|
||||
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
export const SIDEBAR_WIDTH = '16rem';
|
||||
export const SIDEBAR_WIDTH_MOBILE = '18rem';
|
||||
export const SIDEBAR_WIDTH_ICON = '3rem';
|
||||
export const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
|
||||
79
src/lib/components/ui/sidebar/context.svelte.ts
Normal file
79
src/lib/components/ui/sidebar/context.svelte.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import { IsMobile } from '$lib/hooks/is-mobile.svelte.js';
|
||||
import { SIDEBAR_KEYBOARD_SHORTCUT } from './constants.js';
|
||||
|
||||
type Getter<T> = () => T;
|
||||
|
||||
export type SidebarStateProps = {
|
||||
/**
|
||||
* A getter function that returns the current open state of the sidebar.
|
||||
* We use a getter function here to support `bind:open` on the `Sidebar.Provider`
|
||||
* component.
|
||||
*/
|
||||
open: Getter<boolean>;
|
||||
|
||||
/**
|
||||
* A function that sets the open state of the sidebar. To support `bind:open`, we need
|
||||
* a source of truth for changing the open state to ensure it will be synced throughout
|
||||
* the sub-components and any `bind:` references.
|
||||
*/
|
||||
setOpen: (open: boolean) => void;
|
||||
};
|
||||
|
||||
class SidebarState {
|
||||
readonly props: SidebarStateProps;
|
||||
open = $derived.by(() => this.props.open());
|
||||
openMobile = $state(false);
|
||||
setOpen: SidebarStateProps['setOpen'];
|
||||
#isMobile: IsMobile;
|
||||
state = $derived.by(() => (this.open ? 'expanded' : 'collapsed'));
|
||||
|
||||
constructor(props: SidebarStateProps) {
|
||||
this.setOpen = props.setOpen;
|
||||
this.#isMobile = new IsMobile();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
// Convenience getter for checking if the sidebar is mobile
|
||||
// without this, we would need to use `sidebar.isMobile.current` everywhere
|
||||
get isMobile() {
|
||||
return this.#isMobile.current;
|
||||
}
|
||||
|
||||
// Event handler to apply to the `<svelte:window>`
|
||||
handleShortcutKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
this.toggle();
|
||||
}
|
||||
};
|
||||
|
||||
setOpenMobile = (value: boolean) => {
|
||||
this.openMobile = value;
|
||||
};
|
||||
|
||||
toggle = () => {
|
||||
return this.#isMobile.current ? (this.openMobile = !this.openMobile) : this.setOpen(!this.open);
|
||||
};
|
||||
}
|
||||
|
||||
const SYMBOL_KEY = 'scn-sidebar';
|
||||
|
||||
/**
|
||||
* Instantiates a new `SidebarState` instance and sets it in the context.
|
||||
*
|
||||
* @param props The constructor props for the `SidebarState` class.
|
||||
* @returns The `SidebarState` instance.
|
||||
*/
|
||||
export function setSidebar(props: SidebarStateProps): SidebarState {
|
||||
return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the `SidebarState` instance from the context. This is a class instance,
|
||||
* so you cannot destructure it.
|
||||
* @returns The `SidebarState` instance.
|
||||
*/
|
||||
export function useSidebar(): SidebarState {
|
||||
return getContext(Symbol.for(SYMBOL_KEY));
|
||||
}
|
||||
75
src/lib/components/ui/sidebar/index.ts
Normal file
75
src/lib/components/ui/sidebar/index.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import Content from './sidebar-content.svelte';
|
||||
import Footer from './sidebar-footer.svelte';
|
||||
import GroupAction from './sidebar-group-action.svelte';
|
||||
import GroupContent from './sidebar-group-content.svelte';
|
||||
import GroupLabel from './sidebar-group-label.svelte';
|
||||
import Group from './sidebar-group.svelte';
|
||||
import Header from './sidebar-header.svelte';
|
||||
import Input from './sidebar-input.svelte';
|
||||
import Inset from './sidebar-inset.svelte';
|
||||
import MenuAction from './sidebar-menu-action.svelte';
|
||||
import MenuBadge from './sidebar-menu-badge.svelte';
|
||||
import MenuButton from './sidebar-menu-button.svelte';
|
||||
import MenuItem from './sidebar-menu-item.svelte';
|
||||
import MenuSkeleton from './sidebar-menu-skeleton.svelte';
|
||||
import MenuSubButton from './sidebar-menu-sub-button.svelte';
|
||||
import MenuSubItem from './sidebar-menu-sub-item.svelte';
|
||||
import MenuSub from './sidebar-menu-sub.svelte';
|
||||
import Menu from './sidebar-menu.svelte';
|
||||
import Provider from './sidebar-provider.svelte';
|
||||
import Rail from './sidebar-rail.svelte';
|
||||
import Separator from './sidebar-separator.svelte';
|
||||
import Trigger from './sidebar-trigger.svelte';
|
||||
import Root from './sidebar.svelte';
|
||||
import { useSidebar } from './context.svelte.js';
|
||||
|
||||
export {
|
||||
Content,
|
||||
Footer,
|
||||
Group,
|
||||
GroupAction,
|
||||
GroupContent,
|
||||
GroupLabel,
|
||||
Header,
|
||||
Input,
|
||||
Inset,
|
||||
Menu,
|
||||
MenuAction,
|
||||
MenuBadge,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuSkeleton,
|
||||
MenuSub,
|
||||
MenuSubButton,
|
||||
MenuSubItem,
|
||||
Provider,
|
||||
Rail,
|
||||
Root,
|
||||
Separator,
|
||||
//
|
||||
Root as Sidebar,
|
||||
Content as SidebarContent,
|
||||
Footer as SidebarFooter,
|
||||
Group as SidebarGroup,
|
||||
GroupAction as SidebarGroupAction,
|
||||
GroupContent as SidebarGroupContent,
|
||||
GroupLabel as SidebarGroupLabel,
|
||||
Header as SidebarHeader,
|
||||
Input as SidebarInput,
|
||||
Inset as SidebarInset,
|
||||
Menu as SidebarMenu,
|
||||
MenuAction as SidebarMenuAction,
|
||||
MenuBadge as SidebarMenuBadge,
|
||||
MenuButton as SidebarMenuButton,
|
||||
MenuItem as SidebarMenuItem,
|
||||
MenuSkeleton as SidebarMenuSkeleton,
|
||||
MenuSub as SidebarMenuSub,
|
||||
MenuSubButton as SidebarMenuSubButton,
|
||||
MenuSubItem as SidebarMenuSubItem,
|
||||
Provider as SidebarProvider,
|
||||
Rail as SidebarRail,
|
||||
Separator as SidebarSeparator,
|
||||
Trigger as SidebarTrigger,
|
||||
Trigger,
|
||||
useSidebar,
|
||||
};
|
||||
24
src/lib/components/ui/sidebar/sidebar-content.svelte
Normal file
24
src/lib/components/ui/sidebar/sidebar-content.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="sidebar-content"
|
||||
data-sidebar="content"
|
||||
class={cn(
|
||||
'no-scrollbar flex min-h-0 flex-1 flex-col gap-0 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
21
src/lib/components/ui/sidebar/sidebar-footer.svelte
Normal file
21
src/lib/components/ui/sidebar/sidebar-footer.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="sidebar-footer"
|
||||
data-sidebar="footer"
|
||||
class={cn('flex flex-col gap-2 p-2', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
33
src/lib/components/ui/sidebar/sidebar-group-action.svelte
Normal file
33
src/lib/components/ui/sidebar/sidebar-group-action.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLButtonAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
child,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLButtonAttributes> & {
|
||||
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||
} = $props();
|
||||
|
||||
const mergedProps = $derived({
|
||||
class: cn(
|
||||
'absolute top-3.5 right-3 w-5 rounded-md p-0 text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 flex aspect-square items-center justify-center outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 md:after:hidden [&>svg]:shrink-0',
|
||||
className,
|
||||
),
|
||||
'data-slot': 'sidebar-group-action',
|
||||
'data-sidebar': 'group-action',
|
||||
...restProps,
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<button bind:this={ref} {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
21
src/lib/components/ui/sidebar/sidebar-group-content.svelte
Normal file
21
src/lib/components/ui/sidebar/sidebar-group-content.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.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="sidebar-group-content"
|
||||
data-sidebar="group-content"
|
||||
class={cn('w-full text-sm', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
33
src/lib/components/ui/sidebar/sidebar-group-label.svelte
Normal file
33
src/lib/components/ui/sidebar/sidebar-group-label.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
child,
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
|
||||
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||
} = $props();
|
||||
|
||||
const mergedProps = $derived({
|
||||
class: cn(
|
||||
'h-8 rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 flex shrink-0 items-center outline-hidden [&>svg]:shrink-0',
|
||||
className,
|
||||
),
|
||||
'data-slot': 'sidebar-group-label',
|
||||
'data-sidebar': 'group-label',
|
||||
...restProps,
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div bind:this={ref} {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user