first init
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Box } from '@/components/ui/box'
|
||||
import { Lucide } from '@/components/ui/lucide'
|
||||
import {
|
||||
ComboboxRoot,
|
||||
ComboboxControl,
|
||||
ComboboxTrigger,
|
||||
ComboboxContent,
|
||||
ComboboxItemGroup,
|
||||
ComboboxItem,
|
||||
ComboboxItemText,
|
||||
} from '@/components/ui/combobox'
|
||||
import { logout } from '@/modules/auth'
|
||||
import { useImpersonate } from '@/modules/user/composables/useImpersonate'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useRoleSwitcher } from '@/composables/useRoleSwitcher'
|
||||
|
||||
interface Props {
|
||||
class?: string
|
||||
boxClass?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const {
|
||||
canSwitchRole,
|
||||
roleCollection,
|
||||
roleComboValue,
|
||||
switchingRole,
|
||||
onRoleValueChange,
|
||||
onRoleInputValueChange,
|
||||
} = useRoleSwitcher()
|
||||
|
||||
const { impersonating, loading: impersonateLoading, stopImpersonation } = useImpersonate()
|
||||
|
||||
const goToProfile = (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
router.push({ name: 'profile-overview-2' })
|
||||
}
|
||||
|
||||
const handleLogout = async (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (impersonating.value || impersonateLoading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await logout()
|
||||
} catch {
|
||||
// Still redirect to login if the session is already invalid.
|
||||
}
|
||||
|
||||
authStore.clearSession()
|
||||
await router.push({ name: 'login' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-bind="$attrs" :class="[
|
||||
'invisible opacity-0 scale-95 transition-all duration-200 delay-0 group-hover/profile:visible group-hover/profile:opacity-100 group-hover/profile:scale-100 group-hover/profile:delay-200',
|
||||
props.class,
|
||||
]">
|
||||
<Box
|
||||
:class="`text-foreground before:shadow-foreground/5 flex w-64 flex-col gap-2.5 px-6 py-5 before:rounded-2xl before:shadow-xl before:backdrop-blur after:rounded-2xl ${props.boxClass ?? ''}`">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<div class="font-medium">
|
||||
Selamat Datang, {{ authStore.userName }}
|
||||
</div>
|
||||
<div v-if="authStore.userActiveRole" class="text-xs opacity-70">
|
||||
{{ authStore.userActiveRole }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="canSwitchRole">
|
||||
<div class="bg-foreground/5 h-px"></div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="text-xs font-medium opacity-70">Tukar Peranan</div>
|
||||
<ComboboxRoot class="w-full" :collection="roleCollection" :value="roleComboValue"
|
||||
:disabled="switchingRole || authStore.loading" @value-change="onRoleValueChange"
|
||||
@input-value-change="onRoleInputValueChange">
|
||||
<ComboboxControl>
|
||||
<ComboboxTrigger class="w-full" />
|
||||
</ComboboxControl>
|
||||
<ComboboxContent class="w-(--reference-width)">
|
||||
<ComboboxItemGroup>
|
||||
<ComboboxItem v-for="item in roleCollection.items" :key="item.value" :item="item">
|
||||
<ComboboxItemText>{{ item.label }}</ComboboxItemText>
|
||||
</ComboboxItem>
|
||||
</ComboboxItemGroup>
|
||||
</ComboboxContent>
|
||||
</ComboboxRoot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="bg-foreground/5 h-px"></div>
|
||||
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<a class="hover:bg-foreground/5 -mx-3 flex gap-2.5 rounded-lg px-4 py-1.5" href="" @click="goToProfile">
|
||||
<Lucide icon="Users" /> Profil
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-foreground/5 h-px"></div>
|
||||
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<a v-if="impersonating" href="" class="hover:bg-foreground/5 -mx-3 flex gap-2.5 rounded-lg px-4 py-1.5"
|
||||
:class="{ 'pointer-events-none opacity-50': impersonateLoading }" @click.prevent="stopImpersonation">
|
||||
<Lucide icon="VenetianMask" /> Keluar Penyamaran
|
||||
</a>
|
||||
<a class="hover:bg-foreground/5 -mx-3 flex gap-2.5 rounded-lg px-4 py-1.5"
|
||||
:class="(impersonating || impersonateLoading) && 'pointer-events-none cursor-not-allowed opacity-40'" href=""
|
||||
:aria-disabled="impersonating || impersonateLoading" @click="handleLogout">
|
||||
<Lucide icon="Power" /> Log Keluar
|
||||
</a>
|
||||
</div>
|
||||
</Box>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { Lucide } from '@/components/ui/lucide'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
interface Props {
|
||||
class?: string
|
||||
innerClass?: any
|
||||
avatarClass?: string
|
||||
textClass?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const authStore = useAuthStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['side-menu__account group/profile transition-[width]', props.class]">
|
||||
<div :class="['flex cursor-pointer items-center transition', props.innerClass]">
|
||||
<div :class="[
|
||||
'relative flex-none overflow-hidden rounded-full border-4',
|
||||
props.avatarClass ?? 'h-10 w-10 border-background/20 dark:border-foreground/20',
|
||||
]">
|
||||
<img v-if="authStore.userImageUrl" class="absolute top-0 h-full w-full object-cover"
|
||||
:src="authStore.userImageUrl" :alt="authStore.userName" />
|
||||
<div v-else
|
||||
class="bg-primary text-primary-foreground absolute inset-0 flex items-center justify-center text-sm font-medium">
|
||||
{{ authStore.userName }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-3 flex w-full items-center gap-3 overflow-hidden transition-opacity
|
||||
group-[.side-menu--collapsed]:group-hover:opacity-100
|
||||
xl:group-[.side-menu--collapsed]:opacity-0">
|
||||
<div :class="['flex-1 min-w-0', props.textClass]">
|
||||
<!-- Name + Active Role -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="truncate font-semibold text-sm">
|
||||
{{ authStore.userName }}
|
||||
</div>
|
||||
|
||||
<span class="shrink-0 rounded-full bg-green-500 px-2 py-0.5 text-[10px] font-medium text-white">
|
||||
{{ authStore.userActiveRole }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Position -->
|
||||
<div class="mt-0.5 truncate text-xs text-slate-500">
|
||||
<span class="font-medium text-slate-600">Jawatan:</span>
|
||||
{{ authStore.userPosition }}
|
||||
</div>
|
||||
|
||||
<!-- Roles Summary -->
|
||||
<div class="mt-2 flex items-center gap-1 text-[11px] text-slate-500">
|
||||
<Lucide icon="Shield" class="h-3 w-3" />
|
||||
<span class="truncate">
|
||||
{{authStore.roles.map(role => role.name).join(' • ')}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Lucide icon="ChevronRight"
|
||||
class="h-4 w-4 flex-none text-slate-400 transition-transform group-hover:translate-x-1" />
|
||||
</div>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as AccountDropdown } from './AccountDropdown.vue'
|
||||
export { default as AccountTrigger } from './AccountTrigger.vue'
|
||||
@@ -0,0 +1,99 @@
|
||||
<script lang="ts" setup>
|
||||
import { Chart, getColor } from '@/components/ui/chart'
|
||||
import type { Chart as ChartInstance } from 'chart.js'
|
||||
|
||||
// Fake visitor data
|
||||
const chartData = new Array(40).fill(0).map((data, key) => {
|
||||
if (key % 3 == 0 || key % 5 == 0) {
|
||||
return Math.ceil(Math.random() * (0 - 20) + 20)
|
||||
} else {
|
||||
return Math.ceil(Math.random() * (0 - 7) + 7)
|
||||
}
|
||||
})
|
||||
|
||||
// Fake visitor bar color
|
||||
const chartColor = () =>
|
||||
chartData.map((data) => {
|
||||
if (data >= 8 && data <= 14) {
|
||||
return getColor('--color-primary', 0.5)
|
||||
} else if (data >= 15) {
|
||||
return getColor('--color-primary', 0.6)
|
||||
}
|
||||
|
||||
return getColor('--color-primary', 0.4)
|
||||
})
|
||||
|
||||
const getRef = (chart: ChartInstance<'bar'>) => {
|
||||
setInterval(() => {
|
||||
const datasets = chart.data.datasets[0]
|
||||
if (datasets) {
|
||||
// Swap visitor data
|
||||
let newData = datasets.data[0]
|
||||
datasets.data.shift()
|
||||
newData !== undefined && datasets.data.push(newData)
|
||||
|
||||
// Swap visitor bar color
|
||||
if (datasets.backgroundColor && Array.isArray(datasets.backgroundColor)) {
|
||||
let newColor = datasets.backgroundColor[0]
|
||||
datasets.backgroundColor.shift()
|
||||
datasets.backgroundColor.push(newColor)
|
||||
}
|
||||
}
|
||||
|
||||
chart.update()
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Chart
|
||||
:config="{
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: chartData,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Html Template',
|
||||
barThickness: 6,
|
||||
data: chartData,
|
||||
backgroundColor: chartColor(),
|
||||
borderColor: () => getColor('--color-primary', 0.1),
|
||||
borderWidth: 0.8,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
display: false,
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
ticks: {
|
||||
display: false,
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
border: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}"
|
||||
:get-ref="getRef"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts" setup>
|
||||
import { Chart, getColor } from '@/components/ui/chart'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Chart
|
||||
:config="{
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Html Template',
|
||||
barThickness: 8,
|
||||
maxBarThickness: 6,
|
||||
data: [60, 150, 30, 200, 180, 50, 180, 120, 230, 180, 250, 270],
|
||||
backgroundColor: () => getColor('--color-primary', 0.3),
|
||||
borderColor: () => getColor('--color-primary', 0.6),
|
||||
borderWidth: 0.5,
|
||||
},
|
||||
{
|
||||
label: 'VueJs Template',
|
||||
barThickness: 8,
|
||||
maxBarThickness: 6,
|
||||
data: [50, 135, 40, 180, 190, 60, 150, 90, 250, 170, 240, 250],
|
||||
backgroundColor: () => getColor('--color-foreground', 0.2),
|
||||
borderColor: () => getColor('--color-foreground', 0.3),
|
||||
borderWidth: 0.5,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
font: {
|
||||
size: 11,
|
||||
},
|
||||
color: () => getColor('--color-foreground', 0.5),
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
border: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
ticks: {
|
||||
display: false,
|
||||
},
|
||||
grid: {
|
||||
color: () => getColor('--color-foreground', 0.2),
|
||||
},
|
||||
border: {
|
||||
dash: [2, 2],
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts" setup>
|
||||
import { Chart, getColor } from '@/components/ui/chart'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Chart
|
||||
:config="{
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Html', 'Vuejs', 'Laravel'],
|
||||
datasets: [
|
||||
{
|
||||
data: [15, 10, 65],
|
||||
backgroundColor: [
|
||||
getColor('--color-pending', 0.4),
|
||||
getColor('--color-primary', 0.4),
|
||||
getColor('--color-warning', 0.4),
|
||||
],
|
||||
borderWidth: 1,
|
||||
borderColor: [
|
||||
getColor('--color-pending', 0.9),
|
||||
getColor('--color-primary', 0.9),
|
||||
getColor('--color-warning', 0.9),
|
||||
],
|
||||
offset: 20,
|
||||
borderRadius: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
cutout: '85%',
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts" setup>
|
||||
import { Chart, getColor } from '@/components/ui/chart'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Chart
|
||||
:config="{
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Yellow', 'Dark'],
|
||||
datasets: [
|
||||
{
|
||||
data: [15, 10, 65],
|
||||
backgroundColor: [
|
||||
getColor('--color-pending', 0.4),
|
||||
getColor('--color-warning', 0.4),
|
||||
getColor('--color-primary', 0.4),
|
||||
],
|
||||
borderWidth: 1,
|
||||
borderColor: [
|
||||
getColor('--color-pending', 0.9),
|
||||
getColor('--color-warning', 0.9),
|
||||
getColor('--color-primary', 0.9),
|
||||
],
|
||||
offset: 20,
|
||||
borderRadius: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
cutout: '83%',
|
||||
},
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,92 @@
|
||||
<script lang="ts" setup>
|
||||
import { Chart, getColor } from '@/components/ui/chart'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Chart
|
||||
:config="{
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
],
|
||||
datasets: [
|
||||
{
|
||||
label: '# of Votes',
|
||||
data: [0, 200, 250, 200, 700, 550, 650, 1050, 950, 1100, 900, 1200],
|
||||
borderWidth: 1.5,
|
||||
borderColor: () => getColor('--color-primary', 0.5),
|
||||
backgroundColor: 'transparent',
|
||||
pointBorderColor: 'transparent',
|
||||
tension: 0.3,
|
||||
},
|
||||
{
|
||||
label: '# of Votes',
|
||||
data: [0, 300, 400, 560, 320, 600, 720, 850, 690, 805, 1200, 1010],
|
||||
borderWidth: 1.5,
|
||||
borderDash: [1, 4],
|
||||
borderColor: () => getColor('--color-foreground'),
|
||||
backgroundColor: 'transparent',
|
||||
pointBorderColor: 'transparent',
|
||||
tension: 0.3,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
font: {
|
||||
size: 12,
|
||||
},
|
||||
color: () => getColor('--color-foreground', 0.5),
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
border: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
ticks: {
|
||||
font: {
|
||||
size: 12,
|
||||
},
|
||||
color: () => getColor('--color-foreground', 0.5),
|
||||
callback: function (value, index, values) {
|
||||
return '$' + value
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
color: () => getColor('--color-foreground', 0.2),
|
||||
},
|
||||
border: {
|
||||
dash: [2, 2],
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,72 @@
|
||||
<script lang="ts" setup>
|
||||
import { Chart, getColor } from '@/components/ui/chart'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Chart
|
||||
:config="{
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
],
|
||||
datasets: [
|
||||
{
|
||||
label: '# of Votes',
|
||||
data: [0, 200, 250, 200, 500, 450, 850, 1050, 950, 1100, 900, 1200],
|
||||
borderWidth: 1,
|
||||
borderColor: () => getColor('--color-primary', 0.8),
|
||||
backgroundColor: 'transparent',
|
||||
pointBorderColor: 'transparent',
|
||||
tension: 0.4,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
display: false,
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
border: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
ticks: {
|
||||
display: false,
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
border: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,82 @@
|
||||
<script lang="ts" setup>
|
||||
import { Chart, getColor } from '@/components/ui/chart'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Chart
|
||||
:config="{
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
],
|
||||
datasets: [
|
||||
{
|
||||
label: '# of Votes',
|
||||
data: [0, 200, 250, 200, 700, 550, 650, 1050, 950, 1100, 900, 1200],
|
||||
borderWidth: 1,
|
||||
borderColor: () => getColor('--color-primary', 0.8),
|
||||
backgroundColor: 'transparent',
|
||||
pointBorderColor: 'transparent',
|
||||
tension: 0.4,
|
||||
},
|
||||
{
|
||||
label: '# of Votes',
|
||||
data: [0, 300, 400, 560, 320, 600, 720, 850, 690, 805, 1200, 1010],
|
||||
borderDash: [2, 2],
|
||||
borderWidth: 1,
|
||||
borderColor: () => getColor('--color-foreground', 0.8),
|
||||
backgroundColor: 'transparent',
|
||||
pointBorderColor: 'transparent',
|
||||
tension: 0.4,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
display: false,
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
border: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
ticks: {
|
||||
display: false,
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
border: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts" setup>
|
||||
import { Chart, getColor } from '@/components/ui/chart'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Chart
|
||||
:config="{
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: ['31 - 50 Years old', '>= 50 Years old', '17 - 30 Years old'],
|
||||
datasets: [
|
||||
{
|
||||
data: [15, 10, 65],
|
||||
backgroundColor: [
|
||||
getColor('--color-pending', 0.4),
|
||||
getColor('--color-warning', 0.4),
|
||||
getColor('--color-primary', 0.4),
|
||||
],
|
||||
borderWidth: 1,
|
||||
borderColor: [
|
||||
getColor('--color-pending', 0.9),
|
||||
getColor('--color-warning', 0.9),
|
||||
getColor('--color-primary', 0.9),
|
||||
],
|
||||
offset: 20,
|
||||
borderRadius: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,8 @@
|
||||
export { default as Line1 } from './Line1.vue' //report-line-chart
|
||||
export { default as Line2 } from './Line2.vue' //simple-line-chart-1
|
||||
export { default as Line3 } from './Line3.vue' //simple-line-chart-3, simple-line-chart-4
|
||||
export { default as Pie1 } from './Pie1.vue' //report-pie-chart
|
||||
export { default as Donut1 } from './Donut1.vue' //report-donut-chart
|
||||
export { default as Donut2 } from './Donut2.vue' //report-donut-chart-1
|
||||
export { default as Bar1 } from './Bar1.vue' //report-bar-chart
|
||||
export { default as Bar2 } from './Bar2.vue' //report-bar-chart-1
|
||||
@@ -0,0 +1,132 @@
|
||||
<script lang="ts" setup>
|
||||
import _ from 'lodash'
|
||||
import { ref } from 'vue'
|
||||
import fakers from '@/utils/faker'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Box } from '@/components/ui/box'
|
||||
import {
|
||||
MenuRoot,
|
||||
MenuTrigger,
|
||||
MenuPositioner,
|
||||
MenuContent,
|
||||
MenuCheckboxItem,
|
||||
} from '@/components/ui/menu'
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
} from '@/components/ui/table'
|
||||
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
|
||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import {
|
||||
CarouselRoot,
|
||||
CarouselPrevTrigger,
|
||||
CarouselNextTrigger,
|
||||
CarouselItemGroup,
|
||||
CarouselItem,
|
||||
} from '@/components/ui/carousel'
|
||||
import {
|
||||
DatePickerRoot,
|
||||
DatePickerControl,
|
||||
DatePickerInput,
|
||||
DatePickerTrigger,
|
||||
DatePickerPositioner,
|
||||
DatePickerContent,
|
||||
DatePickerYearSelect,
|
||||
DatePickerMonthSelect,
|
||||
DatePickerView,
|
||||
DatePickerViewControl,
|
||||
DatePickerPrevTrigger,
|
||||
DatePickerNextTrigger,
|
||||
DatePickerRangeText,
|
||||
DatePickerTable,
|
||||
DatePickerTableHead,
|
||||
DatePickerTableRow,
|
||||
DatePickerTableHeader,
|
||||
DatePickerTableBody,
|
||||
DatePickerTableCell,
|
||||
DatePickerTableCellTrigger,
|
||||
DatePickerContext,
|
||||
} from '@/components/ui/datepicker'
|
||||
import {
|
||||
PaginationContext,
|
||||
PaginationRoot,
|
||||
PaginationItem,
|
||||
PaginationPrevTrigger,
|
||||
PaginationNextTrigger,
|
||||
PaginationEllipsis,
|
||||
} from '@/components/ui/pagination'
|
||||
import { Map } from '@/components/ui/map'
|
||||
import { Lucide } from '@/components/ui/lucide'
|
||||
import { Line1, Line2, Pie1, Donut1, Donut2 } from '@/components/chart-presets'
|
||||
|
||||
const pc = ref(false)
|
||||
const electronic = ref(false)
|
||||
const smartphone = ref(false)
|
||||
const photography = ref(false)
|
||||
const sport = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- BEGIN: Daily Notes -->
|
||||
<CarouselRoot :default-page="0" :slide-count="4">
|
||||
<div class="flex h-10 items-center">
|
||||
<h2 class="me-auto truncate text-lg font-medium">Daily Notes</h2>
|
||||
<CarouselPrevTrigger as-child>
|
||||
<Box as-child>
|
||||
<Button variant="ghost" class="box me-2 px-2">
|
||||
<Lucide icon="ChevronLeft" />
|
||||
</Button>
|
||||
</Box>
|
||||
</CarouselPrevTrigger>
|
||||
<CarouselNextTrigger as-child>
|
||||
<Box as-child>
|
||||
<Button variant="ghost" class="box px-2">
|
||||
<Lucide icon="ChevronRight" />
|
||||
</Button>
|
||||
</Box>
|
||||
</CarouselNextTrigger>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<Box>
|
||||
<CarouselItemGroup>
|
||||
<CarouselItem
|
||||
as-child
|
||||
v-for="(faker, fakerKey) in _.take(fakers, 4)"
|
||||
:key="fakerKey"
|
||||
:index="fakerKey"
|
||||
>
|
||||
<div>
|
||||
<div class="truncate text-base font-medium">
|
||||
{{ faker['news'][0]!['title'] }}
|
||||
</div>
|
||||
<div class="mt-2 text-xs opacity-50">Posted 20 Hours ago</div>
|
||||
<div class="mt-2 text-justify opacity-70 line-clamp-3">
|
||||
{{ faker['news'][0]!['shortContent'] }}
|
||||
</div>
|
||||
<div class="mt-5 flex font-medium">
|
||||
<Button variant="ghost" type="button" size="sm" class="border border-foreground/20">
|
||||
View Notes
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="ms-auto border border-foreground/20"
|
||||
type="button"
|
||||
size="sm"
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
</CarouselItemGroup>
|
||||
</Box>
|
||||
</div>
|
||||
</CarouselRoot>
|
||||
<!-- END: Daily Notes -->
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as DailyNotes } from './DailyNotes.vue'
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<a
|
||||
class="pr-3 pl-3.5 py-1 border border-foreground/30 bg-background/20 flex items-center gap-2 text-xs"
|
||||
target="_blank"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
</template>
|
||||
@@ -0,0 +1,101 @@
|
||||
<script lang="ts" setup>
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import {
|
||||
SquareChevronRight,
|
||||
Clipboard,
|
||||
Check,
|
||||
} from "@lucide/vue";
|
||||
import { ref, useSlots, onUnmounted } from "vue";
|
||||
|
||||
const { class: className } = defineProps<{
|
||||
class?: string;
|
||||
}>();
|
||||
|
||||
const slots = useSlots();
|
||||
const packageManager = ref("yarn");
|
||||
const copied = ref(false);
|
||||
const commandRef = ref<HTMLDivElement | null>(null);
|
||||
let copyTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
onUnmounted(() => {
|
||||
if (copyTimeout) clearTimeout(copyTimeout);
|
||||
});
|
||||
|
||||
function handleCopy() {
|
||||
const codeElement = commandRef.value;
|
||||
if (!codeElement) return;
|
||||
|
||||
const text = codeElement.textContent || "";
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
copied.value = true;
|
||||
|
||||
if (copyTimeout) clearTimeout(copyTimeout);
|
||||
copyTimeout = setTimeout(() => {
|
||||
copied.value = false;
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
function getSlotText() {
|
||||
const slotContent = slots.default?.();
|
||||
if (!slotContent || slotContent.length === 0) return "";
|
||||
return slotContent
|
||||
.map((vnode) => (typeof vnode.children === "string" ? vnode.children : ""))
|
||||
.join("");
|
||||
}
|
||||
|
||||
function getDisplayText() {
|
||||
const text = getSlotText();
|
||||
if (packageManager.value === "npm") {
|
||||
return text.replace("add", "install");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="twMerge([
|
||||
'mt-8 mb-10 border border-foreground/20 bg-background/50',
|
||||
className,
|
||||
])
|
||||
">
|
||||
<div class="flex items-center py-2 px-5 gap-5">
|
||||
<SquareChevronRight class="stroke-1 size-4 fill-foreground/10" />
|
||||
<div class="flex items-center w-full gap-2">
|
||||
<div @click="packageManager = 'pnpm'" :class="twMerge([
|
||||
'cursor-pointer px-3.5 py-1 [&.active]:border [&.active]:border-foreground/20 [&.active]:bg-foreground/10',
|
||||
packageManager == 'pnpm' && 'active',
|
||||
])
|
||||
">
|
||||
pnpm
|
||||
</div>
|
||||
<div @click="packageManager = 'npm'" :class="twMerge([
|
||||
'cursor-pointer px-3.5 py-1 [&.active]:border [&.active]:border-foreground/20 [&.active]:bg-foreground/10',
|
||||
packageManager == 'npm' && 'active',
|
||||
])
|
||||
">
|
||||
npm
|
||||
</div>
|
||||
<div @click="packageManager = 'yarn'" :class="twMerge([
|
||||
'cursor-pointer px-3.5 py-1 [&.active]:border [&.active]:border-foreground/20 [&.active]:bg-foreground/10',
|
||||
packageManager == 'yarn' && 'active',
|
||||
])
|
||||
">
|
||||
yarn
|
||||
</div>
|
||||
<div @click="packageManager = 'bun'" :class="twMerge([
|
||||
'cursor-pointer px-3.5 py-1 [&.active]:border [&.active]:border-foreground/20 [&.active]:bg-foreground/10',
|
||||
packageManager == 'bun' && 'active',
|
||||
])
|
||||
">
|
||||
bun
|
||||
</div>
|
||||
<Check v-if="copied" class="ms-auto cursor-pointer size-4 stroke-1 fill-foreground/10" />
|
||||
<Clipboard v-else @click="handleCopy" class="ms-auto cursor-pointer size-4 stroke-1 fill-foreground/10" />
|
||||
</div>
|
||||
</div>
|
||||
<div ref="commandRef" class="p-5 border-t border-foreground/20">
|
||||
{{ packageManager }} {{ getDisplayText() }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div class="hidden xl:block pl-14 col-span-2 text-foreground/50">
|
||||
<div class="flex flex-col lg:mt-10 pt-10 lg:pt-0 sticky top-40">
|
||||
<div class="font-medium text-foreground/70 mb-2">On This Page</div>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts" setup>
|
||||
import * as tabs from "@zag-js/tabs";
|
||||
import { useMachine, normalizeProps } from "@zag-js/vue";
|
||||
import { computed } from "vue";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
|
||||
const { class: className } = defineProps<{
|
||||
class?: string;
|
||||
}>();
|
||||
|
||||
const service = useMachine(tabs.machine, {
|
||||
id: crypto.randomUUID(),
|
||||
defaultValue: "preview",
|
||||
});
|
||||
|
||||
const api = computed(() => tabs.connect(service, normalizeProps));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-10" v-bind="api.getRootProps()">
|
||||
<div class="flex px-3" v-bind="api.getListProps()">
|
||||
<button v-bind="api.getTriggerProps({ value: 'preview' })"
|
||||
class="px-2.5 opacity-70 cursor-pointer data-selected:opacity-100">
|
||||
Preview
|
||||
</button>
|
||||
<button v-bind="api.getTriggerProps({ value: 'code' })"
|
||||
class="px-2.5 opacity-70 cursor-pointer data-selected:opacity-100">
|
||||
Code
|
||||
</button>
|
||||
</div>
|
||||
<div v-bind="api.getContentProps({ value: 'preview' })" :class="cn([
|
||||
'mt-4 backdrop-blur-md border border-foreground/20 bg-background/50 min-h-100 flex flex-col md:flex-row gap-3 items-center justify-center px-5 py-8 sm:p-8 lg:py-20 lg:px-20',
|
||||
className,
|
||||
])
|
||||
">
|
||||
<slot name="preview" />
|
||||
</div>
|
||||
<div v-bind="api.getContentProps({ value: 'code' })" class="-mt-4">
|
||||
<slot name="code" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script lang="ts" setup>
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { FileScan, Clipboard, ArrowDown, Check } from "@lucide/vue";
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
|
||||
const { title = "" } = defineProps<{
|
||||
title?: string;
|
||||
}>();
|
||||
|
||||
const expand = ref(false);
|
||||
const copied = ref(false);
|
||||
const codeRef = ref<HTMLDivElement | null>(null);
|
||||
let copyTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
if (codeRef.value) {
|
||||
if (
|
||||
codeRef.value.offsetHeight < 300 &&
|
||||
!(codeRef.value.offsetParent === null)
|
||||
) {
|
||||
expand.value = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (copyTimeout) clearTimeout(copyTimeout);
|
||||
});
|
||||
|
||||
function handleCopy() {
|
||||
const codeElement = codeRef.value?.querySelector("code");
|
||||
if (!codeElement) return;
|
||||
|
||||
const text = codeElement.textContent || "";
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
copied.value = true;
|
||||
|
||||
if (copyTimeout) clearTimeout(copyTimeout);
|
||||
copyTimeout = setTimeout(() => {
|
||||
copied.value = false;
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-8 mb-10 border border-foreground/20 bg-background/50 relative">
|
||||
<div class="flex items-center py-2 px-5 gap-5">
|
||||
<FileScan v-if="title" class="stroke-1 size-4 fill-foreground/10" />
|
||||
<div class="flex items-center w-full gap-2">
|
||||
<div>{{ title }}</div>
|
||||
<div @click="expand = !expand" :class="twMerge([
|
||||
'ms-auto mr-2 cursor-pointer px-3.5 py-1 relative after:w-px after:h-5 after:inset-y-0 after:my-auto after:bg-foreground/20 after:right-0 after:absolute',
|
||||
codeRef &&
|
||||
codeRef.offsetHeight < 300 &&
|
||||
!(codeRef.offsetParent === null) &&
|
||||
'opacity-0',
|
||||
])
|
||||
">
|
||||
{{ expand ? "Collapse" : "Expand" }}
|
||||
</div>
|
||||
<Check v-if="copied" :class="twMerge([
|
||||
'cursor-pointer size-4 stroke-1 fill-foreground/10',
|
||||
!title && 'mt-1',
|
||||
])
|
||||
" />
|
||||
<Clipboard v-else @click="handleCopy" :class="twMerge([
|
||||
'cursor-pointer size-4 stroke-1 fill-foreground/10',
|
||||
!title && 'mt-1',
|
||||
])
|
||||
" />
|
||||
</div>
|
||||
</div>
|
||||
<div :class="twMerge([
|
||||
'text-xs px-5 border-t border-foreground/20 overflow-x-auto overflow-y-hidden before:h-3/4 before:absolute before:bg-linear-to-b before:from-transparent before:to-background/80 before:inset-x-0 before:bottom-0',
|
||||
expand && 'max-h-auto before:hidden',
|
||||
!expand && 'max-h-100',
|
||||
!title && '-mt-13 border-0',
|
||||
])
|
||||
" ref="codeRef">
|
||||
<pre><code><slot /></code></pre>
|
||||
<div v-if="!expand" @click="expand = true"
|
||||
class="absolute bottom-0 inset-x-0 mx-auto w-26 mb-8 py-1 cursor-pointer border border-foreground/50 bg-foreground/15 flex items-center justify-center gap-2 backdrop-blur">
|
||||
<ArrowDown class="size-4" /> Expand
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
const { class: className } = defineProps<{
|
||||
class?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p :class="twMerge(['mt-2 opacity-70', className])">
|
||||
<slot />
|
||||
</p>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="text-xl font-medium">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="mt-2 text-base opacity-70">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<h2 class="text-2xl font-medium">
|
||||
<slot />
|
||||
</h2>
|
||||
</template>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn([
|
||||
'p-7 sm:p-10 lg:px-20 lg:py-14 sm:mx-10 lg:ml-0 xl:mr-0 col-span-10 xl:col-span-8 border-x sm:border-y border-foreground/20 bg-background/50 mb-10 relative',
|
||||
'before:absolute before:inset-0 before:bg-background/50 before:z-[-1]',
|
||||
])
|
||||
">
|
||||
<div
|
||||
class="after:bg-accent after:bg-center after:bg-no-repeat after:fixed after:size-200 after:inset-x-0 after:mx-auto after:top-0 after:blur-2xl dark:after:opacity-50">
|
||||
<div class="z-50 relative">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
export { default as Wrapper } from "./Wrapper.vue";
|
||||
export { default as Menu } from "./Menu.vue";
|
||||
export { default as Title } from "./Title.vue";
|
||||
export { default as Subtitle } from "./Subtitle.vue";
|
||||
export { default as Preview } from "./Preview.vue";
|
||||
export { default as SectionTitle } from "./SectionTitle.vue";
|
||||
export { default as SectionContent } from "./SectionContent.vue";
|
||||
export { default as InstallPackage } from "./InstallPackage.vue";
|
||||
export { default as PreviewCode } from "./PreviewCode.vue";
|
||||
export { default as ApiButton } from "./ApiButton.vue";
|
||||
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import { Box } from '@/components/ui/box'
|
||||
import { Lucide } from '@/components/ui/lucide'
|
||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import fakers from '@/utils/faker'
|
||||
import _ from 'lodash'
|
||||
|
||||
interface Props {
|
||||
class?: string
|
||||
boxClass?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-bind="$attrs"
|
||||
:class="[
|
||||
'invisible opacity-0 scale-95 transition-all duration-200 delay-0 group-hover/notifications:visible group-hover/notifications:opacity-100 group-hover/notifications:scale-100 group-hover/notifications:delay-200',
|
||||
props.class,
|
||||
]"
|
||||
>
|
||||
<Box
|
||||
:class="`before:shadow-foreground/5 z-50 flex w-96 flex-col gap-2.5 px-6 py-5 before:rounded-2xl before:shadow-xl before:backdrop-blur after:rounded-2xl ${props.boxClass ?? ''}`"
|
||||
>
|
||||
<div class="flex place-content-between items-center">
|
||||
<div class="font-medium">Notifications</div>
|
||||
<a class="text-primary" href="">View More</a>
|
||||
</div>
|
||||
<div class="mt-1 flex flex-col gap-2.5">
|
||||
<a
|
||||
v-for="(faker, fakerKey) in _.take(fakers, 5)"
|
||||
:key="fakerKey"
|
||||
class="hover:border-foreground/10 hover:bg-foreground/5 -mx-2 flex items-center gap-3.5 rounded-2xl border border-transparent p-2"
|
||||
href=""
|
||||
>
|
||||
<AvatarRoot class="size-11 flex-none rounded-full">
|
||||
<AvatarFallback>PA</AvatarFallback>
|
||||
<AvatarImage :src="faker['photos'][2]" alt="avatar" />
|
||||
</AvatarRoot>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex place-content-between items-center">
|
||||
<div class="font-medium">{{ faker['users'][0]!['name'] }}</div>
|
||||
<div class="text-xs opacity-70">{{ faker['times'][0] }}</div>
|
||||
</div>
|
||||
<div class="line-clamp-2 opacity-70">
|
||||
{{ faker['news'][0]!['shortContent'] }}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</Box>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as NotificationDropdown } from './NotificationDropdown.vue'
|
||||
@@ -0,0 +1,151 @@
|
||||
<script lang="ts" setup>
|
||||
import _ from 'lodash'
|
||||
import { ref } from 'vue'
|
||||
import fakers from '@/utils/faker'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Box } from '@/components/ui/box'
|
||||
import {
|
||||
MenuRoot,
|
||||
MenuTrigger,
|
||||
MenuPositioner,
|
||||
MenuContent,
|
||||
MenuCheckboxItem,
|
||||
} from '@/components/ui/menu'
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
} from '@/components/ui/table'
|
||||
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
|
||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import {
|
||||
CarouselRoot,
|
||||
CarouselPrevTrigger,
|
||||
CarouselNextTrigger,
|
||||
CarouselItemGroup,
|
||||
CarouselItem,
|
||||
} from '@/components/ui/carousel'
|
||||
import {
|
||||
DatePickerRoot,
|
||||
DatePickerControl,
|
||||
DatePickerInput,
|
||||
DatePickerTrigger,
|
||||
DatePickerPositioner,
|
||||
DatePickerContent,
|
||||
DatePickerYearSelect,
|
||||
DatePickerMonthSelect,
|
||||
DatePickerView,
|
||||
DatePickerViewControl,
|
||||
DatePickerPrevTrigger,
|
||||
DatePickerNextTrigger,
|
||||
DatePickerRangeText,
|
||||
DatePickerTable,
|
||||
DatePickerTableHead,
|
||||
DatePickerTableRow,
|
||||
DatePickerTableHeader,
|
||||
DatePickerTableBody,
|
||||
DatePickerTableCell,
|
||||
DatePickerTableCellTrigger,
|
||||
DatePickerContext,
|
||||
} from '@/components/ui/datepicker'
|
||||
import {
|
||||
PaginationContext,
|
||||
PaginationRoot,
|
||||
PaginationItem,
|
||||
PaginationPrevTrigger,
|
||||
PaginationNextTrigger,
|
||||
PaginationEllipsis,
|
||||
} from '@/components/ui/pagination'
|
||||
import { Map } from '@/components/ui/map'
|
||||
import { Lucide } from '@/components/ui/lucide'
|
||||
import { Line1, Line2, Pie1, Donut1, Donut2 } from '@/components/chart-presets'
|
||||
|
||||
const pc = ref(false)
|
||||
const electronic = ref(false)
|
||||
const smartphone = ref(false)
|
||||
const photography = ref(false)
|
||||
const sport = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- BEGIN: Official Stores -->
|
||||
<div class="block h-10 items-center sm:flex">
|
||||
<h2 class="me-5 truncate text-lg font-medium">Official Store</h2>
|
||||
<div class="relative mt-3 sm:ms-auto sm:mt-0">
|
||||
<Lucide
|
||||
class="[--color:var(--color-foreground)]/50 absolute inset-y-0 start-0 z-10 my-auto ms-3"
|
||||
icon="MapPin"
|
||||
/>
|
||||
<Input class="box pl-10 sm:w-56" type="text" placeholder="Filter by city" />
|
||||
</div>
|
||||
</div>
|
||||
<Box class="mt-12 p-5 sm:mt-5">
|
||||
<div>250 Official stores in 21 countries, click the marker to see location details.</div>
|
||||
<Map
|
||||
class="mt-5 w-full h-[327px]"
|
||||
:center="[13.388, 52.517]"
|
||||
:zoom="14"
|
||||
:markers="{
|
||||
type: 'FeatureCollection',
|
||||
features: [
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Cafe Berlin', type: 'cafe' },
|
||||
geometry: { type: 'Point', coordinates: [13.388, 52.517] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Restaurant Alex', type: 'restaurant' },
|
||||
geometry: { type: 'Point', coordinates: [13.39, 52.518] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Coffee House', type: 'cafe' },
|
||||
geometry: { type: 'Point', coordinates: [13.385, 52.515] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Pizza Place', type: 'restaurant' },
|
||||
geometry: { type: 'Point', coordinates: [13.392, 52.519] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Burger Joint', type: 'restaurant' },
|
||||
geometry: { type: 'Point', coordinates: [13.387, 52.516] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Starbucks', type: 'cafe' },
|
||||
geometry: { type: 'Point', coordinates: [13.391, 52.52] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Sushi Bar', type: 'restaurant' },
|
||||
geometry: { type: 'Point', coordinates: [13.395, 52.522] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Bakery', type: 'cafe' },
|
||||
geometry: { type: 'Point', coordinates: [13.383, 52.514] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Italian Restaurant', type: 'restaurant' },
|
||||
geometry: { type: 'Point', coordinates: [13.398, 52.525] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Tea House', type: 'cafe' },
|
||||
geometry: { type: 'Point', coordinates: [13.38, 52.512] },
|
||||
},
|
||||
],
|
||||
}"
|
||||
/>
|
||||
</Box>
|
||||
<!-- END: Official Stores -->
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as OfficialStores } from './OfficialStores.vue'
|
||||
@@ -0,0 +1,142 @@
|
||||
<script setup lang="ts">
|
||||
import { Lucide } from '@/components/ui/lucide'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { DialogRoot, DialogContent } from '@/components/ui/dialog'
|
||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import fakers from '@/utils/faker'
|
||||
|
||||
const { open } = defineProps<{
|
||||
open?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:open', value: boolean): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot :open="open" @openChange="(details) => emit('update:open', details.open)">
|
||||
<DialogContent class="sm:max-w-2xl p-0">
|
||||
<div class="relative border-b border-dashed border-foreground/15">
|
||||
<Lucide class="absolute inset-y-0 my-auto ml-4 opacity-70 z-50" icon="Search" />
|
||||
<Input
|
||||
class="w-full pl-12 bg-transparent border-none pr-19 h-14 focus:ring-offset-transparent focus-visible:ring-transparent shadow-none"
|
||||
type="text"
|
||||
placeholder="Quick search..."
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-y-0 right-0 flex items-center h-6 px-2 my-auto mr-4 border rounded-lg border-foreground/30 text-xs opacity-70"
|
||||
>
|
||||
esc
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 px-5 mt-4">
|
||||
<a
|
||||
class="hover:bg-foreground/5 flex items-center gap-x-1.5 rounded-full border border-foreground/15 px-3 py-0.5 text-xs font-medium"
|
||||
href=""
|
||||
>
|
||||
<Lucide class="size-4" icon="Users2" />
|
||||
Users
|
||||
</a>
|
||||
<a
|
||||
class="hover:bg-foreground/5 flex items-center gap-x-1.5 rounded-full border border-foreground/15 px-3 py-0.5 text-xs font-medium"
|
||||
href=""
|
||||
>
|
||||
<Lucide class="size-4" icon="Building2" />
|
||||
Departments
|
||||
</a>
|
||||
<a
|
||||
class="hover:bg-foreground/5 flex items-center gap-x-1.5 rounded-full border border-foreground/15 px-3 py-0.5 text-xs font-medium"
|
||||
href=""
|
||||
>
|
||||
<Lucide class="size-4" icon="KanbanSquare" />
|
||||
Products
|
||||
</a>
|
||||
<a
|
||||
class="hover:bg-foreground/5 flex items-center gap-x-1.5 rounded-full border border-foreground/15 px-3 py-0.5 text-xs font-medium"
|
||||
href=""
|
||||
>
|
||||
<Lucide class="size-4" icon="MailCheck" />
|
||||
Mails
|
||||
</a>
|
||||
</div>
|
||||
<div class="px-5 py-4 mt-2">
|
||||
<div class="flex items-center">
|
||||
<div class="text-xs uppercase opacity-70">Users</div>
|
||||
<a class="ml-auto text-xs opacity-70" href=""> See All </a>
|
||||
</div>
|
||||
<div class="mt-3.5 flex flex-col gap-1">
|
||||
<template v-for="(faker, fakerKey) in fakers.slice(0, 3)" :key="fakerKey">
|
||||
<a
|
||||
class="hover:border-foreground/10 hover:bg-foreground/5 -mx-1 flex items-center gap-2.5 rounded-xl border border-transparent p-1"
|
||||
href=""
|
||||
>
|
||||
<AvatarRoot class="size-8">
|
||||
<AvatarFallback>{{ faker.users[0]?.name.charAt(0) }}</AvatarFallback>
|
||||
<AvatarImage :src="faker.photos[2]" />
|
||||
</AvatarRoot>
|
||||
<div class="font-medium truncate text-sm">
|
||||
{{ faker.users[0]?.name }}
|
||||
</div>
|
||||
<div class="hidden opacity-70 sm:block text-xs">
|
||||
{{ faker.jobs[0] }}
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-5 py-4 border-t border-dashed border-foreground/15">
|
||||
<div class="flex items-center">
|
||||
<div class="text-xs uppercase opacity-70">Departments</div>
|
||||
<a class="ml-auto text-xs opacity-70" href=""> See All </a>
|
||||
</div>
|
||||
<div class="mt-3.5 flex flex-col gap-1">
|
||||
<template v-for="(faker, fakerKey) in fakers.slice(0, 3)" :key="fakerKey">
|
||||
<a
|
||||
class="hover:border-foreground/10 hover:bg-foreground/5 -mx-1 flex items-center gap-2.5 rounded-xl border border-transparent p-1"
|
||||
href=""
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center overflow-hidden border rounded-lg border-primary/10 bg-primary/10 text-primary size-8"
|
||||
>
|
||||
<Lucide v-if="fakerKey % 2 === 0" class="stroke-1" icon="Store" />
|
||||
<Lucide v-else class="stroke-1" icon="Hotel" />
|
||||
</div>
|
||||
<div class="font-medium truncate text-sm">
|
||||
{{ faker.products[0]?.name }}
|
||||
</div>
|
||||
<div class="hidden opacity-70 sm:block text-xs">
|
||||
{{ faker.products[0]?.category }}
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-5 py-4 border-t border-dashed border-foreground/15 pb-6">
|
||||
<div class="flex items-center">
|
||||
<div class="text-xs uppercase opacity-70">Products</div>
|
||||
<a class="ml-auto text-xs opacity-70" href=""> See All </a>
|
||||
</div>
|
||||
<div class="mt-3.5 flex flex-col gap-1">
|
||||
<template v-for="(faker, fakerKey) in fakers.slice(0, 3)" :key="fakerKey">
|
||||
<a
|
||||
class="hover:border-foreground/10 hover:bg-foreground/5 -mx-1 flex items-center gap-2.5 rounded-xl border border-transparent p-1"
|
||||
href=""
|
||||
>
|
||||
<AvatarRoot class="size-8 rounded-lg overflow-hidden">
|
||||
<AvatarFallback>{{ faker.foods[0]?.name.charAt(0) }}</AvatarFallback>
|
||||
<AvatarImage :src="faker.images[2]" />
|
||||
</AvatarRoot>
|
||||
<div class="font-medium truncate text-sm">
|
||||
{{ faker.foods[0]?.name }}
|
||||
</div>
|
||||
<div class="hidden opacity-70 sm:block text-xs">
|
||||
{{ faker.products[1]?.category }}
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogRoot>
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as QuickSearchDialog } from './QuickSearchDialog.vue'
|
||||
@@ -0,0 +1,191 @@
|
||||
<script lang="ts" setup>
|
||||
import _ from 'lodash'
|
||||
import { ref } from 'vue'
|
||||
import fakers from '@/utils/faker'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Box } from '@/components/ui/box'
|
||||
import {
|
||||
MenuRoot,
|
||||
MenuTrigger,
|
||||
MenuPositioner,
|
||||
MenuContent,
|
||||
MenuCheckboxItem,
|
||||
} from '@/components/ui/menu'
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
} from '@/components/ui/table'
|
||||
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
|
||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import {
|
||||
CarouselRoot,
|
||||
CarouselPrevTrigger,
|
||||
CarouselNextTrigger,
|
||||
CarouselItemGroup,
|
||||
CarouselItem,
|
||||
} from '@/components/ui/carousel'
|
||||
import {
|
||||
DatePickerRoot,
|
||||
DatePickerControl,
|
||||
DatePickerInput,
|
||||
DatePickerTrigger,
|
||||
DatePickerPositioner,
|
||||
DatePickerContent,
|
||||
DatePickerYearSelect,
|
||||
DatePickerMonthSelect,
|
||||
DatePickerView,
|
||||
DatePickerViewControl,
|
||||
DatePickerPrevTrigger,
|
||||
DatePickerNextTrigger,
|
||||
DatePickerRangeText,
|
||||
DatePickerTable,
|
||||
DatePickerTableHead,
|
||||
DatePickerTableRow,
|
||||
DatePickerTableHeader,
|
||||
DatePickerTableBody,
|
||||
DatePickerTableCell,
|
||||
DatePickerTableCellTrigger,
|
||||
DatePickerContext,
|
||||
} from '@/components/ui/datepicker'
|
||||
import {
|
||||
PaginationContext,
|
||||
PaginationRoot,
|
||||
PaginationItem,
|
||||
PaginationPrevTrigger,
|
||||
PaginationNextTrigger,
|
||||
PaginationEllipsis,
|
||||
} from '@/components/ui/pagination'
|
||||
import { Map } from '@/components/ui/map'
|
||||
import { Lucide } from '@/components/ui/lucide'
|
||||
import { Line1, Line2, Pie1, Donut1, Donut2 } from '@/components/chart-presets'
|
||||
|
||||
const pc = ref(false)
|
||||
const electronic = ref(false)
|
||||
const smartphone = ref(false)
|
||||
const photography = ref(false)
|
||||
const sport = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- BEGIN: Recent Activities -->
|
||||
<div class="flex h-10 items-center">
|
||||
<h2 class="me-5 truncate text-lg font-medium">Recent Activities</h2>
|
||||
<a class="text-primary ms-auto truncate" href=""> Show More </a>
|
||||
</div>
|
||||
<div
|
||||
class="before:bg-foreground/10 relative mt-5 before:absolute before:ms-5 before:mt-5 before:block before:h-[85%] before:w-px"
|
||||
>
|
||||
<div class="relative mb-3 flex items-center">
|
||||
<div
|
||||
class="before:bg-foreground/10 before:absolute before:ms-5 before:mt-5 before:block before:h-px before:w-20"
|
||||
>
|
||||
<AvatarRoot class="size-11">
|
||||
<AvatarFallback>PA</AvatarFallback>
|
||||
<AvatarImage :src="fakers[9]!['photos'][0]" alt="avatar" />
|
||||
</AvatarRoot>
|
||||
</div>
|
||||
<Box class="z-10 ms-4 flex-1 px-5 py-3 before:hidden">
|
||||
<div class="flex items-center">
|
||||
<div class="font-medium">
|
||||
{{ fakers[9]!['users'][0]!['name'] }}
|
||||
</div>
|
||||
<div class="ms-auto text-xs opacity-70">07:00 PM</div>
|
||||
</div>
|
||||
<div class="mt-1 opacity-70">Has joined the team</div>
|
||||
</Box>
|
||||
</div>
|
||||
<div class="relative mb-3 flex items-center">
|
||||
<div
|
||||
class="before:bg-foreground/10 before:absolute before:ms-5 before:mt-5 before:block before:h-px before:w-20"
|
||||
>
|
||||
<AvatarRoot class="size-11">
|
||||
<AvatarFallback>PA</AvatarFallback>
|
||||
<AvatarImage :src="fakers[8]!['photos'][0]" alt="avatar" />
|
||||
</AvatarRoot>
|
||||
</div>
|
||||
<Box class="z-10 ms-4 flex-1 px-5 py-3 before:hidden">
|
||||
<div class="flex items-center">
|
||||
<div class="font-medium">
|
||||
{{ fakers[8]!['users'][0]!['name'] }}
|
||||
</div>
|
||||
<div class="ms-auto text-xs opacity-70">07:00 PM</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mt-1 opacity-70">Added 3 new photos</div>
|
||||
<div class="mt-2 flex gap-2">
|
||||
<AvatarRoot class="size-8 rounded-lg border-none ring-0">
|
||||
<AvatarFallback>PA</AvatarFallback>
|
||||
<AvatarImage :src="fakers[6]!['photos'][0]" alt="avatar" />
|
||||
</AvatarRoot>
|
||||
<AvatarRoot class="size-8 rounded-lg border-none ring-0">
|
||||
<AvatarFallback>PA</AvatarFallback>
|
||||
<AvatarImage :src="fakers[6]!['photos'][1]" alt="avatar" />
|
||||
</AvatarRoot>
|
||||
<AvatarRoot class="size-8 rounded-lg border-none ring-0">
|
||||
<AvatarFallback>PA</AvatarFallback>
|
||||
<AvatarImage :src="fakers[6]!['photos'][2]" alt="avatar" />
|
||||
</AvatarRoot>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</div>
|
||||
<div class="my-4 text-center text-xs opacity-70">12 November</div>
|
||||
<div class="relative mb-3 flex items-center">
|
||||
<div
|
||||
class="before:bg-foreground/10 before:absolute before:ms-5 before:mt-5 before:block before:h-px before:w-20"
|
||||
>
|
||||
<AvatarRoot class="size-11">
|
||||
<AvatarFallback>PA</AvatarFallback>
|
||||
<AvatarImage :src="fakers[7]!['photos'][0]" alt="avatar" />
|
||||
</AvatarRoot>
|
||||
</div>
|
||||
<Box class="z-10 ms-4 flex-1 px-5 py-3 before:hidden">
|
||||
<div class="flex items-center">
|
||||
<div class="font-medium">
|
||||
{{ fakers[7]!['users'][0]!['name'] }}
|
||||
</div>
|
||||
<div class="ms-auto text-xs opacity-70">07:00 PM</div>
|
||||
</div>
|
||||
<div class="mt-1 opacity-70">
|
||||
Has changed
|
||||
<a class="text-primary" href="">
|
||||
{{ fakers[7]!['products'][0]!['name'] }}
|
||||
</a>
|
||||
price and description
|
||||
</div>
|
||||
</Box>
|
||||
</div>
|
||||
<div class="relative mb-3 flex items-center">
|
||||
<div
|
||||
class="before:bg-foreground/10 before:absolute before:ms-5 before:mt-5 before:block before:h-px before:w-20"
|
||||
>
|
||||
<AvatarRoot class="size-11">
|
||||
<AvatarFallback>PA</AvatarFallback>
|
||||
<AvatarImage :src="fakers[6]!['photos'][0]" alt="avatar" />
|
||||
</AvatarRoot>
|
||||
</div>
|
||||
<Box class="z-10 ms-4 flex-1 px-5 py-3 before:hidden">
|
||||
<div class="flex items-center">
|
||||
<div class="font-medium">
|
||||
{{ fakers[6]!['users'][0]!['name'] }}
|
||||
</div>
|
||||
<div class="ms-auto text-xs opacity-70">07:00 PM</div>
|
||||
</div>
|
||||
<div class="mt-1 opacity-70">
|
||||
Has changed
|
||||
<a class="text-primary" href="">
|
||||
{{ fakers[6]!['products'][0]!['name'] }}
|
||||
</a>
|
||||
description
|
||||
</div>
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END: Recent Activities -->
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as RecentActivities } from './RecentActivities.vue'
|
||||
@@ -0,0 +1,167 @@
|
||||
<script lang="ts" setup>
|
||||
import _ from 'lodash'
|
||||
import { ref } from 'vue'
|
||||
import fakers from '@/utils/faker'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Box } from '@/components/ui/box'
|
||||
import {
|
||||
MenuRoot,
|
||||
MenuTrigger,
|
||||
MenuPositioner,
|
||||
MenuContent,
|
||||
MenuCheckboxItem,
|
||||
} from '@/components/ui/menu'
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
} from '@/components/ui/table'
|
||||
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
|
||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import {
|
||||
CarouselRoot,
|
||||
CarouselPrevTrigger,
|
||||
CarouselNextTrigger,
|
||||
CarouselItemGroup,
|
||||
CarouselItem,
|
||||
} from '@/components/ui/carousel'
|
||||
import {
|
||||
DatePickerRoot,
|
||||
DatePickerControl,
|
||||
DatePickerInput,
|
||||
DatePickerTrigger,
|
||||
DatePickerPositioner,
|
||||
DatePickerContent,
|
||||
DatePickerYearSelect,
|
||||
DatePickerMonthSelect,
|
||||
DatePickerView,
|
||||
DatePickerViewControl,
|
||||
DatePickerPrevTrigger,
|
||||
DatePickerNextTrigger,
|
||||
DatePickerRangeText,
|
||||
DatePickerTable,
|
||||
DatePickerTableHead,
|
||||
DatePickerTableRow,
|
||||
DatePickerTableHeader,
|
||||
DatePickerTableBody,
|
||||
DatePickerTableCell,
|
||||
DatePickerTableCellTrigger,
|
||||
DatePickerContext,
|
||||
} from '@/components/ui/datepicker'
|
||||
import {
|
||||
PaginationContext,
|
||||
PaginationRoot,
|
||||
PaginationItem,
|
||||
PaginationPrevTrigger,
|
||||
PaginationNextTrigger,
|
||||
PaginationEllipsis,
|
||||
} from '@/components/ui/pagination'
|
||||
import { Map } from '@/components/ui/map'
|
||||
import { Lucide } from '@/components/ui/lucide'
|
||||
import { Line1, Line2, Pie1, Donut1, Donut2 } from '@/components/chart-presets'
|
||||
|
||||
const pc = ref(false)
|
||||
const electronic = ref(false)
|
||||
const smartphone = ref(false)
|
||||
const photography = ref(false)
|
||||
const sport = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- BEGIN: Schedules -->
|
||||
<div class="flex h-10 items-center">
|
||||
<h2 class="me-5 truncate text-lg font-medium">Schedules</h2>
|
||||
<a class="text-primary ms-auto flex items-center truncate" href="">
|
||||
<Lucide class="me-1" icon="Plus" /> Add New Schedules
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<Box>
|
||||
<div class="flex">
|
||||
<Lucide class="size-5 opacity-70" icon="ChevronLeft" />
|
||||
<div class="mx-auto text-base font-medium">April</div>
|
||||
<Lucide class="size-5 opacity-70" icon="ChevronRight" />
|
||||
</div>
|
||||
<div class="mt-5 grid grid-cols-7 gap-4 text-center [&>span]:text-sm [&>span]:shadow-none">
|
||||
<div class="font-medium">Su</div>
|
||||
<div class="font-medium">Mo</div>
|
||||
<div class="font-medium">Tu</div>
|
||||
<div class="font-medium">We</div>
|
||||
<div class="font-medium">Th</div>
|
||||
<div class="font-medium">Fr</div>
|
||||
<div class="font-medium">Sa</div>
|
||||
<Badge variant="ghost" class="opacity-70">29</Badge>
|
||||
<Badge variant="ghost" class="opacity-70">30</Badge>
|
||||
<Badge variant="ghost" class="opacity-70">31</Badge>
|
||||
<Badge variant="ghost">1</Badge>
|
||||
<Badge variant="ghost">2</Badge>
|
||||
<Badge variant="ghost">3</Badge>
|
||||
<Badge variant="ghost">4</Badge>
|
||||
<Badge variant="ghost">5</Badge>
|
||||
<Badge variant="success" look="outline">6</Badge>
|
||||
<Badge variant="ghost">7</Badge>
|
||||
<Badge variant="primary" look="outline">8</Badge>
|
||||
<Badge variant="ghost">9</Badge>
|
||||
<Badge variant="ghost">10</Badge>
|
||||
<Badge variant="ghost">11</Badge>
|
||||
<Badge variant="ghost">12</Badge>
|
||||
<Badge variant="ghost">13</Badge>
|
||||
<Badge variant="ghost">14</Badge>
|
||||
<Badge variant="ghost">15</Badge>
|
||||
<Badge variant="ghost">16</Badge>
|
||||
<Badge variant="ghost">17</Badge>
|
||||
<Badge variant="ghost">18</Badge>
|
||||
<Badge variant="ghost">19</Badge>
|
||||
<Badge variant="ghost">20</Badge>
|
||||
<Badge variant="ghost">21</Badge>
|
||||
<Badge variant="ghost">22</Badge>
|
||||
<Badge variant="pending" look="outline">23</Badge>
|
||||
<Badge variant="ghost">24</Badge>
|
||||
<Badge variant="ghost">25</Badge>
|
||||
<Badge variant="ghost">26</Badge>
|
||||
<Badge variant="warning" look="outline">27</Badge>
|
||||
<Badge variant="ghost">28</Badge>
|
||||
<Badge variant="ghost">29</Badge>
|
||||
<Badge variant="ghost">30</Badge>
|
||||
<Badge variant="ghost" class="opacity-70">1</Badge>
|
||||
<Badge variant="ghost" class="opacity-70">2</Badge>
|
||||
<Badge variant="ghost" class="opacity-70">3</Badge>
|
||||
<Badge variant="ghost" class="opacity-70">4</Badge>
|
||||
<Badge variant="ghost" class="opacity-70">5</Badge>
|
||||
<Badge variant="ghost" class="opacity-70">6</Badge>
|
||||
<Badge variant="ghost" class="opacity-70">7</Badge>
|
||||
<Badge variant="ghost" class="opacity-70">8</Badge>
|
||||
<Badge variant="ghost" class="opacity-70">9</Badge>
|
||||
</div>
|
||||
<div class="mt-5 border-t border-foreground/10 pt-5">
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="bg-(--color)/20 border-(--color)/60 me-3 size-2 rounded-full border [--color:var(--color-pending)]"
|
||||
></div>
|
||||
<span class="truncate">UI/UX Workshop</span>
|
||||
<span class="font-medium xl:ms-auto">23th</span>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center">
|
||||
<div
|
||||
class="bg-(--color)/20 border-(--color)/60 me-3 size-2 rounded-full border [--color:var(--color-primary)]"
|
||||
></div>
|
||||
<span class="truncate"> VueJs Frontend Development </span>
|
||||
<span class="font-medium xl:ms-auto">10th</span>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center">
|
||||
<div
|
||||
class="bg-(--color)/20 border-(--color)/60 me-3 size-2 rounded-full border [--color:var(--color-warning)]"
|
||||
></div>
|
||||
<span class="truncate">Laravel Rest API</span>
|
||||
<span class="font-medium xl:ms-auto">31th</span>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</div>
|
||||
<!-- END: Schedules -->
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Schedules } from './Schedules.vue'
|
||||
@@ -0,0 +1,245 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Lucide, type Icon } from '@/components/ui/lucide'
|
||||
import { slideUp, slideDown } from '@/utils/helper'
|
||||
import { cn } from '@/utils/cn'
|
||||
import _ from 'lodash'
|
||||
|
||||
interface Menu {
|
||||
icon?: Icon
|
||||
title?: string
|
||||
route_name?: string
|
||||
params?: any
|
||||
badge?: number
|
||||
sub_menu?: Menu[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
menu: (string | Menu)[]
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const formattedMenu = ref<(string | Menu)[]>([])
|
||||
|
||||
// State to track open menus based on unique index/path
|
||||
const openMenus = ref<Record<string, boolean>>({})
|
||||
|
||||
const isMenuActive = (menu: string | Menu): boolean => {
|
||||
if (typeof menu === 'string') return false
|
||||
if (menu.route_name) {
|
||||
if (menu.route_name === '/') {
|
||||
return route.path === '/'
|
||||
}
|
||||
return route.name === menu.route_name || route.path.includes(menu.route_name)
|
||||
}
|
||||
if (menu.sub_menu) {
|
||||
return menu.sub_menu.some((sub) => isMenuActive(sub))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const findActiveSubMenus = (subMenu: Menu[]): boolean => {
|
||||
return subMenu.some((item) => {
|
||||
if (item.route_name) {
|
||||
if (item.route_name === '/') {
|
||||
return route.path === '/'
|
||||
}
|
||||
return route.name === item.route_name || route.path.includes(item.route_name)
|
||||
}
|
||||
if (item.sub_menu) return findActiveSubMenus(item.sub_menu)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
const initMenu = (menuList: (string | Menu)[], parentKey = 'menu') => {
|
||||
const newOpenMenus: Record<string, boolean> = {}
|
||||
|
||||
const traverse = (list: (string | Menu)[], pk: string) => {
|
||||
list.forEach((item, index) => {
|
||||
const key = `${pk}-${index}`
|
||||
if (typeof item !== 'string' && item.sub_menu) {
|
||||
if (findActiveSubMenus(item.sub_menu)) {
|
||||
newOpenMenus[key] = true
|
||||
}
|
||||
traverse(item.sub_menu, key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
traverse(menuList, parentKey)
|
||||
openMenus.value = { ...openMenus.value, ...newOpenMenus }
|
||||
}
|
||||
|
||||
watch(
|
||||
[() => props.menu, () => route.name],
|
||||
() => {
|
||||
formattedMenu.value = props.menu
|
||||
initMenu(props.menu)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const toggleMenu = (key: string, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
|
||||
const el = (event.currentTarget as HTMLElement).nextElementSibling as HTMLElement
|
||||
if (el) {
|
||||
if (openMenus.value[key]) {
|
||||
slideUp(el, 300)
|
||||
openMenus.value[key] = false
|
||||
} else {
|
||||
slideDown(el, 300)
|
||||
openMenus.value[key] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const linkTo = (menu: Menu, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
if (menu.route_name) {
|
||||
router.push({
|
||||
name: menu.route_name,
|
||||
params: menu.params,
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul class="scrollable">
|
||||
<template v-for="(menu, menuKey) in formattedMenu" :key="menuKey">
|
||||
<!-- BEGIN: First Child -->
|
||||
<li v-if="typeof menu === 'string'" class="side-menu__group-label">
|
||||
{{ menu }}
|
||||
</li>
|
||||
<li v-else>
|
||||
<a
|
||||
href=""
|
||||
@click="
|
||||
(event) => {
|
||||
if (menu.sub_menu) {
|
||||
toggleMenu(`menu-${menuKey}`, event)
|
||||
} else {
|
||||
linkTo(menu, event)
|
||||
}
|
||||
}
|
||||
"
|
||||
:class="
|
||||
cn(
|
||||
'side-menu__link',
|
||||
isMenuActive(menu) ? 'side-menu__link--active' : '',
|
||||
)
|
||||
"
|
||||
>
|
||||
<Lucide
|
||||
v-if="menu.icon"
|
||||
class="side-menu__link__icon"
|
||||
:icon="menu.icon"
|
||||
/>
|
||||
<div class="side-menu__link__title">{{ menu.title }}</div>
|
||||
<div v-if="menu.badge" class="side-menu__link__badge">
|
||||
{{ menu.badge }}
|
||||
</div>
|
||||
<Lucide
|
||||
v-if="menu.sub_menu"
|
||||
:class="
|
||||
cn(
|
||||
'side-menu__link__chevron transition-transform duration-300',
|
||||
openMenus[`menu-${menuKey}`] ? 'rotate-180' : '',
|
||||
)
|
||||
"
|
||||
icon="ChevronDown"
|
||||
/>
|
||||
</a>
|
||||
<!-- BEGIN: Second Child -->
|
||||
<ul
|
||||
v-if="menu.sub_menu"
|
||||
:class="cn('hidden', { block: openMenus[`menu-${menuKey}`] })"
|
||||
>
|
||||
<li v-for="(subMenu, subMenuKey) in menu.sub_menu" :key="subMenuKey">
|
||||
<a
|
||||
href=""
|
||||
@click="
|
||||
(event) => {
|
||||
if (subMenu.sub_menu) {
|
||||
toggleMenu(`menu-${menuKey}-${subMenuKey}`, event)
|
||||
} else {
|
||||
linkTo(subMenu, event)
|
||||
}
|
||||
}
|
||||
"
|
||||
:class="
|
||||
cn(
|
||||
'side-menu__link',
|
||||
isMenuActive(subMenu) ? 'side-menu__link--active' : '',
|
||||
)
|
||||
"
|
||||
>
|
||||
<Lucide
|
||||
v-if="subMenu.icon"
|
||||
class="side-menu__link__icon"
|
||||
:icon="subMenu.icon"
|
||||
/>
|
||||
<div class="side-menu__link__title">
|
||||
{{ subMenu.title }}
|
||||
</div>
|
||||
<div v-if="subMenu.badge" class="side-menu__link__badge">
|
||||
{{ subMenu.badge }}
|
||||
</div>
|
||||
<Lucide
|
||||
v-if="subMenu.sub_menu"
|
||||
:class="
|
||||
cn(
|
||||
'side-menu__link__chevron transition-transform duration-300',
|
||||
openMenus[`menu-${menuKey}-${subMenuKey}`] ? 'rotate-180' : '',
|
||||
)
|
||||
"
|
||||
icon="ChevronDown"
|
||||
/>
|
||||
</a>
|
||||
<!-- BEGIN: Third Child -->
|
||||
<ul
|
||||
v-if="subMenu.sub_menu"
|
||||
:class="
|
||||
cn('hidden', { block: openMenus[`menu-${menuKey}-${subMenuKey}`] })
|
||||
"
|
||||
>
|
||||
<li
|
||||
v-for="(lastSubMenu, lastSubMenuKey) in subMenu.sub_menu"
|
||||
:key="lastSubMenuKey"
|
||||
>
|
||||
<a
|
||||
href=""
|
||||
@click.prevent="linkTo(lastSubMenu, $event)"
|
||||
:class="
|
||||
cn(
|
||||
'side-menu__link',
|
||||
isMenuActive(lastSubMenu) ? 'side-menu__link--active' : '',
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="side-menu__link__icon">
|
||||
<Lucide v-if="lastSubMenu.icon" :icon="lastSubMenu.icon" />
|
||||
</div>
|
||||
<div class="side-menu__link__title">
|
||||
{{ lastSubMenu.title }}
|
||||
</div>
|
||||
<div v-if="lastSubMenu.badge" class="side-menu__link__badge">
|
||||
{{ lastSubMenu.badge }}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- END: Third Child -->
|
||||
</li>
|
||||
</ul>
|
||||
<!-- END: Second Child -->
|
||||
</li>
|
||||
<!-- END: First Child -->
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default as SideMenu } from './SideMenu.vue'
|
||||
@@ -0,0 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
import { SheetRoot, SheetContent } from '@/components/ui/sheet'
|
||||
import { Lucide } from '@/components/ui/lucide'
|
||||
import { useColorSchemeStore, type ColorSchemes } from '@/stores/color-scheme'
|
||||
import { useDarkModeStore } from '@/stores/dark-mode'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const themeSwitcherSheet = ref(false)
|
||||
const setThemeSwitcherSheet = (value: boolean) => {
|
||||
themeSwitcherSheet.value = value
|
||||
}
|
||||
|
||||
const setColorSchemeClass = () => {
|
||||
const el = document.querySelectorAll('html')[0]
|
||||
el?.setAttribute('data-theme', useColorSchemeStore().colorSchemeValue)
|
||||
if (useDarkModeStore().darkModeValue) el?.classList.add('dark')
|
||||
}
|
||||
const colorSchemeStore = useColorSchemeStore()
|
||||
const switchColorScheme = (colorScheme: ColorSchemes) => {
|
||||
useColorSchemeStore().setColorScheme(colorScheme)
|
||||
setColorSchemeClass()
|
||||
setThemeSwitcherSheet(false)
|
||||
}
|
||||
setColorSchemeClass()
|
||||
|
||||
const setDarkModeClass = () => {
|
||||
const el = document.querySelectorAll('html')[0]
|
||||
useDarkModeStore().darkModeValue ? el?.classList.add('dark') : el?.classList.remove('dark')
|
||||
}
|
||||
const darkModeStore = useDarkModeStore()
|
||||
const switchDarkMode = (darkMode: boolean) => {
|
||||
useDarkModeStore().setDarkMode(darkMode)
|
||||
setDarkModeClass()
|
||||
setThemeSwitcherSheet(false)
|
||||
}
|
||||
setDarkModeClass()
|
||||
|
||||
const colorSchemes: Array<ColorSchemes> = ['default', '1', '2', '3', '4', '5']
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<SheetRoot :open="themeSwitcherSheet" @openChange="(details) => setThemeSwitcherSheet(details.open)">
|
||||
<SheetContent>
|
||||
<a class="absolute inset-y-0 left-0 right-auto my-auto ml-[60px] flex h-8 w-8 items-center justify-center rounded-full border border-white/90 bg-white/5 text-white/90 transition-all hover:rotate-180 hover:scale-105 hover:bg-white/10 focus:outline-none sm:ml-[105px] sm:h-14 sm:w-14"
|
||||
@click="
|
||||
(event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
setThemeSwitcherSheet(false)
|
||||
}
|
||||
">
|
||||
<Lucide class="h-3 w-3 stroke-1 sm:h-8 sm:w-8" icon="X" />
|
||||
</a>
|
||||
<div class="absolute inset-0 px-7 py-5 overflow-y-scroll">
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<div class="text-base font-medium">Accent Colors</div>
|
||||
<div class="mt-0.5 opacity-70">Choose your accent color</div>
|
||||
<div class="mt-5 grid grid-cols-6 gap-x-3 gap-y-4">
|
||||
<div v-for="colorScheme in colorSchemes" :key="colorScheme">
|
||||
<a @click="
|
||||
(event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
switchColorScheme(colorScheme)
|
||||
}
|
||||
" :data-theme="colorScheme" :class="[
|
||||
'h-10 cursor-pointer bg-primary block rounded-(--radius) transition-all duration-100 ring-offset-4 ring-offset-background hover:scale-[105%]',
|
||||
colorSchemeStore.colorSchemeValue == colorScheme
|
||||
? 'ring-2 ring-foreground/20'
|
||||
: 'ring-1 ring-foreground/10',
|
||||
]"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-b border-dashed border-foreground/15 my-5"></div>
|
||||
<div>
|
||||
<div class="text-base font-medium">Appearance</div>
|
||||
<div class="mt-0.5 opacity-70">Choose your appearance</div>
|
||||
<div class="mt-5 grid grid-cols-2 gap-x-5 gap-y-4">
|
||||
<div>
|
||||
<a @click="
|
||||
(event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
switchDarkMode(false)
|
||||
}
|
||||
" :class="[
|
||||
'h-12 cursor-pointer bg-background block rounded-(--radius) transition-all duration-100 ring-offset-4 ring-offset-background hover:scale-[105%]',
|
||||
!darkModeStore.darkModeValue
|
||||
? 'ring-2 ring-foreground/20'
|
||||
: 'ring-1 ring-foreground/10',
|
||||
]">
|
||||
<div class="h-full overflow-hidden rounded-(--radius) bg-slate-200"></div>
|
||||
</a>
|
||||
<div class="mt-2.5 text-center text-xs capitalize">Light</div>
|
||||
</div>
|
||||
<div>
|
||||
<a @click="
|
||||
(event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
switchDarkMode(true)
|
||||
}
|
||||
" :class="[
|
||||
'h-12 cursor-pointer bg-background block rounded-(--radius) transition-all duration-100 ring-offset-4 ring-offset-background hover:scale-[105%]',
|
||||
darkModeStore.darkModeValue
|
||||
? 'ring-2 ring-foreground/20'
|
||||
: 'ring-1 ring-foreground/10',
|
||||
]">
|
||||
<div class="h-full overflow-hidden rounded-(--radius) bg-slate-900"></div>
|
||||
</a>
|
||||
<div class="mt-2.5 text-center text-xs capitalize">Dark</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</SheetRoot>
|
||||
<div :class="[
|
||||
'fixed inset-y-0 right-0 z-50 my-auto flex hover:w-20 transition-all w-14 h-12 cursor-pointer border-(--color)/50 items-center border justify-center rounded-l-full shadow-lg overflow-hidden bg-background/80 [--color:var(--color-primary)]',
|
||||
'before:bg-(--color)/20 before:absolute before:inset-0',
|
||||
]" @click="
|
||||
(event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
setThemeSwitcherSheet(true)
|
||||
}
|
||||
">
|
||||
<Lucide class="animate-spin" icon="Settings" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ThemeSwitcher } from './ThemeSwitcher.vue'
|
||||
@@ -0,0 +1,166 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Lucide, type Icon } from '@/components/ui/lucide'
|
||||
import { slideUp, slideDown } from '@/utils/helper'
|
||||
|
||||
interface Menu {
|
||||
icon?: Icon
|
||||
title?: string
|
||||
route_name?: string
|
||||
params?: any
|
||||
badge?: number
|
||||
sub_menu?: Menu[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
menu: (string | Menu)[]
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const formattedMenu = ref<(string | Menu)[]>([])
|
||||
|
||||
// State to track open menus based on unique index/path
|
||||
const openMenus = ref<Record<string, boolean>>({})
|
||||
|
||||
watch(
|
||||
() => props.menu,
|
||||
() => {
|
||||
formattedMenu.value = props.menu
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const isMenuActive = (menu: string | Menu): boolean => {
|
||||
if (typeof menu === 'string') return false
|
||||
if (menu.route_name && route.name === menu.route_name) return true
|
||||
if (menu.sub_menu) {
|
||||
return menu.sub_menu.some((sub) => isMenuActive(sub))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const linkTo = (menu: Menu, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
if (menu.route_name) {
|
||||
router.push({
|
||||
name: menu.route_name,
|
||||
params: menu.params,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const toggleMenu = (key: string, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
|
||||
// Get the <ul> element that is a sibling of the clicked <a>
|
||||
const el = (event.currentTarget as HTMLElement).nextElementSibling as HTMLElement
|
||||
if (el) {
|
||||
if (openMenus.value[key]) {
|
||||
slideUp(el, 300)
|
||||
openMenus.value[key] = false
|
||||
} else {
|
||||
slideDown(el, 300)
|
||||
openMenus.value[key] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul class="top-menu">
|
||||
<template v-for="(menu, menuKey) in formattedMenu" :key="menuKey">
|
||||
<li v-if="typeof menu !== 'string'">
|
||||
<a
|
||||
href=""
|
||||
@click="(event) => {
|
||||
if (menu.sub_menu) {
|
||||
toggleMenu(`menu-${menuKey}`, event)
|
||||
} else {
|
||||
linkTo(menu, event)
|
||||
}
|
||||
}"
|
||||
:class="[
|
||||
'top-menu__link',
|
||||
isMenuActive(menu) ? 'top-menu__link--active' : '',
|
||||
]"
|
||||
>
|
||||
<div class="top-menu__link__icon">
|
||||
<Lucide v-if="menu.icon" :icon="menu.icon" />
|
||||
</div>
|
||||
<div class="top-menu__link__title">
|
||||
{{ menu.title }}
|
||||
<Lucide
|
||||
v-if="menu.sub_menu"
|
||||
:class="[
|
||||
'top-menu__link__chevron transition-transform duration-300',
|
||||
openMenus[`menu-${menuKey}`] ? 'rotate-180' : '',
|
||||
]"
|
||||
icon="ChevronDown"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
<!-- BEGIN: Second Child -->
|
||||
<ul v-if="menu.sub_menu" class="hidden">
|
||||
<li v-for="(subMenu, subMenuKey) in menu.sub_menu" :key="subMenuKey">
|
||||
<a
|
||||
href=""
|
||||
@click="(event) => {
|
||||
if (subMenu.sub_menu) {
|
||||
toggleMenu(`menu-${menuKey}-${subMenuKey}`, event)
|
||||
} else {
|
||||
linkTo(subMenu, event)
|
||||
}
|
||||
}"
|
||||
:class="[
|
||||
'top-menu__link',
|
||||
isMenuActive(subMenu) ? 'top-menu__link--active' : '',
|
||||
]"
|
||||
>
|
||||
<div class="top-menu__link__icon">
|
||||
<Lucide v-if="subMenu.icon" :icon="subMenu.icon" />
|
||||
</div>
|
||||
<div class="top-menu__link__title">
|
||||
{{ subMenu.title }}
|
||||
<Lucide
|
||||
v-if="subMenu.sub_menu"
|
||||
:class="[
|
||||
'top-menu__link__chevron transition-transform duration-300',
|
||||
openMenus[`menu-${menuKey}-${subMenuKey}`] ? 'rotate-180' : '',
|
||||
]"
|
||||
icon="ChevronDown"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
<!-- BEGIN: Third Child -->
|
||||
<ul v-if="subMenu.sub_menu" class="hidden">
|
||||
<li
|
||||
v-for="(lastSubMenu, lastSubMenuKey) in subMenu.sub_menu"
|
||||
:key="lastSubMenuKey"
|
||||
>
|
||||
<a
|
||||
href=""
|
||||
@click.prevent="linkTo(lastSubMenu, $event)"
|
||||
:class="[
|
||||
'top-menu__link',
|
||||
isMenuActive(lastSubMenu) ? 'top-menu__link--active' : '',
|
||||
]"
|
||||
>
|
||||
<div class="top-menu__link__icon">
|
||||
<Lucide v-if="lastSubMenu.icon" :icon="lastSubMenu.icon" />
|
||||
</div>
|
||||
<div class="top-menu__link__title">
|
||||
{{ lastSubMenu.title }}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- END: Third Child -->
|
||||
</li>
|
||||
</ul>
|
||||
<!-- END: Second Child -->
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
import TopMenu from "./TopMenu.vue";
|
||||
|
||||
export { TopMenu };
|
||||
@@ -0,0 +1,109 @@
|
||||
<script lang="ts" setup>
|
||||
import _ from 'lodash'
|
||||
import { ref } from 'vue'
|
||||
import fakers from '@/utils/faker'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Box } from '@/components/ui/box'
|
||||
import {
|
||||
MenuRoot,
|
||||
MenuTrigger,
|
||||
MenuPositioner,
|
||||
MenuContent,
|
||||
MenuCheckboxItem,
|
||||
} from '@/components/ui/menu'
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
} from '@/components/ui/table'
|
||||
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
|
||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import {
|
||||
CarouselRoot,
|
||||
CarouselPrevTrigger,
|
||||
CarouselNextTrigger,
|
||||
CarouselItemGroup,
|
||||
CarouselItem,
|
||||
} from '@/components/ui/carousel'
|
||||
import {
|
||||
DatePickerRoot,
|
||||
DatePickerControl,
|
||||
DatePickerInput,
|
||||
DatePickerTrigger,
|
||||
DatePickerPositioner,
|
||||
DatePickerContent,
|
||||
DatePickerYearSelect,
|
||||
DatePickerMonthSelect,
|
||||
DatePickerView,
|
||||
DatePickerViewControl,
|
||||
DatePickerPrevTrigger,
|
||||
DatePickerNextTrigger,
|
||||
DatePickerRangeText,
|
||||
DatePickerTable,
|
||||
DatePickerTableHead,
|
||||
DatePickerTableRow,
|
||||
DatePickerTableHeader,
|
||||
DatePickerTableBody,
|
||||
DatePickerTableCell,
|
||||
DatePickerTableCellTrigger,
|
||||
DatePickerContext,
|
||||
} from '@/components/ui/datepicker'
|
||||
import {
|
||||
PaginationContext,
|
||||
PaginationRoot,
|
||||
PaginationItem,
|
||||
PaginationPrevTrigger,
|
||||
PaginationNextTrigger,
|
||||
PaginationEllipsis,
|
||||
} from '@/components/ui/pagination'
|
||||
import { Map } from '@/components/ui/map'
|
||||
import { Lucide } from '@/components/ui/lucide'
|
||||
import { Line1, Line2, Pie1, Donut1, Donut2 } from '@/components/chart-presets'
|
||||
|
||||
const pc = ref(false)
|
||||
const electronic = ref(false)
|
||||
const smartphone = ref(false)
|
||||
const photography = ref(false)
|
||||
const sport = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- BEGIN: Transactions -->
|
||||
<div class="flex h-10 items-center">
|
||||
<h2 class="me-5 truncate text-lg font-medium">Transactions</h2>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<Box
|
||||
v-for="(faker, fakerKey) in _.take(fakers, 5)"
|
||||
class="mb-3 flex items-center px-5 py-3 before:hidden"
|
||||
>
|
||||
<AvatarRoot class="size-11">
|
||||
<AvatarFallback>PA</AvatarFallback>
|
||||
<AvatarImage :src="faker['photos'][0]" alt="avatar" />
|
||||
</AvatarRoot>
|
||||
<div class="me-auto ms-4">
|
||||
<div class="font-medium">{{ faker['users'][0]!['name'] }}</div>
|
||||
<div class="mt-1 text-xs opacity-70">
|
||||
{{ faker['dates'][0] }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
:class="{
|
||||
'text-success': faker['trueFalse'][0],
|
||||
'text-danger': !faker['trueFalse'][0],
|
||||
}"
|
||||
>
|
||||
{{ faker['trueFalse'][0] ? '+' : '-' }}${{ faker['totals'][0] }}
|
||||
</div>
|
||||
</Box>
|
||||
<Box as-child>
|
||||
<Button variant="ghost" class="box h-12 w-full border-dotted">View More</Button>
|
||||
</Box>
|
||||
</div>
|
||||
<!-- END: Transactions -->
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Transactions } from './Transactions.vue'
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
import { accordionContent } from "@mykopkb/core/styles/accordion.styles";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { inject } from "vue";
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import type { Api, ItemProps } from "@zag-js/accordion";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("accordionApi");
|
||||
const item = inject<ItemProps>("accordionItem");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn([className, accordionContent])" v-bind="{ ...props, ...$attrs, ...api?.getItemContentProps(item!) }">
|
||||
<slot v-if="asChild" />
|
||||
<div v-else>
|
||||
<slot />
|
||||
</div>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import { accordionItemVariants } from "@mykopkb/core/styles/accordion.styles";
|
||||
import {
|
||||
boxVariants,
|
||||
type BoxVariants,
|
||||
} from "@mykopkb/core/styles/box.styles";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { inject, provide } from "vue";
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import type { Api, ItemProps } from "@zag-js/accordion";
|
||||
|
||||
const {
|
||||
raised,
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<
|
||||
BoxVariants & ItemProps & { class?: string; asChild?: boolean }
|
||||
>();
|
||||
|
||||
const variant = inject<"default" | "boxed">("accordionVariant", "default");
|
||||
const api = inject<Api>("accordionApi");
|
||||
|
||||
provide("accordionItem", props);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn([
|
||||
className,
|
||||
variant == 'boxed' ? boxVariants({ raised, className }) : '',
|
||||
accordionItemVariants({ variant, className }),
|
||||
])
|
||||
" v-bind="{ ...props, ...$attrs, ...api?.getItemProps(props) }">
|
||||
<slot v-if="asChild" />
|
||||
<div v-else>
|
||||
<slot />
|
||||
</div>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script lang="ts" setup>
|
||||
import { accordionRootVariants } from "@mykopkb/core/styles/accordion.styles";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { provide, computed } from "vue";
|
||||
import * as accordion from "@zag-js/accordion";
|
||||
import type { Props } from "@zag-js/accordion";
|
||||
import { useMachine, normalizeProps } from "@zag-js/vue";
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
variant = "default",
|
||||
asChild = false,
|
||||
collapsible = true,
|
||||
...props
|
||||
} = defineProps<
|
||||
Partial<Props> & {
|
||||
class?: string;
|
||||
variant?: "default" | "boxed";
|
||||
asChild?: boolean;
|
||||
}
|
||||
>();
|
||||
|
||||
const service = useMachine(accordion.machine, {
|
||||
collapsible,
|
||||
...props,
|
||||
id: crypto.randomUUID(),
|
||||
});
|
||||
|
||||
const api = computed(() => accordion.connect(service, normalizeProps));
|
||||
|
||||
provide("accordionVariant", variant);
|
||||
provide("accordionApi", api);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn([className, accordionRootVariants({ variant, className })])"
|
||||
v-bind="{ ...props, ...$attrs, ...api.getRootProps() }">
|
||||
<slot v-if="asChild" />
|
||||
<div v-else>
|
||||
<slot />
|
||||
</div>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts" setup>
|
||||
import { ChevronDownIcon } from "@lucide/vue";
|
||||
import {
|
||||
accordionTrigger,
|
||||
accordionItemIndicator,
|
||||
} from "@mykopkb/core/styles/accordion.styles";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { inject } from "vue";
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import type { Api, ItemProps } from "@zag-js/accordion";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("accordionApi");
|
||||
const item = inject<ItemProps>("accordionItem");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn([className, accordionTrigger])" v-bind="{ ...props, ...$attrs, ...api?.getItemTriggerProps(item!) }">
|
||||
<slot v-if="asChild" />
|
||||
<button v-else>
|
||||
<slot />
|
||||
<div v-bind="api?.getItemIndicatorProps(item!)" :class="cn(accordionItemIndicator)">
|
||||
<ChevronDownIcon />
|
||||
</div>
|
||||
</button>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as AccordionRoot } from "./AccordionRoot.vue";
|
||||
export { default as AccordionItem } from "./AccordionItem.vue";
|
||||
export { default as AccordionTrigger } from "./AccordionTrigger.vue";
|
||||
export { default as AccordionContent } from "./AccordionContent.vue";
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts" setup>
|
||||
import { X } from "@lucide/vue";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import { alertCloseTrigger } from "@mykopkb/core/styles/alert.styles";
|
||||
import { inject } from "vue";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}>();
|
||||
|
||||
const context = inject<{
|
||||
present: boolean;
|
||||
setPresent: (value: boolean) => void;
|
||||
} | null>("alertPresent", null);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn([className, alertCloseTrigger])" v-bind="{ ...props, ...$attrs }"
|
||||
@click="context?.setPresent(false)">
|
||||
<slot v-if="asChild" />
|
||||
<div v-else>
|
||||
<slot v-if="$slots.default" />
|
||||
<X v-else />
|
||||
</div>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import { alertDescription } from "@mykopkb/core/styles/alert.styles";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn([className, alertDescription])" v-bind="{ ...props, ...$attrs }">
|
||||
<slot v-if="asChild" />
|
||||
<div v-else>
|
||||
<slot />
|
||||
</div>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import {
|
||||
alertRootVariants,
|
||||
type AlertRootVariants,
|
||||
} from "@mykopkb/core/styles/alert.styles";
|
||||
import { Presence } from "@/components/ui/presence";
|
||||
import { ref, provide } from "vue";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
look,
|
||||
variant,
|
||||
...rest
|
||||
} = defineProps<AlertRootVariants & { class?: string }>();
|
||||
|
||||
const present = ref(true);
|
||||
const setPresent = (value: boolean) => {
|
||||
present.value = value;
|
||||
};
|
||||
|
||||
provide("alertPresent", { present, setPresent });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Presence :class="cn(
|
||||
alertRootVariants({
|
||||
look,
|
||||
variant,
|
||||
}),
|
||||
className
|
||||
)
|
||||
" v-bind="rest" :present="present">
|
||||
<slot />
|
||||
</Presence>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import { alertTitle } from "@mykopkb/core/styles/alert.styles";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn([className, alertTitle])" v-bind="{ ...props, ...$attrs }">
|
||||
<slot v-if="asChild" />
|
||||
<div v-else>
|
||||
<slot />
|
||||
</div>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as AlertRoot } from "./AlertRoot.vue";
|
||||
export { default as AlertTitle } from "./AlertTitle.vue";
|
||||
export { default as AlertDescription } from "./AlertDescription.vue";
|
||||
export { default as AlertCloseTrigger } from "./AlertCloseTrigger.vue";
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Api } from "@zag-js/avatar";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { avatarFallback } from "@mykopkb/core/styles/avatar.styles";
|
||||
import { inject } from "vue";
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
asChild?: boolean;
|
||||
class?: string;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("avatarApi");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn(avatarFallback, className)" v-bind="{ ...props, ...$attrs, ...api?.getFallbackProps() }">
|
||||
<slot v-if="asChild" />
|
||||
<span v-else>
|
||||
<slot />
|
||||
</span>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { avatarImage } from "@mykopkb/core/styles/avatar.styles";
|
||||
import { inject } from "vue";
|
||||
import type { Api } from "@zag-js/avatar";
|
||||
|
||||
const { class: className, ...props } = defineProps<{
|
||||
class?: string;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("avatarApi");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img :class="cn(avatarImage, className)" v-bind="{ ...props, ...$attrs, ...api?.getImageProps() }" />
|
||||
</template>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script lang="ts" setup>
|
||||
import * as avatar from "@zag-js/avatar";
|
||||
import { provide, computed } from "vue";
|
||||
import { useMachine, normalizeProps } from "@zag-js/vue";
|
||||
import type { Props } from "@zag-js/avatar";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import {
|
||||
avatarRootVariants,
|
||||
type AvatarRootVariants,
|
||||
} from "@mykopkb/core/styles/avatar.styles";
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
bordered,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<
|
||||
AvatarRootVariants &
|
||||
Partial<Props> & {
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}
|
||||
>();
|
||||
|
||||
const service = useMachine(avatar.machine, {
|
||||
...props,
|
||||
id: crypto.randomUUID(),
|
||||
});
|
||||
|
||||
const api = computed(() => avatar.connect(service, normalizeProps));
|
||||
|
||||
provide("avatarApi", api);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn(
|
||||
avatarRootVariants({
|
||||
bordered,
|
||||
className,
|
||||
}),
|
||||
className
|
||||
)
|
||||
" v-bind="{ ...props, ...$attrs, ...api.getRootProps() }">
|
||||
<slot v-if="asChild" />
|
||||
<div v-else>
|
||||
<slot />
|
||||
</div>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as AvatarRoot } from "./AvatarRoot.vue";
|
||||
export { default as AvatarFallback } from "./AvatarFallback.vue";
|
||||
export { default as AvatarImage } from "./AvatarImage.vue";
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import {
|
||||
TooltipRoot,
|
||||
TooltipTrigger,
|
||||
TooltipPositioner,
|
||||
TooltipContent,
|
||||
} from "@/components/ui/tooltip";
|
||||
import {
|
||||
badgeVariants,
|
||||
type BadgeVariants,
|
||||
} from "@mykopkb/core/styles/badge.styles";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
look,
|
||||
variant,
|
||||
content,
|
||||
...props
|
||||
} = defineProps<
|
||||
BadgeVariants & {
|
||||
class?: string;
|
||||
content?: string;
|
||||
}
|
||||
>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TooltipRoot :disabled="!content">
|
||||
<TooltipTrigger as-child>
|
||||
<span :class="cn(badgeVariants({ look, variant, className }))" v-bind="{ ...props, ...$attrs }">
|
||||
<slot />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipPositioner>
|
||||
<TooltipContent>{{ content }}</TooltipContent>
|
||||
</TooltipPositioner>
|
||||
</TooltipRoot>
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Badge } from "./Badge.vue";
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import {
|
||||
boxVariants,
|
||||
type BoxVariants,
|
||||
} from "@mykopkb/core/styles/box.styles";
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
raised,
|
||||
...props
|
||||
} = defineProps<
|
||||
BoxVariants & {
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}
|
||||
>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn(boxVariants({ raised, className }), className)" v-bind="{ ...props, ...$attrs }">
|
||||
<slot v-if="asChild" />
|
||||
<div v-else>
|
||||
<slot />
|
||||
</div>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Box } from "./Box.vue";
|
||||
@@ -0,0 +1,61 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { ChevronRight, Ellipsis } from "@lucide/vue";
|
||||
import {
|
||||
MenuRoot,
|
||||
MenuTrigger,
|
||||
MenuPositioner,
|
||||
MenuContent,
|
||||
MenuItem,
|
||||
} from "@/components/ui/menu";
|
||||
import { BreadcrumbItem, BreadcrumbLink, BreadcrumbList } from ".";
|
||||
|
||||
const { class: className, ...props } = defineProps<{
|
||||
class?: string;
|
||||
items: string[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav aria-label="breadcrumb" data-slot="breadcrumb" v-bind="{ ...props, ...$attrs }" :class="cn(className)">
|
||||
<BreadcrumbList>
|
||||
<template v-if="items.length <= 3">
|
||||
<template v-for="(item, key) in items">
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink>{{ item }}</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<ChevronRight v-if="key < items.length - 1" />
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink>{{ items[0] }}</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<ChevronRight />
|
||||
<BreadcrumbItem>
|
||||
<MenuRoot>
|
||||
<MenuTrigger asChild>
|
||||
<BreadcrumbLink>
|
||||
<Ellipsis />
|
||||
</BreadcrumbLink>
|
||||
</MenuTrigger>
|
||||
<MenuPositioner>
|
||||
<MenuContent>
|
||||
<MenuItem v-for="(item, itemKey) in items.slice(1, -2)" :value="item" :key="itemKey">
|
||||
{{ item }}
|
||||
</MenuItem>
|
||||
</MenuContent>
|
||||
</MenuPositioner>
|
||||
</MenuRoot>
|
||||
</BreadcrumbItem>
|
||||
<ChevronRight />
|
||||
<template v-for="(item, index) in items.slice(-2)">
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink>{{ item }}</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<ChevronRight v-if="index < 1" />
|
||||
</template>
|
||||
</template>
|
||||
</BreadcrumbList>
|
||||
</nav>
|
||||
</template>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { breadcrumbItem } from "@mykopkb/core/styles/breadcrumb.styles";
|
||||
|
||||
const { class: className, ...props } = defineProps<{
|
||||
class?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li :class="cn(className, breadcrumbItem)" v-bind="{ ...props, ...$attrs }">
|
||||
<slot />
|
||||
</li>
|
||||
</template>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { breadcrumbLink } from "@mykopkb/core/styles/breadcrumb.styles";
|
||||
|
||||
const { class: className, ...props } = defineProps<{
|
||||
class?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a :class="cn(className, breadcrumbLink)" v-bind="{ ...props, ...$attrs }">
|
||||
<slot />
|
||||
</a>
|
||||
</template>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { breadcrumbList } from "@mykopkb/core/styles/breadcrumb.styles";
|
||||
|
||||
const { class: className, ...props } = defineProps<{
|
||||
class?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ol :class="cn(className, breadcrumbList)" v-bind="{ ...props, ...$attrs }">
|
||||
<slot />
|
||||
</ol>
|
||||
</template>
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as Breadcrumb } from "./Breadcrumb.vue";
|
||||
export { default as BreadcrumbItem } from "./BreadcrumbItem.vue";
|
||||
export { default as BreadcrumbLink } from "./BreadcrumbLink.vue";
|
||||
export { default as BreadcrumbList } from "./BreadcrumbList.vue";
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import {
|
||||
buttonVariants,
|
||||
type ButtonVariants,
|
||||
} from "@mykopkb/core/styles/button.styles";
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
look,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<
|
||||
ButtonVariants & {
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}
|
||||
>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot v-bind="{ ...props, ...$attrs }" :class="cn(buttonVariants({ look, variant, size, className }), className)">
|
||||
<slot v-if="asChild" />
|
||||
<button v-else>
|
||||
<slot />
|
||||
</button>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Button } from "./Button.vue";
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts" setup>
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { carouselControl } from "@mykopkb/core/styles/carousel.styles";
|
||||
import type { Api } from "@zag-js/carousel";
|
||||
import { inject } from "vue";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("carouselApi");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn(carouselControl, className)" v-bind="{ ...props, ...$attrs, ...api?.getControlProps() }">
|
||||
<slot v-if="asChild" />
|
||||
<div v-else>
|
||||
<slot />
|
||||
</div>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { carouselIndicator } from "@mykopkb/core/styles/carousel.styles";
|
||||
import type { Api, IndicatorProps } from "@zag-js/carousel";
|
||||
import { inject } from "vue";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
index,
|
||||
...props
|
||||
} = defineProps<
|
||||
{
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
} & IndicatorProps
|
||||
>();
|
||||
|
||||
const api = inject<Api>("carouselApi");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button :class="cn(carouselIndicator, className)"
|
||||
v-bind="{ ...props, ...$attrs, ...api?.getIndicatorProps({ index }) }" />
|
||||
</template>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { carouselIndicatorGroup } from "@mykopkb/core/styles/carousel.styles";
|
||||
import type { Api } from "@zag-js/carousel";
|
||||
import { inject } from "vue";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("carouselApi");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn(carouselIndicatorGroup, className)"
|
||||
v-bind="{ ...props, ...$attrs, ...api?.getIndicatorGroupProps() }">
|
||||
<slot v-if="asChild" />
|
||||
<div v-else>
|
||||
<slot />
|
||||
</div>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { Box } from "@/components/ui/box";
|
||||
import { carouselItem } from "@mykopkb/core/styles/carousel.styles";
|
||||
import type { Api, ItemProps } from "@zag-js/carousel";
|
||||
import { inject } from "vue";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
index,
|
||||
...props
|
||||
} = defineProps<
|
||||
{
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
} & ItemProps
|
||||
>();
|
||||
|
||||
const api = inject<Api>("carouselApi");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot v-bind="{ ...props, ...$attrs, ...api?.getItemProps({ index }) }">
|
||||
<slot v-if="asChild" />
|
||||
<Box v-else :class="cn(carouselItem, className)">
|
||||
<slot />
|
||||
</Box>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts" setup>
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { carouselItemGroup } from "@mykopkb/core/styles/carousel.styles";
|
||||
import type { Api } from "@zag-js/carousel";
|
||||
import { inject } from "vue";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("carouselApi");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn(carouselItemGroup, className)" v-bind="{ ...props, ...$attrs, ...api?.getItemGroupProps() }">
|
||||
<slot v-if="asChild" />
|
||||
<div v-else>
|
||||
<slot />
|
||||
</div>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowRight } from "@lucide/vue";
|
||||
import { carouselNextTrigger } from "@mykopkb/core/styles/carousel.styles";
|
||||
import type { Api } from "@zag-js/carousel";
|
||||
import { inject } from "vue";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("carouselApi");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot v-bind="{ ...props, ...$attrs, ...api?.getNextTriggerProps() }">
|
||||
<slot v-if="asChild" />
|
||||
<Button variant="ghost" v-else :class="cn(carouselNextTrigger, className)">
|
||||
<slot v-if="$slots.default" />
|
||||
<ArrowRight v-else />
|
||||
</Button>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft } from "@lucide/vue";
|
||||
import { carouselPrevTrigger } from "@mykopkb/core/styles/carousel.styles";
|
||||
import type { Api } from "@zag-js/carousel";
|
||||
import { inject } from "vue";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("carouselApi");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot v-bind="{ ...props, ...$attrs, ...api?.getPrevTriggerProps() }">
|
||||
<slot v-if="asChild" />
|
||||
<Button variant="ghost" v-else :class="cn(carouselPrevTrigger, className)">
|
||||
<slot v-if="$slots.default" />
|
||||
<ArrowLeft v-else />
|
||||
</Button>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script lang="ts" setup>
|
||||
import * as carousel from "@zag-js/carousel";
|
||||
import { useMachine, normalizeProps } from "@zag-js/vue";
|
||||
import type { Props } from "@zag-js/carousel";
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { carouselRoot } from "@mykopkb/core/styles/carousel.styles";
|
||||
import { computed, provide } from "vue";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
defaultPage,
|
||||
slideCount,
|
||||
spacing = "2rem",
|
||||
allowMouseDrag = true,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<Partial<Props> & { asChild?: boolean; class?: string }>();
|
||||
|
||||
const service = useMachine(carousel.machine, {
|
||||
defaultPage,
|
||||
slideCount,
|
||||
spacing,
|
||||
allowMouseDrag,
|
||||
...props,
|
||||
id: crypto.randomUUID(),
|
||||
});
|
||||
|
||||
const api = computed(() => carousel.connect(service, normalizeProps));
|
||||
|
||||
provide("carouselApi", api);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn(carouselRoot, className)" v-bind="{ ...props, ...$attrs, ...api.getRootProps() }">
|
||||
<slot v-if="asChild" />
|
||||
<div v-else>
|
||||
<slot />
|
||||
</div>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,8 @@
|
||||
export { default as CarouselRoot } from "./CarouselRoot.vue";
|
||||
export { default as CarouselControl } from "./CarouselControl.vue";
|
||||
export { default as CarouselPrevTrigger } from "./CarouselPrevTrigger.vue";
|
||||
export { default as CarouselNextTrigger } from "./CarouselNextTrigger.vue";
|
||||
export { default as CarouselIndicatorGroup } from "./CarouselIndicatorGroup.vue";
|
||||
export { default as CarouselIndicator } from "./CarouselIndicator.vue";
|
||||
export { default as CarouselItemGroup } from "./CarouselItemGroup.vue";
|
||||
export { default as CarouselItem } from "./CarouselItem.vue";
|
||||
@@ -0,0 +1,36 @@
|
||||
<script lang="ts" setup generic="TType extends ChartType">
|
||||
import ChartJs from "chart.js/auto";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { chart } from "@mykopkb/core/styles/chart.styles";
|
||||
import type { ChartType, ChartConfiguration } from "chart.js";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
config,
|
||||
getRef,
|
||||
...props
|
||||
} = defineProps<{
|
||||
class?: string;
|
||||
config: ChartConfiguration<TType>;
|
||||
getRef?: (chart: ChartJs<TType>) => void;
|
||||
}>();
|
||||
|
||||
const chartRef = ref<
|
||||
| (HTMLCanvasElement & {
|
||||
instance?: ChartJs<TType>;
|
||||
})
|
||||
| null
|
||||
>(null);
|
||||
|
||||
onMounted(() => {
|
||||
if (chartRef.value && !chartRef.value.instance) {
|
||||
chartRef.value.instance = new ChartJs(chartRef.value, config);
|
||||
getRef?.(chartRef.value.instance);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<canvas :class="cn(chart, className)" ref="chartRef" v-bind="props" />
|
||||
</template>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as Chart } from "./Chart.vue";
|
||||
export { getColor } from "./utils.ts";
|
||||
@@ -0,0 +1,11 @@
|
||||
export function getColor(name: string, opacity = 1) {
|
||||
const color = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue(name)
|
||||
.trim();
|
||||
if (opacity < 1) {
|
||||
return `color-mix(in oklch, ${color} ${opacity * 100}%, transparent ${
|
||||
100 - opacity * 100
|
||||
}%)`;
|
||||
}
|
||||
return color;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import { CheckIcon } from "@lucide/vue";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { checkboxControl } from "@mykopkb/core/styles/checkbox.styles";
|
||||
import { CheckboxIndicator } from ".";
|
||||
import type { Api } from "@zag-js/checkbox";
|
||||
import { inject } from "vue";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
asChild?: boolean;
|
||||
class?: string;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("checkboxApi");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn(checkboxControl, className)" v-bind="{ ...props, ...$attrs, ...api?.getControlProps() }">
|
||||
<slot v-if="asChild" />
|
||||
<div v-else>
|
||||
<CheckboxIndicator>
|
||||
<CheckIcon />
|
||||
</CheckboxIndicator>
|
||||
</div>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Api } from "@zag-js/checkbox";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { checkboxHiddenInput } from "@mykopkb/core/styles/checkbox.styles";
|
||||
import { inject } from "vue";
|
||||
|
||||
const { class: className, ...props } = defineProps<{
|
||||
class?: string;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("checkboxApi");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input :class="cn(checkboxHiddenInput, className)" v-bind="{ ...props, ...$attrs, ...api?.getHiddenInputProps() }" />
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts" setup>
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import type { Api } from "@zag-js/checkbox";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { checkboxIndicator } from "@mykopkb/core/styles/checkbox.styles";
|
||||
import { inject } from "vue";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
asChild?: boolean;
|
||||
class?: string;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("checkboxApi");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn(checkboxIndicator, className)" v-bind="{ ...props, ...$attrs, ...api?.getIndicatorProps() }">
|
||||
<slot v-if="asChild" />
|
||||
<div v-else>
|
||||
<slot />
|
||||
</div>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import type { Api } from "@zag-js/checkbox";
|
||||
import { checkboxLabel } from "@mykopkb/core/styles/checkbox.styles";
|
||||
import { label } from "@mykopkb/core/styles/label.styles";
|
||||
import { inject } from "vue";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
asChild?: boolean;
|
||||
class?: string;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("checkboxApi");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn([label, checkboxLabel, className])" v-bind="{ ...props, ...$attrs, ...api?.getLabelProps() }">
|
||||
<slot v-if="asChild" />
|
||||
<span v-else>
|
||||
<slot />
|
||||
</span>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { checkboxRoot } from "@mykopkb/core/styles/checkbox.styles";
|
||||
import * as checkbox from "@zag-js/checkbox";
|
||||
import { useMachine, normalizeProps } from "@zag-js/vue";
|
||||
import type { Props } from "@zag-js/checkbox";
|
||||
import { CheckboxHiddenInput } from ".";
|
||||
import { computed, provide } from "vue";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
checked = undefined,
|
||||
...props
|
||||
} = defineProps<Partial<Props> & { class?: string }>();
|
||||
|
||||
const service = useMachine(
|
||||
checkbox.machine,
|
||||
computed(() => ({
|
||||
...props,
|
||||
checked,
|
||||
id: crypto.randomUUID(),
|
||||
}))
|
||||
);
|
||||
|
||||
const api = computed(() => checkbox.connect(service, normalizeProps));
|
||||
|
||||
provide("checkboxApi", api);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label :class="cn(checkboxRoot, className)" v-bind="{ ...props, ...$attrs, ...api.getRootProps() }">
|
||||
<slot />
|
||||
<CheckboxHiddenInput />
|
||||
</label>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
export { default as CheckboxRoot } from "./CheckboxRoot.vue";
|
||||
export { default as CheckboxLabel } from "./CheckboxLabel.vue";
|
||||
export { default as CheckboxControl } from "./CheckboxControl.vue";
|
||||
export { default as CheckboxIndicator } from "./CheckboxIndicator.vue";
|
||||
export { default as CheckboxHiddenInput } from "./CheckboxHiddenInput.vue";
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts" setup>
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { comboboxClearTrigger } from "@mykopkb/core/styles/combobox.styles";
|
||||
import type { Api } from "@zag-js/combobox";
|
||||
import { inject } from "vue";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("comboboxApi");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot v-bind="{ ...props, ...$attrs, ...api?.getClearTriggerProps() }">
|
||||
<slot v-if="asChild" />
|
||||
<span :class="cn(comboboxClearTrigger, className)" v-else>
|
||||
<slot />
|
||||
</span>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { comboboxContent } from "@mykopkb/core/styles/combobox.styles";
|
||||
import { Box } from "@/components/ui/box";
|
||||
import { ComboboxPositioner } from ".";
|
||||
import type { Api } from "@zag-js/combobox";
|
||||
import { inject } from "vue";
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("comboboxApi");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<ComboboxPositioner>
|
||||
<Slot v-bind="{ ...props, ...$attrs, ...api?.getContentProps() }">
|
||||
<slot v-if="asChild" />
|
||||
<Box v-else raised="single" :class="cn(comboboxContent, className)">
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
</Box>
|
||||
</Slot>
|
||||
</ComboboxPositioner>
|
||||
</Teleport>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Api } from "@zag-js/combobox";
|
||||
import { inject } from "vue";
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { comboboxControl } from "@mykopkb/core/styles/combobox.styles";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("comboboxApi");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn(comboboxControl, className)" v-bind="{ ...props, ...$attrs, ...api?.getLabelProps() }">
|
||||
<slot v-if="asChild" />
|
||||
<div v-else>
|
||||
<slot />
|
||||
</div>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { comboboxInput } from "@mykopkb/core/styles/combobox.styles";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { Api } from "@zag-js/combobox";
|
||||
import { inject } from "vue";
|
||||
|
||||
const { class: className, ...props } = defineProps<{
|
||||
class?: string;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("comboboxApi");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Input :class="cn(comboboxInput, className)" v-bind="{ ...props, ...$attrs, ...api?.getInputProps() }" />
|
||||
</template>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts" setup>
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { comboboxItem } from "@mykopkb/core/styles/combobox.styles";
|
||||
import type { ItemProps } from "@zag-js/combobox";
|
||||
import type { Api } from "@zag-js/combobox";
|
||||
import { provide, inject } from "vue";
|
||||
import { ComboboxItemIndicator } from ".";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<ItemProps & { class?: string; asChild?: boolean }>();
|
||||
|
||||
const api = inject<Api>("comboboxApi");
|
||||
|
||||
provide("comboboxItem", props);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn(comboboxItem, className)" v-bind="{ ...props, ...$attrs, ...api?.getItemProps(props) }">
|
||||
<slot v-if="asChild" />
|
||||
<div v-else>
|
||||
<slot />
|
||||
<ComboboxItemIndicator />
|
||||
</div>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { comboboxItemGroup } from "@mykopkb/core/styles/combobox.styles";
|
||||
import type { Api } from "@zag-js/combobox";
|
||||
import { provide, inject } from "vue";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("comboboxApi");
|
||||
const itemGroupId = { id: crypto.randomUUID() };
|
||||
|
||||
provide("comboboxItemGroupId", itemGroupId);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn(comboboxItemGroup, className)"
|
||||
v-bind="{ ...props, ...$attrs, ...api?.getItemGroupProps(itemGroupId) }">
|
||||
<slot v-if="asChild" />
|
||||
<div v-else>
|
||||
<slot />
|
||||
</div>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts" setup>
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { comboboxItemGroupLabel } from "@mykopkb/core/styles/combobox.styles";
|
||||
import type { Api, ItemGroupProps } from "@zag-js/combobox";
|
||||
import { inject } from "vue";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("comboboxApi");
|
||||
const itemGroupId = inject<ItemGroupProps>("comboboxItemGroupId");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn(comboboxItemGroupLabel, className)" v-bind="{
|
||||
...props, ...$attrs, ...api?.getItemGroupLabelProps({
|
||||
htmlFor: itemGroupId?.id!,
|
||||
})
|
||||
}">
|
||||
<slot v-if="asChild" />
|
||||
<label v-else>
|
||||
<slot />
|
||||
</label>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { comboboxItemIndicator } from "@mykopkb/core/styles/combobox.styles";
|
||||
import { Check } from "@lucide/vue";
|
||||
import type { Api, ItemProps } from "@zag-js/combobox";
|
||||
import { inject } from "vue";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("comboboxApi");
|
||||
const item = inject<ItemProps>("comboboxItem");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn(comboboxItemIndicator, className)"
|
||||
v-bind="{ ...props, ...$attrs, ...api?.getItemIndicatorProps(item!) }">
|
||||
<slot v-if="asChild" />
|
||||
<div v-else>
|
||||
<slot v-if="$slots.default" />
|
||||
<Check v-else class="size-3.5" />
|
||||
</div>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { comboboxItemText } from "@mykopkb/core/styles/combobox.styles";
|
||||
import type { Api, ItemProps } from "@zag-js/combobox";
|
||||
import { inject } from "vue";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("comboboxApi");
|
||||
const item = inject<ItemProps>("comboboxItem");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn(comboboxItemText, className)" v-bind="{ ...props, ...$attrs, ...api?.getItemTextProps(item!) }">
|
||||
<slot v-if="asChild" />
|
||||
<div v-else>
|
||||
<slot />
|
||||
</div>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { comboboxLabel } from "@mykopkb/core/styles/combobox.styles";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import type { Api } from "@zag-js/combobox";
|
||||
import { inject } from "vue";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("comboboxApi");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot v-bind="{ ...props, ...$attrs, ...api?.getLabelProps() }">
|
||||
<slot v-if="asChild" />
|
||||
<Label v-else :class="cn(comboboxLabel, className)">
|
||||
<slot />
|
||||
</Label>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { comboboxPositioner } from "@mykopkb/core/styles/combobox.styles";
|
||||
import type { Api } from "@zag-js/combobox";
|
||||
import { inject } from "vue";
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("comboboxApi");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn(comboboxPositioner, className)" v-bind="{ ...props, ...$attrs, ...api?.getPositionerProps() }">
|
||||
<slot v-if="asChild" />
|
||||
<div v-else>
|
||||
<slot />
|
||||
</div>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { comboboxRoot } from "@mykopkb/core/styles/combobox.styles";
|
||||
import { provide, computed } from "vue";
|
||||
import * as combobox from "@zag-js/combobox";
|
||||
import { useMachine, normalizeProps } from "@zag-js/vue";
|
||||
import type { Props } from "@zag-js/combobox";
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
multiple = false,
|
||||
selectionBehavior = "clear",
|
||||
value,
|
||||
asChild = false,
|
||||
onOpenChange,
|
||||
onInputValueChange,
|
||||
onValueChange,
|
||||
open = undefined,
|
||||
...props
|
||||
} = defineProps<Partial<Props> & { asChild?: boolean; class?: string }>();
|
||||
|
||||
const service = useMachine(combobox.machine, {
|
||||
multiple,
|
||||
selectionBehavior,
|
||||
onOpenChange,
|
||||
onInputValueChange,
|
||||
onValueChange,
|
||||
open,
|
||||
...props,
|
||||
id: crypto.randomUUID(),
|
||||
});
|
||||
|
||||
const api = computed(() => combobox.connect(service, normalizeProps));
|
||||
|
||||
provide("comboboxApi", api);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot :class="cn(comboboxRoot, className)" :data-multiple="multiple"
|
||||
v-bind="{ ...props, ...$attrs, ...api.getRootProps() }">
|
||||
<slot v-if="asChild" />
|
||||
<div v-else>
|
||||
<slot />
|
||||
</div>
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ChevronsUpDownIcon } from "@lucide/vue";
|
||||
import { comboboxTrigger } from "@mykopkb/core/styles/combobox.styles";
|
||||
import { ComboboxClearTrigger } from ".";
|
||||
import type { Api } from "@zag-js/combobox";
|
||||
import { inject } from "vue";
|
||||
import { Slot } from "@/components/ui/slot";
|
||||
|
||||
const {
|
||||
class: className,
|
||||
asChild = false,
|
||||
...props
|
||||
} = defineProps<{
|
||||
class?: string;
|
||||
asChild?: boolean;
|
||||
}>();
|
||||
|
||||
const api = inject<Api>("comboboxApi");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot v-bind="{ ...props, ...$attrs, ...api?.getTriggerProps() }">
|
||||
<Button variant="ghost" v-if="!asChild" :class="cn(comboboxTrigger, className)">
|
||||
<div>{{ api?.valueAsString || "Select Options..." }}</div>
|
||||
<ComboboxClearTrigger>Clear</ComboboxClearTrigger>
|
||||
<ChevronsUpDownIcon />
|
||||
</Button>
|
||||
<slot v-else />
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
export { default as ComboboxRoot } from "./ComboboxRoot.vue";
|
||||
export { default as ComboboxLabel } from "./ComboboxLabel.vue";
|
||||
export { default as ComboboxControl } from "./ComboboxControl.vue";
|
||||
export { default as ComboboxInput } from "./ComboboxInput.vue";
|
||||
export { default as ComboboxTrigger } from "./ComboboxTrigger.vue";
|
||||
export { default as ComboboxClearTrigger } from "./ComboboxClearTrigger.vue";
|
||||
export { default as ComboboxPositioner } from "./ComboboxPositioner.vue";
|
||||
export { default as ComboboxContent } from "./ComboboxContent.vue";
|
||||
export { default as ComboboxItemGroup } from "./ComboboxItemGroup.vue";
|
||||
export { default as ComboboxItemGroupLabel } from "./ComboboxItemGroupLabel.vue";
|
||||
export { default as ComboboxItem } from "./ComboboxItem.vue";
|
||||
export { default as ComboboxItemText } from "./ComboboxItemText.vue";
|
||||
export { default as ComboboxItemIndicator } from "./ComboboxItemIndicator.vue";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user