feat: update sidebar and header layout; fix dnd-kit-vue package error
- Fixed import error for `dnd-kit-vue` by installing the package. - Moved user navigation from sidebar to header, adding a dropdown for account options. - Added homepage and notification icons to the header. - Implemented sticky header functionality during scroll. - Grouped sidebar menu items under a "Master" label for better organization. - Updated relevant components and package.json to include new dependencies.
This commit is contained in:
parent
0418bec226
commit
0c732e71cf
64
app/components/AppSidebar.vue
Normal file
64
app/components/AppSidebar.vue
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Database,
|
||||||
|
Grid,
|
||||||
|
HelpCircle,
|
||||||
|
Settings
|
||||||
|
} from "@lucide/vue"
|
||||||
|
|
||||||
|
import {
|
||||||
|
Sidebar,
|
||||||
|
SidebarContent,
|
||||||
|
SidebarGroupContent,
|
||||||
|
SidebarGroupLabel,
|
||||||
|
SidebarHeader,
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
} from '@/components/ui/sidebar'
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
user: {
|
||||||
|
name: "shadcn",
|
||||||
|
email: "m@example.com",
|
||||||
|
avatar: "/avatars/shadcn.jpg",
|
||||||
|
},
|
||||||
|
navMain: [
|
||||||
|
{
|
||||||
|
title: "Dashboard",
|
||||||
|
url: "#",
|
||||||
|
icon: Grid,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Sidebar collapsible="offcanvas">
|
||||||
|
<SidebarHeader>
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton as-child class="data-[slot=sidebar-menu-button]:p-1.5!">
|
||||||
|
<a href="#">
|
||||||
|
<Database class="size-5!" />
|
||||||
|
<span class="text-base font-semibold">Profitra</span>
|
||||||
|
</a>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarHeader>
|
||||||
|
<SidebarContent>
|
||||||
|
<SidebarGroupContent class="flex flex-col gap-2">
|
||||||
|
<SidebarGroupLabel>Master</SidebarGroupLabel>
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem v-for="item in data.navMain" :key="item.title">
|
||||||
|
<SidebarMenuButton :tooltip="item.title">
|
||||||
|
<component :is="item.icon" v-if="item.icon" />
|
||||||
|
<span>{{ item.title }}</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarContent>
|
||||||
|
</Sidebar>
|
||||||
|
</template>
|
||||||
275
app/components/ChartAreaInteractive.vue
Normal file
275
app/components/ChartAreaInteractive.vue
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ChartConfig } from '@/components/ui/chart'
|
||||||
|
|
||||||
|
// import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"
|
||||||
|
import { VisArea, VisAxis, VisLine, VisXYContainer } from "@unovis/vue"
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
import {
|
||||||
|
|
||||||
|
ChartContainer,
|
||||||
|
ChartCrosshair,
|
||||||
|
ChartLegendContent,
|
||||||
|
ChartTooltip,
|
||||||
|
ChartTooltipContent,
|
||||||
|
componentToString,
|
||||||
|
} from '@/components/ui/chart'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
|
||||||
|
const description = "An interactive area chart"
|
||||||
|
|
||||||
|
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 },
|
||||||
|
]
|
||||||
|
type Data = typeof chartData[number]
|
||||||
|
|
||||||
|
const chartConfig = {
|
||||||
|
// visitors: {
|
||||||
|
// label: 'Visitors',
|
||||||
|
// },
|
||||||
|
mobile: {
|
||||||
|
label: "Mobile",
|
||||||
|
color: "var(--primary)",
|
||||||
|
},
|
||||||
|
desktop: {
|
||||||
|
label: "Desktop",
|
||||||
|
color: "var(--primary)",
|
||||||
|
},
|
||||||
|
} satisfies ChartConfig
|
||||||
|
|
||||||
|
const svgDefs = `
|
||||||
|
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop
|
||||||
|
offset="5%"
|
||||||
|
stop-color="var(--color-desktop)"
|
||||||
|
stop-opacity="0.8"
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
`
|
||||||
|
|
||||||
|
const timeRange = ref("90d")
|
||||||
|
const filterRange = computed(() => {
|
||||||
|
return chartData.filter((item) => {
|
||||||
|
const date = new Date(item.date)
|
||||||
|
const referenceDate = new Date("2024-06-30")
|
||||||
|
let daysToSubtract = 90
|
||||||
|
if (timeRange.value === "30d") {
|
||||||
|
daysToSubtract = 30
|
||||||
|
}
|
||||||
|
else if (timeRange.value === "7d") {
|
||||||
|
daysToSubtract = 7
|
||||||
|
}
|
||||||
|
const startDate = new Date(referenceDate)
|
||||||
|
startDate.setDate(startDate.getDate() - daysToSubtract)
|
||||||
|
return date >= startDate
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card class="pt-0">
|
||||||
|
<CardHeader class="flex items-center gap-2 space-y-0 border-b py-5 sm:flex-row">
|
||||||
|
<div class="grid flex-1 gap-1">
|
||||||
|
<CardTitle>Area Chart - Interactive</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Showing total visitors for the last 3 months
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Select v-model="timeRange">
|
||||||
|
<SelectTrigger
|
||||||
|
class="hidden w-[160px] rounded-lg sm:ml-auto sm:flex"
|
||||||
|
aria-label="Select a value"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Last 3 months" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent class="rounded-xl">
|
||||||
|
<SelectItem value="90d" class="rounded-lg">
|
||||||
|
Last 3 months
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="30d" class="rounded-lg">
|
||||||
|
Last 30 days
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="7d" class="rounded-lg">
|
||||||
|
Last 7 days
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="px-2 pt-4 sm:px-6 sm:pt-6 pb-4">
|
||||||
|
<ChartContainer :config="chartConfig" class="aspect-auto h-[250px] w-full" :cursor="false">
|
||||||
|
<VisXYContainer
|
||||||
|
:data="filterRange"
|
||||||
|
:svg-defs="svgDefs"
|
||||||
|
:margin="{ left: -40 }"
|
||||||
|
:y-domain="[0, 1200]"
|
||||||
|
>
|
||||||
|
<VisArea
|
||||||
|
:x="(d: Data) => d.date"
|
||||||
|
:y="[(d: Data) => d.mobile, (d: Data) => d.desktop]"
|
||||||
|
:color="(d: Data, i: number) => ['url(#fillMobile)', 'url(#fillDesktop)'][i]"
|
||||||
|
:opacity="0.6"
|
||||||
|
/>
|
||||||
|
<VisLine
|
||||||
|
:x="(d: Data) => d.date"
|
||||||
|
:y="[(d: Data) => d.mobile, (d: Data) => d.mobile + d.desktop]"
|
||||||
|
:color="(d: Data, i: number) => [chartConfig.mobile.color, chartConfig.desktop.color][i]"
|
||||||
|
:line-width="1"
|
||||||
|
/>
|
||||||
|
<VisAxis
|
||||||
|
type="x"
|
||||||
|
:x="(d: Data) => d.date"
|
||||||
|
:tick-line="false"
|
||||||
|
:domain-line="false"
|
||||||
|
:grid-line="false"
|
||||||
|
:num-ticks="6"
|
||||||
|
:tick-format="(d: number, index: number) => {
|
||||||
|
const date = new Date(d)
|
||||||
|
return date.toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
})
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<VisAxis
|
||||||
|
type="y"
|
||||||
|
:num-ticks="3"
|
||||||
|
:tick-line="false"
|
||||||
|
:domain-line="false"
|
||||||
|
/>
|
||||||
|
<ChartTooltip />
|
||||||
|
<ChartCrosshair
|
||||||
|
:template="componentToString(chartConfig, ChartTooltipContent, {
|
||||||
|
labelFormatter: (d) => {
|
||||||
|
return new Date(d).toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})"
|
||||||
|
:color="(d: Data, i: number) => [chartConfig.mobile.color, chartConfig.desktop.color][i % 2]"
|
||||||
|
/>
|
||||||
|
</VisXYContainer>
|
||||||
|
|
||||||
|
<ChartLegendContent />
|
||||||
|
</ChartContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
477
app/components/DataTable.vue
Normal file
477
app/components/DataTable.vue
Normal file
@ -0,0 +1,477 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { z } from "zod"
|
||||||
|
import DraggableRow from "./DraggableRow.vue"
|
||||||
|
import DragHandle from "./DragHandle.vue"
|
||||||
|
|
||||||
|
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(),
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {
|
||||||
|
ColumnDef,
|
||||||
|
ColumnFiltersState,
|
||||||
|
SortingState,
|
||||||
|
VisibilityState,
|
||||||
|
} from "@tanstack/vue-table"
|
||||||
|
import { RestrictToVerticalAxis } from "@dnd-kit/abstract/modifiers"
|
||||||
|
import {
|
||||||
|
IconChevronDown,
|
||||||
|
IconChevronLeft,
|
||||||
|
IconChevronRight,
|
||||||
|
IconChevronsLeft,
|
||||||
|
IconChevronsRight,
|
||||||
|
IconCircleCheckFilled,
|
||||||
|
IconDotsVertical,
|
||||||
|
IconLayoutColumns,
|
||||||
|
IconLoader,
|
||||||
|
IconPlus,
|
||||||
|
} from "@tabler/icons-vue"
|
||||||
|
import {
|
||||||
|
FlexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
useVueTable,
|
||||||
|
} from "@tanstack/vue-table"
|
||||||
|
import { DragDropProvider } from "dnd-kit-vue"
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Tabs,
|
||||||
|
TabsContent,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
} from '@/components/ui/tabs'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: TableData[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
interface TableData {
|
||||||
|
id: number
|
||||||
|
header: string
|
||||||
|
type: string
|
||||||
|
status: string
|
||||||
|
target: string
|
||||||
|
limit: string
|
||||||
|
reviewer: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorting = ref<SortingState>([])
|
||||||
|
const columnFilters = ref<ColumnFiltersState>([])
|
||||||
|
const columnVisibility = ref<VisibilityState>({})
|
||||||
|
const rowSelection = ref({})
|
||||||
|
|
||||||
|
const columns: ColumnDef<TableData>[] = [
|
||||||
|
{
|
||||||
|
id: "drag",
|
||||||
|
header: () => null,
|
||||||
|
cell: ({ row }) => h(DragHandle),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => h(Checkbox, {
|
||||||
|
"modelValue": table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate"),
|
||||||
|
"onUpdate:modelValue": value => table.toggleAllPageRowsSelected(!!value),
|
||||||
|
"aria-label": "Select all",
|
||||||
|
}),
|
||||||
|
cell: ({ row }) => h(Checkbox, {
|
||||||
|
"modelValue": row.getIsSelected(),
|
||||||
|
"onUpdate:modelValue": value => row.toggleSelected(!!value),
|
||||||
|
"aria-label": "Select row",
|
||||||
|
}),
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "header",
|
||||||
|
header: "Header",
|
||||||
|
cell: ({ row }) => h("div", String(row.getValue("header"))),
|
||||||
|
enableHiding: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "type",
|
||||||
|
header: "Section Type",
|
||||||
|
cell: ({ row }) => h(Badge, {
|
||||||
|
variant: "outline",
|
||||||
|
}, () => String(row.getValue("type"))),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "status",
|
||||||
|
header: "Status",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const status = row.getValue("status") as string
|
||||||
|
return h("div", { class: "flex items-center gap-2" }, [
|
||||||
|
status === "Done"
|
||||||
|
? h(IconCircleCheckFilled, { class: "h-4 w-4 text-emerald-500" })
|
||||||
|
: h(IconLoader, { class: "h-4 w-4 animate-spin text-muted-foreground" }),
|
||||||
|
h("span", {}, status),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "target",
|
||||||
|
header: () => h("div", { class: "flex items-center gap-1" }, [
|
||||||
|
"Target",
|
||||||
|
]),
|
||||||
|
cell: ({ row }) => h(Button, {
|
||||||
|
variant: "ghost",
|
||||||
|
size: "sm",
|
||||||
|
class: "h-auto p-1 text-xs font-mono",
|
||||||
|
}, () => [
|
||||||
|
h("span", { class: "ml-1 font-semibold" }, String(row.getValue("target"))),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "limit",
|
||||||
|
header: () => h("div", { class: "flex items-center gap-1" }, [
|
||||||
|
"Limit",
|
||||||
|
]),
|
||||||
|
cell: ({ row }) => h(Button, {
|
||||||
|
variant: "ghost",
|
||||||
|
size: "sm",
|
||||||
|
class: "h-auto p-1 text-xs font-mono",
|
||||||
|
}, () => [
|
||||||
|
h("span", { class: "ml-1 font-semibold" }, String(row.getValue("limit"))),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "reviewer",
|
||||||
|
header: "Reviewer",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const reviewer = row.getValue("reviewer") as string
|
||||||
|
const isAssigned = reviewer !== "Assign reviewer"
|
||||||
|
|
||||||
|
if (isAssigned) {
|
||||||
|
return h("span", {}, reviewer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h(Select, {}, {
|
||||||
|
default: () => [
|
||||||
|
h(SelectTrigger, { class: "w-full" }, {
|
||||||
|
default: () => h(SelectValue, { placeholder: "Assign reviewer" }),
|
||||||
|
}),
|
||||||
|
h(SelectContent, {}, {
|
||||||
|
default: () => [
|
||||||
|
h(SelectItem, { value: "eddie" }, () => "Eddie Lake"),
|
||||||
|
h(SelectItem, { value: "jamik" }, () => "Jamik Tashpulatov"),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
cell: () => h(DropdownMenu, {}, {
|
||||||
|
default: () => [
|
||||||
|
h(DropdownMenuTrigger, { asChild: true }, {
|
||||||
|
default: () => h(Button, {
|
||||||
|
variant: "ghost",
|
||||||
|
class: "h-8 w-8 p-0",
|
||||||
|
}, {
|
||||||
|
default: () => [
|
||||||
|
h("span", { class: "sr-only" }, "Open menu"),
|
||||||
|
h(IconDotsVertical, { class: "h-4 w-4" }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
h(DropdownMenuContent, { align: "end" }, {
|
||||||
|
default: () => [
|
||||||
|
h(DropdownMenuItem, {}, () => "Edit"),
|
||||||
|
h(DropdownMenuItem, {}, () => "Make a copy"),
|
||||||
|
h(DropdownMenuItem, {}, () => "Favorite"),
|
||||||
|
h(DropdownMenuSeparator, {}),
|
||||||
|
h(DropdownMenuItem, {}, () => "Delete"),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const table = useVueTable({
|
||||||
|
get data() {
|
||||||
|
return props.data
|
||||||
|
},
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
onSortingChange: (updaterOrValue) => {
|
||||||
|
sorting.value = typeof updaterOrValue === "function"
|
||||||
|
? updaterOrValue(sorting.value)
|
||||||
|
: updaterOrValue
|
||||||
|
},
|
||||||
|
onColumnFiltersChange: (updaterOrValue) => {
|
||||||
|
columnFilters.value = typeof updaterOrValue === "function"
|
||||||
|
? updaterOrValue(columnFilters.value)
|
||||||
|
: updaterOrValue
|
||||||
|
},
|
||||||
|
onColumnVisibilityChange: (updaterOrValue) => {
|
||||||
|
columnVisibility.value = typeof updaterOrValue === "function"
|
||||||
|
? updaterOrValue(columnVisibility.value)
|
||||||
|
: updaterOrValue
|
||||||
|
},
|
||||||
|
onRowSelectionChange: (updaterOrValue) => {
|
||||||
|
rowSelection.value = typeof updaterOrValue === "function"
|
||||||
|
? updaterOrValue(rowSelection.value)
|
||||||
|
: updaterOrValue
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
get sorting() { return sorting.value },
|
||||||
|
get columnFilters() { return columnFilters.value },
|
||||||
|
get columnVisibility() { return columnVisibility.value },
|
||||||
|
get rowSelection() { return rowSelection.value },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Tabs
|
||||||
|
default-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 default-value="outline">
|
||||||
|
<SelectTrigger
|
||||||
|
id="view-selector"
|
||||||
|
class="flex w-fit @4xl/main:hidden"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Select a view" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="outline">
|
||||||
|
Outline
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="past-performance">
|
||||||
|
Past Performance
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="key-personnel">
|
||||||
|
Key Personnel
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="focus-documents">
|
||||||
|
Focus Documents
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<TabsList class="**:data-[slot=badge]:bg-muted-foreground/30 hidden **:data-[slot=badge]:size-5 **:data-[slot=badge]:rounded-full **:data-[slot=badge]:px-1 @4xl/main:flex">
|
||||||
|
<TabsTrigger value="outline">
|
||||||
|
Outline
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="past-performance">
|
||||||
|
Past Performance <Badge variant="secondary">
|
||||||
|
3
|
||||||
|
</Badge>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="key-personnel">
|
||||||
|
Key Personnel <Badge variant="secondary">
|
||||||
|
2
|
||||||
|
</Badge>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="focus-documents">
|
||||||
|
Focus Documents
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger as-child>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<IconLayoutColumns />
|
||||||
|
<span class="hidden lg:inline">Customize Columns</span>
|
||||||
|
<span class="lg:hidden">Columns</span>
|
||||||
|
<IconChevronDown />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" class="w-56">
|
||||||
|
<template v-for="column in table.getAllColumns().filter((column) => typeof column.accessorFn !== 'undefined' && column.getCanHide())" :key="column.id">
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
class="capitalize"
|
||||||
|
:model-value="column.getIsVisible()"
|
||||||
|
@update:model-value="(value) => {
|
||||||
|
|
||||||
|
column.toggleVisibility(!!value)
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ column.id }}
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
</template>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<IconPlus />
|
||||||
|
<span class="hidden lg:inline">Add Section</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TabsContent
|
||||||
|
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="[RestrictToVerticalAxis]">
|
||||||
|
<Table>
|
||||||
|
<TableHeader class="bg-muted sticky top-0 z-10">
|
||||||
|
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
|
||||||
|
<TableHead v-for="header in headerGroup.headers" :key="header.id" :colspan="header.colSpan">
|
||||||
|
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header" :props="header.getContext()" />
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody class="**:data-[slot=table-cell]:first:w-8">
|
||||||
|
<template v-if="table.getRowModel().rows.length">
|
||||||
|
<DraggableRow v-for="row in table.getRowModel().rows" :key="row.id" :row="row" :index="row.index" />
|
||||||
|
</template>
|
||||||
|
<TableRow v-else>
|
||||||
|
<TableCell
|
||||||
|
:colspan="columns.length"
|
||||||
|
class="h-24 text-center"
|
||||||
|
>
|
||||||
|
No results.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</DragDropProvider>
|
||||||
|
<!-- <DndContext
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
modifiers={[restrictToVerticalAxis]}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
sensors={sensors}
|
||||||
|
id={sortableId}
|
||||||
|
> -->
|
||||||
|
<!-- </DndContext> -->
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between px-4">
|
||||||
|
<div class="text-muted-foreground hidden flex-1 text-sm 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
|
||||||
|
:model-value="table.getState().pagination.pageSize"
|
||||||
|
@update:model-value="(value) => {
|
||||||
|
table.setPageSize(Number(value))
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<SelectTrigger id="rows-per-page" size="sm" class="w-20">
|
||||||
|
<SelectValue :placeholder="`${table.getState().pagination.pageSize}`" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent side="top">
|
||||||
|
<SelectItem v-for="pageSize in [10, 20, 30, 40, 50]" :key="pageSize" :value="`${pageSize}`">
|
||||||
|
{{ pageSize }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-fit items-center justify-center text-sm font-medium">
|
||||||
|
Page {{ table.getState().pagination.pageIndex + 1 }} of
|
||||||
|
{{ table.getPageCount() }}
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto flex items-center gap-2 lg:ml-0">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
class="hidden h-8 w-8 p-0 lg:flex"
|
||||||
|
:disabled="!table.getCanPreviousPage()"
|
||||||
|
@click="table.setPageIndex(0)"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Go to first page</span>
|
||||||
|
<IconChevronsLeft />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
class="size-8"
|
||||||
|
size="icon"
|
||||||
|
:disabled="!table.getCanPreviousPage()"
|
||||||
|
@click="table.previousPage()"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Go to previous page</span>
|
||||||
|
<IconChevronLeft />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
class="size-8"
|
||||||
|
size="icon"
|
||||||
|
:disabled="!table.getCanNextPage()"
|
||||||
|
@click="table.nextPage()"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Go to next page</span>
|
||||||
|
<IconChevronRight />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
class="hidden size-8 lg:flex"
|
||||||
|
size="icon"
|
||||||
|
:disabled="!table.getCanNextPage()"
|
||||||
|
@click="table.setPageIndex(table.getPageCount() - 1)"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Go to last page</span>
|
||||||
|
<IconChevronsRight />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent
|
||||||
|
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" />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent 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" />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent
|
||||||
|
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" />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</template>
|
||||||
19
app/components/DragHandle.vue
Normal file
19
app/components/DragHandle.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { IconGripVertical } from "@tabler/icons-vue"
|
||||||
|
import { useSortableContext } from "dnd-kit-vue"
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
const { handleRef, sortable } = useSortableContext()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Button
|
||||||
|
:ref="handleRef"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="text-muted-foreground size-7 hover:bg-transparent"
|
||||||
|
>
|
||||||
|
<IconGripVertical class="text-muted-foreground size-3" />
|
||||||
|
<span class="sr-only">Drag to reorder</span>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
31
app/components/DraggableRow.vue
Normal file
31
app/components/DraggableRow.vue
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Row } from "@tanstack/vue-table"
|
||||||
|
import type { z } from "zod"
|
||||||
|
import type { schema } from "./DataTable.vue"
|
||||||
|
import { FlexRender } from "@tanstack/vue-table"
|
||||||
|
import { useSortable } from "dnd-kit-vue"
|
||||||
|
import {
|
||||||
|
TableCell,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table'
|
||||||
|
|
||||||
|
const props = defineProps<{ row: Row<z.infer<typeof schema>>, index: number }>()
|
||||||
|
|
||||||
|
const { elementRef, isDragging } = useSortable({
|
||||||
|
id: props.row.original.id,
|
||||||
|
index: props.index,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TableRow
|
||||||
|
:ref="elementRef"
|
||||||
|
:data-state="row.getIsSelected() && 'selected'"
|
||||||
|
:data-dragging="isDragging"
|
||||||
|
class="relative z-0 data-[dragging=true]:z-10 data-[dragging=true]:opacity-80"
|
||||||
|
>
|
||||||
|
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id">
|
||||||
|
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</template>
|
||||||
108
app/components/NavUser.vue
Normal file
108
app/components/NavUser.vue
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Bell,
|
||||||
|
CreditCard,
|
||||||
|
EllipsisVertical,
|
||||||
|
LogOut,
|
||||||
|
UserCircle,
|
||||||
|
} from "@lucide/vue"
|
||||||
|
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
AvatarFallback,
|
||||||
|
AvatarImage,
|
||||||
|
} from '@/components/ui/avatar'
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
import {
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
useSidebar,
|
||||||
|
} from '@/components/ui/sidebar'
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
avatar: string
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
user: User
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { isMobile } = useSidebar()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger as-child>
|
||||||
|
<SidebarMenuButton size="lg"
|
||||||
|
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
|
||||||
|
<Avatar class="h-8 w-8 rounded-lg grayscale">
|
||||||
|
<AvatarImage :src="user.avatar" :alt="user.name" />
|
||||||
|
<AvatarFallback class="rounded-lg">
|
||||||
|
CN
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span class="truncate font-medium">{{ user.name }}</span>
|
||||||
|
<span class="text-muted-foreground truncate text-xs">
|
||||||
|
{{ user.email }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<EllipsisVertical class="ml-auto size-4" />
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent class="w-(--reka-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||||
|
:side="isMobile ? 'bottom' : 'right'" :side-offset="4" align="end">
|
||||||
|
<DropdownMenuLabel class="p-0 font-normal">
|
||||||
|
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||||
|
<Avatar class="h-8 w-8 rounded-lg">
|
||||||
|
<AvatarImage :src="user.avatar" :alt="user.name" />
|
||||||
|
<AvatarFallback class="rounded-lg">
|
||||||
|
CN
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span class="truncate font-medium">{{ user.name }}</span>
|
||||||
|
<span class="text-muted-foreground truncate text-xs">
|
||||||
|
{{ user.email }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<UserCircle />
|
||||||
|
Account
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<CreditCard />
|
||||||
|
Billing
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Bell />
|
||||||
|
Bells
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<LogOut />
|
||||||
|
Log out
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</template>
|
||||||
106
app/components/SectionCards.vue
Normal file
106
app/components/SectionCards.vue
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { IconTrendingDown, IconTrendingUp } from "@tabler/icons-vue"
|
||||||
|
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:shadow-xs lg:px-6 @xl/main:grid-cols-2 @5xl/main:grid-cols-4">
|
||||||
|
<Card class="@container/card">
|
||||||
|
<CardHeader>
|
||||||
|
<CardDescription>Total Revenue</CardDescription>
|
||||||
|
<CardTitle class="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||||
|
$1,250.00
|
||||||
|
</CardTitle>
|
||||||
|
<CardAction>
|
||||||
|
<Badge variant="outline">
|
||||||
|
<IconTrendingUp />
|
||||||
|
+12.5%
|
||||||
|
</Badge>
|
||||||
|
</CardAction>
|
||||||
|
</CardHeader>
|
||||||
|
<CardFooter class="flex-col items-start gap-1.5 text-sm">
|
||||||
|
<div class="line-clamp-1 flex gap-2 font-medium">
|
||||||
|
Trending up this month <IconTrendingUp class="size-4" />
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground">
|
||||||
|
Visitors for the last 6 months
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
<Card class="@container/card">
|
||||||
|
<CardHeader>
|
||||||
|
<CardDescription>New Customers</CardDescription>
|
||||||
|
<CardTitle class="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||||
|
1,234
|
||||||
|
</CardTitle>
|
||||||
|
<CardAction>
|
||||||
|
<Badge variant="outline">
|
||||||
|
<IconTrendingDown />
|
||||||
|
-20%
|
||||||
|
</Badge>
|
||||||
|
</CardAction>
|
||||||
|
</CardHeader>
|
||||||
|
<CardFooter class="flex-col items-start gap-1.5 text-sm">
|
||||||
|
<div class="line-clamp-1 flex gap-2 font-medium">
|
||||||
|
Down 20% this period <IconTrendingDown class="size-4" />
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground">
|
||||||
|
Acquisition needs attention
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
<Card class="@container/card">
|
||||||
|
<CardHeader>
|
||||||
|
<CardDescription>Active Accounts</CardDescription>
|
||||||
|
<CardTitle class="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||||
|
45,678
|
||||||
|
</CardTitle>
|
||||||
|
<CardAction>
|
||||||
|
<Badge variant="outline">
|
||||||
|
<IconTrendingUp />
|
||||||
|
+12.5%
|
||||||
|
</Badge>
|
||||||
|
</CardAction>
|
||||||
|
</CardHeader>
|
||||||
|
<CardFooter class="flex-col items-start gap-1.5 text-sm">
|
||||||
|
<div class="line-clamp-1 flex gap-2 font-medium">
|
||||||
|
Strong user retention <IconTrendingUp class="size-4" />
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground">
|
||||||
|
Engagement exceed targets
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
<Card class="@container/card">
|
||||||
|
<CardHeader>
|
||||||
|
<CardDescription>Growth Rate</CardDescription>
|
||||||
|
<CardTitle class="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||||
|
4.5%
|
||||||
|
</CardTitle>
|
||||||
|
<CardAction>
|
||||||
|
<Badge variant="outline">
|
||||||
|
<IconTrendingUp />
|
||||||
|
+4.5%
|
||||||
|
</Badge>
|
||||||
|
</CardAction>
|
||||||
|
</CardHeader>
|
||||||
|
<CardFooter class="flex-col items-start gap-1.5 text-sm">
|
||||||
|
<div class="line-clamp-1 flex gap-2 font-medium">
|
||||||
|
Steady performance increase <IconTrendingUp class="size-4" />
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground">
|
||||||
|
Meets growth projections
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
101
app/components/SiteHeader.vue
Normal file
101
app/components/SiteHeader.vue
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
AvatarFallback,
|
||||||
|
AvatarImage,
|
||||||
|
} from '@/components/ui/avatar'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import { SidebarTrigger } from '@/components/ui/sidebar'
|
||||||
|
import {
|
||||||
|
Bell,
|
||||||
|
CreditCard,
|
||||||
|
Home,
|
||||||
|
LogOut,
|
||||||
|
UserCircle,
|
||||||
|
} from "@lucide/vue"
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
name: "shadcn",
|
||||||
|
email: "m@example.com",
|
||||||
|
avatar: "/avatars/shadcn.jpg",
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header
|
||||||
|
class="sticky top-0 z-10 flex h-(--header-height) shrink-0 items-center gap-2 border-b bg-background 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">
|
||||||
|
<SidebarTrigger class="-ml-1" />
|
||||||
|
<Separator orientation="vertical" class="mx-2 data-[orientation=vertical]:h-4" />
|
||||||
|
<div class="ml-auto flex items-center gap-2">
|
||||||
|
<Button variant="ghost" size="icon" class="size-8" as-child>
|
||||||
|
<a href="/">
|
||||||
|
<Home class="size-4" />
|
||||||
|
<span class="sr-only">Homepage</span>
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" class="size-8">
|
||||||
|
<Bell class="size-4" />
|
||||||
|
<span class="sr-only">Notifications</span>
|
||||||
|
</Button>
|
||||||
|
<Separator orientation="vertical" class="mx-1 data-[orientation=vertical]:h-4" />
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger as-child>
|
||||||
|
<Button variant="ghost" size="icon" class="size-8 rounded-full">
|
||||||
|
<Avatar class="h-8 w-8">
|
||||||
|
<AvatarImage :src="user.avatar" :alt="user.name" />
|
||||||
|
<AvatarFallback class="rounded-lg">
|
||||||
|
CN
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent class="w-56 rounded-lg" side="bottom" align="end">
|
||||||
|
<DropdownMenuLabel class="p-0 font-normal">
|
||||||
|
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||||
|
<Avatar class="h-8 w-8 rounded-lg">
|
||||||
|
<AvatarImage :src="user.avatar" :alt="user.name" />
|
||||||
|
<AvatarFallback class="rounded-lg">
|
||||||
|
CN
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span class="truncate font-medium">{{ user.name }}</span>
|
||||||
|
<span class="text-muted-foreground truncate text-xs">
|
||||||
|
{{ user.email }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<UserCircle />
|
||||||
|
Account
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<CreditCard />
|
||||||
|
Billing
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<LogOut />
|
||||||
|
Log out
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
18
app/components/ui/avatar/Avatar.vue
Normal file
18
app/components/ui/avatar/Avatar.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { AvatarRoot } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AvatarRoot
|
||||||
|
data-slot="avatar"
|
||||||
|
:class="cn('relative flex size-8 shrink-0 overflow-hidden rounded-full', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</AvatarRoot>
|
||||||
|
</template>
|
||||||
21
app/components/ui/avatar/AvatarFallback.vue
Normal file
21
app/components/ui/avatar/AvatarFallback.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { AvatarFallbackProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { AvatarFallback } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<AvatarFallbackProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AvatarFallback
|
||||||
|
data-slot="avatar-fallback"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="cn('bg-muted flex size-full items-center justify-center rounded-full', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</AvatarFallback>
|
||||||
|
</template>
|
||||||
16
app/components/ui/avatar/AvatarImage.vue
Normal file
16
app/components/ui/avatar/AvatarImage.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { AvatarImageProps } from "reka-ui"
|
||||||
|
import { AvatarImage } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<AvatarImageProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AvatarImage
|
||||||
|
data-slot="avatar-image"
|
||||||
|
v-bind="props"
|
||||||
|
class="aspect-square size-full"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</AvatarImage>
|
||||||
|
</template>
|
||||||
3
app/components/ui/avatar/index.ts
Normal file
3
app/components/ui/avatar/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { default as Avatar } from "./Avatar.vue"
|
||||||
|
export { default as AvatarFallback } from "./AvatarFallback.vue"
|
||||||
|
export { default as AvatarImage } from "./AvatarImage.vue"
|
||||||
26
app/components/ui/badge/Badge.vue
Normal file
26
app/components/ui/badge/Badge.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PrimitiveProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import type { BadgeVariants } from "."
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { Primitive } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { badgeVariants } from "."
|
||||||
|
|
||||||
|
const props = defineProps<PrimitiveProps & {
|
||||||
|
variant?: BadgeVariants["variant"]
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
data-slot="badge"
|
||||||
|
:class="cn(badgeVariants({ variant }), props.class)"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
26
app/components/ui/badge/index.ts
Normal file
26
app/components/ui/badge/index.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import type { VariantProps } from "class-variance-authority"
|
||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
|
export { default as Badge } from "./Badge.vue"
|
||||||
|
|
||||||
|
export const badgeVariants = cva(
|
||||||
|
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
export type BadgeVariants = VariantProps<typeof badgeVariants>
|
||||||
59
app/components/ui/chart/ChartContainer.vue
Normal file
59
app/components/ui/chart/ChartContainer.vue
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import type { ChartConfig } from "."
|
||||||
|
import { useId } from "reka-ui"
|
||||||
|
import { computed, toRefs } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { provideChartContext } from "."
|
||||||
|
import ChartStyle from "./ChartStyle.vue"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
id?: HTMLAttributes["id"]
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
config: ChartConfig
|
||||||
|
cursor?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineSlots<{
|
||||||
|
default: {
|
||||||
|
id: string
|
||||||
|
config: ChartConfig
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { config } = toRefs(props)
|
||||||
|
const uniqueId = useId()
|
||||||
|
const chartId = computed(() => `chart-${props.id || uniqueId.replace(/:/g, "")}`)
|
||||||
|
|
||||||
|
provideChartContext({
|
||||||
|
id: uniqueId,
|
||||||
|
config,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="chart"
|
||||||
|
:data-chart="chartId"
|
||||||
|
:class="cn(
|
||||||
|
`[&_.tick_text]:!fill-muted-foreground [&_.tick_line]:!stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex flex-col aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden [&_[data-vis-xy-container]]:h-full [&_[data-vis-single-container]]:h-full h-full [&_[data-vis-xy-container]]:w-full [&_[data-vis-single-container]]:w-full w-full `,
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
:style="{
|
||||||
|
'--vis-tooltip-padding': '0px',
|
||||||
|
'--vis-tooltip-background-color': 'transparent',
|
||||||
|
'--vis-tooltip-border-color': 'transparent',
|
||||||
|
'--vis-tooltip-text-color': 'none',
|
||||||
|
'--vis-tooltip-shadow-color': 'none',
|
||||||
|
'--vis-tooltip-backdrop-filter': 'none',
|
||||||
|
'--vis-crosshair-circle-stroke-color': '#0000',
|
||||||
|
'--vis-crosshair-line-stroke-width': cursor ? '1px' : '0px',
|
||||||
|
'--vis-font-family': 'var(--font-sans)',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot :id="uniqueId" :config="config" />
|
||||||
|
<ChartStyle :id="chartId" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
60
app/components/ui/chart/ChartLegendContent.vue
Normal file
60
app/components/ui/chart/ChartLegendContent.vue
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { computed, onMounted, ref } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { useChart } from "."
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
hideIcon?: boolean
|
||||||
|
nameKey?: string
|
||||||
|
verticalAlign?: "bottom" | "top"
|
||||||
|
// payload?: any[]
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>(), {
|
||||||
|
verticalAlign: "bottom",
|
||||||
|
})
|
||||||
|
|
||||||
|
const { id, config } = useChart()
|
||||||
|
|
||||||
|
const payload = computed(() => Object.entries(config.value).map(([key, value]) => {
|
||||||
|
return {
|
||||||
|
key: props.nameKey || key,
|
||||||
|
itemConfig: value,
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const containerSelector = ref("")
|
||||||
|
onMounted(() => {
|
||||||
|
containerSelector.value = `[data-chart="chart-${id}"]>[data-vis-xy-container]`
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="containerSelector"
|
||||||
|
:class="cn(
|
||||||
|
'flex items-center justify-center gap-4',
|
||||||
|
verticalAlign === 'top' ? 'pb-3' : 'pt-3',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="{ key, itemConfig } in payload"
|
||||||
|
:key="key"
|
||||||
|
:class="cn(
|
||||||
|
'[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3',
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<component :is="itemConfig.icon" v-if="itemConfig.icon" />
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="h-2 w-2 shrink-0 rounded-xs"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: itemConfig?.color,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{{ itemConfig.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
42
app/components/ui/chart/ChartStyle.vue
Normal file
42
app/components/ui/chart/ChartStyle.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { Primitive } from "reka-ui"
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { THEMES, useChart } from "."
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
id?: HTMLAttributes["id"]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { config } = useChart()
|
||||||
|
|
||||||
|
const colorConfig = computed(() => {
|
||||||
|
return Object.entries(config.value).filter(
|
||||||
|
([, config]) => config.theme || config.color,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
v-if="colorConfig.length"
|
||||||
|
as="style"
|
||||||
|
>
|
||||||
|
{{ Object.entries(THEMES)
|
||||||
|
.map(
|
||||||
|
([theme, prefix]) => `
|
||||||
|
${prefix} [data-chart=${id}] {
|
||||||
|
${colorConfig
|
||||||
|
.map(([key, itemConfig]) => {
|
||||||
|
const color
|
||||||
|
= itemConfig.theme?.[theme as keyof typeof itemConfig.theme]
|
||||||
|
|| itemConfig.color
|
||||||
|
return color ? ` --color-${key}: ${color};` : null
|
||||||
|
})
|
||||||
|
.join("\n")}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.join("\n") }}
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
105
app/components/ui/chart/ChartTooltipContent.vue
Normal file
105
app/components/ui/chart/ChartTooltipContent.vue
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import type { ChartConfig } from "."
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
hideLabel?: boolean
|
||||||
|
hideIndicator?: boolean
|
||||||
|
indicator?: "line" | "dot" | "dashed"
|
||||||
|
nameKey?: string
|
||||||
|
labelKey?: string
|
||||||
|
labelFormatter?: (d: number | Date) => string
|
||||||
|
payload?: Record<string, any>
|
||||||
|
config?: ChartConfig
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
color?: string
|
||||||
|
x?: number | Date
|
||||||
|
}>(), {
|
||||||
|
payload: () => ({}),
|
||||||
|
config: () => ({}),
|
||||||
|
indicator: "dot",
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: currently we use `createElement` and `render` to render the
|
||||||
|
// const chartContext = useChart(null)
|
||||||
|
|
||||||
|
const payload = computed(() => {
|
||||||
|
return Object.entries(props.payload).map(([key, value]) => {
|
||||||
|
// const key = `${props.nameKey || item.name || item.dataKey || "value"}`
|
||||||
|
const itemConfig = props.config[key]
|
||||||
|
const indicatorColor = props.config[key]?.color ?? props.payload.fill
|
||||||
|
|
||||||
|
return { key, value, itemConfig, indicatorColor }
|
||||||
|
}).filter(i => i.itemConfig)
|
||||||
|
})
|
||||||
|
|
||||||
|
const nestLabel = computed(() => Object.keys(props.payload).length === 1 && props.indicator !== "dot")
|
||||||
|
const tooltipLabel = computed(() => {
|
||||||
|
if (props.hideLabel)
|
||||||
|
return null
|
||||||
|
if (props.labelFormatter && props.x !== undefined) {
|
||||||
|
return props.labelFormatter(props.x)
|
||||||
|
}
|
||||||
|
return props.labelKey ? props.config[props.labelKey]?.label || props.payload[props.labelKey] : props.x
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="cn(
|
||||||
|
'border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<div v-if="!nestLabel && tooltipLabel" class="font-medium">
|
||||||
|
{{ tooltipLabel }}
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-1.5">
|
||||||
|
<div
|
||||||
|
v-for="{ value, itemConfig, indicatorColor, key } in payload"
|
||||||
|
:key="key"
|
||||||
|
:class="
|
||||||
|
cn('[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5',
|
||||||
|
indicator === 'dot' && 'items-center')"
|
||||||
|
>
|
||||||
|
<component :is="itemConfig.icon" v-if="itemConfig?.icon" />
|
||||||
|
<template v-else-if="!hideIndicator">
|
||||||
|
<div
|
||||||
|
:class="cn(
|
||||||
|
'shrink-0 rounded-xs border-(--color-border) bg-(--color-bg)',
|
||||||
|
{
|
||||||
|
'h-2.5 w-2.5': indicator === 'dot',
|
||||||
|
'w-1': indicator === 'line',
|
||||||
|
'w-0 border-[1.5px] border-dashed bg-transparent':
|
||||||
|
indicator === 'dashed',
|
||||||
|
'my-0.5': nestLabel && indicator === 'dashed',
|
||||||
|
},
|
||||||
|
)"
|
||||||
|
:style="{
|
||||||
|
'--color-bg': indicatorColor,
|
||||||
|
'--color-border': indicatorColor,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div :class="cn('flex flex-1 justify-between leading-none', nestLabel ? 'items-end' : 'items-center')">
|
||||||
|
<div class="grid gap-1.5">
|
||||||
|
<div v-if="nestLabel" class="font-medium">
|
||||||
|
{{ tooltipLabel }}
|
||||||
|
</div>
|
||||||
|
<span class="text-muted-foreground">
|
||||||
|
{{ itemConfig?.label || value }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span v-if="value" class="text-foreground font-mono font-medium tabular-nums">
|
||||||
|
{{ value.toLocaleString() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
29
app/components/ui/chart/index.ts
Normal file
29
app/components/ui/chart/index.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import type { Component, Ref } from "vue"
|
||||||
|
import { createContext } from "reka-ui"
|
||||||
|
|
||||||
|
export { default as ChartContainer } from "./ChartContainer.vue"
|
||||||
|
export { default as ChartLegendContent } from "./ChartLegendContent.vue"
|
||||||
|
export { default as ChartTooltipContent } from "./ChartTooltipContent.vue"
|
||||||
|
export { componentToString } from "./utils"
|
||||||
|
|
||||||
|
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||||
|
export const THEMES = { light: "", dark: ".dark" } as const
|
||||||
|
|
||||||
|
export type ChartConfig = {
|
||||||
|
[k in string]: {
|
||||||
|
label?: string | Component
|
||||||
|
icon?: string | Component
|
||||||
|
} & (
|
||||||
|
| { color?: string, theme?: never }
|
||||||
|
| { color?: never, theme: Record<keyof typeof THEMES, string> }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartContextProps {
|
||||||
|
id: string
|
||||||
|
config: Ref<ChartConfig>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const [useChart, provideChartContext] = createContext<ChartContextProps>("Chart")
|
||||||
|
|
||||||
|
export { VisCrosshair as ChartCrosshair, VisTooltip as ChartTooltip } from "@unovis/vue"
|
||||||
44
app/components/ui/chart/utils.ts
Normal file
44
app/components/ui/chart/utils.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import type { ChartConfig } from "."
|
||||||
|
import { isClient } from "@vueuse/core"
|
||||||
|
import { useId } from "reka-ui"
|
||||||
|
import { h, render } from "vue"
|
||||||
|
|
||||||
|
// Simple cache using a Map to store serialized object keys
|
||||||
|
const cache = new Map<string, string>()
|
||||||
|
|
||||||
|
// Convert object to a consistent string key
|
||||||
|
function serializeKey(key: Record<string, any>): string {
|
||||||
|
return JSON.stringify(key, Object.keys(key).sort())
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Constructor<P = any> {
|
||||||
|
__isFragment?: never
|
||||||
|
__isTeleport?: never
|
||||||
|
__isSuspense?: never
|
||||||
|
new (...args: any[]): {
|
||||||
|
$props: P
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function componentToString<P>(config: ChartConfig, component: Constructor<P>, props?: P) {
|
||||||
|
if (!isClient)
|
||||||
|
return
|
||||||
|
|
||||||
|
// This function will be called once during mount lifecycle
|
||||||
|
const id = useId()
|
||||||
|
|
||||||
|
// https://unovis.dev/docs/auxiliary/Crosshair#component-props
|
||||||
|
return (_data: any, x: number | Date) => {
|
||||||
|
const data = "data" in _data ? _data.data : _data
|
||||||
|
const serializedKey = `${id}-${serializeKey(data)}`
|
||||||
|
const cachedContent = cache.get(serializedKey)
|
||||||
|
if (cachedContent)
|
||||||
|
return cachedContent
|
||||||
|
|
||||||
|
const vnode = h<unknown>(component, { ...props, payload: data, config, x })
|
||||||
|
const div = document.createElement("div")
|
||||||
|
render(vnode, div)
|
||||||
|
cache.set(serializedKey, div.innerHTML)
|
||||||
|
return div.innerHTML
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/components/ui/checkbox/Checkbox.vue
Normal file
35
app/components/ui/checkbox/Checkbox.vue
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { CheckboxRootEmits, CheckboxRootProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { Check } from "@lucide/vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
const emits = defineEmits<CheckboxRootEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CheckboxRoot
|
||||||
|
v-slot="slotProps"
|
||||||
|
data-slot="checkbox"
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class="
|
||||||
|
cn('peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
props.class)"
|
||||||
|
>
|
||||||
|
<CheckboxIndicator
|
||||||
|
data-slot="checkbox-indicator"
|
||||||
|
class="grid place-content-center text-current transition-none"
|
||||||
|
>
|
||||||
|
<slot v-bind="slotProps">
|
||||||
|
<Check class="size-3.5" />
|
||||||
|
</slot>
|
||||||
|
</CheckboxIndicator>
|
||||||
|
</CheckboxRoot>
|
||||||
|
</template>
|
||||||
1
app/components/ui/checkbox/index.ts
Normal file
1
app/components/ui/checkbox/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as Checkbox } from "./Checkbox.vue"
|
||||||
19
app/components/ui/dropdown-menu/DropdownMenu.vue
Normal file
19
app/components/ui/dropdown-menu/DropdownMenu.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DropdownMenuRootEmits, DropdownMenuRootProps } from "reka-ui"
|
||||||
|
import { DropdownMenuRoot, useForwardPropsEmits } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuRootProps>()
|
||||||
|
const emits = defineEmits<DropdownMenuRootEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuRoot
|
||||||
|
v-slot="slotProps"
|
||||||
|
data-slot="dropdown-menu"
|
||||||
|
v-bind="forwarded"
|
||||||
|
>
|
||||||
|
<slot v-bind="slotProps" />
|
||||||
|
</DropdownMenuRoot>
|
||||||
|
</template>
|
||||||
39
app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
Normal file
39
app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DropdownMenuCheckboxItemEmits, DropdownMenuCheckboxItemProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { Check } from "@lucide/vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import {
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuItemIndicator,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class=" cn(
|
||||||
|
`focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuItemIndicator>
|
||||||
|
<slot name="indicator-icon">
|
||||||
|
<Check class="size-4" />
|
||||||
|
</slot>
|
||||||
|
</DropdownMenuItemIndicator>
|
||||||
|
</span>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
</template>
|
||||||
39
app/components/ui/dropdown-menu/DropdownMenuContent.vue
Normal file
39
app/components/ui/dropdown-menu/DropdownMenuContent.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DropdownMenuContentEmits, DropdownMenuContentProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import {
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes["class"] }>(),
|
||||||
|
{
|
||||||
|
sideOffset: 4,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const emits = defineEmits<DropdownMenuContentEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuPortal>
|
||||||
|
<DropdownMenuContent
|
||||||
|
data-slot="dropdown-menu-content"
|
||||||
|
v-bind="{ ...$attrs, ...forwarded }"
|
||||||
|
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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 z-50 max-h-(--reka-dropdown-menu-content-available-height) min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenuPortal>
|
||||||
|
</template>
|
||||||
15
app/components/ui/dropdown-menu/DropdownMenuGroup.vue
Normal file
15
app/components/ui/dropdown-menu/DropdownMenuGroup.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DropdownMenuGroupProps } from "reka-ui"
|
||||||
|
import { DropdownMenuGroup } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuGroupProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuGroup
|
||||||
|
data-slot="dropdown-menu-group"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</template>
|
||||||
31
app/components/ui/dropdown-menu/DropdownMenuItem.vue
Normal file
31
app/components/ui/dropdown-menu/DropdownMenuItem.vue
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DropdownMenuItemProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { DropdownMenuItem, useForwardProps } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<DropdownMenuItemProps & {
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
inset?: boolean
|
||||||
|
variant?: "default" | "destructive"
|
||||||
|
}>(), {
|
||||||
|
variant: "default",
|
||||||
|
})
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "inset", "variant", "class")
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuItem
|
||||||
|
data-slot="dropdown-menu-item"
|
||||||
|
:data-inset="inset ? '' : undefined"
|
||||||
|
:data-variant="variant"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="cn(`relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!`, props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</template>
|
||||||
23
app/components/ui/dropdown-menu/DropdownMenuLabel.vue
Normal file
23
app/components/ui/dropdown-menu/DropdownMenuLabel.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DropdownMenuLabelProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { DropdownMenuLabel, useForwardProps } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuLabelProps & { class?: HTMLAttributes["class"], inset?: boolean }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class", "inset")
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuLabel
|
||||||
|
data-slot="dropdown-menu-label"
|
||||||
|
:data-inset="inset ? '' : undefined"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
</template>
|
||||||
21
app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
Normal file
21
app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DropdownMenuRadioGroupEmits, DropdownMenuRadioGroupProps } from "reka-ui"
|
||||||
|
import {
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuRadioGroupProps>()
|
||||||
|
const emits = defineEmits<DropdownMenuRadioGroupEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuRadioGroup
|
||||||
|
data-slot="dropdown-menu-radio-group"
|
||||||
|
v-bind="forwarded"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuRadioGroup>
|
||||||
|
</template>
|
||||||
40
app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue
Normal file
40
app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DropdownMenuRadioItemEmits, DropdownMenuRadioItemProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { Circle } from "@lucide/vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import {
|
||||||
|
DropdownMenuItemIndicator,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const emits = defineEmits<DropdownMenuRadioItemEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuRadioItem
|
||||||
|
data-slot="dropdown-menu-radio-item"
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class="cn(
|
||||||
|
`focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuItemIndicator>
|
||||||
|
<slot name="indicator-icon">
|
||||||
|
<Circle class="size-2 fill-current" />
|
||||||
|
</slot>
|
||||||
|
</DropdownMenuItemIndicator>
|
||||||
|
</span>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
</template>
|
||||||
23
app/components/ui/dropdown-menu/DropdownMenuSeparator.vue
Normal file
23
app/components/ui/dropdown-menu/DropdownMenuSeparator.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DropdownMenuSeparatorProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import {
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
} from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuSeparatorProps & {
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuSeparator
|
||||||
|
data-slot="dropdown-menu-separator"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="cn('bg-border -mx-1 my-1 h-px', props.class)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
17
app/components/ui/dropdown-menu/DropdownMenuShortcut.vue
Normal file
17
app/components/ui/dropdown-menu/DropdownMenuShortcut.vue
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
data-slot="dropdown-menu-shortcut"
|
||||||
|
:class="cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
18
app/components/ui/dropdown-menu/DropdownMenuSub.vue
Normal file
18
app/components/ui/dropdown-menu/DropdownMenuSub.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DropdownMenuSubEmits, DropdownMenuSubProps } from "reka-ui"
|
||||||
|
import {
|
||||||
|
DropdownMenuSub,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuSubProps>()
|
||||||
|
const emits = defineEmits<DropdownMenuSubEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuSub v-slot="slotProps" data-slot="dropdown-menu-sub" v-bind="forwarded">
|
||||||
|
<slot v-bind="slotProps" />
|
||||||
|
</DropdownMenuSub>
|
||||||
|
</template>
|
||||||
27
app/components/ui/dropdown-menu/DropdownMenuSubContent.vue
Normal file
27
app/components/ui/dropdown-menu/DropdownMenuSubContent.vue
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DropdownMenuSubContentEmits, DropdownMenuSubContentProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import {
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
const emits = defineEmits<DropdownMenuSubContentEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuSubContent
|
||||||
|
data-slot="dropdown-menu-sub-content"
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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 z-50 min-w-[8rem] max-w-(--reka-dropdown-menu-content-available-width) origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</template>
|
||||||
31
app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
Normal file
31
app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DropdownMenuSubTriggerProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { ChevronRight } from "@lucide/vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import {
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
useForwardProps,
|
||||||
|
} from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes["class"], inset?: boolean }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class", "inset")
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuSubTrigger
|
||||||
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:data-inset="inset ? '' : undefined"
|
||||||
|
:class="cn(
|
||||||
|
`relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!`,
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<ChevronRight class="ml-auto size-4" />
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
</template>
|
||||||
17
app/components/ui/dropdown-menu/DropdownMenuTrigger.vue
Normal file
17
app/components/ui/dropdown-menu/DropdownMenuTrigger.vue
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DropdownMenuTriggerProps } from "reka-ui"
|
||||||
|
import { DropdownMenuTrigger, useForwardProps } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuTriggerProps>()
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(props)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
data-slot="dropdown-menu-trigger"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
</template>
|
||||||
16
app/components/ui/dropdown-menu/index.ts
Normal file
16
app/components/ui/dropdown-menu/index.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export { default as DropdownMenu } from "./DropdownMenu.vue"
|
||||||
|
|
||||||
|
export { default as DropdownMenuCheckboxItem } from "./DropdownMenuCheckboxItem.vue"
|
||||||
|
export { default as DropdownMenuContent } from "./DropdownMenuContent.vue"
|
||||||
|
export { default as DropdownMenuGroup } from "./DropdownMenuGroup.vue"
|
||||||
|
export { default as DropdownMenuItem } from "./DropdownMenuItem.vue"
|
||||||
|
export { default as DropdownMenuLabel } from "./DropdownMenuLabel.vue"
|
||||||
|
export { default as DropdownMenuRadioGroup } from "./DropdownMenuRadioGroup.vue"
|
||||||
|
export { default as DropdownMenuRadioItem } from "./DropdownMenuRadioItem.vue"
|
||||||
|
export { default as DropdownMenuSeparator } from "./DropdownMenuSeparator.vue"
|
||||||
|
export { default as DropdownMenuShortcut } from "./DropdownMenuShortcut.vue"
|
||||||
|
export { default as DropdownMenuSub } from "./DropdownMenuSub.vue"
|
||||||
|
export { default as DropdownMenuSubContent } from "./DropdownMenuSubContent.vue"
|
||||||
|
export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue"
|
||||||
|
export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue"
|
||||||
|
export { DropdownMenuPortal } from "reka-ui"
|
||||||
19
app/components/ui/select/Select.vue
Normal file
19
app/components/ui/select/Select.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectRootEmits, SelectRootProps } from "reka-ui"
|
||||||
|
import { SelectRoot, useForwardPropsEmits } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<SelectRootProps>()
|
||||||
|
const emits = defineEmits<SelectRootEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectRoot
|
||||||
|
v-slot="slotProps"
|
||||||
|
data-slot="select"
|
||||||
|
v-bind="forwarded"
|
||||||
|
>
|
||||||
|
<slot v-bind="slotProps" />
|
||||||
|
</SelectRoot>
|
||||||
|
</template>
|
||||||
51
app/components/ui/select/SelectContent.vue
Normal file
51
app/components/ui/select/SelectContent.vue
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectContentEmits, SelectContentProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import {
|
||||||
|
SelectContent,
|
||||||
|
SelectPortal,
|
||||||
|
SelectViewport,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { SelectScrollDownButton, SelectScrollUpButton } from "."
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<SelectContentProps & { class?: HTMLAttributes["class"] }>(),
|
||||||
|
{
|
||||||
|
position: "popper",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const emits = defineEmits<SelectContentEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectPortal>
|
||||||
|
<SelectContent
|
||||||
|
data-slot="select-content"
|
||||||
|
v-bind="{ ...$attrs, ...forwarded }"
|
||||||
|
:class="cn(
|
||||||
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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 relative z-50 max-h-(--reka-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
|
||||||
|
position === 'popper'
|
||||||
|
&& 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectViewport :class="cn('p-1', position === 'popper' && 'h-(--reka-select-trigger-height) w-full min-w-(--reka-select-trigger-width) scroll-my-1')">
|
||||||
|
<slot />
|
||||||
|
</SelectViewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectContent>
|
||||||
|
</SelectPortal>
|
||||||
|
</template>
|
||||||
15
app/components/ui/select/SelectGroup.vue
Normal file
15
app/components/ui/select/SelectGroup.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectGroupProps } from "reka-ui"
|
||||||
|
import { SelectGroup } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<SelectGroupProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectGroup
|
||||||
|
data-slot="select-group"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</SelectGroup>
|
||||||
|
</template>
|
||||||
44
app/components/ui/select/SelectItem.vue
Normal file
44
app/components/ui/select/SelectItem.vue
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectItemProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { Check } from "@lucide/vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import {
|
||||||
|
SelectItem,
|
||||||
|
SelectItemIndicator,
|
||||||
|
SelectItemText,
|
||||||
|
useForwardProps,
|
||||||
|
} from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<SelectItemProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectItem
|
||||||
|
data-slot="select-item"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
`focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2`,
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span class="absolute right-2 flex size-3.5 items-center justify-center">
|
||||||
|
<SelectItemIndicator>
|
||||||
|
<slot name="indicator-icon">
|
||||||
|
<Check class="size-4" />
|
||||||
|
</slot>
|
||||||
|
</SelectItemIndicator>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<SelectItemText>
|
||||||
|
<slot />
|
||||||
|
</SelectItemText>
|
||||||
|
</SelectItem>
|
||||||
|
</template>
|
||||||
15
app/components/ui/select/SelectItemText.vue
Normal file
15
app/components/ui/select/SelectItemText.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectItemTextProps } from "reka-ui"
|
||||||
|
import { SelectItemText } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<SelectItemTextProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectItemText
|
||||||
|
data-slot="select-item-text"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</SelectItemText>
|
||||||
|
</template>
|
||||||
17
app/components/ui/select/SelectLabel.vue
Normal file
17
app/components/ui/select/SelectLabel.vue
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectLabelProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { SelectLabel } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<SelectLabelProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectLabel
|
||||||
|
data-slot="select-label"
|
||||||
|
:class="cn('text-muted-foreground px-2 py-1.5 text-xs', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</SelectLabel>
|
||||||
|
</template>
|
||||||
26
app/components/ui/select/SelectScrollDownButton.vue
Normal file
26
app/components/ui/select/SelectScrollDownButton.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectScrollDownButtonProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { ChevronDown } from "@lucide/vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { SelectScrollDownButton, useForwardProps } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<SelectScrollDownButtonProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectScrollDownButton
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="cn('flex cursor-default items-center justify-center py-1', props.class)"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<ChevronDown class="size-4" />
|
||||||
|
</slot>
|
||||||
|
</SelectScrollDownButton>
|
||||||
|
</template>
|
||||||
26
app/components/ui/select/SelectScrollUpButton.vue
Normal file
26
app/components/ui/select/SelectScrollUpButton.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectScrollUpButtonProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { ChevronUp } from "@lucide/vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { SelectScrollUpButton, useForwardProps } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<SelectScrollUpButtonProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectScrollUpButton
|
||||||
|
data-slot="select-scroll-up-button"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="cn('flex cursor-default items-center justify-center py-1', props.class)"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<ChevronUp class="size-4" />
|
||||||
|
</slot>
|
||||||
|
</SelectScrollUpButton>
|
||||||
|
</template>
|
||||||
19
app/components/ui/select/SelectSeparator.vue
Normal file
19
app/components/ui/select/SelectSeparator.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectSeparatorProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { SelectSeparator } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<SelectSeparatorProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectSeparator
|
||||||
|
data-slot="select-separator"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="cn('bg-border pointer-events-none -mx-1 my-1 h-px', props.class)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
33
app/components/ui/select/SelectTrigger.vue
Normal file
33
app/components/ui/select/SelectTrigger.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectTriggerProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { ChevronDown } from "@lucide/vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { SelectIcon, SelectTrigger, useForwardProps } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<SelectTriggerProps & { class?: HTMLAttributes["class"], size?: "sm" | "default" }>(),
|
||||||
|
{ size: "default" },
|
||||||
|
)
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class", "size")
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectTrigger
|
||||||
|
data-slot="select-trigger"
|
||||||
|
:data-size="size"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="cn(
|
||||||
|
`border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground 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:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<SelectIcon as-child>
|
||||||
|
<ChevronDown class="size-4 opacity-50" />
|
||||||
|
</SelectIcon>
|
||||||
|
</SelectTrigger>
|
||||||
|
</template>
|
||||||
15
app/components/ui/select/SelectValue.vue
Normal file
15
app/components/ui/select/SelectValue.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectValueProps } from "reka-ui"
|
||||||
|
import { SelectValue } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<SelectValueProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectValue
|
||||||
|
data-slot="select-value"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</SelectValue>
|
||||||
|
</template>
|
||||||
11
app/components/ui/select/index.ts
Normal file
11
app/components/ui/select/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export { default as Select } from "./Select.vue"
|
||||||
|
export { default as SelectContent } from "./SelectContent.vue"
|
||||||
|
export { default as SelectGroup } from "./SelectGroup.vue"
|
||||||
|
export { default as SelectItem } from "./SelectItem.vue"
|
||||||
|
export { default as SelectItemText } from "./SelectItemText.vue"
|
||||||
|
export { default as SelectLabel } from "./SelectLabel.vue"
|
||||||
|
export { default as SelectScrollDownButton } from "./SelectScrollDownButton.vue"
|
||||||
|
export { default as SelectScrollUpButton } from "./SelectScrollUpButton.vue"
|
||||||
|
export { default as SelectSeparator } from "./SelectSeparator.vue"
|
||||||
|
export { default as SelectTrigger } from "./SelectTrigger.vue"
|
||||||
|
export { default as SelectValue } from "./SelectValue.vue"
|
||||||
19
app/components/ui/sheet/Sheet.vue
Normal file
19
app/components/ui/sheet/Sheet.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogRootEmits, DialogRootProps } from "reka-ui"
|
||||||
|
import { DialogRoot, useForwardPropsEmits } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<DialogRootProps>()
|
||||||
|
const emits = defineEmits<DialogRootEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogRoot
|
||||||
|
v-slot="slotProps"
|
||||||
|
data-slot="sheet"
|
||||||
|
v-bind="forwarded"
|
||||||
|
>
|
||||||
|
<slot v-bind="slotProps" />
|
||||||
|
</DialogRoot>
|
||||||
|
</template>
|
||||||
15
app/components/ui/sheet/SheetClose.vue
Normal file
15
app/components/ui/sheet/SheetClose.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogCloseProps } from "reka-ui"
|
||||||
|
import { DialogClose } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<DialogCloseProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogClose
|
||||||
|
data-slot="sheet-close"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DialogClose>
|
||||||
|
</template>
|
||||||
62
app/components/ui/sheet/SheetContent.vue
Normal file
62
app/components/ui/sheet/SheetContent.vue
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { X } from "@lucide/vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import {
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogPortal,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import SheetOverlay from "./SheetOverlay.vue"
|
||||||
|
|
||||||
|
interface SheetContentProps extends DialogContentProps {
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
side?: "top" | "right" | "bottom" | "left"
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<SheetContentProps>(), {
|
||||||
|
side: "right",
|
||||||
|
})
|
||||||
|
const emits = defineEmits<DialogContentEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class", "side")
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogPortal>
|
||||||
|
<SheetOverlay />
|
||||||
|
<DialogContent
|
||||||
|
data-slot="sheet-content"
|
||||||
|
:class="cn(
|
||||||
|
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||||
|
side === 'right'
|
||||||
|
&& 'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
|
||||||
|
side === 'left'
|
||||||
|
&& 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
|
||||||
|
side === 'top'
|
||||||
|
&& 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
|
||||||
|
side === 'bottom'
|
||||||
|
&& 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
|
||||||
|
props.class)"
|
||||||
|
v-bind="{ ...$attrs, ...forwarded }"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<DialogClose
|
||||||
|
class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"
|
||||||
|
>
|
||||||
|
<X class="size-4" />
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogContent>
|
||||||
|
</DialogPortal>
|
||||||
|
</template>
|
||||||
21
app/components/ui/sheet/SheetDescription.vue
Normal file
21
app/components/ui/sheet/SheetDescription.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogDescriptionProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { DialogDescription } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogDescription
|
||||||
|
data-slot="sheet-description"
|
||||||
|
:class="cn('text-muted-foreground text-sm', props.class)"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DialogDescription>
|
||||||
|
</template>
|
||||||
16
app/components/ui/sheet/SheetFooter.vue
Normal file
16
app/components/ui/sheet/SheetFooter.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="sheet-footer"
|
||||||
|
:class="cn('mt-auto flex flex-col gap-2 p-4', props.class)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
15
app/components/ui/sheet/SheetHeader.vue
Normal file
15
app/components/ui/sheet/SheetHeader.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="sheet-header"
|
||||||
|
:class="cn('flex flex-col gap-1.5 p-4', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
21
app/components/ui/sheet/SheetOverlay.vue
Normal file
21
app/components/ui/sheet/SheetOverlay.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogOverlayProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { DialogOverlay } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogOverlay
|
||||||
|
data-slot="sheet-overlay"
|
||||||
|
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80', props.class)"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DialogOverlay>
|
||||||
|
</template>
|
||||||
21
app/components/ui/sheet/SheetTitle.vue
Normal file
21
app/components/ui/sheet/SheetTitle.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogTitleProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { DialogTitle } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogTitle
|
||||||
|
data-slot="sheet-title"
|
||||||
|
:class="cn('text-foreground font-semibold', props.class)"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DialogTitle>
|
||||||
|
</template>
|
||||||
15
app/components/ui/sheet/SheetTrigger.vue
Normal file
15
app/components/ui/sheet/SheetTrigger.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogTriggerProps } from "reka-ui"
|
||||||
|
import { DialogTrigger } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<DialogTriggerProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogTrigger
|
||||||
|
data-slot="sheet-trigger"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DialogTrigger>
|
||||||
|
</template>
|
||||||
8
app/components/ui/sheet/index.ts
Normal file
8
app/components/ui/sheet/index.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export { default as Sheet } from "./Sheet.vue"
|
||||||
|
export { default as SheetClose } from "./SheetClose.vue"
|
||||||
|
export { default as SheetContent } from "./SheetContent.vue"
|
||||||
|
export { default as SheetDescription } from "./SheetDescription.vue"
|
||||||
|
export { default as SheetFooter } from "./SheetFooter.vue"
|
||||||
|
export { default as SheetHeader } from "./SheetHeader.vue"
|
||||||
|
export { default as SheetTitle } from "./SheetTitle.vue"
|
||||||
|
export { default as SheetTrigger } from "./SheetTrigger.vue"
|
||||||
96
app/components/ui/sidebar/Sidebar.vue
Normal file
96
app/components/ui/sidebar/Sidebar.vue
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SidebarProps } from "."
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Sheet, SheetContent } from '@/components/ui/sheet'
|
||||||
|
import SheetDescription from '@/components/ui/sheet/SheetDescription.vue'
|
||||||
|
import SheetHeader from '@/components/ui/sheet/SheetHeader.vue'
|
||||||
|
import SheetTitle from '@/components/ui/sheet/SheetTitle.vue'
|
||||||
|
import { SIDEBAR_WIDTH_MOBILE, useSidebar } from "./utils"
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<SidebarProps>(), {
|
||||||
|
side: "left",
|
||||||
|
variant: "sidebar",
|
||||||
|
collapsible: "offcanvas",
|
||||||
|
})
|
||||||
|
|
||||||
|
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="collapsible === 'none'"
|
||||||
|
data-slot="sidebar"
|
||||||
|
:class="cn('bg-sidebar text-sidebar-foreground flex w-(--sidebar-width) flex-col', props.class)"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Sheet v-else-if="isMobile" :open="openMobile" v-bind="$attrs" @update:open="setOpenMobile">
|
||||||
|
<SheetContent
|
||||||
|
data-sidebar="sidebar"
|
||||||
|
data-slot="sidebar"
|
||||||
|
data-mobile="true"
|
||||||
|
:side="side"
|
||||||
|
class="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||||
|
:style="{
|
||||||
|
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<SheetHeader class="sr-only">
|
||||||
|
<SheetTitle>Sidebar</SheetTitle>
|
||||||
|
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||||
|
</SheetHeader>
|
||||||
|
<div class="flex h-full w-full flex-col">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="group peer text-sidebar-foreground hidden md:block"
|
||||||
|
data-slot="sidebar"
|
||||||
|
:data-state="state"
|
||||||
|
:data-collapsible="state === 'collapsed' ? collapsible : ''"
|
||||||
|
:data-variant="variant"
|
||||||
|
:data-side="side"
|
||||||
|
>
|
||||||
|
<!-- This is what handles the sidebar gap on desktop -->
|
||||||
|
<div
|
||||||
|
:class="cn(
|
||||||
|
'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
|
||||||
|
'group-data-[collapsible=offcanvas]:w-0',
|
||||||
|
'group-data-[side=right]:rotate-180',
|
||||||
|
variant === 'floating' || variant === 'inset'
|
||||||
|
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
|
||||||
|
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
|
||||||
|
)"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
:class="cn(
|
||||||
|
'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
|
||||||
|
side === 'left'
|
||||||
|
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
|
||||||
|
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
|
||||||
|
// Adjust the padding for floating and inset variants.
|
||||||
|
variant === 'floating' || variant === 'inset'
|
||||||
|
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
|
||||||
|
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-sidebar="sidebar"
|
||||||
|
class="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
18
app/components/ui/sidebar/SidebarContent.vue
Normal file
18
app/components/ui/sidebar/SidebarContent.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-content"
|
||||||
|
data-sidebar="content"
|
||||||
|
:class="cn('flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
18
app/components/ui/sidebar/SidebarFooter.vue
Normal file
18
app/components/ui/sidebar/SidebarFooter.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-footer"
|
||||||
|
data-sidebar="footer"
|
||||||
|
:class="cn('flex flex-col gap-2 p-2', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
18
app/components/ui/sidebar/SidebarGroup.vue
Normal file
18
app/components/ui/sidebar/SidebarGroup.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-group"
|
||||||
|
data-sidebar="group"
|
||||||
|
:class="cn('relative flex w-full min-w-0 flex-col p-2', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
27
app/components/ui/sidebar/SidebarGroupAction.vue
Normal file
27
app/components/ui/sidebar/SidebarGroupAction.vue
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PrimitiveProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { Primitive } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<PrimitiveProps & {
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
data-slot="sidebar-group-action"
|
||||||
|
data-sidebar="group-action"
|
||||||
|
:as="as"
|
||||||
|
:as-child="asChild"
|
||||||
|
:class="cn(
|
||||||
|
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
|
'after:absolute after:-inset-2 md:after:hidden',
|
||||||
|
'group-data-[collapsible=icon]:hidden',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
18
app/components/ui/sidebar/SidebarGroupContent.vue
Normal file
18
app/components/ui/sidebar/SidebarGroupContent.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-group-content"
|
||||||
|
data-sidebar="group-content"
|
||||||
|
:class="cn('w-full text-sm', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
25
app/components/ui/sidebar/SidebarGroupLabel.vue
Normal file
25
app/components/ui/sidebar/SidebarGroupLabel.vue
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PrimitiveProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { Primitive } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<PrimitiveProps & {
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
data-slot="sidebar-group-label"
|
||||||
|
data-sidebar="group-label"
|
||||||
|
:as="as"
|
||||||
|
:as-child="asChild"
|
||||||
|
:class="cn(
|
||||||
|
'text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
|
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
|
||||||
|
props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
18
app/components/ui/sidebar/SidebarHeader.vue
Normal file
18
app/components/ui/sidebar/SidebarHeader.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-header"
|
||||||
|
data-sidebar="header"
|
||||||
|
:class="cn('flex flex-col gap-2 p-2', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
22
app/components/ui/sidebar/SidebarInput.vue
Normal file
22
app/components/ui/sidebar/SidebarInput.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Input
|
||||||
|
data-slot="sidebar-input"
|
||||||
|
data-sidebar="input"
|
||||||
|
:class="cn(
|
||||||
|
'bg-background h-8 w-full shadow-none',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Input>
|
||||||
|
</template>
|
||||||
21
app/components/ui/sidebar/SidebarInset.vue
Normal file
21
app/components/ui/sidebar/SidebarInset.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main
|
||||||
|
data-slot="sidebar-inset"
|
||||||
|
:class="cn(
|
||||||
|
'bg-background relative flex w-full flex-1 flex-col',
|
||||||
|
'md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
18
app/components/ui/sidebar/SidebarMenu.vue
Normal file
18
app/components/ui/sidebar/SidebarMenu.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ul
|
||||||
|
data-slot="sidebar-menu"
|
||||||
|
data-sidebar="menu"
|
||||||
|
:class="cn('flex w-full min-w-0 flex-col gap-1', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
35
app/components/ui/sidebar/SidebarMenuAction.vue
Normal file
35
app/components/ui/sidebar/SidebarMenuAction.vue
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PrimitiveProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { Primitive } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<PrimitiveProps & {
|
||||||
|
showOnHover?: boolean
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>(), {
|
||||||
|
as: "button",
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
data-slot="sidebar-menu-action"
|
||||||
|
data-sidebar="menu-action"
|
||||||
|
:class="cn(
|
||||||
|
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
|
'after:absolute after:-inset-2 md:after:hidden',
|
||||||
|
'peer-data-[size=sm]/menu-button:top-1',
|
||||||
|
'peer-data-[size=default]/menu-button:top-1.5',
|
||||||
|
'peer-data-[size=lg]/menu-button:top-2.5',
|
||||||
|
'group-data-[collapsible=icon]:hidden',
|
||||||
|
showOnHover
|
||||||
|
&& 'peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
:as="as"
|
||||||
|
:as-child="asChild"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
26
app/components/ui/sidebar/SidebarMenuBadge.vue
Normal file
26
app/components/ui/sidebar/SidebarMenuBadge.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-menu-badge"
|
||||||
|
data-sidebar="menu-badge"
|
||||||
|
:class="cn(
|
||||||
|
'text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none',
|
||||||
|
'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
|
||||||
|
'peer-data-[size=sm]/menu-button:top-1',
|
||||||
|
'peer-data-[size=default]/menu-button:top-1.5',
|
||||||
|
'peer-data-[size=lg]/menu-button:top-2.5',
|
||||||
|
'group-data-[collapsible=icon]:hidden',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
48
app/components/ui/sidebar/SidebarMenuButton.vue
Normal file
48
app/components/ui/sidebar/SidebarMenuButton.vue
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Component } from "vue"
|
||||||
|
import type { SidebarMenuButtonProps } from "./SidebarMenuButtonChild.vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
|
import SidebarMenuButtonChild from "./SidebarMenuButtonChild.vue"
|
||||||
|
import { useSidebar } from "./utils"
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<SidebarMenuButtonProps & {
|
||||||
|
tooltip?: string | Component
|
||||||
|
}>(), {
|
||||||
|
as: "button",
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
})
|
||||||
|
|
||||||
|
const { isMobile, state } = useSidebar()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "tooltip")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SidebarMenuButtonChild v-if="!tooltip" v-bind="{ ...delegatedProps, ...$attrs }">
|
||||||
|
<slot />
|
||||||
|
</SidebarMenuButtonChild>
|
||||||
|
|
||||||
|
<Tooltip v-else>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<SidebarMenuButtonChild v-bind="{ ...delegatedProps, ...$attrs }">
|
||||||
|
<slot />
|
||||||
|
</SidebarMenuButtonChild>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="right"
|
||||||
|
align="center"
|
||||||
|
:hidden="state !== 'collapsed' || isMobile"
|
||||||
|
>
|
||||||
|
<template v-if="typeof tooltip === 'string'">
|
||||||
|
{{ tooltip }}
|
||||||
|
</template>
|
||||||
|
<component :is="tooltip" v-else />
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</template>
|
||||||
36
app/components/ui/sidebar/SidebarMenuButtonChild.vue
Normal file
36
app/components/ui/sidebar/SidebarMenuButtonChild.vue
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PrimitiveProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import type { SidebarMenuButtonVariants } from "."
|
||||||
|
import { Primitive } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { sidebarMenuButtonVariants } from "."
|
||||||
|
|
||||||
|
export interface SidebarMenuButtonProps extends PrimitiveProps {
|
||||||
|
variant?: SidebarMenuButtonVariants["variant"]
|
||||||
|
size?: SidebarMenuButtonVariants["size"]
|
||||||
|
isActive?: boolean
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<SidebarMenuButtonProps>(), {
|
||||||
|
as: "button",
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
data-slot="sidebar-menu-button"
|
||||||
|
data-sidebar="menu-button"
|
||||||
|
:data-size="size"
|
||||||
|
:data-active="isActive"
|
||||||
|
:class="cn(sidebarMenuButtonVariants({ variant, size }), props.class)"
|
||||||
|
:as="as"
|
||||||
|
:as-child="asChild"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
18
app/components/ui/sidebar/SidebarMenuItem.vue
Normal file
18
app/components/ui/sidebar/SidebarMenuItem.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<li
|
||||||
|
data-slot="sidebar-menu-item"
|
||||||
|
data-sidebar="menu-item"
|
||||||
|
:class="cn('group/menu-item relative', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
35
app/components/ui/sidebar/SidebarMenuSkeleton.vue
Normal file
35
app/components/ui/sidebar/SidebarMenuSkeleton.vue
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
showIcon?: boolean
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const width = computed(() => {
|
||||||
|
return `${Math.floor(Math.random() * 40) + 50}%`
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-menu-skeleton"
|
||||||
|
data-sidebar="menu-skeleton"
|
||||||
|
:class="cn('flex h-8 items-center gap-2 rounded-md px-2', props.class)"
|
||||||
|
>
|
||||||
|
<Skeleton
|
||||||
|
v-if="showIcon"
|
||||||
|
class="size-4 rounded-md"
|
||||||
|
data-sidebar="menu-skeleton-icon"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Skeleton
|
||||||
|
class="h-4 max-w-(--skeleton-width) flex-1"
|
||||||
|
data-sidebar="menu-skeleton-text"
|
||||||
|
:style="{ '--skeleton-width': width }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
22
app/components/ui/sidebar/SidebarMenuSub.vue
Normal file
22
app/components/ui/sidebar/SidebarMenuSub.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ul
|
||||||
|
data-slot="sidebar-menu-sub"
|
||||||
|
data-sidebar="menu-badge"
|
||||||
|
:class="cn(
|
||||||
|
'border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5',
|
||||||
|
'group-data-[collapsible=icon]:hidden',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
36
app/components/ui/sidebar/SidebarMenuSubButton.vue
Normal file
36
app/components/ui/sidebar/SidebarMenuSubButton.vue
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PrimitiveProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { Primitive } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<PrimitiveProps & {
|
||||||
|
size?: "sm" | "md"
|
||||||
|
isActive?: boolean
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>(), {
|
||||||
|
as: "a",
|
||||||
|
size: "md",
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
data-slot="sidebar-menu-sub-button"
|
||||||
|
data-sidebar="menu-sub-button"
|
||||||
|
:as="as"
|
||||||
|
:as-child="asChild"
|
||||||
|
:data-size="size"
|
||||||
|
:data-active="isActive"
|
||||||
|
:class="cn(
|
||||||
|
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
|
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
|
||||||
|
size === 'sm' && 'text-xs',
|
||||||
|
size === 'md' && 'text-sm',
|
||||||
|
'group-data-[collapsible=icon]:hidden',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
18
app/components/ui/sidebar/SidebarMenuSubItem.vue
Normal file
18
app/components/ui/sidebar/SidebarMenuSubItem.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<li
|
||||||
|
data-slot="sidebar-menu-sub-item"
|
||||||
|
data-sidebar="menu-sub-item"
|
||||||
|
:class="cn('group/menu-sub-item relative', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
82
app/components/ui/sidebar/SidebarProvider.vue
Normal file
82
app/components/ui/sidebar/SidebarProvider.vue
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes, Ref } from "vue"
|
||||||
|
import { defaultDocument, useEventListener, useMediaQuery, useVModel } from "@vueuse/core"
|
||||||
|
import { TooltipProvider } from "reka-ui"
|
||||||
|
import { computed, ref } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { provideSidebarContext, SIDEBAR_COOKIE_MAX_AGE, SIDEBAR_COOKIE_NAME, SIDEBAR_KEYBOARD_SHORTCUT, SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON } from "./utils"
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
defaultOpen?: boolean
|
||||||
|
open?: boolean
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>(), {
|
||||||
|
defaultOpen: !defaultDocument?.cookie.includes(`${SIDEBAR_COOKIE_NAME}=false`),
|
||||||
|
open: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
"update:open": [open: boolean]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isMobile = useMediaQuery("(max-width: 768px)")
|
||||||
|
const openMobile = ref(false)
|
||||||
|
|
||||||
|
const open = useVModel(props, "open", emits, {
|
||||||
|
defaultValue: props.defaultOpen ?? false,
|
||||||
|
passive: (props.open === undefined) as false,
|
||||||
|
}) as Ref<boolean>
|
||||||
|
|
||||||
|
function setOpen(value: boolean) {
|
||||||
|
open.value = value // emits('update:open', value)
|
||||||
|
|
||||||
|
// This sets the cookie to keep the sidebar state.
|
||||||
|
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open.value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOpenMobile(value: boolean) {
|
||||||
|
openMobile.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to toggle the sidebar.
|
||||||
|
function toggleSidebar() {
|
||||||
|
return isMobile.value ? setOpenMobile(!openMobile.value) : setOpen(!open.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEventListener("keydown", (event: KeyboardEvent) => {
|
||||||
|
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
|
||||||
|
event.preventDefault()
|
||||||
|
toggleSidebar()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||||
|
// This makes it easier to style the sidebar with Tailwind classes.
|
||||||
|
const state = computed(() => open.value ? "expanded" : "collapsed")
|
||||||
|
|
||||||
|
provideSidebarContext({
|
||||||
|
state,
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
isMobile,
|
||||||
|
openMobile,
|
||||||
|
setOpenMobile,
|
||||||
|
toggleSidebar,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TooltipProvider :delay-duration="0">
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-wrapper"
|
||||||
|
:style="{
|
||||||
|
'--sidebar-width': SIDEBAR_WIDTH,
|
||||||
|
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
|
||||||
|
}"
|
||||||
|
:class="cn('group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full', props.class)"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
</template>
|
||||||
33
app/components/ui/sidebar/SidebarRail.vue
Normal file
33
app/components/ui/sidebar/SidebarRail.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { useSidebar } from "./utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { toggleSidebar } = useSidebar()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
data-sidebar="rail"
|
||||||
|
data-slot="sidebar-rail"
|
||||||
|
aria-label="Toggle Sidebar"
|
||||||
|
:tabindex="-1"
|
||||||
|
title="Toggle Sidebar"
|
||||||
|
:class="cn(
|
||||||
|
'hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-0.5 sm:flex',
|
||||||
|
'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
|
||||||
|
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
|
||||||
|
'hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full',
|
||||||
|
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
|
||||||
|
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
@click="toggleSidebar"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
19
app/components/ui/sidebar/SidebarSeparator.vue
Normal file
19
app/components/ui/sidebar/SidebarSeparator.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Separator
|
||||||
|
data-slot="sidebar-separator"
|
||||||
|
data-sidebar="separator"
|
||||||
|
:class="cn('bg-sidebar-border mx-2 w-auto', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Separator>
|
||||||
|
</template>
|
||||||
27
app/components/ui/sidebar/SidebarTrigger.vue
Normal file
27
app/components/ui/sidebar/SidebarTrigger.vue
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { PanelLeft } from "@lucide/vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { useSidebar } from "./utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { toggleSidebar } = useSidebar()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Button
|
||||||
|
data-sidebar="trigger"
|
||||||
|
data-slot="sidebar-trigger"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
:class="cn('h-7 w-7', props.class)"
|
||||||
|
@click="toggleSidebar"
|
||||||
|
>
|
||||||
|
<PanelLeft />
|
||||||
|
<span class="sr-only">Toggle Sidebar</span>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
60
app/components/ui/sidebar/index.ts
Normal file
60
app/components/ui/sidebar/index.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import type { VariantProps } from "class-variance-authority"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
|
export interface SidebarProps {
|
||||||
|
side?: "left" | "right"
|
||||||
|
variant?: "sidebar" | "floating" | "inset"
|
||||||
|
collapsible?: "offcanvas" | "icon" | "none"
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export { default as Sidebar } from "./Sidebar.vue"
|
||||||
|
export { default as SidebarContent } from "./SidebarContent.vue"
|
||||||
|
export { default as SidebarFooter } from "./SidebarFooter.vue"
|
||||||
|
export { default as SidebarGroup } from "./SidebarGroup.vue"
|
||||||
|
export { default as SidebarGroupAction } from "./SidebarGroupAction.vue"
|
||||||
|
export { default as SidebarGroupContent } from "./SidebarGroupContent.vue"
|
||||||
|
export { default as SidebarGroupLabel } from "./SidebarGroupLabel.vue"
|
||||||
|
export { default as SidebarHeader } from "./SidebarHeader.vue"
|
||||||
|
export { default as SidebarInput } from "./SidebarInput.vue"
|
||||||
|
export { default as SidebarInset } from "./SidebarInset.vue"
|
||||||
|
export { default as SidebarMenu } from "./SidebarMenu.vue"
|
||||||
|
export { default as SidebarMenuAction } from "./SidebarMenuAction.vue"
|
||||||
|
export { default as SidebarMenuBadge } from "./SidebarMenuBadge.vue"
|
||||||
|
export { default as SidebarMenuButton } from "./SidebarMenuButton.vue"
|
||||||
|
export { default as SidebarMenuItem } from "./SidebarMenuItem.vue"
|
||||||
|
export { default as SidebarMenuSkeleton } from "./SidebarMenuSkeleton.vue"
|
||||||
|
export { default as SidebarMenuSub } from "./SidebarMenuSub.vue"
|
||||||
|
export { default as SidebarMenuSubButton } from "./SidebarMenuSubButton.vue"
|
||||||
|
export { default as SidebarMenuSubItem } from "./SidebarMenuSubItem.vue"
|
||||||
|
export { default as SidebarProvider } from "./SidebarProvider.vue"
|
||||||
|
export { default as SidebarRail } from "./SidebarRail.vue"
|
||||||
|
export { default as SidebarSeparator } from "./SidebarSeparator.vue"
|
||||||
|
export { default as SidebarTrigger } from "./SidebarTrigger.vue"
|
||||||
|
|
||||||
|
export { useSidebar } from "./utils"
|
||||||
|
|
||||||
|
export const sidebarMenuButtonVariants = cva(
|
||||||
|
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||||
|
outline:
|
||||||
|
"bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-8 text-sm",
|
||||||
|
sm: "h-7 text-xs",
|
||||||
|
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export type SidebarMenuButtonVariants = VariantProps<typeof sidebarMenuButtonVariants>
|
||||||
19
app/components/ui/sidebar/utils.ts
Normal file
19
app/components/ui/sidebar/utils.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import type { ComputedRef, Ref } from "vue"
|
||||||
|
import { createContext } from "reka-ui"
|
||||||
|
|
||||||
|
export const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
||||||
|
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
||||||
|
export const SIDEBAR_WIDTH = "16rem"
|
||||||
|
export const SIDEBAR_WIDTH_MOBILE = "18rem"
|
||||||
|
export const SIDEBAR_WIDTH_ICON = "3rem"
|
||||||
|
export const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||||
|
|
||||||
|
export const [useSidebar, provideSidebarContext] = createContext<{
|
||||||
|
state: ComputedRef<"expanded" | "collapsed">
|
||||||
|
open: Ref<boolean>
|
||||||
|
setOpen: (value: boolean) => void
|
||||||
|
isMobile: Ref<boolean>
|
||||||
|
openMobile: Ref<boolean>
|
||||||
|
setOpenMobile: (value: boolean) => void
|
||||||
|
toggleSidebar: () => void
|
||||||
|
}>("Sidebar")
|
||||||
17
app/components/ui/skeleton/Skeleton.vue
Normal file
17
app/components/ui/skeleton/Skeleton.vue
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
interface SkeletonProps {
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<SkeletonProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="skeleton"
|
||||||
|
:class="cn('animate-pulse rounded-md bg-primary/10', props.class)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
1
app/components/ui/skeleton/index.ts
Normal file
1
app/components/ui/skeleton/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as Skeleton } from "./Skeleton.vue"
|
||||||
16
app/components/ui/table/Table.vue
Normal file
16
app/components/ui/table/Table.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div data-slot="table-container" class="relative w-full overflow-auto">
|
||||||
|
<table data-slot="table" :class="cn('w-full caption-bottom text-sm', props.class)">
|
||||||
|
<slot />
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
17
app/components/ui/table/TableBody.vue
Normal file
17
app/components/ui/table/TableBody.vue
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<tbody
|
||||||
|
data-slot="table-body"
|
||||||
|
:class="cn('[&_tr:last-child]:border-0', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</tbody>
|
||||||
|
</template>
|
||||||
17
app/components/ui/table/TableCaption.vue
Normal file
17
app/components/ui/table/TableCaption.vue
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<caption
|
||||||
|
data-slot="table-caption"
|
||||||
|
:class="cn('text-muted-foreground mt-4 text-sm', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</caption>
|
||||||
|
</template>
|
||||||
22
app/components/ui/table/TableCell.vue
Normal file
22
app/components/ui/table/TableCell.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<td
|
||||||
|
data-slot="table-cell"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 *:[[role=checkbox]]:translate-y-0.5',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
34
app/components/ui/table/TableEmpty.vue
Normal file
34
app/components/ui/table/TableEmpty.vue
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import TableCell from "./TableCell.vue"
|
||||||
|
import TableRow from "./TableRow.vue"
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
colspan?: number
|
||||||
|
}>(), {
|
||||||
|
colspan: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'p-4 whitespace-nowrap align-middle text-sm text-foreground',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center py-10">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</template>
|
||||||
17
app/components/ui/table/TableFooter.vue
Normal file
17
app/components/ui/table/TableFooter.vue
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<tfoot
|
||||||
|
data-slot="table-footer"
|
||||||
|
:class="cn('bg-muted/50 border-t font-medium [&>tr]:last:border-b-0', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</tfoot>
|
||||||
|
</template>
|
||||||
17
app/components/ui/table/TableHead.vue
Normal file
17
app/components/ui/table/TableHead.vue
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<th
|
||||||
|
data-slot="table-head"
|
||||||
|
:class="cn('text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 *:[[role=checkbox]]:translate-y-0.5', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</th>
|
||||||
|
</template>
|
||||||
17
app/components/ui/table/TableHeader.vue
Normal file
17
app/components/ui/table/TableHeader.vue
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<thead
|
||||||
|
data-slot="table-header"
|
||||||
|
:class="cn('[&_tr]:border-b', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</thead>
|
||||||
|
</template>
|
||||||
17
app/components/ui/table/TableRow.vue
Normal file
17
app/components/ui/table/TableRow.vue
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<tr
|
||||||
|
data-slot="table-row"
|
||||||
|
:class="cn('hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
9
app/components/ui/table/index.ts
Normal file
9
app/components/ui/table/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export { default as Table } from "./Table.vue"
|
||||||
|
export { default as TableBody } from "./TableBody.vue"
|
||||||
|
export { default as TableCaption } from "./TableCaption.vue"
|
||||||
|
export { default as TableCell } from "./TableCell.vue"
|
||||||
|
export { default as TableEmpty } from "./TableEmpty.vue"
|
||||||
|
export { default as TableFooter } from "./TableFooter.vue"
|
||||||
|
export { default as TableHead } from "./TableHead.vue"
|
||||||
|
export { default as TableHeader } from "./TableHeader.vue"
|
||||||
|
export { default as TableRow } from "./TableRow.vue"
|
||||||
10
app/components/ui/table/utils.ts
Normal file
10
app/components/ui/table/utils.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import type { Updater } from "@tanstack/vue-table"
|
||||||
|
|
||||||
|
import type { Ref } from "vue"
|
||||||
|
import { isFunction } from "@tanstack/vue-table"
|
||||||
|
|
||||||
|
export function valueUpdater<T>(updaterOrValue: Updater<T>, ref: Ref<T>) {
|
||||||
|
ref.value = isFunction(updaterOrValue)
|
||||||
|
? updaterOrValue(ref.value)
|
||||||
|
: updaterOrValue
|
||||||
|
}
|
||||||
24
app/components/ui/tabs/Tabs.vue
Normal file
24
app/components/ui/tabs/Tabs.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { TabsRootEmits, TabsRootProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { TabsRoot, useForwardPropsEmits } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<TabsRootProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
const emits = defineEmits<TabsRootEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TabsRoot
|
||||||
|
v-slot="slotProps"
|
||||||
|
data-slot="tabs"
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class="cn('flex flex-col gap-2', props.class)"
|
||||||
|
>
|
||||||
|
<slot v-bind="slotProps" />
|
||||||
|
</TabsRoot>
|
||||||
|
</template>
|
||||||
21
app/components/ui/tabs/TabsContent.vue
Normal file
21
app/components/ui/tabs/TabsContent.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { TabsContentProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { TabsContent } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<TabsContentProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TabsContent
|
||||||
|
data-slot="tabs-content"
|
||||||
|
:class="cn('flex-1 outline-none', props.class)"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</TabsContent>
|
||||||
|
</template>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user