mirror of
https://github.com/versia-pub/frontend.git
synced 2026-03-13 03:29:16 +01:00
refactor: ♻️ Redesign post composer
This commit is contained in:
parent
ef7475aead
commit
7ff9d2302a
17 changed files with 327 additions and 203 deletions
|
|
@ -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"
|
||||
:disabled="store.sending" :mode="store.contentType === 'text/html' ? 'rich' : 'plain'" />
|
||||
<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>
|
||||
|
||||
<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 />
|
||||
<Files v-model:files="store.files" :composer-key="composerKey" />
|
||||
</div>
|
||||
<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'" />
|
||||
|
||||
<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 v-if="store.files.length > 0" align="block-end" class="overflow-x-auto *:shrink-0">
|
||||
<Files v-model:files="store.files" :composer-key="composerKey" />
|
||||
</InputGroupAddon>
|
||||
|
||||
<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">
|
||||
{{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue