mirror of
https://github.com/versia-pub/frontend.git
synced 2025-12-06 08:28:20 +01:00
refactor: ♻️ Replace HeadlessUI with Ark UI, improve UI
This commit is contained in:
parent
d109e09a72
commit
3c68c2e788
|
|
@ -1,7 +1,6 @@
|
|||
<template>
|
||||
<ButtonsBase
|
||||
class="bg-white/10 hover:bg-white/20 !text-left flex flex-row gap-x-3 !rounded-none !ring-0 !p-4 sm:!p-3">
|
||||
<iconify-icon :icon="icon" width="1.25rem" height="1.25rem" class="text-gray-200" aria-hidden="true" />
|
||||
<ButtonsBase class="hover:bg-white/20 !rounded-sm !text-left flex flex-row gap-x-3 !ring-0 !p-4 sm:!p-2">
|
||||
<iconify-icon :icon="icon" width="none" class="text-gray-200 size-5" aria-hidden="true" />
|
||||
<slot />
|
||||
</ButtonsBase>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,12 @@
|
|||
<template>
|
||||
<div class="px-6 py-4">
|
||||
<div class="py-2 relative">
|
||||
<div v-if="respondingTo" class="mb-4">
|
||||
<span v-if="respondingType === 'reply'" class="text-gray-400 uppercase text-xs font-semibold">
|
||||
<iconify-icon width="1rem" height="1rem" icon="tabler:arrow-back-up" class="text-gray-400 mb-0.5"
|
||||
aria-hidden="true" />
|
||||
Replying to
|
||||
</span>
|
||||
<span v-else-if="respondingType === 'quote'" class="text-gray-400 uppercase text-xs font-semibold">
|
||||
<iconify-icon width="1rem" height="1rem" icon="tabler:quote" class="text-gray-400"
|
||||
aria-hidden="true" />
|
||||
Quoting
|
||||
</span>
|
||||
<OverlayScrollbarsComponent :defer="true" class="mt-2 max-h-72 overflow-y-auto">
|
||||
<LazySocialElementsNotesNote :note="respondingTo" :small="true" :disabled="true" />
|
||||
<OverlayScrollbarsComponent :defer="true" class="max-h-72 overflow-y-auto">
|
||||
<LazySocialElementsNotesNote :note="respondingTo" :small="true" :disabled="true"
|
||||
class="!rounded-none !bg-pink-500/10" />
|
||||
</OverlayScrollbarsComponent>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="pb-2 relative">
|
||||
<textarea :disabled="submitting" ref="textarea" v-model="content" :placeholder="chosenSplash"
|
||||
class="resize-none min-h-48 prose prose-invert max-h-[70dvh] w-full p-0 focus:!ring-0 !ring-none !border-none !outline-none placeholder:text-zinc-500 bg-transparent appearance-none focus:!border-none focus:!outline-none disabled:cursor-not-allowed"></textarea>
|
||||
<div
|
||||
|
|
|
|||
52
components/composer/modal.client.vue
Normal file
52
components/composer/modal.client.vue
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<HeadlessTransitionRoot as="template" :show="open">
|
||||
<Dialog.Root v-model:open="open" :close-on-escape="true" :close-on-interact-outside="true"
|
||||
@update:open="o => open = o">
|
||||
|
||||
<Teleport to="body">
|
||||
<Dialog.Positioner
|
||||
class="flex min-h-full items-start z-50 justify-center p-4 text-center sm:items-center sm:p-0 fixed inset-0 w-screen overflow-y-auto">
|
||||
<HeadlessTransitionChild as="template" enter="ease-out duration-200" enter-from="opacity-0"
|
||||
enter-to="opacity-100" leave="ease-in duration-200" leave-from="opacity-100"
|
||||
leave-to="opacity-0">
|
||||
<Dialog.Backdrop class="fixed inset-0 bg-black/70" @click="open = false" />
|
||||
</HeadlessTransitionChild>
|
||||
<HeadlessTransitionChild as="template" enter="ease-out duration-200"
|
||||
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enter-to="opacity-100 translate-y-0 sm:scale-100" leave="ease-in duration-200"
|
||||
leave-from="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
|
||||
<Dialog.Content
|
||||
class="relative transform overflow-hidden rounded-lg bg-dark-700 ring-1 ring-dark-800 text-left shadow-xl transition-all sm:my-8 w-full max-w-xl">
|
||||
<Composer v-if="instance" :instance="instance" />
|
||||
</Dialog.Content>
|
||||
</HeadlessTransitionChild>
|
||||
</Dialog.Positioner>
|
||||
</Teleport>
|
||||
</Dialog.Root>
|
||||
</HeadlessTransitionRoot>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Dialog } from "@ark-ui/vue";
|
||||
const open = ref(false);
|
||||
useListen("note:reply", async (note) => {
|
||||
open.value = true;
|
||||
await nextTick();
|
||||
useEvent("composer:reply", note);
|
||||
});
|
||||
useListen("note:quote", async (note) => {
|
||||
open.value = true;
|
||||
await nextTick();
|
||||
useEvent("composer:quote", note);
|
||||
});
|
||||
useListen("composer:open", () => {
|
||||
if (tokenData.value) open.value = true;
|
||||
});
|
||||
useListen("composer:close", () => {
|
||||
open.value = false;
|
||||
});
|
||||
const log = console.log;
|
||||
const tokenData = useTokenData();
|
||||
const instance = useInstance();
|
||||
</script>
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
<template>
|
||||
<HeadlessTransitionRoot as="template" :show="open">
|
||||
<HeadlessDialog as="div" class="relative z-50" @close="useEvent('composer:close')">
|
||||
<HeadlessTransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0"
|
||||
enter-to="opacity-100" leave="ease-in duration-200" leave-from="opacity-100" leave-to="opacity-0">
|
||||
<div class="fixed inset-0 bg-black/70 transition-opacity" />
|
||||
</HeadlessTransitionChild>
|
||||
|
||||
<div class="fixed inset-0 z-10 w-screen overflow-y-auto">
|
||||
<div class="flex min-h-full items-start justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<HeadlessTransitionChild as="template" enter="ease-out duration-300"
|
||||
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enter-to="opacity-100 translate-y-0 sm:scale-100" leave="ease-in duration-200"
|
||||
leave-from="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
|
||||
<HeadlessDialogPanel
|
||||
class="relative transform rounded-lg bg-dark-700 ring-1 ring-dark-800 text-left shadow-xl transition-all sm:my-8 w-full max-w-xl">
|
||||
<Composer v-if="instance" :instance="instance" />
|
||||
</HeadlessDialogPanel>
|
||||
</HeadlessTransitionChild>
|
||||
</div>
|
||||
</div>
|
||||
</HeadlessDialog>
|
||||
</HeadlessTransitionRoot>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const open = ref(false);
|
||||
useListen("note:reply", async (note) => {
|
||||
open.value = true;
|
||||
await nextTick();
|
||||
useEvent("composer:reply", note);
|
||||
});
|
||||
useListen("note:quote", async (note) => {
|
||||
open.value = true;
|
||||
await nextTick();
|
||||
useEvent("composer:quote", note);
|
||||
});
|
||||
useListen("composer:open", () => {
|
||||
if (tokenData.value) open.value = true;
|
||||
});
|
||||
useListen("composer:close", () => {
|
||||
open.value = false;
|
||||
});
|
||||
const client = useMegalodon();
|
||||
const tokenData = useTokenData();
|
||||
const instance = useInstance();
|
||||
</script>
|
||||
|
|
@ -1,29 +1,34 @@
|
|||
<template>
|
||||
<HeadlessMenu v-slot="{ close }" v-bind="$props">
|
||||
<Menu.Root :positioning="{
|
||||
strategy: 'fixed',
|
||||
}" @update:open="(o) => open = o" :open="open">
|
||||
<Menu.Trigger>
|
||||
<slot name="button"></slot>
|
||||
|
||||
<HeadlessMenuItems @click="close" class="fixed z-20 inset-0 z-5 bg-black/50">
|
||||
|
||||
</HeadlessMenuItems>
|
||||
|
||||
</Menu.Trigger>
|
||||
<div @mousedown="open = false" @touchstart="open = false" v-if="open" class="fixed inset-0 z-10 bg-black/50">
|
||||
</div>
|
||||
<Menu.Positioner :class="isSmallScreen && '!bottom-0 !top-[unset] fixed inset-x-0 w-full !translate-y-0'">
|
||||
<transition enter-active-class="transition ease-in duration-100"
|
||||
enter-from-class="transform opacity-0 translate-y-full sm:translate-y-0 scale-95"
|
||||
enter-to-class="transform translate-y-0 opacity-100 scale-100"
|
||||
leave-active-class="transition ease-out duration-75" leave-from-class="transform opacity-100 scale-100"
|
||||
leave-to-class="transform opacity-0 scale-95">
|
||||
<HeadlessMenuItems
|
||||
:class="['z-20 mt-2 rounded overflow-hidden bg-dark-900 shadow-lg ring-1 ring-white/10 focus:outline-none',
|
||||
isSmallScreen ? 'bottom-0 fixed inset-x-0 w-full origin-bottom' : 'absolute right-0 origin-top-right top-full min-w-56']">
|
||||
<div v-if="isSmallScreen" class="w-full bg-white/10 py-2">
|
||||
<Menu.Content v-if="open"
|
||||
:class="['z-20 mt-2 rounded overflow-hidden p-1 space-y-1 bg-dark-700 shadow-lg ring-1 ring-white/10 focus:outline-none min-w-56']">
|
||||
<div v-if="isSmallScreen" class="w-full py-2">
|
||||
<div class="rounded-full h-1 bg-gray-400 w-12 mx-auto"></div>
|
||||
</div>
|
||||
<slot name="items"></slot>
|
||||
</HeadlessMenuItems>
|
||||
</Menu.Content>
|
||||
</transition>
|
||||
</HeadlessMenu>
|
||||
</Menu.Positioner>
|
||||
</Menu.Root>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Menu } from "@ark-ui/vue";
|
||||
const { width } = useWindowSize();
|
||||
const isSmallScreen = computed(() => width.value < 768);
|
||||
|
||||
const open = ref(false);
|
||||
</script>
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
<template>
|
||||
<header class="absolute inset-x-0 top-0 z-50">
|
||||
<div
|
||||
class="relative isolate warning-background flex items-center gap-x-6 overflow-hidden bg-dark-900 px-6 py-2.5 sm:px-3.5 sm:before:flex-1">
|
||||
<div class="flex flex-wrap justify-center gap-x-4 gap-y-2 w-full">
|
||||
<p class="text-sm text-gray-50 bg-dark-900 px-2 rounded py-1">
|
||||
<strong class="font-semibold">Warning!</strong> • This is a testing site used for
|
||||
development, not a finished page.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
</div>
|
||||
</div>
|
||||
<nav class="flex items-center justify-between p-6 lg:px-8" aria-label="Global">
|
||||
<div class="flex lg:flex-1">
|
||||
<NuxtLink href="/" class="-m-1.5 p-1.5">
|
||||
<img crossorigin="anonymous" class="h-8 w-auto" src="/logo.webp" alt="Lysand logo" />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="flex lg:hidden">
|
||||
<button type="button"
|
||||
class="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-gray-200"
|
||||
@click="mobileMenuOpen = true">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<iconify-icon icon="tabler:menu-2" width="1.5rem" height="1.5rem" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="hidden lg:flex lg:gap-x-12">
|
||||
<NuxtLink v-for="item in navigation" :key="item.name" :href="item.href"
|
||||
class="text-sm font-semibold leading-6 text-gray-50">{{ item.name }}</NuxtLink>
|
||||
</div>
|
||||
<div class="hidden lg:flex lg:flex-1 lg:justify-end">
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
<HeadlessDialog as="div" class="lg:hidden" @close="mobileMenuOpen = false" :open="mobileMenuOpen">
|
||||
<div class="fixed inset-0 z-50" />
|
||||
<HeadlessDialogPanel
|
||||
class="fixed inset-y-0 right-0 z-50 w-full overflow-y-auto bg-dark-800 px-6 py-6 sm:max-w-sm sm:ring-1 sm:ring-gray-50/10">
|
||||
<div class="flex items-center justify-between">
|
||||
<NuxtLink href="/" class="-m-1.5 p-1.5">
|
||||
<img crossorigin="anonymous" class="h-8 w-auto" src="/logo.webp" alt="Lysand Logo" />
|
||||
</NuxtLink>
|
||||
<button type="button" class="-m-2.5 rounded-md p-2.5 text-gray-200" @click="mobileMenuOpen = false">
|
||||
<span class="sr-only">Close menu</span>
|
||||
<iconify-icon icon="tabler:x" width="1.5rem" height="1.5rem" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-6 flow-root">
|
||||
<div class="-my-6 divide-y divide-gray-400/10">
|
||||
<div class="space-y-2 py-6">
|
||||
<NuxtLink v-for="item in navigation" :key="item.name" :href="item.href"
|
||||
class="-mx-3 block rounded-lg px-3 py-2 text-base font-semibold leading-7 text-gray-50 hover:bg-gray-900">
|
||||
{{
|
||||
item.name }}</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HeadlessDialogPanel>
|
||||
</HeadlessDialog>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const navigation = [
|
||||
{ name: "About", href: "/" },
|
||||
{ name: "Roadmap", href: "/" },
|
||||
{ name: "Documentation", href: "/" },
|
||||
{ name: "Team", href: "/" },
|
||||
];
|
||||
|
||||
const mobileMenuOpen = ref(false);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.warning-background {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='40' height='40' viewBox='0 0 40 40' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23f9d63d' fill-opacity='0.4' fill-rule='evenodd'%3E%3Cpath d='M0 40L40 0H20L0 20M40 40V20L20 40'/%3E%3C/g%3E%3C/svg%3E");
|
||||
}
|
||||
</style>
|
||||
|
|
@ -70,23 +70,20 @@
|
|||
<ClientOnly>
|
||||
<DropdownsAdaptiveDropdown>
|
||||
<template #button>
|
||||
<HeadlessMenuButton class="flex flex-col items-center justify-center p-2 rounded">
|
||||
<button class="flex flex-col items-center justify-center p-2 rounded">
|
||||
<iconify-icon icon="tabler:home" class="text-2xl" />
|
||||
<span class="text-xs">Timelines</span>
|
||||
</HeadlessMenuButton>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template #items>
|
||||
<ClientOnly>
|
||||
<HeadlessMenuItem v-for="timeline in visibleTimelines" :key="timeline.href"
|
||||
:href="timeline.href">
|
||||
<NuxtLink>
|
||||
<Menu.Item value="" v-for="timeline in visibleTimelines" :key="timeline.href">
|
||||
<NuxtLink :href="timeline.href">
|
||||
<ButtonsDropdownElement :icon="timeline.icon" class="w-full">
|
||||
{{ timeline.name }}
|
||||
</ButtonsDropdownElement>
|
||||
</NuxtLink>
|
||||
</HeadlessMenuItem>
|
||||
</ClientOnly>
|
||||
</Menu.Item>
|
||||
</template>
|
||||
</DropdownsAdaptiveDropdown>
|
||||
<NuxtLink href="/notifications" class="flex flex-col items-center justify-center p-2 rounded">
|
||||
|
|
@ -102,33 +99,33 @@
|
|||
</ClientOnly>
|
||||
<DropdownsAdaptiveDropdown v-else>
|
||||
<template #button>
|
||||
<HeadlessMenuButton class="flex flex-col items-center justify-center p-2 rounded">
|
||||
<button class="flex flex-col items-center justify-center p-2 rounded">
|
||||
<iconify-icon icon="tabler:user" class="text-2xl" />
|
||||
<span class="text-xs">Account</span>
|
||||
</HeadlessMenuButton>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template #items>
|
||||
<ClientOnly>
|
||||
<HeadlessMenuItem v-if="tokenData">
|
||||
<Menu.Item value="" v-if="tokenData">
|
||||
<ButtonsDropdownElement icon="tabler:logout" class="w-full"
|
||||
@click="signOut().finally(() => loadingAuth = false)" :loading="loadingAuth">
|
||||
Sign Out
|
||||
</ButtonsDropdownElement>
|
||||
</HeadlessMenuItem>
|
||||
<HeadlessMenuItem v-if="!tokenData">
|
||||
</Menu.Item>
|
||||
<Menu.Item value="" v-if="!tokenData">
|
||||
<ButtonsDropdownElement icon="tabler:login" class="w-full"
|
||||
@click="signIn().finally(() => loadingAuth = false)" :loading="loadingAuth">
|
||||
Sign In
|
||||
</ButtonsDropdownElement>
|
||||
</HeadlessMenuItem>
|
||||
<HeadlessMenuItem v-if="!tokenData">
|
||||
</Menu.Item>
|
||||
<Menu.Item value="" v-if="!tokenData">
|
||||
<NuxtLink href="/register">
|
||||
<ButtonsDropdownElement icon="tabler:certificate" class="w-full">
|
||||
Register
|
||||
</ButtonsDropdownElement>
|
||||
</NuxtLink>
|
||||
</HeadlessMenuItem>
|
||||
</Menu.Item>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
</DropdownsAdaptiveDropdown>
|
||||
|
|
@ -142,6 +139,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Menu } from "@ark-ui/vue";
|
||||
const { $pwa } = useNuxtApp();
|
||||
const timelines = ref([
|
||||
{
|
||||
|
|
@ -167,6 +165,7 @@ const timelines = ref([
|
|||
requiresAuth: true,
|
||||
},
|
||||
]);
|
||||
const log = console.log;
|
||||
|
||||
const visibleTimelines = computed(() =>
|
||||
timelines.value.filter(
|
||||
|
|
|
|||
57
components/social-elements/notes/attachment-dialog.vue
Normal file
57
components/social-elements/notes/attachment-dialog.vue
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<HeadlessTransitionRoot as="template" :show="lightbox">
|
||||
<Dialog.Root v-model:open="lightbox" :close-on-escape="true" :close-on-interact-outside="true"
|
||||
@update:open="o => lightbox = o">
|
||||
|
||||
<Teleport to="body">
|
||||
<Dialog.Positioner class="z-50">
|
||||
<HeadlessTransitionChild as="template" enter="ease-out duration-200" enter-from="opacity-0"
|
||||
enter-to="opacity-100" leave="ease-in duration-200" leave-from="opacity-100"
|
||||
leave-to="opacity-0">
|
||||
<Dialog.Backdrop class="fixed inset-0 bg-black/70 !z-40" @click="lightbox = false" />
|
||||
</HeadlessTransitionChild>
|
||||
|
||||
<Dialog.Content
|
||||
class="w-screen h-screen flex !z-50 justify-center items-center flex-col overflow-hidden p-10 fixed inset-0">
|
||||
<div class="w-full absolute inset-x-0 top-0 p-10 shrink text-gray-400 flex flex-row gap-3">
|
||||
<a @click.stop :href="attachment?.url" target="_blank" download class="ml-auto">
|
||||
<iconify-icon icon="tabler:download" width="1.5rem" height="1.5rem" />
|
||||
<span class="sr-only">Close</span>
|
||||
</a>
|
||||
<button @click.stop="lightbox = false">
|
||||
<iconify-icon icon="tabler:x" width="1.5rem" height="1.5rem" />
|
||||
<span class="sr-only">Close</span>
|
||||
</button>
|
||||
</div>
|
||||
<HeadlessTransitionChild as="template" enter="ease-out duration-200"
|
||||
enter-from="opacity-0 sm:scale-95" enter-to="opacity-100 sm:scale-100"
|
||||
leave="ease-in duration-200" leave-from="opacity-100 sm:scale-100"
|
||||
leave-to="opacity-0 sm:scale-95">
|
||||
<img @click.stop v-if="attachment?.type === 'image'"
|
||||
class="rounded max-w-full min-w-[30%] max-h-[70%]" :src="attachment.url"
|
||||
:alt="attachment.description ?? ''" :title="attachment.description ?? ''" />
|
||||
</HeadlessTransitionChild>
|
||||
<span @click.stop v-if="attachment?.description"
|
||||
class="text-gray-300 rounded mt-6 -mb-20 px-4 py-2 max-w-xl ring-1 ring-white/5 bg-dark-900 max-h-40 overflow-y-auto">
|
||||
{{ attachment.description }}
|
||||
</span>
|
||||
</Dialog.Content>
|
||||
</Dialog.Positioner>
|
||||
</Teleport>
|
||||
</Dialog.Root>
|
||||
</HeadlessTransitionRoot>
|
||||
</template>t
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Dialog } from "@ark-ui/vue";
|
||||
import type { Attachment } from "~/types/mastodon/attachment";
|
||||
|
||||
const lightbox = ref(false);
|
||||
const attachment = ref<Attachment | null>(null);
|
||||
|
||||
useListen("attachment:view", async (a) => {
|
||||
attachment.value = a;
|
||||
await nextTick();
|
||||
lightbox.value = true;
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,55 +1,26 @@
|
|||
<template>
|
||||
<div @click="lightbox = true" tabindex="0" aria-label="Open attachment in lightbox" @keydown="lightbox = true"
|
||||
<div tabindex="0" aria-label="Open attachment in lightbox"
|
||||
class="aspect-video w-full rounded ring-white/5 shadow overflow-hidden ring-1 hover:ring-2 duration-100">
|
||||
<img v-if="attachment.type === 'image'" tabindex="-1"
|
||||
class="object-cover w-full h-full rounded duration-150 hover:scale-[102%] ease-in-out" :src="attachment.url"
|
||||
:alt="attachment.description ?? ''" :title="attachment.description ?? ''" />
|
||||
:alt="attachment.description ?? ''" :title="attachment.description ?? ''" @click="openLightbox"
|
||||
@keydown="openLightbox" />
|
||||
<video v-else-if="attachment.type === 'video'" class="object-cover w-full h-full rounded" controls
|
||||
:alt="attachment.description ?? ''" :title="attachment.description ?? ''">
|
||||
<source :src="attachment.url" type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
<HeadlessTransitionRoot appear :show="lightbox" as="template">
|
||||
<HeadlessDialog @close="lightbox = false">
|
||||
<div class="fixed inset-0 overflow-y-auto z-50 bg-black/70">
|
||||
<div class="flex min-h-full items-center justify-center text-center">
|
||||
<HeadlessTransitionChild as="template" enter="duration-100 ease-out" enter-from="opacity-0 scale-95"
|
||||
enter-to="opacity-100 scale-100">
|
||||
<HeadlessDialogPanel
|
||||
class="w-screen h-screen flex justify-center items-center flex-col relative overflow-hidden p-10"
|
||||
@click="lightbox = false">
|
||||
<div class="w-full absolute inset-x-0 top-0 p-10 shrink text-gray-400 flex flex-row gap-3">
|
||||
<a @click.stop :href="attachment.url" target="_blank" download class="ml-auto">
|
||||
<iconify-icon icon="tabler:download" width="1.5rem" height="1.5rem" />
|
||||
<span class="sr-only">Close</span>
|
||||
</a>
|
||||
<button @click.stop="lightbox = false">
|
||||
<iconify-icon icon="tabler:x" width="1.5rem" height="1.5rem" />
|
||||
<span class="sr-only">Close</span>
|
||||
</button>
|
||||
</div>
|
||||
<img @click.stop v-if="attachment.type === 'image'"
|
||||
class="rounded max-w-full min-w-[30%] max-h-[70%]" :src="attachment.url"
|
||||
:alt="attachment.description ?? ''" :title="attachment.description ?? ''" />
|
||||
<span @click.stop v-if="attachment.description"
|
||||
class="text-gray-300 rounded mt-6 -mb-20 px-4 py-2 max-w-xl ring-1 ring-white/5 bg-dark-900 max-h-40 overflow-y-auto">
|
||||
{{ attachment.description }}
|
||||
</span>
|
||||
</HeadlessDialogPanel>
|
||||
</HeadlessTransitionChild>
|
||||
</div>
|
||||
</div>
|
||||
</HeadlessDialog>
|
||||
</HeadlessTransitionRoot>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Attachment } from "~/types/mastodon/attachment";
|
||||
|
||||
const lightbox = ref(false);
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
attachment: Attachment;
|
||||
}>();
|
||||
|
||||
const openLightbox = () => {
|
||||
useEvent("attachment:view", props.attachment);
|
||||
};
|
||||
</script>
|
||||
|
|
@ -24,17 +24,17 @@
|
|||
class="text-gray-200 group-hover:group-enabled:text-blue-600" aria-hidden="true" />
|
||||
<span class="text-gray-400 mt-0.5 ml-2">{{ numberFormat(note?.replies_count) }}</span>
|
||||
</button>
|
||||
<button class="group" :disabled="!isSignedIn">
|
||||
<button class="group" @click="likeFn" :disabled="!isSignedIn">
|
||||
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:heart" v-if="!note?.favourited"
|
||||
class="size-5 text-gray-200 group-hover:group-enabled:text-pink-600" aria-hidden="true" />
|
||||
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:heart-filled" v-else
|
||||
class="size-5 text-pink-600 group-hover:group-enabled:text-gray-200" aria-hidden="true" />
|
||||
<span class="text-gray-400 mt-0.5 ml-2">{{ numberFormat(note?.favourites_count) }}</span>
|
||||
</button>
|
||||
<button class="group" :disabled="!isSignedIn">
|
||||
<button class="group" @click="reblogFn" :disabled="!isSignedIn">
|
||||
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:repeat" v-if="!note?.reblogged"
|
||||
class="size-5 text-gray-200 group-hover:group-enabled:text-green-600" aria-hidden="true" />
|
||||
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:repeat-off" v-else
|
||||
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:repeat" v-else
|
||||
class="size-5 text-green-600 group-hover:group-enabled:text-gray-200" aria-hidden="true" />
|
||||
<span class="text-gray-400 mt-0.5 ml-2">{{ numberFormat(note?.reblogs_count) }}</span>
|
||||
</button>
|
||||
|
|
@ -45,32 +45,30 @@
|
|||
</button>
|
||||
<DropdownsAdaptiveDropdown>
|
||||
<template #button>
|
||||
<HeadlessMenuButton>
|
||||
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:dots"
|
||||
class="size-5 text-gray-200" aria-hidden="true" />
|
||||
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:dots" class="size-5 text-gray-200"
|
||||
aria-hidden="true" />
|
||||
<span class="sr-only">Open menu</span>
|
||||
</HeadlessMenuButton>
|
||||
</template>
|
||||
|
||||
<template #items>
|
||||
<HeadlessMenuItem>
|
||||
<Menu.Item value="">
|
||||
<ButtonsDropdownElement @click="copy(JSON.stringify(note, null, 4))" icon="tabler:code"
|
||||
class="w-full">
|
||||
Copy API
|
||||
Response
|
||||
</ButtonsDropdownElement>
|
||||
</HeadlessMenuItem>
|
||||
<HeadlessMenuItem>
|
||||
</Menu.Item>
|
||||
<Menu.Item value="">
|
||||
<ButtonsDropdownElement @click="copy(url)" icon="tabler:link" class="w-full">
|
||||
Copy Link
|
||||
</ButtonsDropdownElement>
|
||||
</HeadlessMenuItem>
|
||||
<HeadlessMenuItem>
|
||||
</Menu.Item>
|
||||
<Menu.Item value="">
|
||||
<ButtonsDropdownElement @click="remove" icon="tabler:backspace" :disabled="!isSignedIn"
|
||||
class="w-full border-r-2 border-red-500">
|
||||
Delete
|
||||
</ButtonsDropdownElement>
|
||||
</HeadlessMenuItem>
|
||||
</Menu.Item>
|
||||
</template>
|
||||
</DropdownsAdaptiveDropdown>
|
||||
</div>
|
||||
|
|
@ -79,6 +77,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Menu } from "@ark-ui/vue";
|
||||
import Skeleton from "~/components/skeleton/Skeleton.vue";
|
||||
import type { Status } from "~/types/mastodon/status";
|
||||
|
||||
|
|
@ -94,6 +93,8 @@ const props = withDefaults(
|
|||
},
|
||||
);
|
||||
|
||||
const noteRef = ref(props.note);
|
||||
|
||||
const tokenData = useTokenData();
|
||||
const isSignedIn = useSignedIn();
|
||||
const client = useMegalodon(tokenData);
|
||||
|
|
@ -108,7 +109,7 @@ const {
|
|||
reblog,
|
||||
isReply,
|
||||
reblogDisplayName,
|
||||
} = useNoteData(ref(props.note), client);
|
||||
} = useNoteData(noteRef, client);
|
||||
|
||||
const { copy } = useClipboard();
|
||||
const numberFormat = (number = 0) =>
|
||||
|
|
@ -117,4 +118,38 @@ const numberFormat = (number = 0) =>
|
|||
compactDisplay: "short",
|
||||
maximumFractionDigits: 1,
|
||||
}).format(number);
|
||||
|
||||
const likeFn = async () => {
|
||||
if (!note.value) return;
|
||||
if (note.value.favourited) {
|
||||
const output = await client.value?.unfavouriteStatus(note.value.id);
|
||||
|
||||
if (output?.data) {
|
||||
noteRef.value = output.data;
|
||||
}
|
||||
} else {
|
||||
const output = await client.value?.favouriteStatus(note.value.id);
|
||||
|
||||
if (output?.data) {
|
||||
noteRef.value = output.data;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const reblogFn = async () => {
|
||||
if (!note.value) return;
|
||||
if (note.value?.reblogged) {
|
||||
const output = await client.value?.unreblogStatus(note.value.id);
|
||||
|
||||
if (output?.data) {
|
||||
noteRef.value = output.data;
|
||||
}
|
||||
} else {
|
||||
const output = await client.value?.reblogStatus(note.value.id);
|
||||
|
||||
if (output?.data.reblog) {
|
||||
noteRef.value = output.data.reblog;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import mitt from "mitt";
|
||||
import type { Attachment } from "~/types/mastodon/attachment";
|
||||
import type { Status } from "~/types/mastodon/status";
|
||||
|
||||
export type NotificationEvent = {
|
||||
|
|
@ -24,6 +25,7 @@ type ApplicationEvents = {
|
|||
"composer:send": Status;
|
||||
"composer:close": undefined;
|
||||
"notification:new": NotificationEvent;
|
||||
"attachment:view": Attachment;
|
||||
};
|
||||
|
||||
const emitter = mitt<ApplicationEvents>();
|
||||
|
|
|
|||
|
|
@ -45,8 +45,11 @@ export const useNoteData = (
|
|||
: null,
|
||||
);
|
||||
|
||||
const url = computed(
|
||||
() => `/@${renderedNote.value?.account.acct}/${renderedNote.value?.id}`,
|
||||
const url = computed(() =>
|
||||
new URL(
|
||||
`/@${renderedNote.value?.account.acct}/${renderedNote.value?.id}`,
|
||||
window.location.origin,
|
||||
).toString(),
|
||||
);
|
||||
|
||||
const remove = async () => {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<LazyComposerModal />
|
||||
<LazySocialElementsNotesAttachmentDialog />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
"lint": "bunx @biomejs/biome check ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@ark-ui/vue": "^3.1.0",
|
||||
"@nuxt/fonts": "^0.7.0",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@vee-validate/nuxt": "^4.13.0",
|
||||
|
|
|
|||
Loading…
Reference in a new issue