mirror of
https://github.com/versia-pub/frontend.git
synced 2026-01-26 04:16:02 +01:00
refactor: ♻️ Redesign post composer
This commit is contained in:
parent
ef7475aead
commit
7ff9d2302a
|
|
@ -1,18 +0,0 @@
|
|||
<template>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as="div">
|
||||
<slot />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{{ tooltip }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||
|
||||
const { tooltip } = defineProps<{
|
||||
tooltip: string;
|
||||
}>();
|
||||
</script>
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
<template>
|
||||
<ComposerButton :tooltip="m.game_tough_seal_adore()">
|
||||
<Button variant="ghost" size="icon">
|
||||
<AtSign class="!size-5" />
|
||||
</Button>
|
||||
</ComposerButton>
|
||||
<ComposerButton :tooltip="m.plane_born_koala_hope()">
|
||||
<Toggle variant="default" size="sm" :model-value="contentType === 'text/html'" @update:model-value="
|
||||
(i) =>
|
||||
(contentType = i ? 'text/html' : 'text/plain')
|
||||
">
|
||||
<LetterText class="!size-5" />
|
||||
</Toggle>
|
||||
</ComposerButton>
|
||||
<VisibilityPicker v-model:visibility="visibility">
|
||||
<Button variant="ghost" size="icon" :disabled="relation?.type === 'edit'">
|
||||
<component :is="visibilities[visibility].icon" />
|
||||
</Button>
|
||||
</VisibilityPicker>
|
||||
<ComposerButton :tooltip="m.blue_ornate_coyote_tickle()">
|
||||
<Button variant="ghost" size="icon">
|
||||
<Smile class="!size-5" />
|
||||
</Button>
|
||||
</ComposerButton>
|
||||
<ComposerButton :tooltip="m.top_patchy_earthworm_vent()">
|
||||
<Button variant="ghost" size="icon" @click="emit('pickFile')">
|
||||
<FilePlus2 class="!size-5" />
|
||||
</Button>
|
||||
</ComposerButton>
|
||||
<ComposerButton :tooltip="m.frail_broad_mallard_dart()">
|
||||
<Toggle variant="default" size="sm" v-model="sensitive">
|
||||
<TriangleAlert class="!size-5" />
|
||||
</Toggle>
|
||||
</ComposerButton>
|
||||
<CharacterCounter class="ml-auto" :max="authStore.instance?.configuration.statuses.max_characters ?? 0" :current="rawContent.length" />
|
||||
<Button type="submit" size="lg" :disabled="sending || !canSend" @click="emit('submit')">
|
||||
<Loader v-if="sending" class="!size-5 animate-spin" />
|
||||
{{
|
||||
relation?.type === "edit"
|
||||
? m.gaudy_strong_puma_slide()
|
||||
: m.free_teal_bulldog_learn()
|
||||
}}
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
AtSign,
|
||||
FilePlus2,
|
||||
LetterText,
|
||||
Loader,
|
||||
Smile,
|
||||
TriangleAlert,
|
||||
} from "lucide-vue-next";
|
||||
import * as m from "~~/paraglide/messages.js";
|
||||
import { Button } from "../ui/button";
|
||||
import { Toggle } from "../ui/toggle";
|
||||
import ComposerButton from "./button.vue";
|
||||
import CharacterCounter from "./character-counter.vue";
|
||||
import { visibilities } from "./visibilities";
|
||||
import VisibilityPicker from "./visibility-picker.vue";
|
||||
|
||||
const { relation, sending, canSend, rawContent } = defineProps<{
|
||||
relation?: ComposerState["relation"];
|
||||
sending: boolean;
|
||||
canSend: boolean;
|
||||
rawContent: string;
|
||||
}>();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const contentType = defineModel<ComposerState["contentType"]>("contentType", {
|
||||
required: true,
|
||||
});
|
||||
const visibility = defineModel<ComposerState["visibility"]>("visibility", {
|
||||
required: true,
|
||||
});
|
||||
const sensitive = defineModel<ComposerState["sensitive"]>("sensitive", {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [];
|
||||
pickFile: [];
|
||||
}>();
|
||||
</script>
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
<template>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<div v-bind="$attrs" class="m-1">
|
||||
<TriangleAlert v-if="isOverflowing" class="text-destructive-foreground size-6" />
|
||||
<svg v-else viewBox="0 0 100 100" class="transform rotate-[-90deg] size-6">
|
||||
<!-- Background Circle -->
|
||||
<circle cx="50" cy="50" r="46" stroke="currentColor" class="text-muted" stroke-width="8"
|
||||
fill="none" />
|
||||
<!-- Progress Circle -->
|
||||
<circle cx="50" cy="50" r="46" stroke="currentColor" stroke-width="8" fill="none"
|
||||
stroke-dasharray="100" :stroke-dashoffset="100 - percentage" pathLength="100"
|
||||
stroke-linecap="round" class="text-accent-foreground transition-all duration-500" />
|
||||
</svg>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent class="text-center">
|
||||
<p>{{ current }} / {{ max }}</p>
|
||||
<p v-if="isOverflowing">Too long!</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TriangleAlert } from "lucide-vue-next";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||
|
||||
const { max, current } = defineProps<{
|
||||
max: number;
|
||||
current: number;
|
||||
}>();
|
||||
|
||||
const percentage = computed(() => {
|
||||
return Math.min((current / max) * 100, 100);
|
||||
});
|
||||
const isOverflowing = computed(() => {
|
||||
return current > max;
|
||||
});
|
||||
</script>
|
||||
|
|
@ -3,29 +3,94 @@
|
|||
<Note :note="relation.note" :hide-actions="true" :small-layout="true" />
|
||||
</div>
|
||||
|
||||
<ContentWarning v-if="store.sensitive" v-model="store.contentWarning" />
|
||||
|
||||
<EditorContent @paste-files="uploadFiles" v-model:content="store.content" v-model:raw-content="store.rawContent" :placeholder="getRandomSplash()"
|
||||
class="[&>.tiptap]:!border-none [&>.tiptap]:!ring-0 [&>.tiptap]:!outline-none [&>.tiptap]:rounded-none p-0 [&>.tiptap]:max-h-[50dvh] [&>.tiptap]:overflow-y-auto [&>.tiptap]:min-h-48 [&>.tiptap]:!ring-offset-0 [&>.tiptap]:h-full"
|
||||
<InputGroup class="p-1">
|
||||
<InputGroupAddon v-if="store.sensitive" align="block-start" class="pt-3">
|
||||
<Input v-model:model-value="store.contentWarning" placeholder="Put your content warning here" />
|
||||
</InputGroupAddon>
|
||||
|
||||
<EditorContent data-slot="input-group-control" @paste-files="uploadFiles" v-model:content="store.content"
|
||||
v-model:raw-content="store.rawContent" :placeholder="getRandomSplash()"
|
||||
class=" placeholder:text-muted-foreground flex field-sizing-content min-h-58 w-full px-4 text-base disabled:opacity-50 md:text-sm flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none"
|
||||
:disabled="store.sending" :mode="store.contentType === 'text/html' ? 'rich' : 'plain'" />
|
||||
|
||||
<div class="w-full flex flex-row gap-2 overflow-x-auto *:shrink-0 pb-2">
|
||||
<input type="file" ref="fileInput" @change="uploadFileFromEvent" class="hidden" multiple />
|
||||
<InputGroupAddon v-if="store.files.length > 0" align="block-end" class="overflow-x-auto *:shrink-0">
|
||||
<Files v-model:files="store.files" :composer-key="composerKey" />
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
|
||||
<DialogFooter class="items-center flex-row overflow-x-auto">
|
||||
<ComposerButtons @submit="send" @pick-file="fileInput?.click()" v-model:content-type="store.contentType" v-model:sensitive="store.sensitive" v-model:visibility="store.visibility" :relation="store.relation" :sending="store.sending" :can-send="store.canSend" :raw-content="store.rawContent" />
|
||||
</DialogFooter>
|
||||
<InputGroupAddon align="block-end">
|
||||
<Select v-model:model-value="store.contentType">
|
||||
<SelectTrigger as-child disable-default-classes disable-select-icon>
|
||||
<InputGroupButton variant="ghost" size="icon-sm">
|
||||
<LetterText v-if="store.contentType === 'text/html'" />
|
||||
<Type v-else />
|
||||
</InputGroupButton>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="text/plain">
|
||||
Plain text
|
||||
</SelectItem>
|
||||
<SelectItem value="text/html">
|
||||
Rich text
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<VisibilityPicker v-model:visibility="store.visibility">
|
||||
<InputGroupButton variant="ghost" size="icon-sm" :disabled="store.relation?.type === 'edit'">
|
||||
<component :is="visibilities[store.visibility].icon" />
|
||||
</InputGroupButton>
|
||||
</VisibilityPicker>
|
||||
<InputGroupButton variant="ghost" size="icon-sm" @click="fileInput?.click()">
|
||||
<FilePlus2 />
|
||||
</InputGroupButton>
|
||||
<Toggle size="sm" v-model="store.sensitive">
|
||||
<TriangleAlert />
|
||||
</Toggle>
|
||||
<InputGroupText :class="['ml-auto', charactersLeft < 0 && 'text-destructive']">
|
||||
{{ charactersLeft.toLocaleString(getLocale(), {
|
||||
maximumFractionDigits: 2,
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
}) }}
|
||||
</InputGroupText>
|
||||
<Separator orientation="vertical" class="h-4!" />
|
||||
<InputGroupButton variant="default" size="icon-sm" :disabled="store.sending || !store.canSend"
|
||||
@click="send">
|
||||
<Spinner v-if="store.sending" />
|
||||
<ArrowUp v-else />
|
||||
<span class="sr-only">Send</span>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
|
||||
<input type="file" ref="fileInput" @change="uploadFileFromEvent" class="hidden" multiple />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ArrowUp,
|
||||
FilePlus2,
|
||||
LetterText,
|
||||
TriangleAlert,
|
||||
Type,
|
||||
} from "lucide-vue-next";
|
||||
import { Separator } from "reka-ui";
|
||||
import Note from "~/components/notes/note.vue";
|
||||
import { getLocale } from "~~/paraglide/runtime";
|
||||
import EditorContent from "../editor/content.vue";
|
||||
import { DialogFooter } from "../ui/dialog";
|
||||
import ComposerButtons from "./buttons.vue";
|
||||
import ContentWarning from "./content-warning.vue";
|
||||
import { Input } from "../ui/input";
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupText,
|
||||
} from "../ui/input-group";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "../ui/select";
|
||||
import { Spinner } from "../ui/spinner";
|
||||
import { Toggle } from "../ui/toggle";
|
||||
import Files from "./files.vue";
|
||||
import { visibilities } from "./visibilities";
|
||||
import VisibilityPicker from "./visibility-picker.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
relation?: ComposerState["relation"];
|
||||
|
|
@ -38,6 +103,12 @@ const composerKey = props.relation
|
|||
? (`${props.relation.type}-${props.relation.note.id}` as const)
|
||||
: "blank";
|
||||
const store = useComposerStore(composerKey)();
|
||||
const authStore = useAuthStore();
|
||||
const charactersLeft = computed(() => {
|
||||
const max = authStore.instance?.configuration.statuses.max_characters ?? 0;
|
||||
|
||||
return max - store.rawContent.length;
|
||||
});
|
||||
|
||||
watch([Control_Enter, Command_Enter], () => {
|
||||
if (store.sending || !preferences.ctrl_enter_send.value) {
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
<template>
|
||||
<Input v-model:model-value="contentWarning" placeholder="Put your content warning here" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
const contentWarning = defineModel<string>();
|
||||
</script>
|
||||
|
|
@ -81,7 +81,7 @@ const relation = ref(
|
|||
>
|
||||
<DialogContent
|
||||
:hide-close="true"
|
||||
class="sm:max-w-xl max-w-full w-[calc(100%-2*0.5rem)] grid-cols-1 max-h-[90dvh] p-5 pt-6 top-2 sm:top-1/2 translate-y-0 sm:-translate-y-1/2"
|
||||
class="sm:max-w-xl max-w-full w-[calc(100%-2*0.5rem)] grid-cols-1 max-h-[90dvh] p-0 top-2 sm:top-1/2 translate-y-0 sm:-translate-y-1/2 border-none bg-transparent shadow-none"
|
||||
>
|
||||
<DialogTitle class="sr-only">
|
||||
{{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<BubbleMenu :editor="editor" />
|
||||
<EditorContent :editor="editor"
|
||||
v-bind="$attrs"
|
||||
:class="[$style.content, 'relative prose prose-sm dark:prose-invert break-words prose-a:no-underline prose-a:hover:underline prose-p:first-of-type:mt-0']" />
|
||||
:class="$style.content" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -36,6 +36,11 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
const editor = new Editor({
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: "relative prose prose-sm dark:prose-invert wrap-break-word prose-a:no-underline prose-a:hover:underline prose-p:first-of-type:mt-0 focus:outline-none w-full",
|
||||
},
|
||||
},
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Placeholder.configure({
|
||||
|
|
@ -118,6 +123,6 @@ onUnmounted(() => {
|
|||
}
|
||||
|
||||
.tiptap .emoji>img {
|
||||
@apply h-[1lh] align-middle inline hover:scale-110 transition-transform duration-75 ease-in-out;
|
||||
@apply h-lh align-middle inline hover:scale-110 transition-transform duration-75 ease-in-out;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
32
app/components/ui/input-group/InputGroup.vue
Normal file
32
app/components/ui/input-group/InputGroup.vue
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<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="input-group"
|
||||
role="group"
|
||||
:class="cn(
|
||||
'group/input-group border-input bg-background relative flex w-full items-center rounded-md border outline-none',
|
||||
'h-9 min-w-0 has-[>textarea]:h-auto',
|
||||
|
||||
// Variants based on alignment.
|
||||
'has-[>[data-align=inline-start]]:[&>input]:pl-2',
|
||||
'has-[>[data-align=inline-end]]:[&>input]:pr-2',
|
||||
'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
|
||||
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
|
||||
|
||||
// Focus state.
|
||||
'has-[[data-slot=input-group-control]:focus-visible]:ring-ring has-[[data-slot=input-group-control]:focus-visible]:ring-1',
|
||||
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
39
app/components/ui/input-group/InputGroupAddon.vue
Normal file
39
app/components/ui/input-group/InputGroupAddon.vue
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { InputGroupVariants } from ".";
|
||||
import { inputGroupAddonVariants } from ".";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
align?: InputGroupVariants["align"];
|
||||
class?: HTMLAttributes["class"];
|
||||
}>(),
|
||||
{
|
||||
align: "inline-start",
|
||||
},
|
||||
);
|
||||
|
||||
function handleInputGroupAddonClick(e: MouseEvent) {
|
||||
const currentTarget = e.currentTarget as HTMLElement | null;
|
||||
const target = e.target as HTMLElement | null;
|
||||
if (target?.closest("button")) {
|
||||
return;
|
||||
}
|
||||
if (currentTarget?.parentElement) {
|
||||
currentTarget.parentElement?.querySelector("input")?.focus();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
role="group"
|
||||
data-slot="input-group-addon"
|
||||
:data-align="props.align"
|
||||
:class="cn(inputGroupAddonVariants({ align: props.align }), props.class)"
|
||||
@click="handleInputGroupAddonClick"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
21
app/components/ui/input-group/InputGroupButton.vue
Normal file
21
app/components/ui/input-group/InputGroupButton.vue
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { InputGroupButtonProps } from ".";
|
||||
import { inputGroupButtonVariants } from ".";
|
||||
|
||||
const props = withDefaults(defineProps<InputGroupButtonProps>(), {
|
||||
size: "xs",
|
||||
variant: "ghost",
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
:data-size="props.size"
|
||||
:variant="props.variant"
|
||||
:class="cn(inputGroupButtonVariants({ size: props.size }), props.class)"
|
||||
>
|
||||
<slot />
|
||||
</Button>
|
||||
</template>
|
||||
19
app/components/ui/input-group/InputGroupInput.vue
Normal file
19
app/components/ui/input-group/InputGroupInput.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Input
|
||||
data-slot="input-group-control"
|
||||
:class="cn(
|
||||
'flex-1 rounded-none border-0 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent ring-offset-transparent dark:bg-transparent',
|
||||
props.class,
|
||||
)"
|
||||
/>
|
||||
</template>
|
||||
19
app/components/ui/input-group/InputGroupText.vue
Normal file
19
app/components/ui/input-group/InputGroupText.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
:class="cn(
|
||||
'text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*=\'size-\'])]:size-4',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
19
app/components/ui/input-group/InputGroupTextarea.vue
Normal file
19
app/components/ui/input-group/InputGroupTextarea.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Textarea
|
||||
data-slot="input-group-control"
|
||||
:class="cn(
|
||||
'flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 focus-visible:ring-transparent ring-offset-transparent dark:bg-transparent',
|
||||
props.class,
|
||||
)"
|
||||
/>
|
||||
</template>
|
||||
62
app/components/ui/input-group/index.ts
Normal file
62
app/components/ui/input-group/index.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import type { VariantProps } from "class-variance-authority";
|
||||
import { cva } from "class-variance-authority";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import type { ButtonVariants } from "@/components/ui/button";
|
||||
|
||||
export { default as InputGroup } from "./InputGroup.vue";
|
||||
export { default as InputGroupAddon } from "./InputGroupAddon.vue";
|
||||
export { default as InputGroupButton } from "./InputGroupButton.vue";
|
||||
export { default as InputGroupInput } from "./InputGroupInput.vue";
|
||||
export { default as InputGroupText } from "./InputGroupText.vue";
|
||||
export { default as InputGroupTextarea } from "./InputGroupTextarea.vue";
|
||||
|
||||
export const inputGroupAddonVariants = cva(
|
||||
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
align: {
|
||||
"inline-start":
|
||||
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
|
||||
"inline-end":
|
||||
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
|
||||
"block-start":
|
||||
"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
|
||||
"block-end":
|
||||
"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
align: "inline-start",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type InputGroupVariants = VariantProps<typeof inputGroupAddonVariants>;
|
||||
|
||||
export const inputGroupButtonVariants = cva(
|
||||
"text-sm shadow-none flex gap-2 items-center",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
|
||||
sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
|
||||
"icon-xs":
|
||||
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
||||
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "xs",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type InputGroupButtonVariants = VariantProps<
|
||||
typeof inputGroupButtonVariants
|
||||
>;
|
||||
|
||||
export interface InputGroupButtonProps {
|
||||
variant?: ButtonVariants["variant"];
|
||||
size?: InputGroupButtonVariants["size"];
|
||||
class?: HTMLAttributes["class"];
|
||||
}
|
||||
17
app/components/ui/spinner/Spinner.vue
Normal file
17
app/components/ui/spinner/Spinner.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import { Loader2Icon } from "lucide-vue-next";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loader2Icon
|
||||
role="status"
|
||||
aria-label="Loading"
|
||||
:class="cn('size-4 animate-spin', props.class)"
|
||||
/>
|
||||
</template>
|
||||
1
app/components/ui/spinner/index.ts
Normal file
1
app/components/ui/spinner/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as Spinner } from "./Spinner.vue";
|
||||
|
|
@ -252,12 +252,6 @@ packages:
|
|||
resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-create-class-features-plugin@7.27.1':
|
||||
resolution: {integrity: sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0
|
||||
|
||||
'@babel/helper-create-class-features-plugin@7.28.5':
|
||||
resolution: {integrity: sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
|
@ -279,10 +273,6 @@ packages:
|
|||
resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-member-expression-to-functions@7.27.1':
|
||||
resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-member-expression-to-functions@7.28.5':
|
||||
resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
|
@ -6149,19 +6139,6 @@ snapshots:
|
|||
lru-cache: 5.1.1
|
||||
semver: 6.3.1
|
||||
|
||||
'@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.28.5)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
'@babel/helper-annotate-as-pure': 7.27.3
|
||||
'@babel/helper-member-expression-to-functions': 7.27.1
|
||||
'@babel/helper-optimise-call-expression': 7.27.1
|
||||
'@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.5)
|
||||
'@babel/helper-skip-transparent-expression-wrappers': 7.27.1
|
||||
'@babel/traverse': 7.28.5
|
||||
semver: 6.3.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/helper-create-class-features-plugin@7.28.5(@babel/core@7.28.5)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
|
|
@ -6187,7 +6164,7 @@ snapshots:
|
|||
'@babel/core': 7.28.5
|
||||
'@babel/helper-compilation-targets': 7.27.2
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
debug: 4.4.1
|
||||
debug: 4.4.3
|
||||
lodash.debounce: 4.0.8
|
||||
resolve: 1.22.10
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -6195,13 +6172,6 @@ snapshots:
|
|||
|
||||
'@babel/helper-globals@7.28.0': {}
|
||||
|
||||
'@babel/helper-member-expression-to-functions@7.27.1':
|
||||
dependencies:
|
||||
'@babel/traverse': 7.28.5
|
||||
'@babel/types': 7.28.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/helper-member-expression-to-functions@7.28.5':
|
||||
dependencies:
|
||||
'@babel/traverse': 7.28.5
|
||||
|
|
@ -6380,7 +6350,7 @@ snapshots:
|
|||
'@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.5)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
'@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.5)
|
||||
'@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5)
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -6388,7 +6358,7 @@ snapshots:
|
|||
'@babel/plugin-transform-class-static-block@7.27.1(@babel/core@7.28.5)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
'@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.5)
|
||||
'@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5)
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -6591,7 +6561,7 @@ snapshots:
|
|||
'@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.28.5)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
'@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.5)
|
||||
'@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5)
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -6600,7 +6570,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
'@babel/helper-annotate-as-pure': 7.27.3
|
||||
'@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.5)
|
||||
'@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5)
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
|
|||
Loading…
Reference in a new issue