🎉 创世提交

This commit is contained in:
柚子
2025-01-22 01:48:07 +08:00
commit 5d3481b9ea
92 changed files with 11705 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
import { Component } from 'solid-js';
import { A } from '@solidjs/router';
export interface AppCardProps {
category: string;
pkgname: string;
name: string;
description: string;
icon?: string;
}
const AppCard: Component<AppCardProps> = (props) => {
return (
<A
href={`/app/${props.category}/${props.pkgname}`}
class="block p-4 rounded-lg border border-border bg-card hover:border-primary/50 transition-colors"
>
<div class="flex items-start gap-3">
<div class="w-12 h-12 rounded-lg bg-muted flex items-center justify-center overflow-hidden">
{props.icon ? (
<img src={props.icon} alt={props.name} class="w-full h-full object-cover" />
) : (
<div class="text-2xl font-bold text-muted-foreground">
{props.name.charAt(0)}
</div>
)}
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between gap-2">
<h3 class="font-medium text-base truncate">{props.name}</h3>
</div>
<p class="mt-1 text-sm text-muted-foreground line-clamp-2">
{props.description}
</p>
</div>
</div>
</A>
);
};
export default AppCard;

View File

@@ -0,0 +1,52 @@
import { Component, For } from 'solid-js';
import { AppItem } from '@/types/app';
import AppCard from '../AppCard';
import { Skeleton } from "@/components/ui/skeleton";
interface AppListProps {
apps: AppItem[];
loading: boolean;
category?: string;
}
const AppList: Component<AppListProps> = (props) => {
return (
<div class="grid auto-rows-auto grid-cols-[repeat(auto-fit,minmax(200px,1fr))] gap-4 pb-6">
{props.loading ? (
<For each={Array(8).fill(0)}>
{() => (
<div class="p-4 rounded-lg border border-border/40 bg-card">
<div class="flex items-start gap-3">
<Skeleton width={48} height={48} radius={8} />
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between gap-2">
<Skeleton height={20} width={96} />
<Skeleton height={16} width={48} />
</div>
<div class="mt-1">
<Skeleton height={16} width={100} class="mt-2" />
<Skeleton height={16} width={100} class="mt-2" />
</div>
</div>
</div>
</div>
)}
</For>
) : (
<For each={props.apps}>
{(app) => (
<AppCard
category={props.category || app.category || ''}
pkgname={app.pkgname}
name={app.name}
description={app.more}
icon={app.icon}
/>
)}
</For>
)}
</div>
);
};
export default AppList;

View File

@@ -0,0 +1,53 @@
import { Component } from 'solid-js';
import { Skeleton } from "@/components/ui/skeleton";
import BaseCarousel from "@/components/ui/base-carousel";
import { HomeLink } from '@/types/home';
interface HomeCarouselProps {
slides?: HomeLink[];
loading?: boolean;
}
const HomeCarousel: Component<HomeCarouselProps> = (props) => {
const renderItem = (slide: NonNullable<HomeCarouselProps['slides']>[number]) => (
<a
href={slide.linkUrl}
target="_blank"
rel="noopener noreferrer"
class="text-center w-full h-[120px] relative bg-cover bg-center bg-no-repeat block"
style={{"background-image": `url(${slide.imgUrl})`}}
>
{/* 添加一个半透明的遮罩层使文字更易读 */}
<div class="absolute inset-0 bg-black/50" />
<div class="relative z-10 p-4 flex flex-col justify-center h-full">
<h3 class="text-xl font-semibold mb-2 break-words text-white">{slide.name}</h3>
<p class="text-sm text-gray-200 break-words">{slide.more}</p>
</div>
</a>
);
const renderSkeleton = () => (
<div class="text-center w-full h-full relative">
<div class="absolute inset-0">
<Skeleton class="w-full h-full" />
</div>
<div class="relative z-10 p-4 flex flex-col justify-center h-full">
<div class="mb-2">
<Skeleton height={24} width={60} class="mx-auto" />
</div>
<Skeleton height={16} width={80} class="mx-auto" />
</div>
</div>
);
return (
<BaseCarousel
items={props.slides}
loading={props.loading}
renderItem={renderItem}
renderSkeleton={renderSkeleton}
/>
);
};
export default HomeCarousel;

View File

@@ -0,0 +1,40 @@
import { Component } from 'solid-js';
import { Skeleton } from "@/components/ui/skeleton";
import BaseCarousel from "@/components/ui/base-carousel";
interface ScreenshotCarouselProps {
screenshots?: {
url: string;
title: string;
}[];
loading?: boolean;
}
const ScreenshotCarousel: Component<ScreenshotCarouselProps> = (props) => {
const renderItem = (screenshot: NonNullable<ScreenshotCarouselProps['screenshots']>[0]) => (
<div class="w-full h-full aspect-[16/9]">
<img
src={screenshot.url}
alt={screenshot.title}
class="w-full h-full object-cover rounded-lg"
/>
</div>
);
const renderSkeleton = () => (
<div class="w-full h-full aspect-[16/9]">
<Skeleton class="w-full h-full rounded-lg" />
</div>
);
return (
<BaseCarousel
items={props.screenshots}
loading={props.loading}
renderItem={renderItem}
renderSkeleton={renderSkeleton}
/>
);
};
export default ScreenshotCarousel;

View File

