chore: ⬆️ Upgrade to Nuxt 4
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 1s
Deploy to GitHub Pages / build (push) Failing after 1s
Deploy to GitHub Pages / deploy (push) Has been skipped
Docker / build (push) Failing after 1s
Mirror to Codeberg / Mirror (push) Failing after 1s

This commit is contained in:
Jesse Wierzbinski 2025-07-16 07:48:39 +02:00
parent 8debe97f63
commit 7f7cf20311
386 changed files with 2376 additions and 2332 deletions

View file

@ -0,0 +1,19 @@
<script setup lang="ts">
import {
type AlertDialogEmits,
type AlertDialogProps,
AlertDialogRoot,
useForwardPropsEmits,
} from "reka-ui";
const props = defineProps<AlertDialogProps>();
const emits = defineEmits<AlertDialogEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<AlertDialogRoot data-slot="alert-dialog" v-bind="forwarded">
<slot />
</AlertDialogRoot>
</template>

View file

@ -0,0 +1,22 @@
<script setup lang="ts">
import { AlertDialogAction, type AlertDialogActionProps } from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
const props = defineProps<
AlertDialogActionProps & { class?: HTMLAttributes["class"] }
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<AlertDialogAction v-bind="delegatedProps" :class="cn(buttonVariants(), props.class)">
<slot />
</AlertDialogAction>
</template>

View file

@ -0,0 +1,29 @@
<script setup lang="ts">
import { AlertDialogCancel, type AlertDialogCancelProps } from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
const props = defineProps<
AlertDialogCancelProps & { class?: HTMLAttributes["class"] }
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<AlertDialogCancel
v-bind="delegatedProps"
:class="cn(
buttonVariants({ variant: 'outline' }),
'mt-2 sm:mt-0',
props.class,
)"
>
<slot />
</AlertDialogCancel>
</template>

View file

@ -0,0 +1,46 @@
<script setup lang="ts">
import {
AlertDialogContent,
type AlertDialogContentEmits,
type AlertDialogContentProps,
AlertDialogOverlay,
AlertDialogPortal,
useForwardPropsEmits,
} from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<
AlertDialogContentProps & { class?: HTMLAttributes["class"] }
>();
const emits = defineEmits<AlertDialogContentEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<AlertDialogPortal>
<AlertDialogOverlay
data-slot="alert-dialog-overlay"
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80"
/>
<AlertDialogContent
data-slot="alert-dialog-content"
v-bind="forwarded"
:class="
cn(
'bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
props.class,
)
"
>
<slot />
</AlertDialogContent>
</AlertDialogPortal>
</template>

View file

@ -0,0 +1,28 @@
<script setup lang="ts">
import {
AlertDialogDescription,
type AlertDialogDescriptionProps,
} from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<
AlertDialogDescriptionProps & { class?: HTMLAttributes["class"] }
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<AlertDialogDescription
data-slot="alert-dialog-description"
v-bind="delegatedProps"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot />
</AlertDialogDescription>
</template>

View file

@ -0,0 +1,22 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<div
data-slot="alert-dialog-footer"
:class="
cn(
'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
props.class,
)
"
>
<slot />
</div>
</template>

View file

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<div
data-slot="alert-dialog-header"
:class="cn('flex flex-col gap-2 text-center sm:text-left', props.class)"
>
<slot />
</div>
</template>

View file

@ -0,0 +1,25 @@
<script setup lang="ts">
import { AlertDialogTitle, type AlertDialogTitleProps } from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<
AlertDialogTitleProps & { class?: HTMLAttributes["class"] }
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<AlertDialogTitle
data-slot="alert-dialog-title"
v-bind="delegatedProps"
:class="cn('text-lg font-semibold', props.class)"
>
<slot />
</AlertDialogTitle>
</template>

View file

@ -0,0 +1,11 @@
<script setup lang="ts">
import { AlertDialogTrigger, type AlertDialogTriggerProps } from "reka-ui";
const props = defineProps<AlertDialogTriggerProps>();
</script>
<template>
<AlertDialogTrigger data-slot="alert-dialog-trigger" v-bind="props">
<slot />
</AlertDialogTrigger>
</template>

View file

@ -0,0 +1,9 @@
export { default as AlertDialog } from "./AlertDialog.vue";
export { default as AlertDialogAction } from "./AlertDialogAction.vue";
export { default as AlertDialogCancel } from "./AlertDialogCancel.vue";
export { default as AlertDialogContent } from "./AlertDialogContent.vue";
export { default as AlertDialogDescription } from "./AlertDialogDescription.vue";
export { default as AlertDialogFooter } from "./AlertDialogFooter.vue";
export { default as AlertDialogHeader } from "./AlertDialogHeader.vue";
export { default as AlertDialogTitle } from "./AlertDialogTitle.vue";
export { default as AlertDialogTrigger } from "./AlertDialogTrigger.vue";

View file

@ -0,0 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
import { type AlertVariants, alertVariants } from ".";
const props = defineProps<{
class?: HTMLAttributes["class"];
variant?: AlertVariants["variant"];
layout?: AlertVariants["layout"];
}>();
</script>
<template>
<div
data-slot="alert"
:class="cn(alertVariants({ variant, layout }), props.class)"
role="alert"
>
<slot />
</div>
</template>

View file

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<div
data-slot="alert-description"
:class="cn('text-muted-foreground text-sm [&_p]:leading-relaxed', props.class)"
>
<slot />
</div>
</template>

View file

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<div
data-slot="alert-title"
:class="cn('line-clamp-1 min-h-4 font-medium tracking-tight', props.class)"
>
<slot />
</div>
</template>

View file

