Merge branch 'dev'
This commit is contained in:
@@ -23,7 +23,7 @@ module.exports = {
|
|||||||
ecmaVersion: 'latest'
|
ecmaVersion: 'latest'
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
indent: ['error', 2],
|
indent: ['error', 2, { SwitchCase: 1 }],
|
||||||
'prettier/prettier': [
|
'prettier/prettier': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
|
|||||||
16
components.json
Normal file
16
components.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://shadcn-vue.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"typescript": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "src/assets/main.css",
|
||||||
|
"baseColor": "slate",
|
||||||
|
"cssVariables": true
|
||||||
|
},
|
||||||
|
"framework": "vite",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils"
|
||||||
|
}
|
||||||
|
}
|
||||||
378
package-lock.json
generated
378
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -17,8 +17,14 @@
|
|||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@phosphor-icons/vue": "^2.1.6",
|
||||||
"@vueuse/core": "^10.9.0",
|
"@vueuse/core": "^10.9.0",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.1.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
|
"radix-vue": "^1.6.1",
|
||||||
|
"tailwind-merge": "^2.2.2",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vue-router": "^4.3.0"
|
"vue-router": "^4.3.0"
|
||||||
},
|
},
|
||||||
@@ -38,6 +44,7 @@
|
|||||||
"npm-run-all2": "^6.1.2",
|
"npm-run-all2": "^6.1.2",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
|
"sass": "^1.72.0",
|
||||||
"start-server-and-test": "^2.0.3",
|
"start-server-and-test": "^2.0.3",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
"typescript": "~5.4.0",
|
"typescript": "~5.4.0",
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useDark } from '@vueuse/core'
|
||||||
import { RouterView } from 'vue-router'
|
import { RouterView } from 'vue-router'
|
||||||
|
|
||||||
|
const dark = useDark()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RouterView />
|
<div class="h-screen">
|
||||||
|
<RouterView />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,3 +1,78 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
|
||||||
|
--primary: 222.2 47.4% 11.2%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--secondary: 210 40% 96.1%;
|
||||||
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--accent: 210 40% 96.1%;
|
||||||
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--ring: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
|
||||||
|
--primary: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--ring: 212.7 26.8% 83.9%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
/* color palette from <https://github.com/vuejs/theme> */
|
|
||||||
:root {
|
|
||||||
--vt-c-white: #ffffff;
|
|
||||||
--vt-c-white-soft: #f8f8f8;
|
|
||||||
--vt-c-white-mute: #f2f2f2;
|
|
||||||
|
|
||||||
--vt-c-black: #181818;
|
|
||||||
--vt-c-black-soft: #222222;
|
|
||||||
--vt-c-black-mute: #282828;
|
|
||||||
|
|
||||||
--vt-c-indigo: #2c3e50;
|
|
||||||
|
|
||||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
|
||||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
|
||||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
|
||||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
|
||||||
|
|
||||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
|
||||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
|
||||||
--vt-c-text-dark-1: var(--vt-c-white);
|
|
||||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* semantic color variables for this project */
|
|
||||||
:root {
|
|
||||||
--color-background: var(--vt-c-white);
|
|
||||||
--color-background-soft: var(--vt-c-white-soft);
|
|
||||||
--color-background-mute: var(--vt-c-white-mute);
|
|
||||||
|
|
||||||
--color-border: var(--vt-c-divider-light-2);
|
|
||||||
--color-border-hover: var(--vt-c-divider-light-1);
|
|
||||||
|
|
||||||
--color-heading: var(--vt-c-text-light-1);
|
|
||||||
--color-text: var(--vt-c-text-light-1);
|
|
||||||
|
|
||||||
--section-gap: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--color-background: var(--vt-c-black);
|
|
||||||
--color-background-soft: var(--vt-c-black-soft);
|
|
||||||
--color-background-mute: var(--vt-c-black-mute);
|
|
||||||
|
|
||||||
--color-border: var(--vt-c-divider-dark-2);
|
|
||||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
|
||||||
|
|
||||||
--color-heading: var(--vt-c-text-dark-1);
|
|
||||||
--color-text: var(--vt-c-text-dark-2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
min-height: 100vh;
|
|
||||||
color: var(--color-text);
|
|
||||||
background: var(--color-background);
|
|
||||||
transition:
|
|
||||||
color 0.5s,
|
|
||||||
background-color 0.5s;
|
|
||||||
line-height: 1.6;
|
|
||||||
font-family:
|
|
||||||
Inter,
|
|
||||||
-apple-system,
|
|
||||||
BlinkMacSystemFont,
|
|
||||||
'Segoe UI',
|
|
||||||
Roboto,
|
|
||||||
Oxygen,
|
|
||||||
Ubuntu,
|
|
||||||
Cantarell,
|
|
||||||
'Fira Sans',
|
|
||||||
'Droid Sans',
|
|
||||||
'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
font-size: 15px;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
35
src/components/calendar/Calendar.vue
Normal file
35
src/components/calendar/Calendar.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useCalendar } from '@/stores/CalendarStore'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import CalendarMenu from './CalendarMenu.vue'
|
||||||
|
import Century from './state/Century.vue'
|
||||||
|
import Decade from './state/Decade.vue'
|
||||||
|
import Monthly from './state/Monthly.vue'
|
||||||
|
import Year from './state/Year.vue'
|
||||||
|
|
||||||
|
const { currentConfig } = useCalendar()
|
||||||
|
|
||||||
|
const currentViewComponent = computed(() => {
|
||||||
|
switch (currentConfig.viewType) {
|
||||||
|
case 'month':
|
||||||
|
return Monthly
|
||||||
|
|
||||||
|
case 'year':
|
||||||
|
return Year
|
||||||
|
|
||||||
|
case 'decade':
|
||||||
|
return Decade
|
||||||
|
|
||||||
|
case 'century':
|
||||||
|
default:
|
||||||
|
return Century
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-full grid grid-rows-[auto,1fr]">
|
||||||
|
<CalendarMenu />
|
||||||
|
<component :is="currentViewComponent" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
38
src/components/calendar/CalendarEvent.vue
Normal file
38
src/components/calendar/CalendarEvent.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Popover, PopoverTrigger } from '@/components/ui/popover'
|
||||||
|
import type { CalendarEvent } from '@/models/Events'
|
||||||
|
|
||||||
|
import CalendarEventDetails from './CalendarEventDetails.vue'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
event: CalendarEvent
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<button
|
||||||
|
class="text-xs px-2 py-1 block w-full text-left rounded-sm whitespace-nowrap overflow-hidden text-ellipsis"
|
||||||
|
: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-yellow-100 hover:bg-yellow-200': event.category === 'bénédiction'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ event.title }}
|
||||||
|
</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
|
||||||
|
<CalendarEventDetails :event />
|
||||||
|
</Popover>
|
||||||
|
</template>
|
||||||
53
src/components/calendar/CalendarEventDetails.vue
Normal file
53
src/components/calendar/CalendarEventDetails.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { getRelativeString } from '@/models/Date'
|
||||||
|
import type { CalendarEvent } from '@/models/Events'
|
||||||
|
import { useCalendar } from '@/stores/CalendarStore'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import { PhHourglassMedium } from '@phosphor-icons/vue'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { PopoverContent } from '@/components/ui/popover'
|
||||||
|
|
||||||
|
const { defaultDate, getFormattedDateTitle } = useCalendar()
|
||||||
|
|
||||||
|
const props = defineProps<{ event: CalendarEvent }>()
|
||||||
|
|
||||||
|
const dateDifference = computed(() => getRelativeString(defaultDate, props.event.date))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PopoverContent class="w-96" align="start" :align-offset="50" :collision-padding="20">
|
||||||
|
<div class="grid gap-1">
|
||||||
|
<div class="text-lg font-semibold">
|
||||||
|
{{ event.title }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-1 space-y-1">
|
||||||
|
<p class="font-semibold">{{ getFormattedDateTitle(event.date, true) }}</p>
|
||||||
|
<p class="text-sm italic opacity-75 flex items-center gap-1">
|
||||||
|
<PhHourglassMedium size="16" weight="fill" /> {{ dateDifference }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="event.category || event.secondaryCategories">
|
||||||
|
<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 class="mt-2 italic text-sm text-slate-500">
|
||||||
|
{{ event.description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</template>
|
||||||
61
src/components/calendar/CalendarMenu.vue
Normal file
61
src/components/calendar/CalendarMenu.vue
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<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'
|
||||||
|
import CalendarMenuNav from './CalendarMenuNav.vue'
|
||||||
|
import CalendarMenuToday from './CalendarMenuToday.vue'
|
||||||
|
import CalendarSwitch from './CalendarSwitch.vue'
|
||||||
|
import CalendarSearch from './search/CalendarSearch.vue'
|
||||||
|
|
||||||
|
const { defaultDate, getFormattedDateTitle, currentDate } = useCalendar()
|
||||||
|
const { selectedDate } = storeToRefs(useCalendar())
|
||||||
|
|
||||||
|
const mainDateTitle = computed(() => getFormattedDateTitle(selectedDate.value, true))
|
||||||
|
|
||||||
|
const dateDifference = computed(() => getRelativeString(defaultDate, selectedDate.value))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header class="border-slate-700 border-b-[1px]">
|
||||||
|
<div class="pt-4 container">
|
||||||
|
<div class="grid md:grid-cols-12 items-center">
|
||||||
|
<div class="md:col-span-9">
|
||||||
|
<div class="flex items-center gap-6">
|
||||||
|
<menu class="flex items-center gap-2">
|
||||||
|
<li>
|
||||||
|
<CalendarSwitch />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<CalendarMenuToday />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<CalendarMenuNav />
|
||||||
|
</li>
|
||||||
|
</menu>
|
||||||
|
<div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-3 flex justify-end">
|
||||||
|
<CalendarSearch />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="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>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
71
src/components/calendar/CalendarMenuNav.vue
Normal file
71
src/components/calendar/CalendarMenuNav.vue
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useCalendar } from '@/stores/CalendarStore'
|
||||||
|
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
|
import {
|
||||||
|
PhCaretDoubleLeft,
|
||||||
|
PhCaretDoubleRight,
|
||||||
|
PhCaretLeft,
|
||||||
|
PhCaretRight
|
||||||
|
} from '@phosphor-icons/vue'
|
||||||
|
import Button from '../ui/button/Button.vue'
|
||||||
|
|
||||||
|
const { decrementMonth, incrementMonth, decrementYear, incrementYear } = useCalendar()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<!-- Implement decrementDate to account for other mods -->
|
||||||
|
<TooltipProvider :delayDuration="250">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button variant="outline" size="icon" @click="decrementYear()">
|
||||||
|
<PhCaretDoubleLeft size="18" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Année précédente</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<TooltipProvider :delayDuration="250">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button variant="outline" size="icon" @click="decrementMonth()">
|
||||||
|
<PhCaretLeft size="18" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Mois précédent</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<TooltipProvider :delayDuration="250">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button variant="outline" size="icon" @click="incrementMonth()">
|
||||||
|
<PhCaretRight size="18" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Mois suivant</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<TooltipProvider :delayDuration="250">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button variant="outline" size="icon" @click="incrementYear()">
|
||||||
|
<PhCaretDoubleRight size="18" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Année suivante</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
33
src/components/calendar/CalendarMenuToday.vue
Normal file
33
src/components/calendar/CalendarMenuToday.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
|
import { useCalendar } from '@/stores/CalendarStore'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import Button from '../ui/button/Button.vue'
|
||||||
|
import { areDatesIdentical } from '@/models/Date'
|
||||||
|
|
||||||
|
const { defaultDate, selectedDate } = storeToRefs(useCalendar())
|
||||||
|
const { jumpToDefaultDate, getFormattedDateTitle } = useCalendar()
|
||||||
|
|
||||||
|
const defaultDateFormatted = computed(() => getFormattedDateTitle(defaultDate.value, true))
|
||||||
|
|
||||||
|
const isDefaultDate = computed(() => {
|
||||||
|
return areDatesIdentical(selectedDate.value, defaultDate.value)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TooltipProvider :delayDuration="250">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button @click="jumpToDefaultDate" size="sm" :disabled="isDefaultDate">
|
||||||
|
Aujourd'hui
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{{ defaultDateFormatted }}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</template>
|
||||||
42
src/components/calendar/CalendarSwitch.vue
Normal file
42
src/components/calendar/CalendarSwitch.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useCalendar } from '@/stores/CalendarStore'
|
||||||
|
import { PhCalendarBlank } from '@phosphor-icons/vue'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger
|
||||||
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
import Button from '../ui/button/Button.vue'
|
||||||
|
|
||||||
|
const { currentConfig, viewTypeOptions, getViewTypeTitle, setViewType } = useCalendar()
|
||||||
|
|
||||||
|
const viewTypeTitle = computed(() => getViewTypeTitle(currentConfig.viewType))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<Button size="sm" variant="secondary">
|
||||||
|
<PhCalendarBlank size="18" weight="fill" />
|
||||||
|
|
||||||
|
{{ viewTypeTitle }}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuLabel>Mode d'affichage</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
v-for="option in viewTypeOptions"
|
||||||
|
:key="option"
|
||||||
|
@click="setViewType(option)"
|
||||||
|
>
|
||||||
|
{{ getViewTypeTitle(option) }}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</template>
|
||||||
83
src/components/calendar/CalendarTile.vue
Normal file
83
src/components/calendar/CalendarTile.vue
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<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 } from 'vue'
|
||||||
|
import CalendarEvent from './CalendarEvent.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
date: LeimDate
|
||||||
|
faded?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { defaultDate, selectDate } = useCalendar()
|
||||||
|
const { selectedDate } = storeToRefs(useCalendar())
|
||||||
|
const { currentEvents } = storeToRefs(useCalendarEvents())
|
||||||
|
|
||||||
|
const eventsForTheDay = computed(() => {
|
||||||
|
return currentEvents.value.filter((currentEvent) => {
|
||||||
|
return areDatesIdentical(currentEvent.date, props.date)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const isDefaultDate = computed(() => {
|
||||||
|
return areDatesIdentical(props.date, defaultDate)
|
||||||
|
})
|
||||||
|
|
||||||
|
const isSelectedDate = computed(() => {
|
||||||
|
return areDatesIdentical(props.date, selectedDate.value)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
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
|
||||||
|
v-if="eventsForTheDay.length > 0"
|
||||||
|
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 eventsForTheDay" :key="event.title" class="grid pointer-events-auto">
|
||||||
|
<CalendarEvent :event />
|
||||||
|
</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>
|
||||||
3
src/components/calendar/Search.ts
Normal file
3
src/components/calendar/Search.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const searchUnifier = new RegExp(/[^a-zA-Z0-9\-'']/g)
|
||||||
|
|
||||||
|
export type SearchMode = 'characters' | 'events' | undefined
|
||||||
435
src/components/calendar/search/CalendarSearch.vue
Normal file
435
src/components/calendar/search/CalendarSearch.vue
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
isCharacter,
|
||||||
|
type Character,
|
||||||
|
characterCategories,
|
||||||
|
type CharacterCategory
|
||||||
|
} from '@/models/Characters'
|
||||||
|
import type { LeimDateOrder } from '@/models/Date'
|
||||||
|
import {
|
||||||
|
isCalendarEvent,
|
||||||
|
type CalendarEvent,
|
||||||
|
calendarEventCategories,
|
||||||
|
type CalendarEventCategory
|
||||||
|
} from '@/models/Events'
|
||||||
|
import { capitalize } from '@/utils/Strings'
|
||||||
|
import { useCharacters } from '@/stores/CharacterStore'
|
||||||
|
import { useCalendarEvents } from '@/stores/EventStore'
|
||||||
|
import { useMagicKeys, useStorage, whenever } from '@vueuse/core'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { searchUnifier, type SearchMode } from '../Search'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { CommandEmpty, CommandGroup, CommandItem, CommandList } from '@/components/ui/command'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import {
|
||||||
|
Pagination,
|
||||||
|
PaginationEllipsis,
|
||||||
|
PaginationFirst,
|
||||||
|
PaginationLast,
|
||||||
|
PaginationList,
|
||||||
|
PaginationListItem,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrev
|
||||||
|
} from '@/components/ui/pagination'
|
||||||
|
import {
|
||||||
|
TagsInput,
|
||||||
|
TagsInputInput,
|
||||||
|
TagsInputItem,
|
||||||
|
TagsInputItemDelete,
|
||||||
|
TagsInputItemText
|
||||||
|
} from '@/components/ui/tags-input'
|
||||||
|
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
|
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 { baseEvents } = useCalendarEvents()
|
||||||
|
|
||||||
|
const modalOpen = ref<boolean>(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)
|
||||||
|
|
||||||
|
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 = baseEvents
|
||||||
|
} else if (selectedEntity.value === 'characters') {
|
||||||
|
dataToFilter = characters
|
||||||
|
} else {
|
||||||
|
dataToFilter = [...baseEvents, ...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
|
||||||
|
let 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)
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
let 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)
|
||||||
|
})
|
||||||
|
|
||||||
|
return hitTitle && hitCategories
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return results
|
||||||
|
})
|
||||||
|
|
||||||
|
function resetSearch() {
|
||||||
|
searchQuery.value = ''
|
||||||
|
resetPage()
|
||||||
|
selectedCategories.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDialog() {
|
||||||
|
modalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDialog() {
|
||||||
|
modalOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEntitySwitch() {
|
||||||
|
resetPage()
|
||||||
|
selectedCategories.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key combos to deploy modal
|
||||||
|
const keys = useMagicKeys()
|
||||||
|
|
||||||
|
whenever(keys.control_period, () => {
|
||||||
|
openDialog()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Categories
|
||||||
|
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 filteredFrameworks = computed(() =>
|
||||||
|
currentCategories.value.filter((i) => !selectedCategories.value.includes(i))
|
||||||
|
)
|
||||||
|
|
||||||
|
function handleCategorySelect(e: any) {
|
||||||
|
if (typeof e.detail.value === 'string') {
|
||||||
|
searchCategory.value = ''
|
||||||
|
selectedCategories.value.push(e.detail.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredFrameworks.value.length === 0) {
|
||||||
|
categoryFilterOpened.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog v-model:open="modalOpen" @update:open="resetSearch()">
|
||||||
|
<DialogTrigger>
|
||||||
|
<Button search-slash>
|
||||||
|
<PhMagnifyingGlass size="20" weight="light" />
|
||||||
|
Recherche avancée
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent
|
||||||
|
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>
|
||||||
|
<DialogTitle> Recherche avancée </DialogTitle>
|
||||||
|
</VisuallyHidden>
|
||||||
|
<VisuallyHidden>
|
||||||
|
<DialogDescription>
|
||||||
|
Rechercher les données disponibles sur le calendrier
|
||||||
|
</DialogDescription>
|
||||||
|
</VisuallyHidden>
|
||||||
|
|
||||||
|
<!-- Dialog header -->
|
||||||
|
<div class="grid gap-3" id="searchForm">
|
||||||
|
<div class="relative w-full h-fit">
|
||||||
|
<Input
|
||||||
|
id="search"
|
||||||
|
type="text"
|
||||||
|
placeholder="Rechercher le calendrier"
|
||||||
|
class="pl-10 py-6 text-lg"
|
||||||
|
v-model:model-value="searchQuery"
|
||||||
|
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>
|
||||||
|
<ToggleGroup
|
||||||
|
type="single"
|
||||||
|
class="justify-start"
|
||||||
|
v-model="selectedEntity"
|
||||||
|
@update:model-value="handleEntitySwitch()"
|
||||||
|
>
|
||||||
|
<ToggleGroupItem value="events" aria-label="Uniquement les évènements">
|
||||||
|
Évènements
|
||||||
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="characters" aria-label="Uniquement les personnages">
|
||||||
|
Personnages
|
||||||
|
</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<TagsInput class="px-0 gap-0 w-52" :model-value="selectedCategories">
|
||||||
|
<div class="flex gap-2 flex-wrap items-center px-3">
|
||||||
|
<TagsInputItem v-for="item in selectedCategories" :key="item" :value="item">
|
||||||
|
<TagsInputItemText class="capitalize" />
|
||||||
|
<TagsInputItemDelete />
|
||||||
|
</TagsInputItem>
|
||||||
|
</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>
|
||||||
|
<TagsInputInput
|
||||||
|
class="w-full px-3"
|
||||||
|
:class="selectedCategories.length > 0 ? 'mt-2' : ''"
|
||||||
|
@keydown.enter.prevent
|
||||||
|
/>
|
||||||
|
</ComboboxInput>
|
||||||
|
</ComboboxAnchor>
|
||||||
|
|
||||||
|
<ComboboxPortal :to="'#searchForm'">
|
||||||
|
<CommandList
|
||||||
|
position="popper"
|
||||||
|
class="w-[--radix-popper-anchor-width] rounded-md mt-2 border bg-popover text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50"
|
||||||
|
:dismissable="true"
|
||||||
|
>
|
||||||
|
<CommandEmpty />
|
||||||
|
<CommandGroup>
|
||||||
|
<CommandItem
|
||||||
|
v-for="framework in filteredFrameworks"
|
||||||
|
:key="framework"
|
||||||
|
:value="framework"
|
||||||
|
@select.prevent="handleCategorySelect"
|
||||||
|
>
|
||||||
|
{{ capitalize(framework) }}
|
||||||
|
</CommandItem>
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</ComboboxPortal>
|
||||||
|
</ComboboxRoot>
|
||||||
|
</TagsInput>
|
||||||
|
|
||||||
|
<TooltipProvider :delayDuration="250">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button
|
||||||
|
:variant="selectedOrder === 'desc' ? 'secondary' : 'outline'"
|
||||||
|
size="icon"
|
||||||
|
@click="setOrderDesc()"
|
||||||
|
>
|
||||||
|
<PhClockCounterClockwise size="18" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Plus ancien</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<TooltipProvider :delayDuration="250">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button
|
||||||
|
:variant="selectedOrder === 'asc' ? 'secondary' : 'outline'"
|
||||||
|
size="icon"
|
||||||
|
@click="setOrderAsc()"
|
||||||
|
>
|
||||||
|
<PhClockClockwise size="18" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Plus récent</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div v-if="searchResults.length > 0" class="grow overflow-y-auto">
|
||||||
|
<SearchList
|
||||||
|
:results="searchResults"
|
||||||
|
:current-entity="selectedEntity"
|
||||||
|
:order="selectedOrder"
|
||||||
|
:start-at="startOfList"
|
||||||
|
:end-at="endOfList"
|
||||||
|
@jumped-to-date="closeDialog()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<Pagination
|
||||||
|
v-model:page="currentPage"
|
||||||
|
:total="searchResults.length"
|
||||||
|
:items-per-page="itemsPerPage"
|
||||||
|
:sibling-count="2"
|
||||||
|
show-edges
|
||||||
|
:default-page="1"
|
||||||
|
>
|
||||||
|
<PaginationList v-slot="{ items }" class="flex items-center gap-1">
|
||||||
|
<PaginationFirst />
|
||||||
|
<PaginationPrev />
|
||||||
|
|
||||||
|
<template v-for="(item, index) in items">
|
||||||
|
<PaginationListItem
|
||||||
|
v-if="item.type === 'page'"
|
||||||
|
:key="index"
|
||||||
|
:value="item.value"
|
||||||
|
as-child
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
class="w-10 h-10 p-0"
|
||||||
|
:variant="item.value === currentPage ? 'default' : 'outline'"
|
||||||
|
>
|
||||||
|
{{ item.value }}
|
||||||
|
</Button>
|
||||||
|
</PaginationListItem>
|
||||||
|
<PaginationEllipsis v-else :key="item.type" :index="index" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<PaginationNext />
|
||||||
|
<PaginationLast />
|
||||||
|
</PaginationList>
|
||||||
|
</Pagination>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
196
src/components/calendar/search/lists/SearchList.vue
Normal file
196
src/components/calendar/search/lists/SearchList.vue
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { isCharacter, type Character } from '@/models/Characters'
|
||||||
|
import { compareDates, getRelativeString, 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 '../../Search'
|
||||||
|
|
||||||
|
import { PhHourglassMedium, PhPlant, PhSkull, PhArrowSquareOut } from '@phosphor-icons/vue'
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
results: (Character | CalendarEvent)[]
|
||||||
|
currentEntity: SearchMode
|
||||||
|
order: LeimDateOrder
|
||||||
|
startAt: number
|
||||||
|
endAt: number
|
||||||
|
limit?: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits(['jumpedToDate'])
|
||||||
|
|
||||||
|
const { defaultDate, getFormattedDateTitle, 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.date
|
||||||
|
} else if (isCharacter(a) && a.birth) {
|
||||||
|
firstDate = a.birth
|
||||||
|
} else {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCalendarEvent(b)) {
|
||||||
|
secondDate = b.date
|
||||||
|
} 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">
|
||||||
|
<button
|
||||||
|
v-if="isCalendarEvent(r)"
|
||||||
|
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': !r.category,
|
||||||
|
'text-white bg-lime-600 hover:bg-lime-700': r.category === 'naissance',
|
||||||
|
'text-white bg-stone-500 hover:bg-stone-700': r.category === 'mort',
|
||||||
|
'text-white bg-orange-600 hover:bg-orange-700': r.category === 'catastrophe',
|
||||||
|
'text-white bg-pink-600 hover:bg-pink-700': r.category === 'catastrophe naturelle',
|
||||||
|
'text-white bg-sky-600 hover:bg-sky-700': r.category === 'législation',
|
||||||
|
'text-white bg-purple-600 hover:bg-purple-700': r.category === 'religion',
|
||||||
|
'text-white bg-emerald-600 hover:bg-emerald-700': r.category === 'joueurs',
|
||||||
|
'text-slate-900 bg-amber-300 hover:bg-amber-400': r.category === 'inauguration',
|
||||||
|
'text-slate-900 bg-emerald-200 hover:bg-emerald-300': r.category === 'invention',
|
||||||
|
'text-slate-900 bg-cyan-300 hover:bg-cyan-400': r.category === 'science',
|
||||||
|
'text-slate-900 bg-yellow-100 hover:bg-yellow-200': r.category === 'bénédiction'
|
||||||
|
}"
|
||||||
|
@click="handleJumpToDate(r.date)"
|
||||||
|
>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<h2 class="font-bold">
|
||||||
|
{{ r.title }}
|
||||||
|
</h2>
|
||||||
|
<div v-if="r.wiki">
|
||||||
|
<Button variant="link" size="xs" as-child class="text-inherit">
|
||||||
|
<a :href="r.wiki" target="_blank">
|
||||||
|
Wiki
|
||||||
|
<PhArrowSquareOut size="16" weight="fill" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-1 flex gap-4 items-center">
|
||||||
|
<p class="opacity-75">{{ getFormattedDateTitle(r.date, true) }}</p>
|
||||||
|
<p class="text-sm italic opacity-75 flex items-center gap-1">
|
||||||
|
<PhHourglassMedium size="16" weight="fill" />
|
||||||
|
{{ getRelativeString(defaultDate, r.date) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="r.category || r.secondaryCategories" class="absolute top-3 right-4">
|
||||||
|
<ul class="flex gap-1">
|
||||||
|
<li v-if="r.category">
|
||||||
|
<Badge class="mix-blend-luminosity font-bold bg-gray-600" variant="secondary">
|
||||||
|
{{ r.category }}
|
||||||
|
</Badge>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li v-for="cat in r.secondaryCategories" :key="cat">
|
||||||
|
<Badge class="mix-blend-luminosity bg-gray-600" variant="secondary">
|
||||||
|
{{ cat }}
|
||||||
|
</Badge>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="r.description" class="text-sm">
|
||||||
|
<hr class="my-2 border-white opacity-50" />
|
||||||
|
<span class="opacity-75">
|
||||||
|
{{ r.description }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="isCharacter(r)"
|
||||||
|
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">
|
||||||
|
{{ r.name }}
|
||||||
|
</h2>
|
||||||
|
<div v-if="r.wiki">
|
||||||
|
<Button variant="link" size="xs" as-child>
|
||||||
|
<a :href="r.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="r.birth">
|
||||||
|
<TooltipProvider :delayDuration="100">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button @click="handleJumpToDate(r.birth)" variant="ghost" size="xs">
|
||||||
|
<PhPlant size="16" weight="fill" />
|
||||||
|
{{ getFormattedDateTitle(r.birth, true) }}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Date de naissance</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</li>
|
||||||
|
<span v-if="r.birth && r.death">-</span>
|
||||||
|
<li v-if="r.death">
|
||||||
|
<TooltipProvider :delayDuration="100">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button @click="handleJumpToDate(r.death)" variant="ghost" size="xs">
|
||||||
|
<PhSkull size="16" weight="fill" />
|
||||||
|
{{ getFormattedDateTitle(r.death, true) }}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Date de décès</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</li>
|
||||||
|
</menu>
|
||||||
|
|
||||||
|
<hr v-if="r.description" class="border-white opacity-25" />
|
||||||
|
|
||||||
|
<div v-if="r.description" class="text-sm">
|
||||||
|
<span class="opacity-75">
|
||||||
|
{{ r.description }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
9
src/components/calendar/state/Century.vue
Normal file
9
src/components/calendar/state/Century.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useCalendarEvents } from '@/stores/EventStore'
|
||||||
|
|
||||||
|
const { currentEvents } = useCalendarEvents()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>Century</div>
|
||||||
|
</template>
|
||||||
3
src/components/calendar/state/Decade.vue
Normal file
3
src/components/calendar/state/Decade.vue
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<div>Decade</div>
|
||||||
|
</template>
|
||||||
75
src/components/calendar/state/Monthly.vue
Normal file
75
src/components/calendar/state/Monthly.vue
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { LeimDate } from '@/models/Date'
|
||||||
|
import { useCalendar } from '@/stores/CalendarStore'
|
||||||
|
import { useThrottleFn } from '@vueuse/core'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import CalendarTile from '../CalendarTile.vue'
|
||||||
|
|
||||||
|
const { staticConfig, currentDate, decrementMonth, incrementMonth } = useCalendar()
|
||||||
|
|
||||||
|
const daysPerMonth = computed(() => 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" :class="`grid-cols-10`" @wheel="handleWheel">
|
||||||
|
<CalendarTile
|
||||||
|
v-for="day in daysPerMonth"
|
||||||
|
:key="day"
|
||||||
|
:date="{
|
||||||
|
day: day,
|
||||||
|
month: currentDate.currentMonth,
|
||||||
|
year: currentDate.currentYear,
|
||||||
|
period: currentDate.currentPeriod
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<CalendarTile
|
||||||
|
v-for="nextMonthDay in 8"
|
||||||
|
:key="nextMonthDay"
|
||||||
|
faded
|
||||||
|
:date="getNextMonthDate(nextMonthDay)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
3
src/components/calendar/state/Year.vue
Normal file
3
src/components/calendar/state/Year.vue
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<div>Annuel</div>
|
||||||
|
</template>
|
||||||
16
src/components/ui/badge/Badge.vue
Normal file
16
src/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>
|
||||||
24
src/components/ui/badge/index.ts
Normal file
24
src/components/ui/badge/index.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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
src/components/ui/button/Button.vue
Normal file
34
src/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
src/components/ui/button/index.ts
Normal file
32
src/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>
|
||||||
35
src/components/ui/command/Command.vue
Normal file
35
src/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
src/components/ui/command/CommandDialog.vue
Normal file
23
src/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
src/components/ui/command/CommandEmpty.vue
Normal file
20
src/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
src/components/ui/command/CommandGroup.vue
Normal file
36
src/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
src/components/ui/command/CommandInput.vue
Normal file
40
src/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
src/components/ui/command/CommandItem.vue
Normal file
31
src/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
src/components/ui/command/CommandList.vue
Normal file
33
src/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
src/components/ui/command/CommandSeparator.vue
Normal file
20
src/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
src/components/ui/command/CommandShortcut.vue
Normal file
14
src/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
src/components/ui/command/index.ts
Normal file
9
src/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
src/components/ui/dialog/Dialog.vue
Normal file
19
src/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
src/components/ui/dialog/DialogClose.vue
Normal file
11
src/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
src/components/ui/dialog/DialogContent.vue
Normal file
51
src/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-full 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
src/components/ui/dialog/DialogDescription.vue
Normal file
24
src/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
src/components/ui/dialog/DialogFooter.vue
Normal file
12
src/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
src/components/ui/dialog/DialogHeader.vue
Normal file
14
src/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
src/components/ui/dialog/DialogScrollContent.vue
Normal file
64
src/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
src/components/ui/dialog/DialogTitle.vue
Normal file
24
src/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
src/components/ui/dialog/DialogTrigger.vue
Normal file
11
src/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
src/components/ui/dialog/index.ts
Normal file
9
src/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
src/components/ui/dropdown-menu/DropdownMenu.vue
Normal file
19
src/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
src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
Normal file
42
src/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
src/components/ui/dropdown-menu/DropdownMenuContent.vue
Normal file
43
src/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
src/components/ui/dropdown-menu/DropdownMenuGroup.vue
Normal file
11
src/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
src/components/ui/dropdown-menu/DropdownMenuItem.vue
Normal file
32
src/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
src/components/ui/dropdown-menu/DropdownMenuLabel.vue
Normal file
26
src/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
src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
Normal file
19
src/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
src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue
Normal file
43
src/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
src/components/ui/dropdown-menu/DropdownMenuSeparator.vue
Normal file
24
src/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
src/components/ui/dropdown-menu/DropdownMenuShortcut.vue
Normal file
14
src/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
src/components/ui/dropdown-menu/DropdownMenuSub.vue
Normal file
19
src/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
src/components/ui/dropdown-menu/DropdownMenuSubContent.vue
Normal file
35
src/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
src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
Normal file
35
src/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
src/components/ui/dropdown-menu/DropdownMenuTrigger.vue
Normal file
13
src/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
src/components/ui/dropdown-menu/index.ts
Normal file
16
src/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
src/components/ui/input/Input.vue
Normal file
32
src/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
src/components/ui/input/index.ts
Normal file
1
src/components/ui/input/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as Input } from './Input.vue'
|
||||||
25
src/components/ui/pagination/PaginationEllipsis.vue
Normal file
25
src/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
src/components/ui/pagination/PaginationFirst.vue
Normal file
30
src/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
src/components/ui/pagination/PaginationLast.vue
Normal file
30
src/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
src/components/ui/pagination/PaginationNext.vue
Normal file
30
src/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
src/components/ui/pagination/PaginationPrev.vue
Normal file
30
src/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
src/components/ui/pagination/index.ts
Normal file
6
src/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
src/components/ui/popover/Popover.vue
Normal file
15
src/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
src/components/ui/popover/PopoverContent.vue
Normal file
48
src/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
src/components/ui/popover/PopoverTrigger.vue
Normal file
11
src/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
src/components/ui/popover/index.ts
Normal file
3
src/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
src/components/ui/select/Select.vue
Normal file
15
src/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
src/components/ui/select/SelectContent.vue
Normal file
63
src/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
src/components/ui/select/SelectGroup.vue
Normal file
19
src/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
src/components/ui/select/SelectItem.vue
Normal file
44
src/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
src/components/ui/select/SelectItemText.vue
Normal file
11
src/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
src/components/ui/select/SelectLabel.vue
Normal file
13
src/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
src/components/ui/select/SelectScrollDownButton.vue
Normal file
31
src/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
src/components/ui/select/SelectScrollUpButton.vue
Normal file
27
src/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
src/components/ui/select/SelectSeparator.vue
Normal file
17
src/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
src/components/ui/select/SelectTrigger.vue
Normal file
33
src/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
src/components/ui/select/SelectValue.vue
Normal file
11
src/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
src/components/ui/select/index.ts
Normal file
11
src/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
src/components/ui/tabs/Tabs.vue
Normal file
15
src/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
src/components/ui/tabs/TabsContent.vue
Normal file
27
src/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
src/components/ui/tabs/TabsList.vue
Normal file
27
src/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
src/components/ui/tabs/TabsTrigger.vue
Normal file
29
src/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
src/components/ui/tabs/index.ts
Normal file
4
src/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
src/components/ui/tags-input/TagsInput.vue
Normal file
35
src/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
src/components/ui/tags-input/TagsInputInput.vue
Normal file
22
src/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
src/components/ui/tags-input/TagsInputItem.vue
Normal file
30
src/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>
|
||||||
27
src/components/ui/tags-input/TagsInputItemDelete.vue
Normal file
27
src/components/ui/tags-input/TagsInputItemDelete.vue
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type HTMLAttributes, computed } from 'vue'
|
||||||
|
import { TagsInputItemDelete, type TagsInputItemDeleteProps, useForwardProps } from 'radix-vue'
|
||||||
|
import { X } from 'lucide-vue-next'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<TagsInputItemDeleteProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TagsInputItemDelete
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="cn('flex rounded bg-transparent mr-1', props.class)"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<X class="w-4 h-4" />
|
||||||
|
</slot>
|
||||||
|
</TagsInputItemDelete>
|
||||||
|
</template>
|
||||||
22
src/components/ui/tags-input/TagsInputItemText.vue
Normal file
22
src/components/ui/tags-input/TagsInputItemText.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type HTMLAttributes, computed } from 'vue'
|
||||||
|
import { TagsInputItemText, type TagsInputItemTextProps, useForwardProps } from 'radix-vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<TagsInputItemTextProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TagsInputItemText
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="cn('py-1 px-2 text-sm rounded bg-transparent', props.class)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
5
src/components/ui/tags-input/index.ts
Normal file
5
src/components/ui/tags-input/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export { default as TagsInput } from './TagsInput.vue'
|
||||||
|
export { default as TagsInputInput } from './TagsInputInput.vue'
|
||||||
|
export { default as TagsInputItem } from './TagsInputItem.vue'
|
||||||
|
export { default as TagsInputItemDelete } from './TagsInputItemDelete.vue'
|
||||||
|
export { default as TagsInputItemText } from './TagsInputItemText.vue'
|
||||||
44
src/components/ui/toggle-group/ToggleGroup.vue
Normal file
44
src/components/ui/toggle-group/ToggleGroup.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { VariantProps } from 'class-variance-authority'
|
||||||
|
import { type HTMLAttributes, computed, provide } from 'vue'
|
||||||
|
import {
|
||||||
|
ToggleGroupRoot,
|
||||||
|
type ToggleGroupRootEmits,
|
||||||
|
type ToggleGroupRootProps,
|
||||||
|
useForwardPropsEmits
|
||||||
|
} from 'radix-vue'
|
||||||
|
import type { toggleVariants } from '@/components/ui/toggle'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
type ToggleGroupVariants = VariantProps<typeof toggleVariants>
|
||||||
|
|
||||||
|
const props = defineProps<
|
||||||
|
ToggleGroupRootProps & {
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
variant?: ToggleGroupVariants['variant']
|
||||||
|
size?: ToggleGroupVariants['size']
|
||||||
|
}
|
||||||
|
>()
|
||||||
|
const emits = defineEmits<ToggleGroupRootEmits>()
|
||||||
|
|
||||||
|
provide('toggleGroup', {
|
||||||
|
variant: props.variant,
|
||||||
|
size: props.size
|
||||||
|
})
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ToggleGroupRoot
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class="cn('flex items-center justify-center gap-1', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</ToggleGroupRoot>
|
||||||
|
</template>
|
||||||
43
src/components/ui/toggle-group/ToggleGroupItem.vue
Normal file
43
src/components/ui/toggle-group/ToggleGroupItem.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { VariantProps } from 'class-variance-authority'
|
||||||
|
import { type HTMLAttributes, computed, inject } from 'vue'
|
||||||
|
import { ToggleGroupItem, type ToggleGroupItemProps, useForwardProps } from 'radix-vue'
|
||||||
|
import { toggleVariants } from '@/components/ui/toggle'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
type ToggleGroupVariants = VariantProps<typeof toggleVariants>
|
||||||
|
|
||||||
|
const props = defineProps<
|
||||||
|
ToggleGroupItemProps & {
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
variant?: ToggleGroupVariants['variant']
|
||||||
|
size?: ToggleGroupVariants['size']
|
||||||
|
}
|
||||||
|
>()
|
||||||
|
|
||||||
|
const context = inject<ToggleGroupVariants>('toggleGroup')
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, variant, size, ...delegated } = props
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ToggleGroupItem
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
toggleVariants({
|
||||||
|
variant: context?.variant || variant,
|
||||||
|
size: context?.size || size
|
||||||
|
}),
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</ToggleGroupItem>
|
||||||
|
</template>
|
||||||
2
src/components/ui/toggle-group/index.ts
Normal file
2
src/components/ui/toggle-group/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as ToggleGroup } from './ToggleGroup.vue'
|
||||||
|
export { default as ToggleGroupItem } from './ToggleGroupItem.vue'
|
||||||
37
src/components/ui/toggle/Toggle.vue
Normal file
37
src/components/ui/toggle/Toggle.vue
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type HTMLAttributes, computed } from 'vue'
|
||||||
|
import { Toggle, type ToggleEmits, type ToggleProps, useForwardPropsEmits } from 'radix-vue'
|
||||||
|
import { type ToggleVariants, toggleVariants } from '.'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<
|
||||||
|
ToggleProps & {
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
variant?: ToggleVariants['variant']
|
||||||
|
size?: ToggleVariants['size']
|
||||||
|
}
|
||||||
|
>(),
|
||||||
|
{
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default',
|
||||||
|
disabled: false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const emits = defineEmits<ToggleEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, size, variant, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Toggle v-bind="forwarded" :class="cn(toggleVariants({ variant, size }), props.class)">
|
||||||
|
<slot />
|
||||||
|
</Toggle>
|
||||||
|
</template>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user