@@ -0,0 +1,114 @@
import { Component, For } from 'solid-js';
import { A } from '@solidjs/router';
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarRail
} from '../ui/sidebar';
import "./style.css"
import { Compass, Heart, RefreshCw, Settings, Waves } from 'lucide-solid';
import { useCategoriesStore } from '@/features/categories/store';
import { getIconComponent } from '@/lib/icon';
const menuItems = [
{
title: '探索发现',
url: '/',
icon: () => <Compass size={20} />
},
{
title: '我的收藏',
url: '/',
icon: () => <Heart size={20} />
},
{
title: '下载列表',
url: '/',
icon: () => <Waves size={20} />
}
];
const footerItems = [
{
title: '检查更新',
url: '/update',
icon: () => <RefreshCw size={20} />
},
{
title: '应用设置',
url: '/settings',
icon: () => <Settings size={20} />
}
];
const AppSidebar: Component = () => {
const { categories } = useCategoriesStore();
return (
<Sidebar>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Spark Store</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<For each={menuItems}>
{(item) => (
<SidebarMenuItem>
<SidebarMenuButton as={A} href={item.url}>
{item.icon ? item.icon() : null}
<span>{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
)}
</For>
</SidebarMenu>
</SidebarGroupContent>
<SidebarGroupLabel></SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<For each={categories()}>
{(category) => {
return (
<SidebarMenuItem>
<SidebarMenuButton as={A} href={`/categories/${category.id}`}>
{getIconComponent(category.icon)({
size: 20,
iconNode: []
})}
<span>{category.name_zh_cn}</span>
</SidebarMenuButton>
</SidebarMenuItem>
);
}}
</For>
</SidebarMenu>
</SidebarGroupContent>
<SidebarGroupLabel></SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<For each={footerItems}>
{(item) => (
<SidebarMenuItem>
<SidebarMenuButton as={A} href={item.url}>
{item.icon ? item.icon() : null}
<span>{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
)}
</For>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarRail />
</Sidebar>
);
};
export default AppSidebar;

View File

@@ -0,0 +1,28 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 85%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark,
[data-kb-theme="dark"] {
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}

View File

@@ -0,0 +1,85 @@
import { Component, Show } from "solid-js";
import { Maximize2, Minus, X, ArrowLeft, RotateCw, Search } from "lucide-solid";
import { useTitleBarStore } from "./store";
import { Button } from "@/components/ui/button";
import { SidebarTrigger } from "../ui/sidebar";
import { useNavigate } from "@solidjs/router";
import { TextField, TextFieldInput } from "@/components/ui/text-field";
interface TitleBarProps {
onRefresh?: () => void;
isSidebarOpen?: boolean;
}
const TitleBar: Component<TitleBarProps> = (props) => {
const { goBack, canGoBack, refresh } = useTitleBarStore();
const navigate = useNavigate();
let searchInput: HTMLInputElement | undefined;
const handleSearch = () => {
if (searchInput?.value) {
navigate(`/search?q=${encodeURIComponent(searchInput.value)}`);
searchInput.value = "";
}
};
const handleKeyPress = (e: KeyboardEvent) => {
if (e.key === "Enter") {
handleSearch();
}
};
return (
<div
class={`h-12 border-b flex items-center justify-between px-2 bg-background fixed top-0 right-0 z-50 ${
props.isSidebarOpen ? "left-[var(--sidebar-width)]" : "left-0"
}`}
>
<div class="flex items-center gap-2">
<SidebarTrigger class="h-8 px-2" />
<Show when={canGoBack()}>
<Button variant="ghost" size="icon" class="h-8 px-2" onClick={goBack}>
<ArrowLeft size={16} />
</Button>
</Show>
<Button
variant="ghost"
size="icon"
class="h-8 px-2"
onClick={() => refresh(props.onRefresh)}
>
<RotateCw size={16} />
</Button>
<TextField>
<TextFieldInput
ref={searchInput}
placeholder="搜索应用..."
class="h-8"
onKeyPress={handleKeyPress}
/>
</TextField>
<Button
variant="ghost"
size="icon"
class="h-8 px-2"
onClick={handleSearch}
>
<Search size={16} />
</Button>
</div>
<div class="flex items-center gap-2">
<Button variant="ghost" class="h-8 px-2">
<Minus size={16} />
</Button>
<Button variant="ghost" class="h-8 px-2">
<Maximize2 size={16} />
</Button>
<Button variant="ghost" class="h-8 px-2">
<X size={16} />
</Button>
</div>
</div>
);
};
export default TitleBar;

View File

@@ -0,0 +1,54 @@
import { useNavigate, useLocation, useBeforeLeave } from '@solidjs/router';
export const useTitleBarStore = () => {
const navigate = useNavigate();
const location = useLocation();
let lastScrollPosition = 0;
const canGoBack = () => {
return location.pathname.startsWith('/app');
};
const saveScrollPosition = () => {
const mainContent = document.getElementById('main-content');
if (mainContent) {
lastScrollPosition = mainContent.scrollTop;
}
};
const restoreScrollPosition = () => {
if (lastScrollPosition) {
const mainContent = document.getElementById('main-content');
if (mainContent) {
mainContent.scrollTop = lastScrollPosition;
lastScrollPosition = 0;
}
}
};
const goBack = () => {
navigate(-1);
// 增加延迟时间以确保页面完全加载
setTimeout(restoreScrollPosition, 50);
};
const refresh = (onRefresh?: () => void) => {
if (onRefresh) {
onRefresh();
}
};
// 使用useBeforeLeave在路由离开前保存滚动位置
useBeforeLeave((_e) => {
if(!location.pathname.startsWith('/app'))
{
saveScrollPosition();
}
});
return {
canGoBack,
goBack,
refresh
};
};

View File

@@ -0,0 +1,95 @@
import { Component, JSX, Show, createSignal } from 'solid-js';
import { Card, CardContent } from '@/components/ui/card';
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious
} from '@/components/ui/carousel';
import Autoplay from "embla-carousel-autoplay"
interface BaseCarouselProps {
loading?: boolean;
renderItem: (item: any) => JSX.Element;
renderSkeleton: () => JSX.Element;
items?: any[];
}
const BaseCarousel: Component<BaseCarouselProps> = (props) => {
return (
<div class="relative w-full group">
<Carousel
opts={{
align: 'start',
loop: true
}}
plugins={[
Autoplay({
delay: 2000
})
]}
class="w-full"
>
<CarouselContent class="-ml-1">
<Show
when={!props.loading}
fallback={
<>
{Array(3).fill(0).map(() => (
<CarouselItem class="pl-1 basis-full sm:basis-1/2 lg:basis-1/3">
<Card>
<CardContent class="flex items-center justify-center p-2 sm:p-4 lg:p-6">
{props.renderSkeleton()}
</CardContent>
</Card>
</CarouselItem>
))}
</>
}
>
{props.items?.map((item) => {
// 如果item包含url属性说明是图片类型的轮播项
if (item.url) {
const [isLoaded, setIsLoaded] = createSignal(false);
const img = new Image();
img.onload = () => {
if (img.naturalWidth > 0) {
setIsLoaded(true);
}
};
img.src = item.url;
return (
<Show when={isLoaded() || (img.complete && img.naturalWidth > 0)}>
<CarouselItem class="pl-1 basis-full sm:basis-1/2 lg:basis-1/3">
<Card class='overflow-hidden'>
<CardContent class="flex items-center justify-center p-0">
{props.renderItem(item)}
</CardContent>
</Card>
</CarouselItem>
</Show>
);
}
return (
<CarouselItem class="pl-1 basis-full sm:basis-1/2 lg:basis-1/3">
<Card class='overflow-hidden'>
<CardContent class="flex items-center justify-center p-0">
{props.renderItem(item)}
</CardContent>
</Card>
</CarouselItem>
);
})}
</Show>
</CarouselContent>
<CarouselPrevious class="left-3 opacity-0 group-hover:opacity-100 transition-opacity bg-background/80 hover:bg-background" />
<CarouselNext class="right-3 opacity-0 group-hover:opacity-100 transition-opacity bg-background/80 hover:bg-background" />
</Carousel>
</div>
);
};
export default BaseCarousel;

View File

@@ -0,0 +1,53 @@
import type { JSX, ValidComponent } from "solid-js"
import { splitProps } from "solid-js"
import * as ButtonPrimitive from "@kobalte/core/button"
import type { PolymorphicProps } from "@kobalte/core/polymorphic"
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline"
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 px-3 text-xs",
lg: "h-11 px-8",
icon: "size-10"
}
},
defaultVariants: {
variant: "default",
size: "default"
}
}
)
type ButtonProps<T extends ValidComponent = "button"> = ButtonPrimitive.ButtonRootProps<T> &
VariantProps<typeof buttonVariants> & { class?: string | undefined; children?: JSX.Element }
const Button = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, ButtonProps<T>>
) => {
const [local, others] = splitProps(props as ButtonProps, ["variant", "size", "class"])
return (
<ButtonPrimitive.Root
class={cn(buttonVariants({ variant: local.variant, size: local.size }), local.class)}
{...others}
/>
)
}
export type { ButtonProps }
export { Button, buttonVariants }

View File

@@ -0,0 +1,43 @@
import type { Component, ComponentProps } from "solid-js"
import { splitProps } from "solid-js"
import { cn } from "@/lib/utils"
const Card: Component<ComponentProps<"div">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<div
class={cn("rounded-lg border bg-card text-card-foreground shadow-sm", local.class)}
{...others}
/>
)
}
const CardHeader: Component<ComponentProps<"div">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return <div class={cn("flex flex-col space-y-1.5 p-6", local.class)} {...others} />
}
const CardTitle: Component<ComponentProps<"h3">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<h3 class={cn("text-lg font-semibold leading-none tracking-tight", local.class)} {...others} />
)
}
const CardDescription: Component<ComponentProps<"p">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return <p class={cn("text-sm text-muted-foreground", local.class)} {...others} />
}
const CardContent: Component<ComponentProps<"div">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return <div class={cn("p-6 pt-0", local.class)} {...others} />
}
const CardFooter: Component<ComponentProps<"div">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return <div class={cn("flex items-center p-6 pt-0", local.class)} {...others} />
}
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,263 @@
import type { Accessor, Component, ComponentProps, VoidProps } from "solid-js"
import {
createContext,
createEffect,
createMemo,
createSignal,
mergeProps,
splitProps,
useContext
} from "solid-js"
import type { CreateEmblaCarouselType } from "embla-carousel-solid"
import createEmblaCarousel from "embla-carousel-solid"
import { cn } from "@/lib/utils"
import type { ButtonProps } from "@/components/ui/button"
import { Button } from "@/components/ui/button"
export type CarouselApi = CreateEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof createEmblaCarousel>
type CarouselOptions = NonNullable<UseCarouselParameters[0]>
type CarouselPlugin = NonNullable<UseCarouselParameters[1]>
type CarouselProps = {
opts?: ReturnType<CarouselOptions>
plugins?: ReturnType<CarouselPlugin>
orientation?: "horizontal" | "vertical"
setApi?: (api: CarouselApi) => void
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof createEmblaCarousel>[0]
api: ReturnType<typeof createEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: Accessor<boolean>
canScrollNext: Accessor<boolean>
} & CarouselProps
const CarouselContext = createContext<Accessor<CarouselContextProps> | null>(null)
const useCarousel = () => {
const context = useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context()
}
const Carousel: Component<CarouselProps & ComponentProps<"div">> = (rawProps) => {
const props = mergeProps<(CarouselProps & ComponentProps<"div">)[]>(
{ orientation: "horizontal" },
rawProps
)
const [local, others] = splitProps(props, [
"orientation",
"opts",
"setApi",
"plugins",
"class",
"children"
])
const [carouselRef, api] = createEmblaCarousel(
() => ({
...local.opts,
axis: local.orientation === "horizontal" ? "x" : "y"
}),
() => (local.plugins === undefined ? [] : local.plugins)
)
const [canScrollPrev, setCanScrollPrev] = createSignal(false)
const [canScrollNext, setCanScrollNext] = createSignal(false)
const onSelect = (api: NonNullable<ReturnType<CarouselApi>>) => {
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}
const scrollPrev = () => {
api()?.scrollPrev()
}
const scrollNext = () => {
api()?.scrollNext()
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
}
createEffect(() => {
if (!api() || !local.setApi) {
return
}
local.setApi(api)
})
createEffect(() => {
if (!api()) {
return
}
onSelect(api()!)
api()!.on("reInit", onSelect)
api()!.on("select", onSelect)
return () => {
api()?.off("select", onSelect)
}
})
const value = createMemo(
() =>
({
carouselRef,
api,
opts: local.opts,
orientation: local.orientation || (local.opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext
}) satisfies CarouselContextProps
)
return (
<CarouselContext.Provider value={value}>
<div
onKeyDown={handleKeyDown}
class={cn("relative", local.class)}
role="region"
aria-roledescription="carousel"
{...others}
>
{local.children}
</div>
</CarouselContext.Provider>
)
}
const CarouselContent: Component<ComponentProps<"div">> = (props) => {
const [local, others] = splitProps(props, ["class"])
const { carouselRef, orientation } = useCarousel()
return (
<div ref={carouselRef} class="overflow-hidden">
<div
class={cn("flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", local.class)}
{...others}
/>
</div>
)
}
const CarouselItem: Component<ComponentProps<"div">> = (props) => {
const [local, others] = splitProps(props, ["class"])
const { orientation } = useCarousel()
return (
<div
role="group"
aria-roledescription="slide"
class={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
local.class
)}
{...others}
/>
)
}
type CarouselButtonProps = VoidProps<ButtonProps>
const CarouselPrevious: Component<CarouselButtonProps> = (rawProps) => {
const props = mergeProps<CarouselButtonProps[]>({ variant: "outline", size: "icon" }, rawProps)
const [local, others] = splitProps(props, ["class", "variant", "size"])
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
variant={local.variant}
size={local.size}
class={cn(
"absolute size-8 touch-manipulation rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
local.class
)}
disabled={!canScrollPrev()}
onClick={scrollPrev}
{...others}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4"
>
<path d="M5 12l14 0" />
<path d="M5 12l6 6" />
<path d="M5 12l6 -6" />
</svg>
<span class="sr-only">Previous slide</span>
</Button>
)
}
const CarouselNext: Component<CarouselButtonProps> = (rawProps) => {
const props = mergeProps<CarouselButtonProps[]>({ variant: "outline", size: "icon" }, rawProps)
const [local, others] = splitProps(props, ["class", "variant", "size"])
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
variant={local.variant}
size={local.size}
class={cn(
"absolute size-8 touch-manipulation rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
local.class
)}
disabled={!canScrollNext()}
onClick={scrollNext}
{...others}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4"
>
<path d="M5 12l14 0" />
<path d="M13 18l6 -6" />
<path d="M13 6l6 6" />
</svg>
<span class="sr-only">Next slide</span>
</Button>
)
}
export { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext }

View File

@@ -0,0 +1,9 @@
import * as CollapsiblePrimitive from "@kobalte/core/collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.Trigger
const CollapsibleContent = CollapsiblePrimitive.Content
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View File

