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>
|
<template>
|
||||||
<ButtonsBase
|
<ButtonsBase class="hover:bg-white/20 !rounded-sm !text-left flex flex-row gap-x-3 !ring-0 !p-4 sm:!p-2">
|
||||||
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="none" class="text-gray-200 size-5" aria-hidden="true" />
|
||||||
<iconify-icon :icon="icon" width="1.25rem" height="1.25rem" class="text-gray-200" aria-hidden="true" />
|
|
||||||
<slot />
|
<slot />
|
||||||
</ButtonsBase>
|
</ButtonsBase>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
|
<div v-if="respondingTo" class="mb-4">
|
||||||
|
<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="px-6 py-4">
|
||||||
<div class="py-2 relative">
|
<div class="pb-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>
|
|
||||||
</div>
|
|
||||||
<textarea :disabled="submitting" ref="textarea" v-model="content" :placeholder="chosenSplash"
|
<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>
|
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
|
<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>
|
<template>
|
||||||
<HeadlessMenu v-slot="{ close }" v-bind="$props">
|
<Menu.Root :positioning="{
|
||||||
<slot name="button"></slot>
|
strategy: 'fixed',
|
||||||
|
}" @update:open="(o) => open = o" :open="open">
|
||||||
<HeadlessMenuItems @click="close" class="fixed z-20 inset-0 z-5 bg-black/50">
|
<Menu.Trigger>
|
||||||
|
<slot name="button"></slot>
|
||||||
</HeadlessMenuItems>
|
</Menu.Trigger>
|
||||||
|
<div @mousedown="open = false" @touchstart="open = false" v-if="open" class="fixed inset-0 z-10 bg-black/50">
|
||||||
<transition enter-active-class="transition ease-in duration-100"
|
</div>
|
||||||
enter-from-class="transform opacity-0 translate-y-full sm:translate-y-0 scale-95"
|
<Menu.Positioner :class="isSmallScreen && '!bottom-0 !top-[unset] fixed inset-x-0 w-full !translate-y-0'">
|
||||||
enter-to-class="transform translate-y-0 opacity-100 scale-100"
|
<transition enter-active-class="transition ease-in duration-100"
|
||||||
leave-active-class="transition ease-out duration-75" leave-from-class="transform opacity-100 scale-100"
|
enter-from-class="transform opacity-0 translate-y-full sm:translate-y-0 scale-95"
|
||||||
leave-to-class="transform opacity-0 scale-95">
|
enter-to-class="transform translate-y-0 opacity-100 scale-100"
|
||||||
<HeadlessMenuItems
|
leave-active-class="transition ease-out duration-75" leave-from-class="transform opacity-100 scale-100"
|
||||||
:class="['z-20 mt-2 rounded overflow-hidden bg-dark-900 shadow-lg ring-1 ring-white/10 focus:outline-none',
|
leave-to-class="transform opacity-0 scale-95">
|
||||||
isSmallScreen ? 'bottom-0 fixed inset-x-0 w-full origin-bottom' : 'absolute right-0 origin-top-right top-full min-w-56']">
|
<Menu.Content v-if="open"
|
||||||
<div v-if="isSmallScreen" class="w-full bg-white/10 py-2">
|
: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 class="rounded-full h-1 bg-gray-400 w-12 mx-auto"></div>
|
<div v-if="isSmallScreen" class="w-full py-2">
|
||||||
</div>
|
<div class="rounded-full h-1 bg-gray-400 w-12 mx-auto"></div>
|
||||||
<slot name="items"></slot>
|
</div>
|
||||||
</HeadlessMenuItems>
|
<slot name="items"></slot>
|
||||||
</transition>
|
</Menu.Content>
|
||||||
</HeadlessMenu>
|
</transition>
|
||||||
|
</Menu.Positioner>
|
||||||
|
</Menu.Root>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Menu } from "@ark-ui/vue";
|
||||||
const { width } = useWindowSize();
|
const { width } = useWindowSize();
|
||||||
const isSmallScreen = computed(() => width.value < 768);
|
const isSmallScreen = computed(() => width.value < 768);
|
||||||
|
|
||||||
|
const open = ref(false);
|
||||||
</script>
|
</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>
|
<ClientOnly>
|
||||||
<DropdownsAdaptiveDropdown>
|
<DropdownsAdaptiveDropdown>
|
||||||
<template #button>
|
<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" />
|
<iconify-icon icon="tabler:home" class="text-2xl" />
|
||||||
<span class="text-xs">Timelines</span>
|
<span class="text-xs">Timelines</span>
|
||||||
</HeadlessMenuButton>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #items>
|
<template #items>
|
||||||
<ClientOnly>
|
<Menu.Item value="" v-for="timeline in visibleTimelines" :key="timeline.href">
|
||||||
<HeadlessMenuItem v-for="timeline in visibleTimelines" :key="timeline.href"
|
<NuxtLink :href="timeline.href">
|
||||||
:href="timeline.href">
|
<ButtonsDropdownElement :icon="timeline.icon" class="w-full">
|
||||||
<NuxtLink>
|
{{ timeline.name }}
|
||||||
<ButtonsDropdownElement :icon="timeline.icon" class="w-full">
|
</ButtonsDropdownElement>
|
||||||
{{ timeline.name }}
|
</NuxtLink>
|
||||||
</ButtonsDropdownElement>
|
</Menu.Item>
|
||||||
</NuxtLink>
|
|
||||||
</HeadlessMenuItem>
|
|
||||||
</ClientOnly>
|
|
||||||
</template>
|
</template>
|
||||||
</DropdownsAdaptiveDropdown>
|
</DropdownsAdaptiveDropdown>
|
||||||
<NuxtLink href="/notifications" class="flex flex-col items-center justify-center p-2 rounded">
|
<NuxtLink href="/notifications" class="flex flex-col items-center justify-center p-2 rounded">
|
||||||
|
|
@ -102,33 +99,33 @@
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
<DropdownsAdaptiveDropdown v-else>
|
<DropdownsAdaptiveDropdown v-else>
|
||||||
<template #button>
|
<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" />
|
<iconify-icon icon="tabler:user" class="text-2xl" />
|
||||||
<span class="text-xs">Account</span>
|
<span class="text-xs">Account</span>
|
||||||
</HeadlessMenuButton>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #items>
|
<template #items>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<HeadlessMenuItem v-if="tokenData">
|
<Menu.Item value="" v-if="tokenData">
|
||||||
<ButtonsDropdownElement icon="tabler:logout" class="w-full"
|
<ButtonsDropdownElement icon="tabler:logout" class="w-full"
|
||||||
@click="signOut().finally(() => loadingAuth = false)" :loading="loadingAuth">
|
@click="signOut().finally(() => loadingAuth = false)" :loading="loadingAuth">
|
||||||
Sign Out
|
Sign Out
|
||||||
</ButtonsDropdownElement>
|
</ButtonsDropdownElement>
|
||||||
</HeadlessMenuItem>
|
</Menu.Item>
|
||||||
<HeadlessMenuItem v-if="!tokenData">
|
<Menu.Item value="" v-if="!tokenData">
|
||||||
<ButtonsDropdownElement icon="tabler:login" class="w-full"
|
<ButtonsDropdownElement icon="tabler:login" class="w-full"
|
||||||
@click="signIn().finally(() => loadingAuth = false)" :loading="loadingAuth">
|
@click="signIn().finally(() => loadingAuth = false)" :loading="loadingAuth">
|
||||||
Sign In
|
Sign In
|
||||||
</ButtonsDropdownElement>
|
</ButtonsDropdownElement>
|
||||||
</HeadlessMenuItem>
|
</Menu.Item>
|
||||||
<HeadlessMenuItem v-if="!tokenData">
|
<Menu.Item value="" v-if="!tokenData">
|
||||||
<NuxtLink href="/register">
|
<NuxtLink href="/register">
|
||||||
<ButtonsDropdownElement icon="tabler:certificate" class="w-full">
|
<ButtonsDropdownElement icon="tabler:certificate" class="w-full">
|
||||||
Register
|
Register
|
||||||
</ButtonsDropdownElement>
|
</ButtonsDropdownElement>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</HeadlessMenuItem>
|
</Menu.Item>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</template>
|
</template>
|
||||||
</DropdownsAdaptiveDropdown>
|
</DropdownsAdaptiveDropdown>
|
||||||
|
|
@ -142,6 +139,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { Menu } from "@ark-ui/vue";
|
||||||
const { $pwa } = useNuxtApp();
|
const { $pwa } = useNuxtApp();
|
||||||
const timelines = ref([
|
const timelines = ref([
|
||||||
{
|
{
|
||||||
|
|
@ -167,6 +165,7 @@ const timelines = ref([
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
const log = console.log;
|
||||||
|
|
||||||
const visibleTimelines = computed(() =>
|
const visibleTimelines = computed(() =>
|
||||||
timelines.value.filter(
|
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>
|
<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">
|
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"
|
<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"
|
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
|
<video v-else-if="attachment.type === 'video'" class="object-cover w-full h-full rounded" controls
|
||||||
:alt="attachment.description ?? ''" :title="attachment.description ?? ''">
|
:alt="attachment.description ?? ''" :title="attachment.description ?? ''">
|
||||||
<source :src="attachment.url" type="video/mp4" />
|
<source :src="attachment.url" type="video/mp4" />
|
||||||
Your browser does not support the video tag.
|
Your browser does not support the video tag.
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Attachment } from "~/types/mastodon/attachment";
|
import type { Attachment } from "~/types/mastodon/attachment";
|
||||||
|
|
||||||
const lightbox = ref(false);
|
const props = defineProps<{
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
attachment: Attachment;
|
attachment: Attachment;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const openLightbox = () => {
|
||||||
|
useEvent("attachment:view", props.attachment);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -24,17 +24,17 @@
|
||||||
class="text-gray-200 group-hover:group-enabled:text-blue-600" aria-hidden="true" />
|
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>
|
<span class="text-gray-400 mt-0.5 ml-2">{{ numberFormat(note?.replies_count) }}</span>
|
||||||
</button>
|
</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"
|
<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" />
|
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
|
<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" />
|
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>
|
<span class="text-gray-400 mt-0.5 ml-2">{{ numberFormat(note?.favourites_count) }}</span>
|
||||||
</button>
|
</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"
|
<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" />
|
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" />
|
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>
|
<span class="text-gray-400 mt-0.5 ml-2">{{ numberFormat(note?.reblogs_count) }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -45,32 +45,30 @@
|
||||||
</button>
|
</button>
|
||||||
<DropdownsAdaptiveDropdown>
|
<DropdownsAdaptiveDropdown>
|
||||||
<template #button>
|
<template #button>
|
||||||
<HeadlessMenuButton>
|
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:dots" class="size-5 text-gray-200"
|
||||||
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:dots"
|
aria-hidden="true" />
|
||||||
class="size-5 text-gray-200" aria-hidden="true" />
|
<span class="sr-only">Open menu</span>
|
||||||
<span class="sr-only">Open menu</span>
|
|
||||||
</HeadlessMenuButton>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #items>
|
<template #items>
|
||||||
<HeadlessMenuItem>
|
<Menu.Item value="">
|
||||||
<ButtonsDropdownElement @click="copy(JSON.stringify(note, null, 4))" icon="tabler:code"
|
<ButtonsDropdownElement @click="copy(JSON.stringify(note, null, 4))" icon="tabler:code"
|
||||||
class="w-full">
|
class="w-full">
|
||||||
Copy API
|
Copy API
|
||||||
Response
|
Response
|
||||||
</ButtonsDropdownElement>
|
</ButtonsDropdownElement>
|
||||||
</HeadlessMenuItem>
|
</Menu.Item>
|
||||||
<HeadlessMenuItem>
|
<Menu.Item value="">
|
||||||
<ButtonsDropdownElement @click="copy(url)" icon="tabler:link" class="w-full">
|
<ButtonsDropdownElement @click="copy(url)" icon="tabler:link" class="w-full">
|
||||||
Copy Link
|
Copy Link
|
||||||
</ButtonsDropdownElement>
|
</ButtonsDropdownElement>
|
||||||
</HeadlessMenuItem>
|
</Menu.Item>
|
||||||
<HeadlessMenuItem>
|
<Menu.Item value="">
|
||||||
<ButtonsDropdownElement @click="remove" icon="tabler:backspace" :disabled="!isSignedIn"
|
<ButtonsDropdownElement @click="remove" icon="tabler:backspace" :disabled="!isSignedIn"
|
||||||
class="w-full border-r-2 border-red-500">
|
class="w-full border-r-2 border-red-500">
|
||||||
Delete
|
Delete
|
||||||
</ButtonsDropdownElement>
|
</ButtonsDropdownElement>
|
||||||
</HeadlessMenuItem>
|
</Menu.Item>
|
||||||
</template>
|
</template>
|
||||||
</DropdownsAdaptiveDropdown>
|
</DropdownsAdaptiveDropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -79,6 +77,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { Menu } from "@ark-ui/vue";
|
||||||
import Skeleton from "~/components/skeleton/Skeleton.vue";
|
import Skeleton from "~/components/skeleton/Skeleton.vue";
|
||||||
import type { Status } from "~/types/mastodon/status";
|
import type { Status } from "~/types/mastodon/status";
|
||||||
|
|
||||||
|
|
@ -94,6 +93,8 @@ const props = withDefaults(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const noteRef = ref(props.note);
|
||||||
|
|
||||||
const tokenData = useTokenData();
|
const tokenData = useTokenData();
|
||||||
const isSignedIn = useSignedIn();
|
const isSignedIn = useSignedIn();
|
||||||
const client = useMegalodon(tokenData);
|
const client = useMegalodon(tokenData);
|
||||||
|
|
@ -108,7 +109,7 @@ const {
|
||||||
reblog,
|
reblog,
|
||||||
isReply,
|
isReply,
|
||||||
reblogDisplayName,
|
reblogDisplayName,
|
||||||
} = useNoteData(ref(props.note), client);
|
} = useNoteData(noteRef, client);
|
||||||
|
|
||||||
const { copy } = useClipboard();
|
const { copy } = useClipboard();
|
||||||
const numberFormat = (number = 0) =>
|
const numberFormat = (number = 0) =>
|
||||||
|
|
@ -117,4 +118,38 @@ const numberFormat = (number = 0) =>
|
||||||
compactDisplay: "short",
|
compactDisplay: "short",
|
||||||
maximumFractionDigits: 1,
|
maximumFractionDigits: 1,
|
||||||
}).format(number);
|
}).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>
|
</script>
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import mitt from "mitt";
|
import mitt from "mitt";
|
||||||
|
import type { Attachment } from "~/types/mastodon/attachment";
|
||||||
import type { Status } from "~/types/mastodon/status";
|
import type { Status } from "~/types/mastodon/status";
|
||||||
|
|
||||||
export type NotificationEvent = {
|
export type NotificationEvent = {
|
||||||
|
|
@ -24,6 +25,7 @@ type ApplicationEvents = {
|
||||||
"composer:send": Status;
|
"composer:send": Status;
|
||||||
"composer:close": undefined;
|
"composer:close": undefined;
|
||||||
"notification:new": NotificationEvent;
|
"notification:new": NotificationEvent;
|
||||||
|
"attachment:view": Attachment;
|
||||||
};
|
};
|
||||||
|
|
||||||
const emitter = mitt<ApplicationEvents>();
|
const emitter = mitt<ApplicationEvents>();
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,11 @@ export const useNoteData = (
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const url = computed(
|
const url = computed(() =>
|
||||||
() => `/@${renderedNote.value?.account.acct}/${renderedNote.value?.id}`,
|
new URL(
|
||||||
|
`/@${renderedNote.value?.account.acct}/${renderedNote.value?.id}`,
|
||||||
|
window.location.origin,
|
||||||
|
).toString(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const remove = async () => {
|
const remove = async () => {
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<LazyComposerModal />
|
<LazyComposerModal />
|
||||||
|
<LazySocialElementsNotesAttachmentDialog />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
"lint": "bunx @biomejs/biome check ."
|
"lint": "bunx @biomejs/biome check ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ark-ui/vue": "^3.1.0",
|
||||||
"@nuxt/fonts": "^0.7.0",
|
"@nuxt/fonts": "^0.7.0",
|
||||||
"@tailwindcss/typography": "^0.5.13",
|
"@tailwindcss/typography": "^0.5.13",
|
||||||
"@vee-validate/nuxt": "^4.13.0",
|
"@vee-validate/nuxt": "^4.13.0",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue