starting again

This commit is contained in:
Abhinav Raut
2024-05-25 11:49:15 +05:30
parent 7bf69d3b8c
commit edab73a8fa
223 changed files with 14810 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
<template>
<div class="bg-background text-foreground">
<div v-if="$route.path !== '/login'">
<ResizablePanelGroup direction="horizontal" auto-save-id="app.vue.resizable.panel">
<ResizablePanel id="resize-panel-1" collapsible :default-size="10" :collapsed-size="1" :min-size="3"
:max-size="20" :class="cn(isCollapsed && 'min-w-[50px] transition-all duration-200 ease-in-out')"
@expand="toggleNav(false)" @collapse="toggleNav(true)">
<NavBar :is-collapsed="isCollapsed" :links="navLinks" />
</ResizablePanel>
<ResizableHandle id="resize-handle-1" with-handle />
<ResizablePanel id="resize-panel-2">
<div class="w-full h-screen">
<RouterView />
</div>
</ResizablePanel>
</ResizablePanelGroup>
</div>
<div v-else>
<RouterView />
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue"
import { RouterView, useRouter } from 'vue-router'
import { cn } from '@/lib/utils'
import api from '@/api';
import { useUserStore } from '@/stores/user'
import NavBar from './components/NavBar.vue'
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@/components/ui/resizable'
// State
const isCollapsed = ref(false)
const navLinks = [
{
title: 'Dashboard',
component: 'dashboard',
label: '',
icon: 'lucide:layout-dashboard',
},
{
title: 'Conversations',
component: 'conversations',
label: '',
icon: 'lucide:message-circle-more',
},
]
const userStore = useUserStore()
const router = useRouter()
// Functions, methods
function toggleNav (v) {
isCollapsed.value = v
}
onMounted(() => {
api.getAgentProfile().then((resp) => {
if (resp.data.data) {
userStore.$patch((state) => {
state.userAvatar = resp.data.data.avatar_url
state.userFirstName = resp.data.data.first_name
state.userLastName = resp.data.data.last_name
})
}
}).catch((error) => {
if (error.response.status === 401) {
router.push("/login")
}
})
})
</script>
+47
View File
@@ -0,0 +1,47 @@
import axios from 'axios';
import qs from 'qs';
const http = axios.create({
timeout: 10000,
responseType: "json",
});
// Request interceptor.
http.interceptors.request.use((request) => {
// Set content type for POST/PUT requests.
if ((request.method === "post" || request.method === "put") && !request.headers["Content-Type"]) {
request.headers["Content-Type"] = "application/x-www-form-urlencoded"
request.data = qs.stringify(request.data)
}
return request
})
const login = (data) => http.post(`/api/login`, data);
const getTeams = () => http.get("/api/teams")
const getAgents = () => http.get("/api/agents")
const getAgentProfile = () => http.get("/api/profile")
const getTags = () => http.get("/api/tags")
const upsertTags = (uuid, data) => http.post(`/api/conversation/${uuid}/tags`, data);
const updateAssignee = (uuid, assignee_type, data) => http.put(`/api/conversation/${uuid}/assignee/${assignee_type}`, data);
const updateStatus = (uuid, data) => http.put(`/api/conversation/${uuid}/status`, data);
const updatePriority = (uuid, data) => http.put(`/api/conversation/${uuid}/priority`, data);
const getMessages = (uuid) => http.get(`/api/conversation/${uuid}/messages`);
const getConversation = (uuid) => http.get(`/api/conversation/${uuid}`);
const getConversations = () => http.get('/api/conversations');
const getCannedResponses = () => http.get('/api/canned_responses');
export default {
login,
getTags,
getTeams,
getAgents,
getConversation,
getConversations,
getMessages,
getAgentProfile,
updateAssignee,
updateStatus,
updatePriority,
upsertTags,
getCannedResponses,
}
+143
View File
@@ -0,0 +1,143 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
// App default font-size.
// Default: 16px, 15px looked very wide.
:root {
font-size: 14px;
}
.tab-container-default {
padding: 20px 20px;
}
// Theme.
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 20 14.3% 4.1%;
--card: 0 0% 100%;
--card-foreground: 20 14.3% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 20 14.3% 4.1%;
--primary: 24 9.8% 10%;
--primary-foreground: 60 9.1% 97.8%;
--secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%;
--muted: 60 4.8% 95.9%;
--muted-foreground: 25 5.3% 44.7%;
--accent: 60 4.8% 95.9%;
--accent-foreground: 24 9.8% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 60 9.1% 97.8%;
--border:20 5.9% 90%;
--input:20 5.9% 90%;
--ring:20 14.3% 4.1%;
--radius: 0.25rem;
}
.dark {
--background:20 14.3% 4.1%;
--foreground:60 9.1% 97.8%;
--card:20 14.3% 4.1%;
--card-foreground:60 9.1% 97.8%;
--popover:20 14.3% 4.1%;
--popover-foreground:60 9.1% 97.8%;
--primary:60 9.1% 97.8%;
--primary-foreground:24 9.8% 10%;
--secondary:12 6.5% 15.1%;
--secondary-foreground:60 9.1% 97.8%;
--muted:12 6.5% 15.1%;
--muted-foreground:24 5.4% 63.9%;
--accent:12 6.5% 15.1%;
--accent-foreground:60 9.1% 97.8%;
--destructive:0 62.8% 30.6%;
--destructive-foreground:60 9.1% 97.8%;
--border:12 6.5% 15.1%;
--input:12 6.5% 15.1%;
--ring:24 5.7% 82.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
// charts
@layer base {
:root {
--vis-tooltip-background-color: none !important;
--vis-tooltip-border-color: none !important;
--vis-tooltip-text-color: none !important;
--vis-tooltip-shadow-color: none !important;
--vis-tooltip-backdrop-filter: none !important;
--vis-tooltip-padding: none !important;
--vis-primary-color: var(--primary);
--vis-secondary-color: 160 81% 40%;
--vis-text-color: var(--muted-foreground);
}
}
// Shake animation
@keyframes shake {
0% {
transform: translateX(0);
}
15% {
transform: translateX(-5px);
}
25% {
transform: translateX(5px);
}
35% {
transform: translateX(-5px);
}
45% {
transform: translateX(5px);
}
55% {
transform: translateX(-5px);
}
65% {
transform: translateX(5px);
}
75% {
transform: translateX(-5px);
}
85% {
transform: translateX(5px);
}
95% {
transform: translateX(-5px);
}
100% {
transform: translateX(0);
}
}
.animate-shake {
animation: shake 0.5s infinite;
}
@@ -0,0 +1,17 @@
<script setup>
import { DonutChart } from '@/components/ui/chart-donut'
const valueFormatter = (tick) => typeof tick === 'number' ? `$ ${new Intl.NumberFormat('us').format(tick).toString()}` : ''
const dataDonut = [
{ name: 'Jan', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Feb', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Mar', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Apr', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'May', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Jun', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
]
</script>
<template>
<DonutChart index="name" :category="'total'" :data="dataDonut" :value-formatter="valueFormatter" />
</template>
@@ -0,0 +1,7 @@
<template>
<h1 class="h-screen flex items-center justify-center">
<div class="flex flex-row items-center justify-center">
<p>Select a conversation from the left panel.</p>
</div>
</h1>
</template>
@@ -0,0 +1,71 @@
<template>
<div class="h-screen text-left">
<Error :errorMessage="conversationStore.conversations.errorMessage"></Error>
<div v-if="!conversationStore.conversations.loading">
<ScrollArea>
<div class="border-b flex items-center cursor-pointer p-2 flex-row"
v-for="conversation in conversationStore.conversations.data" :key="conversation.uuid"
@click="router.push('/conversations/' + conversation.uuid)">
<div>
<Avatar class="size-[55px]">
<AvatarImage :src=conversation.contact_avatar_url />
<AvatarFallback>
{{ conversation.contact_first_name.substring(0, 2).toUpperCase() }}
</AvatarFallback>
</Avatar>
</div>
<div class="ml-3 w-full">
<div class="flex justify-between">
<div>
<p class="text-base font-normal">
{{ conversation.contact_first_name + ' ' + conversation.contact_last_name }}
</p>
</div>
<div>
<span class="text-xs text-muted-foreground">
{{ format(conversation.updated_at, 'h:mm a') }}
</span>
</div>
</div>
<div>
<p class="text-gray-600 max-w-xs text-sm dark:text-white">
{{ conversation.last_message }}
</p>
</div>
</div>
</div>
</ScrollArea>
</div>
<div v-else>
<div class="flex items-center gap-5 p-6 border-b" v-for="index in 10" :key="index">
<Skeleton class="h-12 w-12 rounded-full" />
<div class="space-y-2">
<Skeleton class="h-4 w-[250px]" />
<Skeleton class="h-4 w-[200px]" />
</div>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useConversationStore } from '@/stores/conversation'
import { format } from 'date-fns'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Error } from '@/components/ui/error'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Skeleton } from '@/components/ui/skeleton'
// Stores, states.
const conversationStore = useConversationStore()
const router = useRouter()
// Functions, methods.
onMounted(() => {
conversationStore.fetchConversations()
});
</script>
@@ -0,0 +1,376 @@
<template>
<div class="p-3">
<div>
<Avatar class="size-20">
<AvatarImage :src=conversationStore.conversation.data.contact_avatar_url />
<AvatarFallback>
{{ conversationStore.conversation.data.contact_first_name.toUpperCase().substring(0, 2) }}
</AvatarFallback>
</Avatar>
<h4 class="text-l ">
{{ conversationStore.conversation.data.contact_first_name + ' ' +
conversationStore.conversation.data.contact_last_name }}
</h4>
<p class="text-sm text-muted-foreground flex gap-2 mt-1" v-if="conversationStore.conversation.data.contact_email">
<Mail class="size-3 mt-1"></Mail>
{{ conversationStore.conversation.data.contact_email }}
</p>
<p class="text-sm text-muted-foreground flex gap-2 mt-1"
v-if="conversationStore.conversation.data.contact_phone_number">
<Phone class="size-3 mt-1"></Phone>
{{ conversationStore.conversation.data.contact_phone_number }}
</p>
</div>
<Accordion type="single" collapsible class="border-t mt-4">
<AccordionItem :value="actionAccordion.title">
<AccordionTrigger>
<p>{{ actionAccordion.title }}</p>
</AccordionTrigger>
<AccordionContent>
<!-- Assigned agent -->
<!-- <Select v-model="conversationStore.conversation.data.assigned_agent_uuid"
@update:modelValue="handleAssignedAgentChange" id="select-agent">
<SelectTrigger class="mb-3">
<SelectValue placeholder="Assigned agent" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Assigned agent</SelectLabel>
<SelectItem :value="agent.uuid" v-for="(agent) in agents" :key="agent.uuid">
{{ agent.first_name + ' ' + agent.last_name }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select> -->
<div class="mb-3">
<Popover v-model:open="agentSelectDropdownOpen">
<PopoverTrigger as-child>
<Button variant="outline" role="combobox" :aria-expanded="agentSelectDropdownOpen"
class="w-full justify-between">
{{ conversationStore.conversation.data.assigned_agent_uuid
? agents.find((agent) => agent.uuid ===
conversationStore.conversation.data.assigned_agent_uuid)?.first_name + ' ' + agents.find((agent) =>
agent.uuid === conversationStore.conversation.data.assigned_agent_uuid)?.last_name
: "Select agent..." }}
<CaretSortIcon class="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent class="p-0 PopoverContent">
<Command @update:modelValue="handleAssignedAgentChange">
<CommandInput class="h-9" placeholder="Search agent..." />
<CommandEmpty>No agent found.</CommandEmpty>
<CommandList>
<CommandGroup>
<CommandItem v-for="agent in agents" :key="agent.uuid"
:value="agent.first_name + ' ' + agent.last_name" @select="(ev) => {
if (typeof ev.detail.value === 'string') {
conversationStore.conversation.data.assigned_agent_uuid = ev.detail.value
}
agentSelectDropdownOpen = false
}">
{{ agent.first_name + ' ' + agent.last_name }}
<CheckIcon :class="cn(
'ml-auto h-4 w-4',
conversationStore.conversation.data.assigned_agent_uuid === agent.uuid ? 'opacity-100' : 'opacity-0',
)" />
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<!-- Assigned agent end -->
<!-- Assigned team -->
<!-- <Select v-model="conversationStore.conversation.data.assigned_team_uuid"
@update:modelValue="handleAssignedTeamChange">
<SelectTrigger class="mb-3">
<SelectValue placeholder="Assigned team" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Assigned team</SelectLabel>
<SelectItem :value="team.uuid" v-for="(team) in teams" :key="team.uuid">
{{ team.name }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select> -->
<div class="mb-3">
<Popover v-model:open="teamSelectDropdownOpen">
<PopoverTrigger as-child>
<Button variant="outline" role="combobox" :aria-expanded="teamSelectDropdownOpen"
class="w-full justify-between">
{{ conversationStore.conversation.data.assigned_team_uuid
? teams.find((team) => team.uuid ===
conversationStore.conversation.data.assigned_team_uuid)?.name
: "Select team..." }}
<CaretSortIcon class="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent class="p-0 PopoverContent">
<Command @update:modelValue="handleAssignedTeamChange">
<CommandInput class="h-9" placeholder="Search team..." />
<CommandEmpty>No team found.</CommandEmpty>
<CommandList>
<CommandGroup>
<CommandItem v-for="team in teams" :key="team.uuid" :value="team.name" @select="(ev) => {
if (ev.detail.value) {
const selectedTeamName = ev.detail.value;
const selectedTeam = teams.find(team => team.name === selectedTeamName);
if (selectedTeam) {
conversationStore.conversation.data.assigned_team_uuid = selectedTeam.uuid;
}
}
teamSelectDropdownOpen = false
}">
{{ team.name }}
<CheckIcon :class="cn(
'ml-auto h-4 w-4',
conversationStore.conversation.data.assigned_team_uuid === team.uuid ? 'opacity-100' : 'opacity-0',
)" />
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<!-- assigned team end -->
<!-- Priority -->
<Select v-model="conversationStore.conversation.data.priority" @update:modelValue="handlePriorityChange">
<SelectTrigger class="mb-3">
<SelectValue placeholder="Priority" class="font-medium" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Priority</SelectLabel>
<SelectItem value="Low">
Low
</SelectItem>
<SelectItem value="Medium">
Medium
</SelectItem>
<SelectItem value="High">
High
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<!-- Priority -->
<!-- Tags -->
<TagsInput class="px-0 gap-0 w-full" :model-value="tagsSelected" @update:modelValue="handleUpsertTags">
<div class="flex gap-2 flex-wrap items-center px-3">
<TagsInputItem v-for="item in tagsSelected" :key="item" :value="item">
<TagsInputItemText />
<TagsInputItemDelete />
</TagsInputItem>
</div>
<ComboboxRoot v-model="tagsSelected" v-model:open="tagDropdownOpen" v-model:searchTerm="tagSearchTerm"
class="w-full">
<ComboboxAnchor as-child>
<ComboboxInput placeholder="Add tags..." as-child>
<TagsInputInput class="w-full px-3" :class="tagsSelected.length > 0 ? 'mt-2' : ''"
@keydown.enter.prevent />
</ComboboxInput>
</ComboboxAnchor>
<ComboboxPortal>
<CommandList position="popper"
class="w-[--radix-popper-anchor-width] rounded-md mt-2 border bg-popover text-popover-foreground shadow-md outline-none 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">
<CommandEmpty />
<CommandGroup>
<CommandItem v-for="ftag in tagsFiltered" :key="ftag.value" :value="ftag.label" @select.prevent="(ev) => {
if (typeof ev.detail.value === 'string') {
tagSearchTerm = ''
tagsSelected.push(ev.detail.value)
tagDropdownOpen = false
}
if (tagsFiltered.length === 0) {
tagDropdownOpen = false
}
}">
{{ ftag.label }}
</CommandItem>
</CommandGroup>
</CommandList>
</ComboboxPortal>
</ComboboxRoot>
</TagsInput>
<!-- Tags end -->
</AccordionContent>
</AccordionItem>
</Accordion>
<Accordion type="single" collapsible>
<AccordionItem :value="infoAccordion.title">
<AccordionTrigger>
<p>{{ infoAccordion.title }}</p>
</AccordionTrigger>
<AccordionContent>
<div class="flex flex-col gap-1 mb-5">
<p class="font-medium">Initiated at</p>
<p>
{{ format(conversationStore.conversation.data.created_at, "PPpp") }}
</p>
</div>
<div class="flex flex-col gap-1 mb-5">
<p class="font-medium">
Resolved at
</p>
<p v-if="conversationStore.conversation.data.resolved_at">
{{ format(conversationStore.conversation.data.resolved_at, "PPpp") }}
</p>
<p v-else>
-
</p>
</div>
<div class="flex flex-col gap-1 mb-5">
<p class="font-medium">
Closed at
</p>
<p v-if="conversationStore.conversation.data.closed_at">
{{ format(conversationStore.conversation.data.closed_at, "PPpp") }}
</p>
<p v-else>
-
</p>
</div>
<div class="flex flex-col gap-1 mb-5">
<p class="font-medium">SLA</p>
<p>48 hours remaining</p>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useConversationStore } from '@/stores/conversation'
import { format } from 'date-fns'
import api from '@/api';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { CaretSortIcon, CheckIcon } from '@radix-icons/vue'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { ComboboxAnchor, ComboboxInput, ComboboxPortal, ComboboxRoot } from 'radix-vue'
import { CommandEmpty, CommandGroup, CommandInput, Command, CommandItem, CommandList } from '@/components/ui/command'
import { TagsInput, TagsInputInput, TagsInputItem, TagsInputItemDelete, TagsInputItemText } from '@/components/ui/tags-input'
import { Mail, Phone } from "lucide-vue-next"
// Stores, states.
const conversationStore = useConversationStore();
const agents = ref([])
const teams = ref([])
const agentSelectDropdownOpen = ref(false)
const teamSelectDropdownOpen = ref(false)
const tags = ref([])
const tagIDMap = {}
const tagsSelected = ref(conversationStore.conversation.data.tags)
const tagDropdownOpen = ref(false)
const tagSearchTerm = ref('')
const teamSearchTerm = ref('')
const tagsFiltered = computed(() => tags.value.filter(i => !tagsSelected.value.includes(i.label)))
// const agentsFiltered = computed(() => tags.value.filter(i => !tagsSelected.value.includes(i.label)))
const actionAccordion = {
"title": "Action"
}
const infoAccordion = {
"title": "Information"
}
// Functions, methods.
onMounted(() => {
api.getAgents().then((resp) => {
agents.value = resp.data.data;
}).catch(error => {
console.log(error)
})
api.getTeams().then((resp) => {
teams.value = resp.data.data;
}).catch(error => {
console.log(error)
})
api.getTags().then(async (resp) => {
let dt = resp.data.data
dt.forEach(item => {
tags.value.push({
label: item.name,
value: item.id,
})
tagIDMap[item.name] = item.id
})
})
})
const handleAssignedAgentChange = (v) => {
conversationStore.updateAssignee("agent", {
"assignee_uuid": v
})
}
const handleAssignedTeamChange = (v) => {
conversationStore.updateAssignee("team", {
"assignee_uuid": v
})
}
const handlePriorityChange = (v) => {
conversationStore.updatePriority(v)
}
const handleUpsertTags = () => {
let tagIDs = tagsSelected.value.map((tag) => {
if (tag in tagIDMap) {
return tagIDMap[tag]
}
})
conversationStore.upsertTags({
"tag_ids": JSON.stringify(tagIDs)
})
}
</script>
<style></style>
@@ -0,0 +1,121 @@
<template>
<div class="h-screen" v-if="conversationStore.conversation.data">
<div class="h-14 border-b px-4">
<div class="flex flex-row justify-between items-center pt-2">
<div class="flex h-5 items-center space-x-4 text-sm">
<TooltipProvider :delay-duration=200>
<Tooltip>
<TooltipTrigger>#{{ conversationStore.conversation.data.reference_number }}
</TooltipTrigger>
<TooltipContent>
<p>Reference number</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Separator orientation="vertical" />
<TooltipProvider :delay-duration=200>
<Tooltip>
<TooltipTrigger>
<Badge :variant="getBadgeVariant">{{ conversationStore.conversation.data.status }}
</Badge>
</TooltipTrigger>
<TooltipContent>
<p>Status</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Separator orientation="vertical" />
<TooltipProvider :delay-duration=200>
<Tooltip>
<TooltipTrigger>
<Badge variant="default">{{ conversationStore.conversation.data.priority }}
</Badge>
</TooltipTrigger>
<TooltipContent>
<p>Priority</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div>
<DropdownMenu>
<DropdownMenuTrigger>
<Icon icon="lucide:ellipsis-vertical" class="mt-2 size-6"></Icon>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem @click="handleUpdateStatus('Open')">
<span>Open</span>
</DropdownMenuItem>
<DropdownMenuItem @click="handleUpdateStatus('Processing')">
<span>Processing</span>
</DropdownMenuItem>
<DropdownMenuItem @click="handleUpdateStatus('Spam')">
<span>Mark as spam</span>
</DropdownMenuItem>
<DropdownMenuItem @click="handleUpdateStatus('Resolved')">
<span>Resolve</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
<div class="flex flex-col h-screen">
<Error :error-message="conversationStore.messages.errorMessage"></Error>
<ScrollArea v-if="conversationStore.messages.data" class="flex-1">
<MessageBubble v-for="message in conversationStore.messages.data" :key="message.uuid" :message="message"
:conversation="conversationStore.conversation.data" />
</ScrollArea>
<TextEditor @send="sendMessage" :identifier="conversationStore.conversation.data.uuid"
:canned-responses="cannedResponsesStore.responses"></TextEditor>
</div>
</div>
</template>
<script setup>
import { computed, onMounted } from 'vue';
import { useConversationStore } from '@/stores/conversation'
import { useCannedResponses } from '@/stores/canned_responses'
import { Separator } from '@/components/ui/separator'
import { Error } from '@/components/ui/error'
import { Badge } from '@/components/ui/badge'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { ScrollArea } from '@/components/ui/scroll-area'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from '@/components/ui/tooltip'
import MessageBubble from './MessageBubble.vue'
import TextEditor from './TextEditor.vue'
import { Icon } from '@iconify/vue'
// Store, state.
const conversationStore = useConversationStore()
const cannedResponsesStore = useCannedResponses()
// Functions, methods.
const sendMessage = (message) => {
// TODO: Create msg.
console.log(message)
}
const getBadgeVariant = computed(() => {
return conversationStore.conversation.data.status == "Spam" ? "destructive" : "default"
})
const handleUpdateStatus = (status) => {
conversationStore.updateStatus(status)
}
onMounted(() => {
cannedResponsesStore.fetchAll()
})
</script>
+67
View File
@@ -0,0 +1,67 @@
<template>
<div class="flex flex-col" v-if="message.type === 'incoming' || message.type === 'outgoing'">
<div class="self-start w-11/12 flex px-5 py-3 mt-2 ">
<Avatar class="mt-1">
<AvatarImage :src=getAvatar />
<AvatarFallback>
{{ message.first_name.toUpperCase().substring(0, 2) }}
</AvatarFallback>
</Avatar>
<div class="ml-5">
<div class="flex gap-3">
<p class="font-medium">
{{ message.first_name + ' ' + message.last_name }}
</p>
<p class="text-muted-foreground text-xs mt-1">
{{ format(message.updated_at, 'h:mm a') }}
</p>
</div>
<Letter :html=message.content class="mb-1" />
</div>
</div>
</div>
<div class="self-start flex px-5 py-3 mt-2 bg-[#FFF7E6] opacity-80" v-if="message.type === 'internal_note'">
<Avatar class="mt-1">
<AvatarImage :src=getAvatar />
<AvatarFallback>
{{ message.first_name.toUpperCase().substring(0, 2) }}
</AvatarFallback>
</Avatar>
<div class="ml-5">
<div class="flex gap-3">
<p class="font-medium">
{{ message.first_name + ' ' + message.last_name }}
</p>
<div class="flex items-center text-xs text-muted-foreground">
{{ format(message.updated_at, 'h:mm a') }}
<LockKeyhole class="w-4 h-4 ml-2" />
</div>
</div>
<Letter :html=message.content class="mb-1" />
</div>
</div>
<div class="text-center p-2" v-if="message.type === 'activity'">
<div class="text-sm text-muted-foreground">
{{ message.content }}
<span>{{ format(message.updated_at, 'h:mm a') }}</span>
</div>
</div>
</template>
<script setup>
import { computed } from "vue"
import { format } from 'date-fns'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Letter } from 'vue-letter'
import { LockKeyhole } from "lucide-vue-next"
const props = defineProps({
message: Object,
})
const getAvatar = computed(() => {
return props.message.avatar_url ? props.message.avatar_url : ''
})
</script>
+77
View File
@@ -0,0 +1,77 @@
<script setup>
import { RouterLink, useRoute } from 'vue-router'
import { cn } from '@/lib/utils'
import { Icon } from '@iconify/vue'
import { buttonVariants } from '@/components/ui/button'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
TooltipProvider
} from '@/components/ui/tooltip'
defineProps({
isCollapsed: Boolean,
links: Array,
})
const route = useRoute();
const getButtonVariant = (title) => {
return route.name === title.toLowerCase() ? "default" : "ghost"
}
</script>
<template>
<div :data-collapsed="isCollapsed" class="group flex flex-col gap-4 py-2 data-[collapsed=true]:py-2">
<nav class="grid gap-1 px-2 group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2">
<template v-for="(link, index) of links">
<!-- Collapsed -->
<router-link :to="{ name: link.component }" v-if="isCollapsed" :key="`1-${index}`">
<TooltipProvider :delay-duration=10>
<Tooltip>
<TooltipTrigger as-child>
<span :class="cn(
buttonVariants({ variant: getButtonVariant(link.title), size: 'icon' }),
'h-9 w-9',
link.variant === getButtonVariant(link.title)
&& 'dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white',
)">
<Icon :icon="link.icon" class="size-4" />
<span class="sr-only">{{ link.title }}</span>
</span>
</TooltipTrigger>
<TooltipContent side="right" class="flex items-center gap-4">
{{ link.title }}
<span v-if="link.label" class="ml-auto text-muted-foreground">
{{ link.label }}
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</router-link>
<!-- Expanded -->
<router-link v-else :to="{ name: link.component }" :key="`2-${index}`" :class="cn(
buttonVariants({ variant: getButtonVariant(link.title), size: 'sm' }),
link.variant === getButtonVariant(link.title)
&& 'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white',
'justify-start',
)">
<Icon :icon="link.icon" class="mr-2 size-4" />
{{ link.title }}
<span v-if="link.label" :class="cn(
'ml-auto',
link.variant === getButtonVariant(link.title)
&& 'text-background dark:text-white',
)">
{{ link.label }}
</span>
</router-link>
</template>
</nav>
</div>
</template>
+243
View File
@@ -0,0 +1,243 @@
<template>
<div v-if="showResponses && filteredCannedResponses.length > 0"
class="w-full drop-shadow-sm overflow-hidden p-2 border-t">
<ScrollArea>
<ul class="space-y-2 max-h-96">
<li v-for="(response, index) in filteredCannedResponses" :key="response.id"
@click="selectResponse(response.content)" class="cursor-pointer rounded p-1"
:class="{ 'bg-secondary': cannedResponseIndex === index }"
:ref="el => cannedResponseRefItems.push(el)">
<span class="font-semibold">{{ response.title }}</span> - {{ response.content }}
</li>
</ul>
</ScrollArea>
</div>
<div class="relative w-auto rounded-none border-y mb-[49px] fullscreen">
<div class="flex justify-between bg-[#F5F5F4] dark:bg-white">
<Tabs default-value="account">
<TabsList>
<TabsTrigger value="account">
Reply
</TabsTrigger>
<TabsTrigger value="password">
Internal note
</TabsTrigger>
</TabsList>
</Tabs>
<!-- <Toggle class="px-2 py-2 bg-white" variant="outline" @click="toggleFullScreen">
<Fullscreen class="w-full h-full" />
</Toggle> -->
</div>
<EditorContent :editor="editor" @keyup="checkTrigger" @keydown="navigateResponses" />
<div class="flex justify-between items-center border h-14 p-1 px-2">
<div class="flex justify-items-start gap-2">
<Toggle class="px-2 py-2 " variant="outline" @click="applyBold" :pressed="isBold">
<Bold class="w-full h-full" />
</Toggle>
<Toggle class="px-2 py-2 " variant="outline" @click="applyItalic" :pressed="isItalic">
<Italic class="w-full h-full" />
</Toggle>
<!-- File Upload -->
<input type="files" class="hidden" ref="fileInput" multiple>
<Toggle class="px-2 py-2" variant="outline" @click="handleFileUpload">
<Paperclip class="w-full h-full" />
</Toggle>
</div>
<Button class="h-8 w-6 px-8 " @click="handleSend" :disabled="!hasText">
Send
</Button>
</div>
</div>
</template>
<script setup>
import { ref, watchEffect, onUnmounted, onMounted, computed, watch, nextTick } from "vue"
import { Button } from '@/components/ui/button'
import { useEditor, EditorContent } from '@tiptap/vue-3'
import Placeholder from "@tiptap/extension-placeholder"
import StarterKit from '@tiptap/starter-kit'
import { Toggle } from '@/components/ui/toggle'
import { Paperclip, Bold, Italic } from "lucide-vue-next"
import { ScrollArea } from '@/components/ui/scroll-area'
import {
Tabs,
TabsList,
TabsTrigger,
} from '@/components/ui/tabs'
const emit = defineEmits(['send'])
const props = defineProps({
identifier: String, // Unique identifier for the editor could be the uuid of conversation.
cannedResponses: Array
})
const editor = ref(useEditor({
content: '',
extensions: [
StarterKit,
Placeholder.configure({
placeholder: "Type a message...",
keyboardShortcuts: {
'Control-b': () => applyBold(),
'Control-i': () => applyItalic(),
}
}),
],
autofocus: true,
editorProps: {
attributes: {
class: "outline-none",
},
},
}))
const saveEditorContent = () => {
if (editor.value && props.identifier) {
// Skip single `/`
if (editor.value.getText() === "/") {
return
}
const content = editor.value.getHTML()
localStorage.setItem(getDraftLocalStorageKey(), content)
}
}
const cannedResponseRefItems = ref([])
const inputText = ref('')
const isBold = ref(false)
const isItalic = ref(false)
const fileInput = ref(null)
const cannedResponseIndex = ref(0)
const contentSaverInterval = setInterval(saveEditorContent, 200)
const showResponses = ref(false)
watchEffect(() => {
if (editor.value) {
isBold.value = editor.value.isActive('bold')
isItalic.value = editor.value.isActive('italic')
}
})
watchEffect(() => {
if (editor.value) {
inputText.value = editor.value.getText()
}
})
watch(cannedResponseIndex, () => {
nextTick(() => {
if (cannedResponseRefItems.value[cannedResponseIndex.value]) {
cannedResponseRefItems.value[cannedResponseIndex.value].scrollIntoView({ behavior: 'smooth', block: 'nearest' })
}
})
})
onMounted(() => {
if (editor.value) {
const draftContent = localStorage.getItem(getDraftLocalStorageKey())
editor.value.commands.setContent(draftContent)
}
})
onUnmounted(() => {
clearInterval(contentSaverInterval)
})
const filteredCannedResponses = computed(() => {
if (inputText.value.startsWith('/')) {
const searchQuery = inputText.value.slice(1).toLowerCase()
return props.cannedResponses.filter(response => response.title.toLowerCase().includes(searchQuery))
}
return []
})
const hasText = computed(() => {
if (editor.value) {
return editor.value.getText().length === 0 ? false : true
}
return false
})
const getDraftLocalStorageKey = () => {
return `content.${props.identifier}`
}
const navigateResponses = (event) => {
if (!showResponses.value) return
switch (event.key) {
case 'ArrowDown':
event.preventDefault()
cannedResponseIndex.value = (cannedResponseIndex.value + 1) % filteredCannedResponses.value.length
break
case 'ArrowUp':
event.preventDefault()
cannedResponseIndex.value = (cannedResponseIndex.value - 1 + filteredCannedResponses.value.length) % filteredCannedResponses.value.length
break
case 'Enter':
selectResponse(filteredCannedResponses.value[cannedResponseIndex.value].content)
break
}
}
const checkTrigger = (e) => {
if (e.key === "/")
showResponses.value = true
}
const selectResponse = (message) => {
editor.value.commands.setContent(message)
showResponses.value = false
editor.value.chain().focus()
}
const handleSend = () => {
emit('send', editor.value.getHTML())
editor.value.commands.clearContent()
}
const applyBold = () => {
editor.value.chain().focus().toggleBold().run()
}
const applyItalic = () => {
editor.value.chain().focus().toggleItalic().run()
}
const handleFileUpload = (event) => {
const files = Array.from(event.target.files)
console.log(files)
}
function toggleFullScreen () {
const editorElement = editor.value?.editorView?.dom
if (editorElement && editorElement.requestFullscreen) {
editorElement.requestFullscreen().catch(err => {
console.error(`Error attempting to enable full-screen mode: ${err.message} (${err.name})`)
})
} else {
console.error("Fullscreen API is not available.")
}
}
</script>
<style lang="scss">
// Moving placeholder to the top
.tiptap p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
color: #adb5bd;
pointer-events: none;
height: 0;
}
// Editor height
.ProseMirror {
min-height: 150px;
overflow: scroll;
padding: 10px 10px;
}
</style>
@@ -0,0 +1,24 @@
<script setup>
import { AccordionRoot, useForwardPropsEmits } from "radix-vue";
const props = defineProps({
collapsible: { type: Boolean, required: false },
disabled: { type: Boolean, required: false },
dir: { type: String, required: false },
orientation: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
type: { type: null, required: false },
modelValue: { type: null, required: false },
defaultValue: { type: null, required: false },
});
const emits = defineEmits(["update:modelValue"]);
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<AccordionRoot v-bind="forwarded">
<slot />
</AccordionRoot>
</template>
@@ -0,0 +1,28 @@
<script setup>
import { computed } from "vue";
import { AccordionContent } from "radix-vue";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<AccordionContent
v-bind="delegatedProps"
class="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
>
<div :class="cn('pb-4 pt-0', props.class)">
<slot />
</div>
</AccordionContent>
</template>
@@ -0,0 +1,27 @@
<script setup>
import { computed } from "vue";
import { AccordionItem, useForwardProps } from "radix-vue";
import { cn } from "@/lib/utils";
const props = defineProps({
disabled: { type: Boolean, required: false },
value: { type: String, required: true },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<AccordionItem v-bind="forwardedProps" :class="cn('border-b', props.class)">
<slot />
</AccordionItem>
</template>
@@ -0,0 +1,39 @@
<script setup>
import { computed } from "vue";
import { AccordionHeader, AccordionTrigger } from "radix-vue";
import { ChevronDownIcon } from "@radix-icons/vue";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<AccordionHeader class="flex">
<AccordionTrigger
v-bind="delegatedProps"
:class="
cn(
'flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
props.class
)
"
>
<slot />
<slot name="icon">
<ChevronDownIcon
class="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200"
/>
</slot>
</AccordionTrigger>
</AccordionHeader>
</template>
@@ -0,0 +1,4 @@
export { default as Accordion } from "./Accordion.vue";
export { default as AccordionContent } from "./AccordionContent.vue";
export { default as AccordionItem } from "./AccordionItem.vue";
export { default as AccordionTrigger } from "./AccordionTrigger.vue";
@@ -0,0 +1,17 @@
<script setup>
import { AlertDialogRoot, useForwardPropsEmits } from "radix-vue";
const props = defineProps({
open: { type: Boolean, required: false },
defaultOpen: { type: Boolean, required: false },
});
const emits = defineEmits(["update:open"]);
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<AlertDialogRoot v-bind="forwarded">
<slot />
</AlertDialogRoot>
</template>
@@ -0,0 +1,27 @@
<script setup>
import { computed } from "vue";
import { AlertDialogAction } from "radix-vue";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<AlertDialogAction
v-bind="delegatedProps"
:class="cn(buttonVariants(), props.class)"
>
<slot />
</AlertDialogAction>
</template>
@@ -0,0 +1,29 @@
<script setup>
import { computed } from "vue";
import { AlertDialogCancel } from "radix-vue";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<AlertDialogCancel
v-bind="delegatedProps"
:class="
cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', props.class)
"
>
<slot />
</AlertDialogCancel>
</template>
@@ -0,0 +1,54 @@
<script setup>
import { computed } from "vue";
import {
AlertDialogContent,
AlertDialogOverlay,
AlertDialogPortal,
useForwardPropsEmits,
} from "radix-vue";
import { cn } from "@/lib/utils";
const props = defineProps({
forceMount: { type: Boolean, required: false },
trapFocus: { type: Boolean, required: false },
disableOutsidePointerEvents: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const emits = defineEmits([
"escapeKeyDown",
"pointerDownOutside",
"focusOutside",
"interactOutside",
"openAutoFocus",
"closeAutoFocus",
]);
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<AlertDialogPortal>
<AlertDialogOverlay
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
/>
<AlertDialogContent
v-bind="forwarded"
:class="
cn(
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
props.class
)
"
>
<slot />
</AlertDialogContent>
</AlertDialogPortal>
</template>
@@ -0,0 +1,26 @@
<script setup>
import { computed } from "vue";
import { AlertDialogDescription } from "radix-vue";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<AlertDialogDescription
v-bind="delegatedProps"
:class="cn('text-sm text-muted-foreground', props.class)"
>
<slot />
</AlertDialogDescription>
</template>
@@ -0,0 +1,20 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<div
:class="
cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2',
props.class
)
"
>
<slot />
</div>
</template>
@@ -0,0 +1,15 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<div
:class="cn('flex flex-col gap-y-2 text-center sm:text-left', props.class)"
>
<slot />
</div>
</template>
@@ -0,0 +1,26 @@
<script setup>
import { computed } from "vue";
import { AlertDialogTitle } from "radix-vue";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<AlertDialogTitle
v-bind="delegatedProps"
:class="cn('text-lg font-semibold', props.class)"
>
<slot />
</AlertDialogTitle>
</template>
@@ -0,0 +1,14 @@
<script setup>
import { AlertDialogTrigger } from "radix-vue";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
});
</script>
<template>
<AlertDialogTrigger v-bind="props">
<slot />
</AlertDialogTrigger>
</template>
@@ -0,0 +1,9 @@
export { default as AlertDialog } from "./AlertDialog.vue";
export { default as AlertDialogTrigger } from "./AlertDialogTrigger.vue";
export { default as AlertDialogContent } from "./AlertDialogContent.vue";
export { default as AlertDialogHeader } from "./AlertDialogHeader.vue";
export { default as AlertDialogTitle } from "./AlertDialogTitle.vue";
export { default as AlertDialogDescription } from "./AlertDialogDescription.vue";
export { default as AlertDialogFooter } from "./AlertDialogFooter.vue";
export { default as AlertDialogAction } from "./AlertDialogAction.vue";
export { default as AlertDialogCancel } from "./AlertDialogCancel.vue";
@@ -0,0 +1,15 @@
<script setup>
import { alertVariants } from ".";
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
variant: { type: null, required: false },
});
</script>
<template>
<div :class="cn(alertVariants({ variant }), props.class)" role="alert">
<slot />
</div>
</template>
@@ -0,0 +1,13 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<div :class="cn('text-sm [&_p]:leading-relaxed', props.class)">
<slot />
</div>
</template>
@@ -0,0 +1,13 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<h5 :class="cn('mb-1 font-medium leading-none tracking-tight', props.class)">
<slot />
</h5>
</template>
+21
View File
@@ -0,0 +1,21 @@
import { cva } from "class-variance-authority";
export { default as Alert } from "./Alert.vue";
export { default as AlertTitle } from "./AlertTitle.vue";
export { default as AlertDescription } from "./AlertDescription.vue";
export const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
);
@@ -0,0 +1,17 @@
<script setup>
import { AvatarRoot } from "radix-vue";
import { avatarVariant } from ".";
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
size: { type: null, required: false, default: "sm" },
shape: { type: null, required: false, default: "circle" },
});
</script>
<template>
<AvatarRoot :class="cn(avatarVariant({ size, shape }), props.class)">
<slot />
</AvatarRoot>
</template>
@@ -0,0 +1,15 @@
<script setup>
import { AvatarFallback } from "radix-vue";
const props = defineProps({
delayMs: { type: Number, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
});
</script>
<template>
<AvatarFallback v-bind="props">
<slot />
</AvatarFallback>
</template>
@@ -0,0 +1,13 @@
<script setup>
import { AvatarImage } from "radix-vue";
const props = defineProps({
src: { type: String, required: true },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
});
</script>
<template>
<AvatarImage v-bind="props" class="h-full w-full object-cover" />
</template>
@@ -0,0 +1,22 @@
import { cva } from "class-variance-authority";
export { default as Avatar } from "./Avatar.vue";
export { default as AvatarImage } from "./AvatarImage.vue";
export { default as AvatarFallback } from "./AvatarFallback.vue";
export const avatarVariant = cva(
"inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden",
{
variants: {
size: {
sm: "h-10 w-10 text-xs",
base: "h-16 w-16 text-2xl",
lg: "h-32 w-32 text-5xl",
},
shape: {
circle: "rounded-full",
square: "rounded-md",
},
},
}
);
@@ -0,0 +1,15 @@
<script setup>
import { badgeVariants } from ".";
import { cn } from "@/lib/utils";
const props = defineProps({
variant: { type: null, required: false },
class: { type: null, required: false },
});
</script>
<template>
<div :class="cn(badgeVariants({ variant }), props.class)">
<slot />
</div>
</template>
+23
View File
@@ -0,0 +1,23 @@
import { cva } from "class-variance-authority";
export { default as Badge } from "./Badge.vue";
export const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
);
@@ -0,0 +1,23 @@
<script setup>
import { Primitive } from "radix-vue";
import { buttonVariants } from ".";
import { cn } from "@/lib/utils";
const props = defineProps({
variant: { type: null, required: false },
size: { type: null, required: false },
class: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false, default: "button" },
});
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>
@@ -0,0 +1,34 @@
import { cva } from "class-variance-authority";
export { default as Button } from "./Button.vue";
export const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
xs: "h-7 rounded px-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
+17
View File
@@ -0,0 +1,17 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<div
:class="
cn('rounded-xl border bg-card text-card-foreground shadow', props.class)
"
>
<slot />
</div>
</template>
@@ -0,0 +1,13 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<div :class="cn('p-6 pt-0', props.class)">
<slot />
</div>
</template>
@@ -0,0 +1,13 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<p :class="cn('text-sm text-muted-foreground', props.class)">
<slot />
</p>
</template>
@@ -0,0 +1,13 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<div :class="cn('flex items-center p-6 pt-0', props.class)">
<slot />
</div>
</template>
@@ -0,0 +1,13 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<div :class="cn('flex flex-col gap-y-1.5 p-6', props.class)">
<slot />
</div>
</template>
@@ -0,0 +1,13 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<h3 :class="cn('font-semibold leading-none tracking-tight', props.class)">
<slot />
</h3>
</template>
+6
View File
@@ -0,0 +1,6 @@
export { default as Card } from "./Card.vue";
export { default as CardHeader } from "./CardHeader.vue";
export { default as CardTitle } from "./CardTitle.vue";
export { default as CardDescription } from "./CardDescription.vue";
export { default as CardContent } from "./CardContent.vue";
export { default as CardFooter } from "./CardFooter.vue";
@@ -0,0 +1,86 @@
<script setup>
import { VisDonut, VisSingleContainer } from '@unovis/vue';
import { Donut } from '@unovis/ts';
import { computed, ref } from 'vue';
import { useMounted } from '@vueuse/core';
import { ChartSingleTooltip, defaultColors } from '@/components/ui/chart';
import { cn } from '@/lib/utils';
const props = defineProps({
data: { type: Array, required: true },
colors: { type: Array, required: false },
index: { type: null, required: true },
margin: { type: null, required: false, default: () => ({ top: 0, bottom: 0, left: 0, right: 0 }) },
showLegend: { type: Boolean, required: false, default: true },
showTooltip: { type: Boolean, required: false, default: true },
filterOpacity: { type: Number, required: false, default: 0.2 },
category: { type: String, required: true },
type: { type: String, required: false, default: 'donut' },
sortFunction: { type: Function, required: false, default: () => undefined },
valueFormatter: { type: Function, required: false, default: (tick) => `${tick}` },
customTooltip: { type: null, required: false }
}
);
const category = computed(() => props.category);
const index = computed(() => props.index);
const isMounted = useMounted();
const activeSegmentKey = ref();
const colors = computed(() => props.colors?.length ? props.colors : defaultColors(props.data.filter((d) => d[props.category]).filter(Boolean).length));
const legendItems = computed(() => props.data.map((item, i) => ({
name: item[props.index],
color: colors.value[i],
inactive: false
})));
const totalValue = computed(() => props.data.reduce((prev, curr) => {
return prev + curr[props.category];
}, 0))
</script>
<template>
<div :class="cn('w-full h-48 flex flex-col items-end', $attrs.class ?? '')">
<VisSingleContainer
:style="{ height: isMounted ? '100%' : 'auto' }"
:margin="{ left: 20, right: 20 }"
:data="data"
>
<ChartSingleTooltip
:selector="Donut.selectors.segment"
:index="category"
:items="legendItems"
:value-formatter="valueFormatter"
:custom-tooltip="customTooltip"
/>
<VisDonut
:value="(d) => d[category]"
:sort-function="sortFunction"
:color="colors"
:arc-width="type === 'donut' ? 20 : 0"
:show-background="false"
:central-label="type === 'donut' ? valueFormatter(totalValue) : ''"
:events="{
[Donut.selectors.segment]: {
click: (d, ev, i, elements) => {
if (d?.data?.[index] === activeSegmentKey) {
activeSegmentKey = undefined;
elements.forEach((el) => (el.style.opacity = '1'));
} else {
activeSegmentKey = d?.data?.[index];
elements.forEach(
(el) => (el.style.opacity = `${filterOpacity}`),
);
elements[i].style.opacity = '1';
}
},
},
}"
/>
<slot />
</VisSingleContainer>
</div>
</template>
@@ -0,0 +1 @@
export { default as DonutChart } from "./DonutChart.vue";
@@ -0,0 +1,124 @@
<script setup>
import { CurveType } from "@unovis/ts";
import { VisAxis, VisLine, VisXYContainer } from "@unovis/vue";
import { Axis, Line } from "@unovis/ts";
import { computed, ref } from "vue";
import { useMounted } from "@vueuse/core";
import {
ChartCrosshair,
ChartLegend,
defaultColors,
} from "@/components/ui/chart";
import { cn } from "@/lib/utils";
const props = defineProps({
data: { type: Array, required: true },
categories: { type: Array, required: true },
index: { type: null, required: true },
colors: { type: Array, required: false },
margin: {
type: null,
required: false,
default: () => ({ top: 0, bottom: 0, left: 0, right: 0 }),
},
filterOpacity: { type: Number, required: false, default: 0.2 },
xFormatter: { type: Function, required: false },
yFormatter: { type: Function, required: false },
showXAxis: { type: Boolean, required: false, default: true },
showYAxis: { type: Boolean, required: false, default: true },
showTooltip: { type: Boolean, required: false, default: true },
showLegend: { type: Boolean, required: false, default: true },
showGridLine: { type: Boolean, required: false, default: true },
customTooltip: { type: null, required: false },
curveType: { type: String, required: false, default: CurveType.MonotoneX },
});
const emits = defineEmits(["legendItemClick"]);
const index = computed(() => props.index);
const colors = computed(() =>
props.colors?.length ? props.colors : defaultColors(props.categories.length),
);
const legendItems = ref(
props.categories.map((category, i) => ({
name: category,
color: colors.value[i],
inactive: false,
})),
);
const isMounted = useMounted();
function handleLegendItemClick(d, i) {
emits("legendItemClick", d, i);
}
</script>
<template>
<div
:class="cn('w-full h-[400px] flex flex-col items-end', $attrs.class ?? '')"
>
<ChartLegend
v-if="showLegend"
v-model:items="legendItems"
@legend-item-click="handleLegendItemClick"
/>
<VisXYContainer
:margin="{ left: 20, right: 20 }"
:data="data"
:style="{ height: isMounted ? '100%' : 'auto' }"
>
<ChartCrosshair
v-if="showTooltip"
:colors="colors"
:items="legendItems"
:index="index"
:custom-tooltip="customTooltip"
/>
<template v-for="(category, i) in categories" :key="category">
<VisLine
:x="(d, i) => i"
:y="(d) => d[category]"
:curve-type="curveType"
:color="colors[i]"
:attributes="{
[Line.selectors.line]: {
opacity: legendItems.find((item) => item.name === category)
?.inactive
? filterOpacity
: 1,
},
}"
/>
</template>
<VisAxis
v-if="showXAxis"
type="x"
:tick-format="xFormatter ?? ((v) => data[v]?.[index])"
:grid-line="false"
:tick-line="false"
tick-text-color="hsl(var(--vis-text-color))"
/>
<VisAxis
v-if="showYAxis"
type="y"
:tick-line="false"
:tick-format="yFormatter"
:domain-line="false"
:grid-line="showGridLine"
:attributes="{
[Axis.selectors.grid]: {
class: 'text-muted',
},
}"
tick-text-color="hsl(var(--vis-text-color))"
/>
<slot />
</VisXYContainer>
</div>
</template>
@@ -0,0 +1 @@
export { default as LineChart } from "./LineChart.vue";
@@ -0,0 +1,45 @@
<script setup>
import { VisCrosshair, VisTooltip } from "@unovis/vue";
import { omit } from "@unovis/ts";
import { createApp } from "vue";
import { ChartTooltip } from ".";
const props = defineProps({
colors: { type: Array, required: true, default: () => [] },
index: { type: String, required: true },
items: { type: Array, required: true },
customTooltip: { type: null, required: false },
});
// Use weakmap to store reference to each datapoint for Tooltip
const wm = new WeakMap();
function template(d) {
if (wm.has(d)) {
return wm.get(d);
} else {
const componentDiv = document.createElement("div");
const omittedData = Object.entries(omit(d, [props.index])).map(
([key, value]) => {
const legendReference = props.items.find((i) => i.name === key);
return { ...legendReference, value };
},
);
const TooltipComponent = props.customTooltip ?? ChartTooltip;
createApp(TooltipComponent, {
title: d[props.index].toString(),
data: omittedData,
}).mount(componentDiv);
wm.set(d, componentDiv.innerHTML);
return componentDiv.innerHTML;
}
}
function color(d, i) {
return props.colors[i] ?? "transparent";
}
</script>
<template>
<VisTooltip :horizontal-shift="20" :vertical-shift="20" />
<VisCrosshair :template="template" :color="color" />
</template>
@@ -0,0 +1,55 @@
<script setup>
import { VisBulletLegend } from "@unovis/vue";
import { BulletLegend } from "@unovis/ts";
import { nextTick, onMounted, ref } from "vue";
import { buttonVariants } from "@/components/ui/button";
const props = defineProps({
items: { type: Array, required: true, default: () => [] },
});
const emits = defineEmits(["legendItemClick", "update:items"]);
const elRef = ref();
onMounted(() => {
const selector = `.${BulletLegend.selectors.item}`;
nextTick(() => {
const elements = elRef.value?.querySelectorAll(selector);
const classes = buttonVariants({ variant: "ghost", size: "xs" }).split(" ");
elements?.forEach((el) =>
el.classList.add(...classes, "!inline-flex", "!mr-2"),
);
});
});
function onLegendItemClick(d, i) {
emits("legendItemClick", d, i);
const isBulletActive = !props.items[i].inactive;
const isFilterApplied = props.items.some((i) => i.inactive);
if (isFilterApplied && isBulletActive) {
// reset filter
emits(
"update:items",
props.items.map((item) => ({ ...item, inactive: false })),
);
} else {
// apply selection, set other item as inactive
emits(
"update:items",
props.items.map((item) =>
item.name === d.name
? { ...d, inactive: false }
: { ...item, inactive: true },
),
);
}
}
</script>
<template>
<div ref="elRef" class="w-max">
<VisBulletLegend :items="items" :on-legend-item-click="onLegendItemClick" />
</div>
</template>
@@ -0,0 +1,63 @@
<script setup>
import { VisTooltip } from '@unovis/vue';
import { omit } from '@unovis/ts';
import { createApp } from 'vue';
import { ChartTooltip } from '.';
const props = defineProps({
selector: { type: String, required: true },
index: { type: String, required: true },
items: { type: Array, required: false },
valueFormatter: { type: Function, required: false, default: (tick) => `${tick}` },
customTooltip: { type: null, required: false }
}
);
// Use weakmap to store reference to each datapoint for Tooltip
const wm = new WeakMap();
function template(d, i, elements) {
if (props.index in d) {
if (wm.has(d)) {
return wm.get(d);
} else
{
const componentDiv = document.createElement('div');
const omittedData = Object.entries(omit(d, [props.index])).map(([key, value]) => {
const legendReference = props.items?.find((i) => i.name === key);
return { ...legendReference, value: props.valueFormatter(value) };
});
const TooltipComponent = props.customTooltip ?? ChartTooltip;
createApp(TooltipComponent, { title: d[props.index], data: omittedData }).mount(componentDiv);
wm.set(d, componentDiv.innerHTML);
return componentDiv.innerHTML;
}
}
else
{
const data = d.data;
if (wm.has(data)) {
return wm.get(data);
} else
{
const style = getComputedStyle(elements[i]);
const omittedData = [{ name: data.name, value: props.valueFormatter(data[props.index]), color: style.fill }];
const componentDiv = document.createElement('div');
const TooltipComponent = props.customTooltip ?? ChartTooltip;
createApp(TooltipComponent, { title: d[props.index], data: omittedData }).mount(componentDiv);
wm.set(d, componentDiv.innerHTML);
return componentDiv.innerHTML;
}
}
}
</script>
<template>
<VisTooltip
:horizontal-shift="20"
:vertical-shift="20"
:triggers="{
[selector]: template,
}"
/>
</template>
@@ -0,0 +1,36 @@
<script setup>
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
defineProps({
title: { type: String, required: false },
data: { type: Array, required: true },
});
</script>
<template>
<Card class="text-sm">
<CardHeader v-if="title" class="p-3 border-b">
<CardTitle>
{{ title }}
</CardTitle>
</CardHeader>
<CardContent class="p-3 min-w-[180px] flex flex-col gap-1">
<div v-for="(item, key) in data" :key="key" class="flex justify-between">
<div class="flex items-center">
<span class="w-2.5 h-2.5 mr-2">
<svg width="100%" height="100%" viewBox="0 0 30 30">
<path
d=" M 15 15 m -14, 0 a 14,14 0 1,1 28,0 a 14,14 0 1,1 -28,0"
:stroke="item.color"
:fill="item.color"
stroke-width="1"
/>
</svg>
</span>
<span>{{ item.name }}</span>
</div>
<span class="font-semibold ml-4">{{ item.value }}</span>
</div>
</CardContent>
</Card>
</template>
+23
View File
@@ -0,0 +1,23 @@
export { default as ChartTooltip } from "./ChartTooltip.vue";
export { default as ChartSingleTooltip } from "./ChartSingleTooltip.vue";
export { default as ChartLegend } from "./ChartLegend.vue";
export { default as ChartCrosshair } from "./ChartCrosshair.vue";
export function defaultColors(count = 3) {
const quotient = Math.floor(count / 2);
const remainder = count % 2;
const primaryCount = quotient + remainder;
const secondaryCount = quotient;
return [
...Array.from(Array(primaryCount).keys()).map(
(i) => `hsl(var(--vis-primary-color) / ${1 - (1 / primaryCount) * i})`,
),
...Array.from(Array(secondaryCount).keys()).map(
(i) =>
`hsl(var(--vis-secondary-color) / ${1 - (1 / secondaryCount) * i})`,
),
];
}
export * from "./interface";
@@ -0,0 +1 @@
export {};
@@ -0,0 +1,51 @@
<script setup>
import { computed } from "vue";
import { ComboboxRoot, useForwardPropsEmits } from "radix-vue";
import { cn } from "@/lib/utils";
const props = defineProps({
modelValue: { type: null, required: false, default: "" },
defaultValue: { type: null, required: false },
open: { type: Boolean, required: false, default: true },
defaultOpen: { type: Boolean, required: false },
searchTerm: { type: String, required: false },
multiple: { type: Boolean, required: false },
disabled: { type: Boolean, required: false },
name: { type: String, required: false },
dir: { type: String, required: false },
filterFunction: { type: Function, required: false },
displayValue: { type: Function, required: false },
resetSearchTermOnBlur: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const emits = defineEmits([
"update:modelValue",
"update:open",
"update:searchTerm",
]);
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<ComboboxRoot
v-bind="forwarded"
:class="
cn(
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
props.class
)
"
>
<slot />
</ComboboxRoot>
</template>
@@ -0,0 +1,26 @@
<script setup>
import { useForwardPropsEmits } from "radix-vue";
import Command from "./Command.vue";
import { Dialog, DialogContent } from "@/components/ui/dialog";
const props = defineProps({
open: { type: Boolean, required: false },
defaultOpen: { type: Boolean, required: false },
modal: { type: Boolean, required: false },
});
const emits = defineEmits(["update:open"]);
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<Dialog v-bind="forwarded">
<DialogContent class="overflow-hidden p-0 shadow-lg">
<Command
class="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"
>
<slot />
</Command>
</DialogContent>
</Dialog>
</template>
@@ -0,0 +1,26 @@
<script setup>
import { computed } from "vue";
import { ComboboxEmpty } from "radix-vue";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<ComboboxEmpty
v-bind="delegatedProps"
:class="cn('py-6 text-center text-sm', props.class)"
>
<slot />
</ComboboxEmpty>
</template>
@@ -0,0 +1,38 @@
<script setup>
import { computed } from "vue";
import { ComboboxGroup, ComboboxLabel } from "radix-vue";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
heading: { type: String, required: false },
});
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<ComboboxGroup
v-bind="delegatedProps"
:class="
cn(
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
props.class
)
"
>
<ComboboxLabel
v-if="heading"
class="px-2 py-1.5 text-xs font-medium text-muted-foreground"
>
{{ heading }}
</ComboboxLabel>
<slot />
</ComboboxGroup>
</template>
@@ -0,0 +1,43 @@
<script setup>
import { computed } from "vue";
import { MagnifyingGlassIcon } from "@radix-icons/vue";
import { ComboboxInput, useForwardProps } from "radix-vue";
import { cn } from "@/lib/utils";
defineOptions({
inheritAttrs: false,
});
const props = defineProps({
type: { type: String, required: false },
disabled: { type: Boolean, required: false },
autoFocus: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<div class="flex items-center border-b px-3" cmdk-input-wrapper>
<MagnifyingGlassIcon class="mr-2 h-4 w-4 shrink-0 opacity-50" />
<ComboboxInput
v-bind="{ ...forwardedProps, ...$attrs }"
auto-focus
:class="
cn(
'flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
props.class
)
"
/>
</div>
</template>
@@ -0,0 +1,36 @@
<script setup>
import { computed } from "vue";
import { ComboboxItem, useForwardPropsEmits } from "radix-vue";
import { cn } from "@/lib/utils";
const props = defineProps({
value: { type: null, required: true },
disabled: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const emits = defineEmits(["select"]);
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<ComboboxItem
v-bind="forwarded"
:class="
cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class
)
"
>
<slot />
</ComboboxItem>
</template>
@@ -0,0 +1,53 @@
<script setup>
import { computed } from "vue";
import { ComboboxContent, useForwardPropsEmits } from "radix-vue";
import { cn } from "@/lib/utils";
const props = defineProps({
forceMount: { type: Boolean, required: false },
position: { type: String, required: false },
bodyLock: { type: Boolean, required: false },
dismissable: { type: Boolean, required: false, default: false },
side: { type: null, required: false },
sideOffset: { type: Number, required: false },
align: { type: null, required: false },
alignOffset: { type: Number, required: false },
avoidCollisions: { type: Boolean, required: false },
collisionBoundary: { type: null, required: false },
collisionPadding: { type: [Number, Object], required: false },
arrowPadding: { type: Number, required: false },
sticky: { type: String, required: false },
hideWhenDetached: { type: Boolean, required: false },
updatePositionStrategy: { type: String, required: false },
prioritizePosition: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
disableOutsidePointerEvents: { type: Boolean, required: false },
class: { type: null, required: false },
});
const emits = defineEmits([
"escapeKeyDown",
"pointerDownOutside",
"focusOutside",
"interactOutside",
]);
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<ComboboxContent
v-bind="forwarded"
:class="cn('max-h-[300px] overflow-y-auto overflow-x-hidden', props.class)"
>
<div role="presentation">
<slot />
</div>
</ComboboxContent>
</template>
@@ -0,0 +1,26 @@
<script setup>
import { computed } from "vue";
import { ComboboxSeparator } from "radix-vue";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<ComboboxSeparator
v-bind="delegatedProps"
:class="cn('-mx-1 h-px bg-border', props.class)"
>
<slot />
</ComboboxSeparator>
</template>
@@ -0,0 +1,17 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<span
:class="
cn('ml-auto text-xs tracking-widest text-muted-foreground', props.class)
"
>
<slot />
</span>
</template>
@@ -0,0 +1,9 @@
export { default as Command } from "./Command.vue";
export { default as CommandDialog } from "./CommandDialog.vue";
export { default as CommandEmpty } from "./CommandEmpty.vue";
export { default as CommandGroup } from "./CommandGroup.vue";
export { default as CommandInput } from "./CommandInput.vue";
export { default as CommandItem } from "./CommandItem.vue";
export { default as CommandList } from "./CommandList.vue";
export { default as CommandSeparator } from "./CommandSeparator.vue";
export { default as CommandShortcut } from "./CommandShortcut.vue";
@@ -0,0 +1,18 @@
<script setup>
import { DialogRoot, useForwardPropsEmits } from "radix-vue";
const props = defineProps({
open: { type: Boolean, required: false },
defaultOpen: { type: Boolean, required: false },
modal: { type: Boolean, required: false },
});
const emits = defineEmits(["update:open"]);
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DialogRoot v-bind="forwarded">
<slot />
</DialogRoot>
</template>
@@ -0,0 +1,14 @@
<script setup>
import { DialogClose } from "radix-vue";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
});
</script>
<template>
<DialogClose v-bind="props">
<slot />
</DialogClose>
</template>
@@ -0,0 +1,63 @@
<script setup>
import { computed } from "vue";
import {
DialogClose,
DialogContent,
DialogOverlay,
DialogPortal,
useForwardPropsEmits,
} from "radix-vue";
import { Cross2Icon } from "@radix-icons/vue";
import { cn } from "@/lib/utils";
const props = defineProps({
forceMount: { type: Boolean, required: false },
trapFocus: { type: Boolean, required: false },
disableOutsidePointerEvents: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const emits = defineEmits([
"escapeKeyDown",
"pointerDownOutside",
"focusOutside",
"interactOutside",
"openAutoFocus",
"closeAutoFocus",
]);
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DialogPortal>
<DialogOverlay
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
/>
<DialogContent
v-bind="forwarded"
:class="
cn(
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
props.class
)
"
>
<slot />
<DialogClose
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
>
<Cross2Icon class="w-4 h-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogPortal>
</template>
@@ -0,0 +1,28 @@
<script setup>
import { computed } from "vue";
import { DialogDescription, useForwardProps } from "radix-vue";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DialogDescription
v-bind="forwardedProps"
:class="cn('text-sm text-muted-foreground', props.class)"
>
<slot />
</DialogDescription>
</template>
@@ -0,0 +1,20 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<div
:class="
cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2',
props.class
)
"
>
<slot />
</div>
</template>
@@ -0,0 +1,15 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<div
:class="cn('flex flex-col gap-y-1.5 text-center sm:text-left', props.class)"
>
<slot />
</div>
</template>
@@ -0,0 +1,70 @@
<script setup>
import { computed } from "vue";
import {
DialogClose,
DialogContent,
DialogOverlay,
DialogPortal,
useForwardPropsEmits,
} from "radix-vue";
import { cn } from "@/lib/utils";
const props = defineProps({
forceMount: { type: Boolean, required: false },
trapFocus: { type: Boolean, required: false },
disableOutsidePointerEvents: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const emits = defineEmits([
"escapeKeyDown",
"pointerDownOutside",
"focusOutside",
"interactOutside",
"openAutoFocus",
"closeAutoFocus",
]);
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DialogPortal>
<DialogOverlay
class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
>
<DialogContent
:class="
cn(
'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
props.class
)
"
v-bind="forwarded"
@pointer-down-outside="(event) => {
const originalEvent = event.detail.originalEvent;
const target = originalEvent.target;
if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
event.preventDefault();
}
}"
>
<slot />
<DialogClose
class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary"
>
<Cross2Icon class="w-4 h-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogOverlay>
</DialogPortal>
</template>
@@ -0,0 +1,30 @@
<script setup>
import { computed } from "vue";
import { DialogTitle, useForwardProps } from "radix-vue";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DialogTitle
v-bind="forwardedProps"
:class="
cn('text-lg font-semibold leading-none tracking-tight', props.class)
"
>
<slot />
</DialogTitle>
</template>
@@ -0,0 +1,14 @@
<script setup>
import { DialogTrigger } from "radix-vue";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
});
</script>
<template>
<DialogTrigger v-bind="props">
<slot />
</DialogTrigger>
</template>
@@ -0,0 +1,9 @@
export { default as Dialog } from "./Dialog.vue";
export { default as DialogClose } from "./DialogClose.vue";
export { default as DialogTrigger } from "./DialogTrigger.vue";
export { default as DialogHeader } from "./DialogHeader.vue";
export { default as DialogTitle } from "./DialogTitle.vue";
export { default as DialogDescription } from "./DialogDescription.vue";
export { default as DialogContent } from "./DialogContent.vue";
export { default as DialogScrollContent } from "./DialogScrollContent.vue";
export { default as DialogFooter } from "./DialogFooter.vue";
@@ -0,0 +1,19 @@
<script setup>
import { DropdownMenuRoot, useForwardPropsEmits } from "radix-vue";
const props = defineProps({
defaultOpen: { type: Boolean, required: false },
open: { type: Boolean, required: false },
dir: { type: String, required: false },
modal: { type: Boolean, required: false },
});
const emits = defineEmits(["update:open"]);
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DropdownMenuRoot v-bind="forwarded">
<slot />
</DropdownMenuRoot>
</template>
@@ -0,0 +1,47 @@
<script setup>
import { computed } from "vue";
import {
DropdownMenuCheckboxItem,
DropdownMenuItemIndicator,
useForwardPropsEmits,
} from "radix-vue";
import { CheckIcon } from "@radix-icons/vue";
import { cn } from "@/lib/utils";
const props = defineProps({
checked: { type: [Boolean, String], required: false },
disabled: { type: Boolean, required: false },
textValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const emits = defineEmits(["select", "update:checked"]);
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuCheckboxItem
v-bind="forwarded"
:class="
cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class
)
"
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<CheckIcon class="w-4 h-4" />
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuCheckboxItem>
</template>
@@ -0,0 +1,60 @@
<script setup>
import { computed } from "vue";
import {
DropdownMenuContent,
DropdownMenuPortal,
useForwardPropsEmits,
} from "radix-vue";
import { cn } from "@/lib/utils";
const props = defineProps({
forceMount: { type: Boolean, required: false },
loop: { type: Boolean, required: false },
side: { type: null, required: false },
sideOffset: { type: Number, required: false, default: 4 },
align: { type: null, required: false },
alignOffset: { type: Number, required: false },
avoidCollisions: { type: Boolean, required: false },
collisionBoundary: { type: null, required: false },
collisionPadding: { type: [Number, Object], required: false },
arrowPadding: { type: Number, required: false },
sticky: { type: String, required: false },
hideWhenDetached: { type: Boolean, required: false },
updatePositionStrategy: { type: String, required: false },
prioritizePosition: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const emits = defineEmits([
"escapeKeyDown",
"pointerDownOutside",
"focusOutside",
"interactOutside",
"closeAutoFocus",
]);
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuPortal>
<DropdownMenuContent
v-bind="forwarded"
:class="
cn(
'z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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',
props.class
)
"
>
<slot />
</DropdownMenuContent>
</DropdownMenuPortal>
</template>
@@ -0,0 +1,14 @@
<script setup>
import { DropdownMenuGroup } from "radix-vue";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
});
</script>
<template>
<DropdownMenuGroup v-bind="props">
<slot />
</DropdownMenuGroup>
</template>
@@ -0,0 +1,37 @@
<script setup>
import { computed } from "vue";
import { DropdownMenuItem, useForwardProps } from "radix-vue";
import { cn } from "@/lib/utils";
const props = defineProps({
disabled: { type: Boolean, required: false },
textValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
inset: { type: Boolean, required: false },
});
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DropdownMenuItem
v-bind="forwardedProps"
:class="
cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
props.class
)
"
>
<slot />
</DropdownMenuItem>
</template>
@@ -0,0 +1,31 @@
<script setup>
import { computed } from "vue";
import { DropdownMenuLabel, useForwardProps } from "radix-vue";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
inset: { type: Boolean, required: false },
});
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DropdownMenuLabel
v-bind="forwardedProps"
:class="
cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)
"
>
<slot />
</DropdownMenuLabel>
</template>
@@ -0,0 +1,18 @@
<script setup>
import { DropdownMenuRadioGroup, useForwardPropsEmits } from "radix-vue";
const props = defineProps({
modelValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
});
const emits = defineEmits(["update:modelValue"]);
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DropdownMenuRadioGroup v-bind="forwarded">
<slot />
</DropdownMenuRadioGroup>
</template>
@@ -0,0 +1,48 @@
<script setup>
import { computed } from "vue";
import {
DropdownMenuItemIndicator,
DropdownMenuRadioItem,
useForwardPropsEmits,
} from "radix-vue";
import { DotFilledIcon } from "@radix-icons/vue";
import { cn } from "@/lib/utils";
const props = defineProps({
value: { type: String, required: true },
disabled: { type: Boolean, required: false },
textValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const emits = defineEmits(["select"]);
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuRadioItem
v-bind="forwarded"
:class="
cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class
)
"
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<DotFilledIcon class="h-4 w-4 fill-current" />
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuRadioItem>
</template>
@@ -0,0 +1,24 @@
<script setup>
import { computed } from "vue";
import { DropdownMenuSeparator } from "radix-vue";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<DropdownMenuSeparator
v-bind="delegatedProps"
:class="cn('-mx-1 my-1 h-px bg-muted', props.class)"
/>
</template>
@@ -0,0 +1,13 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<span :class="cn('ml-auto text-xs tracking-widest opacity-60', props.class)">
<slot />
</span>
</template>
@@ -0,0 +1,17 @@
<script setup>
import { DropdownMenuSub, useForwardPropsEmits } from "radix-vue";
const props = defineProps({
defaultOpen: { type: Boolean, required: false },
open: { type: Boolean, required: false },
});
const emits = defineEmits(["update:open"]);
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DropdownMenuSub v-bind="forwarded">
<slot />
</DropdownMenuSub>
</template>
@@ -0,0 +1,54 @@
<script setup>
import { computed } from "vue";
import { DropdownMenuSubContent, useForwardPropsEmits } from "radix-vue";
import { cn } from "@/lib/utils";
const props = defineProps({
forceMount: { type: Boolean, required: false },
loop: { type: Boolean, required: false },
sideOffset: { type: Number, required: false },
alignOffset: { type: Number, required: false },
avoidCollisions: { type: Boolean, required: false },
collisionBoundary: { type: null, required: false },
collisionPadding: { type: [Number, Object], required: false },
arrowPadding: { type: Number, required: false },
sticky: { type: String, required: false },
hideWhenDetached: { type: Boolean, required: false },
updatePositionStrategy: { type: String, required: false },
prioritizePosition: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const emits = defineEmits([
"escapeKeyDown",
"pointerDownOutside",
"focusOutside",
"interactOutside",
"entryFocus",
"openAutoFocus",
"closeAutoFocus",
]);
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuSubContent
v-bind="forwarded"
:class="
cn(
'z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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',
props.class
)
"
>
<slot />
</DropdownMenuSubContent>
</template>
@@ -0,0 +1,37 @@
<script setup>
import { computed } from "vue";
import { DropdownMenuSubTrigger, useForwardProps } from "radix-vue";
import { ChevronRightIcon } from "@radix-icons/vue";
import { cn } from "@/lib/utils";
const props = defineProps({
disabled: { type: Boolean, required: false },
textValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DropdownMenuSubTrigger
v-bind="forwardedProps"
:class="
cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
props.class
)
"
>
<slot />
<ChevronRightIcon class="ml-auto h-4 w-4" />
</DropdownMenuSubTrigger>
</template>
@@ -0,0 +1,17 @@
<script setup>
import { DropdownMenuTrigger, useForwardProps } from "radix-vue";
const props = defineProps({
disabled: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
});
const forwardedProps = useForwardProps(props);
</script>
<template>
<DropdownMenuTrigger class="outline-none" v-bind="forwardedProps">
<slot />
</DropdownMenuTrigger>
</template>
@@ -0,0 +1,16 @@
export { DropdownMenuPortal } from "radix-vue";
export { default as DropdownMenu } from "./DropdownMenu.vue";
export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue";
export { default as DropdownMenuContent } from "./DropdownMenuContent.vue";
export { default as DropdownMenuGroup } from "./DropdownMenuGroup.vue";
export { default as DropdownMenuRadioGroup } from "./DropdownMenuRadioGroup.vue";
export { default as DropdownMenuItem } from "./DropdownMenuItem.vue";
export { default as DropdownMenuCheckboxItem } from "./DropdownMenuCheckboxItem.vue";
export { default as DropdownMenuRadioItem } from "./DropdownMenuRadioItem.vue";
export { default as DropdownMenuShortcut } from "./DropdownMenuShortcut.vue";
export { default as DropdownMenuSeparator } from "./DropdownMenuSeparator.vue";
export { default as DropdownMenuLabel } from "./DropdownMenuLabel.vue";
export { default as DropdownMenuSub } from "./DropdownMenuSub.vue";
export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue";
export { default as DropdownMenuSubContent } from "./DropdownMenuSubContent.vue";
@@ -0,0 +1,18 @@
<script setup lang="ts">
import { TriangleAlert } from 'lucide-vue-next';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
defineProps({
errorMessage: String,
});
</script>
<template>
<Alert variant="destructive" v-if="errorMessage">
<TriangleAlert class="w-4 h-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
{{ errorMessage }}
</AlertDescription>
</Alert>
</template>
@@ -0,0 +1 @@
export { default as Error } from "./Error.vue";
@@ -0,0 +1,29 @@
<script setup>
import { useVModel } from "@vueuse/core";
import { cn } from "@/lib/utils";
const props = defineProps({
defaultValue: { type: [String, Number], required: false },
modelValue: { type: [String, Number], required: false },
class: { type: null, required: false },
});
const emits = defineEmits(["update:modelValue"]);
const modelValue = useVModel(props, "modelValue", emits, {
passive: true,
defaultValue: props.defaultValue,
});
</script>
<template>
<input
v-model="modelValue"
:class="
cn(
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
props.class
)
"
/>
</template>
@@ -0,0 +1 @@
export { default as Input } from "./Input.vue";
@@ -0,0 +1,32 @@
<script setup>
import { computed } from "vue";
import { Label } from "radix-vue";
import { cn } from "@/lib/utils";
const props = defineProps({
for: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<Label
v-bind="delegatedProps"
:class="
cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
props.class
)
"
>
<slot />
</Label>
</template>
@@ -0,0 +1 @@
export { default as Label } from "./Label.vue";
@@ -0,0 +1,18 @@
<script setup>
import { PopoverRoot, useForwardPropsEmits } from "radix-vue";
const props = defineProps({
defaultOpen: { type: Boolean, required: false },
open: { type: Boolean, required: false },
modal: { type: Boolean, required: false },
});
const emits = defineEmits(["update:open"]);
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<PopoverRoot v-bind="forwarded">
<slot />
</PopoverRoot>
</template>
@@ -0,0 +1,62 @@
<script setup>
import { computed } from "vue";
import { PopoverContent, PopoverPortal, useForwardPropsEmits } from "radix-vue";
import { cn } from "@/lib/utils";
defineOptions({
inheritAttrs: false,
});
const props = defineProps({
forceMount: { type: Boolean, required: false },
trapFocus: { type: Boolean, required: false },
side: { type: null, required: false },
sideOffset: { type: Number, required: false, default: 4 },
align: { type: null, required: false, default: "center" },
alignOffset: { type: Number, required: false },
avoidCollisions: { type: Boolean, required: false },
collisionBoundary: { type: null, required: false },
collisionPadding: { type: [Number, Object], required: false },
arrowPadding: { type: Number, required: false },
sticky: { type: String, required: false },
hideWhenDetached: { type: Boolean, required: false },
updatePositionStrategy: { type: String, required: false },
prioritizePosition: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
disableOutsidePointerEvents: { type: Boolean, required: false },
class: { type: null, required: false },
});
const emits = defineEmits([
"escapeKeyDown",
"pointerDownOutside",
"focusOutside",
"interactOutside",
"openAutoFocus",
"closeAutoFocus",
]);
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<PopoverPortal>
<PopoverContent
v-bind="{ ...forwarded, ...$attrs }"
:class="
cn(
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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',
props.class
)
"
>
<slot />
</PopoverContent>
</PopoverPortal>
</template>
@@ -0,0 +1,14 @@
<script setup>
import { PopoverTrigger } from "radix-vue";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
});
</script>
<template>
<PopoverTrigger v-bind="props">
<slot />
</PopoverTrigger>
</template>
@@ -0,0 +1,4 @@
export { PopoverAnchor } from "radix-vue";
export { default as Popover } from "./Popover.vue";
export { default as PopoverTrigger } from "./PopoverTrigger.vue";
export { default as PopoverContent } from "./PopoverContent.vue";

Some files were not shown because too many files have changed in this diff Show More