@ -0,0 +1,29 @@
import { cva, type VariantProps } from "class-variance-authority";
export { default as Alert } from "./Alert.vue";
export { default as AlertDescription } from "./AlertDescription.vue";
export { default as AlertTitle } from "./AlertTitle.vue";
export const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 grid text-sm [&>svg]:size-4 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
layout: {
default:
"has-[>svg]:grid-cols-[1fr_auto] grid-rows-2 gap-x-3 gap-y-1 items-start",
button: "grid-cols-[auto_1fr_auto] items-center gap-x-3 gap-y-0.5 *:data-[slot=alert-description]:col-start-2 *:data-[slot=alert-description]:row-start-2 has-[>[data-slot=alert-description]]:[&>button]:row-span-2",
},
},
defaultVariants: {
variant: "default",
layout: "default",
},
},
);
export type AlertVariants = VariantProps<typeof alertVariants>;

View file

@ -0,0 +1,18 @@
<script setup lang="ts">
import { AvatarRoot } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<AvatarRoot
data-slot="avatar"
:class="cn('relative flex size-8 shrink-0 overflow-hidden rounded-full', props.class)"
>
<slot />
</AvatarRoot>
</template>

View file

@ -0,0 +1,25 @@
<script setup lang="ts">
import { AvatarFallback, type AvatarFallbackProps } from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<
AvatarFallbackProps & { class?: HTMLAttributes["class"] }
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<AvatarFallback
data-slot="avatar-fallback"
v-bind="delegatedProps"
:class="cn('bg-muted flex size-full items-center justify-center', props.class)"
>
<slot />
</AvatarFallback>
</template>

View file

@ -0,0 +1,16 @@
<script setup lang="ts">
import type { AvatarImageProps } from "reka-ui";
import { AvatarImage } from "reka-ui";
const props = defineProps<AvatarImageProps>();
</script>
<template>
<AvatarImage
data-slot="avatar-image"
v-bind="props"
class="aspect-square size-full"
>
<slot />
</AvatarImage>
</template>

View file

@ -0,0 +1,3 @@
export { default as Avatar } from "./Avatar.vue";
export { default as AvatarFallback } from "./AvatarFallback.vue";
export { default as AvatarImage } from "./AvatarImage.vue";

View file

@ -0,0 +1,30 @@
<script setup lang="ts">
import type { PrimitiveProps } from "reka-ui";
import { Primitive } from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
import { type BadgeVariants, badgeVariants } from ".";
const props = defineProps<
PrimitiveProps & {
variant?: BadgeVariants["variant"];
class?: HTMLAttributes["class"];
}
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<Primitive
data-slot="badge"
:class="cn(badgeVariants({ variant }), props.class)"
v-bind="delegatedProps"
>
<slot />
</Primitive>
</template>

View file

@ -0,0 +1,25 @@
import { cva, type VariantProps } from "class-variance-authority";
export { default as Badge } from "./Badge.vue";
export const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
export type BadgeVariants = VariantProps<typeof badgeVariants>;

View file

@ -0,0 +1,27 @@
<script setup lang="ts">
import { Primitive, type PrimitiveProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
import { type ButtonVariants, buttonVariants } from ".";
interface Props extends PrimitiveProps {
variant?: ButtonVariants["variant"];
size?: ButtonVariants["size"];
class?: HTMLAttributes["class"];
}
const props = withDefaults(defineProps<Props>(), {
as: "button",
});
</script>
<template>
<Primitive
data-slot="button"
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>

View file

@ -0,0 +1,35 @@
import { cva, type VariantProps } from "class-variance-authority";
export { default as Button } from "./Button.vue";
export const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export type ButtonVariants = VariantProps<typeof buttonVariants>;

View file

@ -0,0 +1,18 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<div data-slot="card" :class="cn(
'bg-card text-card-foreground flex flex-col gap-6 rounded-md border p-4 shadow-sm',
props.class,
)
">
<slot />
</div>
</template>

View file

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<div
data-slot="card-action"
:class="cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', props.class)"
>
<slot />
</div>
</template>

View file

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<div
data-slot="card-content"
:class="cn('flex flex-col', props.class)"
>
<slot />
</div>
</template>

View file

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<p
data-slot="card-description"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot />
</p>
</template>

View file

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<div
data-slot="card-footer"
:class="cn('flex items-center [.border-t]:pt-6', props.class)"
>
<slot />
</div>
</template>

View file

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<div
data-slot="card-header"
:class="cn('@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6', props.class)"
>
<slot />
</div>
</template>

View file

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<h3
data-slot="card-title"
:class="cn('leading-none font-semibold', props.class)"
>
<slot />
</h3>
</template>

View file

@ -0,0 +1,7 @@
export { default as Card } from "./Card.vue";
export { default as CardAction } from "./CardAction.vue";
export { default as CardContent } from "./CardContent.vue";
export { default as CardDescription } from "./CardDescription.vue";
export { default as CardFooter } from "./CardFooter.vue";
export { default as CardHeader } from "./CardHeader.vue";
export { default as CardTitle } from "./CardTitle.vue";

View file

@ -0,0 +1,41 @@
<script setup lang="ts">
import { Check } from "lucide-vue-next";
import type { CheckboxRootEmits, CheckboxRootProps } from "reka-ui";
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<
CheckboxRootProps & { class?: HTMLAttributes["class"] }
>();
const emits = defineEmits<CheckboxRootEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<CheckboxRoot
data-slot="checkbox"
v-bind="forwarded"
:class="
cn('peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
props.class)"
>
<CheckboxIndicator
data-slot="checkbox-indicator"
class="flex items-center justify-center text-current transition-none"
>
<slot>
<Check class="size-3.5" />
</slot>
</CheckboxIndicator>
<!-- Fixes an issue where empty buttons behave weirdly in tanstack table layouts -->
<Check class="size-3.5 opacity-0" />
</CheckboxRoot>
</template>

View file

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

View file

@ -0,0 +1,19 @@
<script setup lang="ts">
import type { CollapsibleRootEmits, CollapsibleRootProps } from "reka-ui";
import { CollapsibleRoot, useForwardPropsEmits } from "reka-ui";
const props = defineProps<CollapsibleRootProps>();
const emits = defineEmits<CollapsibleRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<CollapsibleRoot
v-slot="{ open }"
data-slot="collapsible"
v-bind="forwarded"
>
<slot :open="open" />
</CollapsibleRoot>
</template>

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import { CollapsibleContent, type CollapsibleContentProps } from "reka-ui";
const props = defineProps<CollapsibleContentProps>();
</script>
<template>
<CollapsibleContent
data-slot="collapsible-content"
v-bind="props"
>
<slot />
</CollapsibleContent>
</template>

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import { CollapsibleTrigger, type CollapsibleTriggerProps } from "reka-ui";
const props = defineProps<CollapsibleTriggerProps>();
</script>
<template>
<CollapsibleTrigger
data-slot="collapsible-trigger"
v-bind="props"
>
<slot />
</CollapsibleTrigger>
</template>

View file

@ -0,0 +1,3 @@
export { default as Collapsible } from "./Collapsible.vue";
export { default as CollapsibleContent } from "./CollapsibleContent.vue";
export { default as CollapsibleTrigger } from "./CollapsibleTrigger.vue";

View file

@ -0,0 +1,100 @@
<script setup lang="ts">
import type { ListboxRootEmits, ListboxRootProps } from "reka-ui";
import { ListboxRoot, useFilter, useForwardPropsEmits } from "reka-ui";
import { computed, type HTMLAttributes, reactive, ref, watch } from "vue";
import { cn } from "@/lib/utils";
import { provideCommandContext } from ".";
const props = withDefaults(
defineProps<ListboxRootProps & { class?: HTMLAttributes["class"] }>(),
{
modelValue: "",
},
);
const emits = defineEmits<ListboxRootEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const allItems = ref<Map<string, string>>(new Map());
const allGroups = ref<Map<string, Set<string>>>(new Map());
const { contains } = useFilter({ sensitivity: "base" });
const filterState = reactive({
search: "",
filtered: {
/** The count of all visible items. */
count: 0,
/** Map from visible item id to its search score. */
items: new Map() as Map<string, number>,
/** Set of groups with at least one visible item. */
groups: new Set() as Set<string>,
},
});
function filterItems() {
if (!filterState.search) {
filterState.filtered.count = allItems.value.size;
// Do nothing, each item will know to show itself because search is empty
return;
}
// Reset the groups
filterState.filtered.groups = new Set();
let itemCount = 0;
// Check which items should be included
for (const [id, value] of allItems.value) {
const score = contains(value, filterState.search);
filterState.filtered.items.set(id, score ? 1 : 0);
if (score) {
itemCount++;
}
}
// Check which groups have at least 1 item shown
for (const [groupId, group] of allGroups.value) {
for (const itemId of group) {
if ((filterState.filtered.items.get(itemId) ?? 0) > 0) {
filterState.filtered.groups.add(groupId);
break;
}
}
}
filterState.filtered.count = itemCount;
}
function handleSelect() {
filterState.search = "";
}
watch(
() => filterState.search,
() => {
filterItems();
},
);
provideCommandContext({
allItems,
allGroups,
filterState,
});
</script>
<template>
<ListboxRoot
data-slot="command"
v-bind="forwarded"
:class="cn('bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md', props.class)"
>
<slot />
</ListboxRoot>
</template>

View file

@ -0,0 +1,42 @@
<script setup lang="ts">
import type { DialogRootEmits, DialogRootProps } from "reka-ui";
import { useForwardPropsEmits } from "reka-ui";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import Command from "./Command.vue";
const props = withDefaults(
defineProps<
DialogRootProps & {
title?: string;
description?: string;
}
>(),
{
title: "Command Palette",
description: "Search for a command to run...",
},
);
const emits = defineEmits<DialogRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<Dialog v-bind="forwarded">
<DialogHeader class="sr-only">
<DialogTitle>{{ title }}</DialogTitle>
<DialogDescription>{{ description }}</DialogDescription>
</DialogHeader>
<DialogContent class="overflow-hidden p-0 ">
<Command>
<slot />
</Command>
</DialogContent>
</Dialog>
</template>

View file

@ -0,0 +1,32 @@
<script setup lang="ts">
import type { PrimitiveProps } from "reka-ui";
import { Primitive } from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
import { useCommand } from ".";
const props = defineProps<
PrimitiveProps & { class?: HTMLAttributes["class"] }
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const { filterState } = useCommand();
const isRender = computed(
() => !!filterState.search && filterState.filtered.count === 0,
);
</script>
<template>
<Primitive
v-if="isRender"
data-slot="command-empty"
v-bind="delegatedProps" :class="cn('py-6 text-center text-sm', props.class)"
>
<slot />
</Primitive>
</template>

View file

@ -0,0 +1,52 @@
<script setup lang="ts">
import type { ListboxGroupProps } from "reka-ui";
import { ListboxGroup, ListboxGroupLabel, useId } from "reka-ui";
import { computed, type HTMLAttributes, onMounted, onUnmounted } from "vue";
import { cn } from "@/lib/utils";
import { provideCommandGroupContext, useCommand } from ".";
const props = defineProps<
ListboxGroupProps & {
class?: HTMLAttributes["class"];
heading?: string;
}
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const { allGroups, filterState } = useCommand();
const id = useId();
const isRender = computed(() =>
filterState.search ? filterState.filtered.groups.has(id) : true,
);
provideCommandGroupContext({ id });
onMounted(() => {
if (!allGroups.value.has(id)) {
allGroups.value.set(id, new Set());
}
});
onUnmounted(() => {
allGroups.value.delete(id);
});
</script>
<template>
<ListboxGroup
v-bind="delegatedProps"
:id="id"
data-slot="command-group"
:class="cn('text-foreground overflow-hidden p-1', props.class)"
:hidden="isRender ? undefined : true"
>
<ListboxGroupLabel v-if="heading" class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
{{ heading }}
</ListboxGroupLabel>
<slot />
</ListboxGroup>
</template>

View file

@ -0,0 +1,47 @@
<script setup lang="ts">
import { Search } from "lucide-vue-next";
import {
ListboxFilter,
type ListboxFilterProps,
useForwardProps,
} from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
import { useCommand } from ".";
defineOptions({
inheritAttrs: false,
});
const props = defineProps<
ListboxFilterProps & {
class?: HTMLAttributes["class"];
}
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
const { filterState } = useCommand();
</script>
<template>
<div
data-slot="command-input-wrapper"
class="flex h-12 items-center gap-2 border-b px-3"
>
<Search class="size-4 shrink-0 opacity-50" />
<ListboxFilter
v-bind="{ ...forwardedProps, ...$attrs }"
v-model="filterState.search"
data-slot="command-input"
auto-focus
:class="cn('placeholder:text-muted-foreground flex h-12 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50', props.class)"
/>
</div>
</template>

View file

@ -0,0 +1,89 @@
<script setup lang="ts">
import { useCurrentElement } from "@vueuse/core";
import type { ListboxItemEmits, ListboxItemProps } from "reka-ui";
import { ListboxItem, useForwardPropsEmits, useId } from "reka-ui";
import {
computed,
type HTMLAttributes,
onMounted,
onUnmounted,
ref,
} from "vue";
import { cn } from "@/lib/utils";
import { useCommand, useCommandGroup } from ".";
const props = defineProps<
ListboxItemProps & { class?: HTMLAttributes["class"] }
>();
const emits = defineEmits<ListboxItemEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const id = useId();
const { filterState, allItems, allGroups } = useCommand();
const groupContext = useCommandGroup();
const isRender = computed(() => {
if (filterState.search) {
const filteredCurrentItem = filterState.filtered.items.get(id);
// If the filtered items is undefined means not in the all times map yet
// Do the first render to add into the map
if (filteredCurrentItem === undefined) {
return true;
}
// Check with filter
return filteredCurrentItem > 0;
}
return true;
});
const itemRef = ref();
const currentElement = useCurrentElement(itemRef);
onMounted(() => {
if (!(currentElement.value instanceof HTMLElement)) {
return;
}
// textValue to perform filter
allItems.value.set(
id,
currentElement.value.textContent ?? props.value?.toString() ?? "",
);
const groupId = groupContext?.id;
if (groupId) {
if (allGroups.value.has(groupId)) {
allGroups.value.get(groupId)?.add(id);
} else {
allGroups.value.set(groupId, new Set([id]));
}
}
});
onUnmounted(() => {
allItems.value.delete(id);
});
</script>
<template>
<ListboxItem
v-if="isRender"
v-bind="forwarded"
:id="id"
ref="itemRef"
data-slot="command-item"
:class="cn(`data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-2 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`, props.class)"
@select="() => {
filterState.search = ''
}"
>
<slot />
</ListboxItem>
</template>

View file

@ -0,0 +1,30 @@
<script setup lang="ts">
import type { ListboxContentProps } from "reka-ui";
import { ListboxContent, useForwardProps } from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<
ListboxContentProps & { class?: HTMLAttributes["class"] }
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardProps(delegatedProps);
</script>
<template>
<ListboxContent
data-slot="command-list"
v-bind="forwarded"
:class="cn('max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto', props.class)"
>
<div role="presentation">
<slot />
</div>
</ListboxContent>
</template>

View file

@ -0,0 +1,26 @@
<script setup lang="ts">
import type { SeparatorProps } from "reka-ui";
import { Separator } from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<
SeparatorProps & { class?: HTMLAttributes["class"] }
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<Separator
data-slot="command-separator"
v-bind="delegatedProps"
:class="cn('bg-border -mx-1 h-px', props.class)"
>
<slot />
</Separator>
</template>

View file

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<span
data-slot="command-shortcut"
:class="cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)"
>
<slot />
</span>
</template>

View file

@ -0,0 +1,29 @@
import { createContext } from "reka-ui";
import type { Ref } from "vue";
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";
export const [useCommand, provideCommandContext] = createContext<{
allItems: Ref<Map<string, string>>;
allGroups: Ref<Map<string, Set<string>>>;
filterState: {
search: string;
filtered: {
count: number;
items: Map<string, number>;
groups: Set<string>;
};
};
}>("Command");
export const [useCommandGroup, provideCommandGroupContext] = createContext<{
id?: string;
}>("CommandGroup");

View file

@ -0,0 +1,22 @@
<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
data-slot="dialog"
v-bind="forwarded"
>
<slot />
</DialogRoot>
</template>

View file

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

View file

@ -0,0 +1,55 @@
<script setup lang="ts">
import { X } from "lucide-vue-next";
import {
DialogClose,
DialogContent,
type DialogContentEmits,
type DialogContentProps,
DialogPortal,
useForwardPropsEmits,
} from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
import DialogOverlay from "./DialogOverlay.vue";
const props = defineProps<
DialogContentProps & {
class?: HTMLAttributes["class"];
hideClose?: boolean;
}
>();
const emits = defineEmits<DialogContentEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DialogPortal>
<DialogOverlay />
<DialogContent
data-slot="dialog-content"
v-bind="forwarded"
:class="
cn(
'bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] max-h-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
props.class,
)"
>
<slot />
<DialogClose
v-if="!hideClose"
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<X />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogPortal>
</template>

View file

@ -0,0 +1,31 @@
<script setup lang="ts">
import {
DialogDescription,
type DialogDescriptionProps,
useForwardProps,
} from "reka-ui";
import { computed, type HTMLAttributes } from "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
data-slot="dialog-description"
v-bind="forwardedProps"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot />
</DialogDescription>
</template>

View file

@ -0,0 +1,15 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{ class?: HTMLAttributes["class"] }>();
</script>
<template>
<div
data-slot="dialog-footer"
:class="cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', props.class)"
>
<slot />
</div>
</template>

View file

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<div
data-slot="dialog-header"
:class="cn('flex flex-col gap-2 text-center sm:text-left', props.class)"
>
<slot />
</div>
</template>

View file

@ -0,0 +1,25 @@
<script setup lang="ts">
import { DialogOverlay, type DialogOverlayProps } from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<
DialogOverlayProps & { class?: HTMLAttributes["class"] }
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<DialogOverlay
data-slot="dialog-overlay"
v-bind="delegatedProps"
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 backdrop-blur-md', props.class)"
>
<slot />
</DialogOverlay>
</template>

View file

@ -0,0 +1,61 @@
<script setup lang="ts">
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";
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-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,27 @@
<script setup lang="ts">
import { DialogTitle, type DialogTitleProps, useForwardProps } from "reka-ui";
import { computed, type HTMLAttributes } from "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
data-slot="dialog-title"
v-bind="forwardedProps"
:class="cn('text-lg leading-none font-semibold', props.class)"
>
<slot />
</DialogTitle>
</template>

View file

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

View file

@ -0,0 +1,10 @@
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 DialogOverlay } from "./DialogOverlay.vue";
export { default as DialogScrollContent } from "./DialogScrollContent.vue";
export { default as DialogTitle } from "./DialogTitle.vue";
export { default as DialogTrigger } from "./DialogTrigger.vue";

View file

@ -0,0 +1,22 @@
<script lang="ts" setup>
import { useForwardPropsEmits } from "reka-ui";
import type { DrawerRootEmits, DrawerRootProps } from "vaul-vue";
import { DrawerRoot } from "vaul-vue";
const props = withDefaults(defineProps<DrawerRootProps>(), {
shouldScaleBackground: true,
}) as DrawerRootProps;
const emits = defineEmits<DrawerRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DrawerRoot
data-slot="drawer"
v-bind="forwarded"
>
<slot />
</DrawerRoot>
</template>

View file

@ -0,0 +1,15 @@
<script lang="ts" setup>
import type { DrawerCloseProps } from "vaul-vue";
import { DrawerClose } from "vaul-vue";
const props = defineProps<DrawerCloseProps>();
</script>
<template>
<DrawerClose
data-slot="drawer-close"
v-bind="props"
>
<slot />
</DrawerClose>
</template>

View file

@ -0,0 +1,36 @@
<script lang="ts" setup>
import type { DialogContentEmits, DialogContentProps } from "reka-ui";
import { useForwardPropsEmits } from "reka-ui";
import { DrawerContent, DrawerPortal } from "vaul-vue";
import type { HtmlHTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
import DrawerOverlay from "./DrawerOverlay.vue";
const props = defineProps<
DialogContentProps & { class?: HtmlHTMLAttributes["class"] }
>();
const emits = defineEmits<DialogContentEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DrawerPortal>
<DrawerOverlay />
<DrawerContent
data-slot="drawer-content"
v-bind="forwarded"
:class="cn(
`group/drawer-content bg-background fixed z-50 flex h-auto flex-col`,
`data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg`,
`data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg`,
`data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:sm:max-w-sm`,
`data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:sm:max-w-sm`,
props.class,
)"
>
<div class="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
<slot />
</DrawerContent>
</DrawerPortal>
</template>

View file

@ -0,0 +1,26 @@
<script lang="ts" setup>
import type { DrawerDescriptionProps } from "vaul-vue";
import { DrawerDescription } from "vaul-vue";
import { computed, type HtmlHTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<
DrawerDescriptionProps & { class?: HtmlHTMLAttributes["class"] }
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<DrawerDescription
data-slot="drawer-description"
v-bind="delegatedProps"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot />
</DrawerDescription>
</template>

View file

@ -0,0 +1,17 @@
<script lang="ts" setup>
import type { HtmlHTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HtmlHTMLAttributes["class"];
}>();
</script>
<template>
<div
data-slot="drawer-footer"
:class="cn('mt-auto flex flex-col gap-2 p-4', props.class)"
>
<slot />
</div>
</template>

View file

@ -0,0 +1,17 @@
<script lang="ts" setup>
import type { HtmlHTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HtmlHTMLAttributes["class"];
}>();
</script>
<template>
<div
data-slot="drawer-header"
:class="cn('flex flex-col gap-1.5 p-4', props.class)"
>
<slot />
</div>
</template>

View file

@ -0,0 +1,24 @@
<script lang="ts" setup>
import type { DialogOverlayProps } from "reka-ui";
import { DrawerOverlay } from "vaul-vue";
import { computed, type HtmlHTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<
DialogOverlayProps & { class?: HtmlHTMLAttributes["class"] }
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<DrawerOverlay
data-slot="drawer-overlay"
v-bind="delegatedProps"
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80', props.class)"
/>
</template>

View file

@ -0,0 +1,26 @@
<script lang="ts" setup>
import type { DrawerTitleProps } from "vaul-vue";
import { DrawerTitle } from "vaul-vue";
import { computed, type HtmlHTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<
DrawerTitleProps & { class?: HtmlHTMLAttributes["class"] }
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<DrawerTitle
data-slot="drawer-title"
v-bind="delegatedProps"
:class="cn('text-foreground font-semibold', props.class)"
>
<slot />
</DrawerTitle>
</template>

View file

@ -0,0 +1,15 @@
<script lang="ts" setup>
import type { DrawerTriggerProps } from "vaul-vue";
import { DrawerTrigger } from "vaul-vue";
const props = defineProps<DrawerTriggerProps>();
</script>
<template>
<DrawerTrigger
data-slot="drawer-trigger"
v-bind="props"
>
<slot />
</DrawerTrigger>
</template>

View file

@ -0,0 +1,9 @@
export { default as Drawer } from "./Drawer.vue";
export { default as DrawerClose } from "./DrawerClose.vue";
export { default as DrawerContent } from "./DrawerContent.vue";
export { default as DrawerDescription } from "./DrawerDescription.vue";
export { default as DrawerFooter } from "./DrawerFooter.vue";
export { default as DrawerHeader } from "./DrawerHeader.vue";
export { default as DrawerOverlay } from "./DrawerOverlay.vue";
export { default as DrawerTitle } from "./DrawerTitle.vue";
export { default as DrawerTrigger } from "./DrawerTrigger.vue";

View file

@ -0,0 +1,22 @@
<script setup lang="ts">
import {
DropdownMenuRoot,
type DropdownMenuRootEmits,
type DropdownMenuRootProps,
useForwardPropsEmits,
} from "reka-ui";
const props = defineProps<DropdownMenuRootProps>();
const emits = defineEmits<DropdownMenuRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DropdownMenuRoot
data-slot="dropdown-menu"
v-bind="forwarded"
>
<slot />
</DropdownMenuRoot>
</template>

View file

@ -0,0 +1,43 @@
<script setup lang="ts">
import { Check } from "lucide-vue-next";
import {
DropdownMenuCheckboxItem,
type DropdownMenuCheckboxItemEmits,
type DropdownMenuCheckboxItemProps,
DropdownMenuItemIndicator,
useForwardPropsEmits,
} from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
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
data-slot="dropdown-menu-checkbox-item"
v-bind="forwarded"
:class=" cn(
`focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
props.class,
)"
>
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<Check class="size-4" />
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuCheckboxItem>
</template>

View file

@ -0,0 +1,41 @@
<script setup lang="ts">
import {
DropdownMenuContent,
type DropdownMenuContentEmits,
type DropdownMenuContentProps,
DropdownMenuPortal,
useForwardPropsEmits,
} from "reka-ui";
import { computed, type HTMLAttributes } from "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
data-slot="dropdown-menu-content"
v-bind="forwarded"
:class="cn('bg-popover text-popover-foreground 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 max-h-(--reka-dropdown-menu-content-available-height) min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md', props.class)"
>
<slot />
</DropdownMenuContent>
</DropdownMenuPortal>
</template>

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import { DropdownMenuGroup, type DropdownMenuGroupProps } from "reka-ui";
const props = defineProps<DropdownMenuGroupProps>();
</script>
<template>
<DropdownMenuGroup
data-slot="dropdown-menu-group"
v-bind="props"
>
<slot />
</DropdownMenuGroup>
</template>

View file

@ -0,0 +1,39 @@
<script setup lang="ts">
import { reactiveOmit } from "@vueuse/core";
import {
DropdownMenuItem,
type DropdownMenuItemProps,
useForwardProps,
} from "reka-ui";
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = withDefaults(
defineProps<
DropdownMenuItemProps & {
class?: HTMLAttributes["class"];
inset?: boolean;
variant?: "default" | "destructive";
}
>(),
{
variant: "default",
},
);
const delegatedProps = reactiveOmit(props, "inset", "variant");
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DropdownMenuItem
data-slot="dropdown-menu-item"
:data-inset="inset ? '' : undefined"
:data-variant="variant"
v-bind="forwardedProps"
:class="cn(`focus:bg-accent w-full focus:text-accent-foreground data-[variant=destructive]:text-destructive-foreground data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive-foreground data-[variant=destructive]:*:[svg]:!text-destructive-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`, props.class)"
>
<slot />
</DropdownMenuItem>
</template>

View file

@ -0,0 +1,31 @@
<script setup lang="ts">
import { reactiveOmit } from "@vueuse/core";
import {
DropdownMenuLabel,
type DropdownMenuLabelProps,
useForwardProps,
} from "reka-ui";
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<
DropdownMenuLabelProps & {
class?: HTMLAttributes["class"];
inset?: boolean;
}
>();
const delegatedProps = reactiveOmit(props, "class", "inset");
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DropdownMenuLabel
data-slot="dropdown-menu-label"
:data-inset="inset ? '' : undefined"
v-bind="forwardedProps"
:class="cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', props.class)"
>
<slot />
</DropdownMenuLabel>
</template>

View file

@ -0,0 +1,22 @@
<script setup lang="ts">
import {
DropdownMenuRadioGroup,
type DropdownMenuRadioGroupEmits,
type DropdownMenuRadioGroupProps,
useForwardPropsEmits,
} from "reka-ui";
const props = defineProps<DropdownMenuRadioGroupProps>();
const emits = defineEmits<DropdownMenuRadioGroupEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DropdownMenuRadioGroup
data-slot="dropdown-menu-radio-group"
v-bind="forwarded"
>
<slot />
</DropdownMenuRadioGroup>
</template>

View file

@ -0,0 +1,44 @@
<script setup lang="ts">
import { Circle } from "lucide-vue-next";
import {
DropdownMenuItemIndicator,
DropdownMenuRadioItem,
type DropdownMenuRadioItemEmits,
type DropdownMenuRadioItemProps,
useForwardPropsEmits,
} from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
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
data-slot="dropdown-menu-radio-item"
v-bind="forwarded"
:class="cn(
`focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
props.class,
)"
>
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<Circle class="size-2 fill-current" />
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuRadioItem>
</template>

View file

@ -0,0 +1,28 @@
<script setup lang="ts">
import {
DropdownMenuSeparator,
type DropdownMenuSeparatorProps,
} from "reka-ui";
import { computed, type HTMLAttributes } from "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
data-slot="dropdown-menu-separator"
v-bind="delegatedProps"
:class="cn('bg-border -mx-1 my-1 h-px', props.class)"
/>
</template>

View file

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<span
data-slot="dropdown-menu-shortcut"
:class="cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)"
>
<slot />
</span>
</template>

View file

@ -0,0 +1,19 @@
<script setup lang="ts">
import {
DropdownMenuSub,
type DropdownMenuSubEmits,
type DropdownMenuSubProps,
useForwardPropsEmits,
} from "reka-ui";
const props = defineProps<DropdownMenuSubProps>();
const emits = defineEmits<DropdownMenuSubEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DropdownMenuSub data-slot="dropdown-menu-sub" v-bind="forwarded">
<slot />
</DropdownMenuSub>
</template>

View file

@ -0,0 +1,33 @@
<script setup lang="ts">
import {
DropdownMenuSubContent,
type DropdownMenuSubContentEmits,
type DropdownMenuSubContentProps,
useForwardPropsEmits,
} from "reka-ui";
import { computed, type HTMLAttributes } from "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
data-slot="dropdown-menu-sub-content"
v-bind="forwarded"
:class="cn('bg-popover text-popover-foreground 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 min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg', props.class)"
>
<slot />
</DropdownMenuSubContent>
</template>

View file

@ -0,0 +1,35 @@
<script setup lang="ts">
import { reactiveOmit } from "@vueuse/core";
import { ChevronRight } from "lucide-vue-next";
import {
DropdownMenuSubTrigger,
type DropdownMenuSubTriggerProps,
useForwardProps,
} from "reka-ui";
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<
DropdownMenuSubTriggerProps & {
class?: HTMLAttributes["class"];
inset?: boolean;
}
>();
const delegatedProps = reactiveOmit(props, "class", "inset");
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DropdownMenuSubTrigger
data-slot="dropdown-menu-sub-trigger"
v-bind="forwardedProps"
:class="cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
props.class,
)"
>
<slot />
<ChevronRight class="ml-auto size-4" />
</DropdownMenuSubTrigger>
</template>

View file

@ -0,0 +1,20 @@
<script setup lang="ts">
import {
DropdownMenuTrigger,
type DropdownMenuTriggerProps,
useForwardProps,
} from "reka-ui";
const props = defineProps<DropdownMenuTriggerProps>();
const forwardedProps = useForwardProps(props);
</script>
<template>
<DropdownMenuTrigger
data-slot="dropdown-menu-trigger"
v-bind="forwardedProps"
>
<slot />
</DropdownMenuTrigger>
</template>

View file

@ -0,0 +1,15 @@
export { DropdownMenuPortal } from "reka-ui";
export { default as DropdownMenu } from "./DropdownMenu.vue";
export { default as DropdownMenuCheckboxItem } from "./DropdownMenuCheckboxItem.vue";
export { default as DropdownMenuContent } from "./DropdownMenuContent.vue";
export { default as DropdownMenuGroup } from "./DropdownMenuGroup.vue";
export { default as DropdownMenuItem } from "./DropdownMenuItem.vue";
export { default as DropdownMenuLabel } from "./DropdownMenuLabel.vue";
export { default as DropdownMenuRadioGroup } from "./DropdownMenuRadioGroup.vue";
export { default as DropdownMenuRadioItem } from "./DropdownMenuRadioItem.vue";
export { default as DropdownMenuSeparator } from "./DropdownMenuSeparator.vue";
export { default as DropdownMenuShortcut } from "./DropdownMenuShortcut.vue";
export { default as DropdownMenuSub } from "./DropdownMenuSub.vue";
export { default as DropdownMenuSubContent } from "./DropdownMenuSubContent.vue";
export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue";
export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue";

View file

@ -0,0 +1,17 @@
<script lang="ts" setup>
import { Slot } from "reka-ui";
import { useFormField } from "./useFormField";
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
</script>
<template>
<Slot
:id="formItemId"
data-slot="form-control"
:aria-describedby="!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`"
:aria-invalid="!!error"
>
<slot />
</Slot>
</template>

View file

@ -0,0 +1,21 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
import { useFormField } from "./useFormField";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
const { formDescriptionId } = useFormField();
</script>
<template>
<p
:id="formDescriptionId"
data-slot="form-description"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot />
</p>
</template>

View file

@ -0,0 +1,22 @@
<script lang="ts" setup>
import { useId } from "reka-ui";
import { type HTMLAttributes, provide } from "vue";
import { cn } from "@/lib/utils";
import { FORM_ITEM_INJECTION_KEY } from "./injectionKeys";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
const id = useId();
provide(FORM_ITEM_INJECTION_KEY, id);
</script>
<template>
<div
data-slot="form-item"
:class="cn('grid gap-2', props.class)"
>
<slot />
</div>
</template>

View file

@ -0,0 +1,25 @@
<script lang="ts" setup>
import type { LabelProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import { useFormField } from "./useFormField";
const props = defineProps<LabelProps & { class?: HTMLAttributes["class"] }>();
const { error, formItemId } = useFormField();
</script>
<template>
<Label
data-slot="form-label"
:data-error="!!error"
:class="cn(
'data-[error=true]:text-destructive-foreground',
props.class,
)"
:for="formItemId"
>
<slot />
</Label>
</template>

View file

@ -0,0 +1,22 @@
<script lang="ts" setup>
import { ErrorMessage } from "vee-validate";
import { type HTMLAttributes, toValue } from "vue";
import { cn } from "@/lib/utils";
import { useFormField } from "./useFormField";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
const { name, formMessageId } = useFormField();
</script>
<template>
<ErrorMessage
:id="formMessageId"
data-slot="form-message"
as="p"
:name="toValue(name)"
:class="cn('text-destructive-foreground text-sm', props.class)"
/>
</template>

View file

@ -0,0 +1,11 @@
export {
Field as FormField,
FieldArray as FormFieldArray,
Form,
} from "vee-validate";
export { default as FormControl } from "./FormControl.vue";
export { default as FormDescription } from "./FormDescription.vue";
export { default as FormItem } from "./FormItem.vue";
export { default as FormLabel } from "./FormLabel.vue";
export { default as FormMessage } from "./FormMessage.vue";
export { FORM_ITEM_INJECTION_KEY } from "./injectionKeys";

View file

@ -0,0 +1,3 @@
import type { InjectionKey } from "vue";
export const FORM_ITEM_INJECTION_KEY = Symbol() as InjectionKey<string>;

View file

@ -0,0 +1,37 @@
import {
FieldContextKey,
useFieldError,
useIsFieldDirty,
useIsFieldTouched,
useIsFieldValid,
} from "vee-validate";
import { inject } from "vue";
import { FORM_ITEM_INJECTION_KEY } from "./injectionKeys";
export function useFormField() {
const fieldContext = inject(FieldContextKey);
const fieldItemContext = inject(FORM_ITEM_INJECTION_KEY);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
const { name } = fieldContext;
const id = fieldItemContext;
const fieldState = {
valid: useIsFieldValid(name),
isDirty: useIsFieldDirty(name),
isTouched: useIsFieldTouched(name),
error: useFieldError(name),
};
return {
id,
name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
}

View file

@ -0,0 +1,22 @@
<script setup lang="ts">
import {
HoverCardRoot,
type HoverCardRootEmits,
type HoverCardRootProps,
useForwardPropsEmits,
} from "reka-ui";
const props = defineProps<HoverCardRootProps>();
const emits = defineEmits<HoverCardRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<HoverCardRoot
data-slot="hover-card"
v-bind="forwarded"
>
<slot />
</HoverCardRoot>
</template>

View file

@ -0,0 +1,42 @@
<script setup lang="ts">
import {
HoverCardContent,
type HoverCardContentProps,
HoverCardPortal,
useForwardProps,
} from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = withDefaults(
defineProps<HoverCardContentProps & { class?: HTMLAttributes["class"] }>(),
{
sideOffset: 4,
},
);
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<HoverCardPortal>
<HoverCardContent
data-slot="hover-card-content"
v-bind="forwardedProps"
:class="
cn(
'bg-popover text-popover-foreground 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 w-64 rounded-md border p-4 shadow-md outline-hidden',
props.class,
)
"
>
<slot />
</HoverCardContent>
</HoverCardPortal>
</template>

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import { HoverCardTrigger, type HoverCardTriggerProps } from "reka-ui";
const props = defineProps<HoverCardTriggerProps>();
</script>
<template>
<HoverCardTrigger
data-slot="hover-card-trigger"
v-bind="props"
>
<slot />
</HoverCardTrigger>
</template>

View file

@ -0,0 +1,3 @@
export { default as HoverCard } from "./HoverCard.vue";
export { default as HoverCardContent } from "./HoverCardContent.vue";
export { default as HoverCardTrigger } from "./HoverCardTrigger.vue";

View file

@ -0,0 +1,32 @@
<script setup lang="ts">
import { useVModel } from "@vueuse/core";
import type { HTMLAttributes } from "vue";
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"
data-slot="input"
:class="cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
props.class,
)"
>
</template>

View file

@ -0,0 +1,2 @@
export { default as Input } from "./Input.vue";
export { default as UrlInput } from "./url.vue";

View file

@ -0,0 +1,57 @@
<script setup lang="ts">
import { useVModel } from "@vueuse/core";
import { Check, X } from "lucide-vue-next";
import type { HTMLAttributes } from "vue";
import * as m from "~~/paraglide/messages.js";
import Input from "./Input.vue";
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,
});
const isValid = defineModel<boolean>("isValid");
const tryGuessUrl = (string: string) =>
URL.canParse(`https://${string}`) &&
string.includes(".") &&
string.length > 3 &&
string.charAt(string.length - 1) !== ".";
const isValidUrl = computed(
() =>
URL.canParse(modelValue.value as string) ||
tryGuessUrl(modelValue.value as string),
);
watch(modelValue, (value) => {
if (!URL.canParse(value as string) && tryGuessUrl(value as string)) {
modelValue.value = `https://${value}`;
}
});
watch(isValidUrl, (value) => {
isValid.value = value;
});
</script>
<template>
<div class="space-y-2">
<Input v-model="modelValue" v-bind="$attrs" />
<p v-if="isValidUrl" class="text-green-600 text-xs">
{{ m.sunny_small_warbler_express() }}
</p>
<p v-else-if="(modelValue?.toString().length ?? 0) > 0" class="text-destructive text-xs">
{{ m.teal_late_grebe_blend() }}
</p>
</div>
</template>

View file

@ -0,0 +1,28 @@
<script setup lang="ts">
import { Label, type LabelProps } from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<LabelProps & { class?: HTMLAttributes["class"] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<Label
data-slot="label"
v-bind="delegatedProps"
:class="
cn(
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
props.class,
)
"
>
<slot />
</Label>
</template>

View file

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

View file

@ -0,0 +1,25 @@
<script setup lang="ts">
import type { NumberFieldRootEmits, NumberFieldRootProps } from "reka-ui";
import { NumberFieldRoot, useForwardPropsEmits } from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<
NumberFieldRootProps & { class?: HTMLAttributes["class"] }
>();
const emits = defineEmits<NumberFieldRootEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<NumberFieldRoot v-bind="forwarded" :class="cn('grid gap-1.5', props.class)">
<slot />
</NumberFieldRoot>
</template>

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