🎉 创世提交
This commit is contained in:
41
src/components/AppCard/index.tsx
Normal file
41
src/components/AppCard/index.tsx
Normal 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;
|
||||
52
src/components/AppList/index.tsx
Normal file
52
src/components/AppList/index.tsx
Normal 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;
|
||||
53
src/components/HomeCarousel/index.tsx
Normal file
53
src/components/HomeCarousel/index.tsx
Normal 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;
|
||||
40
src/components/ScreenshotCarousel/index.tsx
Normal file
40
src/components/ScreenshotCarousel/index.tsx
Normal 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;
|
||||
114
src/components/Sidebar/index.tsx
Normal file
114
src/components/Sidebar/index.tsx
Normal 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;
|
||||
28
src/components/Sidebar/style.css
Normal file
28
src/components/Sidebar/style.css
Normal 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%;
|
||||
}
|
||||
}
|
||||
85
src/components/TitleBar/index.tsx
Normal file
85
src/components/TitleBar/index.tsx
Normal 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;
|
||||
54
src/components/TitleBar/store.ts
Normal file
54
src/components/TitleBar/store.ts
Normal 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
|
||||
};
|
||||
};
|
||||
95
src/components/ui/base-carousel.tsx
Normal file
95
src/components/ui/base-carousel.tsx
Normal 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;
|
||||
53
src/components/ui/button.tsx
Normal file
53
src/components/ui/button.tsx
Normal 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 }
|
||||
43
src/components/ui/card.tsx
Normal file
43
src/components/ui/card.tsx
Normal 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 }
|
||||
263
src/components/ui/carousel.tsx
Normal file
263
src/components/ui/carousel.tsx
Normal 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 }
|
||||
9
src/components/ui/collapsible.tsx
Normal file
9
src/components/ui/collapsible.tsx
Normal 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 }
|
||||
260
src/components/ui/dropdown-menu.tsx
Normal file
260
src/components/ui/dropdown-menu.tsx
Normal 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
|
||||
}
|
||||
29
src/components/ui/separator.tsx
Normal file
29
src/components/ui/separator.tsx
Normal 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
172
src/components/ui/sheet.tsx
Normal 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
|
||||
}
|
||||
691
src/components/ui/sidebar.tsx
Normal file
691
src/components/ui/sidebar.tsx
Normal 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
|
||||
}
|
||||
24
src/components/ui/skeleton.tsx
Normal file
24
src/components/ui/skeleton.tsx
Normal 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 }
|
||||
152
src/components/ui/text-field.tsx
Normal file
152
src/components/ui/text-field.tsx
Normal 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
163
src/components/ui/toast.tsx
Normal 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 }
|
||||
35
src/components/ui/tooltip.tsx
Normal file
35
src/components/ui/tooltip.tsx
Normal 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 }
|
||||
Reference in New Issue
Block a user