@@ -0,0 +1,260 @@
import type { Component, ComponentProps, JSX, ValidComponent } from "solid-js"
import { splitProps } from "solid-js"
import * as DropdownMenuPrimitive from "@kobalte/core/dropdown-menu"
import type { PolymorphicProps } from "@kobalte/core/polymorphic"
import { cn } from "@/lib/utils"
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenu: Component<DropdownMenuPrimitive.DropdownMenuRootProps> = (props) => {
return <DropdownMenuPrimitive.Root gutter={4} {...props} />
}
type DropdownMenuContentProps<T extends ValidComponent = "div"> =
DropdownMenuPrimitive.DropdownMenuContentProps<T> & {
class?: string | undefined
}
const DropdownMenuContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, DropdownMenuContentProps<T>>
) => {
const [, rest] = splitProps(props as DropdownMenuContentProps, ["class"])
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
class={cn(
"z-50 min-w-32 origin-[var(--kb-menu-content-transform-origin)] animate-content-hide overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[expanded]:animate-content-show",
props.class
)}
{...rest}
/>
</DropdownMenuPrimitive.Portal>
)
}
type DropdownMenuItemProps<T extends ValidComponent = "div"> =
DropdownMenuPrimitive.DropdownMenuItemProps<T> & {
class?: string | undefined
}
const DropdownMenuItem = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, DropdownMenuItemProps<T>>
) => {
const [, rest] = splitProps(props as DropdownMenuItemProps, ["class"])
return (
<DropdownMenuPrimitive.Item
class={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
props.class
)}
{...rest}
/>
)
}
const DropdownMenuShortcut: Component<ComponentProps<"span">> = (props) => {
const [, rest] = splitProps(props, ["class"])
return <span class={cn("ml-auto text-xs tracking-widest opacity-60", props.class)} {...rest} />
}
const DropdownMenuLabel: Component<ComponentProps<"div"> & { inset?: boolean }> = (props) => {
const [, rest] = splitProps(props, ["class", "inset"])
return (
<div
class={cn("px-2 py-1.5 text-sm font-semibold", props.inset && "pl-8", props.class)}
{...rest}
/>
)
}
type DropdownMenuSeparatorProps<T extends ValidComponent = "hr"> =
DropdownMenuPrimitive.DropdownMenuSeparatorProps<T> & {
class?: string | undefined
}
const DropdownMenuSeparator = <T extends ValidComponent = "hr">(
props: PolymorphicProps<T, DropdownMenuSeparatorProps<T>>
) => {
const [, rest] = splitProps(props as DropdownMenuSeparatorProps, ["class"])
return (
<DropdownMenuPrimitive.Separator
class={cn("-mx-1 my-1 h-px bg-muted", props.class)}
{...rest}
/>
)
}
type DropdownMenuSubTriggerProps<T extends ValidComponent = "div"> =
DropdownMenuPrimitive.DropdownMenuSubTriggerProps<T> & {
class?: string | undefined
children?: JSX.Element
}
const DropdownMenuSubTrigger = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, DropdownMenuSubTriggerProps<T>>
) => {
const [, rest] = splitProps(props as DropdownMenuSubTriggerProps, ["class", "children"])
return (
<DropdownMenuPrimitive.SubTrigger
class={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
props.class
)}
{...rest}
>
{props.children}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="ml-auto size-4"
>
<path d="M9 6l6 6l-6 6" />
</svg>
</DropdownMenuPrimitive.SubTrigger>
)
}
type DropdownMenuSubContentProps<T extends ValidComponent = "div"> =
DropdownMenuPrimitive.DropdownMenuSubContentProps<T> & {
class?: string | undefined
}
const DropdownMenuSubContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, DropdownMenuSubContentProps<T>>
) => {
const [, rest] = splitProps(props as DropdownMenuSubContentProps, ["class"])
return (
<DropdownMenuPrimitive.SubContent
class={cn(
"z-50 min-w-32 origin-[var(--kb-menu-content-transform-origin)] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in",
props.class
)}
{...rest}
/>
)
}
type DropdownMenuCheckboxItemProps<T extends ValidComponent = "div"> =
DropdownMenuPrimitive.DropdownMenuCheckboxItemProps<T> & {
class?: string | undefined
children?: JSX.Element
}
const DropdownMenuCheckboxItem = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, DropdownMenuCheckboxItemProps<T>>
) => {
const [, rest] = splitProps(props as DropdownMenuCheckboxItemProps, ["class", "children"])
return (
<DropdownMenuPrimitive.CheckboxItem
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
props.class
)}
{...rest}
>
<span class="absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4"
>
<path d="M5 12l5 5l10 -10" />
</svg>
</DropdownMenuPrimitive.ItemIndicator>
</span>
{props.children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
type DropdownMenuGroupLabelProps<T extends ValidComponent = "span"> =
DropdownMenuPrimitive.DropdownMenuGroupLabelProps<T> & {
class?: string | undefined
}
const DropdownMenuGroupLabel = <T extends ValidComponent = "span">(
props: PolymorphicProps<T, DropdownMenuGroupLabelProps<T>>
) => {
const [, rest] = splitProps(props as DropdownMenuGroupLabelProps, ["class"])
return (
<DropdownMenuPrimitive.GroupLabel
class={cn("px-2 py-1.5 text-sm font-semibold", props.class)}
{...rest}
/>
)
}
type DropdownMenuRadioItemProps<T extends ValidComponent = "div"> =
DropdownMenuPrimitive.DropdownMenuRadioItemProps<T> & {
class?: string | undefined
children?: JSX.Element
}
const DropdownMenuRadioItem = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, DropdownMenuRadioItemProps<T>>
) => {
const [, rest] = splitProps(props as DropdownMenuRadioItemProps, ["class", "children"])
return (
<DropdownMenuPrimitive.RadioItem
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
props.class
)}
{...rest}
>
<span class="absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-2 fill-current"
>
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
</svg>
</DropdownMenuPrimitive.ItemIndicator>
</span>
{props.children}
</DropdownMenuPrimitive.RadioItem>
)
}
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuPortal,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuShortcut,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
DropdownMenuCheckboxItem,
DropdownMenuGroup,
DropdownMenuGroupLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem
}

View File

@@ -0,0 +1,29 @@
import type { ValidComponent } from "solid-js"
import { splitProps } from "solid-js"
import type { PolymorphicProps } from "@kobalte/core/polymorphic"
import * as SeparatorPrimitive from "@kobalte/core/separator"
import { cn } from "@/lib/utils"
type SeparatorRootProps<T extends ValidComponent = "hr"> =
SeparatorPrimitive.SeparatorRootProps<T> & { class?: string | undefined }
const Separator = <T extends ValidComponent = "hr">(
props: PolymorphicProps<T, SeparatorRootProps<T>>
) => {
const [local, others] = splitProps(props as SeparatorRootProps, ["class", "orientation"])
return (
<SeparatorPrimitive.Root
orientation={local.orientation ?? "horizontal"}
class={cn(
"shrink-0 bg-border",
local.orientation === "vertical" ? "h-full w-px" : "h-px w-full",
local.class
)}
{...others}
/>
)
}
export { Separator }

172
src/components/ui/sheet.tsx Normal file
View File

@@ -0,0 +1,172 @@
import type { Component, ComponentProps, JSX, ValidComponent } from "solid-js"
import { splitProps } from "solid-js"
import * as SheetPrimitive from "@kobalte/core/dialog"
import type { PolymorphicProps } from "@kobalte/core/polymorphic"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.CloseButton
const portalVariants = cva("fixed inset-0 z-50 flex", {
variants: {
position: {
top: "items-start",
bottom: "items-end",
left: "justify-start",
right: "justify-end"
}
},
defaultVariants: { position: "right" }
})
type PortalProps = SheetPrimitive.DialogPortalProps & VariantProps<typeof portalVariants>
const SheetPortal: Component<PortalProps> = (props) => {
const [local, others] = splitProps(props, ["position", "children"])
return (
<SheetPrimitive.Portal {...others}>
<div class={portalVariants({ position: local.position })}>{local.children}</div>
</SheetPrimitive.Portal>
)
}
type DialogOverlayProps<T extends ValidComponent = "div"> = SheetPrimitive.DialogOverlayProps<T> & {
class?: string | undefined
}
const SheetOverlay = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, DialogOverlayProps<T>>
) => {
const [local, others] = splitProps(props as DialogOverlayProps, ["class"])
return (
<SheetPrimitive.Overlay
class={cn(
"fixed inset-0 z-50 bg-black/80 data-[expanded=]:animate-in data-[closed=]:animate-out data-[closed=]:fade-out-0 data-[expanded=]:fade-in-0",
local.class
)}
{...others}
/>
)
}
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[closed=]:duration-300 data-[expanded=]:duration-500 data-[expanded=]:animate-in data-[closed=]:animate-out",
{
variants: {
position: {
top: "inset-x-0 top-0 border-b data-[closed=]:slide-out-to-top data-[expanded=]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[closed=]:slide-out-to-bottom data-[expanded=]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[closed=]:slide-out-to-left data-[expanded]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[closed=]:slide-out-to-right data-[expanded=]:slide-in-from-right sm:max-w-sm"
}
},
defaultVariants: {
position: "right"
}
}
)
type DialogContentProps<T extends ValidComponent = "div"> = SheetPrimitive.DialogContentProps<T> &
VariantProps<typeof sheetVariants> & { class?: string | undefined; children?: JSX.Element }
const SheetContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, DialogContentProps<T>>
) => {
const [local, others] = splitProps(props as DialogContentProps, ["position", "class", "children"])
return (
<SheetPortal position={local.position}>
<SheetOverlay />
<SheetPrimitive.Content
class={cn(
sheetVariants({ position: local.position }),
local.class,
"max-h-screen overflow-y-auto"
)}
{...others}
>
{local.children}
<SheetPrimitive.CloseButton class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4"
>
<path d="M18 6l-12 12" />
<path d="M6 6l12 12" />
</svg>
<span class="sr-only">Close</span>
</SheetPrimitive.CloseButton>
</SheetPrimitive.Content>
</SheetPortal>
)
}
const SheetHeader: Component<ComponentProps<"div">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<div class={cn("flex flex-col space-y-2 text-center sm:text-left", local.class)} {...others} />
)
}
const SheetFooter: Component<ComponentProps<"div">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<div
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", local.class)}
{...others}
/>
)
}
type DialogTitleProps<T extends ValidComponent = "h2"> = SheetPrimitive.DialogTitleProps<T> & {
class?: string | undefined
}
const SheetTitle = <T extends ValidComponent = "h2">(
props: PolymorphicProps<T, DialogTitleProps<T>>
) => {
const [local, others] = splitProps(props as DialogTitleProps, ["class"])
return (
<SheetPrimitive.Title
class={cn("text-lg font-semibold text-foreground", local.class)}
{...others}
/>
)
}
type DialogDescriptionProps<T extends ValidComponent = "p"> =
SheetPrimitive.DialogDescriptionProps<T> & { class?: string | undefined }
const SheetDescription = <T extends ValidComponent = "p">(
props: PolymorphicProps<T, DialogDescriptionProps<T>>
) => {
const [local, others] = splitProps(props as DialogDescriptionProps, ["class"])
return (
<SheetPrimitive.Description
class={cn("text-sm text-muted-foreground", local.class)}
{...others}
/>
)
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription
}

View File

