move calendar, dropdown-menu, select, dialog, number-field components to

the ui package
This commit is contained in:
Gregor Vostrak
2026-03-03 18:15:29 +01:00
parent bce6cb9395
commit c581ad8854
66 changed files with 106 additions and 191 deletions

View File

@@ -1,12 +1,6 @@
<script setup lang="ts">
import { Popover, PopoverContent, PopoverTrigger, Button } from '..';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/Components/ui/select';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '..';
import { Field, FieldLabel } from '../field';
import { Settings } from 'lucide-vue-next';
import { ref, watch } from 'vue';

View File

@@ -7,7 +7,7 @@ import {
NumberFieldDecrement,
NumberFieldIncrement,
NumberFieldInput,
} from '@/Components/ui/number-field';
} from '..';
const props = defineProps<{
name: string;

View File

@@ -7,7 +7,7 @@ import {
type WeekStartDay,
} from '@/packages/ui/src/utils/time';
import { Popover, PopoverContent, PopoverTrigger } from '@/packages/ui/src/popover';
import { Calendar } from '@/Components/ui/calendar';
import { Calendar } from '..';
import { Button } from '@/packages/ui/src/Buttons';
import { CalendarIcon } from 'lucide-vue-next';
import { parseDate, type DateValue } from '@internationalized/date';

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Dialog, DialogContent, DialogFooter } from '@/Components/ui/dialog';
import { Dialog, DialogContent, DialogFooter } from './dialog/index';
import { computed } from 'vue';
const props = defineProps({

View File

@@ -1,11 +1,5 @@
<script setup lang="ts">
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/Components/ui/select';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '..';
import type { BillableKey } from '@/types/projects';
const model = defineModel<BillableKey>({

View File

@@ -1,13 +1,7 @@
<script setup lang="ts">
import { Field, FieldDescription, FieldLabel } from '../field';
import BillableRateInput from '@/packages/ui/src/Input/BillableRateInput.vue';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/Components/ui/select';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '..';
import {
Tooltip,
TooltipContent,

View File

@@ -17,13 +17,7 @@ import type {
} from '@/packages/api/src';
import TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue';
import BillableIcon from '@/packages/ui/src/Icons/BillableIcon.vue';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/Components/ui/select';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '..';
import { Button } from '@/packages/ui/src/Buttons';
import DatePicker from '@/packages/ui/src/Input/DatePicker.vue';
import DurationHumanInput from '@/packages/ui/src/Input/DurationHumanInput.vue';

View File

@@ -17,13 +17,7 @@ import type {
} from '@/packages/api/src';
import TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue';
import BillableIcon from '@/packages/ui/src/Icons/BillableIcon.vue';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/Components/ui/select';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '..';
import { Button } from '@/packages/ui/src/Buttons';
import DatePicker from '@/packages/ui/src/Input/DatePicker.vue';
import DurationHumanInput from '@/packages/ui/src/Input/DurationHumanInput.vue';

View File

@@ -16,13 +16,7 @@ import {
} from '@/packages/api/src';
import { Checkbox } from '@/packages/ui/src';
import { TagIcon } from '@heroicons/vue/20/solid';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/Components/ui/select';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '..';
import { Button } from '@/packages/ui/src/Buttons';
import TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue';
import type { Tag, Task } from '@/packages/api/src';

View File

@@ -1,11 +1,6 @@
<script setup lang="ts">
import { TrashIcon, PencilIcon, DocumentDuplicateIcon } from '@heroicons/vue/20/solid';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '..';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '..';
const props = withDefaults(
defineProps<{

View File

@@ -1,11 +1,6 @@
<script setup lang="ts">
import { PlusIcon, XMarkIcon } from '@heroicons/vue/20/solid';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '..';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '..';
const props = defineProps<{
hasActiveTimer: boolean;

View File

@@ -0,0 +1,70 @@
<script lang="ts" setup>
import { cn } from '@/lib/utils';
import {
CalendarRoot,
type CalendarRootEmits,
type CalendarRootProps,
useForwardPropsEmits,
} from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
import {
CalendarCell,
CalendarCellTrigger,
CalendarGrid,
CalendarGridBody,
CalendarGridHead,
CalendarGridRow,
CalendarHeadCell,
CalendarHeader,
CalendarHeading,
CalendarNextButton,
CalendarPrevButton,
} from '.';
const props = defineProps<CalendarRootProps & { class?: HTMLAttributes['class'] }>();
const emits = defineEmits<CalendarRootEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<CalendarRoot v-slot="{ grid, weekDays }" :class="cn('p-3', props.class)" v-bind="forwarded">
<CalendarHeader>
<CalendarPrevButton />
<CalendarHeading />
<CalendarNextButton />
</CalendarHeader>
<div class="flex flex-col gap-y-4 mt-4 sm:flex-row sm:gap-x-4 sm:gap-y-0">
<CalendarGrid v-for="month in grid" :key="month.value.toString()">
<CalendarGridHead>
<CalendarGridRow>
<CalendarHeadCell v-for="day in weekDays" :key="day">
{{ day }}
</CalendarHeadCell>
</CalendarGridRow>
</CalendarGridHead>
<CalendarGridBody>
<CalendarGridRow
v-for="(weekDates, index) in month.rows"
:key="`weekDate-${index}`"
class="mt-2 w-full">
<CalendarCell
v-for="weekDate in weekDates"
:key="weekDate.toString()"
:date="weekDate">
<CalendarCellTrigger :day="weekDate" :month="month.value" />
</CalendarCell>
</CalendarGridRow>
</CalendarGridBody>
</CalendarGrid>
</div>
</CalendarRoot>
</template>

View File

@@ -0,0 +1,28 @@
<script lang="ts" setup>
import { CalendarCell, type CalendarCellProps, useForwardProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
import { twMerge } from 'tailwind-merge';
const props = defineProps<CalendarCellProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<CalendarCell
:class="
twMerge(
'relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md [&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-view])]:bg-accent/50',
props.class
)
"
v-bind="forwardedProps">
<slot />
</CalendarCell>
</template>

View File

@@ -0,0 +1,38 @@
<script lang="ts" setup>
import { cn, buttonVariants } from '@/packages/ui/src';
import { CalendarCellTrigger, type CalendarCellTriggerProps, useForwardProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<CalendarCellTriggerProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<CalendarCellTrigger
:class="
cn(
buttonVariants({ variant: 'ghost' }),
'h-8 w-8 p-0 font-normal',
'[&[data-today]:not([data-selected])]:border-accent [&[data-today]:not([data-selected])]:border [&[data-today]:not([data-selected])]:text-accent-foreground',
// Selected
'data-[selected]:bg-quaternary data-[selected]:text-primary-foreground data-[selected]:opacity-100 data-[selected]:hover:bg-quaternary data-[selected]:hover:text-primary-foreground data-[selected]:focus:bg-primary data-[selected]:focus:text-primary-foreground',
// Disabled
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
// Unavailable
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
// Outside months
'data-[outside-view]:text-muted-foreground data-[outside-view]:opacity-50 [&[data-outside-view][data-selected]]:bg-accent/50 [&[data-outside-view][data-selected]]:text-muted-foreground [&[data-outside-view][data-selected]]:opacity-30',
props.class
)
"
v-bind="forwardedProps">
<slot />
</CalendarCellTrigger>
</template>

View File

@@ -0,0 +1,23 @@
<script lang="ts" setup>
import { cn } from '@/lib/utils';
import { CalendarGrid, type CalendarGridProps, useForwardProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<CalendarGridProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<CalendarGrid
:class="cn('w-full border-collapse space-y-1', props.class)"
v-bind="forwardedProps">
<slot />
</CalendarGrid>
</template>

View File

@@ -0,0 +1,11 @@
<script lang="ts" setup>
import { CalendarGridBody, type CalendarGridBodyProps } from 'reka-ui';
const props = defineProps<CalendarGridBodyProps>();
</script>
<template>
<CalendarGridBody v-bind="props">
<slot />
</CalendarGridBody>
</template>

View File

@@ -0,0 +1,12 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue';
import { CalendarGridHead, type CalendarGridHeadProps } from 'reka-ui';
const props = defineProps<CalendarGridHeadProps & { class?: HTMLAttributes['class'] }>();
</script>
<template>
<CalendarGridHead v-bind="props">
<slot />
</CalendarGridHead>
</template>

View File

@@ -0,0 +1,21 @@
<script lang="ts" setup>
import { cn } from '@/lib/utils';
import { CalendarGridRow, type CalendarGridRowProps, useForwardProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<CalendarGridRowProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<CalendarGridRow :class="cn('flex', props.class)" v-bind="forwardedProps">
<slot />
</CalendarGridRow>
</template>

View File

@@ -0,0 +1,23 @@
<script lang="ts" setup>
import { cn } from '@/lib/utils';
import { CalendarHeadCell, type CalendarHeadCellProps, useForwardProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<CalendarHeadCellProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<CalendarHeadCell
:class="cn('w-8 rounded-md text-[0.8rem] font-normal text-muted-foreground', props.class)"
v-bind="forwardedProps">
<slot />
</CalendarHeadCell>
</template>

View File

@@ -0,0 +1,23 @@
<script lang="ts" setup>
import { cn } from '@/lib/utils';
import { CalendarHeader, type CalendarHeaderProps, useForwardProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<CalendarHeaderProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<CalendarHeader
:class="cn('relative flex w-full items-center justify-between pt-1', props.class)"
v-bind="forwardedProps">
<slot />
</CalendarHeader>
</template>

View File

@@ -0,0 +1,30 @@
<script lang="ts" setup>
import { cn } from '@/lib/utils';
import { CalendarHeading, type CalendarHeadingProps, useForwardProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<CalendarHeadingProps & { class?: HTMLAttributes['class'] }>();
defineSlots<{
default: (props: { headingValue: string }) => unknown;
}>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<CalendarHeading
v-slot="{ headingValue }"
:class="cn('text-sm font-medium', props.class)"
v-bind="forwardedProps">
<slot :heading-value>
{{ headingValue }}
</slot>
</CalendarHeading>
</template>

View File

@@ -0,0 +1,32 @@
<script lang="ts" setup>
import { cn, buttonVariants } from '@/packages/ui/src/index';
import { ChevronRight } from 'lucide-vue-next';
import { CalendarNext, type CalendarNextProps, useForwardProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<CalendarNextProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<CalendarNext
:class="
cn(
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
props.class
)
"
v-bind="forwardedProps">
<slot>
<ChevronRight class="h-4 w-4" />
</slot>
</CalendarNext>
</template>

View File

@@ -0,0 +1,32 @@
<script lang="ts" setup>
import { cn, buttonVariants } from '@/packages/ui/src';
import { ChevronLeft } from 'lucide-vue-next';
import { CalendarPrev, type CalendarPrevProps, useForwardProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<CalendarPrevProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<CalendarPrev
:class="
cn(
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
props.class
)
"
v-bind="forwardedProps">
<slot>
<ChevronLeft class="h-4 w-4" />
</slot>
</CalendarPrev>
</template>

View File

@@ -0,0 +1,12 @@
export { default as Calendar } from './Calendar.vue';
export { default as CalendarCell } from './CalendarCell.vue';
export { default as CalendarCellTrigger } from './CalendarCellTrigger.vue';
export { default as CalendarGrid } from './CalendarGrid.vue';
export { default as CalendarGridBody } from './CalendarGridBody.vue';
export { default as CalendarGridHead } from './CalendarGridHead.vue';
export { default as CalendarGridRow } from './CalendarGridRow.vue';
export { default as CalendarHeadCell } from './CalendarHeadCell.vue';
export { default as CalendarHeader } from './CalendarHeader.vue';
export { default as CalendarHeading } from './CalendarHeading.vue';
export { default as CalendarNextButton } from './CalendarNextButton.vue';
export { default as CalendarPrevButton } from './CalendarPrevButton.vue';

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import {
DialogRoot,
type DialogRootEmits,
type DialogRootProps,
useForwardPropsEmits,
} from 'reka-ui';
const props = defineProps<DialogRootProps>();
const emits = defineEmits<DialogRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DialogRoot v-bind="forwarded">
<slot />
</DialogRoot>
</template>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { DialogClose, type DialogCloseProps } from 'reka-ui';
const props = defineProps<DialogCloseProps>();
</script>
<template>
<DialogClose v-bind="props">
<slot />
</DialogClose>
</template>

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import {
DialogContent,
type DialogContentEmits,
type DialogContentProps,
DialogOverlay,
DialogPortal,
useForwardPropsEmits,
} from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
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 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0">
<div class="absolute inset-0 bg-default-background opacity-30" />
</DialogOverlay>
<div
:class="
cn(
'fixed top-0 left-0 z-50 pointer-events-none w-screen h-screen flex items-start px-2 pt-3 md:pt-20 xl:pt-32 justify-center overflow-auto'
)
">
<DialogContent
v-bind="forwarded"
:class="
cn(
'pointer-events-auto bg-default-background grid w-full max-w-lg border border-border-tertiary shadow-lg duration-200 rounded-lg 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',
props.class
)
">
<slot />
</DialogContent>
</div>
</DialogPortal>
</template>

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { DialogDescription, type DialogDescriptionProps, useForwardProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
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>

View 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>

View 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>

View File

@@ -0,0 +1,61 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { X } from 'lucide-vue-next';
import {
DialogClose,
DialogContent,
type DialogContentEmits,
type DialogContentProps,
DialogOverlay,
DialogPortal,
useForwardPropsEmits,
} from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
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-4 right-4 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>

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { DialogTitle, type DialogTitleProps, useForwardProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
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-lg font-semibold leading-none tracking-tight', props.class)">
<slot />
</DialogTitle>
</template>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { DialogTrigger, type DialogTriggerProps } from 'reka-ui';
const props = defineProps<DialogTriggerProps>();
</script>
<template>
<DialogTrigger v-bind="props">
<slot />
</DialogTrigger>
</template>

View File

@@ -0,0 +1,9 @@
export { default as Dialog } from './Dialog.vue';
export { default as DialogClose } from './DialogClose.vue';
export { default as DialogContent } from './DialogContent.vue';
export { default as DialogDescription } from './DialogDescription.vue';
export { default as DialogFooter } from './DialogFooter.vue';
export { default as DialogHeader } from './DialogHeader.vue';
export { default as DialogScrollContent } from './DialogScrollContent.vue';
export { default as DialogTitle } from './DialogTitle.vue';
export { default as DialogTrigger } from './DialogTrigger.vue';

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { Component, HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
import { Label } from '@/Components/ui/label';
import { Label } from '../label/index';
const props = defineProps<{
class?: HTMLAttributes['class'];

View File

@@ -78,6 +78,52 @@ import {
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from './dropdown-menu/index';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectItemText,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
} from './select/index';
import {
NumberField,
NumberFieldContent,
NumberFieldDecrement,
NumberFieldIncrement,
NumberFieldInput,
} from './number-field/index';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogScrollContent,
DialogTitle,
DialogTrigger,
} from './dialog/index';
import {
Calendar,
CalendarCell,
CalendarCellTrigger,
CalendarGrid,
CalendarGridBody,
CalendarGridHead,
CalendarGridRow,
CalendarHeadCell,
CalendarHeader,
CalendarHeading,
CalendarNextButton,
CalendarPrevButton,
} from './calendar/index';
import { Label } from './label/index';
import {
Field,
FieldContent,
@@ -185,4 +231,42 @@ export {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectItemText,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
NumberField,
NumberFieldContent,
NumberFieldDecrement,
NumberFieldIncrement,
NumberFieldInput,
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogScrollContent,
DialogTitle,
DialogTrigger,
Calendar,
CalendarCell,
CalendarCellTrigger,
CalendarGrid,
CalendarGridBody,
CalendarGridHead,
CalendarGridRow,
CalendarHeadCell,
CalendarHeader,
CalendarHeading,
CalendarNextButton,
CalendarPrevButton,
Label,
};

View File

@@ -0,0 +1,24 @@
<script setup lang="ts">
import type { LabelProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
import { reactiveOmit } from '@vueuse/core';
import { Label } from 'reka-ui';
import { cn } from '@/lib/utils';
const props = defineProps<LabelProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = reactiveOmit(props, 'class');
</script>
<template>
<Label
v-bind="delegatedProps"
:class="
cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
props.class
)
">
<slot />
</Label>
</template>

View File

@@ -0,0 +1 @@
export { default as Label } from './Label.vue';

View File

@@ -0,0 +1,64 @@
<script setup lang="ts">
import type { NumberFieldRootEmits, NumberFieldRootProps } from 'reka-ui';
import { cn } from '@/lib/utils';
import { NumberFieldRoot, useForwardPropsEmits } from 'reka-ui';
import { computed, type HTMLAttributes, inject, type ComputedRef } from 'vue';
import type { Organization } from '@/packages/api/src';
const props = defineProps<
NumberFieldRootProps & {
class?: HTMLAttributes['class'];
formatOptions?: {
maximumFractionDigits?: number;
minimumFractionDigits?: number;
};
}
>();
const emits = defineEmits<NumberFieldRootEmits>();
const delegatedProps = computed(() => {
const { class: _, formatOptions: __, ...delegated } = props;
return delegated;
});
const organization = inject<ComputedRef<Organization>>('organization');
const locale = computed(() => {
const format = organization?.value?.number_format || 'comma-point';
// space poin is not supported in reka-ui
switch (format) {
case 'point-comma':
return 'de-DE'; // Uses point for thousands and comma for decimal
case 'comma-point':
return 'en-US'; // Uses comma for thousands and point for decimal
case 'space-comma':
return 'sv-SE'; // Uses space for thousands and comma for decimal
case 'apostrophe-point':
return 'de-CH'; // Uses apostrophe for thousands and point for decimal
default:
return 'en-US';
}
});
const defaultFormatOptions = {
maximumFractionDigits: 2,
};
const formatOptions = computed(() => ({
...defaultFormatOptions,
...props.formatOptions,
}));
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<NumberFieldRoot
v-bind="forwarded"
:locale="locale"
:format-options="formatOptions"
:class="cn('grid gap-1.5', props.class)">
<slot />
</NumberFieldRoot>
</template>

View File

@@ -0,0 +1,20 @@
<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(
'relative [&>[data-slot=input]]:has-[[data-slot=increment]]:pr-5 [&>[data-slot=input]]:has-[[data-slot=decrement]]:pl-5',
props.class
)
">
<slot />
</div>
</template>

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import type { NumberFieldDecrementProps } from 'reka-ui';
import { cn } from '@/lib/utils';
import { Minus } from 'lucide-vue-next';
import { NumberFieldDecrement, useForwardProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<NumberFieldDecrementProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardProps(delegatedProps);
</script>
<template>
<NumberFieldDecrement
data-slot="decrement"
v-bind="forwarded"
:class="
cn(
'absolute top-1/2 -translate-y-1/2 left-0 p-3 disabled:cursor-not-allowed disabled:opacity-20',
props.class
)
">
<slot>
<Minus class="h-4 w-4" />
</slot>
</NumberFieldDecrement>
</template>

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import type { NumberFieldIncrementProps } from 'reka-ui';
import { cn } from '@/lib/utils';
import { Plus } from 'lucide-vue-next';
import { NumberFieldIncrement, useForwardProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<NumberFieldIncrementProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardProps(delegatedProps);
</script>
<template>
<NumberFieldIncrement
data-slot="increment"
v-bind="forwarded"
:class="
cn(
'absolute top-1/2 -translate-y-1/2 right-0 disabled:cursor-not-allowed disabled:opacity-20 p-3',
props.class
)
">
<slot>
<Plus class="h-4 w-4" />
</slot>
</NumberFieldIncrement>
</template>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
import { NumberFieldInput } from 'reka-ui';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<NumberFieldInput
data-slot="input"
:class="
cn(
'flex h-9 w-full rounded-md border border-input-border bg-input-background px-3 py-1 text-base sm:text-sm text-center shadow-sm transition-colors placeholder:text-muted-foreground focus:border-input-border focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
props.class
)
" />
</template>

View File

@@ -0,0 +1,5 @@
export { default as NumberField } from './NumberField.vue';
export { default as NumberFieldContent } from './NumberFieldContent.vue';
export { default as NumberFieldDecrement } from './NumberFieldDecrement.vue';
export { default as NumberFieldIncrement } from './NumberFieldIncrement.vue';
export { default as NumberFieldInput } from './NumberFieldInput.vue';

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { SelectRootEmits, SelectRootProps } from 'reka-ui';
import { SelectRoot, useForwardPropsEmits } from 'reka-ui';
const props = defineProps<SelectRootProps>();
const emits = defineEmits<SelectRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<SelectRoot v-bind="forwarded">
<slot />
</SelectRoot>
</template>

View File

@@ -0,0 +1,61 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import {
SelectContent,
type SelectContentEmits,
type SelectContentProps,
SelectPortal,
SelectViewport,
useForwardPropsEmits,
} from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
import { SelectScrollDownButton, SelectScrollUpButton } from '.';
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 border-popover-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-[--reka-select-trigger-height] w-full min-w-[--reka-select-trigger-width]'
)
">
<slot />
</SelectViewport>
<SelectScrollDownButton />
</SelectContent>
</SelectPortal>
</template>

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { SelectGroup, type SelectGroupProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
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>

View File

@@ -0,0 +1,43 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { Check } from 'lucide-vue-next';
import {
SelectItem,
SelectItemIndicator,
type SelectItemProps,
SelectItemText,
useForwardProps,
} from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
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-2 pr-8 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 right-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>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { SelectItemText, type SelectItemTextProps } from 'reka-ui';
const props = defineProps<SelectItemTextProps>();
</script>
<template>
<SelectItemText v-bind="props">
<slot />
</SelectItemText>
</template>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
import { SelectLabel, type SelectLabelProps } from 'reka-ui';
const props = defineProps<SelectLabelProps & { class?: HTMLAttributes['class'] }>();
</script>
<template>
<SelectLabel :class="cn('px-2 py-1.5 text-sm font-semibold', props.class)">
<slot />
</SelectLabel>
</template>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { ChevronDown } from 'lucide-vue-next';
import { SelectScrollDownButton, type SelectScrollDownButtonProps, useForwardProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
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 />
</slot>
</SelectScrollDownButton>
</template>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { ChevronUp } from 'lucide-vue-next';
import { SelectScrollUpButton, type SelectScrollUpButtonProps, useForwardProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
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 />
</slot>
</SelectScrollUpButton>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { SelectSeparator, type SelectSeparatorProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
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>

View File

@@ -0,0 +1,78 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { ChevronDown } from 'lucide-vue-next';
import { SelectIcon, SelectTrigger, type SelectTriggerProps, useForwardProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = withDefaults(
defineProps<
SelectTriggerProps & {
size?: 'default' | 'sm' | 'lg';
class?: HTMLAttributes['class'];
showChevron?: boolean;
variant?: 'default' | 'outline';
active?: boolean;
}
>(),
{
showChevron: true,
variant: 'default',
active: false,
size: 'default',
}
);
const delegatedProps = computed(() => {
const {
class: _,
showChevron: __,
variant: ___,
active: ____,
size: _____,
...delegated
} = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
const sizeClasses = computed(() => {
switch (props.size) {
case 'sm':
return 'h-8 px-3 text-xs';
case 'lg':
return 'h-10 px-4 text-sm';
default:
return 'h-9 px-3 text-sm';
}
});
const variantClasses = computed(() => {
if (props.variant === 'outline') {
if (props.active) {
return 'border border-accent-300/50 bg-accent-50 hover:bg-accent-100 dark:border-accent-300/50 dark:bg-accent-300/5 dark:hover:bg-accent-300/10';
}
return 'border shadow-xs hover:text-text-primary bg-card-background dark:bg-transparent border-input dark:border-input hover:bg-white/5';
}
return 'border border-input-border bg-input-background shadow-sm';
});
</script>
<template>
<SelectTrigger
v-bind="forwardedProps"
:class="
cn(
'flex items-center justify-between gap-3 whitespace-nowrap rounded-md data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start font-medium transition-colors',
sizeClasses,
variantClasses,
props.class
)
">
<slot />
<SelectIcon v-if="showChevron" as-child>
<ChevronDown class="w-4 h-4 text-icon-default shrink-0" />
</SelectIcon>
</SelectTrigger>
</template>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { SelectValue, type SelectValueProps } from 'reka-ui';
const props = defineProps<SelectValueProps>();
</script>
<template>
<SelectValue v-bind="props">
<slot />
</SelectValue>
</template>

View File

@@ -0,0 +1,11 @@
export { default as Select } from './Select.vue';
export { default as SelectContent } from './SelectContent.vue';
export { default as SelectGroup } from './SelectGroup.vue';
export { default as SelectItem } from './SelectItem.vue';
export { default as SelectItemText } from './SelectItemText.vue';
export { default as SelectLabel } from './SelectLabel.vue';
export { default as SelectScrollDownButton } from './SelectScrollDownButton.vue';
export { default as SelectScrollUpButton } from './SelectScrollUpButton.vue';
export { default as SelectSeparator } from './SelectSeparator.vue';
export { default as SelectTrigger } from './SelectTrigger.vue';
export { default as SelectValue } from './SelectValue.vue';