added shadcn and started PROJECT.md for tracking
This commit is contained in:
167
src/lib/components/app-sidebar.svelte
Normal file
167
src/lib/components/app-sidebar.svelte
Normal file
@@ -0,0 +1,167 @@
|
||||
<script lang="ts">
|
||||
import CameraIcon from "@tabler/icons-svelte/icons/camera";
|
||||
import ChartBarIcon from "@tabler/icons-svelte/icons/chart-bar";
|
||||
import DashboardIcon from "@tabler/icons-svelte/icons/dashboard";
|
||||
import DatabaseIcon from "@tabler/icons-svelte/icons/database";
|
||||
import FileAiIcon from "@tabler/icons-svelte/icons/file-ai";
|
||||
import FileDescriptionIcon from "@tabler/icons-svelte/icons/file-description";
|
||||
import FileWordIcon from "@tabler/icons-svelte/icons/file-word";
|
||||
import FolderIcon from "@tabler/icons-svelte/icons/folder";
|
||||
import HelpIcon from "@tabler/icons-svelte/icons/help";
|
||||
import InnerShadowTopIcon from "@tabler/icons-svelte/icons/inner-shadow-top";
|
||||
import ListDetailsIcon from "@tabler/icons-svelte/icons/list-details";
|
||||
import ReportIcon from "@tabler/icons-svelte/icons/report";
|
||||
import SearchIcon from "@tabler/icons-svelte/icons/search";
|
||||
import SettingsIcon from "@tabler/icons-svelte/icons/settings";
|
||||
import UsersIcon from "@tabler/icons-svelte/icons/users";
|
||||
import * as Sidebar from "#lib/components/ui/sidebar/index.js";
|
||||
import NavDocuments from "./nav-documents.svelte";
|
||||
import NavMain from "./nav-main.svelte";
|
||||
import NavSecondary from "./nav-secondary.svelte";
|
||||
import NavUser from "./nav-user.svelte";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
const data = {
|
||||
user: {
|
||||
name: "shadcn",
|
||||
email: "m@example.com",
|
||||
avatar: "/avatars/shadcn.jpg",
|
||||
},
|
||||
navMain: [
|
||||
{
|
||||
title: "Dashboard",
|
||||
url: "#",
|
||||
icon: DashboardIcon,
|
||||
},
|
||||
{
|
||||
title: "Lifecycle",
|
||||
url: "#",
|
||||
icon: ListDetailsIcon,
|
||||
},
|
||||
{
|
||||
title: "Analytics",
|
||||
url: "#",
|
||||
icon: ChartBarIcon,
|
||||
},
|
||||
{
|
||||
title: "Projects",
|
||||
url: "#",
|
||||
icon: FolderIcon,
|
||||
},
|
||||
{
|
||||
title: "Team",
|
||||
url: "#",
|
||||
icon: UsersIcon,
|
||||
},
|
||||
],
|
||||
navClouds: [
|
||||
{
|
||||
title: "Capture",
|
||||
icon: CameraIcon,
|
||||
isActive: true,
|
||||
url: "#",
|
||||
items: [
|
||||
{
|
||||
title: "Active Proposals",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Archived",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Proposal",
|
||||
icon: FileDescriptionIcon,
|
||||
url: "#",
|
||||
items: [
|
||||
{
|
||||
title: "Active Proposals",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Archived",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Prompts",
|
||||
icon: FileAiIcon,
|
||||
url: "#",
|
||||
items: [
|
||||
{
|
||||
title: "Active Proposals",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Archived",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
navSecondary: [
|
||||
{
|
||||
title: "Settings",
|
||||
url: "#",
|
||||
icon: SettingsIcon,
|
||||
},
|
||||
{
|
||||
title: "Get Help",
|
||||
url: "#",
|
||||
icon: HelpIcon,
|
||||
},
|
||||
{
|
||||
title: "Search",
|
||||
url: "#",
|
||||
icon: SearchIcon,
|
||||
},
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
name: "Data Library",
|
||||
url: "#",
|
||||
icon: DatabaseIcon,
|
||||
},
|
||||
{
|
||||
name: "Reports",
|
||||
url: "#",
|
||||
icon: ReportIcon,
|
||||
},
|
||||
{
|
||||
name: "Word Assistant",
|
||||
url: "#",
|
||||
icon: FileWordIcon,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let { ...restProps }: ComponentProps<typeof Sidebar.Root> = $props();
|
||||
</script>
|
||||
|
||||
<Sidebar.Root collapsible="offcanvas" {...restProps}>
|
||||
<Sidebar.Header>
|
||||
<Sidebar.Menu>
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton class="data-[slot=sidebar-menu-button]:!p-1.5">
|
||||
{#snippet child({ props })}
|
||||
<a href="##" {...props}>
|
||||
<InnerShadowTopIcon class="!size-5" />
|
||||
<span class="text-base font-semibold">Acme Inc.</span>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuButton>
|
||||
</Sidebar.MenuItem>
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.Header>
|
||||
<Sidebar.Content>
|
||||
<NavMain items={data.navMain} />
|
||||
<NavDocuments items={data.documents} />
|
||||
<NavSecondary items={data.navSecondary} class="mt-auto" />
|
||||
</Sidebar.Content>
|
||||
<Sidebar.Footer>
|
||||
<NavUser user={data.user} />
|
||||
</Sidebar.Footer>
|
||||
</Sidebar.Root>
|
||||
247
src/lib/components/chart-area-interactive.svelte
Normal file
247
src/lib/components/chart-area-interactive.svelte
Normal file
@@ -0,0 +1,247 @@
|
||||
<script lang="ts">
|
||||
import { scaleUtc } from "d3-scale";
|
||||
import { curveNatural } from "d3-shape";
|
||||
import { Area, AreaChart } from "layerchart";
|
||||
import * as Card from "#lib/components/ui/card/index.js";
|
||||
import * as Chart from "#lib/components/ui/chart/index.js";
|
||||
import * as Select from "#lib/components/ui/select/index.js";
|
||||
import * as ToggleGroup from "#lib/components/ui/toggle-group/index.js";
|
||||
|
||||
const chartData = [
|
||||
{ date: new Date("2024-04-01"), desktop: 222, mobile: 150 },
|
||||
{ date: new Date("2024-04-02"), desktop: 97, mobile: 180 },
|
||||
{ date: new Date("2024-04-03"), desktop: 167, mobile: 120 },
|
||||
{ date: new Date("2024-04-04"), desktop: 242, mobile: 260 },
|
||||
{ date: new Date("2024-04-05"), desktop: 373, mobile: 290 },
|
||||
{ date: new Date("2024-04-06"), desktop: 301, mobile: 340 },
|
||||
{ date: new Date("2024-04-07"), desktop: 245, mobile: 180 },
|
||||
{ date: new Date("2024-04-08"), desktop: 409, mobile: 320 },
|
||||
{ date: new Date("2024-04-09"), desktop: 59, mobile: 110 },
|
||||
{ date: new Date("2024-04-10"), desktop: 261, mobile: 190 },
|
||||
{ date: new Date("2024-04-11"), desktop: 327, mobile: 350 },
|
||||
{ date: new Date("2024-04-12"), desktop: 292, mobile: 210 },
|
||||
{ date: new Date("2024-04-13"), desktop: 342, mobile: 380 },
|
||||
{ date: new Date("2024-04-14"), desktop: 137, mobile: 220 },
|
||||
{ date: new Date("2024-04-15"), desktop: 120, mobile: 170 },
|
||||
{ date: new Date("2024-04-16"), desktop: 138, mobile: 190 },
|
||||
{ date: new Date("2024-04-17"), desktop: 446, mobile: 360 },
|
||||
{ date: new Date("2024-04-18"), desktop: 364, mobile: 410 },
|
||||
{ date: new Date("2024-04-19"), desktop: 243, mobile: 180 },
|
||||
{ date: new Date("2024-04-20"), desktop: 89, mobile: 150 },
|
||||
{ date: new Date("2024-04-21"), desktop: 137, mobile: 200 },
|
||||
{ date: new Date("2024-04-22"), desktop: 224, mobile: 170 },
|
||||
{ date: new Date("2024-04-23"), desktop: 138, mobile: 230 },
|
||||
{ date: new Date("2024-04-24"), desktop: 387, mobile: 290 },
|
||||
{ date: new Date("2024-04-25"), desktop: 215, mobile: 250 },
|
||||
{ date: new Date("2024-04-26"), desktop: 75, mobile: 130 },
|
||||
{ date: new Date("2024-04-27"), desktop: 383, mobile: 420 },
|
||||
{ date: new Date("2024-04-28"), desktop: 122, mobile: 180 },
|
||||
{ date: new Date("2024-04-29"), desktop: 315, mobile: 240 },
|
||||
{ date: new Date("2024-04-30"), desktop: 454, mobile: 380 },
|
||||
{ date: new Date("2024-05-01"), desktop: 165, mobile: 220 },
|
||||
{ date: new Date("2024-05-02"), desktop: 293, mobile: 310 },
|
||||
{ date: new Date("2024-05-03"), desktop: 247, mobile: 190 },
|
||||
{ date: new Date("2024-05-04"), desktop: 385, mobile: 420 },
|
||||
{ date: new Date("2024-05-05"), desktop: 481, mobile: 390 },
|
||||
{ date: new Date("2024-05-06"), desktop: 498, mobile: 520 },
|
||||
{ date: new Date("2024-05-07"), desktop: 388, mobile: 300 },
|
||||
{ date: new Date("2024-05-08"), desktop: 149, mobile: 210 },
|
||||
{ date: new Date("2024-05-09"), desktop: 227, mobile: 180 },
|
||||
{ date: new Date("2024-05-10"), desktop: 293, mobile: 330 },
|
||||
{ date: new Date("2024-05-11"), desktop: 335, mobile: 270 },
|
||||
{ date: new Date("2024-05-12"), desktop: 197, mobile: 240 },
|
||||
{ date: new Date("2024-05-13"), desktop: 197, mobile: 160 },
|
||||
{ date: new Date("2024-05-14"), desktop: 448, mobile: 490 },
|
||||
{ date: new Date("2024-05-15"), desktop: 473, mobile: 380 },
|
||||
{ date: new Date("2024-05-16"), desktop: 338, mobile: 400 },
|
||||
{ date: new Date("2024-05-17"), desktop: 499, mobile: 420 },
|
||||
{ date: new Date("2024-05-18"), desktop: 315, mobile: 350 },
|
||||
{ date: new Date("2024-05-19"), desktop: 235, mobile: 180 },
|
||||
{ date: new Date("2024-05-20"), desktop: 177, mobile: 230 },
|
||||
{ date: new Date("2024-05-21"), desktop: 82, mobile: 140 },
|
||||
{ date: new Date("2024-05-22"), desktop: 81, mobile: 120 },
|
||||
{ date: new Date("2024-05-23"), desktop: 252, mobile: 290 },
|
||||
{ date: new Date("2024-05-24"), desktop: 294, mobile: 220 },
|
||||
{ date: new Date("2024-05-25"), desktop: 201, mobile: 250 },
|
||||
{ date: new Date("2024-05-26"), desktop: 213, mobile: 170 },
|
||||
{ date: new Date("2024-05-27"), desktop: 420, mobile: 460 },
|
||||
{ date: new Date("2024-05-28"), desktop: 233, mobile: 190 },
|
||||
{ date: new Date("2024-05-29"), desktop: 78, mobile: 130 },
|
||||
{ date: new Date("2024-05-30"), desktop: 340, mobile: 280 },
|
||||
{ date: new Date("2024-05-31"), desktop: 178, mobile: 230 },
|
||||
{ date: new Date("2024-06-01"), desktop: 178, mobile: 200 },
|
||||
{ date: new Date("2024-06-02"), desktop: 470, mobile: 410 },
|
||||
{ date: new Date("2024-06-03"), desktop: 103, mobile: 160 },
|
||||
{ date: new Date("2024-06-04"), desktop: 439, mobile: 380 },
|
||||
{ date: new Date("2024-06-05"), desktop: 88, mobile: 140 },
|
||||
{ date: new Date("2024-06-06"), desktop: 294, mobile: 250 },
|
||||
{ date: new Date("2024-06-07"), desktop: 323, mobile: 370 },
|
||||
{ date: new Date("2024-06-08"), desktop: 385, mobile: 320 },
|
||||
{ date: new Date("2024-06-09"), desktop: 438, mobile: 480 },
|
||||
{ date: new Date("2024-06-10"), desktop: 155, mobile: 200 },
|
||||
{ date: new Date("2024-06-11"), desktop: 92, mobile: 150 },
|
||||
{ date: new Date("2024-06-12"), desktop: 492, mobile: 420 },
|
||||
{ date: new Date("2024-06-13"), desktop: 81, mobile: 130 },
|
||||
{ date: new Date("2024-06-14"), desktop: 426, mobile: 380 },
|
||||
{ date: new Date("2024-06-15"), desktop: 307, mobile: 350 },
|
||||
{ date: new Date("2024-06-16"), desktop: 371, mobile: 310 },
|
||||
{ date: new Date("2024-06-17"), desktop: 475, mobile: 520 },
|
||||
{ date: new Date("2024-06-18"), desktop: 107, mobile: 170 },
|
||||
{ date: new Date("2024-06-19"), desktop: 341, mobile: 290 },
|
||||
{ date: new Date("2024-06-20"), desktop: 408, mobile: 450 },
|
||||
{ date: new Date("2024-06-21"), desktop: 169, mobile: 210 },
|
||||
{ date: new Date("2024-06-22"), desktop: 317, mobile: 270 },
|
||||
{ date: new Date("2024-06-23"), desktop: 480, mobile: 530 },
|
||||
{ date: new Date("2024-06-24"), desktop: 132, mobile: 180 },
|
||||
{ date: new Date("2024-06-25"), desktop: 141, mobile: 190 },
|
||||
{ date: new Date("2024-06-26"), desktop: 434, mobile: 380 },
|
||||
{ date: new Date("2024-06-27"), desktop: 448, mobile: 490 },
|
||||
{ date: new Date("2024-06-28"), desktop: 149, mobile: 200 },
|
||||
{ date: new Date("2024-06-29"), desktop: 103, mobile: 160 },
|
||||
{ date: new Date("2024-06-30"), desktop: 446, mobile: 400 },
|
||||
];
|
||||
|
||||
let timeRange = $state("90d");
|
||||
|
||||
const selectedLabel = $derived.by(() => {
|
||||
switch (timeRange) {
|
||||
case "90d":
|
||||
return "Last 3 months";
|
||||
case "30d":
|
||||
return "Last 30 days";
|
||||
case "7d":
|
||||
return "Last 7 days";
|
||||
default:
|
||||
return "Last 3 months";
|
||||
}
|
||||
});
|
||||
|
||||
const filteredData = $derived(
|
||||
chartData.filter((item) => {
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const referenceDate = new Date("2024-06-30");
|
||||
let daysToSubtract = 90;
|
||||
if (timeRange === "30d") {
|
||||
daysToSubtract = 30;
|
||||
} else if (timeRange === "7d") {
|
||||
daysToSubtract = 7;
|
||||
}
|
||||
|
||||
referenceDate.setDate(referenceDate.getDate() - daysToSubtract);
|
||||
return item.date >= referenceDate;
|
||||
})
|
||||
);
|
||||
|
||||
const chartConfig = {
|
||||
desktop: { label: "Desktop", color: "var(--primary)" },
|
||||
mobile: { label: "Mobile", color: "var(--primary)" },
|
||||
} satisfies Chart.ChartConfig;
|
||||
</script>
|
||||
|
||||
<Card.Root class="@container/card">
|
||||
<Card.Header>
|
||||
<Card.Title>Total Visitors</Card.Title>
|
||||
<Card.Description>
|
||||
<span class="hidden @[540px]/card:block"> Total for the last 3 months </span>
|
||||
<span class="@[540px]/card:hidden">Last 3 months</span>
|
||||
</Card.Description>
|
||||
<Card.Action>
|
||||
<ToggleGroup.Root
|
||||
type="single"
|
||||
bind:value={timeRange}
|
||||
variant="outline"
|
||||
class="hidden *:data-[slot=toggle-group-item]:!px-4 @[767px]/card:flex"
|
||||
>
|
||||
<ToggleGroup.Item value="90d">Last 3 months</ToggleGroup.Item>
|
||||
<ToggleGroup.Item value="30d">Last 30 days</ToggleGroup.Item>
|
||||
<ToggleGroup.Item value="7d">Last 7 days</ToggleGroup.Item>
|
||||
</ToggleGroup.Root>
|
||||
<Select.Root type="single" bind:value={timeRange}>
|
||||
<Select.Trigger
|
||||
size="sm"
|
||||
class="flex w-40 **:data-[slot=select-value]:block **:data-[slot=select-value]:truncate @[767px]/card:hidden"
|
||||
aria-label="Select a value"
|
||||
>
|
||||
<span data-slot="select-value">
|
||||
{selectedLabel}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content class="rounded-xl">
|
||||
<Select.Item value="90d" class="rounded-lg">Last 3 months</Select.Item>
|
||||
<Select.Item value="30d" class="rounded-lg">Last 30 days</Select.Item>
|
||||
<Select.Item value="7d" class="rounded-lg">Last 7 days</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</Card.Action>
|
||||
</Card.Header>
|
||||
<Card.Content class="px-2 pt-4 sm:px-6 sm:pt-6">
|
||||
<Chart.Container config={chartConfig} class="aspect-auto h-[250px] w-full">
|
||||
<AreaChart
|
||||
legend
|
||||
data={filteredData}
|
||||
x="date"
|
||||
xScale={scaleUtc()}
|
||||
series={[
|
||||
{
|
||||
key: "mobile",
|
||||
label: "Mobile",
|
||||
color: chartConfig.mobile.color,
|
||||
},
|
||||
{
|
||||
key: "desktop",
|
||||
label: "Desktop",
|
||||
color: chartConfig.desktop.color,
|
||||
},
|
||||
]}
|
||||
seriesLayout="stack"
|
||||
props={{
|
||||
xAxis: {
|
||||
ticks: timeRange === "7d" ? 7 : undefined,
|
||||
format: (v) => {
|
||||
return v.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
},
|
||||
},
|
||||
yAxis: { format: () => "" },
|
||||
}}
|
||||
>
|
||||
{#snippet marks({ context })}
|
||||
<defs>
|
||||
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stop-color="var(--color-desktop)" stop-opacity={1.0} />
|
||||
<stop offset="95%" stop-color="var(--color-desktop)" stop-opacity={0.1} />
|
||||
</linearGradient>
|
||||
<linearGradient id="fillMobile" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stop-color="var(--color-mobile)" stop-opacity={0.8} />
|
||||
<stop offset="95%" stop-color="var(--color-mobile)" stop-opacity={0.1} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{#each context.series.visibleSeries as s (s.key)}
|
||||
<Area
|
||||
seriesKey={s.key}
|
||||
curve={curveNatural}
|
||||
fillOpacity={0.4}
|
||||
line={{ class: "stroke-1" }}
|
||||
motion="tween"
|
||||
{...s.props}
|
||||
fill={s.key === "desktop" ? "url(#fillDesktop)" : "url(#fillMobile)"}
|
||||
/>
|
||||
{/each}
|
||||
{/snippet}
|
||||
{#snippet tooltip()}
|
||||
<Chart.Tooltip
|
||||
labelFormatter={(v: Date) => {
|
||||
return v.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}}
|
||||
indicator="line"
|
||||
/>
|
||||
{/snippet}
|
||||
</AreaChart>
|
||||
</Chart.Container>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
23
src/lib/components/data-table-actions.svelte
Normal file
23
src/lib/components/data-table-actions.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import DotsVerticalIcon from "@tabler/icons-svelte/icons/dots-vertical";
|
||||
import * as DropdownMenu from "#lib/components/ui/dropdown-menu/index.js";
|
||||
import { Button } from "#lib/components/ui/button/index.js";
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger class="flex size-8 text-muted-foreground data-[state=open]:bg-muted">
|
||||
{#snippet child({ props })}
|
||||
<Button variant="ghost" size="icon" {...props}>
|
||||
<DotsVerticalIcon />
|
||||
<span class="sr-only">Open menu</span>
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end" class="w-32">
|
||||
<DropdownMenu.Item>Edit</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>Make a copy</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>Favorite</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item variant="destructive">Delete</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
189
src/lib/components/data-table-cell-viewer.svelte
Normal file
189
src/lib/components/data-table-cell-viewer.svelte
Normal file
@@ -0,0 +1,189 @@
|
||||
<script lang="ts">
|
||||
import TrendingUpIcon from "@tabler/icons-svelte/icons/trending-up";
|
||||
import { scaleUtc } from "d3-scale";
|
||||
import { curveNatural } from "d3-shape";
|
||||
import { AreaChart } from "layerchart";
|
||||
import * as Chart from "#lib/components/ui/chart/index.js";
|
||||
import * as Drawer from "#lib/components/ui/drawer/index.js";
|
||||
import * as Select from "#lib/components/ui/select/index.js";
|
||||
import { IsMobile } from "#lib/hooks/is-mobile.svelte.js";
|
||||
import { Button } from "#lib/components/ui/button/index.js";
|
||||
import { Input } from "#lib/components/ui/input/index.js";
|
||||
import { Label } from "#lib/components/ui/label/index.js";
|
||||
import { Separator } from "#lib/components/ui/separator/index.js";
|
||||
import type { Schema } from "./schemas.js";
|
||||
|
||||
const chartData = [
|
||||
{ date: new Date("2024-01-01"), desktop: 186, mobile: 80 },
|
||||
{ date: new Date("2024-02-01"), desktop: 305, mobile: 200 },
|
||||
{ date: new Date("2024-03-01"), desktop: 237, mobile: 120 },
|
||||
{ date: new Date("2024-04-01"), desktop: 73, mobile: 190 },
|
||||
{ date: new Date("2024-05-01"), desktop: 209, mobile: 130 },
|
||||
{ date: new Date("2024-06-01"), desktop: 214, mobile: 140 },
|
||||
];
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "var(--primary)",
|
||||
},
|
||||
mobile: {
|
||||
label: "Mobile",
|
||||
color: "var(--primary)",
|
||||
},
|
||||
} satisfies Chart.ChartConfig;
|
||||
|
||||
const isMobile = new IsMobile();
|
||||
|
||||
let { item }: { item: Schema } = $props();
|
||||
|
||||
let type = $derived(item.type);
|
||||
let status = $derived(item.status);
|
||||
let reviewer = $derived(item.reviewer);
|
||||
</script>
|
||||
|
||||
<Drawer.Root direction={isMobile.current ? "bottom" : "right"}>
|
||||
<Drawer.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button variant="link" class="w-fit px-0 text-start text-foreground" {...props}>
|
||||
{item.header}
|
||||
</Button>
|
||||
{/snippet}
|
||||
</Drawer.Trigger>
|
||||
<Drawer.Content>
|
||||
<Drawer.Header class="gap-1">
|
||||
<Drawer.Title>{item.header}</Drawer.Title>
|
||||
<Drawer.Description>Showing total visitors for the last 6 months</Drawer.Description>
|
||||
</Drawer.Header>
|
||||
<div class="flex flex-col gap-4 overflow-y-auto px-4 text-sm">
|
||||
{#if !isMobile.current}
|
||||
<Chart.Container config={chartConfig}>
|
||||
<AreaChart
|
||||
data={chartData}
|
||||
x="date"
|
||||
xScale={scaleUtc()}
|
||||
yDomain={[0, 600]}
|
||||
series={[
|
||||
{
|
||||
key: "mobile",
|
||||
label: "Mobile",
|
||||
color: chartConfig.mobile.color,
|
||||
},
|
||||
{
|
||||
key: "desktop",
|
||||
label: "Desktop",
|
||||
color: chartConfig.desktop.color,
|
||||
},
|
||||
]}
|
||||
seriesLayout="stack"
|
||||
props={{
|
||||
area: {
|
||||
curve: curveNatural,
|
||||
fillOpacity: 0.4,
|
||||
line: { class: "stroke-1" },
|
||||
motion: "tween",
|
||||
},
|
||||
xAxis: {
|
||||
format: (v) => v.toLocaleDateString("en-US", { month: "short" }),
|
||||
},
|
||||
yAxis: { ticks: [0, 300, 600] },
|
||||
}}
|
||||
>
|
||||
{#snippet tooltip()}
|
||||
<Chart.Tooltip
|
||||
labelFormatter={(v: Date) => {
|
||||
return v.toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
});
|
||||
}}
|
||||
indicator="dot"
|
||||
/>
|
||||
{/snippet}
|
||||
</AreaChart>
|
||||
</Chart.Container>
|
||||
<Separator />
|
||||
<div class="grid gap-2">
|
||||
<div class="flex gap-2 leading-none font-medium">
|
||||
Trending up by 5.2% this month
|
||||
<TrendingUpIcon class="size-4" />
|
||||
</div>
|
||||
<div class="text-muted-foreground">
|
||||
Showing total visitors for the last 6 months. This is just some random text to test the
|
||||
layout. It spans multiple lines and should wrap around.
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
{/if}
|
||||
<form class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<Label for="header">Header</Label>
|
||||
<Input id="header" value={item.header} />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<Label for="type">Type</Label>
|
||||
<Select.Root type="single" bind:value={type}>
|
||||
<Select.Trigger id="type" class="w-full">
|
||||
{type ?? "Select a type"}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="Table of Contents">Table of Contents</Select.Item>
|
||||
<Select.Item value="Executive Summary">Executive Summary</Select.Item>
|
||||
<Select.Item value="Technical Approach">Technical Approach</Select.Item>
|
||||
<Select.Item value="Design">Design</Select.Item>
|
||||
<Select.Item value="Capabilities">Capabilities</Select.Item>
|
||||
<Select.Item value="Focus Documents">Focus Documents</Select.Item>
|
||||
<Select.Item value="Narrative">Narrative</Select.Item>
|
||||
<Select.Item value="Cover Page">Cover Page</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<Label for="status">Status</Label>
|
||||
<Select.Root type="single" bind:value={status}>
|
||||
<Select.Trigger id="status" class="w-full">
|
||||
{status ?? "Select a status"}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="Done">Done</Select.Item>
|
||||
<Select.Item value="In Progress">In Progress</Select.Item>
|
||||
<Select.Item value="Not Started">Not Started</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<Label for="target">Target</Label>
|
||||
<Input id="target" value={item.target} />
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<Label for="limit">Limit</Label>
|
||||
<Input id="limit" value={item.limit} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<Label for="reviewer">Reviewer</Label>
|
||||
<Select.Root type="single" bind:value={reviewer}>
|
||||
<Select.Trigger id="reviewer" class="w-full">
|
||||
{reviewer ?? "Select a reviewer"}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="Eddie Lake">Eddie Lake</Select.Item>
|
||||
<Select.Item value="Jamik Tashpulatov">Jamik Tashpulatov</Select.Item>
|
||||
<Select.Item value="Emily Whalen">Emily Whalen</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<Drawer.Footer>
|
||||
<Button>Submit</Button>
|
||||
<Drawer.Close>
|
||||
{#snippet child({ props })}
|
||||
<Button variant="outline" {...props}>Done</Button>
|
||||
{/snippet}
|
||||
</Drawer.Close>
|
||||
</Drawer.Footer>
|
||||
</Drawer.Content>
|
||||
</Drawer.Root>
|
||||
14
src/lib/components/data-table-checkbox.svelte
Normal file
14
src/lib/components/data-table-checkbox.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { Checkbox } from "#lib/components/ui/checkbox/index.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
checked = false,
|
||||
onCheckedChange = (v) => (checked = v),
|
||||
...restProps
|
||||
}: ComponentProps<typeof Checkbox> = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex items-center justify-center">
|
||||
<Checkbox bind:checked={() => checked, onCheckedChange} {...restProps} />
|
||||
</div>
|
||||
17
src/lib/components/data-table-drag-handle.svelte
Normal file
17
src/lib/components/data-table-drag-handle.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import GripVerticalIcon from "@tabler/icons-svelte/icons/grip-vertical";
|
||||
import { Button } from "#lib/components/ui/button/index.js";
|
||||
import type { Attachment } from "svelte/attachments";
|
||||
|
||||
let { attach }: { attach?: Attachment } = $props();
|
||||
</script>
|
||||
|
||||
<Button
|
||||
{@attach attach}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="size-7 text-muted-foreground hover:bg-transparent"
|
||||
>
|
||||
<GripVerticalIcon class="size-3 text-muted-foreground" />
|
||||
<span class="sr-only">Drag to reorder</span>
|
||||
</Button>
|
||||
26
src/lib/components/data-table-features.ts
Normal file
26
src/lib/components/data-table-features.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
columnFilteringFeature,
|
||||
columnVisibilityFeature,
|
||||
createFilteredRowModel,
|
||||
createPaginatedRowModel,
|
||||
createSortedRowModel,
|
||||
rowPaginationFeature,
|
||||
rowSelectionFeature,
|
||||
rowSortingFeature,
|
||||
tableFeatures,
|
||||
} from "@tanstack/svelte-table";
|
||||
|
||||
// New in v9: declare the features this table uses — anything you don't
|
||||
// register is tree-shaken out of the bundle.
|
||||
export const features = tableFeatures({
|
||||
columnFilteringFeature,
|
||||
columnVisibilityFeature,
|
||||
rowPaginationFeature,
|
||||
rowSelectionFeature,
|
||||
rowSortingFeature,
|
||||
filteredRowModel: createFilteredRowModel(),
|
||||
paginatedRowModel: createPaginatedRowModel(),
|
||||
sortedRowModel: createSortedRowModel(),
|
||||
});
|
||||
|
||||
export type DashboardTableFeatures = typeof features;
|
||||
1
src/lib/components/data-table-header-limit.svelte
Normal file
1
src/lib/components/data-table-header-limit.svelte
Normal file
@@ -0,0 +1 @@
|
||||
<div class="w-full text-end">Limit</div>
|
||||
1
src/lib/components/data-table-header-target.svelte
Normal file
1
src/lib/components/data-table-header-target.svelte
Normal file
@@ -0,0 +1 @@
|
||||
<div class="w-full text-end">Target</div>
|
||||
28
src/lib/components/data-table-limit.svelte
Normal file
28
src/lib/components/data-table-limit.svelte
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { toast } from "svelte-sonner";
|
||||
import { Input } from "#lib/components/ui/input/index.js";
|
||||
import { Label } from "#lib/components/ui/label/index.js";
|
||||
import type { DashboardTableFeatures } from "./data-table-features.js";
|
||||
import type { Schema } from "./schemas.js";
|
||||
import type { Row } from "@tanstack/svelte-table";
|
||||
|
||||
let { row }: { row: Row<DashboardTableFeatures, Schema> } = $props();
|
||||
</script>
|
||||
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
|
||||
loading: `Saving ${row.original.header}`,
|
||||
success: "Done",
|
||||
error: "Error",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Label for="{row.original.id}-limit" class="sr-only">Limit</Label>
|
||||
<Input
|
||||
class="h-8 w-16 border-transparent bg-transparent text-end shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background dark:bg-transparent dark:hover:bg-input/30 dark:focus-visible:bg-input/30"
|
||||
value={row.original.limit}
|
||||
id="{row.original.id}-limit"
|
||||
/>
|
||||
</form>
|
||||
33
src/lib/components/data-table-reviewer.svelte
Normal file
33
src/lib/components/data-table-reviewer.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import * as Select from "#lib/components/ui/select/index.js";
|
||||
import { Label } from "#lib/components/ui/label/index.js";
|
||||
import type { DashboardTableFeatures } from "./data-table-features.js";
|
||||
import type { Schema } from "./schemas.js";
|
||||
import type { Row } from "@tanstack/svelte-table";
|
||||
|
||||
let { row }: { row: Row<DashboardTableFeatures, Schema> } = $props();
|
||||
|
||||
const isAssigned = $derived(row.original.reviewer !== "Assign reviewer");
|
||||
let reviewer = $state("");
|
||||
</script>
|
||||
|
||||
{#if isAssigned}
|
||||
{row.original.reviewer}
|
||||
{:else}
|
||||
<Label for="{row.original.id}-reviewer" class="sr-only">Reviewer</Label>
|
||||
<Select.Root type="single" bind:value={reviewer}>
|
||||
<Select.Trigger
|
||||
class="w-38 **:data-[slot=select-value]:block **:data-[slot=select-value]:truncate"
|
||||
size="sm"
|
||||
id="{row.original.id}-reviewer"
|
||||
>
|
||||
<span data-slot="select-value">
|
||||
{reviewer !== "" ? reviewer : "Assign reviewer"}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content align="end">
|
||||
<Select.Item value="Eddie Lake">Eddie Lake</Select.Item>
|
||||
<Select.Item value="Jamik Tashpulatov">Jamik Tashpulatov</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
{/if}
|
||||
19
src/lib/components/data-table-status.svelte
Normal file
19
src/lib/components/data-table-status.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import CircleCheckFilledIcon from "@tabler/icons-svelte/icons/circle-check-filled";
|
||||
import LoaderIcon from "@tabler/icons-svelte/icons/loader";
|
||||
import { Badge } from "#lib/components/ui/badge/index.js";
|
||||
import type { DashboardTableFeatures } from "./data-table-features.js";
|
||||
import type { Schema } from "./schemas.js";
|
||||
import type { Row } from "@tanstack/svelte-table";
|
||||
|
||||
let { row }: { row: Row<DashboardTableFeatures, Schema> } = $props();
|
||||
</script>
|
||||
|
||||
<Badge variant="outline" class="px-1.5 text-muted-foreground">
|
||||
{#if row.original.status === "Done"}
|
||||
<CircleCheckFilledIcon class="fill-green-500 dark:fill-green-400" />
|
||||
{:else}
|
||||
<LoaderIcon />
|
||||
{/if}
|
||||
{row.original.status}
|
||||
</Badge>
|
||||
28
src/lib/components/data-table-target.svelte
Normal file
28
src/lib/components/data-table-target.svelte
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { toast } from "svelte-sonner";
|
||||
import { Input } from "#lib/components/ui/input/index.js";
|
||||
import { Label } from "#lib/components/ui/label/index.js";
|
||||
import type { DashboardTableFeatures } from "./data-table-features.js";
|
||||
import type { Schema } from "./schemas.js";
|
||||
import type { Row } from "@tanstack/svelte-table";
|
||||
|
||||
let { row }: { row: Row<DashboardTableFeatures, Schema> } = $props();
|
||||
</script>
|
||||
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
|
||||
loading: `Saving ${row.original.header}`,
|
||||
success: "Done",
|
||||
error: "Error",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Label for="{row.original.id}-target" class="sr-only">Target</Label>
|
||||
<Input
|
||||
class="h-8 w-16 border-transparent bg-transparent text-end shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background dark:bg-transparent dark:hover:bg-input/30 dark:focus-visible:bg-input/30"
|
||||
value={row.original.target}
|
||||
id="{row.original.id}-target"
|
||||
/>
|
||||
</form>
|
||||
14
src/lib/components/data-table-type.svelte
Normal file
14
src/lib/components/data-table-type.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { Badge } from "#lib/components/ui/badge/index.js";
|
||||
import type { DashboardTableFeatures } from "./data-table-features.js";
|
||||
import type { Schema } from "./schemas.js";
|
||||
import type { Row } from "@tanstack/svelte-table";
|
||||
|
||||
let { row }: { row: Row<DashboardTableFeatures, Schema> } = $props();
|
||||
</script>
|
||||
|
||||
<div class="w-32">
|
||||
<Badge variant="outline" class="px-1.5 text-muted-foreground">
|
||||
{row.original.type}
|
||||
</Badge>
|
||||
</div>
|
||||
355
src/lib/components/data-table.svelte
Normal file
355
src/lib/components/data-table.svelte
Normal file
@@ -0,0 +1,355 @@
|
||||
<script lang="ts">
|
||||
import ChevronDownIcon from "@tabler/icons-svelte/icons/chevron-down";
|
||||
import ChevronLeftIcon from "@tabler/icons-svelte/icons/chevron-left";
|
||||
import ChevronRightIcon from "@tabler/icons-svelte/icons/chevron-right";
|
||||
import ChevronsLeftIcon from "@tabler/icons-svelte/icons/chevrons-left";
|
||||
import ChevronsRightIcon from "@tabler/icons-svelte/icons/chevrons-right";
|
||||
import LayoutColumnsIcon from "@tabler/icons-svelte/icons/layout-columns";
|
||||
import PlusIcon from "@tabler/icons-svelte/icons/plus";
|
||||
import { DragDropProvider } from "@dnd-kit-svelte/svelte";
|
||||
import { useSortable } from "@dnd-kit-svelte/svelte/sortable";
|
||||
import { RestrictToVerticalAxis } from "@dnd-kit/abstract/modifiers";
|
||||
import { move } from "@dnd-kit/helpers";
|
||||
import {
|
||||
FlexRender,
|
||||
type RowSelectionState,
|
||||
createColumnHelper,
|
||||
createTable,
|
||||
createTableState,
|
||||
renderComponent,
|
||||
type Row,
|
||||
} from "@tanstack/svelte-table";
|
||||
import * as DropdownMenu from "#lib/components/ui/dropdown-menu/index.js";
|
||||
import * as Select from "#lib/components/ui/select/index.js";
|
||||
import * as Table from "#lib/components/ui/table/index.js";
|
||||
import * as Tabs from "#lib/components/ui/tabs/index.js";
|
||||
import { Badge } from "#lib/components/ui/badge/index.js";
|
||||
import { Button } from "#lib/components/ui/button/index.js";
|
||||
import { Label } from "#lib/components/ui/label/index.js";
|
||||
import DataTableActions from "./data-table-actions.svelte";
|
||||
import DataTableCellViewer from "./data-table-cell-viewer.svelte";
|
||||
import DataTableCheckbox from "./data-table-checkbox.svelte";
|
||||
import DataTableDragHandle from "./data-table-drag-handle.svelte";
|
||||
import DataTableHeaderLimit from "./data-table-header-limit.svelte";
|
||||
import DataTableHeaderTarget from "./data-table-header-target.svelte";
|
||||
import DataTableLimit from "./data-table-limit.svelte";
|
||||
import DataTableReviewer from "./data-table-reviewer.svelte";
|
||||
import DataTableStatus from "./data-table-status.svelte";
|
||||
import DataTableTarget from "./data-table-target.svelte";
|
||||
import DataTableType from "./data-table-type.svelte";
|
||||
import { features, type DashboardTableFeatures } from "./data-table-features.js";
|
||||
import type { Schema } from "./schemas.js";
|
||||
|
||||
let { data }: { data: Schema[] } = $props();
|
||||
|
||||
const columnHelper = createColumnHelper<DashboardTableFeatures, Schema>();
|
||||
|
||||
const columns = columnHelper.columns([
|
||||
columnHelper.display({
|
||||
id: "drag",
|
||||
header: () => null,
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) =>
|
||||
renderComponent(DataTableCheckbox, {
|
||||
checked: table.getIsAllPageRowsSelected(),
|
||||
indeterminate: table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected(),
|
||||
onCheckedChange: (value: boolean) => table.toggleAllPageRowsSelected(!!value),
|
||||
"aria-label": "Select all",
|
||||
}),
|
||||
cell: ({ row }) =>
|
||||
renderComponent(DataTableCheckbox, {
|
||||
checked: row.getIsSelected(),
|
||||
onCheckedChange: (value: boolean) => row.toggleSelected(!!value),
|
||||
"aria-label": "Select row",
|
||||
}),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
}),
|
||||
columnHelper.accessor("header", {
|
||||
header: "Header",
|
||||
cell: ({ row }) => renderComponent(DataTableCellViewer, { item: row.original }),
|
||||
enableHiding: false,
|
||||
}),
|
||||
columnHelper.accessor("type", {
|
||||
header: "Section Type",
|
||||
cell: ({ row }) => renderComponent(DataTableType, { row }),
|
||||
}),
|
||||
columnHelper.accessor("status", {
|
||||
header: "Status",
|
||||
cell: ({ row }) => renderComponent(DataTableStatus, { row }),
|
||||
}),
|
||||
columnHelper.accessor("target", {
|
||||
header: () => renderComponent(DataTableHeaderTarget, {}),
|
||||
cell: ({ row }) => renderComponent(DataTableTarget, { row }),
|
||||
}),
|
||||
columnHelper.accessor("limit", {
|
||||
header: () => renderComponent(DataTableHeaderLimit, {}),
|
||||
cell: ({ row }) => renderComponent(DataTableLimit, { row }),
|
||||
}),
|
||||
columnHelper.accessor("reviewer", {
|
||||
header: "Reviewer",
|
||||
cell: ({ row }) => renderComponent(DataTableReviewer, { row }),
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: () => renderComponent(DataTableActions, {}),
|
||||
}),
|
||||
]);
|
||||
|
||||
// Keep row selection outside the table so the rest of the app can read or update it.
|
||||
const [rowSelection, setRowSelection] = createTableState<RowSelectionState>({});
|
||||
|
||||
// v9 manages the rest of its state internally — reads like
|
||||
// `table.getRowModel()` are rune-reactive, so no `$state` mirrors are needed.
|
||||
const table = createTable({
|
||||
features,
|
||||
get data() {
|
||||
return data;
|
||||
},
|
||||
columns,
|
||||
getRowId: (row) => row.id.toString(),
|
||||
enableRowSelection: true,
|
||||
autoResetPageIndex: false,
|
||||
state: {
|
||||
get rowSelection() {
|
||||
return rowSelection();
|
||||
},
|
||||
},
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const pagination = $derived(table.atoms.pagination.get());
|
||||
|
||||
let views = [
|
||||
{
|
||||
id: "outline",
|
||||
label: "Outline",
|
||||
badge: 0,
|
||||
},
|
||||
{
|
||||
id: "past-performance",
|
||||
label: "Past Performance",
|
||||
badge: 3,
|
||||
},
|
||||
{
|
||||
id: "key-personnel",
|
||||
label: "Key Personnel",
|
||||
badge: 2,
|
||||
},
|
||||
{
|
||||
id: "focus-documents",
|
||||
label: "Focus Documents",
|
||||
badge: 0,
|
||||
},
|
||||
];
|
||||
|
||||
let view = $state("outline");
|
||||
let viewLabel = $derived(views.find((v) => view === v.id)?.label ?? "Select a view");
|
||||
</script>
|
||||
|
||||
<Tabs.Root value="outline" class="w-full flex-col justify-start gap-6">
|
||||
<div class="flex items-center justify-between px-4 lg:px-6">
|
||||
<Label for="view-selector" class="sr-only">View</Label>
|
||||
<Select.Root type="single" bind:value={view}>
|
||||
<Select.Trigger class="flex w-fit @4xl/main:hidden" size="sm" id="view-selector">
|
||||
{viewLabel}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each views as view (view.id)}
|
||||
<Select.Item value={view.id}>{view.label}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<Tabs.List
|
||||
class="hidden **:data-[slot=badge]:size-5 **:data-[slot=badge]:rounded-full **:data-[slot=badge]:bg-muted-foreground/30 **:data-[slot=badge]:px-1 @4xl/main:flex"
|
||||
>
|
||||
{#each views as view (view.id)}
|
||||
<Tabs.Trigger value={view.id}>
|
||||
{view.label}
|
||||
{#if view.badge > 0}
|
||||
<Badge variant="secondary">{view.badge}</Badge>
|
||||
{/if}
|
||||
</Tabs.Trigger>
|
||||
{/each}
|
||||
</Tabs.List>
|
||||
<div class="flex items-center gap-2">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button variant="outline" size="sm" {...props}>
|
||||
<LayoutColumnsIcon />
|
||||
<span class="hidden lg:inline">Customize Columns</span>
|
||||
<span class="lg:hidden">Columns</span>
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end" class="w-56">
|
||||
{#each table
|
||||
.getAllColumns()
|
||||
.filter((col) => typeof col.accessorFn !== "undefined" && col.getCanHide()) as column (column.id)}
|
||||
<DropdownMenu.CheckboxItem
|
||||
class="capitalize"
|
||||
checked={column.getIsVisible()}
|
||||
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||
>
|
||||
{column.id}
|
||||
</DropdownMenu.CheckboxItem>
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
<Button variant="outline" size="sm">
|
||||
<PlusIcon />
|
||||
<span class="hidden lg:inline">Add Section</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs.Content value="outline" class="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6">
|
||||
<div class="overflow-hidden rounded-lg border">
|
||||
<DragDropProvider
|
||||
modifiers={[
|
||||
// @ts-expect-error @dnd-kit/abstract types are botched atm
|
||||
RestrictToVerticalAxis,
|
||||
]}
|
||||
onDragEnd={(e) => (data = move(data, e))}
|
||||
>
|
||||
<Table.Root>
|
||||
<Table.Header class="sticky top-0 z-10 bg-muted">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<Table.Head colspan={header.colSpan}>
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender {header} />
|
||||
{/if}
|
||||
</Table.Head>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Header>
|
||||
<Table.Body class="**:data-[slot=table-cell]:first:w-8">
|
||||
{#if table.getRowModel().rows?.length}
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
{@render DraggableRow({ row })}
|
||||
{/each}
|
||||
{:else}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-24 text-center">
|
||||
No results.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</DragDropProvider>
|
||||
</div>
|
||||
<div class="flex items-center justify-between px-4">
|
||||
<div class="hidden flex-1 text-sm text-muted-foreground lg:flex">
|
||||
{table.getFilteredSelectedRowModel().rows.length} of
|
||||
{table.getFilteredRowModel().rows.length} row(s) selected.
|
||||
</div>
|
||||
<div class="flex w-full items-center gap-8 lg:w-fit">
|
||||
<div class="hidden items-center gap-2 lg:flex">
|
||||
<Label for="rows-per-page" class="text-sm font-medium">Rows per page</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
bind:value={() => `${pagination.pageSize}`, (v) => table.setPageSize(Number(v))}
|
||||
>
|
||||
<Select.Trigger size="sm" class="w-20" id="rows-per-page">
|
||||
{pagination.pageSize}
|
||||
</Select.Trigger>
|
||||
<Select.Content side="top">
|
||||
{#each [10, 20, 30, 40, 50] as pageSize (pageSize)}
|
||||
<Select.Item value={pageSize.toString()}>
|
||||
{pageSize}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
<div class="flex w-fit items-center justify-center text-sm font-medium">
|
||||
Page {pagination.pageIndex + 1} of
|
||||
{table.getPageCount()}
|
||||
</div>
|
||||
<div class="ms-auto flex items-center gap-2 lg:ms-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
class="hidden h-8 w-8 p-0 lg:flex"
|
||||
onclick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span class="sr-only">Go to first page</span>
|
||||
<ChevronsLeftIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="size-8"
|
||||
size="icon"
|
||||
onclick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span class="sr-only">Go to previous page</span>
|
||||
<ChevronLeftIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="size-8"
|
||||
size="icon"
|
||||
onclick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span class="sr-only">Go to next page</span>
|
||||
<ChevronRightIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="hidden size-8 lg:flex"
|
||||
size="icon"
|
||||
onclick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span class="sr-only">Go to last page</span>
|
||||
<ChevronsRightIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="past-performance" class="flex flex-col px-4 lg:px-6">
|
||||
<div class="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="key-personnel" class="flex flex-col px-4 lg:px-6">
|
||||
<div class="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="focus-documents" class="flex flex-col px-4 lg:px-6">
|
||||
<div class="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
|
||||
{#snippet DraggableRow({ row }: { row: Row<DashboardTableFeatures, Schema> })}
|
||||
{@const { ref, isDragging, handleRef } = useSortable({
|
||||
id: row.original.id,
|
||||
index: () => row.index,
|
||||
})}
|
||||
|
||||
<Table.Row
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
data-dragging={isDragging.current}
|
||||
class="relative z-0 data-[dragging=true]:z-10 data-[dragging=true]:opacity-80"
|
||||
{@attach ref}
|
||||
>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<Table.Cell>
|
||||
{#if cell.column.id === "drag"}
|
||||
<!-- The drag handle needs this row's sortable handleRef, so it renders
|
||||
here instead of through the column def. -->
|
||||
<DataTableDragHandle attach={handleRef} />
|
||||
{:else}
|
||||
<FlexRender {cell} />
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{/snippet}
|
||||
70
src/lib/components/nav-documents.svelte
Normal file
70
src/lib/components/nav-documents.svelte
Normal file
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import DotsIcon from "@tabler/icons-svelte/icons/dots";
|
||||
import FolderIcon from "@tabler/icons-svelte/icons/folder";
|
||||
import Share3Icon from "@tabler/icons-svelte/icons/share-3";
|
||||
import TrashIcon from "@tabler/icons-svelte/icons/trash";
|
||||
import * as DropdownMenu from "#lib/components/ui/dropdown-menu/index.js";
|
||||
import * as Sidebar from "#lib/components/ui/sidebar/index.js";
|
||||
import type { Icon } from "@tabler/icons-svelte";
|
||||
|
||||
let { items }: { items: { name: string; url: string; icon: Icon }[] } = $props();
|
||||
|
||||
const sidebar = Sidebar.useSidebar();
|
||||
</script>
|
||||
|
||||
<Sidebar.Group class="group-data-[collapsible=icon]:hidden">
|
||||
<Sidebar.GroupLabel>Documents</Sidebar.GroupLabel>
|
||||
<Sidebar.Menu>
|
||||
{#each items as item (item.name)}
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton>
|
||||
{#snippet child({ props })}
|
||||
<a {...props} href={item.url}>
|
||||
<item.icon />
|
||||
<span>{item.name}</span>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuButton>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Sidebar.MenuAction
|
||||
{...props}
|
||||
showOnHover
|
||||
class="rounded-sm data-[state=open]:bg-accent"
|
||||
>
|
||||
<DotsIcon />
|
||||
<span class="sr-only">More</span>
|
||||
</Sidebar.MenuAction>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
class="w-24 rounded-lg"
|
||||
side={sidebar.isMobile ? "bottom" : "right"}
|
||||
align={sidebar.isMobile ? "end" : "start"}
|
||||
>
|
||||
<DropdownMenu.Item>
|
||||
<FolderIcon />
|
||||
<span>Open</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<Share3Icon />
|
||||
<span>Share</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item variant="destructive">
|
||||
<TrashIcon />
|
||||
<span>Delete</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Sidebar.MenuItem>
|
||||
{/each}
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton class="text-sidebar-foreground/70">
|
||||
<DotsIcon class="text-sidebar-foreground/70" />
|
||||
<span>More</span>
|
||||
</Sidebar.MenuButton>
|
||||
</Sidebar.MenuItem>
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.Group>
|
||||
45
src/lib/components/nav-main.svelte
Normal file
45
src/lib/components/nav-main.svelte
Normal file
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import CirclePlusFilledIcon from "@tabler/icons-svelte/icons/circle-plus-filled";
|
||||
import MailIcon from "@tabler/icons-svelte/icons/mail";
|
||||
import * as Sidebar from "#lib/components/ui/sidebar/index.js";
|
||||
import { Button } from "#lib/components/ui/button/index.js";
|
||||
import type { Icon } from "@tabler/icons-svelte";
|
||||
|
||||
let { items }: { items: { title: string; url: string; icon?: Icon }[] } = $props();
|
||||
</script>
|
||||
|
||||
<Sidebar.Group>
|
||||
<Sidebar.GroupContent class="flex flex-col gap-2">
|
||||
<Sidebar.Menu>
|
||||
<Sidebar.MenuItem class="flex items-center gap-2">
|
||||
<Sidebar.MenuButton
|
||||
class="min-w-8 bg-primary text-primary-foreground duration-200 ease-linear hover:bg-primary/90 hover:text-primary-foreground active:bg-primary/90 active:text-primary-foreground"
|
||||
tooltipContent="Quick create"
|
||||
>
|
||||
<CirclePlusFilledIcon />
|
||||
<span>Quick Create</span>
|
||||
</Sidebar.MenuButton>
|
||||
<Button
|
||||
size="icon"
|
||||
class="size-8 group-data-[collapsible=icon]:opacity-0"
|
||||
variant="outline"
|
||||
>
|
||||
<MailIcon />
|
||||
<span class="sr-only">Inbox</span>
|
||||
</Button>
|
||||
</Sidebar.MenuItem>
|
||||
</Sidebar.Menu>
|
||||
<Sidebar.Menu>
|
||||
{#each items as item (item.title)}
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton tooltipContent={item.title}>
|
||||
{#if item.icon}
|
||||
<item.icon />
|
||||
{/if}
|
||||
<span>{item.title}</span>
|
||||
</Sidebar.MenuButton>
|
||||
</Sidebar.MenuItem>
|
||||
{/each}
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.GroupContent>
|
||||
</Sidebar.Group>
|
||||
32
src/lib/components/nav-secondary.svelte
Normal file
32
src/lib/components/nav-secondary.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import * as Sidebar from "#lib/components/ui/sidebar/index.js";
|
||||
import type { WithoutChildren } from "#lib/utils.js";
|
||||
import type { Icon } from "@tabler/icons-svelte";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
items,
|
||||
...restProps
|
||||
}: { items: { title: string; url: string; icon: Icon }[] } & WithoutChildren<
|
||||
ComponentProps<typeof Sidebar.Group>
|
||||
> = $props();
|
||||
</script>
|
||||
|
||||
<Sidebar.Group {...restProps}>
|
||||
<Sidebar.GroupContent>
|
||||
<Sidebar.Menu>
|
||||
{#each items as item (item.title)}
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton>
|
||||
{#snippet child({ props })}
|
||||
<a href={item.url} {...props}>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuButton>
|
||||
</Sidebar.MenuItem>
|
||||
{/each}
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.GroupContent>
|
||||
</Sidebar.Group>
|
||||
83
src/lib/components/nav-user.svelte
Normal file
83
src/lib/components/nav-user.svelte
Normal file
@@ -0,0 +1,83 @@
|
||||
<script lang="ts">
|
||||
import CreditCardIcon from "@tabler/icons-svelte/icons/credit-card";
|
||||
import DotsVerticalIcon from "@tabler/icons-svelte/icons/dots-vertical";
|
||||
import LogoutIcon from "@tabler/icons-svelte/icons/logout";
|
||||
import NotificationIcon from "@tabler/icons-svelte/icons/notification";
|
||||
import UserCircleIcon from "@tabler/icons-svelte/icons/user-circle";
|
||||
import * as Avatar from "#lib/components/ui/avatar/index.js";
|
||||
import * as DropdownMenu from "#lib/components/ui/dropdown-menu/index.js";
|
||||
import * as Sidebar from "#lib/components/ui/sidebar/index.js";
|
||||
|
||||
let { user }: { user: { name: string; email: string; avatar: string } } = $props();
|
||||
|
||||
const sidebar = Sidebar.useSidebar();
|
||||
</script>
|
||||
|
||||
<Sidebar.Menu>
|
||||
<Sidebar.MenuItem>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Sidebar.MenuButton
|
||||
{...props}
|
||||
size="lg"
|
||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<Avatar.Root class="size-8 rounded-lg grayscale">
|
||||
<Avatar.Image src={user.avatar} alt={user.name} />
|
||||
<Avatar.Fallback class="rounded-lg">CN</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div class="grid flex-1 text-start text-sm leading-tight">
|
||||
<span class="truncate font-medium">{user.name}</span>
|
||||
<span class="truncate text-xs text-muted-foreground">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
<DotsVerticalIcon class="ms-auto size-4" />
|
||||
</Sidebar.MenuButton>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
class="w-(--bits-dropdown-menu-anchor-width) min-w-56 rounded-lg"
|
||||
side={sidebar.isMobile ? "bottom" : "right"}
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenu.Label class="p-0 font-normal">
|
||||
<div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
|
||||
<Avatar.Root class="size-8 rounded-lg">
|
||||
<Avatar.Image src={user.avatar} alt={user.name} />
|
||||
<Avatar.Fallback class="rounded-lg">CN</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div class="grid flex-1 text-start text-sm leading-tight">
|
||||
<span class="truncate font-medium">{user.name}</span>
|
||||
<span class="truncate text-xs text-muted-foreground">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item>
|
||||
<UserCircleIcon />
|
||||
Account
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<CreditCardIcon />
|
||||
Billing
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<NotificationIcon />
|
||||
Notifications
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item>
|
||||
<LogoutIcon />
|
||||
Log out
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Sidebar.MenuItem>
|
||||
</Sidebar.Menu>
|
||||
13
src/lib/components/schemas.ts
Normal file
13
src/lib/components/schemas.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const schema = z.object({
|
||||
id: z.number(),
|
||||
header: z.string(),
|
||||
type: z.string(),
|
||||
status: z.string(),
|
||||
target: z.string(),
|
||||
limit: z.string(),
|
||||
reviewer: z.string(),
|
||||
});
|
||||
|
||||
export type Schema = z.infer<typeof schema>;
|
||||
91
src/lib/components/section-cards.svelte
Normal file
91
src/lib/components/section-cards.svelte
Normal file
@@ -0,0 +1,91 @@
|
||||
<script lang="ts">
|
||||
import TrendingDownIcon from "@tabler/icons-svelte/icons/trending-down";
|
||||
import TrendingUpIcon from "@tabler/icons-svelte/icons/trending-up";
|
||||
import * as Card from "#lib/components/ui/card/index.js";
|
||||
import { Badge } from "#lib/components/ui/badge/index.js";
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card *:data-[slot=card]:shadow-xs lg:px-6 @xl/main:grid-cols-2 @5xl/main:grid-cols-4 dark:*:data-[slot=card]:bg-card"
|
||||
>
|
||||
<Card.Root class="@container/card">
|
||||
<Card.Header>
|
||||
<Card.Description>Total Revenue</Card.Description>
|
||||
<Card.Title class="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||
$1,250.00
|
||||
</Card.Title>
|
||||
<Card.Action>
|
||||
<Badge variant="outline">
|
||||
<TrendingUpIcon />
|
||||
+12.5%
|
||||
</Badge>
|
||||
</Card.Action>
|
||||
</Card.Header>
|
||||
<Card.Footer class="flex-col items-start gap-1.5 text-sm">
|
||||
<div class="line-clamp-1 flex gap-2 font-medium">
|
||||
Trending up this month <TrendingUpIcon class="size-4" />
|
||||
</div>
|
||||
<div class="text-muted-foreground">Visitors for the last 6 months</div>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
<Card.Root class="@container/card">
|
||||
<Card.Header>
|
||||
<Card.Description>New Customers</Card.Description>
|
||||
<Card.Title class="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||
1,234
|
||||
</Card.Title>
|
||||
<Card.Action>
|
||||
<Badge variant="outline">
|
||||
<TrendingDownIcon />
|
||||
-20%
|
||||
</Badge>
|
||||
</Card.Action>
|
||||
</Card.Header>
|
||||
<Card.Footer class="flex-col items-start gap-1.5 text-sm">
|
||||
<div class="line-clamp-1 flex gap-2 font-medium">
|
||||
Down 20% this period <TrendingDownIcon class="size-4" />
|
||||
</div>
|
||||
<div class="text-muted-foreground">Acquisition needs attention</div>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
<Card.Root class="@container/card">
|
||||
<Card.Header>
|
||||
<Card.Description>Active Accounts</Card.Description>
|
||||
<Card.Title class="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||
45,678
|
||||
</Card.Title>
|
||||
<Card.Action>
|
||||
<Badge variant="outline">
|
||||
<TrendingUpIcon />
|
||||
+12.5%
|
||||
</Badge>
|
||||
</Card.Action>
|
||||
</Card.Header>
|
||||
<Card.Footer class="flex-col items-start gap-1.5 text-sm">
|
||||
<div class="line-clamp-1 flex gap-2 font-medium">
|
||||
Strong user retention <TrendingUpIcon class="size-4" />
|
||||
</div>
|
||||
<div class="text-muted-foreground">Engagement exceed targets</div>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
<Card.Root class="@container/card">
|
||||
<Card.Header>
|
||||
<Card.Description>Growth Rate</Card.Description>
|
||||
<Card.Title class="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||
4.5%
|
||||
</Card.Title>
|
||||
<Card.Action>
|
||||
<Badge variant="outline">
|
||||
<TrendingUpIcon />
|
||||
+4.5%
|
||||
</Badge>
|
||||
</Card.Action>
|
||||
</Card.Header>
|
||||
<Card.Footer class="flex-col items-start gap-1.5 text-sm">
|
||||
<div class="line-clamp-1 flex gap-2 font-medium">
|
||||
Steady performance increase <TrendingUpIcon class="size-4" />
|
||||
</div>
|
||||
<div class="text-muted-foreground">Meets growth projections</div>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
</div>
|
||||
27
src/lib/components/site-header.svelte
Normal file
27
src/lib/components/site-header.svelte
Normal file
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import * as Sidebar from "#lib/components/ui/sidebar/index.js";
|
||||
import { Button } from "#lib/components/ui/button/index.js";
|
||||
import { Separator } from "#lib/components/ui/separator/index.js";
|
||||
</script>
|
||||
|
||||
<header
|
||||
class="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)"
|
||||
>
|
||||
<div class="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
||||
<Sidebar.Trigger class="-ms-1" />
|
||||
<Separator orientation="vertical" class="mx-2 data-vertical:h-4 data-vertical:self-auto" />
|
||||
<h1 class="text-base font-medium">Documents</h1>
|
||||
<div class="ms-auto flex items-center gap-2">
|
||||
<Button
|
||||
href="https://github.com/shadcn-ui/ui/tree/main/apps/v4/app/(examples)/dashboard"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="hidden sm:flex dark:text-foreground"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
26
src/lib/components/ui/avatar/avatar-badge.svelte
Normal file
26
src/lib/components/ui/avatar/avatar-badge.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "#lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
data-slot="avatar-badge"
|
||||
class={cn(
|
||||
"bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none",
|
||||
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
|
||||
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
|
||||
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</span>
|
||||
20
src/lib/components/ui/avatar/avatar-fallback.svelte
Normal file
20
src/lib/components/ui/avatar/avatar-fallback.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||
import { cn } from "#lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AvatarPrimitive.FallbackProps = $props();
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Fallback
|
||||
bind:ref
|
||||
data-slot="avatar-fallback"
|
||||
class={cn(
|
||||
"bg-muted text-muted-foreground rounded-full flex size-full items-center justify-center text-sm group-data-[size=sm]/avatar:text-xs",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
23
src/lib/components/ui/avatar/avatar-group-count.svelte
Normal file
23
src/lib/components/ui/avatar/avatar-group-count.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "#lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="avatar-group-count"
|
||||
class={cn(
|
||||
"bg-muted text-muted-foreground size-8 rounded-full text-sm group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 relative flex shrink-0 items-center justify-center ring-2 ring-background",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
23
src/lib/components/ui/avatar/avatar-group.svelte
Normal file
23
src/lib/components/ui/avatar/avatar-group.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "#lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="avatar-group"
|
||||
class={cn(
|
||||
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
17
src/lib/components/ui/avatar/avatar-image.svelte
Normal file
17
src/lib/components/ui/avatar/avatar-image.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||
import { cn } from "#lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AvatarPrimitive.ImageProps = $props();
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Image
|
||||
bind:ref
|
||||
data-slot="avatar-image"
|
||||
class={cn("rounded-full aspect-square size-full object-cover", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
26
src/lib/components/ui/avatar/avatar.svelte
Normal file
26
src/lib/components/ui/avatar/avatar.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||
import { cn } from "#lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
loadingStatus = $bindable("loading"),
|
||||
size = "default",
|
||||
class: className,
|
||||
...restProps
|
||||
}: AvatarPrimitive.RootProps & {
|
||||
size?: "default" | "sm" | "lg";
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Root
|
||||
bind:ref
|
||||
bind:loadingStatus
|
||||
data-slot="avatar"
|
||||
data-size={size}
|
||||
class={cn(
|
||||
"size-8 rounded-full after:rounded-full data-[size=lg]:size-10 data-[size=sm]:size-6 group/avatar relative flex shrink-0 select-none after:absolute after:inset-0 after:border after:border-border after:mix-blend-darken dark:after:mix-blend-lighten",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
22
src/lib/components/ui/avatar/index.ts
Normal file
22
src/lib/components/ui/avatar/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import Badge from "./avatar-badge.svelte";
|
||||
import Fallback from "./avatar-fallback.svelte";
|
||||
import GroupCount from "./avatar-group-count.svelte";
|
||||
import Group from "./avatar-group.svelte";
|
||||
import Image from "./avatar-image.svelte";
|
||||
import Root from "./avatar.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Image,
|
||||
Fallback,
|
||||
Badge,
|
||||
Group,
|
||||
GroupCount,
|
||||
//
|
||||
Root as Avatar,
|
||||
Image as AvatarImage,
|
||||
Fallback as AvatarFallback,
|
||||
Badge as AvatarBadge,
|
||||
Group as AvatarGroup,
|
||||
GroupCount as AvatarGroupCount,
|
||||
};
|
||||
49
src/lib/components/ui/badge/badge.svelte
Normal file
49
src/lib/components/ui/badge/badge.svelte
Normal file
@@ -0,0 +1,49 @@
|
||||
<script lang="ts" module>
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
export const badgeVariants = tv({
|
||||
base: "h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||
destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20",
|
||||
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "#lib/utils.js";
|
||||
import type { HTMLAnchorAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
href,
|
||||
class: className,
|
||||
variant = "default",
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: BadgeVariant;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={href ? "a" : "span"}
|
||||
bind:this={ref}
|
||||
data-slot="badge"
|
||||
{href}
|
||||
class={cn(badgeVariants({ variant }), className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</svelte:element>
|
||||
2
src/lib/components/ui/badge/index.ts
Normal file
2
src/lib/components/ui/badge/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as Badge } from "./badge.svelte";
|
||||
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
|
||||
23
src/lib/components/ui/card/card-action.svelte
Normal file
23
src/lib/components/ui/card/card-action.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "#lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-action"
|
||||
class={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/card/card-content.svelte
Normal file
20
src/lib/components/ui/card/card-content.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "#lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-content"
|
||||
class={cn("flex flex-col gap-3 px-(--card-spacing)", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/card/card-description.svelte
Normal file
20
src/lib/components/ui/card/card-description.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "#lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
||||
</script>
|
||||
|
||||
<p
|
||||
bind:this={ref}
|
||||
data-slot="card-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</p>
|
||||
20
src/lib/components/ui/card/card-footer.svelte
Normal file
20
src/lib/components/ui/card/card-footer.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "#lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-footer"
|
||||
class={cn("rounded-b-xl px-(--card-spacing) [.border-t]:pt-(--card-spacing) flex items-center", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
23
src/lib/components/ui/card/card-header.svelte
Normal file
23
src/lib/components/ui/card/card-header.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "#lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-header"
|
||||
class={cn(
|
||||
"gap-1 rounded-t-xl px-(--card-spacing) [.border-b]:pb-(--card-spacing) group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/card/card-title.svelte
Normal file
20
src/lib/components/ui/card/card-title.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "#lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-title"
|
||||
class={cn("text-base leading-normal font-medium group-data-[size=sm]/card:text-sm", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
22
src/lib/components/ui/card/card.svelte
Normal file
22
src/lib/components/ui/card/card.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "#lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
size = "default",
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { size?: "default" | "sm" } = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card"
|
||||
data-size={size}
|
||||
class={cn("ring-foreground/10 bg-card text-card-foreground gap-(--card-spacing) overflow-hidden rounded-xl py-(--card-spacing) text-sm shadow-xs ring-1 [--card-spacing:--spacing(6)] has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(4)] *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
25
src/lib/components/ui/card/index.ts
Normal file
25
src/lib/components/ui/card/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import Action from "./card-action.svelte";
|
||||
import Content from "./card-content.svelte";
|
||||
import Description from "./card-description.svelte";
|
||||
import Footer from "./card-footer.svelte";
|
||||
import Header from "./card-header.svelte";
|
||||
import Title from "./card-title.svelte";
|
||||
import Root from "./card.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Description,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
Action,
|
||||
//
|
||||
Root as Card,
|
||||
Content as CardContent,
|
||||
Description as CardDescription,
|
||||
Footer as CardFooter,
|
||||
Header as CardHeader,
|
||||
Title as CardTitle,
|
||||
Action as CardAction,
|
||||
};
|
||||
80
src/lib/components/ui/chart/chart-container.svelte
Normal file
80
src/lib/components/ui/chart/chart-container.svelte
Normal file
@@ -0,0 +1,80 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "#lib/utils.js";
|
||||
import ChartStyle from "./chart-style.svelte";
|
||||
import { setChartContext, type ChartConfig } from "./chart-utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
id = uid,
|
||||
class: className,
|
||||
children,
|
||||
config,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
|
||||
config: ChartConfig;
|
||||
} = $props();
|
||||
|
||||
const chartId = $derived(`chart-${id || uid.replace(/:/g, "")}`);
|
||||
|
||||
setChartContext({
|
||||
get config() {
|
||||
return config;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-chart={chartId}
|
||||
data-slot="chart"
|
||||
class={cn(
|
||||
"flex aspect-video justify-center overflow-visible text-xs",
|
||||
// Overrides
|
||||
//
|
||||
// Stroke around dots/marks when hovering
|
||||
"[&_.lc-highlight-point]:stroke-transparent",
|
||||
// override the default stroke color of lines
|
||||
"[&_.lc-line]:stroke-border/50",
|
||||
|
||||
// by default, layerchart shows a line intersecting the point when hovering, this hides that
|
||||
"[&_.lc-highlight-line]:stroke-0",
|
||||
|
||||
// by default, when you hover a point on a stacked series chart, it will drop the opacity
|
||||
// of the other series, this overrides that
|
||||
"[&_.lc-area-path]:opacity-100 [&_.lc-highlight-line]:opacity-100 [&_.lc-highlight-point]:opacity-100 [&_.lc-spline-path]:opacity-100 [&_.lc-text]:text-xs [&_.lc-text-svg]:overflow-visible",
|
||||
|
||||
// We don't want the little tick lines between the axis labels and the chart, so we remove
|
||||
// the stroke. The alternative is to manually disable `tickMarks` on the x/y axis of every
|
||||
// chart.
|
||||
"[&_.lc-axis-tick]:stroke-0",
|
||||
|
||||
// We don't want to display the rule on the x/y axis, as there is already going to be
|
||||
// a grid line there and rule ends up overlapping the marks because it is rendered after
|
||||
// the marks
|
||||
"[&_.lc-rule-x-line:not(.lc-grid-x-rule)]:stroke-0 [&_.lc-rule-y-line:not(.lc-grid-y-rule)]:stroke-0",
|
||||
"[&_.lc-grid-x-radial-circle]:stroke-border [&_.lc-grid-x-radial-line]:stroke-border",
|
||||
"[&_.lc-grid-y-radial-circle]:stroke-border [&_.lc-grid-y-radial-line]:stroke-border",
|
||||
|
||||
// Legend adjustments
|
||||
"[&_.lc-legend-swatch-button]:items-center [&_.lc-legend-swatch-button]:gap-1.5",
|
||||
"[&_.lc-legend-swatch-group]:items-center [&_.lc-legend-swatch-group]:gap-4",
|
||||
"[&_.lc-legend-swatch]:size-2.5 [&_.lc-legend-swatch]:rounded-[2px]",
|
||||
|
||||
// Labels
|
||||
"[&_.lc-labels-text:not([fill])]:fill-foreground [&_text]:stroke-transparent",
|
||||
|
||||
// Tick labels on th x/y axes
|
||||
"[&_.lc-axis-tick-label]:fill-muted-foreground [&_.lc-axis-tick-label]:font-normal",
|
||||
"[&_.lc-tooltip-rects-g]:fill-transparent",
|
||||
"[&_.lc-layout-svg-g]:fill-transparent",
|
||||
"[&_.lc-root-container]:w-full",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChartStyle id={chartId} {config} />
|
||||
{@render children?.()}
|
||||
</div>
|
||||
37
src/lib/components/ui/chart/chart-style.svelte
Normal file
37
src/lib/components/ui/chart/chart-style.svelte
Normal file
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { THEMES, type ChartConfig } from "./chart-utils.js";
|
||||
|
||||
let { id, config }: { id: string; config: ChartConfig } = $props();
|
||||
|
||||
const colorConfig = $derived(
|
||||
config ? Object.entries(config).filter(([, config]) => config.theme || config.color) : null
|
||||
);
|
||||
|
||||
const themeContents = $derived.by(() => {
|
||||
if (!colorConfig || !colorConfig.length) return;
|
||||
|
||||
const themeContents = [];
|
||||
for (const [_theme, prefix] of Object.entries(THEMES)) {
|
||||
let content = `${prefix} [data-chart=${id}] {\n`;
|
||||
const color = colorConfig.map(([key, itemConfig]) => {
|
||||
const theme = _theme as keyof typeof itemConfig.theme;
|
||||
const color = itemConfig.theme?.[theme] || itemConfig.color;
|
||||
return color ? `\t--color-${key}: ${color};` : null;
|
||||
});
|
||||
|
||||
content += color.join("\n") +"\n}";
|
||||
|
||||
themeContents.push(content);
|
||||
}
|
||||
|
||||
return themeContents.join("\n");
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if themeContents}
|
||||
{#key id}
|
||||
<svelte:element this={"style"}>
|
||||
{themeContents}
|
||||
</svelte:element>
|
||||
{/key}
|
||||
{/if}
|
||||
180
src/lib/components/ui/chart/chart-tooltip.svelte
Normal file
180
src/lib/components/ui/chart/chart-tooltip.svelte
Normal file
@@ -0,0 +1,180 @@
|
||||
<script lang="ts">
|
||||
import { getChartContext, Tooltip as TooltipPrimitive } from "layerchart";
|
||||
import { cn, type WithElementRef, type WithoutChildren } from "#lib/utils.js";
|
||||
import { getPayloadConfigFromPayload, useChart, type TooltipPayload } from "./chart-utils.js";
|
||||
import type { Snippet } from "svelte";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function defaultFormatter(value: any, _payload: TooltipPayload[]) {
|
||||
return `${value}`;
|
||||
}
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
hideLabel = false,
|
||||
indicator = "dot",
|
||||
hideIndicator = false,
|
||||
labelKey,
|
||||
label,
|
||||
labelFormatter = defaultFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
nameKey,
|
||||
color,
|
||||
...restProps
|
||||
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> & {
|
||||
hideLabel?: boolean;
|
||||
label?: string;
|
||||
indicator?: "line" | "dot" | "dashed";
|
||||
nameKey?: string;
|
||||
labelKey?: string;
|
||||
hideIndicator?: boolean;
|
||||
labelClassName?: string;
|
||||
labelFormatter?: // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
((value: any, payload: TooltipPayload[]) => string | number | Snippet) | null;
|
||||
formatter?: Snippet<
|
||||
[
|
||||
{
|
||||
value: unknown;
|
||||
name: string;
|
||||
item: TooltipPayload;
|
||||
index: number;
|
||||
payload: TooltipPayload[];
|
||||
},
|
||||
]
|
||||
>;
|
||||
} = $props();
|
||||
|
||||
const chart = useChart();
|
||||
const chartCtx = getChartContext();
|
||||
|
||||
// Filter to series with defined values (important for item-based charts like Pie/Arc
|
||||
// where only the hovered item has a value)
|
||||
const visibleSeries = $derived(
|
||||
chartCtx.tooltip.series.filter((s: TooltipPayload) => s.value !== undefined)
|
||||
);
|
||||
|
||||
const formattedLabel = $derived.by(() => {
|
||||
if (hideLabel || !visibleSeries?.length) return null;
|
||||
|
||||
const [item] = visibleSeries;
|
||||
const tooltipData = chartCtx.tooltip.data;
|
||||
|
||||
// Get the x-axis label value from the raw tooltip data (e.g. a Date or month string)
|
||||
const dataLabel = tooltipData != null ? chartCtx.x(tooltipData) : undefined;
|
||||
|
||||
const key = labelKey ?? item?.label ?? item?.key ?? "value";
|
||||
const itemConfig = getPayloadConfigFromPayload(
|
||||
chart.config,
|
||||
item,
|
||||
key,
|
||||
tooltipData as Record<string, unknown> | null
|
||||
);
|
||||
|
||||
let value: unknown;
|
||||
if (!labelKey && typeof label === "string") {
|
||||
value = chart.config[label as keyof typeof chart.config]?.label ?? label;
|
||||
} else if (labelKey) {
|
||||
value = itemConfig?.label ?? dataLabel;
|
||||
} else {
|
||||
value = dataLabel;
|
||||
}
|
||||
|
||||
if (value === undefined) return null;
|
||||
if (!labelFormatter) return value;
|
||||
return labelFormatter(value, visibleSeries);
|
||||
});
|
||||
|
||||
const nestLabel = $derived(visibleSeries.length === 1 && indicator !== "dot");
|
||||
</script>
|
||||
|
||||
{#snippet TooltipLabel()}
|
||||
{#if formattedLabel}
|
||||
<div class={cn("font-medium", labelClassName)}>
|
||||
{#if typeof formattedLabel === "function"}
|
||||
{@render formattedLabel()}
|
||||
{:else}
|
||||
{formattedLabel}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<TooltipPrimitive.Root variant="none">
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn(
|
||||
"grid min-w-32 items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#if !nestLabel}
|
||||
{@render TooltipLabel()}
|
||||
{/if}
|
||||
<div class="grid gap-1.5">
|
||||
{#each visibleSeries as item, i (item.key + i)}
|
||||
{@const key = `${nameKey || item.key || item.label || "value"}`}
|
||||
{@const itemConfig = getPayloadConfigFromPayload(
|
||||
chart.config,
|
||||
item,
|
||||
key,
|
||||
chartCtx.tooltip.data
|
||||
)}
|
||||
{@const indicatorColor = color || item.config?.color || item.color}
|
||||
<div
|
||||
class={cn(
|
||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||
indicator === "dot" && "items-center"
|
||||
)}
|
||||
>
|
||||
{#if formatter && item.value !== undefined && item.label}
|
||||
{@render formatter({
|
||||
value: item.value,
|
||||
name: item.label,
|
||||
item,
|
||||
index: i,
|
||||
payload: visibleSeries,
|
||||
})}
|
||||
{:else}
|
||||
{#if itemConfig?.icon}
|
||||
<itemConfig.icon />
|
||||
{:else if !hideIndicator}
|
||||
<div
|
||||
style="--color-bg: {indicatorColor}; --color-border: {indicatorColor};"
|
||||
class={cn("shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)", {
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"h-full w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
})}
|
||||
></div>
|
||||
{/if}
|
||||
<div
|
||||
class={cn(
|
||||
"flex flex-1 shrink-0 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center"
|
||||
)}
|
||||
>
|
||||
<div class="grid gap-1.5">
|
||||
{#if nestLabel}
|
||||
{@render TooltipLabel()}
|
||||
{/if}
|
||||
<span class="text-muted-foreground">
|
||||
{itemConfig?.label || item.label}
|
||||
</span>
|
||||
</div>
|
||||
{#if item.value !== undefined}
|
||||
<span class="font-mono font-medium text-foreground tabular-nums">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</TooltipPrimitive.Root>
|
||||
68
src/lib/components/ui/chart/chart-utils.ts
Normal file
68
src/lib/components/ui/chart/chart-utils.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { getContext, setContext, type Component, type Snippet } from "svelte";
|
||||
import type { Tooltip } from "layerchart";
|
||||
|
||||
export const THEMES = { light: "", dark: ".dark" } as const;
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: string;
|
||||
icon?: Component;
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
);
|
||||
};
|
||||
|
||||
export type ExtractSnippetParams<T> = T extends Snippet<[infer P]> ? P : never;
|
||||
|
||||
export type TooltipPayload = Tooltip.TooltipSeries;
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
export function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: TooltipPayload,
|
||||
key: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data?: Record<string, any> | null
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) return undefined;
|
||||
|
||||
const payloadConfig =
|
||||
"config" in payload && typeof payload.config === "object" && payload.config !== null
|
||||
? payload.config
|
||||
: undefined;
|
||||
|
||||
let configLabelKey: string = key;
|
||||
|
||||
if (payload.key === key) {
|
||||
configLabelKey = payload.key;
|
||||
} else if (payload.label === key) {
|
||||
configLabelKey = payload.label;
|
||||
} else if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||
} else if (
|
||||
payloadConfig !== undefined &&
|
||||
key in payloadConfig &&
|
||||
typeof payloadConfig[key as keyof typeof payloadConfig] === "string"
|
||||
) {
|
||||
configLabelKey = payloadConfig[key as keyof typeof payloadConfig] as string;
|
||||
} else if (data != null && key in data && typeof data[key] === "string") {
|
||||
configLabelKey = data[key] as string;
|
||||
}
|
||||
|
||||
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
|
||||
}
|
||||
|
||||
type ChartContextValue = {
|
||||
config: ChartConfig;
|
||||
};
|
||||
|
||||
const chartContextKey = Symbol("chart-context");
|
||||
|
||||
export function setChartContext(value: ChartContextValue) {
|
||||
return setContext(chartContextKey, value);
|
||||
}
|
||||
|
||||
export function useChart() {
|
||||
return getContext<ChartContextValue>(chartContextKey);
|
||||
}
|
||||
6
src/lib/components/ui/chart/index.ts
Normal file
6
src/lib/components/ui/chart/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import ChartContainer from "./chart-container.svelte";
|
||||
import ChartTooltip from "./chart-tooltip.svelte";
|
||||
|
||||
export { getPayloadConfigFromPayload, type ChartConfig } from "./chart-utils.js";
|
||||
|
||||
export { ChartContainer, ChartTooltip, ChartContainer as Container, ChartTooltip as Tooltip };
|
||||
39
src/lib/components/ui/checkbox/checkbox.svelte
Normal file
39
src/lib/components/ui/checkbox/checkbox.svelte
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { Checkbox as CheckboxPrimitive } from "bits-ui";
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
import MinusIcon from '@lucide/svelte/icons/minus';
|
||||
import { cn, type WithoutChildrenOrChild } from "#lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
checked = $bindable(false),
|
||||
indeterminate = $bindable(false),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
|
||||
</script>
|
||||
|
||||
<CheckboxPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="checkbox"
|
||||
class={cn(
|
||||
"border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-[4px] border shadow-xs transition-shadow group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-input group-has-[:focus-visible]/field-label:data-checked:border-primary peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
bind:checked
|
||||
bind:indeterminate
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked, indeterminate })}
|
||||
<div
|
||||
data-slot="checkbox-indicator"
|
||||
class="[&>svg]:size-3.5 grid place-content-center text-current transition-none"
|
||||
>
|
||||
{#if checked}
|
||||
<CheckIcon />
|
||||
{:else if indeterminate}
|
||||
<MinusIcon />
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</CheckboxPrimitive.Root>
|
||||
6
src/lib/components/ui/checkbox/index.ts
Normal file
6
src/lib/components/ui/checkbox/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import Root from "./checkbox.svelte";
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Checkbox,
|
||||
};
|
||||
7
src/lib/components/ui/drawer/drawer-close.svelte
Normal file
7
src/lib/components/ui/drawer/drawer-close.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DrawerPrimitive.CloseProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Close bind:ref data-slot="drawer-close" {...restProps} />
|
||||
36
src/lib/components/ui/drawer/drawer-content.svelte
Normal file
36
src/lib/components/ui/drawer/drawer-content.svelte
Normal file
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
import { cn } from "#lib/utils.js";
|
||||
import type { WithoutChildrenOrChild } from "#lib/utils.js";
|
||||
import DrawerOverlay from "./drawer-overlay.svelte";
|
||||
import DrawerPortal from "./drawer-portal.svelte";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: DrawerPrimitive.ContentProps & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DrawerPortal>>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPortal {...portalProps}>
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="drawer-content"
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground flex h-auto flex-col text-sm data-[vaul-drawer-direction=bottom]:rounded-t-xl data-[vaul-drawer-direction=bottom]:border-t data-[vaul-drawer-direction=left]:rounded-r-xl data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=right]:rounded-l-xl data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=top]:rounded-b-xl data-[vaul-drawer-direction=top]:border-b group/drawer-content fixed z-50 data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:start-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:end-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=left]:sm:max-w-sm data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<div
|
||||
class="bg-muted mx-auto mt-4 hidden h-1.5 w-[100px] shrink-0 rounded-full mx-auto hidden shrink-0 group-data-[vaul-drawer-direction=bottom]/drawer-content:block"
|
||||
></div>
|
||||
{@render children?.()}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
17
src/lib/components/ui/drawer/drawer-description.svelte
Normal file
17
src/lib/components/ui/drawer/drawer-description.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
import { cn } from "#lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DrawerPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Description
|
||||
bind:ref
|
||||
data-slot="drawer-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
20
src/lib/components/ui/drawer/drawer-footer.svelte
Normal file
20
src/lib/components/ui/drawer/drawer-footer.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "#lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="drawer-footer"
|
||||
class={cn("gap-2 p-4 mt-auto flex flex-col", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
23
src/lib/components/ui/drawer/drawer-header.svelte
Normal file
23
src/lib/components/ui/drawer/drawer-header.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "#lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="drawer-header"
|
||||
class={cn(
|
||||
"gap-0.5 p-4 md:gap-1.5 md:text-left flex flex-col group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
12
src/lib/components/ui/drawer/drawer-nested.svelte
Normal file
12
src/lib/components/ui/drawer/drawer-nested.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
|
||||
let {
|
||||
shouldScaleBackground = true,
|
||||
open = $bindable(false),
|
||||
activeSnapPoint = $bindable(null),
|
||||
...restProps
|
||||
}: DrawerPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.NestedRoot {shouldScaleBackground} bind:open bind:activeSnapPoint {...restProps} />
|
||||
20
src/lib/components/ui/drawer/drawer-overlay.svelte
Normal file
20
src/lib/components/ui/drawer/drawer-overlay.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
import { cn } from "#lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DrawerPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Overlay
|
||||
bind:ref
|
||||
data-slot="drawer-overlay"
|
||||
class={cn(
|
||||
"bg-black/10 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
7
src/lib/components/ui/drawer/drawer-portal.svelte
Normal file
7
src/lib/components/ui/drawer/drawer-portal.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
|
||||
let { ...restProps }: DrawerPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Portal {...restProps} />
|
||||
17
src/lib/components/ui/drawer/drawer-title.svelte
Normal file
17
src/lib/components/ui/drawer/drawer-title.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
import { cn } from "#lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DrawerPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Title
|
||||
bind:ref
|
||||
data-slot="drawer-title"
|
||||
class={cn("text-foreground font-medium", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
7
src/lib/components/ui/drawer/drawer-trigger.svelte
Normal file
7
src/lib/components/ui/drawer/drawer-trigger.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DrawerPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Trigger bind:ref data-slot="drawer-trigger" {...restProps} />
|
||||
12
src/lib/components/ui/drawer/drawer.svelte
Normal file
12
src/lib/components/ui/drawer/drawer.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
|
||||
let {
|
||||
shouldScaleBackground = true,
|
||||
open = $bindable(false),
|
||||
activeSnapPoint = $bindable(null),
|
||||
...restProps
|
||||
}: DrawerPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Root {shouldScaleBackground} bind:open bind:activeSnapPoint {...restProps} />
|
||||
38
src/lib/components/ui/drawer/index.ts
Normal file
38
src/lib/components/ui/drawer/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import Close from "./drawer-close.svelte";
|
||||
import Content from "./drawer-content.svelte";
|
||||
import Description from "./drawer-description.svelte";
|
||||
import Footer from "./drawer-footer.svelte";
|
||||
import Header from "./drawer-header.svelte";
|
||||
import NestedRoot from "./drawer-nested.svelte";
|
||||
import Overlay from "./drawer-overlay.svelte";
|
||||
import Portal from "./drawer-portal.svelte";
|
||||
import Title from "./drawer-title.svelte";
|
||||
import Trigger from "./drawer-trigger.svelte";
|
||||
import Root from "./drawer.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
NestedRoot,
|
||||
Content,
|
||||
Description,
|
||||
Overlay,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
Trigger,
|
||||
Portal,
|
||||
Close,
|
||||
|
||||
//
|
||||
Root as Drawer,
|
||||
NestedRoot as DrawerNestedRoot,
|
||||
Content as DrawerContent,
|
||||
Description as DrawerDescription,
|
||||
Overlay as DrawerOverlay,
|
||||
Footer as DrawerFooter,
|
||||
Header as DrawerHeader,
|
||||
Title as DrawerTitle,
|
||||
Trigger as DrawerTrigger,
|
||||
Portal as DrawerPortal,
|
||||
Close as DrawerClose,
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable([]),
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.CheckboxGroupProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.CheckboxGroup
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="dropdown-menu-checkbox-group"
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import MinusIcon from '@lucide/svelte/icons/minus';
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
import { cn, type WithoutChildrenOrChild } from "#lib/utils.js";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
checked = $bindable(false),
|
||||
indeterminate = $bindable(false),
|
||||
class: className,
|
||||
children: childrenProp,
|
||||
closeOnSelect = false,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
|
||||
children?: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
bind:ref
|
||||
{closeOnSelect}
|
||||
bind:checked
|
||||
bind:indeterminate
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked, indeterminate })}
|
||||
<span
|
||||
class="absolute right-2 flex items-center justify-center pointer-events-none"
|
||||
data-slot="dropdown-menu-checkbox-item-indicator"
|
||||
>
|
||||
{#if indeterminate}
|
||||
<MinusIcon />
|
||||
{:else if checked}
|
||||
<CheckIcon />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.()}
|
||||
{/snippet}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "#lib/utils.js";
|
||||
import DropdownMenuPortal from "./dropdown-menu-portal.svelte";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
sideOffset = 4,
|
||||
align = "start",
|
||||
portalProps,
|
||||
class: className,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.ContentProps & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DropdownMenuPortal>>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPortal {...portalProps}>
|
||||
<DropdownMenuPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-content"
|
||||
{sideOffset}
|
||||
{align}
|
||||
class={cn(
|
||||
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-md p-1 shadow-md ring-1 duration-100 z-50 max-h-(--bits-dropdown-menu-content-available-height) w-(--bits-dropdown-menu-anchor-width) origin-(--bits-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto data-closed:overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</DropdownMenuPortal>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "#lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
...restProps
|
||||
}: ComponentProps<typeof DropdownMenuPrimitive.GroupHeading> & {
|
||||
inset?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.GroupHeading
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-group-heading"
|
||||
data-inset={inset}
|
||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs font-medium data-inset:pl-8", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.GroupProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "#lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.ItemProps & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Item
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "#lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
inset?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs font-medium data-inset:pl-8", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: DropdownMenuPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Portal {...restProps} />
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.RadioGroupProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
import { cn, type WithoutChild } from "#lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children: childrenProp,
|
||||
closeOnSelect = false,
|
||||
...restProps
|
||||
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
bind:ref
|
||||
{closeOnSelect}
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked })}
|
||||
<span
|
||||
class="absolute right-2 flex items-center justify-center pointer-events-none"
|
||||
data-slot="dropdown-menu-radio-item-indicator"
|
||||
>
|
||||
{#if checked}
|
||||
<CheckIcon />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.({ checked })}
|
||||
{/snippet}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "#lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.SeparatorProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Separator
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-separator"
|
||||
class={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "#lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
class={cn("text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</span>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "#lib/utils.js";
|
||||
import DropdownMenuPortal from "./dropdown-menu-portal.svelte";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
align = "start",
|
||||
alignOffset = -3,
|
||||
portalProps,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.SubContentProps & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DropdownMenuPortal>>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPortal {...portalProps}>
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
{align}
|
||||
{alignOffset}
|
||||
class={cn(
|
||||
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-[96px] rounded-md p-1 shadow-lg ring-1 duration-100 z-50 origin-(--bits-dropdown-menu-content-transform-origin) overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</DropdownMenuPortal>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||
import { cn } from "#lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
children,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.SubTriggerProps & {
|
||||
inset?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronRightIcon class="cn-rtl-flip ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.SubProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Sub bind:open {...restProps} />
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} />
|
||||
7
src/lib/components/ui/dropdown-menu/dropdown-menu.svelte
Normal file
7
src/lib/components/ui/dropdown-menu/dropdown-menu.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Root bind:open {...restProps} />
|
||||
54
src/lib/components/ui/dropdown-menu/index.ts
Normal file
54
src/lib/components/ui/dropdown-menu/index.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import CheckboxGroup from "./dropdown-menu-checkbox-group.svelte";
|
||||
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
|
||||
import Content from "./dropdown-menu-content.svelte";
|
||||
import GroupHeading from "./dropdown-menu-group-heading.svelte";
|
||||
import Group from "./dropdown-menu-group.svelte";
|
||||
import Item from "./dropdown-menu-item.svelte";
|
||||
import Label from "./dropdown-menu-label.svelte";
|
||||
import Portal from "./dropdown-menu-portal.svelte";
|
||||
import RadioGroup from "./dropdown-menu-radio-group.svelte";
|
||||
import RadioItem from "./dropdown-menu-radio-item.svelte";
|
||||
import Separator from "./dropdown-menu-separator.svelte";
|
||||
import Shortcut from "./dropdown-menu-shortcut.svelte";
|
||||
import SubContent from "./dropdown-menu-sub-content.svelte";
|
||||
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
|
||||
import Sub from "./dropdown-menu-sub.svelte";
|
||||
import Trigger from "./dropdown-menu-trigger.svelte";
|
||||
import Root from "./dropdown-menu.svelte";
|
||||
|
||||
export {
|
||||
CheckboxGroup,
|
||||
CheckboxItem,
|
||||
Content,
|
||||
Portal,
|
||||
Root as DropdownMenu,
|
||||
CheckboxGroup as DropdownMenuCheckboxGroup,
|
||||
CheckboxItem as DropdownMenuCheckboxItem,
|
||||
Content as DropdownMenuContent,
|
||||
Portal as DropdownMenuPortal,
|
||||
Group as DropdownMenuGroup,
|
||||
Item as DropdownMenuItem,
|
||||
Label as DropdownMenuLabel,
|
||||
RadioGroup as DropdownMenuRadioGroup,
|
||||
RadioItem as DropdownMenuRadioItem,
|
||||
Separator as DropdownMenuSeparator,
|
||||
Shortcut as DropdownMenuShortcut,
|
||||
Sub as DropdownMenuSub,
|
||||
SubContent as DropdownMenuSubContent,
|
||||
SubTrigger as DropdownMenuSubTrigger,
|
||||
Trigger as DropdownMenuTrigger,
|
||||
GroupHeading as DropdownMenuGroupHeading,
|
||||
Group,
|
||||
GroupHeading,
|
||||
Item,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioItem,
|
||||
Root,
|
||||
Separator,
|
||||
Shortcut,
|
||||
Sub,
|
||||
SubContent,
|
||||
SubTrigger,
|
||||
Trigger,
|
||||
};
|
||||
7
src/lib/components/ui/input/index.ts
Normal file
7
src/lib/components/ui/input/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./input.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
};
|
||||
48
src/lib/components/ui/input/input.svelte
Normal file
48
src/lib/components/ui/input/input.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "#lib/utils.js";
|
||||
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
|
||||
|
||||
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
|
||||
|
||||
type Props = WithElementRef<
|
||||
Omit<HTMLInputAttributes, "type"> &
|
||||
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
|
||||
>;
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
type,
|
||||
files = $bindable(),
|
||||
class: className,
|
||||
"data-slot": dataSlot = "input",
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if type === "file"}
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot={dataSlot}
|
||||
class={cn(
|
||||
"dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-md border bg-transparent px-2.5 py-1 text-base shadow-xs transition-[color,box-shadow] file:h-7 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
type="file"
|
||||
bind:files
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot={dataSlot}
|
||||
class={cn(
|
||||
"dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-md border bg-transparent px-2.5 py-1 text-base shadow-xs transition-[color,box-shadow] file:h-7 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{type}
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{/if}
|
||||
7
src/lib/components/ui/label/index.ts
Normal file
7
src/lib/components/ui/label/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./label.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Label,
|
||||
};
|
||||
20
src/lib/components/ui/label/label.svelte
Normal file
20
src/lib/components/ui/label/label.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Label as LabelPrimitive } from "bits-ui";
|
||||
import { cn } from "#lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: LabelPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<LabelPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="label"
|
||||
class={cn(
|
||||
"gap-2 text-sm leading-none font-medium group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
40
src/lib/components/ui/select/index.ts
Normal file
40
src/lib/components/ui/select/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import Content from "./select-content.svelte";
|
||||
import GroupHeading from "./select-group-heading.svelte";
|
||||
import Group from "./select-group.svelte";
|
||||
import Item from "./select-item.svelte";
|
||||
import Label from "./select-label.svelte";
|
||||
import Portal from "./select-portal.svelte";
|
||||
import ScrollDownButton from "./select-scroll-down-button.svelte";
|
||||
import ScrollUpButton from "./select-scroll-up-button.svelte";
|
||||
import Separator from "./select-separator.svelte";
|
||||
import Trigger from "./select-trigger.svelte";
|
||||
import Value from "./select-value.svelte";
|
||||
import Root from "./select.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Group,
|
||||
Label,
|
||||
Item,
|
||||
Content,
|
||||
Trigger,
|
||||
Separator,
|
||||
ScrollDownButton,
|
||||
ScrollUpButton,
|
||||
GroupHeading,
|
||||
Portal,
|
||||
Value,
|
||||
//
|
||||
Root as Select,
|
||||
Group as SelectGroup,
|
||||
Label as SelectLabel,
|
||||
Item as SelectItem,
|
||||
Content as SelectContent,
|
||||
Trigger as SelectTrigger,
|
||||
Separator as SelectSeparator,
|
||||
ScrollDownButton as SelectScrollDownButton,
|
||||
ScrollUpButton as SelectScrollUpButton,
|
||||
GroupHeading as SelectGroupHeading,
|
||||
Portal as SelectPortal,
|
||||
Value as SelectValue,
|
||||
};
|
||||
45
src/lib/components/ui/select/select-content.svelte
Normal file
45
src/lib/components/ui/select/select-content.svelte
Normal file
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChild } from "#lib/utils.js";
|
||||
import type { WithoutChildrenOrChild } from "#lib/utils.js";
|
||||
import SelectPortal from "./select-portal.svelte";
|
||||
import SelectScrollDownButton from "./select-scroll-down-button.svelte";
|
||||
import SelectScrollUpButton from "./select-scroll-up-button.svelte";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 4,
|
||||
portalProps,
|
||||
children,
|
||||
preventScroll = true,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.ContentProps> & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SelectPortal>>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SelectPortal {...portalProps}>
|
||||
<SelectPrimitive.Content
|
||||
bind:ref
|
||||
{sideOffset}
|
||||
{preventScroll}
|
||||
data-slot="select-content"
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-36 rounded-md shadow-md ring-1 duration-100 relative z-50 max-h-(--bits-select-content-available-height) origin-(--bits-select-content-transform-origin) overflow-x-hidden overflow-y-auto",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
class={cn(
|
||||
"h-(--bits-select-anchor-height) w-full min-w-(--bits-select-anchor-width) scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{@render children?.()}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPortal>
|
||||
21
src/lib/components/ui/select/select-group-heading.svelte
Normal file
21
src/lib/components/ui/select/select-group-heading.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn } from "#lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.GroupHeading
|
||||
bind:ref
|
||||
data-slot="select-group-heading"
|
||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</SelectPrimitive.GroupHeading>
|
||||
17
src/lib/components/ui/select/select-group.svelte
Normal file
17
src/lib/components/ui/select/select-group.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn } from "#lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SelectPrimitive.GroupProps = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Group
|
||||
bind:ref
|
||||
data-slot="select-group"
|
||||
class={cn("scroll-my-1 p-1", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
42
src/lib/components/ui/select/select-item.svelte
Normal file
42
src/lib/components/ui/select/select-item.svelte
Normal file
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
import { cn, type WithoutChild } from "#lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value,
|
||||
label,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.ItemProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Item
|
||||
bind:ref
|
||||
{value}
|
||||
{label}
|
||||
data-slot="select-item"
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
"data-highlighted:bg-accent data-highlighted:text-accent-foreground",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ selected, highlighted })}
|
||||
<span class="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
|
||||
{#if selected}
|
||||
<CheckIcon class="pointer-events-none" />
|
||||
{/if}
|
||||
</span>
|
||||
<span class="flex flex-1 gap-2 shrink-0 whitespace-nowrap">
|
||||
{#if childrenProp}
|
||||
{@render childrenProp({ selected, highlighted })}
|
||||
{:else}
|
||||
{label || value}
|
||||
{/if}
|
||||
</span>
|
||||
{/snippet}
|
||||
</SelectPrimitive.Item>
|
||||
20
src/lib/components/ui/select/select-label.svelte
Normal file
20
src/lib/components/ui/select/select-label.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "#lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="select-label"
|
||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
7
src/lib/components/ui/select/select-portal.svelte
Normal file
7
src/lib/components/ui/select/select-portal.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: SelectPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Portal {...restProps} />
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||
import { cn, type WithoutChildrenOrChild } from "#lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
bind:ref
|
||||
data-slot="select-scroll-down-button"
|
||||
class={cn("bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronDownIcon />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
20
src/lib/components/ui/select/select-scroll-up-button.svelte
Normal file
20
src/lib/components/ui/select/select-scroll-up-button.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
|
||||
import { cn, type WithoutChildrenOrChild } from "#lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
bind:ref
|
||||
data-slot="select-scroll-up-button"
|
||||
class={cn("bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronUpIcon />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
18
src/lib/components/ui/select/select-separator.svelte
Normal file
18
src/lib/components/ui/select/select-separator.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { Separator } from "#lib/components/ui/separator/index.js";
|
||||
import { cn } from "#lib/utils.js";
|
||||
import type { Separator as SeparatorPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<Separator
|
||||
bind:ref
|
||||
data-slot="select-separator"
|
||||
class={cn("bg-border -mx-1 my-1 h-px pointer-events-none", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
29
src/lib/components/ui/select/select-trigger.svelte
Normal file
29
src/lib/components/ui/select/select-trigger.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||
import { cn, type WithoutChild } from "#lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
size = "default",
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.TriggerProps> & {
|
||||
size?: "sm" | "default";
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
class={cn(
|
||||
"border-input data-placeholder:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 gap-1.5 rounded-md border bg-transparent py-2 pr-2 pl-2.5 text-sm shadow-xs transition-[color,box-shadow] focus-visible:ring-3 aria-invalid:ring-3 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:flex *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-4 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronDownIcon class="text-muted-foreground size-4 pointer-events-none" />
|
||||
</SelectPrimitive.Trigger>
|
||||
7
src/lib/components/ui/select/select-value.svelte
Normal file
7
src/lib/components/ui/select/select-value.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: SelectPrimitive.ValueProps = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Value bind:ref data-slot="select-value" {...restProps} />
|
||||
11
src/lib/components/ui/select/select.svelte
Normal file
11
src/lib/components/ui/select/select.svelte
Normal file
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
value = $bindable(),
|
||||
...restProps
|
||||
}: SelectPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Root bind:open bind:value={value as never} {...restProps} />
|
||||
7
src/lib/components/ui/separator/index.ts
Normal file
7
src/lib/components/ui/separator/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./separator.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Separator,
|
||||
};
|
||||
21
src/lib/components/ui/separator/separator.svelte
Normal file
21
src/lib/components/ui/separator/separator.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Separator as SeparatorPrimitive } from "bits-ui";
|
||||
import { cn } from "#lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
"data-slot": dataSlot = "separator",
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SeparatorPrimitive.Root
|
||||
bind:ref
|
||||
data-slot={dataSlot}
|
||||
class={cn(
|
||||
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
34
src/lib/components/ui/sheet/index.ts
Normal file
34
src/lib/components/ui/sheet/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import Close from "./sheet-close.svelte";
|
||||
import Content from "./sheet-content.svelte";
|
||||
import Description from "./sheet-description.svelte";
|
||||
import Footer from "./sheet-footer.svelte";
|
||||
import Header from "./sheet-header.svelte";
|
||||
import Overlay from "./sheet-overlay.svelte";
|
||||
import Portal from "./sheet-portal.svelte";
|
||||
import Title from "./sheet-title.svelte";
|
||||
import Trigger from "./sheet-trigger.svelte";
|
||||
import Root from "./sheet.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Close,
|
||||
Trigger,
|
||||
Portal,
|
||||
Overlay,
|
||||
Content,
|
||||
Header,
|
||||
Footer,
|
||||
Title,
|
||||
Description,
|
||||
//
|
||||
Root as Sheet,
|
||||
Close as SheetClose,
|
||||
Trigger as SheetTrigger,
|
||||
Portal as SheetPortal,
|
||||
Overlay as SheetOverlay,
|
||||
Content as SheetContent,
|
||||
Header as SheetHeader,
|
||||
Footer as SheetFooter,
|
||||
Title as SheetTitle,
|
||||
Description as SheetDescription,
|
||||
};
|
||||
7
src/lib/components/ui/sheet/sheet-close.svelte
Normal file
7
src/lib/components/ui/sheet/sheet-close.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: SheetPrimitive.CloseProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Close bind:ref data-slot="sheet-close" {...restProps} />
|
||||
55
src/lib/components/ui/sheet/sheet-content.svelte
Normal file
55
src/lib/components/ui/sheet/sheet-content.svelte
Normal file
@@ -0,0 +1,55 @@
|
||||
<script lang="ts" module>
|
||||
export type Side = "top" | "right" | "bottom" | "left";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
import XIcon from '@lucide/svelte/icons/x';
|
||||
import { Button } from "#lib/components/ui/button/index.js";
|
||||
import { cn, type WithoutChildrenOrChild } from "#lib/utils.js";
|
||||
import SheetOverlay from "./sheet-overlay.svelte";
|
||||
import SheetPortal from "./sheet-portal.svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
side = "right",
|
||||
showCloseButton = true,
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SheetPrimitive.ContentProps> & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SheetPortal>>;
|
||||
side?: Side;
|
||||
showCloseButton?: boolean;
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SheetPortal {...portalProps}>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="sheet-content"
|
||||
data-side={side}
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground fixed z-50 flex flex-col gap-4 bg-clip-padding text-sm shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{#if showCloseButton}
|
||||
<SheetPrimitive.Close data-slot="sheet-close">
|
||||
{#snippet child({ props })}
|
||||
<Button variant="ghost" class="absolute top-4 right-4" size="icon-sm" {...props}>
|
||||
<XIcon />
|
||||
<span class="sr-only">Close</span>
|
||||
</Button>
|
||||
{/snippet}
|
||||
</SheetPrimitive.Close>
|
||||
{/if}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
17
src/lib/components/ui/sheet/sheet-description.svelte
Normal file
17
src/lib/components/ui/sheet/sheet-description.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
import { cn } from "#lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SheetPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Description
|
||||
bind:ref
|
||||
data-slot="sheet-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
20
src/lib/components/ui/sheet/sheet-footer.svelte
Normal file
20
src/lib/components/ui/sheet/sheet-footer.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "#lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="sheet-footer"
|
||||
class={cn("gap-2 p-4 mt-auto flex flex-col", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user