@@ -0,0 +1,691 @@
import type { Accessor, Component, ComponentProps, JSX, ValidComponent } from "solid-js"
import {
createContext,
createEffect,
createMemo,
createSignal,
Match,
mergeProps,
onCleanup,
Show,
splitProps,
Switch,
useContext
} from "solid-js"
import type { PolymorphicProps } from "@kobalte/core"
import { Polymorphic } from "@kobalte/core"
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
import { cn } from "@/lib/utils"
import type { ButtonProps } from "@/components/ui/button"
import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import { Sheet, SheetContent } from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import { TextField, TextFieldInput } from "@/components/ui/text-field"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
const MOBILE_BREAKPOINT = 768
const SIDEBAR_COOKIE_NAME = "sidebar:state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "10rem"
const SIDEBAR_WIDTH_MOBILE = "10rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
type SidebarContext = {
state: Accessor<"expanded" | "collapsed">
open: Accessor<boolean>
setOpen: (open: boolean) => void
openMobile: Accessor<boolean>
setOpenMobile: (open: boolean) => void
isMobile: Accessor<boolean>
toggleSidebar: () => void
}
const SidebarContext = createContext<SidebarContext | null>(null)
function useSidebar() {
const context = useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a Sidebar.")
}
return context
}
export function useIsMobile(fallback = false) {
const [isMobile, setIsMobile] = createSignal(fallback)
createEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
onCleanup(() => mql.removeEventListener("change", onChange))
})
return isMobile
}
type SidebarProviderProps = Omit<ComponentProps<"div">, "style"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
style?: JSX.CSSProperties
}
const SidebarProvider: Component<SidebarProviderProps> = (rawProps) => {
const props = mergeProps({ defaultOpen: true }, rawProps)
const [local, others] = splitProps(props, [
"defaultOpen",
"open",
"onOpenChange",
"class",
"style",
"children"
])
const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = createSignal(false)
// This is the internal state of the sidebar.
// We use open and onOpenChange for control from outside the component.
const [_open, _setOpen] = createSignal(local.defaultOpen)
const open = () => local.open ?? _open()
const setOpen = (value: boolean | ((value: boolean) => boolean)) => {
if (local.onOpenChange) {
return local.onOpenChange?.(typeof value === "function" ? value(open()) : value)
}
_setOpen(value)
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open()}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
}
// Helper to toggle the sidebar.
const toggleSidebar = () => {
return isMobile() ? setOpenMobile((open) => !open) : setOpen((open) => !open)
}
// Adds a keyboard shortcut to toggle the sidebar.
createEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
event.preventDefault()
toggleSidebar()
}
}
window.addEventListener("keydown", handleKeyDown)
onCleanup(() => window.removeEventListener("keydown", handleKeyDown))
})
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = () => (open() ? "expanded" : "collapsed")
const contextValue = {
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar
}
return (
<SidebarContext.Provider value={contextValue}>
<div
style={{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...local.style
}}
class={cn(
"group/sidebar-wrapper flex min-h-svh w-full text-sidebar-foreground has-[[data-variant=inset]]:bg-sidebar",
local.class
)}
{...others}
>
{local.children}
</div>
</SidebarContext.Provider>
)
}
type SidebarProps = ComponentProps<"div"> & {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
}
const Sidebar: Component<SidebarProps> = (rawProps) => {
const props = mergeProps<SidebarProps[]>(
{
side: "left",
variant: "sidebar",
collapsible: "offcanvas"
},
rawProps
)
const [local, others] = splitProps(props, ["side", "variant", "collapsible", "class", "children"])
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
return (
<Switch>
<Match when={local.collapsible === "none"}>
<div
class={cn(
"test flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
local.class
)}
{...others}
>
{local.children}
</div>
</Match>
<Match when={isMobile()}>
<Sheet open={openMobile()} onOpenChange={setOpenMobile} {...others}>
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
class="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE
}}
position={local.side}
>
<div class="flex size-full flex-col">{local.children}</div>
</SheetContent>
</Sheet>
</Match>
<Match when={!isMobile()}>
<div
class="group peer hidden md:block"
data-state={state()}
data-collapsible={state() === "collapsed" ? local.collapsible : ""}
data-variant={local.variant}
data-side={local.side}
>
{/* This is what handles the sidebar gap on desktop */}
<div
class={cn(
"relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
local.variant === "floating" || local.variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
)}
/>
<div
class={cn(
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
local.side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
local.variant === "floating" || local.variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
local.class
)}
{...others}
>
<div
data-sidebar="sidebar"
class="flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
>
{local.children}
</div>
</div>
</div>
</Match>
</Switch>
)
}
type SidebarTriggerProps<T extends ValidComponent = "button"> = ButtonProps<T> & {
onClick?: (event: MouseEvent) => void
}
const SidebarTrigger = <T extends ValidComponent = "button">(props: SidebarTriggerProps<T>) => {
const [local, others] = splitProps(props as SidebarTriggerProps, ["class", "onClick"])
const { toggleSidebar } = useSidebar()
return (
<Button
data-sidebar="trigger"
variant="ghost"
size="icon"
class={cn("size-7", local.class)}
onClick={(event: MouseEvent) => {
local.onClick?.(event)
toggleSidebar()
}}
{...others}
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4"
>
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M9 3v18" />
</svg>
<span class="sr-only">Toggle Sidebar</span>
</Button>
)
}
const SidebarRail: Component<ComponentProps<"button">> = (props) => {
const [local, others] = splitProps(props, ["class"])
const { toggleSidebar } = useSidebar()
return (
<button
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
class={cn(
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
local.class
)}
{...others}
/>
)
}
const SidebarInset: Component<ComponentProps<"main">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<main
class={cn(
"relative flex min-h-svh flex-1 flex-col bg-background",
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
local.class
)}
{...others}
/>
)
}
type SidebarInputProps<T extends ValidComponent = "input"> = ComponentProps<
typeof TextFieldInput<T>
>
const SidebarInput = <T extends ValidComponent = "input">(props: SidebarInputProps<T>) => {
const [local, others] = splitProps(props as SidebarInputProps, ["class"])
return (
<TextField>
<TextFieldInput
data-sidebar="input"
class={cn(
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
local.class
)}
{...others}
/>
</TextField>
)
}
const SidebarHeader: Component<ComponentProps<"div">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<div data-sidebar="header" class={cn("flex flex-col gap-2 p-2", local.class)} {...others} />
)
}
const SidebarFooter: Component<ComponentProps<"div">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<div data-sidebar="footer" class={cn("flex flex-col gap-2 p-2", local.class)} {...others} />
)
}
type SidebarSeparatorProps<T extends ValidComponent = "hr"> = ComponentProps<typeof Separator<T>>
const SidebarSeparator = <T extends ValidComponent = "hr">(props: SidebarSeparatorProps<T>) => {
const [local, others] = splitProps(props as SidebarSeparatorProps, ["class"])
return (
<Separator
data-sidebar="separator"
class={cn("mx-2 w-auto bg-sidebar-border", local.class)}
{...others}
/>
)
}
const SidebarContent: Component<ComponentProps<"div">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<div
data-sidebar="content"
class={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
local.class
)}
{...others}
/>
)
}
const SidebarGroup: Component<ComponentProps<"div">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<div
data-sidebar="group"
class={cn("relative flex w-full min-w-0 flex-col p-2", local.class)}
{...others}
/>
)
}
type SidebarGroupLabelProps<T extends ValidComponent = "div"> = ComponentProps<T>
const SidebarGroupLabel = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, SidebarGroupLabelProps<T>>
) => {
const [local, others] = splitProps(props as SidebarGroupLabelProps, ["class"])
return (
<Polymorphic<SidebarGroupLabelProps>
as="div"
data-sidebar="group-label"
class={cn(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
local.class
)}
{...others}
/>
)
}
type SidebarGroupActionProps<T extends ValidComponent = "button"> = ComponentProps<T>
const SidebarGroupAction = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, SidebarGroupActionProps<T>>
) => {
const [local, others] = splitProps(props as SidebarGroupActionProps, ["class"])
return (
<Polymorphic<SidebarGroupActionProps>
as="button"
data-sidebar="group-action"
class={cn(
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden",
local.class
)}
{...others}
/>
)
}
const SidebarGroupContent: Component<ComponentProps<"div">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return <div data-sidebar="group-content" class={cn("w-full text-sm", local.class)} {...others} />
}
const SidebarMenu: Component<ComponentProps<"ul">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<ul
data-sidebar="menu"
class={cn("flex w-full min-w-0 flex-col gap-1", local.class)}
{...others}
/>
)
}
const SidebarMenuItem: Component<ComponentProps<"li">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<li data-sidebar="menu-item" class={cn("group/menu-item relative", local.class)} {...others} />
)
}
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]"
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0"
}
},
defaultVariants: {
variant: "default",
size: "default"
}
}
)
type SidebarMenuButtonProps<T extends ValidComponent = "button"> = ComponentProps<T> &
VariantProps<typeof sidebarMenuButtonVariants> & {
isActive?: boolean
tooltip?: string
}
const SidebarMenuButton = <T extends ValidComponent = "button">(
rawProps: PolymorphicProps<T, SidebarMenuButtonProps<T>>
) => {
const props = mergeProps({ isActive: false, variant: "default", size: "default" }, rawProps)
const [local, others] = splitProps(props as SidebarMenuButtonProps, [
"isActive",
"tooltip",
"variant",
"size",
"class"
])
const { isMobile, state } = useSidebar()
const button = (
<Polymorphic<SidebarMenuButtonProps>
as="button"
data-sidebar="menu-button"
data-size={local.size}
data-active={local.isActive}
class={cn(
sidebarMenuButtonVariants({ variant: local.variant, size: local.size }),
local.class
)}
{...others}
/>
)
return (
<Show when={local.tooltip} fallback={button}>
<Tooltip placement="right">
<TooltipTrigger class="w-full">{button}</TooltipTrigger>
<TooltipContent hidden={state() !== "collapsed" || isMobile()}>
{local.tooltip}
</TooltipContent>
</Tooltip>
</Show>
)
}
type SidebarMenuActionProps<T extends ValidComponent = "button"> = ComponentProps<T> & {
showOnHover?: boolean
}
const SidebarMenuAction = <T extends ValidComponent = "button">(
rawProps: PolymorphicProps<T, SidebarMenuActionProps<T>>
) => {
const props = mergeProps({ showOnHover: false }, rawProps)
const [local, others] = splitProps(props as SidebarMenuActionProps, ["class", "showOnHover"])
return (
<Polymorphic<SidebarMenuActionProps>
as="button"
data-sidebar="menu-action"
class={cn(
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
local.showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
local.class
)}
{...others}
/>
)
}
const SidebarMenuBadge: Component<ComponentProps<"div">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<div
data-sidebar="menu-badge"
class={cn(
"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
local.class
)}
{...others}
/>
)
}
type SidebarMenuSkeletonProps = ComponentProps<"div"> & {
showIcon?: boolean
}
const SidebarMenuSkeleton: Component<SidebarMenuSkeletonProps> = (rawProps) => {
const props = mergeProps({ showIcon: false }, rawProps)
const [local, others] = splitProps(props, ["class", "showIcon"])
// Random width between 50 to 90%.
const width = createMemo(() => `${Math.floor(Math.random() * 40) + 50}%`)
return (
<div
data-sidebar="menu-skeleton"
class={cn("flex h-8 items-center gap-2 rounded-md px-2", local.class)}
{...others}
>
{local.showIcon && <Skeleton class="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />}
<Skeleton
class="h-4 max-w-[--skeleton-width] flex-1"
data-sidebar="menu-skeleton-text"
style={{
"--skeleton-width": width()
}}
/>
</div>
)
}
const SidebarMenuSub: Component<ComponentProps<"ul">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<ul
data-sidebar="menu-sub"
class={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
local.class
)}
{...others}
/>
)
}
const SidebarMenuSubItem: Component<ComponentProps<"li">> = (props) => <li {...props} />
type SidebarMenuSubButtonProps<T extends ValidComponent = "a"> = ComponentProps<T> & {
size?: "sm" | "md"
isActive?: boolean
}
const SidebarMenuSubButton = <T extends ValidComponent = "a">(
rawProps: PolymorphicProps<T, SidebarMenuSubButtonProps<T>>
) => {
const props = mergeProps({ size: "md" }, rawProps)
const [local, others] = splitProps(props as SidebarMenuSubButtonProps, [
"size",
"isActive",
"class"
])
return (
<Polymorphic<SidebarMenuSubButtonProps>
as="a"
data-sidebar="menu-sub-button"
data-size={local.size}
data-active={local.isActive}
class={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
local.size === "sm" && "text-xs",
local.size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
local.class
)}
{...others}
/>
)
}
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar
}

View File

@@ -0,0 +1,24 @@
import type { ValidComponent } from "solid-js"
import { splitProps } from "solid-js"
import type { PolymorphicProps } from "@kobalte/core/polymorphic"
import * as SkeletonPrimitive from "@kobalte/core/skeleton"
import { cn } from "@/lib/utils"
type SkeletonRootProps<T extends ValidComponent = "div"> =
SkeletonPrimitive.SkeletonRootProps<T> & { class?: string | undefined }
const Skeleton = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, SkeletonRootProps<T>>
) => {
const [local, others] = splitProps(props as SkeletonRootProps, ["class"])
return (
<SkeletonPrimitive.Root
class={cn("bg-primary/10 data-[animate='true']:animate-pulse", local.class)}
{...others}
/>
)
}
export { Skeleton }

View File

@@ -0,0 +1,152 @@
import type { ValidComponent } from "solid-js"
import { mergeProps, splitProps } from "solid-js"
import type { PolymorphicProps } from "@kobalte/core"
import * as TextFieldPrimitive from "@kobalte/core/text-field"
import { cva } from "class-variance-authority"
import { cn } from "@/lib/utils"
type TextFieldRootProps<T extends ValidComponent = "div"> =
TextFieldPrimitive.TextFieldRootProps<T> & {
class?: string | undefined
}
const TextField = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, TextFieldRootProps<T>>
) => {
const [local, others] = splitProps(props as TextFieldRootProps, ["class"])
return <TextFieldPrimitive.Root class={cn("flex flex-col gap-1", local.class)} {...others} />
}
type TextFieldInputProps<T extends ValidComponent = "input"> =
TextFieldPrimitive.TextFieldInputProps<T> & {
class?: string | undefined
type?:
| "button"
| "checkbox"
| "color"
| "date"
| "datetime-local"
| "email"
| "file"
| "hidden"
| "image"
| "month"
| "number"
| "password"
| "radio"
| "range"
| "reset"
| "search"
| "submit"
| "tel"
| "text"
| "time"
| "url"
| "week"
}
const TextFieldInput = <T extends ValidComponent = "input">(
rawProps: PolymorphicProps<T, TextFieldInputProps<T>>
) => {
const props = mergeProps<TextFieldInputProps<T>[]>({ type: "text" }, rawProps)
const [local, others] = splitProps(props as TextFieldInputProps, ["type", "class"])
return (
<TextFieldPrimitive.Input
type={local.type}
class={cn(
"flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[invalid]:border-error-foreground data-[invalid]:text-error-foreground",
local.class
)}
{...others}
/>
)
}
type TextFieldTextAreaProps<T extends ValidComponent = "textarea"> =
TextFieldPrimitive.TextFieldTextAreaProps<T> & { class?: string | undefined }
const TextFieldTextArea = <T extends ValidComponent = "textarea">(
props: PolymorphicProps<T, TextFieldTextAreaProps<T>>
) => {
const [local, others] = splitProps(props as TextFieldTextAreaProps, ["class"])
return (
<TextFieldPrimitive.TextArea
class={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
local.class
)}
{...others}
/>
)
}
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
{
variants: {
variant: {
label: "data-[invalid]:text-destructive",
description: "font-normal text-muted-foreground",
error: "text-xs text-destructive"
}
},
defaultVariants: {
variant: "label"
}
}
)
type TextFieldLabelProps<T extends ValidComponent = "label"> =
TextFieldPrimitive.TextFieldLabelProps<T> & { class?: string | undefined }
const TextFieldLabel = <T extends ValidComponent = "label">(
props: PolymorphicProps<T, TextFieldLabelProps<T>>
) => {
const [local, others] = splitProps(props as TextFieldLabelProps, ["class"])
return <TextFieldPrimitive.Label class={cn(labelVariants(), local.class)} {...others} />
}
type TextFieldDescriptionProps<T extends ValidComponent = "div"> =
TextFieldPrimitive.TextFieldDescriptionProps<T> & {
class?: string | undefined
}
const TextFieldDescription = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, TextFieldDescriptionProps<T>>
) => {
const [local, others] = splitProps(props as TextFieldDescriptionProps, ["class"])
return (
<TextFieldPrimitive.Description
class={cn(labelVariants({ variant: "description" }), local.class)}
{...others}
/>
)
}
type TextFieldErrorMessageProps<T extends ValidComponent = "div"> =
TextFieldPrimitive.TextFieldErrorMessageProps<T> & {
class?: string | undefined
}
const TextFieldErrorMessage = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, TextFieldErrorMessageProps<T>>
) => {
const [local, others] = splitProps(props as TextFieldErrorMessageProps, ["class"])
return (
<TextFieldPrimitive.ErrorMessage
class={cn(labelVariants({ variant: "error" }), local.class)}
{...others}
/>
)
}
export {
TextField,
TextFieldInput,
TextFieldTextArea,
TextFieldLabel,
TextFieldDescription,
TextFieldErrorMessage
}

