mirror of
https://github.com/versia-pub/frontend.git
synced 2025-12-06 16:38:20 +01:00
chore: ⬆️ Upgrade to the latest Shadcn-Vue version
Some checks failed
Some checks failed
This commit is contained in:
parent
7649ecfb80
commit
092bce0f24
18
app.vue
18
app.vue
|
|
@ -18,9 +18,9 @@ import "~/styles/index.css";
|
|||
import { convert } from "html-to-text";
|
||||
import ConfirmationModal from "./components/modals/confirm.vue";
|
||||
import { Toaster } from "./components/ui/sonner";
|
||||
import { TooltipProvider } from "./components/ui/tooltip";
|
||||
import { overwriteGetLocale } from "./paraglide/runtime";
|
||||
import { type EnumSetting, SettingIds } from "./settings";
|
||||
import { TooltipProvider } from "./components/ui/tooltip";
|
||||
// Sin
|
||||
//import "~/styles/mcdonalds.css";
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ useSeoMeta({
|
|||
ogImage: computed(() => instance.value?.banner?.url),
|
||||
twitterTitle: computed(() => instance.value?.title ?? ""),
|
||||
twitterDescription: computed(() =>
|
||||
convert(description.value?.content ?? "")
|
||||
convert(description.value?.content ?? ""),
|
||||
),
|
||||
twitterImage: computed(() => instance.value?.banner?.url),
|
||||
description: computed(() => convert(description.value?.content ?? "")),
|
||||
|
|
@ -76,7 +76,7 @@ useHead({
|
|||
|
||||
if (code && origin && appData.value && route.path !== "/oauth/code") {
|
||||
const newOrigin = new URL(
|
||||
URL.canParse(origin) ? origin : `https://${origin}`
|
||||
URL.canParse(origin) ? origin : `https://${origin}`,
|
||||
);
|
||||
|
||||
signInWithCode(code, appData.value, newOrigin);
|
||||
|
|
@ -84,7 +84,7 @@ if (code && origin && appData.value && route.path !== "/oauth/code") {
|
|||
|
||||
if (origin && !code) {
|
||||
const newOrigin = new URL(
|
||||
URL.canParse(origin) ? origin : `https://${origin}`
|
||||
URL.canParse(origin) ? origin : `https://${origin}`,
|
||||
);
|
||||
|
||||
signIn(appData, newOrigin);
|
||||
|
|
@ -108,4 +108,14 @@ html.theme-changing * {
|
|||
transition: background-color 1s ease, border 1s ease, color 1s ease,
|
||||
box-shadow 1s ease !important;
|
||||
}
|
||||
|
||||
.slide-down-enter-active,
|
||||
.slide-down-leave-active {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.slide-down-enter-from,
|
||||
.slide-down-leave-to {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
12
bun.lock
12
bun.lock
|
|
@ -38,7 +38,7 @@
|
|||
"nanoid": "^5.1.5",
|
||||
"nuxt": "^3.16.1",
|
||||
"nuxt-security": "^2.2.0",
|
||||
"radix-vue": "^1.9.17",
|
||||
"reka-ui": "^2.1.1",
|
||||
"shadcn-nuxt": "1.0.3",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
|
@ -1768,8 +1768,6 @@
|
|||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"radix-vue": ["radix-vue@1.9.17", "", { "dependencies": { "@floating-ui/dom": "^1.6.7", "@floating-ui/vue": "^1.1.0", "@internationalized/date": "^3.5.4", "@internationalized/number": "^3.5.3", "@tanstack/vue-virtual": "^3.8.1", "@vueuse/core": "^10.11.0", "@vueuse/shared": "^10.11.0", "aria-hidden": "^1.2.4", "defu": "^6.1.4", "fast-deep-equal": "^3.1.3", "nanoid": "^5.0.7" }, "peerDependencies": { "vue": ">= 3.2.0" } }, "sha512-mVCu7I2vXt1L2IUYHTt0sZMz7s1K2ZtqKeTIxG3yC5mMFfLBG4FtE1FDeRMpDd+Hhg/ybi9+iXmAP1ISREndoQ=="],
|
||||
|
||||
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
|
||||
|
||||
"randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="],
|
||||
|
|
@ -2426,10 +2424,6 @@
|
|||
|
||||
"prosemirror-trailing-node/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"radix-vue/@vueuse/core": ["@vueuse/core@10.11.1", "", { "dependencies": { "@types/web-bluetooth": "^0.0.20", "@vueuse/metadata": "10.11.1", "@vueuse/shared": "10.11.1", "vue-demi": ">=0.14.8" } }, "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww=="],
|
||||
|
||||
"radix-vue/@vueuse/shared": ["@vueuse/shared@10.11.1", "", { "dependencies": { "vue-demi": ">=0.14.8" } }, "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA=="],
|
||||
|
||||
"randombytes/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"readdir-glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
|
||||
|
|
@ -2590,10 +2584,6 @@
|
|||
|
||||
"nypm/pkg-types/confbox": ["confbox@0.2.1", "", {}, "sha512-hkT3yDPFbs95mNCy1+7qNKC6Pro+/ibzYxtM2iqEigpf0sVw+bg4Zh9/snjsBcf990vfIsg5+1U7VyiyBb3etg=="],
|
||||
|
||||
"radix-vue/@vueuse/core/@types/web-bluetooth": ["@types/web-bluetooth@0.0.20", "", {}, "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="],
|
||||
|
||||
"radix-vue/@vueuse/core/@vueuse/metadata": ["@vueuse/metadata@10.11.1", "", {}, "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw=="],
|
||||
|
||||
"reka-ui/@vueuse/core/@vueuse/metadata": ["@vueuse/metadata@12.8.2", "", {}, "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A=="],
|
||||
|
||||
"resolve-path/http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="],
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
"$schema": "https://shadcn-vue.com/schema.json",
|
||||
"style": "default",
|
||||
"typescript": true,
|
||||
"tsConfigPath": ".nuxt/tsconfig.json",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "styles/index.css",
|
||||
|
|
@ -10,9 +9,11 @@
|
|||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"framework": "nuxt",
|
||||
"aliases": {
|
||||
"components": "~/components",
|
||||
"utils": "@/lib/utils"
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"composables": "@/composables"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@
|
|||
<Toggle
|
||||
variant="default"
|
||||
size="sm"
|
||||
:pressed="state.contentType === 'text/html'"
|
||||
@update:pressed="
|
||||
:model-value="state.contentType === 'text/html'"
|
||||
@update:model-value="
|
||||
(i) =>
|
||||
(state.contentType = i ? 'text/html' : 'text/plain')
|
||||
"
|
||||
|
|
@ -61,6 +61,8 @@
|
|||
<SelectTrigger
|
||||
:as-child="true"
|
||||
:disabled="relation?.type === 'edit'"
|
||||
:disable-default-classes="true"
|
||||
:disable-select-icon="true"
|
||||
>
|
||||
<Button variant="ghost" size="icon">
|
||||
<component
|
||||
|
|
@ -110,11 +112,7 @@
|
|||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as="div">
|
||||
<Toggle
|
||||
variant="default"
|
||||
size="sm"
|
||||
v-model:pressed="state.sensitive"
|
||||
>
|
||||
<Toggle variant="default" size="sm" v-model="state.sensitive">
|
||||
<TriangleAlert class="!size-5" />
|
||||
</Toggle>
|
||||
</TooltipTrigger>
|
||||
|
|
@ -153,19 +151,23 @@ import {
|
|||
Smile,
|
||||
TriangleAlert,
|
||||
} from "lucide-vue-next";
|
||||
import { SelectTrigger } from "radix-vue";
|
||||
import { toast } from "vue-sonner";
|
||||
import Note from "~/components/notes/note.vue";
|
||||
import { Select, SelectContent, SelectItem } from "~/components/ui/select";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "~/components/ui/select";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import { SettingIds } from "~/settings";
|
||||
import EditorContent from "../editor/content.vue";
|
||||
import { Button } from "../ui/button";
|
||||
import { DialogFooter } from "../ui/dialog";
|
||||
import { Input } from "../ui/input";
|
||||
import { Toggle } from "../ui/toggle";
|
||||
import { DialogFooter } from "../ui/dialog";
|
||||
import Files from "./files.vue";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||
import Files from "./files.vue";
|
||||
|
||||
const { Control_Enter, Command_Enter } = useMagicKeys();
|
||||
const ctrlEnterSend = useSetting(SettingIds.CtrlEnterToSend);
|
||||
|
|
@ -221,7 +223,7 @@ const state = reactive({
|
|||
contentType: "text/html" as "text/html" | "text/plain",
|
||||
visibility: (relation?.type === "edit"
|
||||
? relation.note.visibility
|
||||
: defaultVisibility.value.value ?? "public") as Status["visibility"],
|
||||
: (defaultVisibility.value.value ?? "public")) as Status["visibility"],
|
||||
files: (relation?.type === "edit"
|
||||
? relation.note.media_attachments.map((a) => ({
|
||||
apiId: a.id,
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ const relation = ref(
|
|||
type: "reply" | "quote" | "edit";
|
||||
note: Status;
|
||||
source?: StatusSource;
|
||||
} | null
|
||||
} | null,
|
||||
);
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,23 @@
|
|||
<template>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as="button"
|
||||
<DropdownMenuTrigger
|
||||
as="button"
|
||||
:disabled="file.uploading || file.updating"
|
||||
class="block bg-card text-card-foreground shadow-sm h-28 overflow-hidden rounded relative min-w-28 *:disabled:opacity-50">
|
||||
class="block bg-card text-card-foreground shadow-sm h-28 overflow-hidden rounded relative min-w-28 *:disabled:opacity-50"
|
||||
>
|
||||
<Avatar class="h-28 w-full" shape="square">
|
||||
<AvatarImage class="!object-contain" :src="createObjectURL(file.file)" />
|
||||
<AvatarImage
|
||||
class="!object-contain"
|
||||
:src="createObjectURL(file.file)"
|
||||
/>
|
||||
</Avatar>
|
||||
<Badge v-if="!file.uploading && !file.updating" class="absolute bottom-1 right-1" variant="default">{{ formatBytes(file.file.size) }}</Badge>
|
||||
<Badge v-else class="absolute bottom-1 right-1 rounded px-1 !opacity-100" variant="default"><Loader class="animate-spin size-4" /></Badge>
|
||||
<Badge
|
||||
v-if="file.uploading && !file.updating"
|
||||
class="absolute bottom-1 right-1"
|
||||
variant="default"
|
||||
>{{ formatBytes(file.file.size) }}</Badge
|
||||
>
|
||||
<Spinner v-else class="absolute bottom-1 right-1 size-8 p-1.5" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="min-w-48">
|
||||
<DropdownMenuLabel>{{ file.file.name }}</DropdownMenuLabel>
|
||||
|
|
@ -32,6 +42,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { Captions, Delete, Loader, TextCursorInput } from "lucide-vue-next";
|
||||
import Spinner from "~/components/graphics/spinner.vue";
|
||||
import { confirmModalService } from "~/components/modals/composable.ts";
|
||||
import { Avatar, AvatarImage } from "~/components/ui/avatar";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
|
|
@ -121,6 +132,8 @@ const formatBytes = (bytes: number) => {
|
|||
const digitsAfterPoint = 2;
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${Number.parseFloat((bytes / k ** i).toFixed(digitsAfterPoint))} ${sizes[i]}`;
|
||||
return `${Number.parseFloat((bytes / k ** i).toFixed(digitsAfterPoint))} ${
|
||||
sizes[i]
|
||||
}`;
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
29
components/errors/AuthRequired.vue
Normal file
29
components/errors/AuthRequired.vue
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<Alert class="grid grid-cols-[1fr_auto]">
|
||||
<LogIn class="size-4" />
|
||||
<AlertTitle>{{ m.sunny_quick_lionfish_flip() }}</AlertTitle>
|
||||
<AlertDescription class="col-start-1">
|
||||
{{ m.brave_known_pelican_drip() }}
|
||||
</AlertDescription>
|
||||
<!-- Add pl-4 because Alert is adding additional padding, which we don't want -->
|
||||
<Button
|
||||
variant="secondary"
|
||||
class="w-full col-start-2 row-start-1 row-span-2 !pl-4"
|
||||
@click="signInAction"
|
||||
>
|
||||
{{ m.fuzzy_sea_moth_absorb() }}
|
||||
</Button>
|
||||
</Alert>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { LogIn } from "lucide-vue-next";
|
||||
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
|
||||
const appData = useAppData();
|
||||
const signInAction = async () => signIn(appData, await askForInstance());
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
15
components/errors/NoPosts.vue
Normal file
15
components/errors/NoPosts.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<Alert>
|
||||
<AlertTitle>{{ m.fine_arable_lemming_fold() }}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{{ m.petty_honest_fish_stir() }}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
15
components/errors/NotFound.vue
Normal file
15
components/errors/NotFound.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<Alert>
|
||||
<AlertTitle>{{ m.empty_awful_lark_dart() }}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{{ m.clean_even_mayfly_tap() }}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
15
components/errors/ReachedEnd.vue
Normal file
15
components/errors/ReachedEnd.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<Alert>
|
||||
<AlertTitle>{{ m.steep_suave_fish_snap() }}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{{ m.muddy_bland_shark_accept() }}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
12
components/graphics/spinner.vue
Normal file
12
components/graphics/spinner.vue
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<template>
|
||||
<Card class="size-16">
|
||||
<Loader class="size-full animate-spin" />
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Loader } from "lucide-vue-next";
|
||||
import { Card } from "~/components/ui/card";
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
|
@ -1,19 +1,27 @@
|
|||
<template>
|
||||
<div
|
||||
class="fixed md:hidden bottom-0 inset-x-0 border-t h-16 bg-background z-10 flex items-center justify-around *:h-full *:w-full gap-6 px-4 py-2 [&>a>svg]:size-5 [&>button>svg]:size-5">
|
||||
class="fixed md:hidden bottom-0 inset-x-0 border-t h-16 bg-background z-10 flex items-center justify-around *:h-full *:w-full gap-6 px-4 py-2 [&>a>svg]:size-5 [&>button>svg]:size-5"
|
||||
>
|
||||
<Button :as="NuxtLink" href="/" variant="ghost" size="icon">
|
||||
<Home />
|
||||
</Button>
|
||||
<Button :as="NuxtLink" href="/notifications" variant="ghost" size="icon">
|
||||
<Button
|
||||
:as="NuxtLink"
|
||||
href="/notifications"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
>
|
||||
<Bell />
|
||||
</Button>
|
||||
<AccountSwitcher>
|
||||
<Button variant="ghost" size="icon">
|
||||
<User />
|
||||
</Button>
|
||||
</AccountSwitcher>
|
||||
<Button variant="default" size="icon" :title="m.salty_aloof_turkey_nudge()"
|
||||
@click="useEvent('composer:open')">
|
||||
<Button
|
||||
variant="default"
|
||||
size="icon"
|
||||
:title="m.salty_aloof_turkey_nudge()"
|
||||
@click="useEvent('composer:open')"
|
||||
>
|
||||
<Pen />
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -23,6 +31,5 @@
|
|||
import { Bell, Home, Pen, User } from "lucide-vue-next";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import { NuxtLink } from "#components";
|
||||
import AccountSwitcher from "../sidebars/account-switcher.vue";
|
||||
import { Button } from "../ui/button";
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,29 +1,74 @@
|
|||
<template>
|
||||
<Card as="article" class="rounded-none border-0 duration-200 shadow- max-w-full relative">
|
||||
<CardHeader class="pb-4" as="header">
|
||||
<ReblogHeader v-if="note.reblog" :avatar="note.account.avatar" :display-name="note.account.display_name"
|
||||
:url="reblogAccountUrl" :emojis="note.account.emojis" />
|
||||
<Header :author="noteToUse.account" :author-url="accountUrl"
|
||||
:corner-avatar="note.reblog ? note.account.avatar : undefined" :note-url="url"
|
||||
:visibility="noteToUse.visibility" :created-at="new Date(noteToUse.created_at)"
|
||||
:small-layout="smallLayout" class="z-[1]" />
|
||||
<div v-if="topAvatarBar" :class="cn('shrink-0 bg-border w-0.5 absolute top-0 h-7 left-[3rem]')"></div>
|
||||
<div v-if="bottomAvatarBar" :class="cn('shrink-0 bg-border w-0.5 absolute bottom-0 h-[calc(100%-1.5rem)] left-[3rem]')"></div>
|
||||
<Card as="article" class="relative gap-4 items-stretch">
|
||||
<CardHeader as="header">
|
||||
<ReblogHeader
|
||||
v-if="note.reblog"
|
||||
:avatar="note.account.avatar"
|
||||
:display-name="note.account.display_name"
|
||||
:url="reblogAccountUrl"
|
||||
:emojis="note.account.emojis"
|
||||
/>
|
||||
<Header
|
||||
:author="noteToUse.account"
|
||||
:author-url="accountUrl"
|
||||
:corner-avatar="note.reblog ? note.account.avatar : undefined"
|
||||
:note-url="url"
|
||||
:visibility="noteToUse.visibility"
|
||||
:created-at="new Date(noteToUse.created_at)"
|
||||
:small-layout="smallLayout"
|
||||
class="z-[1]"
|
||||
/>
|
||||
<div
|
||||
v-if="topAvatarBar"
|
||||
:class="
|
||||
cn(
|
||||
'shrink-0 bg-border w-0.5 absolute top-0 h-7 left-[3rem]'
|
||||
)
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
v-if="bottomAvatarBar"
|
||||
:class="
|
||||
cn(
|
||||
'shrink-0 bg-border w-0.5 absolute bottom-0 h-[calc(100%-1.5rem)] left-[3rem]'
|
||||
)
|
||||
"
|
||||
></div>
|
||||
</CardHeader>
|
||||
<!-- Simply offset by the size of avatar + 0.75rem (the gap) -->
|
||||
<CardContent :class="contentUnderUsername && (smallLayout ? 'ml-11' : 'ml-[4.25rem]')">
|
||||
<Content :content="noteToUse.content" :quote="note.quote ?? undefined"
|
||||
:attachments="noteToUse.media_attachments" :plain-content="noteToUse.plain_content ?? undefined"
|
||||
:emojis="noteToUse.emojis" :sensitive="noteToUse.sensitive" :content-warning="noteToUse.spoiler_text" />
|
||||
<CardContent
|
||||
:class="
|
||||
contentUnderUsername && (smallLayout ? 'ml-11' : 'ml-[4.25rem]')
|
||||
"
|
||||
>
|
||||
<Content
|
||||
:content="noteToUse.content"
|
||||
:quote="note.quote ?? undefined"
|
||||
:attachments="noteToUse.media_attachments"
|
||||
:plain-content="noteToUse.plain_content ?? undefined"
|
||||
:emojis="noteToUse.emojis"
|
||||
:sensitive="noteToUse.sensitive"
|
||||
:content-warning="noteToUse.spoiler_text"
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter v-if="!hideActions" class="p-4 pt-0">
|
||||
<Actions :reply-count="noteToUse.replies_count" :like-count="noteToUse.favourites_count" :url="url"
|
||||
:api-note-string="JSON.stringify(note, null, 4)" :reblog-count="noteToUse.reblogs_count"
|
||||
:remote-url="noteToUse.url" :is-remote="isRemote" :author-id="noteToUse.account.id"
|
||||
@edit="useEvent('composer:edit', note)" @reply="useEvent('composer:reply', note)"
|
||||
@quote="useEvent('composer:quote', note)" @delete="useEvent('note:delete', note)"
|
||||
:note-id="noteToUse.id" :liked="noteToUse.favourited ?? false"
|
||||
:reblogged="noteToUse.reblogged ?? false" />
|
||||
<CardFooter v-if="!hideActions">
|
||||
<Actions
|
||||
:reply-count="noteToUse.replies_count"
|
||||
:like-count="noteToUse.favourites_count"
|
||||
:url="url"
|
||||
:api-note-string="JSON.stringify(note, null, 4)"
|
||||
:reblog-count="noteToUse.reblogs_count"
|
||||
:remote-url="noteToUse.url"
|
||||
:is-remote="isRemote"
|
||||
:author-id="noteToUse.account.id"
|
||||
@edit="useEvent('composer:edit', note)"
|
||||
@reply="useEvent('composer:reply', note)"
|
||||
@quote="useEvent('composer:quote', note)"
|
||||
@delete="useEvent('note:delete', note)"
|
||||
:note-id="noteToUse.id"
|
||||
:liked="noteToUse.favourited ?? false"
|
||||
:reblogged="noteToUse.reblogged ?? false"
|
||||
/>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
<template>
|
||||
<div>
|
||||
<Note v-if="parent" :note="parent" :hide-actions="true" :content-under-username="true" :bottom-avatar-bar="true" />
|
||||
<Note :note="note" :top-avatar-bar="!!parent" />
|
||||
<Note
|
||||
v-if="parent"
|
||||
:note="parent"
|
||||
:hide-actions="true"
|
||||
:content-under-username="true"
|
||||
:bottom-avatar-bar="true"
|
||||
class="border-b-0 rounded-b-none"
|
||||
/>
|
||||
<Note
|
||||
:note="note"
|
||||
:class="parent && 'border-t-0 rounded-t-none'"
|
||||
:top-avatar-bar="!!parent"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,30 @@
|
|||
<template>
|
||||
<Card class="rounded-none border-0">
|
||||
<Card class="*:w-full p-2">
|
||||
<Collapsible :default-open="true" v-slot="{ open }">
|
||||
<Tooltip>
|
||||
<TooltipTrigger :as-child="true">
|
||||
<CardHeader v-if="notification.account"
|
||||
class="flex-row items-center gap-2 px-4 py-2 border-b border-border">
|
||||
<CardHeader
|
||||
v-if="notification.account"
|
||||
class="flex-row items-center gap-2 px-2"
|
||||
>
|
||||
<component :is="icon" class="size-5 shrink-0" />
|
||||
<Avatar class="size-6 border border-card" :src="notification.account.avatar" :name="notification.account.display_name" />
|
||||
<span class="font-semibold" v-render-emojis="notification.account.emojis">{{
|
||||
notification.account.display_name
|
||||
}}</span>
|
||||
<Avatar
|
||||
class="size-5 border border-card"
|
||||
:src="notification.account.avatar"
|
||||
:name="notification.account.display_name"
|
||||
/>
|
||||
<span
|
||||
class="font-semibold text-sm"
|
||||
v-render-emojis="notification.account.emojis"
|
||||
>{{ notification.account.display_name }}</span
|
||||
>
|
||||
<CollapsibleTrigger :as-child="true">
|
||||
<Button variant="ghost" size="icon" class="ml-auto [&_svg]:data-[state=open]:-rotate-180" :title="open ? 'Collapse' : 'Expand'">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="ml-auto [&_svg]:data-[state=open]:-rotate-180"
|
||||
:title="open ? 'Collapse' : 'Expand'"
|
||||
>
|
||||
<ChevronDown class="duration-200" />
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
|
|
@ -23,9 +36,19 @@
|
|||
</Tooltip>
|
||||
<CollapsibleContent :as-child="true">
|
||||
<CardContent class="p-0">
|
||||
<Note v-if="notification.status" :note="notification.status" :small-layout="true"
|
||||
:hide-actions="true" />
|
||||
<FollowRequest v-else-if="notification.type === 'follow_request' && notification.account" :follower="notification.account" />
|
||||
<Note
|
||||
v-if="notification.status"
|
||||
:note="notification.status"
|
||||
:small-layout="true"
|
||||
:hide-actions="true"
|
||||
/>
|
||||
<FollowRequest
|
||||
v-else-if="
|
||||
notification.type === 'follow_request' &&
|
||||
notification.account
|
||||
"
|
||||
:follower="notification.account"
|
||||
/>
|
||||
</CardContent>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
|
|
|||
|
|
@ -82,7 +82,11 @@ const issuerRedirectUrl = (issuerId: string) => {
|
|||
|
||||
<template>
|
||||
<div class="grid gap-6">
|
||||
<form @submit="form.submitForm" method="post" :action="redirectUrl.toString()">
|
||||
<form
|
||||
@submit="form.submitForm"
|
||||
method="post"
|
||||
:action="redirectUrl.toString()"
|
||||
>
|
||||
<div class="grid gap-6">
|
||||
<FormField v-slot="{ componentField }" name="identifier">
|
||||
<FormItem>
|
||||
|
|
@ -90,9 +94,15 @@ const issuerRedirectUrl = (issuerId: string) => {
|
|||
{{ m.fluffy_soft_wolf_cook() }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="petergriffin" type="text" auto-capitalize="none"
|
||||
auto-complete="idenfifier" auto-correct="off" :disabled="isLoading"
|
||||
v-bind="componentField" />
|
||||
<Input
|
||||
placeholder="petergriffin"
|
||||
type="text"
|
||||
auto-capitalize="none"
|
||||
auto-complete="idenfifier"
|
||||
auto-correct="off"
|
||||
:disabled="isLoading"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
|
@ -103,9 +113,15 @@ const issuerRedirectUrl = (issuerId: string) => {
|
|||
{{ m.livid_bright_wallaby_quiz() }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="hunter2" type="password" auto-capitalize="none"
|
||||
auto-complete="password" auto-correct="off" :disabled="isLoading"
|
||||
v-bind="componentField" />
|
||||
<Input
|
||||
placeholder="hunter2"
|
||||
type="password"
|
||||
auto-capitalize="none"
|
||||
auto-complete="password"
|
||||
auto-correct="off"
|
||||
:disabled="isLoading"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
|
@ -116,7 +132,10 @@ const issuerRedirectUrl = (issuerId: string) => {
|
|||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<div v-if="ssoConfig && ssoConfig.providers.length > 0" class="relative">
|
||||
<div
|
||||
v-if="ssoConfig && ssoConfig.providers.length > 0"
|
||||
class="relative"
|
||||
>
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<span class="w-full border-t" />
|
||||
</div>
|
||||
|
|
@ -126,11 +145,25 @@ const issuerRedirectUrl = (issuerId: string) => {
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="ssoConfig && ssoConfig.providers.length > 0" class="flex flex-col gap-2">
|
||||
<Button as="a" :href="issuerRedirectUrl(provider.id)" variant="outline" type="button" :disabled="isLoading" v-for="provider of ssoConfig.providers">
|
||||
<div
|
||||
v-if="ssoConfig && ssoConfig.providers.length > 0"
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<Button
|
||||
as="a"
|
||||
:href="issuerRedirectUrl(provider.id)"
|
||||
variant="outline"
|
||||
type="button"
|
||||
:disabled="isLoading"
|
||||
v-for="provider of ssoConfig.providers"
|
||||
>
|
||||
<Loader v-if="isLoading" class="mr-2 animate-spin" />
|
||||
<img crossorigin="anonymous" :src="provider.icon" :alt="`${provider.name}'s logo`"
|
||||
class="size-4 mr-2" />
|
||||
<img
|
||||
crossorigin="anonymous"
|
||||
:src="provider.icon"
|
||||
:alt="`${provider.name}'s logo`"
|
||||
class="size-4 mr-2"
|
||||
/>
|
||||
{{ provider.name }}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
<template>
|
||||
<Collapsible :as="Card" class="grid items-center px-6 py-4 gap-4" v-slot="{ open }">
|
||||
<Collapsible
|
||||
:as="Card"
|
||||
class="grid justify-normal items-center px-6 py-4 gap-4"
|
||||
v-slot="{ open }"
|
||||
>
|
||||
<div class="grid grid-cols-[1fr,auto] items-center gap-4">
|
||||
<CardHeader class="space-y-0.5 p-0">
|
||||
<CardTitle class="text-base">
|
||||
|
|
@ -10,16 +14,27 @@
|
|||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CollapsibleTrigger :as-child="true">
|
||||
<Button variant="outline" size="icon" class="ml-auto [&_svg]:data-[state=open]:-rotate-180"
|
||||
:title="open ? 'Collapse' : 'Expand'">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="ml-auto [&_svg]:data-[state=open]:-rotate-180"
|
||||
:title="open ? 'Collapse' : 'Expand'"
|
||||
>
|
||||
<ChevronDown class="duration-200" />
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
</div>
|
||||
<CollapsibleContent :as-child="true">
|
||||
<CardFooter class="p-1">
|
||||
<Textarea :rows="10" :model-value="setting.value"
|
||||
@update:model-value="v => { setting.value = String(v) }" />
|
||||
<Textarea
|
||||
:rows="10"
|
||||
:model-value="setting.value"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
setting.value = String(v);
|
||||
}
|
||||
"
|
||||
/>
|
||||
</CardFooter>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ const editName = async () => {
|
|||
toast.success(m.gaudy_lime_bison_adore());
|
||||
|
||||
identity.value.emojis = identity.value.emojis.map((e) =>
|
||||
e.id === emoji.id ? data : e
|
||||
e.id === emoji.id ? data : e,
|
||||
);
|
||||
} catch {
|
||||
toast.dismiss(id);
|
||||
|
|
@ -134,7 +134,7 @@ const _delete = async () => {
|
|||
toast.success(m.crisp_whole_canary_tear());
|
||||
|
||||
identity.value.emojis = identity.value.emojis.filter(
|
||||
(e) => e.id !== emoji.id
|
||||
(e) => e.id !== emoji.id,
|
||||
);
|
||||
} catch {
|
||||
toast.dismiss(id);
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ const formSchema = toTypedSchema(
|
|||
count:
|
||||
identity.value?.instance.configuration.emojis
|
||||
.emoji_size_limit ?? Number.POSITIVE_INFINITY,
|
||||
})
|
||||
}),
|
||||
),
|
||||
shortcode: z
|
||||
.string()
|
||||
|
|
@ -240,7 +240,7 @@ const formSchema = toTypedSchema(
|
|||
identity.value?.instance.configuration.emojis
|
||||
.max_emoji_shortcode_characters ??
|
||||
Number.POSITIVE_INFINITY,
|
||||
})
|
||||
}),
|
||||
)
|
||||
.regex(emojiValidator),
|
||||
global: z.boolean().default(false),
|
||||
|
|
@ -250,7 +250,7 @@ const formSchema = toTypedSchema(
|
|||
64,
|
||||
m.home_cool_orangutan_hug({
|
||||
count: 64,
|
||||
})
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
alt: z
|
||||
|
|
@ -264,10 +264,10 @@ const formSchema = toTypedSchema(
|
|||
identity.value?.instance.configuration.emojis
|
||||
.max_emoji_description_characters ??
|
||||
Number.POSITIVE_INFINITY,
|
||||
})
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
}),
|
||||
);
|
||||
const { isSubmitting, handleSubmit, values, setFieldValue } = useForm({
|
||||
validationSchema: formSchema,
|
||||
|
|
@ -288,7 +288,7 @@ const submit = handleSubmit(async (values) => {
|
|||
alt: values.alt,
|
||||
category: values.category,
|
||||
global: values.global,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
toast.dismiss(id);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<Card v-if="identity" class="w-full max-h-full overflow-auto">
|
||||
<Card v-if="identity" class="w-full max-h-full block overflow-y-auto">
|
||||
<form class="p-4 grid gap-6" ref="formRef" @submit="handleSubmit">
|
||||
<FormField v-slot="{ handleChange, handleBlur }" name="banner">
|
||||
<FormItem>
|
||||
|
|
@ -156,6 +156,7 @@
|
|||
v-slot="{ componentField, value, handleChange }"
|
||||
name="bot"
|
||||
:as="Card"
|
||||
class="block"
|
||||
>
|
||||
<FormItem
|
||||
class="grid grid-cols-[1fr,auto] items-center p-6 gap-2"
|
||||
|
|
@ -183,6 +184,7 @@
|
|||
v-slot="{ componentField, value, handleChange }"
|
||||
name="locked"
|
||||
:as="Card"
|
||||
class="block"
|
||||
>
|
||||
<FormItem
|
||||
class="grid grid-cols-[1fr,auto] items-center p-6 gap-2"
|
||||
|
|
@ -210,6 +212,7 @@
|
|||
v-slot="{ componentField, value, handleChange }"
|
||||
name="discoverable"
|
||||
:as="Card"
|
||||
class="block"
|
||||
>
|
||||
<FormItem
|
||||
class="grid grid-cols-[1fr,auto] items-center p-6 gap-2"
|
||||
|
|
@ -234,10 +237,6 @@
|
|||
</FormField>
|
||||
</form>
|
||||
</Card>
|
||||
<Profile
|
||||
:account="account"
|
||||
class="max-w-lg overflow-auto hidden xl:block"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -247,7 +246,6 @@ import { Trash } from "lucide-vue-next";
|
|||
import { useForm } from "vee-validate";
|
||||
import { toast } from "vue-sonner";
|
||||
import { z } from "zod";
|
||||
import Profile from "~/components/profiles/profile.vue";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
|
|
@ -287,7 +285,7 @@ const formSchema = toTypedSchema(
|
|||
m.civil_icy_ant_mend({
|
||||
size: identity.value?.instance.configuration.accounts
|
||||
.header_size_limit,
|
||||
})
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
avatar: z
|
||||
|
|
@ -300,7 +298,7 @@ const formSchema = toTypedSchema(
|
|||
m.zippy_caring_raven_edit({
|
||||
size: identity.value?.instance.configuration.accounts
|
||||
.avatar_size_limit,
|
||||
})
|
||||
}),
|
||||
)
|
||||
.or(z.string().url())
|
||||
.optional(),
|
||||
|
|
@ -308,26 +306,26 @@ const formSchema = toTypedSchema(
|
|||
.string()
|
||||
.max(
|
||||
identity.value.instance.configuration.accounts
|
||||
.max_displayname_characters
|
||||
.max_displayname_characters,
|
||||
),
|
||||
username: z
|
||||
.string()
|
||||
.regex(/^[a-z0-9_-]+$/, m.still_upper_otter_dine())
|
||||
.max(
|
||||
identity.value.instance.configuration.accounts
|
||||
.max_username_characters
|
||||
.max_username_characters,
|
||||
),
|
||||
bio: z
|
||||
.string()
|
||||
.max(
|
||||
identity.value.instance.configuration.accounts
|
||||
.max_note_characters
|
||||
.max_note_characters,
|
||||
),
|
||||
bot: z.boolean(),
|
||||
locked: z.boolean(),
|
||||
discoverable: z.boolean(),
|
||||
fields: z.array(z.object({ name: z.string(), value: z.string() })),
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const form = useForm({
|
||||
|
|
@ -367,8 +365,8 @@ const handleSubmit = form.handleSubmit(async (values) => {
|
|||
// Can't compare two arrays directly in JS, so we need to check if all fields are the same
|
||||
fields_attributes: values.fields.every((field) =>
|
||||
account.value.source?.fields?.some(
|
||||
(f) => f.name === field.name && f.value === field.value
|
||||
)
|
||||
(f) => f.name === field.name && f.value === field.value,
|
||||
),
|
||||
)
|
||||
? undefined
|
||||
: values.fields,
|
||||
|
|
@ -387,8 +385,8 @@ const handleSubmit = form.handleSubmit(async (values) => {
|
|||
try {
|
||||
const { data } = await client.value.updateCredentials(
|
||||
Object.fromEntries(
|
||||
Object.entries(changedData).filter(([, v]) => v !== undefined)
|
||||
)
|
||||
Object.entries(changedData).filter(([, v]) => v !== undefined),
|
||||
),
|
||||
);
|
||||
|
||||
toast.dismiss(id);
|
||||
|
|
@ -399,6 +397,12 @@ const handleSubmit = form.handleSubmit(async (values) => {
|
|||
}
|
||||
|
||||
account.value = data;
|
||||
form.resetForm({
|
||||
values: {
|
||||
...form.values,
|
||||
...values,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
const error = e as ResponseError<{ error: string }>;
|
||||
|
||||
|
|
|
|||
|
|
@ -193,25 +193,25 @@ const schema = toTypedSchema(
|
|||
(v) => v.size <= (maxSize ?? Number.MAX_SAFE_INTEGER),
|
||||
m.zippy_caring_raven_edit({
|
||||
size: maxSize ?? Number.MAX_SAFE_INTEGER,
|
||||
})
|
||||
}),
|
||||
),
|
||||
})
|
||||
.or(
|
||||
z.object({
|
||||
url: z.string().url(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.or(
|
||||
z.object({
|
||||
email: z.string().email(),
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const emailToGravatar = async (email: string) => {
|
||||
const sha256 = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(email)
|
||||
new TextEncoder().encode(email),
|
||||
);
|
||||
|
||||
return `https://www.gravatar.com/avatar/${Array.from(new Uint8Array(sha256))
|
||||
|
|
@ -234,7 +234,7 @@ const submit = handleSubmit(async (values) => {
|
|||
} else if ((values as { email: string }).email) {
|
||||
emit(
|
||||
"submitUrl",
|
||||
await emailToGravatar((values as { email: string }).email)
|
||||
await emailToGravatar((values as { email: string }).email),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<Avatar :shape="(shape.value as 'circle' | 'square')">
|
||||
<Avatar :shape="(shape.value as 'circle' | 'square')" :size="size">
|
||||
<AvatarFallback v-if="name">
|
||||
{{ getInitials(name) }}
|
||||
</AvatarFallback>
|
||||
|
|
@ -11,9 +11,10 @@
|
|||
import { SettingIds } from "~/settings";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||
|
||||
const { name } = defineProps<{
|
||||
const { name, size = "base" } = defineProps<{
|
||||
src?: string;
|
||||
name?: string;
|
||||
size?: "base" | "sm" | "lg";
|
||||
}>();
|
||||
|
||||
/**
|
||||
|
|
|
|||
47
components/profiles/profile-badges.vue
Normal file
47
components/profiles/profile-badges.vue
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<div
|
||||
class="flex flex-row flex-wrap gap-2 -mx-2"
|
||||
v-if="isDeveloper || account.bot || roles.length > 0"
|
||||
>
|
||||
<ProfileBadge
|
||||
v-if="isDeveloper"
|
||||
:name="m.nice_bad_grizzly_coax()"
|
||||
:description="m.honest_jolly_shell_blend()"
|
||||
:verified="true"
|
||||
/>
|
||||
<ProfileBadge
|
||||
v-if="account.bot"
|
||||
:name="m.merry_red_shrimp_bump()"
|
||||
:description="m.sweet_mad_jannes_create()"
|
||||
/>
|
||||
<ProfileBadge
|
||||
v-for="role in roles"
|
||||
:key="role.id"
|
||||
:name="role.name"
|
||||
:description="role.description"
|
||||
:icon="role.icon"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Account } from "@versia/client/types";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import ProfileBadge from "./profile-badge.vue";
|
||||
|
||||
const { account } = defineProps<{
|
||||
account: Account;
|
||||
}>();
|
||||
|
||||
const config = useConfig();
|
||||
const roles = account.roles.filter((r) => r.visible);
|
||||
// Get user handle in username@instance format
|
||||
const handle = account.acct.includes("@")
|
||||
? account.acct
|
||||
: `${account.acct}@${
|
||||
identity.value?.instance.domain ?? window.location.host
|
||||
}`;
|
||||
const isDeveloper = config.DEVELOPER_HANDLES.includes(handle);
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
|
@ -1,12 +1,26 @@
|
|||
<template>
|
||||
<CardHeader class="p-0 relative">
|
||||
<CardHeader class="relative w-full">
|
||||
<div class="bg-muted rounded overflow-hidden h-48 md:h-72 w-full">
|
||||
<img :src="header" alt="" class="object-cover w-full h-full" />
|
||||
<img
|
||||
v-if="header"
|
||||
:src="header"
|
||||
alt=""
|
||||
class="object-cover w-full h-full"
|
||||
/>
|
||||
<!-- Shadow overlay at the bottom -->
|
||||
<div class="absolute bottom-0 w-full h-1/3 bg-gradient-to-b from-black/0 to-black/40"></div>
|
||||
<div
|
||||
class="absolute bottom-0 w-full h-1/3 bg-gradient-to-b from-black/0 to-black/40"
|
||||
></div>
|
||||
</div>
|
||||
<div class="absolute bottom-0 translate-y-1/3 left-4 flex flex-row items-start gap-2">
|
||||
<Avatar size="lg" class="border" :src="avatar" :name="displayName" />
|
||||
<div
|
||||
class="absolute bottom-0 translate-y-1/3 left-4 flex flex-row items-start gap-2"
|
||||
>
|
||||
<Avatar
|
||||
size="lg"
|
||||
class="border"
|
||||
:src="avatar"
|
||||
:name="displayName"
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,27 @@
|
|||
<template>
|
||||
<Card>
|
||||
<ProfileHeader :header="account.header" :avatar="account.avatar" :display-name="account.display_name" />
|
||||
<CardContent class="pt-3 gap-4 flex flex-col">
|
||||
<Card class="*:w-full">
|
||||
<ProfileHeader
|
||||
:header="account.header"
|
||||
:avatar="account.avatar"
|
||||
:display-name="account.display_name"
|
||||
/>
|
||||
<CardContent>
|
||||
<div class="flex flex-row justify-end gap-2">
|
||||
<Button variant="secondary" :disabled="isLoading || relationship?.requested" v-if="!isMe && identity"
|
||||
@click="relationship?.following ? unfollow() : follow()">
|
||||
<Button
|
||||
variant="secondary"
|
||||
:disabled="isLoading || relationship?.requested"
|
||||
v-if="!isMe && identity"
|
||||
@click="relationship?.following ? unfollow() : follow()"
|
||||
>
|
||||
<Loader v-if="isLoading" class="animate-spin" />
|
||||
<span v-else>
|
||||
{{ relationship?.following ? m.brief_upper_otter_cuddle() : relationship?.requested ? m.weak_bright_larva_grasp() : m.lazy_major_loris_grasp() }}
|
||||
{{
|
||||
relationship?.following
|
||||
? m.brief_upper_otter_cuddle()
|
||||
: relationship?.requested
|
||||
? m.weak_bright_larva_grasp()
|
||||
: m.lazy_major_loris_grasp()
|
||||
}}
|
||||
</span>
|
||||
</Button>
|
||||
<ProfileActions :account="account">
|
||||
|
|
@ -22,27 +36,31 @@
|
|||
</CardTitle>
|
||||
<CopyableText :text="account.acct">
|
||||
<span
|
||||
class="font-semibold bg-gradient-to-tr from-pink-700 dark:from-indigo-400 via-purple-700 dark:via-purple-400 to-indigo-700 dark:to-indigo-400 text-transparent bg-clip-text">
|
||||
class="font-semibold bg-gradient-to-tr from-pink-700 dark:from-indigo-400 via-purple-700 dark:via-purple-400 to-indigo-700 dark:to-indigo-400 text-transparent bg-clip-text"
|
||||
>
|
||||
@{{ username }}
|
||||
</span>
|
||||
<span class="text-muted-foreground">{{ instance && "@" }}{{ instance }}</span>
|
||||
<span class="text-muted-foreground"
|
||||
>{{ instance && "@" }}{{ instance }}</span
|
||||
>
|
||||
</CopyableText>
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap gap-2 -mx-2" v-if="isDeveloper || account.bot || roles.length > 0">
|
||||
<ProfileBadge v-if="isDeveloper" :name="m.nice_bad_grizzly_coax()" :description="m.honest_jolly_shell_blend()"
|
||||
:verified="true" />
|
||||
<ProfileBadge v-if="account.bot" :name="m.merry_red_shrimp_bump()"
|
||||
:description="m.sweet_mad_jannes_create()" />
|
||||
<ProfileBadge v-for="role in roles" :key="role.id" :name="role.name" :description="role.description"
|
||||
:icon="role.icon" />
|
||||
</div>
|
||||
<ProfileBadges :account="account" />
|
||||
<ProfileContent :content="account.note" :emojis="account.emojis" />
|
||||
</CardContent>
|
||||
<CardFooter class="flex-col items-start gap-4">
|
||||
<ProfileStats :creation-date="new Date(account.created_at || 0)" :follower-count="account.followers_count"
|
||||
:following-count="account.following_count" :note-count="account.statuses_count" />
|
||||
<CardFooter>
|
||||
<ProfileStats
|
||||
:creation-date="new Date(account.created_at || 0)"
|
||||
:follower-count="account.followers_count"
|
||||
:following-count="account.following_count"
|
||||
:note-count="account.statuses_count"
|
||||
/>
|
||||
<Separator v-if="account.fields.length > 0" />
|
||||
<ProfileFields v-if="account.fields.length > 0" :fields="account.fields" :emojis="account.emojis" />
|
||||
<ProfileFields
|
||||
v-if="account.fields.length > 0"
|
||||
:fields="account.fields"
|
||||
:emojis="account.emojis"
|
||||
/>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</template>
|
||||
|
|
@ -59,7 +77,7 @@ import * as m from "~/paraglide/messages.js";
|
|||
import { SettingIds } from "~/settings";
|
||||
import { confirmModalService } from "../modals/composable";
|
||||
import ProfileActions from "./profile-actions.vue";
|
||||
import ProfileBadge from "./profile-badge.vue";
|
||||
import ProfileBadges from "./profile-badges.vue";
|
||||
import ProfileContent from "./profile-content.vue";
|
||||
import ProfileFields from "./profile-fields.vue";
|
||||
import ProfileHeader from "./profile-header.vue";
|
||||
|
|
@ -69,16 +87,9 @@ const { account } = defineProps<{
|
|||
account: Account;
|
||||
}>();
|
||||
|
||||
const config = useConfig();
|
||||
const { relationship, isLoading } = useRelationship(client, account.id);
|
||||
const isMe = identity.value?.account.id === account.id;
|
||||
const [username, instance] = account.acct.split("@");
|
||||
const roles = account.roles.filter((r) => r.visible);
|
||||
// Get user handle in username@instance format
|
||||
const handle = account.acct.includes("@")
|
||||
? account.acct
|
||||
: `${account.acct}@${identity.value?.instance.domain ?? window.location.host}`;
|
||||
const isDeveloper = config.DEVELOPER_HANDLES.includes(handle);
|
||||
|
||||
const confirmFollows = useSetting(SettingIds.ConfirmFollow);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,25 @@
|
|||
<template>
|
||||
<div class="relative">
|
||||
<div class="bg-muted rounded overflow-hidden h-32 w-full">
|
||||
<img :src="account.header" alt="" class="object-cover w-full h-full" />
|
||||
<img
|
||||
:src="account.header"
|
||||
alt=""
|
||||
class="object-cover w-full h-full"
|
||||
/>
|
||||
<!-- Shadow overlay at the bottom -->
|
||||
<div class="absolute bottom-0 w-full h-1/3 bg-gradient-to-b from-black/0 to-black/40"></div>
|
||||
<div
|
||||
class="absolute bottom-0 w-full h-1/3 bg-gradient-to-b from-black/0 to-black/40"
|
||||
></div>
|
||||
</div>
|
||||
<div class="absolute bottom-0 left-1/2 translate-y-1/3 -translate-x-1/2 flex flex-row items-start gap-2">
|
||||
<Avatar size="base" class="border" :src="account.avatar" :name="account.display_name" />
|
||||
<div
|
||||
class="absolute bottom-0 left-1/2 translate-y-1/3 -translate-x-1/2 flex flex-row items-start gap-2"
|
||||
>
|
||||
<Avatar
|
||||
size="base"
|
||||
class="border"
|
||||
:src="account.avatar"
|
||||
:name="account.display_name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center items-center mt-8">
|
||||
|
|
@ -15,19 +28,32 @@
|
|||
</span>
|
||||
<CopyableText :text="account.acct" class="text-sm">
|
||||
<span
|
||||
class="font-semibold bg-gradient-to-tr from-pink-700 dark:from-indigo-400 via-purple-700 dark:via-purple-400 to-indigo-700 dark:to-indigo-400 text-transparent bg-clip-text">
|
||||
class="font-semibold bg-gradient-to-tr from-pink-700 dark:from-indigo-400 via-purple-700 dark:via-purple-400 to-indigo-700 dark:to-indigo-400 text-transparent bg-clip-text"
|
||||
>
|
||||
@{{ username }}
|
||||
</span>
|
||||
<span class="text-muted-foreground">{{ instance && "@" }}{{ instance }}</span>
|
||||
<span class="text-muted-foreground"
|
||||
>{{ instance && "@" }}{{ instance }}</span
|
||||
>
|
||||
</CopyableText>
|
||||
</div>
|
||||
<ProfileContent :content="account.note" :emojis="account.emojis" class="mt-4 max-h-72 overflow-y-auto" />
|
||||
<ProfileContent
|
||||
:content="account.note"
|
||||
:emojis="account.emojis"
|
||||
class="mt-4 max-h-72 overflow-y-auto"
|
||||
/>
|
||||
<Separator v-if="account.fields.length > 0" class="mt-4" />
|
||||
<ProfileFields v-if="account.fields.length > 0" :fields="account.fields" :emojis="account.emojis" class="mt-4 max-h-48 overflow-y-auto" />
|
||||
<ProfileFields
|
||||
v-if="account.fields.length > 0"
|
||||
:fields="account.fields"
|
||||
:emojis="account.emojis"
|
||||
class="mt-4 max-h-48 overflow-y-auto"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Account } from "@versia/client/types";
|
||||
import { Separator } from "~/components/ui/separator";
|
||||
import CopyableText from "../notes/copyable-text.vue";
|
||||
import Avatar from "./avatar.vue";
|
||||
import ProfileContent from "./profile-content.vue";
|
||||
|
|
|
|||
32
components/profiles/tiny-card.vue
Normal file
32
components/profiles/tiny-card.vue
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<template>
|
||||
<Card
|
||||
class="flex-row gap-4 p-2"
|
||||
:class="naked ? 'p-0 bg-transparent ring-0 border-none' : ''"
|
||||
>
|
||||
<Avatar :src="account.avatar" :name="account.display_name" size="sm" />
|
||||
<CardContent class="gap-1">
|
||||
<span
|
||||
class="truncate font-semibold"
|
||||
v-render-emojis="account.emojis"
|
||||
>{{ account.display_name }}</span
|
||||
>
|
||||
<span class="truncate text-xs">
|
||||
@{{ account.username }}@{{ domain }}
|
||||
</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Account } from "@versia/client/types";
|
||||
import { Card, CardContent } from "~/components/ui/card";
|
||||
import Avatar from "./avatar.vue";
|
||||
|
||||
const { account, domain, naked } = defineProps<{
|
||||
account: Account;
|
||||
domain: string;
|
||||
naked?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
<template>
|
||||
<Drawer v-if="isMobile">
|
||||
<DrawerTrigger :as-child="true">
|
||||
<slot />
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<Button @click="switchAccount(identity.account.id)" variant="outline" size="lg"
|
||||
:href="`/@${identity.account.username}`" v-for="identity of identities"
|
||||
class="flex w-full items-center gap-2 px-4 text-left h-20">
|
||||
<Avatar class="size-12" :src="identity.account.avatar" :name="identity.account.display_name" />
|
||||
<div class="grid flex-1 text-left leading-tight">
|
||||
<span class="truncate font-semibold" v-render-emojis="identity.account.emojis">{{
|
||||
identity.account.display_name
|
||||
}}</span>
|
||||
<span class="truncate text-sm">@{{
|
||||
identity.account.acct
|
||||
}}</span>
|
||||
</div>
|
||||
</Button>
|
||||
<Button variant="secondary" size="lg" class="w-full" @click="signInAction">
|
||||
<UserPlus />
|
||||
{{ m.sunny_pink_hyena_walk() }}
|
||||
</Button>
|
||||
<Button variant="secondary" size="lg" @click="signOut(appData, identity)" v-if="identity">
|
||||
<LogOut />
|
||||
{{ m.sharp_big_mallard_reap() }}
|
||||
</Button>
|
||||
<Button variant="secondary" size="lg" :as="NuxtLink" href="/register" v-else>
|
||||
<LogIn />
|
||||
{{ m.honest_few_baboon_pop() }}
|
||||
</Button>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<DropdownMenu v-else>
|
||||
<DropdownMenuTrigger :as-child="true">
|
||||
<slot />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded" side="bottom"
|
||||
align="end" :side-offset="4">
|
||||
<DropdownMenuLabel class="p-0 font-normal">
|
||||
<Button @click="switchAccount(identity.account.id)" variant="ghost" size="lg"
|
||||
:href="`/@${identity.account.username}`" v-for="identity of identities"
|
||||
class="flex w-full items-center gap-2 px-1 text-left text-sm">
|
||||
<Avatar class="size-8" :src="identity.account.avatar" :name="identity.account.display_name" />
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold" v-render-emojis="identity.account.emojis">{{
|
||||
identity.account.display_name
|
||||
}}</span>
|
||||
<span class="truncate text-xs">@{{
|
||||
identity.account.username
|
||||
}}@{{
|
||||
identity.instance.domain
|
||||
}}</span>
|
||||
</div>
|
||||
</Button>
|
||||
<DropdownMenuItem @click="signInAction">
|
||||
<UserPlus />
|
||||
{{ m.sunny_pink_hyena_walk() }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator v-if="identity" />
|
||||
<DropdownMenuGroup v-if="identity">
|
||||
<DropdownMenuItem :as="NuxtLink" :href="`/@${identity.account.username}`">
|
||||
<BadgeCheck />
|
||||
{{ m.factual_arable_jurgen_endure() }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem @click="signOut(appData, identity)" v-if="identity">
|
||||
<LogOut />
|
||||
{{ m.sharp_big_mallard_reap() }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem :as="NuxtLink" href="/register" v-else>
|
||||
<LogIn />
|
||||
{{ m.honest_few_baboon_pop() }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { BadgeCheck, LogIn, LogOut, UserPlus } from "lucide-vue-next";
|
||||
import { toast } from "vue-sonner";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import { NuxtLink } from "#components";
|
||||
import DrawerContent from "../modals/drawer-content.vue";
|
||||
import Avatar from "../profiles/avatar.vue";
|
||||
import { Button } from "../ui/button";
|
||||
import { Drawer, DrawerTrigger } from "../ui/drawer";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu";
|
||||
|
||||
const appData = useAppData();
|
||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||
|
||||
const signInAction = async () => signIn(appData, await askForInstance());
|
||||
|
||||
const switchAccount = async (userId: string) => {
|
||||
if (userId === identity.value?.account.id) {
|
||||
return await navigateTo(`/@${identity.value.account.username}`);
|
||||
}
|
||||
|
||||
const id = toast.loading("Switching account...");
|
||||
|
||||
const identityToSwitch = identities.value.find(
|
||||
(i) => i.account.id === userId,
|
||||
);
|
||||
|
||||
if (!identityToSwitch) {
|
||||
toast.dismiss(id);
|
||||
toast.error("No identity to switch to");
|
||||
return;
|
||||
}
|
||||
|
||||
identity.value = identityToSwitch;
|
||||
toast.dismiss(id);
|
||||
toast.success("Switched account");
|
||||
|
||||
window.location.href = "/";
|
||||
};
|
||||
</script>
|
||||
92
components/sidebars/account/account-switcher.vue
Normal file
92
components/sidebars/account/account-switcher.vue
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<script setup lang="ts">
|
||||
import { BadgeCheck, LogIn, LogOut, UserPlus } from "lucide-vue-next";
|
||||
import { toast } from "vue-sonner";
|
||||
import TinyCard from "~/components/profiles/tiny-card.vue";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "~/components/ui/dropdown-menu";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import { NuxtLink } from "#components";
|
||||
|
||||
const appData = useAppData();
|
||||
|
||||
const signInAction = async () => signIn(appData, await askForInstance());
|
||||
|
||||
const switchAccount = async (userId: string) => {
|
||||
if (userId === identity.value?.account.id) {
|
||||
return await navigateTo(`/@${identity.value.account.username}`);
|
||||
}
|
||||
|
||||
const id = toast.loading("Switching account...");
|
||||
|
||||
const identityToSwitch = identities.value.find(
|
||||
(i) => i.account.id === userId,
|
||||
);
|
||||
|
||||
if (!identityToSwitch) {
|
||||
toast.dismiss(id);
|
||||
toast.error("No identity to switch to");
|
||||
return;
|
||||
}
|
||||
|
||||
identity.value = identityToSwitch;
|
||||
toast.dismiss(id);
|
||||
toast.success("Switched account");
|
||||
|
||||
window.location.href = "/";
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger :as-child="true">
|
||||
<slot />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel> Your accounts </DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
v-for="identity of identities"
|
||||
:key="identity.account.id"
|
||||
@click="switchAccount(identity.account.id)"
|
||||
:href="`/@${identity.account.username}`"
|
||||
>
|
||||
<TinyCard
|
||||
:account="identity.account"
|
||||
:domain="identity.instance.domain"
|
||||
naked
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="signInAction">
|
||||
<UserPlus />
|
||||
{{ m.sunny_pink_hyena_walk() }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator v-if="identity" />
|
||||
<DropdownMenuGroup v-if="identity">
|
||||
<DropdownMenuItem
|
||||
:as="NuxtLink"
|
||||
:href="`/@${identity.account.username}`"
|
||||
>
|
||||
<BadgeCheck />
|
||||
{{ m.factual_arable_jurgen_endure() }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="signOut(appData, identity)">
|
||||
<LogOut />
|
||||
{{ m.sharp_big_mallard_reap() }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuItem v-else :as="NuxtLink" href="/register">
|
||||
<LogIn />
|
||||
{{ m.honest_few_baboon_pop() }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
60
components/sidebars/footer/footer-actions.vue
Normal file
60
components/sidebars/footer/footer-actions.vue
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<script setup lang="ts">
|
||||
import { ChevronsUpDown, DownloadCloud, Pen } from "lucide-vue-next";
|
||||
import TinyCard from "~/components/profiles/tiny-card.vue";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
SidebarFooter,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "~/components/ui/sidebar";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import AccountSwitcher from "../account/account-switcher.vue";
|
||||
const { $pwa } = useNuxtApp();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SidebarFooter>
|
||||
<SidebarMenu class="gap-3">
|
||||
<SidebarMenuItem>
|
||||
<AccountSwitcher>
|
||||
<SidebarMenuButton size="lg">
|
||||
<TinyCard
|
||||
v-if="identity"
|
||||
:account="identity.account"
|
||||
:domain="identity.instance.domain"
|
||||
naked
|
||||
/>
|
||||
<ChevronsUpDown class="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
</AccountSwitcher>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem class="flex flex-col gap-2">
|
||||
<Button
|
||||
v-if="identity"
|
||||
variant="default"
|
||||
size="lg"
|
||||
class="w-full group-data-[collapsible=icon]:px-4"
|
||||
@click="useEvent('composer:open')"
|
||||
>
|
||||
<Pen />
|
||||
<span class="group-data-[collapsible=icon]:hidden">
|
||||
{{ m.salty_aloof_turkey_nudge() }}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
v-if="$pwa?.needRefresh"
|
||||
variant="destructive"
|
||||
size="lg"
|
||||
class="w-full group-data-[collapsible=icon]:px-4"
|
||||
@click="$pwa?.updateServiceWorker(true)"
|
||||
>
|
||||
<DownloadCloud />
|
||||
<span class="group-data-[collapsible=icon]:hidden">
|
||||
{{ m.quaint_low_felix_pave() }}
|
||||
</span>
|
||||
</Button>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarFooter>
|
||||
</template>
|
||||
49
components/sidebars/instance/instance-header.vue
Normal file
49
components/sidebars/instance/instance-header.vue
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<script setup lang="ts">
|
||||
import Avatar from "~/components/profiles/avatar.vue";
|
||||
import {
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "~/components/ui/sidebar";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
|
||||
const instance = useInstance();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<NuxtLink href="/">
|
||||
<SidebarMenuButton size="lg">
|
||||
<Avatar
|
||||
class="size-8"
|
||||
:src="
|
||||
instance?.thumbnail?.url ??
|
||||
'https://cdn.versia.pub/branding/icon.svg'
|
||||
"
|
||||
:name="instance?.title"
|
||||
/>
|
||||
<div
|
||||
class="grid flex-1 text-left text-sm leading-tight"
|
||||
>
|
||||
<span class="truncate font-semibold">
|
||||
{{
|
||||
instance?.title ??
|
||||
m.short_zippy_felix_kick()
|
||||
}}
|
||||
</span>
|
||||
<span class="truncate text-xs">
|
||||
{{
|
||||
instance?.description ??
|
||||
m.top_active_ocelot_cure()
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</NuxtLink>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
</template>
|
||||
|
|
@ -1,209 +1,43 @@
|
|||
<template>
|
||||
<Sidebar collapsible="none" class="hidden md:flex">
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<NuxtLink href="/">
|
||||
<SidebarMenuButton size="lg"
|
||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
|
||||
<Avatar class="size-8" :src="instance?.thumbnail.url ??
|
||||
'https://cdn.versia.pub/branding/icon.svg'
|
||||
" :name="instance?.title" />
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold">{{ instance?.title ?? m.short_zippy_felix_kick()
|
||||
}}</span>
|
||||
<span class="truncate text-xs">{{ instance?.description ?? m.top_active_ocelot_cure() }}</span>
|
||||
</div>
|
||||
<!-- <ChevronsUpDown class="ml-auto" /> -->
|
||||
</SidebarMenuButton>
|
||||
</NuxtLink>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<Sidebar collapsible="offcanvas">
|
||||
<InstanceHeader />
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>{{ m.trite_real_sawfish_drum() }}</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem v-for="item in data.other.filter(
|
||||
i => i.requiresLogin ? !!identity : true,
|
||||
)" :key="item.name">
|
||||
<SidebarMenuButton as-child>
|
||||
<NuxtLink :href="item.url">
|
||||
<component :is="item.icon" />
|
||||
<span>{{ item.name }}</span>
|
||||
</NuxtLink>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
<SidebarGroupLabel>{{
|
||||
m.trite_real_sawfish_drum()
|
||||
}}</SidebarGroupLabel>
|
||||
<NavItems
|
||||
:items="
|
||||
sidebarConfig.other.filter((i) =>
|
||||
i.requiresLogin ? !!identity : true
|
||||
)
|
||||
"
|
||||
/>
|
||||
</SidebarGroup>
|
||||
<SidebarGroup v-if="identity" class="mt-auto">
|
||||
<SidebarGroupLabel>{{ m.close_short_kitten_coax() }}</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
<Collapsible v-for="item in data.navMain" :key="item.title" as-child class="group/collapsible">
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger as-child>
|
||||
<SidebarMenuButton :tooltip="item.title">
|
||||
<component :is="item.icon" />
|
||||
<span>{{ item.title }}</span>
|
||||
<ChevronRight
|
||||
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
<SidebarMenuSubItem v-for="subItem in item.items" :key="subItem.title">
|
||||
<SidebarMenuSubButton as-child>
|
||||
<NuxtLink :href="subItem.url">
|
||||
<span>{{ subItem.title }}</span>
|
||||
</NuxtLink>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
</SidebarMenu>
|
||||
<SidebarGroupLabel>{{
|
||||
m.close_short_kitten_coax()
|
||||
}}</SidebarGroupLabel>
|
||||
<NavGroup :items="sidebarConfig.navMain" />
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<SidebarMenu class="gap-3">
|
||||
<SidebarMenuItem>
|
||||
<AccountSwitcher>
|
||||
<SidebarMenuButton size="lg"
|
||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
|
||||
<Avatar v-if="identity" class="size-8" :src="identity.account.avatar"
|
||||
:name="identity.account.display_name" />
|
||||
<Avatar v-else class="size-8" name="AB" />
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold" v-render-emojis="identity?.account.emojis">{{
|
||||
identity?.account.display_name ?? "Not signed in"
|
||||
}}</span>
|
||||
<span class="truncate text-xs" v-if="identity">@{{ identity?.account.acct }}</span>
|
||||
</div>
|
||||
<ChevronsUpDown class="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
</AccountSwitcher>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem class="flex flex-col gap-2">
|
||||
<Button variant="default" size="lg" class="w-full group-data-[collapsible=icon]:px-4"
|
||||
v-if="identity" @click="useEvent('composer:open')">
|
||||
<Pen />
|
||||
<span class="group-data-[collapsible=icon]:hidden">{{ m.salty_aloof_turkey_nudge() }}</span>
|
||||
</Button>
|
||||
<Button variant="destructive" size="lg" class="w-full group-data-[collapsible=icon]:px-4"
|
||||
v-if="$pwa?.needRefresh" @click="$pwa?.updateServiceWorker(true)">
|
||||
<DownloadCloud />
|
||||
<span class="group-data-[collapsible=icon]:hidden">{{ m.quaint_low_felix_pave() }}</span>
|
||||
</Button>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarFooter>
|
||||
<FooterActions />
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
BedSingle,
|
||||
Bell,
|
||||
ChevronRight,
|
||||
ChevronsUpDown,
|
||||
DownloadCloud,
|
||||
Globe,
|
||||
House,
|
||||
MapIcon,
|
||||
Pen,
|
||||
Settings2,
|
||||
} from "lucide-vue-next";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "~/components/ui/collapsible";
|
||||
import { sidebarConfig } from "~/components/sidebars/sidebar";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarRail,
|
||||
} from "~/components/ui/sidebar";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import Avatar from "../profiles/avatar.vue";
|
||||
import { Button } from "../ui/button";
|
||||
import AccountSwitcher from "./account-switcher.vue";
|
||||
|
||||
const data = {
|
||||
navMain: [
|
||||
{
|
||||
title: m.patchy_seemly_hound_grace(),
|
||||
url: "/preferences",
|
||||
icon: Settings2,
|
||||
items: [
|
||||
{
|
||||
title: m.factual_arable_jurgen_endure(),
|
||||
url: "/preferences/account",
|
||||
},
|
||||
{
|
||||
title: m.tough_clean_wolf_gleam(),
|
||||
url: "/preferences/appearance",
|
||||
},
|
||||
{
|
||||
title: m.legal_best_tadpole_rise(),
|
||||
url: "/preferences/behaviour",
|
||||
},
|
||||
{
|
||||
title: m.novel_trite_sloth_adapt(),
|
||||
url: "/preferences/emojis",
|
||||
},
|
||||
{
|
||||
title: m.safe_green_mink_cook(),
|
||||
url: "/preferences/roles",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
other: [
|
||||
{
|
||||
name: m.bland_chunky_sparrow_propel(),
|
||||
url: "/home",
|
||||
icon: House,
|
||||
requiresLogin: true,
|
||||
},
|
||||
{
|
||||
name: m.lost_trick_dog_grace(),
|
||||
url: "/public",
|
||||
icon: MapIcon,
|
||||
requiresLogin: false,
|
||||
},
|
||||
{
|
||||
name: m.crazy_game_parrot_pave(),
|
||||
url: "/local",
|
||||
icon: BedSingle,
|
||||
requiresLogin: false,
|
||||
},
|
||||
{
|
||||
name: m.real_tame_moose_greet(),
|
||||
url: "/global",
|
||||
icon: Globe,
|
||||
requiresLogin: false,
|
||||
},
|
||||
{
|
||||
name: m.that_patchy_mare_snip(),
|
||||
url: "/notifications",
|
||||
icon: Bell,
|
||||
requiresLogin: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const instance = useInstance();
|
||||
const { $pwa } = useNuxtApp();
|
||||
import FooterActions from "./footer/footer-actions.vue";
|
||||
import InstanceHeader from "./instance/instance-header.vue";
|
||||
import NavGroup from "./navigation/nav-group.vue";
|
||||
import NavItems from "./navigation/nav-items.vue";
|
||||
</script>
|
||||
|
|
|
|||
59
components/sidebars/navigation/nav-group.vue
Normal file
59
components/sidebars/navigation/nav-group.vue
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<script setup lang="ts">
|
||||
import { ChevronRight } from "lucide-vue-next";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "~/components/ui/collapsible";
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
} from "~/components/ui/sidebar";
|
||||
import type { SidebarNavMainItem } from "~/types/sidebar";
|
||||
|
||||
defineProps<{
|
||||
items: SidebarNavMainItem[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SidebarMenu>
|
||||
<Collapsible
|
||||
v-for="item in items"
|
||||
:key="item.title"
|
||||
as-child
|
||||
default-open
|
||||
class="group/collapsible"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger as-child>
|
||||
<SidebarMenuButton :tooltip="item.title">
|
||||
<component :is="item.icon" />
|
||||
{{ item.title }}
|
||||
<ChevronRight
|
||||
class="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180"
|
||||
/>
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
<SidebarMenuSubItem
|
||||
v-for="subItem in item.items"
|
||||
:key="subItem.title"
|
||||
>
|
||||
<SidebarMenuSubButton as-child>
|
||||
<NuxtLink :href="subItem.url">
|
||||
<span>{{ subItem.title }}</span>
|
||||
</NuxtLink>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
</SidebarMenu>
|
||||
</template>
|
||||
25
components/sidebars/navigation/nav-items.vue
Normal file
25
components/sidebars/navigation/nav-items.vue
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "~/components/ui/sidebar";
|
||||
import type { SidebarNavItem } from "~/types/sidebar";
|
||||
|
||||
defineProps<{
|
||||
items: SidebarNavItem[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem v-for="item in items" :key="item.title">
|
||||
<SidebarMenuButton as-child>
|
||||
<NuxtLink :href="item.url">
|
||||
<component :is="item.icon" />
|
||||
<span>{{ item.title }}</span>
|
||||
</NuxtLink>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</template>
|
||||
|
|
@ -1,13 +1,17 @@
|
|||
<template>
|
||||
<Sidebar side="right" collapsible="none" class="w-96 hidden lg:flex">
|
||||
<SidebarContent class="p-2 overflow-y-auto">
|
||||
<Sidebar
|
||||
side="right"
|
||||
collapsible="none"
|
||||
class="hidden md:flex"
|
||||
style="--sidebar-width: 24rem; --sidebar-width-mobile: 18rem"
|
||||
>
|
||||
<SidebarContent class="overflow-y-auto *:p-2 *:gap-2">
|
||||
<NotificationsTimeline />
|
||||
</SidebarContent>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import NotificationsTimeline from "../timelines/notifications.vue";
|
||||
import { Sidebar, SidebarContent, SidebarRail } from "../ui/sidebar";
|
||||
<script setup lang="ts">
|
||||
import NotificationsTimeline from "~/components/timelines/notifications.vue";
|
||||
import { Sidebar, SidebarContent } from "~/components/ui/sidebar";
|
||||
</script>
|
||||
|
|
|
|||
74
components/sidebars/sidebar.ts
Normal file
74
components/sidebars/sidebar.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import {
|
||||
BedSingle,
|
||||
Bell,
|
||||
Globe,
|
||||
House,
|
||||
MapIcon,
|
||||
Settings2,
|
||||
} from "lucide-vue-next";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import type { SidebarConfig } from "~/types/sidebar";
|
||||
|
||||
export const sidebarConfig: SidebarConfig = {
|
||||
navMain: [
|
||||
{
|
||||
title: m.patchy_seemly_hound_grace(),
|
||||
url: "/preferences",
|
||||
icon: Settings2,
|
||||
items: [
|
||||
{
|
||||
title: m.factual_arable_jurgen_endure(),
|
||||
url: "/preferences/account",
|
||||
},
|
||||
{
|
||||
title: m.tough_clean_wolf_gleam(),
|
||||
url: "/preferences/appearance",
|
||||
},
|
||||
{
|
||||
title: m.legal_best_tadpole_rise(),
|
||||
url: "/preferences/behaviour",
|
||||
},
|
||||
{
|
||||
title: m.novel_trite_sloth_adapt(),
|
||||
url: "/preferences/emojis",
|
||||
},
|
||||
{
|
||||
title: m.safe_green_mink_cook(),
|
||||
url: "/preferences/roles",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
other: [
|
||||
{
|
||||
title: m.bland_chunky_sparrow_propel(),
|
||||
url: "/home",
|
||||
icon: House,
|
||||
requiresLogin: true,
|
||||
},
|
||||
{
|
||||
title: m.lost_trick_dog_grace(),
|
||||
url: "/public",
|
||||
icon: MapIcon,
|
||||
requiresLogin: false,
|
||||
},
|
||||
{
|
||||
title: m.crazy_game_parrot_pave(),
|
||||
url: "/local",
|
||||
icon: BedSingle,
|
||||
requiresLogin: false,
|
||||
},
|
||||
{
|
||||
title: m.real_tame_moose_greet(),
|
||||
url: "/global",
|
||||
icon: Globe,
|
||||
requiresLogin: false,
|
||||
},
|
||||
{
|
||||
title: m.that_patchy_mare_snip(),
|
||||
url: "/notifications",
|
||||
icon: Bell,
|
||||
requiresLogin: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SidebarInset, SidebarProvider } from "~/components/ui/sidebar";
|
||||
import Timelines from "~/components/navigation/timelines.vue";
|
||||
import { SidebarInset } from "~/components/ui/sidebar";
|
||||
import { SettingIds } from "~/settings";
|
||||
import Timelines from "../navigation/timelines.vue";
|
||||
import LeftSidebar from "./left-sidebar.vue";
|
||||
import RightSidebar from "./right-sidebar.vue";
|
||||
|
||||
|
|
@ -18,14 +18,15 @@ const showTimelines = computed(
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<SidebarProvider>
|
||||
<LeftSidebar />
|
||||
<SidebarInset :class="cn('relative overflow-y-auto overflow-x-hidden', !isMd && 'pt-4')">
|
||||
<header v-if="showTimelines" class="flex h-16 items-center bg-background/80 backdrop-blur-2xl sticky top-0 inset-x-0 z-10 p-4">
|
||||
<main class="grow h-dvh overflow-y-auto">
|
||||
<header
|
||||
v-if="showTimelines"
|
||||
class="flex h-16 items-center bg-background/80 backdrop-blur-2xl sticky top-0 inset-x-0 z-10 p-4"
|
||||
>
|
||||
<Timelines />
|
||||
</header>
|
||||
<slot />
|
||||
</SidebarInset>
|
||||
</main>
|
||||
<RightSidebar v-if="identity" v-show="showRightSidebar.value" />
|
||||
</SidebarProvider>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,59 +1,52 @@
|
|||
<!-- Timeline.vue -->
|
||||
<template>
|
||||
<TransitionGroup name="timeline-item" tag="div" class="timeline-items first:*:rounded-t divide-y divide-border *:border-x first:*:border-t *:overflow-hidden last:*:rounded-b last:*:!border-b *:shadow-none">
|
||||
<TimelineItem :type="type" v-for="item in items" :key="item.id" :item="item" @update="updateItem"
|
||||
@delete="removeItem" />
|
||||
</TransitionGroup>
|
||||
<div
|
||||
role="status"
|
||||
class="flex flex-col gap-4 items-center *:max-w-2xl *:w-full p-4"
|
||||
>
|
||||
<TimelineItem
|
||||
:type="type"
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
@update="updateItem"
|
||||
@delete="removeItem"
|
||||
/>
|
||||
|
||||
<div v-if="isLoading" class="p-4 flex items-center justify-center h-48">
|
||||
<Loader class="size-8 animate-spin" />
|
||||
</div>
|
||||
<Spinner v-if="isLoading" />
|
||||
|
||||
<div v-if="error" class="timeline-error">
|
||||
{{ error.message }}
|
||||
</div>
|
||||
|
||||
<!-- If there are some posts, but the user scrolled to the end -->
|
||||
<Card v-if="hasReachedEnd && items.length > 0" class="shadow-none bg-transparent border-none p-4">
|
||||
<CardHeader class="text-center gap-y-4">
|
||||
<CardTitle>{{ m.steep_suave_fish_snap() }}</CardTitle>
|
||||
<CardDescription>
|
||||
{{ m.muddy_bland_shark_accept() }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<ReachedEnd v-if="hasReachedEnd && items.length > 0" />
|
||||
|
||||
<!-- If there are no posts at all -->
|
||||
<Card v-else-if="hasReachedEnd && items.length === 0" class="shadow-none bg-transparent border-none p-4">
|
||||
<CardHeader class="text-center gap-y-4">
|
||||
<CardTitle>{{ m.fine_arable_lemming_fold() }}</CardTitle>
|
||||
<CardDescription>
|
||||
{{ m.petty_honest_fish_stir() }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<NoPosts v-else-if="hasReachedEnd && items.length === 0" />
|
||||
|
||||
<div v-else-if="!infiniteScroll.value" class="py-10 px-4">
|
||||
<Button variant="secondary" @click="loadNext" :disabled="isLoading" class="w-full">
|
||||
<Button
|
||||
variant="secondary"
|
||||
@click="loadNext"
|
||||
:disabled="isLoading"
|
||||
class="w-full"
|
||||
>
|
||||
{{ m.gaudy_bland_gorilla_talk() }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-else ref="loadMoreTrigger" class="h-20"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Notification, Status } from "@versia/client/types";
|
||||
import { useIntersectionObserver } from "@vueuse/core";
|
||||
import { Loader } from "lucide-vue-next";
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import { SettingIds } from "~/settings";
|
||||
import NoPosts from "../errors/NoPosts.vue";
|
||||
import ReachedEnd from "../errors/ReachedEnd.vue";
|
||||
import Spinner from "../graphics/spinner.vue";
|
||||
import { Button } from "../ui/button";
|
||||
import TimelineItem from "./timeline-item.vue";
|
||||
|
||||
|
|
@ -92,16 +85,3 @@ onMounted(() => {
|
|||
props.loadNext();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.timeline-item-enter-active,
|
||||
.timeline-item-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.timeline-item-enter-from,
|
||||
.timeline-item-leave-to {
|
||||
opacity: 0;
|
||||
scale: 0.99;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {
|
|||
type AlertDialogProps,
|
||||
AlertDialogRoot,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
} from "reka-ui";
|
||||
|
||||
const props = defineProps<AlertDialogProps>();
|
||||
const emits = defineEmits<AlertDialogEmits>();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AlertDialogAction, type AlertDialogActionProps } from "radix-vue";
|
||||
import { AlertDialogAction, type AlertDialogActionProps } from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import { buttonVariants } from "~/components/ui/button";
|
||||
|
||||
const props = defineProps<
|
||||
AlertDialogActionProps & { class?: HTMLAttributes["class"] }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AlertDialogCancel, type AlertDialogCancelProps } from "radix-vue";
|
||||
import { AlertDialogCancel, type AlertDialogCancelProps } from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import { buttonVariants } from "~/components/ui/button";
|
||||
|
||||
const props = defineProps<
|
||||
AlertDialogCancelProps & { class?: HTMLAttributes["class"] }
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
} from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
|
||||
const props = defineProps<
|
||||
|
|
@ -33,7 +33,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded',
|
||||
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { cn } from "@/lib/utils";
|
|||
import {
|
||||
AlertDialogDescription,
|
||||
type AlertDialogDescriptionProps,
|
||||
} from "radix-vue";
|
||||
} from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
|
||||
const props = defineProps<
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AlertDialogTitle, type AlertDialogTitleProps } from "radix-vue";
|
||||
import { AlertDialogTitle, type AlertDialogTitleProps } from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
|
||||
const props = defineProps<
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { AlertDialogTrigger, type AlertDialogTriggerProps } from "radix-vue";
|
||||
import { AlertDialogTrigger, type AlertDialogTriggerProps } from "reka-ui";
|
||||
|
||||
const props = defineProps<AlertDialogTriggerProps>();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export { default as AlertDescription } from "./AlertDescription.vue";
|
|||
export { default as AlertTitle } from "./AlertTitle.vue";
|
||||
|
||||
export const alertVariants = cva(
|
||||
"relative w-full rounded border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AvatarRoot } from "radix-vue";
|
||||
import { AvatarRoot } from "reka-ui";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { type AvatarVariants, avatarVariant } from ".";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { AvatarFallback, type AvatarFallbackProps } from "radix-vue";
|
||||
import { AvatarFallback, type AvatarFallbackProps } from "reka-ui";
|
||||
|
||||
const props = defineProps<AvatarFallbackProps>();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import { AvatarImage, type AvatarImageProps } from "radix-vue";
|
||||
import type { AvatarImageProps } from "reka-ui";
|
||||
import { AvatarImage } from "reka-ui";
|
||||
|
||||
const props = defineProps<AvatarImageProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AvatarImage v-bind="props" class="h-full w-full object-cover" />
|
||||
<AvatarImage v-bind="props" class="h-full w-full object-cover">
|
||||
<slot />
|
||||
</AvatarImage>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export const avatarVariant = cva(
|
|||
},
|
||||
shape: {
|
||||
circle: "rounded-full",
|
||||
square: "rounded",
|
||||
square: "rounded-md",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Primitive, type PrimitiveProps } from "radix-vue";
|
||||
import { Primitive, type PrimitiveProps } from "reka-ui";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { type ButtonVariants, buttonVariants } from ".";
|
||||
|
||||
|
|
@ -20,7 +20,6 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn(buttonVariants({ variant, size }), props.class)"
|
||||
data-component="button"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
export { default as Button } from "./Button.vue";
|
||||
|
||||
export const buttonVariants = cva(
|
||||
|
|
@ -7,21 +9,21 @@ export const buttonVariants = cva(
|
|||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
"bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 px-3 text-xs",
|
||||
lg: "h-10 px-8",
|
||||
icon: "size-9",
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
|
|
|||
|
|
@ -1,22 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Primitive, type PrimitiveProps } from "radix-vue";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<PrimitiveProps & { class?: HTMLAttributes["class"] }>(),
|
||||
{
|
||||
as: "div",
|
||||
},
|
||||
);
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="props.as" :as-child="props.asChild" :class="cn(
|
||||
'rounded-lg border bg-card/90 backdrop-blur-xl text-card-foreground shadow-sm',
|
||||
props.class,
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'rounded-lg border bg-card text-card-foreground shadow-sm flex flex-col gap-6 p-4 items-center justify-center',
|
||||
props.class
|
||||
)
|
||||
" data-component="card">
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const props = defineProps<{
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('p-6 pt-0', props.class)">
|
||||
<div :class="cn('flex flex-col gap-2', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const props = defineProps<{
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('flex items-center p-6 pt-0', props.class)">
|
||||
<div :class="cn('flex items-center', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Primitive, type PrimitiveProps } from "radix-vue";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<PrimitiveProps & { class?: HTMLAttributes["class"] }>(),
|
||||
{
|
||||
as: "div",
|
||||
},
|
||||
);
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="props.as" :as-child="props.asChild" :class="cn('flex flex-col gap-y-1.5 p-6', props.class)">
|
||||
<div :class="cn('flex flex-col gap-y-1.5', props.class)">
|
||||
<slot />
|
||||
</Primitive>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Check } from "lucide-vue-next";
|
||||
import type { CheckboxRootEmits, CheckboxRootProps } from "radix-vue";
|
||||
import {
|
||||
CheckboxIndicator,
|
||||
CheckboxRoot,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
import type { CheckboxRootEmits, CheckboxRootProps } from "reka-ui";
|
||||
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
|
||||
const props = defineProps<
|
||||
|
|
@ -24,11 +20,15 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<CheckboxRoot v-bind="forwarded" :class="cn('peer size-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||
props.class)">
|
||||
<CheckboxRoot
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn('peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||
props.class)"
|
||||
>
|
||||
<CheckboxIndicator class="flex h-full w-full items-center justify-center text-current">
|
||||
<slot>
|
||||
<Check class="size-4" />
|
||||
<Check class="h-4 w-4" />
|
||||
</slot>
|
||||
</CheckboxIndicator>
|
||||
</CheckboxRoot>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import type { CollapsibleRootEmits, CollapsibleRootProps } from "radix-vue";
|
||||
import { CollapsibleRoot, useForwardPropsEmits } from "radix-vue";
|
||||
import type { CollapsibleRootEmits, CollapsibleRootProps } from "reka-ui";
|
||||
import { CollapsibleRoot, useForwardPropsEmits } from "reka-ui";
|
||||
|
||||
const props = defineProps<CollapsibleRootProps>();
|
||||
const emits = defineEmits<CollapsibleRootEmits>();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { CollapsibleContent, type CollapsibleContentProps } from "radix-vue";
|
||||
import { CollapsibleContent, type CollapsibleContentProps } from "reka-ui";
|
||||
|
||||
const props = defineProps<CollapsibleContentProps>();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { CollapsibleTrigger, type CollapsibleTriggerProps } from "radix-vue";
|
||||
import { CollapsibleTrigger, type CollapsibleTriggerProps } from "reka-ui";
|
||||
|
||||
const props = defineProps<CollapsibleTriggerProps>();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ComboboxRootEmits, ComboboxRootProps } from "radix-vue";
|
||||
import { ComboboxRoot, useForwardPropsEmits } from "radix-vue";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import type { ListboxRootEmits, ListboxRootProps } from "reka-ui";
|
||||
import { ListboxRoot, useFilter, useForwardPropsEmits } from "reka-ui";
|
||||
import { type HTMLAttributes, computed, reactive, ref, watch } from "vue";
|
||||
import { provideCommandContext } from ".";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<ComboboxRootProps & { class?: HTMLAttributes["class"] }>(),
|
||||
defineProps<ListboxRootProps & { class?: HTMLAttributes["class"] }>(),
|
||||
{
|
||||
open: true,
|
||||
modelValue: "",
|
||||
},
|
||||
);
|
||||
|
||||
const emits = defineEmits<ComboboxRootEmits>();
|
||||
const emits = defineEmits<ListboxRootEmits>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
|
@ -21,11 +21,84 @@ const delegatedProps = computed(() => {
|
|||
});
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
|
||||
const allItems = ref<Map<string, string>>(new Map());
|
||||
const allGroups = ref<Map<string, Set<string>>>(new Map());
|
||||
|
||||
const { contains } = useFilter({ sensitivity: "base" });
|
||||
const filterState = reactive({
|
||||
search: "",
|
||||
filtered: {
|
||||
/** The count of all visible items. */
|
||||
count: 0,
|
||||
/** Map from visible item id to its search score. */
|
||||
items: new Map() as Map<string, number>,
|
||||
/** Set of groups with at least one visible item. */
|
||||
groups: new Set() as Set<string>,
|
||||
},
|
||||
});
|
||||
|
||||
function filterItems() {
|
||||
if (!filterState.search) {
|
||||
filterState.filtered.count = allItems.value.size;
|
||||
// Do nothing, each item will know to show itself because search is empty
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the groups
|
||||
filterState.filtered.groups = new Set();
|
||||
let itemCount = 0;
|
||||
|
||||
// Check which items should be included
|
||||
for (const [id, value] of allItems.value) {
|
||||
const score = contains(value, filterState.search);
|
||||
filterState.filtered.items.set(id, score ? 1 : 0);
|
||||
if (score) {
|
||||
itemCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Check which groups have at least 1 item shown
|
||||
for (const [groupId, group] of allGroups.value) {
|
||||
for (const itemId of group) {
|
||||
if (filterState.filtered.items.get(itemId)) {
|
||||
filterState.filtered.groups.add(groupId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterState.filtered.count = itemCount;
|
||||
}
|
||||
|
||||
function handleSelect() {
|
||||
filterState.search = "";
|
||||
}
|
||||
|
||||
watch(
|
||||
() => filterState.search,
|
||||
() => {
|
||||
filterItems();
|
||||
},
|
||||
);
|
||||
|
||||
provideCommandContext({
|
||||
allItems,
|
||||
allGroups,
|
||||
filterState,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxRoot v-bind="forwarded"
|
||||
:class="cn('flex h-full w-full flex-col overflow-hidden rounded bg-popover text-popover-foreground', props.class)">
|
||||
<ListboxRoot
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</ComboboxRoot>
|
||||
</ListboxRoot>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import type { DialogRootEmits, DialogRootProps } from "radix-vue";
|
||||
import { useForwardPropsEmits } from "radix-vue";
|
||||
import { Dialog, DialogContent } from "~/components/ui/dialog";
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||
import type { DialogRootEmits, DialogRootProps } from "reka-ui";
|
||||
import { useForwardPropsEmits } from "reka-ui";
|
||||
import Command from "./Command.vue";
|
||||
|
||||
const props = defineProps<DialogRootProps>();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ComboboxEmptyProps } from "radix-vue";
|
||||
import { ComboboxEmpty } from "radix-vue";
|
||||
import type { PrimitiveProps } from "reka-ui";
|
||||
import { Primitive } from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import { useCommand } from ".";
|
||||
|
||||
const props = defineProps<
|
||||
ComboboxEmptyProps & { class?: HTMLAttributes["class"] }
|
||||
PrimitiveProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
|
|
@ -13,10 +14,15 @@ const delegatedProps = computed(() => {
|
|||
|
||||
return delegated;
|
||||
});
|
||||
|
||||
const { filterState } = useCommand();
|
||||
const isRender = computed(
|
||||
() => !!filterState.search && filterState.filtered.count === 0,
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxEmpty v-bind="delegatedProps" :class="cn('py-6 text-center text-sm', props.class)">
|
||||
<Primitive v-if="isRender" v-bind="delegatedProps" :class="cn('py-6 text-center text-sm', props.class)">
|
||||
<slot />
|
||||
</ComboboxEmpty>
|
||||
</Primitive>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ComboboxGroupProps } from "radix-vue";
|
||||
import { ComboboxGroup, ComboboxLabel } from "radix-vue";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import type { ListboxGroupProps } from "reka-ui";
|
||||
import { ListboxGroup, ListboxGroupLabel, useId } from "reka-ui";
|
||||
import { type HTMLAttributes, computed, onMounted, onUnmounted } from "vue";
|
||||
import { provideCommandGroupContext, useCommand } from ".";
|
||||
|
||||
const props = defineProps<
|
||||
ComboboxGroupProps & {
|
||||
ListboxGroupProps & {
|
||||
class?: HTMLAttributes["class"];
|
||||
heading?: string;
|
||||
}
|
||||
|
|
@ -16,16 +17,43 @@ const delegatedProps = computed(() => {
|
|||
|
||||
return delegated;
|
||||
});
|
||||
|
||||
const { allGroups, filterState } = useCommand();
|
||||
const id = useId();
|
||||
|
||||
const isRender = computed(() =>
|
||||
filterState.search ? filterState.filtered.groups.has(id) : true,
|
||||
);
|
||||
|
||||
provideCommandGroupContext({ id });
|
||||
onMounted(() => {
|
||||
if (!allGroups.value.has(id)) {
|
||||
allGroups.value.set(id, new Set());
|
||||
}
|
||||
});
|
||||
onUnmounted(() => {
|
||||
allGroups.value.delete(id);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxGroup
|
||||
<ListboxGroup
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground', props.class)"
|
||||
:id="id"
|
||||
:class="
|
||||
cn(
|
||||
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
:hidden="isRender ? undefined : true"
|
||||
>
|
||||
<ListboxGroupLabel
|
||||
v-if="heading"
|
||||
class="px-2 py-1.5 text-xs font-medium text-muted-foreground"
|
||||
>
|
||||
<ComboboxLabel v-if="heading" class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
|
||||
{{ heading }}
|
||||
</ComboboxLabel>
|
||||
</ListboxGroupLabel>
|
||||
<slot />
|
||||
</ComboboxGroup>
|
||||
</ListboxGroup>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -2,18 +2,19 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
import { Search } from "lucide-vue-next";
|
||||
import {
|
||||
ComboboxInput,
|
||||
type ComboboxInputProps,
|
||||
ListboxFilter,
|
||||
type ListboxFilterProps,
|
||||
useForwardProps,
|
||||
} from "radix-vue";
|
||||
} from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import { useCommand } from ".";
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = defineProps<
|
||||
ComboboxInputProps & {
|
||||
ListboxFilterProps & {
|
||||
class?: HTMLAttributes["class"];
|
||||
}
|
||||
>();
|
||||
|
|
@ -25,12 +26,18 @@ const delegatedProps = computed(() => {
|
|||
});
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
|
||||
const { filterState } = useCommand();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center border-b px-3" cmdk-input-wrapper>
|
||||
<Search class="mr-2 size-4 shrink-0 opacity-50" />
|
||||
<ComboboxInput v-bind="{ ...forwardedProps, ...$attrs }" auto-focus
|
||||
:class="cn('flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50', props.class)" />
|
||||
<Search class="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<ListboxFilter
|
||||
v-bind="{ ...forwardedProps, ...$attrs }"
|
||||
v-model="filterState.search"
|
||||
auto-focus
|
||||
:class="cn('flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50', props.class)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ComboboxItemEmits, ComboboxItemProps } from "radix-vue";
|
||||
import { ComboboxItem, useForwardPropsEmits } from "radix-vue";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import { useCurrentElement } from "@vueuse/core";
|
||||
import type { ListboxItemEmits, ListboxItemProps } from "reka-ui";
|
||||
import { ListboxItem, useForwardPropsEmits, useId } from "reka-ui";
|
||||
import {
|
||||
type HTMLAttributes,
|
||||
computed,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref,
|
||||
} from "vue";
|
||||
import { useCommand, useCommandGroup } from ".";
|
||||
|
||||
const props = defineProps<
|
||||
ComboboxItemProps & { class?: HTMLAttributes["class"] }
|
||||
ListboxItemProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
const emits = defineEmits<ComboboxItemEmits>();
|
||||
const emits = defineEmits<ListboxItemEmits>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
|
@ -16,13 +24,72 @@ const delegatedProps = computed(() => {
|
|||
});
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
|
||||
const id = useId();
|
||||
const { filterState, allItems, allGroups } = useCommand();
|
||||
const groupContext = useCommandGroup();
|
||||
|
||||
const isRender = computed(() => {
|
||||
if (filterState.search) {
|
||||
const filteredCurrentItem = filterState.filtered.items.get(id);
|
||||
// If the filtered items is undefined means not in the all times map yet
|
||||
// Do the first render to add into the map
|
||||
if (filteredCurrentItem === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check with filter
|
||||
return filteredCurrentItem > 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const itemRef = ref();
|
||||
const currentElement = useCurrentElement(itemRef);
|
||||
onMounted(() => {
|
||||
if (!(currentElement.value instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// textValue to perform filter
|
||||
allItems.value.set(
|
||||
id,
|
||||
currentElement.value.textContent ?? props.value?.toString() ?? "",
|
||||
);
|
||||
|
||||
const groupId = groupContext?.id;
|
||||
if (groupId) {
|
||||
if (allGroups.value.has(groupId)) {
|
||||
allGroups.value.get(groupId)?.add(id);
|
||||
} else {
|
||||
allGroups.value.set(groupId, new Set([id]));
|
||||
}
|
||||
}
|
||||
});
|
||||
onUnmounted(() => {
|
||||
allItems.value.delete(id);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxItem
|
||||
<ListboxItem
|
||||
v-if="isRender"
|
||||
v-bind="forwarded"
|
||||
:class="cn('relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', props.class)"
|
||||
:id="id"
|
||||
ref="itemRef"
|
||||
:class="
|
||||
cn(
|
||||
'relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
@select="
|
||||
() => {
|
||||
filterState.search = '';
|
||||
}
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</ComboboxItem>
|
||||
</ListboxItem>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ComboboxContentEmits, ComboboxContentProps } from "radix-vue";
|
||||
import { ComboboxContent, useForwardPropsEmits } from "radix-vue";
|
||||
import type { ListboxContentProps } from "reka-ui";
|
||||
import { ListboxContent, useForwardProps } from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<ComboboxContentProps & { class?: HTMLAttributes["class"] }>(),
|
||||
{
|
||||
dismissable: false,
|
||||
},
|
||||
);
|
||||
const emits = defineEmits<ComboboxContentEmits>();
|
||||
const props = defineProps<
|
||||
ListboxContentProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
|
@ -18,13 +14,13 @@ const delegatedProps = computed(() => {
|
|||
return delegated;
|
||||
});
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
const forwarded = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxContent v-bind="forwarded" :class="cn('max-h-[300px] overflow-y-auto overflow-x-hidden', props.class)">
|
||||
<ListboxContent v-bind="forwarded" :class="cn('max-h-[300px] overflow-y-auto overflow-x-hidden', props.class)">
|
||||
<div role="presentation">
|
||||
<slot />
|
||||
</div>
|
||||
</ComboboxContent>
|
||||
</ListboxContent>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ComboboxSeparatorProps } from "radix-vue";
|
||||
import { ComboboxSeparator } from "radix-vue";
|
||||
import type { SeparatorProps } from "reka-ui";
|
||||
import { Separator } from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
|
||||
const props = defineProps<
|
||||
ComboboxSeparatorProps & { class?: HTMLAttributes["class"] }
|
||||
SeparatorProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
|
|
@ -16,10 +16,10 @@ const delegatedProps = computed(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxSeparator
|
||||
<Separator
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('-mx-1 h-px bg-border', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</ComboboxSeparator>
|
||||
</Separator>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import { createContext } from "reka-ui";
|
||||
import type { Ref } from "vue";
|
||||
|
||||
export { default as Command } from "./Command.vue";
|
||||
export { default as CommandDialog } from "./CommandDialog.vue";
|
||||
export { default as CommandEmpty } from "./CommandEmpty.vue";
|
||||
|
|
@ -7,3 +10,20 @@ export { default as CommandItem } from "./CommandItem.vue";
|
|||
export { default as CommandList } from "./CommandList.vue";
|
||||
export { default as CommandSeparator } from "./CommandSeparator.vue";
|
||||
export { default as CommandShortcut } from "./CommandShortcut.vue";
|
||||
|
||||
export const [useCommand, provideCommandContext] = createContext<{
|
||||
allItems: Ref<Map<string, string>>;
|
||||
allGroups: Ref<Map<string, Set<string>>>;
|
||||
filterState: {
|
||||
search: string;
|
||||
filtered: {
|
||||
count: number;
|
||||
items: Map<string, number>;
|
||||
groups: Set<string>;
|
||||
};
|
||||
};
|
||||
}>("Command");
|
||||
|
||||
export const [useCommandGroup, provideCommandGroupContext] = createContext<{
|
||||
id?: string;
|
||||
}>("CommandGroup");
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {
|
|||
type DialogRootEmits,
|
||||
type DialogRootProps,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
} from "reka-ui";
|
||||
|
||||
const props = defineProps<DialogRootProps>();
|
||||
const emits = defineEmits<DialogRootEmits>();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { DialogClose, type DialogCloseProps } from "radix-vue";
|
||||
import { DialogClose, type DialogCloseProps } from "reka-ui";
|
||||
|
||||
const props = defineProps<DialogCloseProps>();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
DialogOverlay,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
} from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
|
||||
const props = defineProps<
|
||||
|
|
@ -32,16 +32,24 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|||
<template>
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
class="fixed inset-0 z-50 bg-black/80 backdrop-blur data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
|
||||
<DialogContent v-bind="forwarded" :class="cn(
|
||||
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
||||
/>
|
||||
<DialogContent
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
props.class,
|
||||
)">
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<DialogClose v-if="!props.hideClose"
|
||||
class="absolute right-4 top-4 rounded opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X class="size-4" />
|
||||
<DialogClose
|
||||
v-if="!hideClose"
|
||||
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
|
||||
>
|
||||
<X class="w-4 h-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {
|
|||
DialogDescription,
|
||||
type DialogDescriptionProps,
|
||||
useForwardProps,
|
||||
} from "radix-vue";
|
||||
} from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
|
||||
const props = defineProps<
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const props = defineProps<{ class?: HTMLAttributes["class"] }>();
|
|||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2 gap-y-2',
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
DialogOverlay,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
} from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
|
||||
const props = defineProps<
|
||||
|
|
@ -52,7 +52,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|||
<DialogClose
|
||||
class="absolute top-3 right-3 p-0.5 transition-colors rounded-md hover:bg-secondary"
|
||||
>
|
||||
<X class="size-4" />
|
||||
<X class="w-4 h-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import { DialogTitle, type DialogTitleProps, useForwardProps } from "radix-vue";
|
||||
import { DialogTitle, type DialogTitleProps, useForwardProps } from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
|
||||
const props = defineProps<
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { DialogTrigger, type DialogTriggerProps } from "radix-vue";
|
||||
import { DialogTrigger, type DialogTriggerProps } from "reka-ui";
|
||||
|
||||
const props = defineProps<DialogTriggerProps>();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts" setup>
|
||||
import { useForwardPropsEmits } from "radix-vue";
|
||||
import { useForwardPropsEmits } from "reka-ui";
|
||||
import type { DrawerRootEmits, DrawerRootProps } from "vaul-vue";
|
||||
import { DrawerRoot } from "vaul-vue";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { DialogContentEmits, DialogContentProps } from "radix-vue";
|
||||
import { useForwardPropsEmits } from "radix-vue";
|
||||
import type { DialogContentEmits, DialogContentProps } from "reka-ui";
|
||||
import { useForwardPropsEmits } from "reka-ui";
|
||||
import { DrawerContent, DrawerPortal } from "vaul-vue";
|
||||
import type { HtmlHTMLAttributes } from "vue";
|
||||
import DrawerOverlay from "./DrawerOverlay.vue";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts" setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { DialogOverlayProps } from "radix-vue";
|
||||
import type { DialogOverlayProps } from "reka-ui";
|
||||
import { DrawerOverlay } from "vaul-vue";
|
||||
import { type HtmlHTMLAttributes, computed } from "vue";
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {
|
|||
type DropdownMenuRootEmits,
|
||||
type DropdownMenuRootProps,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
} from "reka-ui";
|
||||
|
||||
const props = defineProps<DropdownMenuRootProps>();
|
||||
const emits = defineEmits<DropdownMenuRootEmits>();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
type DropdownMenuCheckboxItemProps,
|
||||
DropdownMenuItemIndicator,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
} from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
|
||||
const props = defineProps<
|
||||
|
|
@ -25,13 +25,16 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuCheckboxItem v-bind="forwarded" :class="cn(
|
||||
<DropdownMenuCheckboxItem
|
||||
v-bind="forwarded"
|
||||
:class=" cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
props.class,
|
||||
)">
|
||||
)"
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuItemIndicator>
|
||||
<Check class="!mr-0" />
|
||||
<Check class="w-4 h-4" />
|
||||
</DropdownMenuItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
type DropdownMenuContentProps,
|
||||
DropdownMenuPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
} from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { DropdownMenuGroup, type DropdownMenuGroupProps } from "radix-vue";
|
||||
import { DropdownMenuGroup, type DropdownMenuGroupProps } from "reka-ui";
|
||||
|
||||
const props = defineProps<DropdownMenuGroupProps>();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {
|
|||
DropdownMenuItem,
|
||||
type DropdownMenuItemProps,
|
||||
useForwardProps,
|
||||
} from "radix-vue";
|
||||
} from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
|
||||
const props = defineProps<
|
||||
|
|
@ -21,11 +21,16 @@ const forwardedProps = useForwardProps(delegatedProps);
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuItem v-bind="forwardedProps" :class="cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm gap-2 px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:mr-0.5 w-full',
|
||||
<DropdownMenuItem
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'relative flex w-full cursor-default select-none items-center rounded-sm gap-2 px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
inset && 'pl-8',
|
||||
props.class,
|
||||
)">
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {
|
|||
DropdownMenuLabel,
|
||||
type DropdownMenuLabelProps,
|
||||
useForwardProps,
|
||||
} from "radix-vue";
|
||||
} from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
|
||||
const props = defineProps<
|
||||
|
|
@ -24,8 +24,10 @@ const forwardedProps = useForwardProps(delegatedProps);
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuLabel v-bind="forwardedProps"
|
||||
:class="cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)">
|
||||
<DropdownMenuLabel
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuLabel>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {
|
|||
type DropdownMenuRadioGroupEmits,
|
||||
type DropdownMenuRadioGroupProps,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
} from "reka-ui";
|
||||
|
||||
const props = defineProps<DropdownMenuRadioGroupProps>();
|
||||
const emits = defineEmits<DropdownMenuRadioGroupEmits>();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
type DropdownMenuRadioItemEmits,
|
||||
type DropdownMenuRadioItemProps,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
} from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
|
||||
const props = defineProps<
|
||||
|
|
@ -26,13 +26,16 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRadioItem v-bind="forwarded" :class="cn(
|
||||
<DropdownMenuRadioItem
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
props.class,
|
||||
)">
|
||||
)"
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuItemIndicator>
|
||||
<Circle class="size-2 fill-current" />
|
||||
<Circle class="h-2 w-2 fill-current" />
|
||||
</DropdownMenuItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { cn } from "@/lib/utils";
|
|||
import {
|
||||
DropdownMenuSeparator,
|
||||
type DropdownMenuSeparatorProps,
|
||||
} from "radix-vue";
|
||||
} from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
|
||||
const props = defineProps<
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {
|
|||
type DropdownMenuSubEmits,
|
||||
type DropdownMenuSubProps,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
} from "reka-ui";
|
||||
|
||||
const props = defineProps<DropdownMenuSubProps>();
|
||||
const emits = defineEmits<DropdownMenuSubEmits>();
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
type DropdownMenuSubContentEmits,
|
||||
type DropdownMenuSubContentProps,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
} from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
|
||||
const props = defineProps<
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
DropdownMenuSubTrigger,
|
||||
type DropdownMenuSubTriggerProps,
|
||||
useForwardProps,
|
||||
} from "radix-vue";
|
||||
} from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
|
||||
const props = defineProps<
|
||||
|
|
@ -30,6 +30,6 @@ const forwardedProps = useForwardProps(delegatedProps);
|
|||
)"
|
||||
>
|
||||
<slot />
|
||||
<ChevronRight class="ml-auto size-4" />
|
||||
<ChevronRight class="ml-auto h-4 w-4" />
|
||||
</DropdownMenuSubTrigger>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {
|
|||
DropdownMenuTrigger,
|
||||
type DropdownMenuTriggerProps,
|
||||
useForwardProps,
|
||||
} from "radix-vue";
|
||||
} from "reka-ui";
|
||||
|
||||
const props = defineProps<DropdownMenuTriggerProps>();
|
||||
|
||||
|
|
|
|||
|
|
@ -13,4 +13,4 @@ export { default as DropdownMenuSub } from "./DropdownMenuSub.vue";
|
|||
export { default as DropdownMenuSubContent } from "./DropdownMenuSubContent.vue";
|
||||
export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue";
|
||||
export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue";
|
||||
export { DropdownMenuPortal } from "radix-vue";
|
||||
export { DropdownMenuPortal } from "reka-ui";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts" setup>
|
||||
import { Slot } from "radix-vue";
|
||||
import { Slot } from "reka-ui";
|
||||
import { useFormField } from "./useFormField";
|
||||
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
|
||||
|
|
|
|||
|
|
@ -1,26 +1,19 @@
|
|||
<script lang="ts" setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Primitive, type PrimitiveProps, useId } from "radix-vue";
|
||||
import { useId } from "reka-ui";
|
||||
import { type HTMLAttributes, provide } from "vue";
|
||||
import { FORM_ITEM_INJECTION_KEY } from "./injectionKeys";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<
|
||||
PrimitiveProps & {
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"];
|
||||
}
|
||||
>(),
|
||||
{
|
||||
as: "div",
|
||||
},
|
||||
);
|
||||
}>();
|
||||
|
||||
const id = useId();
|
||||
provide(FORM_ITEM_INJECTION_KEY, id);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="props.as" :as-child="props.asChild" :class="cn('space-y-2', props.class)">
|
||||
<div :class="cn('space-y-2', props.class)">
|
||||
<slot />
|
||||
</Primitive>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<script lang="ts" setup>
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { LabelProps } from "radix-vue";
|
||||
import type { LabelProps } from "reka-ui";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import { useFormField } from "./useFormField";
|
||||
|
||||
const props = defineProps<LabelProps & { class?: HTMLAttributes["class"] }>();
|
||||
|
|
|
|||
|
|
@ -4,4 +4,4 @@ export { default as FormItem } from "./FormItem.vue";
|
|||
export { default as FormLabel } from "./FormLabel.vue";
|
||||
export { default as FormMessage } from "./FormMessage.vue";
|
||||
export { FORM_ITEM_INJECTION_KEY } from "./injectionKeys";
|
||||
export { Field as FormField, Form } from "vee-validate";
|
||||
export { Form, Field as FormField } from "vee-validate";
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {
|
|||
type HoverCardRootEmits,
|
||||
type HoverCardRootProps,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
} from "reka-ui";
|
||||
|
||||
const props = defineProps<HoverCardRootProps>();
|
||||
const emits = defineEmits<HoverCardRootEmits>();
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
type HoverCardContentProps,
|
||||
HoverCardPortal,
|
||||
useForwardProps,
|
||||
} from "radix-vue";
|
||||
} from "reka-ui";
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue