Base nuxt move after refactoring until no warnings
There still exists an issue with one Adobe led library ; @international-dates don't seem to compile their sourcemaps correctly. I don't think this is something I can fix however, and it may require hacks and workarounds to solve from what I've gathered
This commit is contained in:
39
components/calendar/Calendar.vue
Normal file
39
components/calendar/Calendar.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { computed, type Component, type ComputedRef } from 'vue'
|
||||
|
||||
import CalendarMenu from './CalendarMenu.vue'
|
||||
|
||||
import MonthlyLayout from './state/monthly/Layout.vue'
|
||||
import CenturyLayout from './state/centennially/Layout.vue'
|
||||
import DecadeLayout from './state/decennially/Layout.vue'
|
||||
import YearLayout from './state/yearly/Layout.vue'
|
||||
|
||||
const { currentConfig } = useCalendar()
|
||||
|
||||
const currentViewComponent: ComputedRef<Component> = computed<Component>(() => {
|
||||
switch (currentConfig.viewType) {
|
||||
case 'month':
|
||||
return MonthlyLayout
|
||||
|
||||
case 'year':
|
||||
return YearLayout
|
||||
|
||||
case 'decade':
|
||||
return DecadeLayout
|
||||
|
||||
case 'century':
|
||||
default:
|
||||
return CenturyLayout
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full grid grid-rows-[auto,1fr]">
|
||||
<CalendarMenu />
|
||||
<KeepAlive>
|
||||
<component :is="currentViewComponent" />
|
||||
</KeepAlive>
|
||||
</div>
|
||||
</template>
|
||||
24
components/calendar/CalendarCurrentDate.vue
Normal file
24
components/calendar/CalendarCurrentDate.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts" setup>
|
||||
import { getRelativeString } from '@/models/Date'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { PhMapPin } from '@phosphor-icons/vue'
|
||||
|
||||
const { defaultDate, getFormattedDateTitle } = useCalendar()
|
||||
const { selectedDate } = storeToRefs(useCalendar())
|
||||
|
||||
const mainDateTitle = computed(() => getFormattedDateTitle(selectedDate.value, true))
|
||||
|
||||
const dateDifference = computed(() => getRelativeString(defaultDate, selectedDate.value))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1 class="text-2xl font-bold flex items-center gap-1">
|
||||
<PhMapPin size="26" weight="bold" /> {{ mainDateTitle }}
|
||||
</h1>
|
||||
<h2 class="text-lg italic opacity-75">
|
||||
{{ dateDifference }}
|
||||
</h2>
|
||||
</template>
|
||||
71
components/calendar/CalendarEvent.vue
Normal file
71
components/calendar/CalendarEvent.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { areDatesIdentical, type LeimDate } from '@/models/Date'
|
||||
|
||||
import type { CalendarEvent } from '@/models/Events'
|
||||
import CalendarEventDetails from './CalendarEventDetails.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
event: CalendarEvent
|
||||
tileDate: LeimDate
|
||||
}>()
|
||||
|
||||
const spansMultipleDays = Boolean(props.event.startDate && props.event.endDate)
|
||||
|
||||
const isStartEvent = spansMultipleDays && areDatesIdentical(props.tileDate, props.event.startDate)
|
||||
const isEndEvent =
|
||||
spansMultipleDays && props.event.endDate && areDatesIdentical(props.tileDate, props.event.endDate)
|
||||
|
||||
const isPopoverOpen = ref<boolean>(false)
|
||||
|
||||
function handleClosePopover() {
|
||||
isPopoverOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiPopover v-model:open="isPopoverOpen">
|
||||
<UiPopoverTrigger as-child>
|
||||
<button
|
||||
class="text-xs px-2 py-1 block w-full text-left rounded-sm whitespace-nowrap overflow-hidden text-ellipsis"
|
||||
:class="
|
||||
cn({
|
||||
'text-white bg-slate-600 hover:bg-slate-700': !event.category,
|
||||
'text-white bg-lime-600 hover:bg-lime-700': event.category === 'naissance',
|
||||
'text-white bg-stone-500 hover:bg-stone-700': event.category === 'mort',
|
||||
'text-white bg-orange-600 hover:bg-orange-700': event.category === 'catastrophe',
|
||||
'text-white bg-pink-600 hover:bg-pink-700': event.category === 'catastrophe naturelle',
|
||||
'text-white bg-sky-600 hover:bg-sky-700': event.category === 'législation',
|
||||
'text-white bg-purple-600 hover:bg-purple-700': event.category === 'religion',
|
||||
'text-white bg-emerald-600 hover:bg-emerald-700': event.category === 'joueurs',
|
||||
'text-slate-900 bg-amber-300 hover:bg-amber-400': event.category === 'inauguration',
|
||||
'text-slate-900 bg-emerald-200 hover:bg-emerald-300': event.category === 'invention',
|
||||
'text-slate-900 bg-cyan-300 hover:bg-cyan-400': event.category === 'science',
|
||||
'text-slate-900 bg-white hover:bg-yellow-200': event.category === 'bénédiction',
|
||||
'text-slate-900 bg-purple-200 hover:bg-purple-300': event.category === 'découverte',
|
||||
'text-slate-900 bg-indigo-200 hover:bg-indigo-300': event.category === 'exploration',
|
||||
'text-white bg-amber-600 hover:bg-amber-700': event.category === 'construction',
|
||||
'text-slate-900 bg-violet-200 hover:bg-violet-300': event.category === 'arcanologie',
|
||||
'text-white bg-rose-600 hover:bg-rose-700': event.category === 'criminalité',
|
||||
'text-white bg-stone-600 hover:bg-stone-700': event.category === 'scandale',
|
||||
'text-slate-900 bg-yellow-500 hover:bg-yellow-600': event.category === 'commerce',
|
||||
'rounded-r-none': isStartEvent,
|
||||
'rounded-l-none': isEndEvent
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ event.title }}
|
||||
</button>
|
||||
</UiPopoverTrigger>
|
||||
|
||||
<CalendarEventDetails
|
||||
v-once
|
||||
:event
|
||||
:spans-multiple-days
|
||||
:is-start-event
|
||||
:is-end-event
|
||||
@query:close-popover="handleClosePopover"
|
||||
/>
|
||||
</UiPopover>
|
||||
</template>
|
||||
245
components/calendar/CalendarEventDetails.vue
Normal file
245
components/calendar/CalendarEventDetails.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<script lang="ts" setup>
|
||||
import { getRelativeString, type LeimDate } from '@/models/Date'
|
||||
import type { CalendarEvent } from '@/models/Events'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { useCalendarEvents } from '@/stores/EventStore'
|
||||
|
||||
import {
|
||||
PhHourglassMedium,
|
||||
PhAlarm,
|
||||
PhHourglassHigh,
|
||||
PhHourglassLow,
|
||||
PhArrowBendDoubleUpLeft,
|
||||
PhArrowBendDoubleUpRight
|
||||
} from '@phosphor-icons/vue'
|
||||
|
||||
const { defaultDate, getFormattedDateTitle, jumpToDate } = useCalendar()
|
||||
const { getRelativeEventFromEvent } = useCalendarEvents()
|
||||
|
||||
const props = defineProps<{
|
||||
event: CalendarEvent
|
||||
spansMultipleDays: boolean
|
||||
isStartEvent?: boolean
|
||||
isEndEvent?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'query:close-popover'): void
|
||||
}>()
|
||||
|
||||
const dateDifference: string = getRelativeString(defaultDate, props.event.startDate)
|
||||
const dateDuration: string | null = props.event.endDate
|
||||
? getRelativeString(props.event.startDate, props.event.endDate, 'compact')
|
||||
: null
|
||||
|
||||
function handleJumpToDate(date: LeimDate) {
|
||||
jumpToDate(date)
|
||||
emit('query:close-popover')
|
||||
}
|
||||
|
||||
function handleGotoRelativeEvent(position: 'next' | 'prev' = 'next') {
|
||||
try {
|
||||
const { targetDate } = getRelativeEventFromEvent(props.event, position, props.isEndEvent)
|
||||
|
||||
handleJumpToDate(targetDate)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiPopoverContent
|
||||
class="event-details w-96"
|
||||
:align="'center'"
|
||||
:align-offset="50"
|
||||
:side="'left'"
|
||||
:collision-padding="60"
|
||||
:hide-when-detached="true"
|
||||
:class="{
|
||||
'border-slate-800': !event.category,
|
||||
'border-lime-800': event.category === 'naissance',
|
||||
'border-stone-600': event.category === 'mort',
|
||||
'border-orange-800': event.category === 'catastrophe',
|
||||
'border-pink-800': event.category === 'catastrophe naturelle',
|
||||
'border-sky-800': event.category === 'législation',
|
||||
'border-purple-800': event.category === 'religion',
|
||||
'border-emerald-800': event.category === 'joueurs',
|
||||
'border-amber-800': event.category === 'inauguration',
|
||||
'border-green-800': event.category === 'invention',
|
||||
'border-cyan-800': event.category === 'science',
|
||||
'border-slate-600': event.category === 'bénédiction',
|
||||
'border-purple-700': event.category === 'découverte',
|
||||
'border-indigo-700': event.category === 'exploration',
|
||||
'border-amber-700': event.category === 'construction',
|
||||
'border-violet-700': event.category === 'arcanologie',
|
||||
'border-rose-800': event.category === 'criminalité',
|
||||
'border-stone-700': event.category === 'scandale',
|
||||
'border-yellow-600': event.category === 'commerce'
|
||||
}"
|
||||
>
|
||||
<div class="grid gap-1">
|
||||
<div class="text-lg font-semibold">
|
||||
{{ event.title }}
|
||||
</div>
|
||||
|
||||
<div class="mb-1">
|
||||
<template v-if="!event.endDate">
|
||||
<p class="font-semibold">{{ getFormattedDateTitle(event.startDate, true) }}</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p class="font-semibold">
|
||||
Du {{ getFormattedDateTitle(event.startDate, true) }} au
|
||||
{{ getFormattedDateTitle(event.endDate, true) }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="mb-1 space-y-1">
|
||||
<p class="text-sm italic opacity-75 flex items-center gap-1">
|
||||
<PhAlarm size="16" weight="fill" /> {{ dateDifference }}
|
||||
</p>
|
||||
<template v-if="dateDuration">
|
||||
<p class="text-sm italic opacity-75 flex items-center gap-1">
|
||||
<PhHourglassMedium size="16" weight="fill" /> Pendant {{ dateDuration }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-if="event.category || event.secondaryCategories">
|
||||
<ul class="flex gap-1">
|
||||
<li v-if="event.category">
|
||||
<UiBadge class="mix-blend-luminosity font-bold bg-gray-600" variant="secondary">
|
||||
{{ event.category }}
|
||||
</UiBadge>
|
||||
</li>
|
||||
|
||||
<li v-for="cat in event.secondaryCategories" :key="cat">
|
||||
<UiBadge class="mix-blend-luminosity bg-gray-600" variant="secondary">
|
||||
{{ cat }}
|
||||
</UiBadge>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<template v-if="event.description">
|
||||
<hr class="border-slate-500 mt-2" >
|
||||
|
||||
<div class="mt-2 text-sm text-slate-300">
|
||||
{{ event.description }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<nav v-if="event.startDate && event.endDate" class="absolute -top-2 right-2 flex gap-2">
|
||||
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child>
|
||||
<button
|
||||
class="flex gap-1"
|
||||
title="Naviguer au début"
|
||||
@click="handleJumpToDate(event.startDate!)"
|
||||
>
|
||||
<PhHourglassHigh size="16" weight="fill" /> Début
|
||||
</button>
|
||||
</UiBadge>
|
||||
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child title="Naviguer à la fin">
|
||||
<button
|
||||
class="flex gap-1"
|
||||
title="Naviguer à la fin"
|
||||
@click="handleJumpToDate(event.endDate!)"
|
||||
>
|
||||
<PhHourglassLow size="16" weight="fill" /> Fin
|
||||
</button>
|
||||
</UiBadge>
|
||||
</nav>
|
||||
|
||||
<nav class="mt-2 flex gap-2">
|
||||
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child>
|
||||
<button
|
||||
class="flex gap-1"
|
||||
title="Évènement précédent"
|
||||
@click="handleGotoRelativeEvent('prev')"
|
||||
>
|
||||
<PhArrowBendDoubleUpLeft size="16" weight="fill" /> Précédent
|
||||
</button>
|
||||
</UiBadge>
|
||||
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child title="Naviguer à la fin">
|
||||
<button
|
||||
class="flex gap-1"
|
||||
title="Évènement suivant"
|
||||
@click="handleGotoRelativeEvent('next')"
|
||||
>
|
||||
<PhArrowBendDoubleUpRight size="16" weight="fill" /> Suivant
|
||||
</button>
|
||||
</UiBadge>
|
||||
</nav>
|
||||
</UiPopoverContent>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.border-slate-800 {
|
||||
--base-color: var(--color-slate-800);
|
||||
}
|
||||
.border-lime-800 {
|
||||
--base-color: var(--color-lime-800);
|
||||
}
|
||||
.border-stone-600 {
|
||||
--base-color: var(--color-stone-600);
|
||||
}
|
||||
.border-orange-800 {
|
||||
--base-color: var(--color-orange-800);
|
||||
}
|
||||
.border-pink-800 {
|
||||
--base-color: var(--color-pink-800);
|
||||
}
|
||||
.border-sky-800 {
|
||||
--base-color: var(--color-sky-800);
|
||||
}
|
||||
.border-purple-800 {
|
||||
--base-color: var(--color-purple-800);
|
||||
}
|
||||
.border-emerald-800 {
|
||||
--base-color: var(--color-emerald-800);
|
||||
}
|
||||
.border-amber-800 {
|
||||
--base-color: var(--color-amber-800);
|
||||
}
|
||||
.border-green-800 {
|
||||
--base-color: var(--color-green-800);
|
||||
}
|
||||
.border-cyan-800 {
|
||||
--base-color: var(--color-cyan-800);
|
||||
}
|
||||
.border-slate-600 {
|
||||
--base-color: var(--color-slate-600);
|
||||
}
|
||||
.border-purple-700 {
|
||||
--base-color: var(--color-purple-700);
|
||||
}
|
||||
.border-indigo-700 {
|
||||
--base-color: var(--color-indigo-700);
|
||||
}
|
||||
.border-amber-700 {
|
||||
--base-color: var(--color-amber-700);
|
||||
}
|
||||
.border-violet-700 {
|
||||
--base-color: var(--color-violet-700);
|
||||
}
|
||||
.border-rose-800 {
|
||||
--base-color: var(--color-rose-800);
|
||||
}
|
||||
.border-stone-700 {
|
||||
--base-color: var(--color-stone-700);
|
||||
}
|
||||
.border-yellow-600 {
|
||||
--base-color: var(--color-yellow-600);
|
||||
}
|
||||
|
||||
.event-details {
|
||||
--bg-color: color-mix(in srgb, var(--base-color), var(--color-slate-950) 85%);
|
||||
background-color: var(--bg-color);
|
||||
|
||||
hr {
|
||||
border-color: var(--base-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
46
components/calendar/CalendarMenu.vue
Normal file
46
components/calendar/CalendarMenu.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts" setup>
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
|
||||
import { PhMagnifyingGlass } from '@phosphor-icons/vue'
|
||||
import CalendarMenuNav from './CalendarMenuNav.vue'
|
||||
import CalendarMenuSubnav from './CalendarMenuSubnav.vue'
|
||||
import CalendarMenuToday from './CalendarMenuToday.vue'
|
||||
import CalendarSwitch from './CalendarSwitch.vue'
|
||||
import CalendarCurrentDate from './CalendarCurrentDate.vue'
|
||||
|
||||
const { revealAdvancedSearch } = useCalendar()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="pt-4 border-slate-700 border-b-[1px]">
|
||||
<div class="px-6 flex justify-between">
|
||||
<menu class="flex items-center gap-2">
|
||||
<li class="flex items-center">
|
||||
<CalendarMenuToday />
|
||||
</li>
|
||||
<li>
|
||||
<CalendarMenuNav />
|
||||
</li>
|
||||
<li class="ml-6">
|
||||
<CalendarCurrentDate />
|
||||
</li>
|
||||
</menu>
|
||||
|
||||
<menu class="flex items-center gap-2">
|
||||
<li>
|
||||
<UiButton search-slash @click="revealAdvancedSearch()">
|
||||
<PhMagnifyingGlass size="20" weight="light" />
|
||||
Recherche avancée
|
||||
</UiButton>
|
||||
</li>
|
||||
<li>
|
||||
<CalendarSwitch />
|
||||
</li>
|
||||
</menu>
|
||||
</div>
|
||||
|
||||
<div class="ml-6">
|
||||
<CalendarMenuSubnav />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
198
components/calendar/CalendarMenuNav.vue
Normal file
198
components/calendar/CalendarMenuNav.vue
Normal file
@@ -0,0 +1,198 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, type ComputedRef } from 'vue'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
|
||||
import {
|
||||
PhCaretDoubleLeft,
|
||||
PhCaretDoubleRight,
|
||||
PhCaretLeft,
|
||||
PhCaretRight
|
||||
} from '@phosphor-icons/vue'
|
||||
|
||||
interface DirectionLabels {
|
||||
pastFar: string
|
||||
pastNear: string
|
||||
futureNear: string
|
||||
futureFar: string
|
||||
}
|
||||
|
||||
const { currentConfig, decrementMonth, incrementMonth, decrementYear, incrementYear } =
|
||||
useCalendar()
|
||||
|
||||
const activeDirectionLabels: ComputedRef<DirectionLabels> = computed(() => {
|
||||
switch (currentConfig.viewType) {
|
||||
case 'month':
|
||||
return {
|
||||
pastFar: 'Année précédente',
|
||||
pastNear: 'Mois précédent',
|
||||
futureNear: 'Mois suivant',
|
||||
futureFar: 'Année suivante'
|
||||
}
|
||||
|
||||
case 'year':
|
||||
return {
|
||||
pastFar: 'Décennie précédente',
|
||||
pastNear: 'Année précédente',
|
||||
futureNear: 'Année suivante',
|
||||
futureFar: 'Décennie suivante'
|
||||
}
|
||||
|
||||
case 'decade':
|
||||
return {
|
||||
pastFar: 'Siècle précédent',
|
||||
pastNear: 'Décennie précédente',
|
||||
futureNear: 'Décennie suivante',
|
||||
futureFar: 'Siècle suivant'
|
||||
}
|
||||
|
||||
case 'century':
|
||||
default:
|
||||
return {
|
||||
pastFar: 'Millénaire précédent',
|
||||
pastNear: 'Siècle précédent',
|
||||
futureNear: 'Siècle suivant',
|
||||
futureFar: 'Millénaire suivant'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function toPastFar(): void {
|
||||
switch (currentConfig.viewType) {
|
||||
case 'month':
|
||||
decrementYear()
|
||||
break
|
||||
|
||||
case 'year':
|
||||
decrementYear(10)
|
||||
break
|
||||
|
||||
case 'decade':
|
||||
decrementYear(100)
|
||||
break
|
||||
|
||||
case 'century':
|
||||
default:
|
||||
decrementYear(1000)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function toPastNear(): void {
|
||||
switch (currentConfig.viewType) {
|
||||
case 'month':
|
||||
decrementMonth()
|
||||
break
|
||||
|
||||
case 'year':
|
||||
decrementYear()
|
||||
break
|
||||
|
||||
case 'decade':
|
||||
decrementYear(10)
|
||||
break
|
||||
|
||||
case 'century':
|
||||
default:
|
||||
decrementYear(100)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function toFutureNear(): void {
|
||||
switch (currentConfig.viewType) {
|
||||
case 'month':
|
||||
incrementMonth()
|
||||
break
|
||||
|
||||
case 'year':
|
||||
incrementYear()
|
||||
break
|
||||
|
||||
case 'decade':
|
||||
incrementYear(10)
|
||||
break
|
||||
|
||||
case 'century':
|
||||
default:
|
||||
incrementYear(100)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function toFutureFar(): void {
|
||||
switch (currentConfig.viewType) {
|
||||
case 'month':
|
||||
incrementYear()
|
||||
break
|
||||
|
||||
case 'year':
|
||||
incrementYear(10)
|
||||
break
|
||||
|
||||
case 'decade':
|
||||
incrementYear(100)
|
||||
break
|
||||
|
||||
case 'century':
|
||||
default:
|
||||
incrementYear(1000)
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton variant="outline" size="icon" @click="toPastFar()">
|
||||
<PhCaretDoubleLeft size="18" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>{{ activeDirectionLabels.pastFar }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton variant="outline" size="icon" @click="toPastNear()">
|
||||
<PhCaretLeft size="18" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>{{ activeDirectionLabels.pastNear }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton variant="outline" size="icon" @click="toFutureNear()">
|
||||
<PhCaretRight size="18" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>{{ activeDirectionLabels.futureNear }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton variant="outline" size="icon" @click="toFutureFar()">
|
||||
<PhCaretDoubleRight size="18" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>{{ activeDirectionLabels.futureFar }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
</template>
|
||||
105
components/calendar/CalendarMenuSubnav.vue
Normal file
105
components/calendar/CalendarMenuSubnav.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<script lang="ts" setup>
|
||||
import { daysPerMonth, monthsPerYear, type LeimDate } from '@/models/Date'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { useCalendarEvents } from '@/stores/EventStore'
|
||||
|
||||
import { PhArrowLineLeft, PhArrowLineRight } from '@phosphor-icons/vue'
|
||||
|
||||
const { currentDate, currentConfig, jumpToDate } = useCalendar()
|
||||
const { getRelativeEventFromDate } = useCalendarEvents()
|
||||
|
||||
function handleGotoPreviousEventPage(position: 'next' | 'prev' = 'next') {
|
||||
let fromDate: LeimDate
|
||||
const toDay = position === 'next' ? daysPerMonth : 1
|
||||
const toMonth = position === 'next' ? monthsPerYear : 0
|
||||
|
||||
switch (currentConfig.viewType) {
|
||||
case 'month':
|
||||
fromDate = {
|
||||
day: toDay,
|
||||
month: currentDate.currentMonth,
|
||||
year: currentDate.currentYear
|
||||
}
|
||||
break
|
||||
|
||||
case 'year':
|
||||
fromDate = {
|
||||
day: toDay,
|
||||
month: toMonth,
|
||||
year: currentDate.currentYear
|
||||
}
|
||||
break
|
||||
|
||||
case 'decade':
|
||||
fromDate = {
|
||||
day: toDay,
|
||||
month: currentDate.currentMonth,
|
||||
year: currentDate.currentYear
|
||||
}
|
||||
break
|
||||
|
||||
case 'century':
|
||||
default:
|
||||
fromDate = {
|
||||
day: toDay,
|
||||
month: currentDate.currentMonth,
|
||||
year: currentDate.currentYear
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
try {
|
||||
const { targetDate } = getRelativeEventFromDate(fromDate, position)
|
||||
|
||||
jumpToDate(targetDate)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-2">
|
||||
<div class="w-40 px-4 py-2 border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm">
|
||||
<span class="text-sm">{{ currentDate.currentDateTitle }}</span>
|
||||
</div>
|
||||
<div class="border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm">
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="rounded-t-sm rounded-b-none"
|
||||
@click="handleGotoPreviousEventPage('prev')"
|
||||
>
|
||||
<PhArrowLineLeft size="22" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>Précédente page à évènements</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
<div class="border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm">
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="rounded-t-sm rounded-b-none"
|
||||
@click="handleGotoPreviousEventPage('next')"
|
||||
>
|
||||
<PhArrowLineRight size="22" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>Prochaine page à évènements</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
31
components/calendar/CalendarMenuToday.vue
Normal file
31
components/calendar/CalendarMenuToday.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { areDatesIdentical } from '@/models/Date'
|
||||
|
||||
const { defaultDate, selectedDate } = storeToRefs(useCalendar())
|
||||
const { jumpToDefaultDate, getFormattedDateTitle } = useCalendar()
|
||||
|
||||
const defaultDateFormatted = getFormattedDateTitle(defaultDate.value, true)
|
||||
|
||||
const isDefaultDate = computed(() => {
|
||||
return areDatesIdentical(selectedDate.value, defaultDate.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton size="sm" :disabled="isDefaultDate" @click="jumpToDefaultDate">
|
||||
Aujourd'hui
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>{{ defaultDateFormatted }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</template>
|
||||
32
components/calendar/CalendarSwitch.vue
Normal file
32
components/calendar/CalendarSwitch.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { PhCalendarBlank } from '@phosphor-icons/vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { currentConfig, viewTypeOptions, getViewTypeTitle, setViewType } = useCalendar()
|
||||
|
||||
const viewTypeTitle = computed(() => getViewTypeTitle(currentConfig.viewType))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiButton size="sm" variant="secondary">
|
||||
<PhCalendarBlank size="18" weight="fill" />
|
||||
|
||||
{{ viewTypeTitle }}
|
||||
</UiButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent :side="'bottom'" :collision-padding="30">
|
||||
<UiDropdownMenuLabel>Mode d'affichage</UiDropdownMenuLabel>
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuItem
|
||||
v-for="option in viewTypeOptions"
|
||||
:key="option"
|
||||
@click="setViewType(option)"
|
||||
>
|
||||
{{ getViewTypeTitle(option) }}
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</template>
|
||||
3
components/calendar/SearchMode.ts
Normal file
3
components/calendar/SearchMode.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const searchUnifier = new RegExp(/[^a-zA-Z0-9\-'']/g)
|
||||
|
||||
export type SearchMode = 'characters' | 'events' | undefined
|
||||
56
components/calendar/Sidebar.vue
Normal file
56
components/calendar/Sidebar.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script lang="ts" setup>
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
|
||||
import { PhList, PhHouse, PhMagnifyingGlass } from '@phosphor-icons/vue'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import Button from '../ui/button/Button.vue'
|
||||
|
||||
const { revealAdvancedSearch } = useCalendar()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="w-16 py-6 border-r-[1px] border-l-slate-500 grid justify-center">
|
||||
<menu class="flex flex-col gap-4">
|
||||
<li class="mb-12">
|
||||
<Button variant="ghost" size="icon" class="rounded-full" @click="console.log">
|
||||
<PhList size="27" />
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<TooltipProvider :delay-duration="100">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="rounded-full" as-child>
|
||||
<RouterLink to="/">
|
||||
<PhHouse size="24" weight="fill" />
|
||||
</RouterLink>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent :side="'right'">
|
||||
<p>Retourner aux outils</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</li>
|
||||
<li>
|
||||
<TooltipProvider :delay-duration="100">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="rounded-full"
|
||||
@click="revealAdvancedSearch()"
|
||||
>
|
||||
<PhMagnifyingGlass size="24" weight="fill" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent :side="'right'">
|
||||
<p>Recherche avancée</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</li>
|
||||
</menu>
|
||||
</nav>
|
||||
</template>
|
||||
426
components/calendar/search/CalendarSearch.vue
Normal file
426
components/calendar/search/CalendarSearch.vue
Normal file
@@ -0,0 +1,426 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
characterCategories,
|
||||
isCharacter,
|
||||
type Character,
|
||||
type CharacterCategory
|
||||
} from '@/models/Characters'
|
||||
import type { LeimDateOrder } from '@/models/Date'
|
||||
import {
|
||||
calendarEventCategories,
|
||||
isCalendarEvent,
|
||||
type CalendarEvent,
|
||||
type CalendarEventCategory
|
||||
} from '@/models/Events'
|
||||
import { useCharacters } from '@/stores/CharacterStore'
|
||||
import { useCalendarEvents } from '@/stores/EventStore'
|
||||
import { capitalize } from '@/utils/Strings'
|
||||
import { useMagicKeys, useScroll, useStorage, whenever } from '@vueuse/core'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { searchUnifier, type SearchMode } from '../SearchMode'
|
||||
|
||||
import { PhClockClockwise, PhClockCounterClockwise, PhMagnifyingGlass } from '@phosphor-icons/vue'
|
||||
import {
|
||||
ComboboxAnchor,
|
||||
ComboboxInput,
|
||||
ComboboxPortal,
|
||||
ComboboxRoot,
|
||||
VisuallyHidden
|
||||
} from 'radix-vue'
|
||||
|
||||
import SearchList from './lists/SearchList.vue'
|
||||
|
||||
const { characters } = useCharacters()
|
||||
const { allEvents } = useCalendarEvents()
|
||||
|
||||
const modalOpen = defineModel({ default: false })
|
||||
|
||||
const searchQuery = ref<string>('')
|
||||
// const searchEnough = computed<boolean>(() => searchQuery.value.length >= 2)
|
||||
|
||||
const selectedEntity = useStorage('se', 'events' as SearchMode)
|
||||
|
||||
// Order
|
||||
const selectedOrder = ref<LeimDateOrder>('asc')
|
||||
function setOrderAsc() {
|
||||
selectedOrder.value = 'asc'
|
||||
resetPage()
|
||||
}
|
||||
|
||||
function setOrderDesc() {
|
||||
selectedOrder.value = 'desc'
|
||||
resetPage()
|
||||
}
|
||||
|
||||
// Limit
|
||||
const currentPage = ref<number>(1)
|
||||
const itemsPerPage: number = 20
|
||||
const startOfList = computed<number>(() => (currentPage.value - 1) * itemsPerPage)
|
||||
const endOfList = computed<number>(() => startOfList.value + itemsPerPage)
|
||||
|
||||
/**
|
||||
* Resets the pagination
|
||||
*/
|
||||
function resetPage() {
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
const searchResults = computed<(Character | CalendarEvent)[]>(() => {
|
||||
let results: (Character | CalendarEvent)[] = []
|
||||
let dataToFilter: Character[] | CalendarEvent[] | (Character | CalendarEvent)[]
|
||||
const shouldFilterCategories = selectedCategories.value.length > 0
|
||||
|
||||
// Assign data to loop over and filter
|
||||
// They are assigned this way for readability
|
||||
if (selectedEntity.value === 'events') {
|
||||
dataToFilter = allEvents
|
||||
} else if (selectedEntity.value === 'characters') {
|
||||
dataToFilter = characters
|
||||
} else {
|
||||
dataToFilter = [...allEvents, ...characters]
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Refactor the categories logic, basically extract the return out of the ifs, like above
|
||||
*/
|
||||
results = dataToFilter.filter((item) => {
|
||||
// Filter calendar events
|
||||
if (isCalendarEvent(item)) {
|
||||
const queryString = new String(searchQuery.value)
|
||||
.replace(searchUnifier, '')
|
||||
.toLocaleLowerCase()
|
||||
|
||||
const hitTitle = item.title
|
||||
.replace(searchUnifier, '')
|
||||
.toLocaleLowerCase()
|
||||
.includes(queryString)
|
||||
const hitDesc = item.description
|
||||
?.replace(searchUnifier, '')
|
||||
.toLocaleLowerCase()
|
||||
.includes(queryString)
|
||||
|
||||
if (!shouldFilterCategories) {
|
||||
return hitTitle || hitDesc
|
||||
}
|
||||
|
||||
// Handle categories logic
|
||||
let hitCategories: boolean = false
|
||||
const allCategories: CalendarEventCategory[] = []
|
||||
|
||||
if (item.category) {
|
||||
allCategories.push(item.category)
|
||||
}
|
||||
|
||||
if (item.secondaryCategories && item.secondaryCategories?.length > 0) {
|
||||
allCategories.push(...item.secondaryCategories)
|
||||
}
|
||||
|
||||
hitCategories = selectedCategories.value.every((selectedCat) => {
|
||||
return allCategories.includes(selectedCat as CalendarEventCategory)
|
||||
})
|
||||
|
||||
return (hitTitle || hitDesc) && hitCategories
|
||||
}
|
||||
|
||||
// Filter characters
|
||||
if (isCharacter(item)) {
|
||||
const queryString = new String(searchQuery.value)
|
||||
.replace(searchUnifier, '')
|
||||
.toLocaleLowerCase()
|
||||
|
||||
const hitTitle = item.name
|
||||
.replace(searchUnifier, '')
|
||||
.toLocaleLowerCase()
|
||||
.includes(queryString)
|
||||
|
||||
if (!shouldFilterCategories) {
|
||||
return hitTitle
|
||||
}
|
||||
|
||||
// Handle categories logic
|
||||
let hitCategories: boolean = false
|
||||
const allCategories: CharacterCategory[] = []
|
||||
|
||||
if (item.category) {
|
||||
allCategories.push(item.category)
|
||||
}
|
||||
|
||||
if (item.secondaryCategories && item.secondaryCategories?.length > 0) {
|
||||
allCategories.push(...item.secondaryCategories)
|
||||
}
|
||||
|
||||
hitCategories = selectedCategories.value.every((selectedCat) => {
|
||||
return allCategories.includes(selectedCat as CharacterCategory)
|
||||
})
|
||||
|
||||
return hitTitle && hitCategories
|
||||
}
|
||||
})
|
||||
|
||||
return results
|
||||
})
|
||||
|
||||
/**
|
||||
* Removes the search query, resets the pagination and removes all selected categories
|
||||
*/
|
||||
function resetSearch() {
|
||||
searchQuery.value = ''
|
||||
resetPage()
|
||||
selectedCategories.value = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the search Uidialog
|
||||
*/
|
||||
function openUiDialog() {
|
||||
modalOpen.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the search Uidialog
|
||||
*/
|
||||
function closeUiDialog() {
|
||||
modalOpen.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches the selectedEntity
|
||||
*/
|
||||
function handleEntitySwitch() {
|
||||
resetPage()
|
||||
selectedCategories.value = []
|
||||
}
|
||||
|
||||
// Key combos to deploy modal
|
||||
const keys = useMagicKeys()
|
||||
|
||||
whenever(keys.control_period, () => {
|
||||
openUiDialog()
|
||||
})
|
||||
|
||||
const searchResultsRef = ref<HTMLElement | null>(null)
|
||||
const { y: searchResultsY } = useScroll(searchResultsRef)
|
||||
|
||||
watch(currentPage, () => {
|
||||
searchResultsY.value = 0
|
||||
})
|
||||
|
||||
// Compute categories based on current selectedEntity
|
||||
const currentCategories = computed(() => {
|
||||
if (selectedEntity.value === 'characters') {
|
||||
return [...characterCategories]
|
||||
} else {
|
||||
return [...calendarEventCategories]
|
||||
}
|
||||
})
|
||||
|
||||
const selectedCategories = ref<(CharacterCategory | CalendarEventCategory)[]>([])
|
||||
const categoryFilterOpened = ref<boolean>(false)
|
||||
const searchCategory = ref<string>('')
|
||||
|
||||
const filteredCategories = computed(() =>
|
||||
currentCategories.value.filter((i) => !selectedCategories.value.includes(i))
|
||||
)
|
||||
|
||||
/**
|
||||
* Handles the category selections from the TagInput component
|
||||
*
|
||||
* @param e Radix Change Event
|
||||
*/
|
||||
function handleCategorySelect(e: (CharacterCategory | CalendarEventCategory)) {
|
||||
if (typeof e === 'string') {
|
||||
searchCategory.value = ''
|
||||
selectedCategories.value.push(e)
|
||||
}
|
||||
|
||||
if (filteredCategories.value.length === 0) {
|
||||
categoryFilterOpened.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiDialog v-model:open="modalOpen" @update:open="resetSearch()">
|
||||
<UiDialogContent
|
||||
class="flex flex-col flex-nowrap top-16 -translate-y-0 data-[state=closed]:slide-out-to-top-[5%] data-[state=open]:slide-in-from-top-[5%]"
|
||||
:class="{
|
||||
'bottom-16': searchResults.length > 0
|
||||
}"
|
||||
>
|
||||
<VisuallyHidden>
|
||||
<UiDialogTitle> Recherche avancée </UiDialogTitle>
|
||||
</VisuallyHidden>
|
||||
<VisuallyHidden>
|
||||
<UiDialogDescription>
|
||||
Rechercher les données disponibles sur le calendrier
|
||||
</UiDialogDescription>
|
||||
</VisuallyHidden>
|
||||
|
||||
<!-- UiDialog header -->
|
||||
<div id="searchForm" class="grid gap-3">
|
||||
<div class="relative w-full h-fit">
|
||||
<UiInput
|
||||
id="search"
|
||||
v-model:model-value="searchQuery"
|
||||
type="text"
|
||||
placeholder="Rechercher le calendrier"
|
||||
class="pl-10 py-6 text-lg"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<span class="absolute start-1 inset-y-0 flex items-center justify-center px-2 opacity-50">
|
||||
<PhMagnifyingGlass size="20" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<UiToggleGroup
|
||||
v-model="selectedEntity"
|
||||
type="single"
|
||||
class="justify-start"
|
||||
@update:model-value="handleEntitySwitch()"
|
||||
>
|
||||
<UiToggleGroupItem value="events" aria-label="Uniquement les évènements">
|
||||
Évènements
|
||||
</UiToggleGroupItem>
|
||||
<UiToggleGroupItem value="characters" aria-label="Uniquement les personnages">
|
||||
Personnages
|
||||
</UiToggleGroupItem>
|
||||
</UiToggleGroup>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<UiTagsInput class="px-0 gap-0 w-72" :model-value="selectedCategories">
|
||||
<div class="flex gap-2 flex-wrap items-center px-3">
|
||||
<UiTagsInputItem v-for="item in selectedCategories" :key="item" :value="item">
|
||||
<UiTagsInputItemText class="capitalize" />
|
||||
<UiTagsInputItemDelete />
|
||||
</UiTagsInputItem>
|
||||
</div>
|
||||
|
||||
<ComboboxRoot
|
||||
v-model="selectedCategories"
|
||||
v-model:open="categoryFilterOpened"
|
||||
v-model:searchTerm="searchCategory"
|
||||
class="w-full"
|
||||
>
|
||||
<ComboboxAnchor as-child>
|
||||
<ComboboxInput placeholder="Catégories" as-child>
|
||||
<UiTagsInputInput
|
||||
class="w-full px-3"
|
||||
:class="selectedCategories.length > 0 ? 'mt-2' : ''"
|
||||
@keydown.enter.prevent
|
||||
/>
|
||||
</ComboboxInput>
|
||||
</ComboboxAnchor>
|
||||
|
||||
<ComboboxPortal :to="'#searchForm'">
|
||||
<UiCommandList
|
||||
position="popper"
|
||||
class="w-[--radix-popper-anchor-width] rounded-md mt-2 border bg-popover text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50"
|
||||
:dismissable="true"
|
||||
>
|
||||
<UiCommandEmpty />
|
||||
<UiCommandGroup>
|
||||
<UiCommandItem
|
||||
v-for="cat in filteredCategories"
|
||||
:key="cat"
|
||||
:value="cat"
|
||||
@select.prevent="handleCategorySelect(cat)"
|
||||
>
|
||||
{{ capitalize(cat) }}
|
||||
</UiCommandItem>
|
||||
</UiCommandGroup>
|
||||
</UiCommandList>
|
||||
</ComboboxPortal>
|
||||
</ComboboxRoot>
|
||||
</UiTagsInput>
|
||||
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
:variant="selectedOrder === 'desc' ? 'secondary' : 'outline'"
|
||||
size="icon"
|
||||
@click="setOrderDesc()"
|
||||
>
|
||||
<PhClockCounterClockwise size="18" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>Plus ancien</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
:variant="selectedOrder === 'asc' ? 'secondary' : 'outline'"
|
||||
size="icon"
|
||||
@click="setOrderAsc()"
|
||||
>
|
||||
<PhClockClockwise size="18" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>Plus récent</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr >
|
||||
|
||||
<div v-if="searchResults.length > 0" ref="searchResultsRef" class="grow overflow-y-auto">
|
||||
<SearchList
|
||||
:results="searchResults"
|
||||
:current-entity="selectedEntity"
|
||||
:order="selectedOrder"
|
||||
:start-at="startOfList"
|
||||
:end-at="endOfList"
|
||||
@jumped-to-date="closeUiDialog()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<UiPagination
|
||||
v-model:page="currentPage"
|
||||
:total="searchResults.length"
|
||||
:items-per-page="itemsPerPage"
|
||||
:sibling-count="2"
|
||||
show-edges
|
||||
:default-page="1"
|
||||
>
|
||||
<UiPaginationList v-slot="{ items }" class="flex items-center gap-1">
|
||||
<UiPaginationFirst />
|
||||
<UiPaginationPrev />
|
||||
|
||||
<template v-for="(item, index) in items">
|
||||
<UiPaginationListItem
|
||||
v-if="item.type === 'page'"
|
||||
:key="index"
|
||||
:value="item.value"
|
||||
as-child
|
||||
>
|
||||
<UiButton
|
||||
class="w-10 h-10 p-0"
|
||||
:variant="item.value === currentPage ? 'default' : 'outline'"
|
||||
>
|
||||
{{ item.value }}
|
||||
</UiButton>
|
||||
</UiPaginationListItem>
|
||||
<UiPaginationEllipsis v-else :key="item.type" :index="index" />
|
||||
</template>
|
||||
|
||||
<UiPaginationNext />
|
||||
<UiPaginationLast />
|
||||
</UiPaginationList>
|
||||
</UiPagination>
|
||||
</div>
|
||||
</UiDialogContent>
|
||||
</UiDialog>
|
||||
</template>
|
||||
89
components/calendar/search/lists/CharacterCallout.vue
Normal file
89
components/calendar/search/lists/CharacterCallout.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Character } from '@/models/Characters'
|
||||
import type { LeimDate } from '@/models/Date'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { PhArrowSquareOut, PhPlant, PhSkull } from '@phosphor-icons/vue'
|
||||
|
||||
defineProps<{
|
||||
character: Character
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'query:date-jump', payload: LeimDate): void
|
||||
}>()
|
||||
|
||||
const { getFormattedDateTitle } = useCalendar()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="block w-full text-left py-3 px-4 border-[1px] border-slate-700 rounded-sm">
|
||||
<div class="grid gap-2">
|
||||
<div class="flex gap-2">
|
||||
<h2 class="font-bold">
|
||||
{{ character.name }}
|
||||
</h2>
|
||||
<div v-if="character.wiki">
|
||||
<Button variant="link" size="xs" as-child>
|
||||
<a :href="character.wiki" target="_blank">
|
||||
Wiki
|
||||
<PhArrowSquareOut size="16" weight="fill" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<menu class="flex gap-2 border-[1px] border-slate-700 rounded-sm w-fit">
|
||||
<li v-if="character.birth">
|
||||
<TooltipProvider :delay-duration="100">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
@click="$emit('query:date-jump', character.birth!)"
|
||||
>
|
||||
<PhPlant size="16" weight="fill" />
|
||||
{{ getFormattedDateTitle(character.birth, true) }}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent :side="'bottom'" :align="'end'" :align-offset="20">
|
||||
<p>Date de naissance</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</li>
|
||||
<span v-if="character.birth && character.death">-</span>
|
||||
<li v-if="character.death">
|
||||
<TooltipProvider :delay-duration="100">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
@click="$emit('query:date-jump', character.death!)"
|
||||
>
|
||||
<PhSkull size="16" weight="fill" />
|
||||
{{ getFormattedDateTitle(character.death, true) }}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent :side="'bottom'" :align="'start'" :align-offset="20">
|
||||
<p>Date de décès</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</li>
|
||||
</menu>
|
||||
|
||||
<hr v-if="character.description" class="border-white opacity-25" >
|
||||
|
||||
<div v-if="character.description" class="text-sm">
|
||||
<span class="opacity-75">
|
||||
{{ character.description }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
114
components/calendar/search/lists/EventCallout.vue
Normal file
114
components/calendar/search/lists/EventCallout.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<script lang="ts" setup>
|
||||
import { getRelativeString, type LeimDate } from '@/models/Date'
|
||||
import type { CalendarEvent } from '@/models/Events'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { PhArrowSquareOut, PhHourglassMedium, PhAlarm } from '@phosphor-icons/vue'
|
||||
|
||||
const props = defineProps<{
|
||||
event: CalendarEvent
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'query:date-jump', payload: LeimDate): void
|
||||
}>()
|
||||
|
||||
const { defaultDate, getFormattedDateTitle } = useCalendar()
|
||||
|
||||
const dateDifference: string = getRelativeString(defaultDate, props.event.startDate)
|
||||
const dateDuration: string | null = props.event.endDate
|
||||
? getRelativeString(props.event.startDate, props.event.endDate, 'compact')
|
||||
: null
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="relative block w-full text-left py-3 px-4 rounded-sm transition-colors"
|
||||
:class="{
|
||||
'text-white bg-slate-600 hover:bg-slate-700': !event.category,
|
||||
'text-white bg-lime-600 hover:bg-lime-700': event.category === 'naissance',
|
||||
'text-white bg-stone-500 hover:bg-stone-700': event.category === 'mort',
|
||||
'text-white bg-orange-600 hover:bg-orange-700': event.category === 'catastrophe',
|
||||
'text-white bg-pink-600 hover:bg-pink-700': event.category === 'catastrophe naturelle',
|
||||
'text-white bg-sky-600 hover:bg-sky-700': event.category === 'législation',
|
||||
'text-white bg-purple-600 hover:bg-purple-700': event.category === 'religion',
|
||||
'text-white bg-emerald-600 hover:bg-emerald-700': event.category === 'joueurs',
|
||||
'text-slate-900 bg-amber-300 hover:bg-amber-400': event.category === 'inauguration',
|
||||
'text-slate-900 bg-emerald-200 hover:bg-emerald-300': event.category === 'invention',
|
||||
'text-slate-900 bg-cyan-300 hover:bg-cyan-400': event.category === 'science',
|
||||
'text-slate-900 bg-white hover:bg-yellow-200': event.category === 'bénédiction',
|
||||
'text-slate-900 bg-purple-200 hover:bg-purple-300': event.category === 'découverte',
|
||||
'text-slate-900 bg-indigo-200 hover:bg-indigo-300': event.category === 'exploration',
|
||||
'text-white bg-amber-600 hover:bg-amber-700': event.category === 'construction',
|
||||
'text-slate-900 bg-violet-200 hover:bg-violet-300': event.category === 'arcanologie',
|
||||
'text-white bg-rose-600 hover:bg-rose-700': event.category === 'criminalité',
|
||||
'text-white bg-stone-600 hover:bg-stone-700': event.category === 'scandale',
|
||||
'text-slate-900 bg-yellow-500 hover:bg-yellow-600': event.category === 'commerce'
|
||||
}"
|
||||
@click="$emit('query:date-jump', event.startDate)"
|
||||
>
|
||||
<div class="flex gap-2 items-center">
|
||||
<h2 class="font-bold">
|
||||
{{ event.title }}
|
||||
</h2>
|
||||
<div v-if="event.wiki">
|
||||
<Button variant="link" size="xs" as-child class="text-inherit">
|
||||
<a :href="event.wiki" target="_blank">
|
||||
Wiki
|
||||
<PhArrowSquareOut size="16" weight="fill" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-1">
|
||||
<template v-if="!event.endDate">
|
||||
<p class="col-span-2 font-semibold text-sm opacity-75">
|
||||
{{ getFormattedDateTitle(event.startDate, true) }}
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p class="col-span-2 font-semibold text-sm opacity-75">
|
||||
Du {{ getFormattedDateTitle(event.startDate, true) }} au
|
||||
{{ getFormattedDateTitle(event.endDate, true) }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="mb-1 flex gap-x-2 items-center">
|
||||
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1">
|
||||
<PhAlarm size="16" weight="fill" /> {{ dateDifference }}
|
||||
</p>
|
||||
<template v-if="dateDuration">
|
||||
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1">
|
||||
<PhHourglassMedium size="16" weight="fill" /> Pendant {{ dateDuration }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="event.category || event.secondaryCategories" class="absolute top-3 right-4">
|
||||
<ul class="flex gap-1">
|
||||
<li v-if="event.category">
|
||||
<Badge class="mix-blend-luminosity font-bold bg-gray-600" variant="secondary">
|
||||
{{ event.category }}
|
||||
</Badge>
|
||||
</li>
|
||||
|
||||
<li v-for="cat in event.secondaryCategories" :key="cat">
|
||||
<Badge class="mix-blend-luminosity bg-gray-600" variant="secondary">
|
||||
{{ cat }}
|
||||
</Badge>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="event.description" class="text-sm">
|
||||
<hr class="my-2 border-white opacity-50" >
|
||||
<span class="opacity-75">
|
||||
{{ event.description }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
74
components/calendar/search/lists/SearchList.vue
Normal file
74
components/calendar/search/lists/SearchList.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<script lang="ts" setup>
|
||||
import { isCharacter, type Character } from '@/models/Characters'
|
||||
import { compareDates, type LeimDate, type LeimDateOrder } from '@/models/Date'
|
||||
import { isCalendarEvent, type CalendarEvent } from '@/models/Events'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { computed } from 'vue'
|
||||
import type { SearchMode } from '../../SearchMode'
|
||||
|
||||
import CharacterCallout from './CharacterCallout.vue'
|
||||
import EventCallout from './EventCallout.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
results: (Character | CalendarEvent)[]
|
||||
currentEntity: SearchMode
|
||||
order: LeimDateOrder
|
||||
startAt: number
|
||||
endAt: number
|
||||
limit?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['jumpedToDate'])
|
||||
|
||||
const { jumpToDate } = useCalendar()
|
||||
|
||||
function handleJumpToDate(date?: LeimDate) {
|
||||
if (!date) return
|
||||
|
||||
jumpToDate(date)
|
||||
emit('jumpedToDate')
|
||||
}
|
||||
|
||||
// Initial sorting of the results
|
||||
const sortedResults = computed(() => {
|
||||
return [...props.results].sort((a, b) => {
|
||||
let firstDate: LeimDate
|
||||
let secondDate: LeimDate
|
||||
|
||||
if (isCalendarEvent(a)) {
|
||||
firstDate = a.startDate
|
||||
} else if (isCharacter(a) && a.birth) {
|
||||
firstDate = a.birth
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
|
||||
if (isCalendarEvent(b)) {
|
||||
secondDate = b.startDate
|
||||
} else if (isCharacter(b) && b.birth) {
|
||||
secondDate = b.birth
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
|
||||
return compareDates(firstDate, secondDate, props.order)
|
||||
})
|
||||
})
|
||||
|
||||
// Page the sorted results
|
||||
const pagedResults = computed(() => sortedResults.value.slice(props.startAt, props.endAt))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul class="grid gap-4">
|
||||
<li v-for="r in pagedResults" :key="isCalendarEvent(r) ? r.title : r.name">
|
||||
<EventCallout v-if="isCalendarEvent(r)" :event="r" @query:date-jump="handleJumpToDate" />
|
||||
|
||||
<CharacterCallout
|
||||
v-else-if="isCharacter(r)"
|
||||
:character="r"
|
||||
@query:date-jump="handleJumpToDate"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
5
components/calendar/state/centennially/Layout.vue
Normal file
5
components/calendar/state/centennially/Layout.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<template>
|
||||
<div>Century</div>
|
||||
</template>
|
||||
3
components/calendar/state/decennially/Layout.vue
Normal file
3
components/calendar/state/decennially/Layout.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div>Decade</div>
|
||||
</template>
|
||||
141
components/calendar/state/monthly/DayTile.vue
Normal file
141
components/calendar/state/monthly/DayTile.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<script lang="ts" setup>
|
||||
import { areDatesIdentical, type LeimDate } from '@/models/Date'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { useCalendarEvents } from '@/stores/EventStore'
|
||||
import { useElementBounding } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref, type ComputedRef } from 'vue'
|
||||
|
||||
import CalendarEventButton from '../../CalendarEvent.vue'
|
||||
import type { CalendarEvent } from '@/models/Events'
|
||||
|
||||
const props = defineProps<{
|
||||
date: LeimDate
|
||||
faded?: boolean
|
||||
}>()
|
||||
|
||||
const calendarTile = ref()
|
||||
const calendarEventsList = ref()
|
||||
|
||||
const { defaultDate, selectDate } = useCalendar()
|
||||
const { selectedDate } = storeToRefs(useCalendar())
|
||||
const { currentEvents } = storeToRefs(useCalendarEvents())
|
||||
|
||||
const eventsForTheDay = computed(() => {
|
||||
return currentEvents.value.filter((currentEvent) => {
|
||||
return (
|
||||
areDatesIdentical(currentEvent.startDate, props.date) ||
|
||||
areDatesIdentical(currentEvent.endDate!, props.date)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const isDefaultDate = computed(() => {
|
||||
return areDatesIdentical(props.date, defaultDate)
|
||||
})
|
||||
|
||||
const isSelectedDate = computed(() => {
|
||||
return areDatesIdentical(props.date, selectedDate.value)
|
||||
})
|
||||
|
||||
// Get bounding elements for both tile and events list
|
||||
const { height: tileHeight, top: tileTop } = useElementBounding(calendarTile)
|
||||
const { top: tileListTop } = useElementBounding(calendarEventsList)
|
||||
|
||||
// Compute the available number of events that can be displayed from refs heights
|
||||
const numberOfEventsToFit: ComputedRef<number> = computed(() => {
|
||||
if (!eventsForTheDay.value.length) return 0
|
||||
|
||||
return Math.trunc((tileHeight.value - (tileListTop.value - tileTop.value)) / 40)
|
||||
})
|
||||
|
||||
const eventsToDisplay: ComputedRef<CalendarEvent[]> = computed(() => {
|
||||
return [...eventsForTheDay.value].splice(0, numberOfEventsToFit.value)
|
||||
})
|
||||
const eventsNotDisplayed = computed(
|
||||
() => eventsForTheDay.value.length - eventsToDisplay.value.length
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="calendarTile"
|
||||
class="tile relative text-xs p-2 border-slate-700"
|
||||
:class="{
|
||||
'text-slate-500': props.faded,
|
||||
'text-slate-300': !props.faded
|
||||
}"
|
||||
>
|
||||
<!-- Used for "display all events" -->
|
||||
<button class="absolute inset-0 w-full h-full cursor-default z-0" />
|
||||
|
||||
<button
|
||||
class="relative z-10 group block w-full text-center cursor-pointer"
|
||||
@click="selectDate(date)"
|
||||
>
|
||||
<span
|
||||
class="inline-flex w-8 h-8 aspect-square items-center justify-center rounded-full border-2 border-transparent font-bold transition-colors group-hover:border-slate-800"
|
||||
:class="{
|
||||
'bg-slate-800': isDefaultDate && !isSelectedDate,
|
||||
'text-white bg-blue-500': isSelectedDate
|
||||
}"
|
||||
>
|
||||
{{ date.day }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<ul
|
||||
ref="calendarEventsList"
|
||||
class="absolute top-12 bottom-2 inset-x-2 grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity"
|
||||
:class="{
|
||||
'opacity-40': props.faded && !isSelectedDate
|
||||
}"
|
||||
>
|
||||
<li v-for="event in eventsToDisplay" :key="event.title" class="grid pointer-events-auto">
|
||||
<CalendarEventButton :event :tile-date="date" />
|
||||
</li>
|
||||
|
||||
<li v-if="eventsNotDisplayed > 0" class="pointer-events-auto">
|
||||
<UiPopover>
|
||||
<UiPopoverTrigger as-child>
|
||||
<button
|
||||
class="text-xs px-2 py-1 block w-full text-left font-bold rounded-sm whitespace-nowrap overflow-hidden text-ellipsis cursor-pointer transition-colors hover:bg-slate-800"
|
||||
>
|
||||
{{ eventsNotDisplayed }} autre{{ eventsNotDisplayed > 1 ? 's' : '' }}
|
||||
</button>
|
||||
</UiPopoverTrigger>
|
||||
<UiPopoverContent class="w-80" :align="'center'" :side="'right'">
|
||||
<div class="text-center mb-4">
|
||||
<span
|
||||
class="inline-flex w-12 h-12 aspect-square items-center justify-center text-lg font-semibold text-slate-300 bg-slate-800 rounded-full"
|
||||
>
|
||||
{{ date.day }}
|
||||
</span>
|
||||
</div>
|
||||
<ul class="grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity">
|
||||
<li
|
||||
v-for="event in eventsForTheDay"
|
||||
:key="event.title"
|
||||
class="grid pointer-events-auto"
|
||||
>
|
||||
<CalendarEventButton :event :tile-date="date" />
|
||||
</li>
|
||||
</ul>
|
||||
</UiPopoverContent>
|
||||
</UiPopover>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tile {
|
||||
&:not(:nth-child(10n)) {
|
||||
border-right-width: 1px;
|
||||
}
|
||||
|
||||
&:nth-child(n + 11) {
|
||||
border-top-width: 1px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
73
components/calendar/state/monthly/Layout.vue
Normal file
73
components/calendar/state/monthly/Layout.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<script lang="ts" setup>
|
||||
import type { LeimDate } from '@/models/Date'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { useThrottleFn } from '@vueuse/core'
|
||||
|
||||
import DayTile from './DayTile.vue'
|
||||
|
||||
const { staticConfig, currentDate, decrementMonth, incrementMonth } = useCalendar()
|
||||
|
||||
const daysPerMonth = staticConfig.daysPerMonth
|
||||
|
||||
function getNextMonthDate(day: number): LeimDate {
|
||||
let nextDay = day
|
||||
let nextMonth = currentDate.currentMonth + 1
|
||||
let nextYear = currentDate.currentYear
|
||||
let nextPeriod = currentDate.currentPeriod
|
||||
|
||||
// If the new value would exceed the max number of month per year
|
||||
if (nextMonth >= staticConfig.monthsPerYear) {
|
||||
nextMonth = 0
|
||||
// Increment the year
|
||||
nextYear++
|
||||
}
|
||||
|
||||
if (nextYear >= 0) {
|
||||
nextPeriod = 'nante'
|
||||
}
|
||||
|
||||
return {
|
||||
day: nextDay,
|
||||
month: nextMonth,
|
||||
year: nextYear,
|
||||
period: nextPeriod
|
||||
}
|
||||
}
|
||||
|
||||
function handleWheel(e: WheelEvent) {
|
||||
const isMovingUp = e.deltaY < 0
|
||||
if (isMovingUp) {
|
||||
moveCalendarLeft()
|
||||
} else {
|
||||
moveCalendarRight()
|
||||
}
|
||||
}
|
||||
|
||||
const moveCalendarLeft = useThrottleFn(() => {
|
||||
decrementMonth()
|
||||
}, 100)
|
||||
|
||||
const moveCalendarRight = useThrottleFn(() => {
|
||||
incrementMonth()
|
||||
}, 100)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-10" @wheel="handleWheel">
|
||||
<DayTile
|
||||
v-for="day in daysPerMonth"
|
||||
:key="day"
|
||||
:date="{
|
||||
day: day,
|
||||
month: currentDate.currentMonth,
|
||||
year: currentDate.currentYear
|
||||
}"
|
||||
/>
|
||||
<DayTile
|
||||
v-for="nextMonthDay in 8"
|
||||
:key="nextMonthDay"
|
||||
faded
|
||||
:date="getNextMonthDate(nextMonthDay)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
56
components/calendar/state/yearly/DayTile.vue
Normal file
56
components/calendar/state/yearly/DayTile.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script lang="ts" setup>
|
||||
import { areDatesIdentical, type LeimDate } from '@/models/Date'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { useCalendarEvents } from '@/stores/EventStore'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, type ComputedRef } from 'vue'
|
||||
|
||||
const { currentDate, defaultDate, selectDate } = useCalendar()
|
||||
const { selectedDate } = storeToRefs(useCalendar())
|
||||
const { currentEvents } = storeToRefs(useCalendarEvents())
|
||||
|
||||
const props = defineProps<{
|
||||
monthNumber: number
|
||||
dayNumber: number
|
||||
}>()
|
||||
|
||||
const tileDate: ComputedRef<LeimDate> = computed(() => {
|
||||
return {
|
||||
day: props.dayNumber,
|
||||
month: props.monthNumber,
|
||||
year: currentDate.currentYear
|
||||
}
|
||||
})
|
||||
|
||||
const isDefaultDate = computed<boolean>(() => {
|
||||
return areDatesIdentical(tileDate.value, defaultDate)
|
||||
})
|
||||
|
||||
const isSelectedDate = computed<boolean>(() => {
|
||||
return areDatesIdentical(tileDate.value, selectedDate.value)
|
||||
})
|
||||
|
||||
const hasAtLeastOneEvent = computed<boolean>(() => {
|
||||
return Boolean(
|
||||
currentEvents.value.find((currentEvent) => {
|
||||
return areDatesIdentical(currentEvent.startDate, tileDate.value)
|
||||
})
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="relative grid place-items-center aspect-square rounded-full border-2 border-transparent transition-colors after:content-[''] after:absolute after:top-1 after:right-1 after:w-[.3rem] after:h-[.3rem] after:rounded-full after:transition-colors"
|
||||
:class="{
|
||||
'hover:bg-slate-800 hover:border-slate-800': !isDefaultDate && !isSelectedDate,
|
||||
'bg-slate-800 hover:bg-slate-600 hover:border-slate-600': isDefaultDate && !isSelectedDate,
|
||||
'text-white bg-blue-500 hover:bg-blue-600 hover:border-blue-600': isSelectedDate,
|
||||
'after:bg-green-600': hasAtLeastOneEvent,
|
||||
'after:bg-slate-950': hasAtLeastOneEvent && isSelectedDate
|
||||
}"
|
||||
@click="selectDate(tileDate)"
|
||||
>
|
||||
<span ref="tileRef" class="text-slate-300 text-[.85em]">{{ dayNumber }}</span>
|
||||
</button>
|
||||
</template>
|
||||
37
components/calendar/state/yearly/Layout.vue
Normal file
37
components/calendar/state/yearly/Layout.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<script lang="ts" setup>
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { useThrottleFn } from '@vueuse/core'
|
||||
|
||||
import MonthTile from './MonthTile.vue'
|
||||
|
||||
const { staticConfig, decrementYear, incrementYear } = useCalendar()
|
||||
|
||||
function handleWheel(e: WheelEvent) {
|
||||
const isMovingUp = e.deltaY < 0
|
||||
if (isMovingUp) {
|
||||
moveCalendarLeft()
|
||||
} else {
|
||||
moveCalendarRight()
|
||||
}
|
||||
}
|
||||
|
||||
const moveCalendarLeft = useThrottleFn(() => {
|
||||
decrementYear()
|
||||
}, 100)
|
||||
|
||||
const moveCalendarRight = useThrottleFn(() => {
|
||||
incrementYear()
|
||||
}, 100)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container mt-[10vh] mb-auto" @wheel="handleWheel">
|
||||
<div ref="test" class="grid grid-cols-5 gap-x-8 gap-y-16">
|
||||
<MonthTile
|
||||
v-for="month in staticConfig.monthsPerYear"
|
||||
:key="month"
|
||||
:month-number="month - 1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
29
components/calendar/state/yearly/MonthTile.vue
Normal file
29
components/calendar/state/yearly/MonthTile.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts" setup>
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
|
||||
import DayTile from './DayTile.vue'
|
||||
|
||||
const { staticConfig, getMonthName } = useCalendar()
|
||||
|
||||
const props = defineProps<{
|
||||
monthNumber: number
|
||||
}>()
|
||||
|
||||
const tileMonthName: string = getMonthName(props.monthNumber)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="font-medium">
|
||||
{{ tileMonthName }}
|
||||
</div>
|
||||
<div class="grid grid-cols-7">
|
||||
<DayTile
|
||||
v-for="day in staticConfig.daysPerMonth"
|
||||
:key="day"
|
||||
:month-number="monthNumber"
|
||||
:day-number="day"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
16
components/ui/badge/Badge.vue
Normal file
16
components/ui/badge/Badge.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { type BadgeVariants, badgeVariants } from '.'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
variant?: BadgeVariants['variant']
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn(badgeVariants({ variant }), props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
25
components/ui/badge/index.ts
Normal file
25
components/ui/badge/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
|
||||
export { default as Badge } from './Badge.vue'
|
||||
|
||||
export const badgeVariants = cva(
|
||||
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
|
||||
secondary:
|
||||
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
destructive:
|
||||
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
||||
outline: 'text-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export type BadgeVariants = VariantProps<typeof badgeVariants>
|
||||
34
components/ui/button/Button.vue
Normal file
34
components/ui/button/Button.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { Primitive, type PrimitiveProps } from 'radix-vue'
|
||||
import { type ButtonVariants, buttonVariants } from '.'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface Props extends PrimitiveProps {
|
||||
variant?: ButtonVariants['variant']
|
||||
size?: ButtonVariants['size']
|
||||
class?: HTMLAttributes['class']
|
||||
searchSlash?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
as: 'button'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn(buttonVariants({ variant, size }), props.class, 'group')"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<span
|
||||
v-if="props.searchSlash"
|
||||
class="h-4 p-1 ml-1 grid place-items-center text-[0.7em] leading-none font-semibold text-slate-500 bg-slate-100 border-slate-300 border-[1px] rounded-[3px] shadow-sm group-hover:text-slate-600 group-hover:bg-slate-200 group-hover:border-slate-400 transition-colors"
|
||||
>
|
||||
<span class="-mt-[2px]">CTRL + :</span>
|
||||
</span>
|
||||
</Primitive>
|
||||
</template>
|
||||
32
components/ui/button/index.ts
Normal file
32
components/ui/button/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
|
||||
export { default as Button } from './Button.vue'
|
||||
|
||||
export const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-1 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline'
|
||||
},
|
||||
size: {
|
||||
default: 'h-10 px-4 py-2',
|
||||
xs: 'h-7 rounded-sm px-2 text-xs',
|
||||
sm: 'h-9 rounded-md px-3 text-sm',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
icon: 'h-10 w-10'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export type ButtonVariants = VariantProps<typeof buttonVariants>
|
||||
28
components/ui/card/Card.vue
Normal file
28
components/ui/card/Card.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
link?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn('rounded-lg border bg-card text-card-foreground shadow-sm transition-all', props.class, {
|
||||
'relative outline outline-2 outline-offset-4 outline-transparent hover:bg-sky-950 hover:-translate-y-[.2rem] focus-within:outline-sky-900':
|
||||
props.link
|
||||
})
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<NuxtLink
|
||||
v-if="props.link"
|
||||
:to="props.link"
|
||||
class="absolute inset-0 focus-visible:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
14
components/ui/card/CardContent.vue
Normal file
14
components/ui/card/CardContent.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('p-6 pt-0', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
14
components/ui/card/CardDescription.vue
Normal file
14
components/ui/card/CardDescription.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('text-sm text-muted-foreground', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
14
components/ui/card/CardFooter.vue
Normal file
14
components/ui/card/CardFooter.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('flex items-center p-6 pt-0', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
14
components/ui/card/CardHeader.vue
Normal file
14
components/ui/card/CardHeader.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('flex flex-col gap-y-1.5 p-6', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
18
components/ui/card/CardTitle.vue
Normal file
18
components/ui/card/CardTitle.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h3
|
||||
:class="
|
||||
cn('text-2xl font-semibold leading-none tracking-tight', props.class)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</h3>
|
||||
</template>
|
||||
6
components/ui/card/index.ts
Normal file
6
components/ui/card/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { default as Card } from './Card.vue'
|
||||
export { default as CardHeader } from './CardHeader.vue'
|
||||
export { default as CardTitle } from './CardTitle.vue'
|
||||
export { default as CardDescription } from './CardDescription.vue'
|
||||
export { default as CardContent } from './CardContent.vue'
|
||||
export { default as CardFooter } from './CardFooter.vue'
|
||||
35
components/ui/command/Command.vue
Normal file
35
components/ui/command/Command.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import type { ComboboxRootEmits, ComboboxRootProps } from 'radix-vue'
|
||||
import { ComboboxRoot, useForwardPropsEmits } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(defineProps<ComboboxRootProps & { class?: HTMLAttributes['class'] }>(), {
|
||||
open: true,
|
||||
modelValue: ''
|
||||
})
|
||||
|
||||
const emits = defineEmits<ComboboxRootEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxRoot
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</ComboboxRoot>
|
||||
</template>
|
||||
23
components/ui/command/CommandDialog.vue
Normal file
23
components/ui/command/CommandDialog.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { useForwardPropsEmits } from 'radix-vue'
|
||||
import type { DialogRootEmits, DialogRootProps } from 'radix-vue'
|
||||
import Command from './Command.vue'
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||
|
||||
const props = defineProps<DialogRootProps>()
|
||||
const emits = defineEmits<DialogRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-bind="forwarded">
|
||||
<DialogContent class="overflow-hidden p-0 shadow-lg">
|
||||
<Command
|
||||
class="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"
|
||||
>
|
||||
<slot />
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
20
components/ui/command/CommandEmpty.vue
Normal file
20
components/ui/command/CommandEmpty.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import type { ComboboxEmptyProps } from 'radix-vue'
|
||||
import { ComboboxEmpty } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<ComboboxEmptyProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxEmpty v-bind="delegatedProps" :class="cn('py-6 text-center text-sm', props.class)">
|
||||
<slot />
|
||||
</ComboboxEmpty>
|
||||
</template>
|
||||
36
components/ui/command/CommandGroup.vue
Normal file
36
components/ui/command/CommandGroup.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import type { ComboboxGroupProps } from 'radix-vue'
|
||||
import { ComboboxGroup, ComboboxLabel } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<
|
||||
ComboboxGroupProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
heading?: string
|
||||
}
|
||||
>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxGroup
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<ComboboxLabel v-if="heading" class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
|
||||
{{ heading }}
|
||||
</ComboboxLabel>
|
||||
<slot />
|
||||
</ComboboxGroup>
|
||||
</template>
|
||||
40
components/ui/command/CommandInput.vue
Normal file
40
components/ui/command/CommandInput.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { Search } from 'lucide-vue-next'
|
||||
import { ComboboxInput, type ComboboxInputProps, useForwardProps } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const props = defineProps<
|
||||
ComboboxInputProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center border-b px-3" cmdk-input-wrapper>
|
||||
<Search class="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<ComboboxInput
|
||||
v-bind="{ ...forwardedProps, ...$attrs }"
|
||||
auto-focus
|
||||
:class="
|
||||
cn(
|
||||
'flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
31
components/ui/command/CommandItem.vue
Normal file
31
components/ui/command/CommandItem.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import type { ComboboxItemEmits, ComboboxItemProps } from 'radix-vue'
|
||||
import { ComboboxItem, useForwardPropsEmits } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<ComboboxItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<ComboboxItemEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxItem
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</ComboboxItem>
|
||||
</template>
|
||||
33
components/ui/command/CommandList.vue
Normal file
33
components/ui/command/CommandList.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import type { ComboboxContentEmits, ComboboxContentProps } from 'radix-vue'
|
||||
import { ComboboxContent, useForwardPropsEmits } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<ComboboxContentProps & { class?: HTMLAttributes['class'] }>(),
|
||||
{
|
||||
dismissable: false
|
||||
}
|
||||
)
|
||||
const emits = defineEmits<ComboboxContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxContent
|
||||
v-bind="forwarded"
|
||||
:class="cn('max-h-[300px] overflow-y-auto overflow-x-hidden', props.class)"
|
||||
>
|
||||
<div role="presentation">
|
||||
<slot />
|
||||
</div>
|
||||
</ComboboxContent>
|
||||
</template>
|
||||
20
components/ui/command/CommandSeparator.vue
Normal file
20
components/ui/command/CommandSeparator.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import type { ComboboxSeparatorProps } from 'radix-vue'
|
||||
import { ComboboxSeparator } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<ComboboxSeparatorProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxSeparator v-bind="delegatedProps" :class="cn('-mx-1 h-px bg-border', props.class)">
|
||||
<slot />
|
||||
</ComboboxSeparator>
|
||||
</template>
|
||||
14
components/ui/command/CommandShortcut.vue
Normal file
14
components/ui/command/CommandShortcut.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :class="cn('ml-auto text-xs tracking-widest text-muted-foreground', props.class)">
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
9
components/ui/command/index.ts
Normal file
9
components/ui/command/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export { default as Command } from './Command.vue'
|
||||
export { default as CommandDialog } from './CommandDialog.vue'
|
||||
export { default as CommandEmpty } from './CommandEmpty.vue'
|
||||
export { default as CommandGroup } from './CommandGroup.vue'
|
||||
export { default as CommandInput } from './CommandInput.vue'
|
||||
export { default as CommandItem } from './CommandItem.vue'
|
||||
export { default as CommandList } from './CommandList.vue'
|
||||
export { default as CommandSeparator } from './CommandSeparator.vue'
|
||||
export { default as CommandShortcut } from './CommandShortcut.vue'
|
||||
19
components/ui/dialog/Dialog.vue
Normal file
19
components/ui/dialog/Dialog.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DialogRoot,
|
||||
type DialogRootEmits,
|
||||
type DialogRootProps,
|
||||
useForwardPropsEmits
|
||||
} from 'radix-vue'
|
||||
|
||||
const props = defineProps<DialogRootProps>()
|
||||
const emits = defineEmits<DialogRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</DialogRoot>
|
||||
</template>
|
||||
11
components/ui/dialog/DialogClose.vue
Normal file
11
components/ui/dialog/DialogClose.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogClose, type DialogCloseProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<DialogCloseProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogClose v-bind="props">
|
||||
<slot />
|
||||
</DialogClose>
|
||||
</template>
|
||||
51
components/ui/dialog/DialogContent.vue
Normal file
51
components/ui/dialog/DialogContent.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
type DialogContentEmits,
|
||||
type DialogContentProps,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits
|
||||
} from 'radix-vue'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<DialogContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
||||
/>
|
||||
<DialogContent
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'fixed left-1/2 top-1/2 z-50 grid w-[calc(100%-2rem)] max-w-4xl -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<DialogClose
|
||||
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
|
||||
>
|
||||
<X class="w-4 h-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</template>
|
||||
24
components/ui/dialog/DialogDescription.vue
Normal file
24
components/ui/dialog/DialogDescription.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { DialogDescription, type DialogDescriptionProps, useForwardProps } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogDescription
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('text-sm text-muted-foreground', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DialogDescription>
|
||||
</template>
|
||||
12
components/ui/dialog/DialogFooter.vue
Normal file
12
components/ui/dialog/DialogFooter.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
14
components/ui/dialog/DialogHeader.vue
Normal file
14
components/ui/dialog/DialogHeader.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('flex flex-col gap-y-1.5 text-center sm:text-left', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
64
components/ui/dialog/DialogScrollContent.vue
Normal file
64
components/ui/dialog/DialogScrollContent.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
type DialogContentEmits,
|
||||
type DialogContentProps,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits
|
||||
} from 'radix-vue'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<DialogContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
||||
>
|
||||
<DialogContent
|
||||
:class="
|
||||
cn(
|
||||
'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
v-bind="forwarded"
|
||||
@pointer-down-outside="
|
||||
(event) => {
|
||||
const originalEvent = event.detail.originalEvent
|
||||
const target = originalEvent.target as HTMLElement
|
||||
if (
|
||||
originalEvent.offsetX > target.clientWidth ||
|
||||
originalEvent.offsetY > target.clientHeight
|
||||
) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<DialogClose
|
||||
class="absolute top-3 right-3 p-0.5 transition-colors rounded-md hover:bg-secondary"
|
||||
>
|
||||
<X class="w-4 h-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogOverlay>
|
||||
</DialogPortal>
|
||||
</template>
|
||||
24
components/ui/dialog/DialogTitle.vue
Normal file
24
components/ui/dialog/DialogTitle.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { DialogTitle, type DialogTitleProps, useForwardProps } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogTitle
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('text-2xl font-semibold leading-none tracking-tight', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DialogTitle>
|
||||
</template>
|
||||
11
components/ui/dialog/DialogTrigger.vue
Normal file
11
components/ui/dialog/DialogTrigger.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogTrigger, type DialogTriggerProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<DialogTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogTrigger v-bind="props">
|
||||
<slot />
|
||||
</DialogTrigger>
|
||||
</template>
|
||||
9
components/ui/dialog/index.ts
Normal file
9
components/ui/dialog/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export { default as Dialog } from './Dialog.vue'
|
||||
export { default as DialogClose } from './DialogClose.vue'
|
||||
export { default as DialogTrigger } from './DialogTrigger.vue'
|
||||
export { default as DialogHeader } from './DialogHeader.vue'
|
||||
export { default as DialogTitle } from './DialogTitle.vue'
|
||||
export { default as DialogDescription } from './DialogDescription.vue'
|
||||
export { default as DialogContent } from './DialogContent.vue'
|
||||
export { default as DialogScrollContent } from './DialogScrollContent.vue'
|
||||
export { default as DialogFooter } from './DialogFooter.vue'
|
||||
19
components/ui/dropdown-menu/DropdownMenu.vue
Normal file
19
components/ui/dropdown-menu/DropdownMenu.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DropdownMenuRoot,
|
||||
type DropdownMenuRootEmits,
|
||||
type DropdownMenuRootProps,
|
||||
useForwardPropsEmits
|
||||
} from 'radix-vue'
|
||||
|
||||
const props = defineProps<DropdownMenuRootProps>()
|
||||
const emits = defineEmits<DropdownMenuRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</DropdownMenuRoot>
|
||||
</template>
|
||||
42
components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
Normal file
42
components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import {
|
||||
DropdownMenuCheckboxItem,
|
||||
type DropdownMenuCheckboxItemEmits,
|
||||
type DropdownMenuCheckboxItemProps,
|
||||
DropdownMenuItemIndicator,
|
||||
useForwardPropsEmits
|
||||
} from 'radix-vue'
|
||||
import { Check } from 'lucide-vue-next'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuCheckboxItem
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuItemIndicator>
|
||||
<Check class="w-4 h-4" />
|
||||
</DropdownMenuItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuCheckboxItem>
|
||||
</template>
|
||||
43
components/ui/dropdown-menu/DropdownMenuContent.vue
Normal file
43
components/ui/dropdown-menu/DropdownMenuContent.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import {
|
||||
DropdownMenuContent,
|
||||
type DropdownMenuContentEmits,
|
||||
type DropdownMenuContentProps,
|
||||
DropdownMenuPortal,
|
||||
useForwardPropsEmits
|
||||
} from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes['class'] }>(),
|
||||
{
|
||||
sideOffset: 4
|
||||
}
|
||||
)
|
||||
const emits = defineEmits<DropdownMenuContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</template>
|
||||
11
components/ui/dropdown-menu/DropdownMenuGroup.vue
Normal file
11
components/ui/dropdown-menu/DropdownMenuGroup.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { DropdownMenuGroup, type DropdownMenuGroupProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<DropdownMenuGroupProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuGroup v-bind="props">
|
||||
<slot />
|
||||
</DropdownMenuGroup>
|
||||
</template>
|
||||
32
components/ui/dropdown-menu/DropdownMenuItem.vue
Normal file
32
components/ui/dropdown-menu/DropdownMenuItem.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { DropdownMenuItem, type DropdownMenuItemProps, useForwardProps } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuItemProps & { class?: HTMLAttributes['class']; inset?: boolean }
|
||||
>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuItem
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
inset && 'pl-8',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
26
components/ui/dropdown-menu/DropdownMenuLabel.vue
Normal file
26
components/ui/dropdown-menu/DropdownMenuLabel.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { DropdownMenuLabel, type DropdownMenuLabelProps, useForwardProps } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuLabelProps & { class?: HTMLAttributes['class']; inset?: boolean }
|
||||
>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuLabel
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuLabel>
|
||||
</template>
|
||||
19
components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
Normal file
19
components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DropdownMenuRadioGroup,
|
||||
type DropdownMenuRadioGroupEmits,
|
||||
type DropdownMenuRadioGroupProps,
|
||||
useForwardPropsEmits
|
||||
} from 'radix-vue'
|
||||
|
||||
const props = defineProps<DropdownMenuRadioGroupProps>()
|
||||
const emits = defineEmits<DropdownMenuRadioGroupEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRadioGroup v-bind="forwarded">
|
||||
<slot />
|
||||
</DropdownMenuRadioGroup>
|
||||
</template>
|
||||
43
components/ui/dropdown-menu/DropdownMenuRadioItem.vue
Normal file
43
components/ui/dropdown-menu/DropdownMenuRadioItem.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import {
|
||||
DropdownMenuItemIndicator,
|
||||
DropdownMenuRadioItem,
|
||||
type DropdownMenuRadioItemEmits,
|
||||
type DropdownMenuRadioItemProps,
|
||||
useForwardPropsEmits
|
||||
} from 'radix-vue'
|
||||
import { Circle } from 'lucide-vue-next'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const emits = defineEmits<DropdownMenuRadioItemEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRadioItem
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuItemIndicator>
|
||||
<Circle class="h-2 w-2 fill-current" />
|
||||
</DropdownMenuItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuRadioItem>
|
||||
</template>
|
||||
24
components/ui/dropdown-menu/DropdownMenuSeparator.vue
Normal file
24
components/ui/dropdown-menu/DropdownMenuSeparator.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { DropdownMenuSeparator, type DropdownMenuSeparatorProps } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuSeparatorProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSeparator
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('-mx-1 my-1 h-px bg-muted', props.class)"
|
||||
/>
|
||||
</template>
|
||||
14
components/ui/dropdown-menu/DropdownMenuShortcut.vue
Normal file
14
components/ui/dropdown-menu/DropdownMenuShortcut.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :class="cn('ml-auto text-xs tracking-widest opacity-60', props.class)">
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
19
components/ui/dropdown-menu/DropdownMenuSub.vue
Normal file
19
components/ui/dropdown-menu/DropdownMenuSub.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DropdownMenuSub,
|
||||
type DropdownMenuSubEmits,
|
||||
type DropdownMenuSubProps,
|
||||
useForwardPropsEmits
|
||||
} from 'radix-vue'
|
||||
|
||||
const props = defineProps<DropdownMenuSubProps>()
|
||||
const emits = defineEmits<DropdownMenuSubEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSub v-bind="forwarded">
|
||||
<slot />
|
||||
</DropdownMenuSub>
|
||||
</template>
|
||||
35
components/ui/dropdown-menu/DropdownMenuSubContent.vue
Normal file
35
components/ui/dropdown-menu/DropdownMenuSubContent.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import {
|
||||
DropdownMenuSubContent,
|
||||
type DropdownMenuSubContentEmits,
|
||||
type DropdownMenuSubContentProps,
|
||||
useForwardPropsEmits
|
||||
} from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<DropdownMenuSubContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSubContent
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuSubContent>
|
||||
</template>
|
||||
35
components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
Normal file
35
components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import {
|
||||
DropdownMenuSubTrigger,
|
||||
type DropdownMenuSubTriggerProps,
|
||||
useForwardProps
|
||||
} from 'radix-vue'
|
||||
import { ChevronRight } from 'lucide-vue-next'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSubTrigger
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
<ChevronRight class="ml-auto h-4 w-4" />
|
||||
</DropdownMenuSubTrigger>
|
||||
</template>
|
||||
13
components/ui/dropdown-menu/DropdownMenuTrigger.vue
Normal file
13
components/ui/dropdown-menu/DropdownMenuTrigger.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { DropdownMenuTrigger, type DropdownMenuTriggerProps, useForwardProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<DropdownMenuTriggerProps>()
|
||||
|
||||
const forwardedProps = useForwardProps(props)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuTrigger class="outline-none" v-bind="forwardedProps">
|
||||
<slot />
|
||||
</DropdownMenuTrigger>
|
||||
</template>
|
||||
16
components/ui/dropdown-menu/index.ts
Normal file
16
components/ui/dropdown-menu/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export { DropdownMenuPortal } from 'radix-vue'
|
||||
|
||||
export { default as DropdownMenu } from './DropdownMenu.vue'
|
||||
export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue'
|
||||
export { default as DropdownMenuContent } from './DropdownMenuContent.vue'
|
||||
export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue'
|
||||
export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue'
|
||||
export { default as DropdownMenuItem } from './DropdownMenuItem.vue'
|
||||
export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue'
|
||||
export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue'
|
||||
export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue'
|
||||
export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue'
|
||||
export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue'
|
||||
export { default as DropdownMenuSub } from './DropdownMenuSub.vue'
|
||||
export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue'
|
||||
export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue'
|
||||
32
components/ui/input/Input.vue
Normal file
32
components/ui/input/Input.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
defaultValue?: string | number
|
||||
modelValue?: string | number
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'update:modelValue', payload: string | number): void
|
||||
}>()
|
||||
|
||||
const modelValue = useVModel(props, 'modelValue', emits, {
|
||||
passive: true,
|
||||
defaultValue: props.defaultValue
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
v-model="modelValue"
|
||||
:class="
|
||||
cn(
|
||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
1
components/ui/input/index.ts
Normal file
1
components/ui/input/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Input } from './Input.vue'
|
||||
25
components/ui/pagination/PaginationEllipsis.vue
Normal file
25
components/ui/pagination/PaginationEllipsis.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { PaginationEllipsis, type PaginationEllipsisProps } from 'radix-vue'
|
||||
import { MoreHorizontal } from 'lucide-vue-next'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<PaginationEllipsisProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationEllipsis
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('w-9 h-9 flex items-center justify-center', props.class)"
|
||||
>
|
||||
<slot>
|
||||
<MoreHorizontal />
|
||||
</slot>
|
||||
</PaginationEllipsis>
|
||||
</template>
|
||||
30
components/ui/pagination/PaginationFirst.vue
Normal file
30
components/ui/pagination/PaginationFirst.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { PaginationFirst, type PaginationFirstProps } from 'radix-vue'
|
||||
import { ChevronsLeft } from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<PaginationFirstProps & { class?: HTMLAttributes['class'] }>(),
|
||||
{
|
||||
asChild: true
|
||||
}
|
||||
)
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationFirst v-bind="delegatedProps">
|
||||
<Button :class="cn('w-10 h-10 p-0', props.class)" variant="outline">
|
||||
<slot>
|
||||
<ChevronsLeft class="h-4 w-4" />
|
||||
</slot>
|
||||
</Button>
|
||||
</PaginationFirst>
|
||||
</template>
|
||||
30
components/ui/pagination/PaginationLast.vue
Normal file
30
components/ui/pagination/PaginationLast.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { PaginationLast, type PaginationLastProps } from 'radix-vue'
|
||||
import { ChevronsRight } from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<PaginationLastProps & { class?: HTMLAttributes['class'] }>(),
|
||||
{
|
||||
asChild: true
|
||||
}
|
||||
)
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationLast v-bind="delegatedProps">
|
||||
<Button :class="cn('w-10 h-10 p-0', props.class)" variant="outline">
|
||||
<slot>
|
||||
<ChevronsRight class="h-4 w-4" />
|
||||
</slot>
|
||||
</Button>
|
||||
</PaginationLast>
|
||||
</template>
|
||||
30
components/ui/pagination/PaginationNext.vue
Normal file
30
components/ui/pagination/PaginationNext.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { PaginationNext, type PaginationNextProps } from 'radix-vue'
|
||||
import { ChevronRight } from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<PaginationNextProps & { class?: HTMLAttributes['class'] }>(),
|
||||
{
|
||||
asChild: true
|
||||
}
|
||||
)
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationNext v-bind="delegatedProps">
|
||||
<Button :class="cn('w-10 h-10 p-0', props.class)" variant="outline">
|
||||
<slot>
|
||||
<ChevronRight class="h-4 w-4" />
|
||||
</slot>
|
||||
</Button>
|
||||
</PaginationNext>
|
||||
</template>
|
||||
30
components/ui/pagination/PaginationPrev.vue
Normal file
30
components/ui/pagination/PaginationPrev.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { PaginationPrev, type PaginationPrevProps } from 'radix-vue'
|
||||
import { ChevronLeft } from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<PaginationPrevProps & { class?: HTMLAttributes['class'] }>(),
|
||||
{
|
||||
asChild: true
|
||||
}
|
||||
)
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationPrev v-bind="delegatedProps">
|
||||
<Button :class="cn('w-10 h-10 p-0', props.class)" variant="outline">
|
||||
<slot>
|
||||
<ChevronLeft class="h-4 w-4" />
|
||||
</slot>
|
||||
</Button>
|
||||
</PaginationPrev>
|
||||
</template>
|
||||
6
components/ui/pagination/index.ts
Normal file
6
components/ui/pagination/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { PaginationRoot as Pagination, PaginationList, PaginationListItem } from 'radix-vue'
|
||||
export { default as PaginationEllipsis } from './PaginationEllipsis.vue'
|
||||
export { default as PaginationFirst } from './PaginationFirst.vue'
|
||||
export { default as PaginationLast } from './PaginationLast.vue'
|
||||
export { default as PaginationNext } from './PaginationNext.vue'
|
||||
export { default as PaginationPrev } from './PaginationPrev.vue'
|
||||
15
components/ui/popover/Popover.vue
Normal file
15
components/ui/popover/Popover.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { PopoverRoot, useForwardPropsEmits } from 'radix-vue'
|
||||
import type { PopoverRootEmits, PopoverRootProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<PopoverRootProps>()
|
||||
const emits = defineEmits<PopoverRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</PopoverRoot>
|
||||
</template>
|
||||
48
components/ui/popover/PopoverContent.vue
Normal file
48
components/ui/popover/PopoverContent.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import {
|
||||
PopoverContent,
|
||||
type PopoverContentEmits,
|
||||
type PopoverContentProps,
|
||||
PopoverPortal,
|
||||
useForwardPropsEmits
|
||||
} from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<PopoverContentProps & { class?: HTMLAttributes['class'] }>(),
|
||||
{
|
||||
align: 'center',
|
||||
sideOffset: 4
|
||||
}
|
||||
)
|
||||
const emits = defineEmits<PopoverContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverPortal>
|
||||
<PopoverContent
|
||||
v-bind="{ ...forwarded, ...$attrs }"
|
||||
:class="
|
||||
cn(
|
||||
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</PopoverContent>
|
||||
</PopoverPortal>
|
||||
</template>
|
||||
11
components/ui/popover/PopoverTrigger.vue
Normal file
11
components/ui/popover/PopoverTrigger.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { PopoverTrigger, type PopoverTriggerProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<PopoverTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverTrigger v-bind="props">
|
||||
<slot />
|
||||
</PopoverTrigger>
|
||||
</template>
|
||||
3
components/ui/popover/index.ts
Normal file
3
components/ui/popover/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as Popover } from './Popover.vue'
|
||||
export { default as PopoverTrigger } from './PopoverTrigger.vue'
|
||||
export { default as PopoverContent } from './PopoverContent.vue'
|
||||
15
components/ui/select/Select.vue
Normal file
15
components/ui/select/Select.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectRootEmits, SelectRootProps } from 'radix-vue'
|
||||
import { SelectRoot, useForwardPropsEmits } from 'radix-vue'
|
||||
|
||||
const props = defineProps<SelectRootProps>()
|
||||
const emits = defineEmits<SelectRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</SelectRoot>
|
||||
</template>
|
||||
63
components/ui/select/SelectContent.vue
Normal file
63
components/ui/select/SelectContent.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import {
|
||||
SelectContent,
|
||||
type SelectContentEmits,
|
||||
type SelectContentProps,
|
||||
SelectPortal,
|
||||
SelectViewport,
|
||||
useForwardPropsEmits
|
||||
} from 'radix-vue'
|
||||
import { SelectScrollDownButton, SelectScrollUpButton } from '.'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<SelectContentProps & { class?: HTMLAttributes['class'] }>(),
|
||||
{
|
||||
position: 'popper'
|
||||
}
|
||||
)
|
||||
const emits = defineEmits<SelectContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectPortal>
|
||||
<SelectContent
|
||||
v-bind="{ ...forwarded, ...$attrs }"
|
||||
:class="
|
||||
cn(
|
||||
'relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
position === 'popper' &&
|
||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectViewport
|
||||
:class="
|
||||
cn(
|
||||
'p-1',
|
||||
position === 'popper' &&
|
||||
'h-[--radix-select-trigger-height] w-full min-w-[--radix-select-trigger-width]'
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</SelectViewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectContent>
|
||||
</SelectPortal>
|
||||
</template>
|
||||
19
components/ui/select/SelectGroup.vue
Normal file
19
components/ui/select/SelectGroup.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { SelectGroup, type SelectGroupProps } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<SelectGroupProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectGroup :class="cn('p-1 w-full', props.class)" v-bind="delegatedProps">
|
||||
<slot />
|
||||
</SelectGroup>
|
||||
</template>
|
||||
44
components/ui/select/SelectItem.vue
Normal file
44
components/ui/select/SelectItem.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import {
|
||||
SelectItem,
|
||||
SelectItemIndicator,
|
||||
type SelectItemProps,
|
||||
SelectItemText,
|
||||
useForwardProps
|
||||
} from 'radix-vue'
|
||||
import { Check } from 'lucide-vue-next'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<SelectItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectItem
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectItemIndicator>
|
||||
<Check class="h-4 w-4" />
|
||||
</SelectItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectItemText>
|
||||
<slot />
|
||||
</SelectItemText>
|
||||
</SelectItem>
|
||||
</template>
|
||||
11
components/ui/select/SelectItemText.vue
Normal file
11
components/ui/select/SelectItemText.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { SelectItemText, type SelectItemTextProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<SelectItemTextProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectItemText v-bind="props">
|
||||
<slot />
|
||||
</SelectItemText>
|
||||
</template>
|
||||
13
components/ui/select/SelectLabel.vue
Normal file
13
components/ui/select/SelectLabel.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { SelectLabel, type SelectLabelProps } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<SelectLabelProps & { class?: HTMLAttributes['class'] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectLabel :class="cn('py-1.5 pl-8 pr-2 text-sm font-semibold', props.class)">
|
||||
<slot />
|
||||
</SelectLabel>
|
||||
</template>
|
||||
31
components/ui/select/SelectScrollDownButton.vue
Normal file
31
components/ui/select/SelectScrollDownButton.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import {
|
||||
SelectScrollDownButton,
|
||||
type SelectScrollDownButtonProps,
|
||||
useForwardProps
|
||||
} from 'radix-vue'
|
||||
import { ChevronDown } from 'lucide-vue-next'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<SelectScrollDownButtonProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectScrollDownButton
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('flex cursor-default items-center justify-center py-1', props.class)"
|
||||
>
|
||||
<slot>
|
||||
<ChevronDown class="h-4 w-4" />
|
||||
</slot>
|
||||
</SelectScrollDownButton>
|
||||
</template>
|
||||
27
components/ui/select/SelectScrollUpButton.vue
Normal file
27
components/ui/select/SelectScrollUpButton.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { SelectScrollUpButton, type SelectScrollUpButtonProps, useForwardProps } from 'radix-vue'
|
||||
import { ChevronUp } from 'lucide-vue-next'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<SelectScrollUpButtonProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectScrollUpButton
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('flex cursor-default items-center justify-center py-1', props.class)"
|
||||
>
|
||||
<slot>
|
||||
<ChevronUp class="h-4 w-4" />
|
||||
</slot>
|
||||
</SelectScrollUpButton>
|
||||
</template>
|
||||
17
components/ui/select/SelectSeparator.vue
Normal file
17
components/ui/select/SelectSeparator.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { SelectSeparator, type SelectSeparatorProps } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<SelectSeparatorProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectSeparator v-bind="delegatedProps" :class="cn('-mx-1 my-1 h-px bg-muted', props.class)" />
|
||||
</template>
|
||||
33
components/ui/select/SelectTrigger.vue
Normal file
33
components/ui/select/SelectTrigger.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { SelectIcon, SelectTrigger, type SelectTriggerProps, useForwardProps } from 'radix-vue'
|
||||
import { ChevronDown } from 'lucide-vue-next'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<SelectTriggerProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectTrigger
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
<SelectIcon as-child>
|
||||
<ChevronDown class="w-4 h-4 opacity-50" />
|
||||
</SelectIcon>
|
||||
</SelectTrigger>
|
||||
</template>
|
||||
11
components/ui/select/SelectValue.vue
Normal file
11
components/ui/select/SelectValue.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { SelectValue, type SelectValueProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<SelectValueProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectValue v-bind="props">
|
||||
<slot />
|
||||
</SelectValue>
|
||||
</template>
|
||||
11
components/ui/select/index.ts
Normal file
11
components/ui/select/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export { default as Select } from './Select.vue'
|
||||
export { default as SelectValue } from './SelectValue.vue'
|
||||
export { default as SelectTrigger } from './SelectTrigger.vue'
|
||||
export { default as SelectContent } from './SelectContent.vue'
|
||||
export { default as SelectGroup } from './SelectGroup.vue'
|
||||
export { default as SelectItem } from './SelectItem.vue'
|
||||
export { default as SelectItemText } from './SelectItemText.vue'
|
||||
export { default as SelectLabel } from './SelectLabel.vue'
|
||||
export { default as SelectSeparator } from './SelectSeparator.vue'
|
||||
export { default as SelectScrollUpButton } from './SelectScrollUpButton.vue'
|
||||
export { default as SelectScrollDownButton } from './SelectScrollDownButton.vue'
|
||||
15
components/ui/tabs/Tabs.vue
Normal file
15
components/ui/tabs/Tabs.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { TabsRoot, useForwardPropsEmits } from 'radix-vue'
|
||||
import type { TabsRootEmits, TabsRootProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<TabsRootProps>()
|
||||
const emits = defineEmits<TabsRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabsRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</TabsRoot>
|
||||
</template>
|
||||
27
components/ui/tabs/TabsContent.vue
Normal file
27
components/ui/tabs/TabsContent.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { TabsContent, type TabsContentProps } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<TabsContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabsContent
|
||||
:class="
|
||||
cn(
|
||||
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
v-bind="delegatedProps"
|
||||
>
|
||||
<slot />
|
||||
</TabsContent>
|
||||
</template>
|
||||
27
components/ui/tabs/TabsList.vue
Normal file
27
components/ui/tabs/TabsList.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { TabsList, type TabsListProps } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<TabsListProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabsList
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</TabsList>
|
||||
</template>
|
||||
29
components/ui/tabs/TabsTrigger.vue
Normal file
29
components/ui/tabs/TabsTrigger.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { TabsTrigger, type TabsTriggerProps, useForwardProps } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<TabsTriggerProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabsTrigger
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</TabsTrigger>
|
||||
</template>
|
||||
4
components/ui/tabs/index.ts
Normal file
4
components/ui/tabs/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as Tabs } from './Tabs.vue'
|
||||
export { default as TabsTrigger } from './TabsTrigger.vue'
|
||||
export { default as TabsList } from './TabsList.vue'
|
||||
export { default as TabsContent } from './TabsContent.vue'
|
||||
35
components/ui/tags-input/TagsInput.vue
Normal file
35
components/ui/tags-input/TagsInput.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import {
|
||||
TagsInputRoot,
|
||||
type TagsInputRootEmits,
|
||||
type TagsInputRootProps,
|
||||
useForwardPropsEmits
|
||||
} from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<TagsInputRootProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<TagsInputRootEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TagsInputRoot
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'flex flex-wrap gap-2 items-center rounded-md border border-input bg-background px-3 py-2 text-sm',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</TagsInputRoot>
|
||||
</template>
|
||||
22
components/ui/tags-input/TagsInputInput.vue
Normal file
22
components/ui/tags-input/TagsInputInput.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { TagsInputInput, type TagsInputInputProps, useForwardProps } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<TagsInputInputProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TagsInputInput
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('text-sm min-h-6 focus:outline-none flex-1 bg-transparent px-1', props.class)"
|
||||
/>
|
||||
</template>
|
||||
30
components/ui/tags-input/TagsInputItem.vue
Normal file
30
components/ui/tags-input/TagsInputItem.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { TagsInputItem, type TagsInputItemProps, useForwardProps } from 'radix-vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<TagsInputItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TagsInputItem
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'flex h-6 items-center rounded bg-secondary data-[state=active]:ring-ring data-[state=active]:ring-2 data-[state=active]:ring-offset-2 ring-offset-background',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</TagsInputItem>
|
||||
</template>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user