163
src/components/ui/toast.tsx Normal file
View File

@@ -0,0 +1,163 @@
import type { JSX, ValidComponent } from "solid-js"
import { Match, splitProps, Switch } from "solid-js"
import { Portal } from "solid-js/web"
import type { PolymorphicProps } from "@kobalte/core/polymorphic"
import * as ToastPrimitive from "@kobalte/core/toast"
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
import { cn } from "@/lib/utils"
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--kb-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--kb-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[opened]:animate-in data-[closed]:animate-out data-[swipe=end]:animate-out data-[closed]:fade-out-80 data-[closed]:slide-out-to-right-full data-[opened]:slide-in-from-top-full data-[opened]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
success: "success border-success-foreground bg-success text-success-foreground",
warning: "warning border-warning-foreground bg-warning text-warning-foreground",
error: "error border-error-foreground bg-error text-error-foreground"
}
},
defaultVariants: {
variant: "default"
}
}
)
type ToastVariant = NonNullable<VariantProps<typeof toastVariants>["variant"]>
type ToastListProps<T extends ValidComponent = "ol"> = ToastPrimitive.ToastListProps<T> & {
class?: string | undefined
}
const Toaster = <T extends ValidComponent = "ol">(
props: PolymorphicProps<T, ToastListProps<T>>
) => {
const [local, others] = splitProps(props as ToastListProps, ["class"])
return (
<Portal>
<ToastPrimitive.Region>
<ToastPrimitive.List
class={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse gap-2 p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
local.class
)}
{...others}
/>
</ToastPrimitive.Region>
</Portal>
)
}
type ToastRootProps<T extends ValidComponent = "li"> = ToastPrimitive.ToastRootProps<T> &
VariantProps<typeof toastVariants> & { class?: string | undefined }
const Toast = <T extends ValidComponent = "li">(props: PolymorphicProps<T, ToastRootProps<T>>) => {
const [local, others] = splitProps(props as ToastRootProps, ["class", "variant"])
return (
<ToastPrimitive.Root
class={cn(toastVariants({ variant: local.variant }), local.class)}
{...others}
/>
)
}
type ToastCloseButtonProps<T extends ValidComponent = "button"> =
ToastPrimitive.ToastCloseButtonProps<T> & { class?: string | undefined }
const ToastClose = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, ToastCloseButtonProps<T>>
) => {
const [local, others] = splitProps(props as ToastCloseButtonProps, ["class"])
return (
<ToastPrimitive.CloseButton
class={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-destructive-foreground group-[.error]:text-error-foreground group-[.success]:text-success-foreground group-[.warning]:text-warning-foreground",
local.class
)}
{...others}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4"
>
<path d="M18 6l-12 12" />
<path d="M6 6l12 12" />
</svg>
</ToastPrimitive.CloseButton>
)
}
type ToastTitleProps<T extends ValidComponent = "div"> = ToastPrimitive.ToastTitleProps<T> & {
class?: string | undefined
}
const ToastTitle = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, ToastTitleProps<T>>
) => {
const [local, others] = splitProps(props as ToastTitleProps, ["class"])
return <ToastPrimitive.Title class={cn("text-sm font-semibold", local.class)} {...others} />
}
type ToastDescriptionProps<T extends ValidComponent = "div"> =
ToastPrimitive.ToastDescriptionProps<T> & { class?: string | undefined }
const ToastDescription = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, ToastDescriptionProps<T>>
) => {
const [local, others] = splitProps(props as ToastDescriptionProps, ["class"])
return <ToastPrimitive.Description class={cn("text-sm opacity-90", local.class)} {...others} />
}
function showToast(props: {
title?: JSX.Element
description?: JSX.Element
variant?: ToastVariant
duration?: number
}) {
ToastPrimitive.toaster.show((data) => (
<Toast toastId={data.toastId} variant={props.variant} duration={props.duration}>
<div class="grid gap-1">
{props.title && <ToastTitle>{props.title}</ToastTitle>}
{props.description && <ToastDescription>{props.description}</ToastDescription>}
</div>
<ToastClose />
</Toast>
))
}
function showToastPromise<T, U>(
promise: Promise<T> | (() => Promise<T>),
options: {
loading?: JSX.Element
success?: (data: T) => JSX.Element
error?: (error: U) => JSX.Element
duration?: number
}
) {
const variant: { [key in ToastPrimitive.ToastPromiseState]: ToastVariant } = {
pending: "default",
fulfilled: "success",
rejected: "error"
}
return ToastPrimitive.toaster.promise<T, U>(promise, (props) => (
<Toast toastId={props.toastId} variant={variant[props.state]} duration={options.duration}>
<Switch>
<Match when={props.state === "pending"}>{options.loading}</Match>
<Match when={props.state === "fulfilled"}>{options.success?.(props.data!)}</Match>
<Match when={props.state === "rejected"}>{options.error?.(props.error!)}</Match>
</Switch>
</Toast>
))
}
export { Toaster, Toast, ToastClose, ToastTitle, ToastDescription, showToast, showToastPromise }

View File

@@ -0,0 +1,35 @@
import type { ValidComponent } from "solid-js"
import { splitProps, type Component } from "solid-js"
import type { PolymorphicProps } from "@kobalte/core/polymorphic"
import * as TooltipPrimitive from "@kobalte/core/tooltip"
import { cn } from "@/lib/utils"
const TooltipTrigger = TooltipPrimitive.Trigger
const Tooltip: Component<TooltipPrimitive.TooltipRootProps> = (props) => {
return <TooltipPrimitive.Root gutter={4} {...props} />
}
type TooltipContentProps<T extends ValidComponent = "div"> =
TooltipPrimitive.TooltipContentProps<T> & { class?: string | undefined }
const TooltipContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, TooltipContentProps<T>>
) => {
const [local, others] = splitProps(props as TooltipContentProps, ["class"])
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
class={cn(
"z-50 origin-[var(--kb-popover-content-transform-origin)] overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95",
local.class
)}
{...others}
/>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent }