refactor: ♻️ Make corner radius more consistent across UI

This commit is contained in:
Jesse Wierzbinski 2025-02-09 18:53:22 +01:00
parent 43eebfcd94
commit 9b5187207b
No known key found for this signature in database
33 changed files with 34 additions and 485 deletions

View file

@ -1,5 +1,5 @@
<template>
<Command class="rounded-lg border shadow-md min-w-[200px]" :selected-value="items[selectedIndex]?.key">
<Command class="rounded border shadow-md min-w-[200px]" :selected-value="items[selectedIndex]?.key">
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup class="mentions-group" heading="Users">

View file

@ -19,7 +19,7 @@
$style.content,
]" v-html="content" v-render-emojis="emojis"></div>
<div v-if="isOverflowing && collapsed"
class="absolute inset-x-0 bottom-0 h-36 bg-gradient-to-t from-black/5 to-transparent rounded-b-md"></div>
class="absolute inset-x-0 bottom-0 h-36 bg-gradient-to-t from-black/5 to-transparent rounded-b"></div>
<Button v-if="isOverflowing" @click="collapsed = !collapsed"
class="absolute bottom-2 right-1/2 translate-x-1/2">{{
collapsed

View file

@ -9,7 +9,7 @@
</path>
</g>
</svg>
<img v-else-if="icon" :src="icon" alt="" class="size-4 rounded-sm" />
<img v-else-if="icon" :src="icon" alt="" class="size-4 rounded" />
{{ name }}
</Badge>
</TooltipTrigger>

View file

@ -35,7 +35,7 @@
<DropdownMenuTrigger :as-child="true">
<slot />
</DropdownMenuTrigger>
<DropdownMenuContent class="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg" side="bottom"
<DropdownMenuContent class="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded" side="bottom"
align="end" :side-offset="4">
<DropdownMenuLabel class="p-0 font-normal">
<Button @click="switchAccount(identity.account.id)" variant="ghost" size="lg"

View file

@ -1,20 +1,6 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import { ChevronDownIcon } from "lucide-vue-next";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "~/components/ui/breadcrumb";
import { Separator } from "~/components/ui/separator";
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "~/components/ui/sidebar";
import { SidebarInset, SidebarProvider } from "~/components/ui/sidebar";
import { SettingIds } from "~/settings";
import Timelines from "../navigation/timelines.vue";
import LeftSidebar from "./left-sidebar.vue";

View file

@ -33,7 +33,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
v-bind="forwarded"
:class="
cn(
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded',
props.class,
)
"

View file

@ -5,7 +5,7 @@ export { default as AlertDescription } from "./AlertDescription.vue";
export { default as AlertTitle } from "./AlertTitle.vue";
export const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
"relative w-full rounded border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {

View file

@ -1,11 +0,0 @@
<script setup lang="ts">
import { AspectRatio, type AspectRatioProps } from "radix-vue";
const props = defineProps<AspectRatioProps>();
</script>
<template>
<AspectRatio v-bind="props">
<slot />
</AspectRatio>
</template>

View file

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

View file

@ -15,7 +15,7 @@ export const avatarVariant = cva(
},
shape: {
circle: "rounded-full",
square: "rounded-md",
square: "rounded",
},
},
},

View file

@ -1,13 +0,0 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<nav aria-label="breadcrumb" :class="props.class">
<slot />
</nav>
</template>

View file

@ -1,22 +0,0 @@
<script lang="ts" setup>
import { cn } from "@/lib/utils";
import { MoreHorizontal } from "lucide-vue-next";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<span
role="presentation"
aria-hidden="true"
:class="cn('flex h-9 w-9 items-center justify-center', props.class)"
>
<slot>
<MoreHorizontal class="h-4 w-4" />
</slot>
<span class="sr-only">More</span>
</span>
</template>

View file

@ -1,16 +0,0 @@
<script lang="ts" setup>
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<li
:class="cn('inline-flex items-center gap-1.5', props.class)"
>
<slot />
</li>
</template>

View file

@ -1,23 +0,0 @@
<script lang="ts" setup>
import { cn } from "@/lib/utils";
import { Primitive, type PrimitiveProps } from "radix-vue";
import type { HTMLAttributes } from "vue";
import { NuxtLink } from "#components";
const props = withDefaults(
defineProps<PrimitiveProps & { class?: HTMLAttributes["class"] }>(),
{
as: NuxtLink,
},
);
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn('transition-colors hover:text-foreground', props.class)"
>
<slot />
</Primitive>
</template>

View file

@ -1,16 +0,0 @@
<script lang="ts" setup>
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<ol
:class="cn('flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5', props.class)"
>
<slot />
</ol>
</template>

View file

@ -1,19 +0,0 @@
<script lang="ts" setup>
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<span
role="link"
aria-disabled="true"
aria-current="page"
:class="cn('font-normal text-foreground', props.class)"
>
<slot />
</span>
</template>

View file

@ -1,21 +0,0 @@
<script lang="ts" setup>
import { cn } from "@/lib/utils";
import { ChevronRight } from "lucide-vue-next";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<li
role="presentation"
aria-hidden="true"
:class="cn('[&>svg]:w-3.5 [&>svg]:h-3.5', props.class)"
>
<slot>
<ChevronRight />
</slot>
</li>
</template>

View file

@ -1,7 +0,0 @@
export { default as Breadcrumb } from "./Breadcrumb.vue";
export { default as BreadcrumbEllipsis } from "./BreadcrumbEllipsis.vue";
export { default as BreadcrumbItem } from "./BreadcrumbItem.vue";
export { default as BreadcrumbLink } from "./BreadcrumbLink.vue";
export { default as BreadcrumbList } from "./BreadcrumbList.vue";
export { default as BreadcrumbPage } from "./BreadcrumbPage.vue";
export { default as BreadcrumbSeparator } from "./BreadcrumbSeparator.vue";

View file

@ -2,7 +2,7 @@ import { type VariantProps, cva } from "class-variance-authority";
export { default as Button } from "./Button.vue";
export const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
@ -19,9 +19,9 @@ export const buttonVariants = cva(
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
sm: "h-8 px-3 text-xs",
lg: "h-10 px-8",
icon: "size-9",
},
},
defaultVariants: {

View file

@ -1,65 +0,0 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import type {
CarouselEmits,
CarouselProps,
WithClassAsProps,
} from "./interface";
import { useProvideCarousel } from "./useCarousel";
const props = withDefaults(defineProps<CarouselProps & WithClassAsProps>(), {
orientation: "horizontal",
});
const emits = defineEmits<CarouselEmits>();
const {
canScrollNext,
canScrollPrev,
carouselApi,
carouselRef,
orientation,
scrollNext,
scrollPrev,
} = useProvideCarousel(props, emits);
defineExpose({
canScrollNext,
canScrollPrev,
carouselApi,
carouselRef,
orientation,
scrollNext,
scrollPrev,
});
function onKeyDown(event: KeyboardEvent) {
const prevKey = props.orientation === "vertical" ? "ArrowUp" : "ArrowLeft";
const nextKey =
props.orientation === "vertical" ? "ArrowDown" : "ArrowRight";
if (event.key === prevKey) {
event.preventDefault();
scrollPrev();
return;
}
if (event.key === nextKey) {
event.preventDefault();
scrollNext();
}
}
</script>
<template>
<div
:class="cn('relative', props.class)"
role="region"
aria-roledescription="carousel"
tabindex="0"
@keydown="onKeyDown"
>
<slot :can-scroll-next :can-scroll-prev :carousel-api :carousel-ref :orientation :scroll-next :scroll-prev />
</div>
</template>

View file

@ -1,29 +0,0 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import type { WithClassAsProps } from "./interface";
import { useCarousel } from "./useCarousel";
defineOptions({
inheritAttrs: false,
});
const props = defineProps<WithClassAsProps>();
const { carouselRef, orientation } = useCarousel();
</script>
<template>
<div ref="carouselRef" class="overflow-hidden">
<div
:class="
cn(
'flex',
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
props.class,
)"
v-bind="$attrs"
>
<slot />
</div>
</div>
</template>

View file

@ -1,23 +0,0 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import type { WithClassAsProps } from "./interface";
import { useCarousel } from "./useCarousel";
const props = defineProps<WithClassAsProps>();
const { orientation } = useCarousel();
</script>
<template>
<div
role="group"
aria-roledescription="slide"
:class="cn(
'min-w-0 shrink-0 grow-0 basis-full',
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
props.class,
)"
>
<slot />
</div>
</template>

View file

@ -1,31 +0,0 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import { ArrowRight } from "lucide-vue-next";
import { Button } from "~/components/ui/button";
import type { WithClassAsProps } from "./interface";
import { useCarousel } from "./useCarousel";
const props = defineProps<WithClassAsProps>();
const { orientation, canScrollNext, scrollNext } = useCarousel();
</script>
<template>
<Button
:disabled="!canScrollNext"
:class="cn(
'touch-manipulation absolute h-8 w-8 rounded-full p-0',
orientation === 'horizontal'
? '-right-12 top-1/2 -translate-y-1/2'
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
props.class,
)"
variant="outline"
@click="scrollNext"
>
<slot>
<ArrowRight class="h-4 w-4 text-current" />
<span class="sr-only">Next Slide</span>
</slot>
</Button>
</template>

View file

@ -1,31 +0,0 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import { ArrowLeft } from "lucide-vue-next";
import { Button } from "~/components/ui/button";
import type { WithClassAsProps } from "./interface";
import { useCarousel } from "./useCarousel";
const props = defineProps<WithClassAsProps>();
const { orientation, canScrollPrev, scrollPrev } = useCarousel();
</script>
<template>
<Button
:disabled="!canScrollPrev"
:class="cn(
'touch-manipulation absolute h-8 w-8 rounded-full p-0',
orientation === 'horizontal'
? '-left-12 top-1/2 -translate-y-1/2'
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
props.class,
)"
variant="outline"
@click="scrollPrev"
>
<slot>
<ArrowLeft class="h-4 w-4 text-current" />
<span class="sr-only">Previous Slide</span>
</slot>
</Button>
</template>

View file

@ -1,8 +0,0 @@
export { default as Carousel } from "./Carousel.vue";
export { default as CarouselContent } from "./CarouselContent.vue";
export { default as CarouselItem } from "./CarouselItem.vue";
export { default as CarouselNext } from "./CarouselNext.vue";
export { default as CarouselPrevious } from "./CarouselPrevious.vue";
export type { UnwrapRefCarouselApi as CarouselApi } from "./interface";
export { useCarousel } from "./useCarousel";

View file

@ -1,25 +0,0 @@
import type useEmblaCarousel from "embla-carousel-vue";
import type { EmblaCarouselVueType } from "embla-carousel-vue";
import type { HTMLAttributes, UnwrapRef } from "vue";
type CarouselApi = EmblaCarouselVueType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
export type UnwrapRefCarouselApi = UnwrapRef<CarouselApi>;
export interface CarouselProps {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical";
}
export type CarouselEmits = (
e: "init-api",
payload: UnwrapRefCarouselApi,
) => void;
export interface WithClassAsProps {
class?: HTMLAttributes["class"];
}

View file

@ -1,69 +0,0 @@
import { createInjectionState } from "@vueuse/core";
import emblaCarouselVue from "embla-carousel-vue";
import { onMounted, ref } from "vue";
import type {
UnwrapRefCarouselApi as CarouselApi,
CarouselEmits,
CarouselProps,
} from "./interface";
const [useProvideCarousel, useInjectCarousel] = createInjectionState(
({ opts, orientation, plugins }: CarouselProps, emits: CarouselEmits) => {
const [emblaNode, emblaApi] = emblaCarouselVue(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins,
);
function scrollPrev() {
emblaApi.value?.scrollPrev();
}
function scrollNext() {
emblaApi.value?.scrollNext();
}
const canScrollNext = ref(false);
const canScrollPrev = ref(false);
function onSelect(api: CarouselApi) {
canScrollNext.value = !!api?.canScrollNext();
canScrollPrev.value = !!api?.canScrollPrev();
}
onMounted(() => {
if (!emblaApi.value) {
return;
}
emblaApi.value?.on("init", onSelect);
emblaApi.value?.on("reInit", onSelect);
emblaApi.value?.on("select", onSelect);
emits("init-api", emblaApi.value);
});
return {
carouselRef: emblaNode,
carouselApi: emblaApi,
canScrollPrev,
canScrollNext,
scrollPrev,
scrollNext,
orientation,
};
},
);
function useCarousel() {
const carouselState = useInjectCarousel();
if (!carouselState) {
throw new Error("useCarousel must be used within a <Carousel />");
}
return carouselState;
}
export { useCarousel, useProvideCarousel };

View file

@ -24,12 +24,8 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<CheckboxRoot
v-bind="forwarded"
:class="
cn('peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
props.class)"
>
<CheckboxRoot v-bind="forwarded" :class="cn('peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
props.class)">
<CheckboxIndicator class="flex h-full w-full items-center justify-center text-current">
<slot>
<Check class="h-4 w-4" />

View file

@ -24,10 +24,8 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<ComboboxRoot
v-bind="forwarded"
:class="cn('flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground', props.class)"
>
<ComboboxRoot v-bind="forwarded"
:class="cn('flex h-full w-full flex-col overflow-hidden rounded bg-popover text-popover-foreground', props.class)">
<slot />
</ComboboxRoot>
</template>

View file

@ -30,10 +30,7 @@ const forwardedProps = useForwardProps(delegatedProps);
<template>
<div class="flex items-center border-b px-3" cmdk-input-wrapper>
<Search class="mr-2 h-4 w-4 shrink-0 opacity-50" />
<ComboboxInput
v-bind="{ ...forwardedProps, ...$attrs }"
auto-focus
:class="cn('flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50', props.class)"
/>
<ComboboxInput v-bind="{ ...forwardedProps, ...$attrs }" auto-focus
:class="cn('flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50', props.class)" />
</div>
</template>

View file

@ -40,7 +40,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<slot />
<DialogClose v-if="!props.hideClose"
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
class="absolute right-4 top-4 rounded opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X class="size-4" />
<span class="sr-only">Close</span>
</DialogClose>

View file

@ -19,5 +19,6 @@ const modelValue = useVModel(props, "modelValue", emits, {
</script>
<template>
<input v-model="modelValue" :class="cn('flex h-10 w-full rounded-md border !border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus:ring-0 focus:ring-offset-0 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50', props.class)">
<input v-model="modelValue"
:class="cn('flex h-10 w-full rounded border !border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus:ring-0 focus:ring-offset-0 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50', props.class)">
</template>

View file

@ -71,6 +71,7 @@ export default (<Config>{
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
DEFAULT: "var(--radius)",
},
animation: {
like: "like 1s ease-in-out",