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 { convert } from "html-to-text";
|
||||||
import ConfirmationModal from "./components/modals/confirm.vue";
|
import ConfirmationModal from "./components/modals/confirm.vue";
|
||||||
import { Toaster } from "./components/ui/sonner";
|
import { Toaster } from "./components/ui/sonner";
|
||||||
|
import { TooltipProvider } from "./components/ui/tooltip";
|
||||||
import { overwriteGetLocale } from "./paraglide/runtime";
|
import { overwriteGetLocale } from "./paraglide/runtime";
|
||||||
import { type EnumSetting, SettingIds } from "./settings";
|
import { type EnumSetting, SettingIds } from "./settings";
|
||||||
import { TooltipProvider } from "./components/ui/tooltip";
|
|
||||||
// Sin
|
// Sin
|
||||||
//import "~/styles/mcdonalds.css";
|
//import "~/styles/mcdonalds.css";
|
||||||
|
|
||||||
|
|
@ -58,7 +58,7 @@ useSeoMeta({
|
||||||
ogImage: computed(() => instance.value?.banner?.url),
|
ogImage: computed(() => instance.value?.banner?.url),
|
||||||
twitterTitle: computed(() => instance.value?.title ?? ""),
|
twitterTitle: computed(() => instance.value?.title ?? ""),
|
||||||
twitterDescription: computed(() =>
|
twitterDescription: computed(() =>
|
||||||
convert(description.value?.content ?? "")
|
convert(description.value?.content ?? ""),
|
||||||
),
|
),
|
||||||
twitterImage: computed(() => instance.value?.banner?.url),
|
twitterImage: computed(() => instance.value?.banner?.url),
|
||||||
description: computed(() => convert(description.value?.content ?? "")),
|
description: computed(() => convert(description.value?.content ?? "")),
|
||||||
|
|
@ -76,7 +76,7 @@ useHead({
|
||||||
|
|
||||||
if (code && origin && appData.value && route.path !== "/oauth/code") {
|
if (code && origin && appData.value && route.path !== "/oauth/code") {
|
||||||
const newOrigin = new URL(
|
const newOrigin = new URL(
|
||||||
URL.canParse(origin) ? origin : `https://${origin}`
|
URL.canParse(origin) ? origin : `https://${origin}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
signInWithCode(code, appData.value, newOrigin);
|
signInWithCode(code, appData.value, newOrigin);
|
||||||
|
|
@ -84,7 +84,7 @@ if (code && origin && appData.value && route.path !== "/oauth/code") {
|
||||||
|
|
||||||
if (origin && !code) {
|
if (origin && !code) {
|
||||||
const newOrigin = new URL(
|
const newOrigin = new URL(
|
||||||
URL.canParse(origin) ? origin : `https://${origin}`
|
URL.canParse(origin) ? origin : `https://${origin}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
signIn(appData, newOrigin);
|
signIn(appData, newOrigin);
|
||||||
|
|
@ -108,4 +108,14 @@ html.theme-changing * {
|
||||||
transition: background-color 1s ease, border 1s ease, color 1s ease,
|
transition: background-color 1s ease, border 1s ease, color 1s ease,
|
||||||
box-shadow 1s ease !important;
|
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>
|
</style>
|
||||||
|
|
|
||||||
12
bun.lock
12
bun.lock
|
|
@ -38,7 +38,7 @@
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"nuxt": "^3.16.1",
|
"nuxt": "^3.16.1",
|
||||||
"nuxt-security": "^2.2.0",
|
"nuxt-security": "^2.2.0",
|
||||||
"radix-vue": "^1.9.17",
|
"reka-ui": "^2.1.1",
|
||||||
"shadcn-nuxt": "1.0.3",
|
"shadcn-nuxt": "1.0.3",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
|
@ -1768,8 +1768,6 @@
|
||||||
|
|
||||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
"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=="],
|
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
|
||||||
|
|
||||||
"randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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",
|
"$schema": "https://shadcn-vue.com/schema.json",
|
||||||
"style": "default",
|
"style": "default",
|
||||||
"typescript": true,
|
"typescript": true,
|
||||||
"tsConfigPath": ".nuxt/tsconfig.json",
|
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "tailwind.config.ts",
|
"config": "tailwind.config.ts",
|
||||||
"css": "styles/index.css",
|
"css": "styles/index.css",
|
||||||
|
|
@ -10,9 +9,11 @@
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
},
|
},
|
||||||
"framework": "nuxt",
|
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "~/components",
|
"components": "@/components",
|
||||||
"utils": "@/lib/utils"
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"composables": "@/composables"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,8 @@
|
||||||
<Toggle
|
<Toggle
|
||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
:pressed="state.contentType === 'text/html'"
|
:model-value="state.contentType === 'text/html'"
|
||||||
@update:pressed="
|
@update:model-value="
|
||||||
(i) =>
|
(i) =>
|
||||||
(state.contentType = i ? 'text/html' : 'text/plain')
|
(state.contentType = i ? 'text/html' : 'text/plain')
|
||||||
"
|
"
|
||||||
|
|
@ -61,6 +61,8 @@
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
:as-child="true"
|
:as-child="true"
|
||||||
:disabled="relation?.type === 'edit'"
|
:disabled="relation?.type === 'edit'"
|
||||||
|
:disable-default-classes="true"
|
||||||
|
:disable-select-icon="true"
|
||||||
>
|
>
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon">
|
||||||
<component
|
<component
|
||||||
|
|
@ -110,11 +112,7 @@
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger as="div">
|
<TooltipTrigger as="div">
|
||||||
<Toggle
|
<Toggle variant="default" size="sm" v-model="state.sensitive">
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
v-model:pressed="state.sensitive"
|
|
||||||
>
|
|
||||||
<TriangleAlert class="!size-5" />
|
<TriangleAlert class="!size-5" />
|
||||||
</Toggle>
|
</Toggle>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|
@ -153,19 +151,23 @@ import {
|
||||||
Smile,
|
Smile,
|
||||||
TriangleAlert,
|
TriangleAlert,
|
||||||
} from "lucide-vue-next";
|
} from "lucide-vue-next";
|
||||||
import { SelectTrigger } from "radix-vue";
|
|
||||||
import { toast } from "vue-sonner";
|
import { toast } from "vue-sonner";
|
||||||
import Note from "~/components/notes/note.vue";
|
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 * as m from "~/paraglide/messages.js";
|
||||||
import { SettingIds } from "~/settings";
|
import { SettingIds } from "~/settings";
|
||||||
import EditorContent from "../editor/content.vue";
|
import EditorContent from "../editor/content.vue";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
|
import { DialogFooter } from "../ui/dialog";
|
||||||
import { Input } from "../ui/input";
|
import { Input } from "../ui/input";
|
||||||
import { Toggle } from "../ui/toggle";
|
import { Toggle } from "../ui/toggle";
|
||||||
import { DialogFooter } from "../ui/dialog";
|
|
||||||
import Files from "./files.vue";
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||||
|
import Files from "./files.vue";
|
||||||
|
|
||||||
const { Control_Enter, Command_Enter } = useMagicKeys();
|
const { Control_Enter, Command_Enter } = useMagicKeys();
|
||||||
const ctrlEnterSend = useSetting(SettingIds.CtrlEnterToSend);
|
const ctrlEnterSend = useSetting(SettingIds.CtrlEnterToSend);
|
||||||
|
|
@ -221,7 +223,7 @@ const state = reactive({
|
||||||
contentType: "text/html" as "text/html" | "text/plain",
|
contentType: "text/html" as "text/html" | "text/plain",
|
||||||
visibility: (relation?.type === "edit"
|
visibility: (relation?.type === "edit"
|
||||||
? relation.note.visibility
|
? relation.note.visibility
|
||||||
: defaultVisibility.value.value ?? "public") as Status["visibility"],
|
: (defaultVisibility.value.value ?? "public")) as Status["visibility"],
|
||||||
files: (relation?.type === "edit"
|
files: (relation?.type === "edit"
|
||||||
? relation.note.media_attachments.map((a) => ({
|
? relation.note.media_attachments.map((a) => ({
|
||||||
apiId: a.id,
|
apiId: a.id,
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ const relation = ref(
|
||||||
type: "reply" | "quote" | "edit";
|
type: "reply" | "quote" | "edit";
|
||||||
note: Status;
|
note: Status;
|
||||||
source?: StatusSource;
|
source?: StatusSource;
|
||||||
} | null
|
} | null,
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger as="button"
|
<DropdownMenuTrigger
|
||||||
|
as="button"
|
||||||
:disabled="file.uploading || file.updating"
|
: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">
|
<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>
|
</Avatar>
|
||||||
<Badge v-if="!file.uploading && !file.updating" class="absolute bottom-1 right-1" variant="default">{{ formatBytes(file.file.size) }}</Badge>
|
<Badge
|
||||||
<Badge v-else class="absolute bottom-1 right-1 rounded px-1 !opacity-100" variant="default"><Loader class="animate-spin size-4" /></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>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent class="min-w-48">
|
<DropdownMenuContent class="min-w-48">
|
||||||
<DropdownMenuLabel>{{ file.file.name }}</DropdownMenuLabel>
|
<DropdownMenuLabel>{{ file.file.name }}</DropdownMenuLabel>
|
||||||
|
|
@ -32,6 +42,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Captions, Delete, Loader, TextCursorInput } from "lucide-vue-next";
|
import { Captions, Delete, Loader, TextCursorInput } from "lucide-vue-next";
|
||||||
|
import Spinner from "~/components/graphics/spinner.vue";
|
||||||
import { confirmModalService } from "~/components/modals/composable.ts";
|
import { confirmModalService } from "~/components/modals/composable.ts";
|
||||||
import { Avatar, AvatarImage } from "~/components/ui/avatar";
|
import { Avatar, AvatarImage } from "~/components/ui/avatar";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
|
|
@ -121,6 +132,8 @@ const formatBytes = (bytes: number) => {
|
||||||
const digitsAfterPoint = 2;
|
const digitsAfterPoint = 2;
|
||||||
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
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>
|
</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>
|
<template>
|
||||||
<div
|
<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">
|
<Button :as="NuxtLink" href="/" variant="ghost" size="icon">
|
||||||
<Home />
|
<Home />
|
||||||
</Button>
|
</Button>
|
||||||
<Button :as="NuxtLink" href="/notifications" variant="ghost" size="icon">
|
<Button
|
||||||
|
:as="NuxtLink"
|
||||||
|
href="/notifications"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
>
|
||||||
<Bell />
|
<Bell />
|
||||||
</Button>
|
</Button>
|
||||||
<AccountSwitcher>
|
<Button variant="ghost" size="icon">
|
||||||
<Button variant="ghost" size="icon">
|
<User />
|
||||||
<User />
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
</AccountSwitcher>
|
variant="default"
|
||||||
<Button variant="default" size="icon" :title="m.salty_aloof_turkey_nudge()"
|
size="icon"
|
||||||
@click="useEvent('composer:open')">
|
:title="m.salty_aloof_turkey_nudge()"
|
||||||
|
@click="useEvent('composer:open')"
|
||||||
|
>
|
||||||
<Pen />
|
<Pen />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -23,6 +31,5 @@
|
||||||
import { Bell, Home, Pen, User } from "lucide-vue-next";
|
import { Bell, Home, Pen, User } from "lucide-vue-next";
|
||||||
import * as m from "~/paraglide/messages.js";
|
import * as m from "~/paraglide/messages.js";
|
||||||
import { NuxtLink } from "#components";
|
import { NuxtLink } from "#components";
|
||||||
import AccountSwitcher from "../sidebars/account-switcher.vue";
|
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,74 @@
|
||||||
<template>
|
<template>
|
||||||
<Card as="article" class="rounded-none border-0 duration-200 shadow- max-w-full relative">
|
<Card as="article" class="relative gap-4 items-stretch">
|
||||||
<CardHeader class="pb-4" as="header">
|
<CardHeader as="header">
|
||||||
<ReblogHeader v-if="note.reblog" :avatar="note.account.avatar" :display-name="note.account.display_name"
|
<ReblogHeader
|
||||||
:url="reblogAccountUrl" :emojis="note.account.emojis" />
|
v-if="note.reblog"
|
||||||
<Header :author="noteToUse.account" :author-url="accountUrl"
|
:avatar="note.account.avatar"
|
||||||
:corner-avatar="note.reblog ? note.account.avatar : undefined" :note-url="url"
|
:display-name="note.account.display_name"
|
||||||
:visibility="noteToUse.visibility" :created-at="new Date(noteToUse.created_at)"
|
:url="reblogAccountUrl"
|
||||||
:small-layout="smallLayout" class="z-[1]" />
|
:emojis="note.account.emojis"
|
||||||
<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>
|
<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>
|
</CardHeader>
|
||||||
<!-- Simply offset by the size of avatar + 0.75rem (the gap) -->
|
<!-- Simply offset by the size of avatar + 0.75rem (the gap) -->
|
||||||
<CardContent :class="contentUnderUsername && (smallLayout ? 'ml-11' : 'ml-[4.25rem]')">
|
<CardContent
|
||||||
<Content :content="noteToUse.content" :quote="note.quote ?? undefined"
|
:class="
|
||||||
:attachments="noteToUse.media_attachments" :plain-content="noteToUse.plain_content ?? undefined"
|
contentUnderUsername && (smallLayout ? 'ml-11' : 'ml-[4.25rem]')
|
||||||
:emojis="noteToUse.emojis" :sensitive="noteToUse.sensitive" :content-warning="noteToUse.spoiler_text" />
|
"
|
||||||
|
>
|
||||||
|
<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>
|
</CardContent>
|
||||||
<CardFooter v-if="!hideActions" class="p-4 pt-0">
|
<CardFooter v-if="!hideActions">
|
||||||
<Actions :reply-count="noteToUse.replies_count" :like-count="noteToUse.favourites_count" :url="url"
|
<Actions
|
||||||
:api-note-string="JSON.stringify(note, null, 4)" :reblog-count="noteToUse.reblogs_count"
|
:reply-count="noteToUse.replies_count"
|
||||||
:remote-url="noteToUse.url" :is-remote="isRemote" :author-id="noteToUse.account.id"
|
:like-count="noteToUse.favourites_count"
|
||||||
@edit="useEvent('composer:edit', note)" @reply="useEvent('composer:reply', note)"
|
:url="url"
|
||||||
@quote="useEvent('composer:quote', note)" @delete="useEvent('note:delete', note)"
|
:api-note-string="JSON.stringify(note, null, 4)"
|
||||||
:note-id="noteToUse.id" :liked="noteToUse.favourited ?? false"
|
:reblog-count="noteToUse.reblogs_count"
|
||||||
:reblogged="noteToUse.reblogged ?? false" />
|
: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>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<Note v-if="parent" :note="parent" :hide-actions="true" :content-under-username="true" :bottom-avatar-bar="true" />
|
<Note
|
||||||
<Note :note="note" :top-avatar-bar="!!parent" />
|
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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,30 @@
|
||||||
<template>
|
<template>
|
||||||
<Card class="rounded-none border-0">
|
<Card class="*:w-full p-2">
|
||||||
<Collapsible :default-open="true" v-slot="{ open }">
|
<Collapsible :default-open="true" v-slot="{ open }">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger :as-child="true">
|
<TooltipTrigger :as-child="true">
|
||||||
<CardHeader v-if="notification.account"
|
<CardHeader
|
||||||
class="flex-row items-center gap-2 px-4 py-2 border-b border-border">
|
v-if="notification.account"
|
||||||
|
class="flex-row items-center gap-2 px-2"
|
||||||
|
>
|
||||||
<component :is="icon" class="size-5 shrink-0" />
|
<component :is="icon" class="size-5 shrink-0" />
|
||||||
<Avatar class="size-6 border border-card" :src="notification.account.avatar" :name="notification.account.display_name" />
|
<Avatar
|
||||||
<span class="font-semibold" v-render-emojis="notification.account.emojis">{{
|
class="size-5 border border-card"
|
||||||
notification.account.display_name
|
:src="notification.account.avatar"
|
||||||
}}</span>
|
: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">
|
<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" />
|
<ChevronDown class="duration-200" />
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
|
|
@ -23,9 +36,19 @@
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<CollapsibleContent :as-child="true">
|
<CollapsibleContent :as-child="true">
|
||||||
<CardContent class="p-0">
|
<CardContent class="p-0">
|
||||||
<Note v-if="notification.status" :note="notification.status" :small-layout="true"
|
<Note
|
||||||
:hide-actions="true" />
|
v-if="notification.status"
|
||||||
<FollowRequest v-else-if="notification.type === 'follow_request' && notification.account" :follower="notification.account" />
|
:note="notification.status"
|
||||||
|
:small-layout="true"
|
||||||
|
:hide-actions="true"
|
||||||
|
/>
|
||||||
|
<FollowRequest
|
||||||
|
v-else-if="
|
||||||
|
notification.type === 'follow_request' &&
|
||||||
|
notification.account
|
||||||
|
"
|
||||||
|
:follower="notification.account"
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,11 @@ const issuerRedirectUrl = (issuerId: string) => {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="grid gap-6">
|
<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">
|
<div class="grid gap-6">
|
||||||
<FormField v-slot="{ componentField }" name="identifier">
|
<FormField v-slot="{ componentField }" name="identifier">
|
||||||
<FormItem>
|
<FormItem>
|
||||||
|
|
@ -90,9 +94,15 @@ const issuerRedirectUrl = (issuerId: string) => {
|
||||||
{{ m.fluffy_soft_wolf_cook() }}
|
{{ m.fluffy_soft_wolf_cook() }}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="petergriffin" type="text" auto-capitalize="none"
|
<Input
|
||||||
auto-complete="idenfifier" auto-correct="off" :disabled="isLoading"
|
placeholder="petergriffin"
|
||||||
v-bind="componentField" />
|
type="text"
|
||||||
|
auto-capitalize="none"
|
||||||
|
auto-complete="idenfifier"
|
||||||
|
auto-correct="off"
|
||||||
|
:disabled="isLoading"
|
||||||
|
v-bind="componentField"
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
@ -103,9 +113,15 @@ const issuerRedirectUrl = (issuerId: string) => {
|
||||||
{{ m.livid_bright_wallaby_quiz() }}
|
{{ m.livid_bright_wallaby_quiz() }}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="hunter2" type="password" auto-capitalize="none"
|
<Input
|
||||||
auto-complete="password" auto-correct="off" :disabled="isLoading"
|
placeholder="hunter2"
|
||||||
v-bind="componentField" />
|
type="password"
|
||||||
|
auto-capitalize="none"
|
||||||
|
auto-complete="password"
|
||||||
|
auto-correct="off"
|
||||||
|
:disabled="isLoading"
|
||||||
|
v-bind="componentField"
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
@ -116,7 +132,10 @@ const issuerRedirectUrl = (issuerId: string) => {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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">
|
<div class="absolute inset-0 flex items-center">
|
||||||
<span class="w-full border-t" />
|
<span class="w-full border-t" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -126,11 +145,25 @@ const issuerRedirectUrl = (issuerId: string) => {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="ssoConfig && ssoConfig.providers.length > 0" class="flex flex-col gap-2">
|
<div
|
||||||
<Button as="a" :href="issuerRedirectUrl(provider.id)" variant="outline" type="button" :disabled="isLoading" v-for="provider of ssoConfig.providers">
|
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" />
|
<Loader v-if="isLoading" class="mr-2 animate-spin" />
|
||||||
<img crossorigin="anonymous" :src="provider.icon" :alt="`${provider.name}'s logo`"
|
<img
|
||||||
class="size-4 mr-2" />
|
crossorigin="anonymous"
|
||||||
|
:src="provider.icon"
|
||||||
|
:alt="`${provider.name}'s logo`"
|
||||||
|
class="size-4 mr-2"
|
||||||
|
/>
|
||||||
{{ provider.name }}
|
{{ provider.name }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
<template>
|
<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">
|
<div class="grid grid-cols-[1fr,auto] items-center gap-4">
|
||||||
<CardHeader class="space-y-0.5 p-0">
|
<CardHeader class="space-y-0.5 p-0">
|
||||||
<CardTitle class="text-base">
|
<CardTitle class="text-base">
|
||||||
|
|
@ -10,16 +14,27 @@
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CollapsibleTrigger :as-child="true">
|
<CollapsibleTrigger :as-child="true">
|
||||||
<Button variant="outline" size="icon" class="ml-auto [&_svg]:data-[state=open]:-rotate-180"
|
<Button
|
||||||
:title="open ? 'Collapse' : 'Expand'">
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
class="ml-auto [&_svg]:data-[state=open]:-rotate-180"
|
||||||
|
:title="open ? 'Collapse' : 'Expand'"
|
||||||
|
>
|
||||||
<ChevronDown class="duration-200" />
|
<ChevronDown class="duration-200" />
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
</div>
|
</div>
|
||||||
<CollapsibleContent :as-child="true">
|
<CollapsibleContent :as-child="true">
|
||||||
<CardFooter class="p-1">
|
<CardFooter class="p-1">
|
||||||
<Textarea :rows="10" :model-value="setting.value"
|
<Textarea
|
||||||
@update:model-value="v => { setting.value = String(v) }" />
|
:rows="10"
|
||||||
|
:model-value="setting.value"
|
||||||
|
@update:model-value="
|
||||||
|
(v) => {
|
||||||
|
setting.value = String(v);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ const editName = async () => {
|
||||||
toast.success(m.gaudy_lime_bison_adore());
|
toast.success(m.gaudy_lime_bison_adore());
|
||||||
|
|
||||||
identity.value.emojis = identity.value.emojis.map((e) =>
|
identity.value.emojis = identity.value.emojis.map((e) =>
|
||||||
e.id === emoji.id ? data : e
|
e.id === emoji.id ? data : e,
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
toast.dismiss(id);
|
toast.dismiss(id);
|
||||||
|
|
@ -134,7 +134,7 @@ const _delete = async () => {
|
||||||
toast.success(m.crisp_whole_canary_tear());
|
toast.success(m.crisp_whole_canary_tear());
|
||||||
|
|
||||||
identity.value.emojis = identity.value.emojis.filter(
|
identity.value.emojis = identity.value.emojis.filter(
|
||||||
(e) => e.id !== emoji.id
|
(e) => e.id !== emoji.id,
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
toast.dismiss(id);
|
toast.dismiss(id);
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,7 @@ const formSchema = toTypedSchema(
|
||||||
count:
|
count:
|
||||||
identity.value?.instance.configuration.emojis
|
identity.value?.instance.configuration.emojis
|
||||||
.emoji_size_limit ?? Number.POSITIVE_INFINITY,
|
.emoji_size_limit ?? Number.POSITIVE_INFINITY,
|
||||||
})
|
}),
|
||||||
),
|
),
|
||||||
shortcode: z
|
shortcode: z
|
||||||
.string()
|
.string()
|
||||||
|
|
@ -240,7 +240,7 @@ const formSchema = toTypedSchema(
|
||||||
identity.value?.instance.configuration.emojis
|
identity.value?.instance.configuration.emojis
|
||||||
.max_emoji_shortcode_characters ??
|
.max_emoji_shortcode_characters ??
|
||||||
Number.POSITIVE_INFINITY,
|
Number.POSITIVE_INFINITY,
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.regex(emojiValidator),
|
.regex(emojiValidator),
|
||||||
global: z.boolean().default(false),
|
global: z.boolean().default(false),
|
||||||
|
|
@ -250,7 +250,7 @@ const formSchema = toTypedSchema(
|
||||||
64,
|
64,
|
||||||
m.home_cool_orangutan_hug({
|
m.home_cool_orangutan_hug({
|
||||||
count: 64,
|
count: 64,
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
alt: z
|
alt: z
|
||||||
|
|
@ -264,10 +264,10 @@ const formSchema = toTypedSchema(
|
||||||
identity.value?.instance.configuration.emojis
|
identity.value?.instance.configuration.emojis
|
||||||
.max_emoji_description_characters ??
|
.max_emoji_description_characters ??
|
||||||
Number.POSITIVE_INFINITY,
|
Number.POSITIVE_INFINITY,
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
const { isSubmitting, handleSubmit, values, setFieldValue } = useForm({
|
const { isSubmitting, handleSubmit, values, setFieldValue } = useForm({
|
||||||
validationSchema: formSchema,
|
validationSchema: formSchema,
|
||||||
|
|
@ -288,7 +288,7 @@ const submit = handleSubmit(async (values) => {
|
||||||
alt: values.alt,
|
alt: values.alt,
|
||||||
category: values.category,
|
category: values.category,
|
||||||
global: values.global,
|
global: values.global,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
toast.dismiss(id);
|
toast.dismiss(id);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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">
|
<form class="p-4 grid gap-6" ref="formRef" @submit="handleSubmit">
|
||||||
<FormField v-slot="{ handleChange, handleBlur }" name="banner">
|
<FormField v-slot="{ handleChange, handleBlur }" name="banner">
|
||||||
<FormItem>
|
<FormItem>
|
||||||
|
|
@ -156,6 +156,7 @@
|
||||||
v-slot="{ componentField, value, handleChange }"
|
v-slot="{ componentField, value, handleChange }"
|
||||||
name="bot"
|
name="bot"
|
||||||
:as="Card"
|
:as="Card"
|
||||||
|
class="block"
|
||||||
>
|
>
|
||||||
<FormItem
|
<FormItem
|
||||||
class="grid grid-cols-[1fr,auto] items-center p-6 gap-2"
|
class="grid grid-cols-[1fr,auto] items-center p-6 gap-2"
|
||||||
|
|
@ -183,6 +184,7 @@
|
||||||
v-slot="{ componentField, value, handleChange }"
|
v-slot="{ componentField, value, handleChange }"
|
||||||
name="locked"
|
name="locked"
|
||||||
:as="Card"
|
:as="Card"
|
||||||
|
class="block"
|
||||||
>
|
>
|
||||||
<FormItem
|
<FormItem
|
||||||
class="grid grid-cols-[1fr,auto] items-center p-6 gap-2"
|
class="grid grid-cols-[1fr,auto] items-center p-6 gap-2"
|
||||||
|
|
@ -210,6 +212,7 @@
|
||||||
v-slot="{ componentField, value, handleChange }"
|
v-slot="{ componentField, value, handleChange }"
|
||||||
name="discoverable"
|
name="discoverable"
|
||||||
:as="Card"
|
:as="Card"
|
||||||
|
class="block"
|
||||||
>
|
>
|
||||||
<FormItem
|
<FormItem
|
||||||
class="grid grid-cols-[1fr,auto] items-center p-6 gap-2"
|
class="grid grid-cols-[1fr,auto] items-center p-6 gap-2"
|
||||||
|
|
@ -234,10 +237,6 @@
|
||||||
</FormField>
|
</FormField>
|
||||||
</form>
|
</form>
|
||||||
</Card>
|
</Card>
|
||||||
<Profile
|
|
||||||
:account="account"
|
|
||||||
class="max-w-lg overflow-auto hidden xl:block"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|
@ -247,7 +246,6 @@ import { Trash } from "lucide-vue-next";
|
||||||
import { useForm } from "vee-validate";
|
import { useForm } from "vee-validate";
|
||||||
import { toast } from "vue-sonner";
|
import { toast } from "vue-sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import Profile from "~/components/profiles/profile.vue";
|
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
|
|
@ -287,7 +285,7 @@ const formSchema = toTypedSchema(
|
||||||
m.civil_icy_ant_mend({
|
m.civil_icy_ant_mend({
|
||||||
size: identity.value?.instance.configuration.accounts
|
size: identity.value?.instance.configuration.accounts
|
||||||
.header_size_limit,
|
.header_size_limit,
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
avatar: z
|
avatar: z
|
||||||
|
|
@ -300,7 +298,7 @@ const formSchema = toTypedSchema(
|
||||||
m.zippy_caring_raven_edit({
|
m.zippy_caring_raven_edit({
|
||||||
size: identity.value?.instance.configuration.accounts
|
size: identity.value?.instance.configuration.accounts
|
||||||
.avatar_size_limit,
|
.avatar_size_limit,
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.or(z.string().url())
|
.or(z.string().url())
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|
@ -308,26 +306,26 @@ const formSchema = toTypedSchema(
|
||||||
.string()
|
.string()
|
||||||
.max(
|
.max(
|
||||||
identity.value.instance.configuration.accounts
|
identity.value.instance.configuration.accounts
|
||||||
.max_displayname_characters
|
.max_displayname_characters,
|
||||||
),
|
),
|
||||||
username: z
|
username: z
|
||||||
.string()
|
.string()
|
||||||
.regex(/^[a-z0-9_-]+$/, m.still_upper_otter_dine())
|
.regex(/^[a-z0-9_-]+$/, m.still_upper_otter_dine())
|
||||||
.max(
|
.max(
|
||||||
identity.value.instance.configuration.accounts
|
identity.value.instance.configuration.accounts
|
||||||
.max_username_characters
|
.max_username_characters,
|
||||||
),
|
),
|
||||||
bio: z
|
bio: z
|
||||||
.string()
|
.string()
|
||||||
.max(
|
.max(
|
||||||
identity.value.instance.configuration.accounts
|
identity.value.instance.configuration.accounts
|
||||||
.max_note_characters
|
.max_note_characters,
|
||||||
),
|
),
|
||||||
bot: z.boolean(),
|
bot: z.boolean(),
|
||||||
locked: z.boolean(),
|
locked: z.boolean(),
|
||||||
discoverable: z.boolean(),
|
discoverable: z.boolean(),
|
||||||
fields: z.array(z.object({ name: z.string(), value: z.string() })),
|
fields: z.array(z.object({ name: z.string(), value: z.string() })),
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const form = useForm({
|
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
|
// 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) =>
|
fields_attributes: values.fields.every((field) =>
|
||||||
account.value.source?.fields?.some(
|
account.value.source?.fields?.some(
|
||||||
(f) => f.name === field.name && f.value === field.value
|
(f) => f.name === field.name && f.value === field.value,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
? undefined
|
? undefined
|
||||||
: values.fields,
|
: values.fields,
|
||||||
|
|
@ -387,8 +385,8 @@ const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
try {
|
try {
|
||||||
const { data } = await client.value.updateCredentials(
|
const { data } = await client.value.updateCredentials(
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
Object.entries(changedData).filter(([, v]) => v !== undefined)
|
Object.entries(changedData).filter(([, v]) => v !== undefined),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
toast.dismiss(id);
|
toast.dismiss(id);
|
||||||
|
|
@ -399,6 +397,12 @@ const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
account.value = data;
|
account.value = data;
|
||||||
|
form.resetForm({
|
||||||
|
values: {
|
||||||
|
...form.values,
|
||||||
|
...values,
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const error = e as ResponseError<{ error: string }>;
|
const error = e as ResponseError<{ error: string }>;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -193,25 +193,25 @@ const schema = toTypedSchema(
|
||||||
(v) => v.size <= (maxSize ?? Number.MAX_SAFE_INTEGER),
|
(v) => v.size <= (maxSize ?? Number.MAX_SAFE_INTEGER),
|
||||||
m.zippy_caring_raven_edit({
|
m.zippy_caring_raven_edit({
|
||||||
size: maxSize ?? Number.MAX_SAFE_INTEGER,
|
size: maxSize ?? Number.MAX_SAFE_INTEGER,
|
||||||
})
|
}),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
.or(
|
.or(
|
||||||
z.object({
|
z.object({
|
||||||
url: z.string().url(),
|
url: z.string().url(),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.or(
|
.or(
|
||||||
z.object({
|
z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const emailToGravatar = async (email: string) => {
|
const emailToGravatar = async (email: string) => {
|
||||||
const sha256 = await crypto.subtle.digest(
|
const sha256 = await crypto.subtle.digest(
|
||||||
"SHA-256",
|
"SHA-256",
|
||||||
new TextEncoder().encode(email)
|
new TextEncoder().encode(email),
|
||||||
);
|
);
|
||||||
|
|
||||||
return `https://www.gravatar.com/avatar/${Array.from(new Uint8Array(sha256))
|
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) {
|
} else if ((values as { email: string }).email) {
|
||||||
emit(
|
emit(
|
||||||
"submitUrl",
|
"submitUrl",
|
||||||
await emailToGravatar((values as { email: string }).email)
|
await emailToGravatar((values as { email: string }).email),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<Avatar :shape="(shape.value as 'circle' | 'square')">
|
<Avatar :shape="(shape.value as 'circle' | 'square')" :size="size">
|
||||||
<AvatarFallback v-if="name">
|
<AvatarFallback v-if="name">
|
||||||
{{ getInitials(name) }}
|
{{ getInitials(name) }}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
|
|
@ -11,9 +11,10 @@
|
||||||
import { SettingIds } from "~/settings";
|
import { SettingIds } from "~/settings";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||||
|
|
||||||
const { name } = defineProps<{
|
const { name, size = "base" } = defineProps<{
|
||||||
src?: string;
|
src?: string;
|
||||||
name?: 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>
|
<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">
|
<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 -->
|
<!-- 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>
|
||||||
<div class="absolute bottom-0 translate-y-1/3 left-4 flex flex-row items-start gap-2">
|
<div
|
||||||
<Avatar size="lg" class="border" :src="avatar" :name="displayName" />
|
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>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,27 @@
|
||||||
<template>
|
<template>
|
||||||
<Card>
|
<Card class="*:w-full">
|
||||||
<ProfileHeader :header="account.header" :avatar="account.avatar" :display-name="account.display_name" />
|
<ProfileHeader
|
||||||
<CardContent class="pt-3 gap-4 flex flex-col">
|
:header="account.header"
|
||||||
|
:avatar="account.avatar"
|
||||||
|
:display-name="account.display_name"
|
||||||
|
/>
|
||||||
|
<CardContent>
|
||||||
<div class="flex flex-row justify-end gap-2">
|
<div class="flex flex-row justify-end gap-2">
|
||||||
<Button variant="secondary" :disabled="isLoading || relationship?.requested" v-if="!isMe && identity"
|
<Button
|
||||||
@click="relationship?.following ? unfollow() : follow()">
|
variant="secondary"
|
||||||
|
:disabled="isLoading || relationship?.requested"
|
||||||
|
v-if="!isMe && identity"
|
||||||
|
@click="relationship?.following ? unfollow() : follow()"
|
||||||
|
>
|
||||||
<Loader v-if="isLoading" class="animate-spin" />
|
<Loader v-if="isLoading" class="animate-spin" />
|
||||||
<span v-else>
|
<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>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
<ProfileActions :account="account">
|
<ProfileActions :account="account">
|
||||||
|
|
@ -22,27 +36,31 @@
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CopyableText :text="account.acct">
|
<CopyableText :text="account.acct">
|
||||||
<span
|
<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 }}
|
@{{ username }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-muted-foreground">{{ instance && "@" }}{{ instance }}</span>
|
<span class="text-muted-foreground"
|
||||||
|
>{{ instance && "@" }}{{ instance }}</span
|
||||||
|
>
|
||||||
</CopyableText>
|
</CopyableText>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row flex-wrap gap-2 -mx-2" v-if="isDeveloper || account.bot || roles.length > 0">
|
<ProfileBadges :account="account" />
|
||||||
<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>
|
|
||||||
<ProfileContent :content="account.note" :emojis="account.emojis" />
|
<ProfileContent :content="account.note" :emojis="account.emojis" />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter class="flex-col items-start gap-4">
|
<CardFooter>
|
||||||
<ProfileStats :creation-date="new Date(account.created_at || 0)" :follower-count="account.followers_count"
|
<ProfileStats
|
||||||
:following-count="account.following_count" :note-count="account.statuses_count" />
|
: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" />
|
<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>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -59,7 +77,7 @@ import * as m from "~/paraglide/messages.js";
|
||||||
import { SettingIds } from "~/settings";
|
import { SettingIds } from "~/settings";
|
||||||
import { confirmModalService } from "../modals/composable";
|
import { confirmModalService } from "../modals/composable";
|
||||||
import ProfileActions from "./profile-actions.vue";
|
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 ProfileContent from "./profile-content.vue";
|
||||||
import ProfileFields from "./profile-fields.vue";
|
import ProfileFields from "./profile-fields.vue";
|
||||||
import ProfileHeader from "./profile-header.vue";
|
import ProfileHeader from "./profile-header.vue";
|
||||||
|
|
@ -69,16 +87,9 @@ const { account } = defineProps<{
|
||||||
account: Account;
|
account: Account;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const config = useConfig();
|
|
||||||
const { relationship, isLoading } = useRelationship(client, account.id);
|
const { relationship, isLoading } = useRelationship(client, account.id);
|
||||||
const isMe = identity.value?.account.id === account.id;
|
const isMe = identity.value?.account.id === account.id;
|
||||||
const [username, instance] = account.acct.split("@");
|
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);
|
const confirmFollows = useSetting(SettingIds.ConfirmFollow);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,25 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="bg-muted rounded overflow-hidden h-32 w-full">
|
<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 -->
|
<!-- 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>
|
||||||
<div class="absolute bottom-0 left-1/2 translate-y-1/3 -translate-x-1/2 flex flex-row items-start gap-2">
|
<div
|
||||||
<Avatar size="base" class="border" :src="account.avatar" :name="account.display_name" />
|
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>
|
</div>
|
||||||
<div class="flex flex-col justify-center items-center mt-8">
|
<div class="flex flex-col justify-center items-center mt-8">
|
||||||
|
|
@ -15,19 +28,32 @@
|
||||||
</span>
|
</span>
|
||||||
<CopyableText :text="account.acct" class="text-sm">
|
<CopyableText :text="account.acct" class="text-sm">
|
||||||
<span
|
<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 }}
|
@{{ username }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-muted-foreground">{{ instance && "@" }}{{ instance }}</span>
|
<span class="text-muted-foreground"
|
||||||
|
>{{ instance && "@" }}{{ instance }}</span
|
||||||
|
>
|
||||||
</CopyableText>
|
</CopyableText>
|
||||||
</div>
|
</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" />
|
<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>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Account } from "@versia/client/types";
|
import type { Account } from "@versia/client/types";
|
||||||
|
import { Separator } from "~/components/ui/separator";
|
||||||
import CopyableText from "../notes/copyable-text.vue";
|
import CopyableText from "../notes/copyable-text.vue";
|
||||||
import Avatar from "./avatar.vue";
|
import Avatar from "./avatar.vue";
|
||||||
import ProfileContent from "./profile-content.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>
|
<template>
|
||||||
<Sidebar collapsible="none" class="hidden md:flex">
|
<Sidebar collapsible="offcanvas">
|
||||||
<SidebarHeader>
|
<InstanceHeader />
|
||||||
<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>
|
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>{{ m.trite_real_sawfish_drum() }}</SidebarGroupLabel>
|
<SidebarGroupLabel>{{
|
||||||
<SidebarMenu>
|
m.trite_real_sawfish_drum()
|
||||||
<SidebarMenuItem v-for="item in data.other.filter(
|
}}</SidebarGroupLabel>
|
||||||
i => i.requiresLogin ? !!identity : true,
|
<NavItems
|
||||||
)" :key="item.name">
|
:items="
|
||||||
<SidebarMenuButton as-child>
|
sidebarConfig.other.filter((i) =>
|
||||||
<NuxtLink :href="item.url">
|
i.requiresLogin ? !!identity : true
|
||||||
<component :is="item.icon" />
|
)
|
||||||
<span>{{ item.name }}</span>
|
"
|
||||||
</NuxtLink>
|
/>
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
<SidebarGroup v-if="identity" class="mt-auto">
|
<SidebarGroup v-if="identity" class="mt-auto">
|
||||||
<SidebarGroupLabel>{{ m.close_short_kitten_coax() }}</SidebarGroupLabel>
|
<SidebarGroupLabel>{{
|
||||||
<SidebarMenu>
|
m.close_short_kitten_coax()
|
||||||
<Collapsible v-for="item in data.navMain" :key="item.title" as-child class="group/collapsible">
|
}}</SidebarGroupLabel>
|
||||||
<SidebarMenuItem>
|
<NavGroup :items="sidebarConfig.navMain" />
|
||||||
<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>
|
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<FooterActions />
|
||||||
<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>
|
|
||||||
<SidebarRail />
|
<SidebarRail />
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import { sidebarConfig } from "~/components/sidebars/sidebar";
|
||||||
BedSingle,
|
|
||||||
Bell,
|
|
||||||
ChevronRight,
|
|
||||||
ChevronsUpDown,
|
|
||||||
DownloadCloud,
|
|
||||||
Globe,
|
|
||||||
House,
|
|
||||||
MapIcon,
|
|
||||||
Pen,
|
|
||||||
Settings2,
|
|
||||||
} from "lucide-vue-next";
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "~/components/ui/collapsible";
|
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarFooter,
|
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupLabel,
|
SidebarGroupLabel,
|
||||||
SidebarHeader,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
SidebarMenuSub,
|
|
||||||
SidebarMenuSubButton,
|
|
||||||
SidebarMenuSubItem,
|
|
||||||
SidebarRail,
|
SidebarRail,
|
||||||
} from "~/components/ui/sidebar";
|
} from "~/components/ui/sidebar";
|
||||||
import * as m from "~/paraglide/messages.js";
|
import * as m from "~/paraglide/messages.js";
|
||||||
import Avatar from "../profiles/avatar.vue";
|
import FooterActions from "./footer/footer-actions.vue";
|
||||||
import { Button } from "../ui/button";
|
import InstanceHeader from "./instance/instance-header.vue";
|
||||||
import AccountSwitcher from "./account-switcher.vue";
|
import NavGroup from "./navigation/nav-group.vue";
|
||||||
|
import NavItems from "./navigation/nav-items.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();
|
|
||||||
</script>
|
</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>
|
<template>
|
||||||
<Sidebar side="right" collapsible="none" class="w-96 hidden lg:flex">
|
<Sidebar
|
||||||
<SidebarContent class="p-2 overflow-y-auto">
|
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 />
|
<NotificationsTimeline />
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarRail />
|
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script setup lang="ts">
|
||||||
import NotificationsTimeline from "../timelines/notifications.vue";
|
import NotificationsTimeline from "~/components/timelines/notifications.vue";
|
||||||
import { Sidebar, SidebarContent, SidebarRail } from "../ui/sidebar";
|
import { Sidebar, SidebarContent } from "~/components/ui/sidebar";
|
||||||
</script>
|
</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">
|
<script setup lang="ts">
|
||||||
import { cn } from "@/lib/utils";
|
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 { SettingIds } from "~/settings";
|
||||||
import Timelines from "../navigation/timelines.vue";
|
|
||||||
import LeftSidebar from "./left-sidebar.vue";
|
import LeftSidebar from "./left-sidebar.vue";
|
||||||
import RightSidebar from "./right-sidebar.vue";
|
import RightSidebar from "./right-sidebar.vue";
|
||||||
|
|
||||||
|
|
@ -18,14 +18,15 @@ const showTimelines = computed(
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SidebarProvider>
|
<LeftSidebar />
|
||||||
<LeftSidebar />
|
<main class="grow h-dvh overflow-y-auto">
|
||||||
<SidebarInset :class="cn('relative overflow-y-auto overflow-x-hidden', !isMd && 'pt-4')">
|
<header
|
||||||
<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">
|
v-if="showTimelines"
|
||||||
<Timelines />
|
class="flex h-16 items-center bg-background/80 backdrop-blur-2xl sticky top-0 inset-x-0 z-10 p-4"
|
||||||
</header>
|
>
|
||||||
<slot />
|
<Timelines />
|
||||||
</SidebarInset>
|
</header>
|
||||||
<RightSidebar v-if="identity" v-show="showRightSidebar.value" />
|
<slot />
|
||||||
</SidebarProvider>
|
</main>
|
||||||
|
<RightSidebar v-if="identity" v-show="showRightSidebar.value" />
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,59 +1,52 @@
|
||||||
<!-- Timeline.vue -->
|
|
||||||
<template>
|
<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">
|
<div
|
||||||
<TimelineItem :type="type" v-for="item in items" :key="item.id" :item="item" @update="updateItem"
|
role="status"
|
||||||
@delete="removeItem" />
|
class="flex flex-col gap-4 items-center *:max-w-2xl *:w-full p-4"
|
||||||
</TransitionGroup>
|
>
|
||||||
|
<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">
|
<Spinner v-if="isLoading" />
|
||||||
<Loader class="size-8 animate-spin" />
|
|
||||||
|
<div v-if="error" class="timeline-error">
|
||||||
|
{{ error.message }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- If there are some posts, but the user scrolled to the end -->
|
||||||
|
<ReachedEnd v-if="hasReachedEnd && items.length > 0" />
|
||||||
|
|
||||||
|
<!-- If there are no posts at all -->
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{{ m.gaudy_bland_gorilla_talk() }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else ref="loadMoreTrigger" class="h-20"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- 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>
|
|
||||||
|
|
||||||
<div v-else-if="!infiniteScroll.value" class="py-10 px-4">
|
|
||||||
<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>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Notification, Status } from "@versia/client/types";
|
import type { Notification, Status } from "@versia/client/types";
|
||||||
import { useIntersectionObserver } from "@vueuse/core";
|
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 * as m from "~/paraglide/messages.js";
|
||||||
import { SettingIds } from "~/settings";
|
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 { Button } from "../ui/button";
|
||||||
import TimelineItem from "./timeline-item.vue";
|
import TimelineItem from "./timeline-item.vue";
|
||||||
|
|
||||||
|
|
@ -92,16 +85,3 @@ onMounted(() => {
|
||||||
props.loadNext();
|
props.loadNext();
|
||||||
});
|
});
|
||||||
</script>
|
</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,
|
type AlertDialogProps,
|
||||||
AlertDialogRoot,
|
AlertDialogRoot,
|
||||||
useForwardPropsEmits,
|
useForwardPropsEmits,
|
||||||
} from "radix-vue";
|
} from "reka-ui";
|
||||||
|
|
||||||
const props = defineProps<AlertDialogProps>();
|
const props = defineProps<AlertDialogProps>();
|
||||||
const emits = defineEmits<AlertDialogEmits>();
|
const emits = defineEmits<AlertDialogEmits>();
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
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 { type HTMLAttributes, computed } from "vue";
|
||||||
import { buttonVariants } from "~/components/ui/button";
|
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
AlertDialogActionProps & { class?: HTMLAttributes["class"] }
|
AlertDialogActionProps & { class?: HTMLAttributes["class"] }
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
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 { type HTMLAttributes, computed } from "vue";
|
||||||
import { buttonVariants } from "~/components/ui/button";
|
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
AlertDialogCancelProps & { class?: HTMLAttributes["class"] }
|
AlertDialogCancelProps & { class?: HTMLAttributes["class"] }
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
AlertDialogOverlay,
|
AlertDialogOverlay,
|
||||||
AlertDialogPortal,
|
AlertDialogPortal,
|
||||||
useForwardPropsEmits,
|
useForwardPropsEmits,
|
||||||
} from "radix-vue";
|
} from "reka-ui";
|
||||||
import { type HTMLAttributes, computed } from "vue";
|
import { type HTMLAttributes, computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
|
|
@ -27,13 +27,13 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||||
<template>
|
<template>
|
||||||
<AlertDialogPortal>
|
<AlertDialogPortal>
|
||||||
<AlertDialogOverlay
|
<AlertDialogOverlay
|
||||||
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"
|
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"
|
||||||
/>
|
/>
|
||||||
<AlertDialogContent
|
<AlertDialogContent
|
||||||
v-bind="forwarded"
|
v-bind="forwarded"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
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,
|
props.class,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
type AlertDialogDescriptionProps,
|
type AlertDialogDescriptionProps,
|
||||||
} from "radix-vue";
|
} from "reka-ui";
|
||||||
import { type HTMLAttributes, computed } from "vue";
|
import { type HTMLAttributes, computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cn } from "@/lib/utils";
|
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";
|
import { type HTMLAttributes, computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { AlertDialogTrigger, type AlertDialogTriggerProps } from "radix-vue";
|
import { AlertDialogTrigger, type AlertDialogTriggerProps } from "reka-ui";
|
||||||
|
|
||||||
const props = defineProps<AlertDialogTriggerProps>();
|
const props = defineProps<AlertDialogTriggerProps>();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ export { default as AlertDescription } from "./AlertDescription.vue";
|
||||||
export { default as AlertTitle } from "./AlertTitle.vue";
|
export { default as AlertTitle } from "./AlertTitle.vue";
|
||||||
|
|
||||||
export const alertVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { AvatarRoot } from "radix-vue";
|
import { AvatarRoot } from "reka-ui";
|
||||||
import type { HTMLAttributes } from "vue";
|
import type { HTMLAttributes } from "vue";
|
||||||
import { type AvatarVariants, avatarVariant } from ".";
|
import { type AvatarVariants, avatarVariant } from ".";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { AvatarFallback, type AvatarFallbackProps } from "radix-vue";
|
import { AvatarFallback, type AvatarFallbackProps } from "reka-ui";
|
||||||
|
|
||||||
const props = defineProps<AvatarFallbackProps>();
|
const props = defineProps<AvatarFallbackProps>();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
<script setup lang="ts">
|
<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>();
|
const props = defineProps<AvatarImageProps>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export const avatarVariant = cva(
|
||||||
},
|
},
|
||||||
shape: {
|
shape: {
|
||||||
circle: "rounded-full",
|
circle: "rounded-full",
|
||||||
square: "rounded",
|
square: "rounded-md",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cn } from "@/lib/utils";
|
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 { HTMLAttributes } from "vue";
|
||||||
import { type ButtonVariants, buttonVariants } from ".";
|
import { type ButtonVariants, buttonVariants } from ".";
|
||||||
|
|
||||||
|
|
@ -20,7 +20,6 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
:as="as"
|
:as="as"
|
||||||
:as-child="asChild"
|
:as-child="asChild"
|
||||||
:class="cn(buttonVariants({ variant, size }), props.class)"
|
:class="cn(buttonVariants({ variant, size }), props.class)"
|
||||||
data-component="button"
|
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</Primitive>
|
</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 { default as Button } from "./Button.vue";
|
||||||
|
|
||||||
export const buttonVariants = cva(
|
export const buttonVariants = cva(
|
||||||
|
|
@ -7,21 +9,21 @@ export const buttonVariants = cva(
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
"bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
outline:
|
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:
|
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",
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-4 py-2",
|
default: "h-10 px-4 py-2",
|
||||||
sm: "h-8 px-3 text-xs",
|
sm: "h-9 rounded-md px-3",
|
||||||
lg: "h-10 px-8",
|
lg: "h-11 rounded-md px-8",
|
||||||
icon: "size-9",
|
icon: "h-10 w-10",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,21 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Primitive, type PrimitiveProps } from "radix-vue";
|
|
||||||
import type { HTMLAttributes } from "vue";
|
import type { HTMLAttributes } from "vue";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = defineProps<{
|
||||||
defineProps<PrimitiveProps & { class?: HTMLAttributes["class"] }>(),
|
class?: HTMLAttributes["class"];
|
||||||
{
|
}>();
|
||||||
as: "div",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Primitive :as="props.as" :as-child="props.asChild" :class="cn(
|
<div
|
||||||
'rounded-lg border bg-card/90 backdrop-blur-xl text-card-foreground shadow-sm',
|
:class="
|
||||||
props.class,
|
cn(
|
||||||
)
|
'rounded-lg border bg-card text-card-foreground shadow-sm flex flex-col gap-6 p-4 items-center justify-center',
|
||||||
" data-component="card">
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</Primitive>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const props = defineProps<{
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="cn('p-6 pt-0', props.class)">
|
<div :class="cn('flex flex-col gap-2', props.class)">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const props = defineProps<{
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="cn('flex items-center p-6 pt-0', props.class)">
|
<div :class="cn('flex items-center', props.class)">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,14 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Primitive, type PrimitiveProps } from "radix-vue";
|
|
||||||
import type { HTMLAttributes } from "vue";
|
import type { HTMLAttributes } from "vue";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = defineProps<{
|
||||||
defineProps<PrimitiveProps & { class?: HTMLAttributes["class"] }>(),
|
class?: HTMLAttributes["class"];
|
||||||
{
|
}>();
|
||||||
as: "div",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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 />
|
<slot />
|
||||||
</Primitive>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Check } from "lucide-vue-next";
|
import { Check } from "lucide-vue-next";
|
||||||
import type { CheckboxRootEmits, CheckboxRootProps } from "radix-vue";
|
import type { CheckboxRootEmits, CheckboxRootProps } from "reka-ui";
|
||||||
import {
|
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from "reka-ui";
|
||||||
CheckboxIndicator,
|
|
||||||
CheckboxRoot,
|
|
||||||
useForwardPropsEmits,
|
|
||||||
} from "radix-vue";
|
|
||||||
import { type HTMLAttributes, computed } from "vue";
|
import { type HTMLAttributes, computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
|
|
@ -24,12 +20,16 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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',
|
<CheckboxRoot
|
||||||
props.class)">
|
v-bind="forwarded"
|
||||||
<CheckboxIndicator class="flex h-full w-full items-center justify-center text-current">
|
:class="
|
||||||
<slot>
|
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',
|
||||||
<Check class="size-4" />
|
props.class)"
|
||||||
</slot>
|
>
|
||||||
</CheckboxIndicator>
|
<CheckboxIndicator class="flex h-full w-full items-center justify-center text-current">
|
||||||
</CheckboxRoot>
|
<slot>
|
||||||
|
<Check class="h-4 w-4" />
|
||||||
|
</slot>
|
||||||
|
</CheckboxIndicator>
|
||||||
|
</CheckboxRoot>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { CollapsibleRootEmits, CollapsibleRootProps } from "radix-vue";
|
import type { CollapsibleRootEmits, CollapsibleRootProps } from "reka-ui";
|
||||||
import { CollapsibleRoot, useForwardPropsEmits } from "radix-vue";
|
import { CollapsibleRoot, useForwardPropsEmits } from "reka-ui";
|
||||||
|
|
||||||
const props = defineProps<CollapsibleRootProps>();
|
const props = defineProps<CollapsibleRootProps>();
|
||||||
const emits = defineEmits<CollapsibleRootEmits>();
|
const emits = defineEmits<CollapsibleRootEmits>();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CollapsibleContent, type CollapsibleContentProps } from "radix-vue";
|
import { CollapsibleContent, type CollapsibleContentProps } from "reka-ui";
|
||||||
|
|
||||||
const props = defineProps<CollapsibleContentProps>();
|
const props = defineProps<CollapsibleContentProps>();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CollapsibleTrigger, type CollapsibleTriggerProps } from "radix-vue";
|
import { CollapsibleTrigger, type CollapsibleTriggerProps } from "reka-ui";
|
||||||
|
|
||||||
const props = defineProps<CollapsibleTriggerProps>();
|
const props = defineProps<CollapsibleTriggerProps>();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { ComboboxRootEmits, ComboboxRootProps } from "radix-vue";
|
import type { ListboxRootEmits, ListboxRootProps } from "reka-ui";
|
||||||
import { ComboboxRoot, useForwardPropsEmits } from "radix-vue";
|
import { ListboxRoot, useFilter, useForwardPropsEmits } from "reka-ui";
|
||||||
import { type HTMLAttributes, computed } from "vue";
|
import { type HTMLAttributes, computed, reactive, ref, watch } from "vue";
|
||||||
|
import { provideCommandContext } from ".";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<ComboboxRootProps & { class?: HTMLAttributes["class"] }>(),
|
defineProps<ListboxRootProps & { class?: HTMLAttributes["class"] }>(),
|
||||||
{
|
{
|
||||||
open: true,
|
|
||||||
modelValue: "",
|
modelValue: "",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const emits = defineEmits<ComboboxRootEmits>();
|
const emits = defineEmits<ListboxRootEmits>();
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
const delegatedProps = computed(() => {
|
||||||
const { class: _, ...delegated } = props;
|
const { class: _, ...delegated } = props;
|
||||||
|
|
@ -21,11 +21,84 @@ const delegatedProps = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ComboboxRoot v-bind="forwarded"
|
<ListboxRoot
|
||||||
:class="cn('flex h-full w-full flex-col overflow-hidden rounded bg-popover text-popover-foreground', props.class)">
|
v-bind="forwarded"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</ComboboxRoot>
|
</ListboxRoot>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { DialogRootEmits, DialogRootProps } from "radix-vue";
|
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||||
import { useForwardPropsEmits } from "radix-vue";
|
import type { DialogRootEmits, DialogRootProps } from "reka-ui";
|
||||||
import { Dialog, DialogContent } from "~/components/ui/dialog";
|
import { useForwardPropsEmits } from "reka-ui";
|
||||||
import Command from "./Command.vue";
|
import Command from "./Command.vue";
|
||||||
|
|
||||||
const props = defineProps<DialogRootProps>();
|
const props = defineProps<DialogRootProps>();
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { ComboboxEmptyProps } from "radix-vue";
|
import type { PrimitiveProps } from "reka-ui";
|
||||||
import { ComboboxEmpty } from "radix-vue";
|
import { Primitive } from "reka-ui";
|
||||||
import { type HTMLAttributes, computed } from "vue";
|
import { type HTMLAttributes, computed } from "vue";
|
||||||
|
import { useCommand } from ".";
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
ComboboxEmptyProps & { class?: HTMLAttributes["class"] }
|
PrimitiveProps & { class?: HTMLAttributes["class"] }
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
const delegatedProps = computed(() => {
|
||||||
|
|
@ -13,10 +14,15 @@ const delegatedProps = computed(() => {
|
||||||
|
|
||||||
return delegated;
|
return delegated;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { filterState } = useCommand();
|
||||||
|
const isRender = computed(
|
||||||
|
() => !!filterState.search && filterState.filtered.count === 0,
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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 />
|
<slot />
|
||||||
</ComboboxEmpty>
|
</Primitive>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { ComboboxGroupProps } from "radix-vue";
|
import type { ListboxGroupProps } from "reka-ui";
|
||||||
import { ComboboxGroup, ComboboxLabel } from "radix-vue";
|
import { ListboxGroup, ListboxGroupLabel, useId } from "reka-ui";
|
||||||
import { type HTMLAttributes, computed } from "vue";
|
import { type HTMLAttributes, computed, onMounted, onUnmounted } from "vue";
|
||||||
|
import { provideCommandGroupContext, useCommand } from ".";
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
ComboboxGroupProps & {
|
ListboxGroupProps & {
|
||||||
class?: HTMLAttributes["class"];
|
class?: HTMLAttributes["class"];
|
||||||
heading?: string;
|
heading?: string;
|
||||||
}
|
}
|
||||||
|
|
@ -16,16 +17,43 @@ const delegatedProps = computed(() => {
|
||||||
|
|
||||||
return delegated;
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ComboboxGroup
|
<ListboxGroup
|
||||||
v-bind="delegatedProps"
|
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="
|
||||||
<ComboboxLabel v-if="heading" class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
|
cn(
|
||||||
{{ heading }}
|
'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',
|
||||||
</ComboboxLabel>
|
props.class
|
||||||
<slot />
|
)
|
||||||
</ComboboxGroup>
|
"
|
||||||
|
:hidden="isRender ? undefined : true"
|
||||||
|
>
|
||||||
|
<ListboxGroupLabel
|
||||||
|
v-if="heading"
|
||||||
|
class="px-2 py-1.5 text-xs font-medium text-muted-foreground"
|
||||||
|
>
|
||||||
|
{{ heading }}
|
||||||
|
</ListboxGroupLabel>
|
||||||
|
<slot />
|
||||||
|
</ListboxGroup>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,19 @@
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Search } from "lucide-vue-next";
|
import { Search } from "lucide-vue-next";
|
||||||
import {
|
import {
|
||||||
ComboboxInput,
|
ListboxFilter,
|
||||||
type ComboboxInputProps,
|
type ListboxFilterProps,
|
||||||
useForwardProps,
|
useForwardProps,
|
||||||
} from "radix-vue";
|
} from "reka-ui";
|
||||||
import { type HTMLAttributes, computed } from "vue";
|
import { type HTMLAttributes, computed } from "vue";
|
||||||
|
import { useCommand } from ".";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
ComboboxInputProps & {
|
ListboxFilterProps & {
|
||||||
class?: HTMLAttributes["class"];
|
class?: HTMLAttributes["class"];
|
||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
|
|
@ -25,12 +26,18 @@ const delegatedProps = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const forwardedProps = useForwardProps(delegatedProps);
|
const forwardedProps = useForwardProps(delegatedProps);
|
||||||
|
|
||||||
|
const { filterState } = useCommand();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center border-b px-3" cmdk-input-wrapper>
|
<div class="flex items-center border-b px-3" cmdk-input-wrapper>
|
||||||
<Search class="mr-2 size-4 shrink-0 opacity-50" />
|
<Search class="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
<ComboboxInput v-bind="{ ...forwardedProps, ...$attrs }" auto-focus
|
<ListboxFilter
|
||||||
: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)" />
|
v-bind="{ ...forwardedProps, ...$attrs }"
|
||||||
</div>
|
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>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,21 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { ComboboxItemEmits, ComboboxItemProps } from "radix-vue";
|
import { useCurrentElement } from "@vueuse/core";
|
||||||
import { ComboboxItem, useForwardPropsEmits } from "radix-vue";
|
import type { ListboxItemEmits, ListboxItemProps } from "reka-ui";
|
||||||
import { type HTMLAttributes, computed } from "vue";
|
import { ListboxItem, useForwardPropsEmits, useId } from "reka-ui";
|
||||||
|
import {
|
||||||
|
type HTMLAttributes,
|
||||||
|
computed,
|
||||||
|
onMounted,
|
||||||
|
onUnmounted,
|
||||||
|
ref,
|
||||||
|
} from "vue";
|
||||||
|
import { useCommand, useCommandGroup } from ".";
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
ComboboxItemProps & { class?: HTMLAttributes["class"] }
|
ListboxItemProps & { class?: HTMLAttributes["class"] }
|
||||||
>();
|
>();
|
||||||
const emits = defineEmits<ComboboxItemEmits>();
|
const emits = defineEmits<ListboxItemEmits>();
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
const delegatedProps = computed(() => {
|
||||||
const { class: _, ...delegated } = props;
|
const { class: _, ...delegated } = props;
|
||||||
|
|
@ -16,13 +24,72 @@ const delegatedProps = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ComboboxItem
|
<ListboxItem
|
||||||
v-bind="forwarded"
|
v-if="isRender"
|
||||||
: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)"
|
v-bind="forwarded"
|
||||||
>
|
:id="id"
|
||||||
<slot />
|
ref="itemRef"
|
||||||
</ComboboxItem>
|
: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 />
|
||||||
|
</ListboxItem>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { ComboboxContentEmits, ComboboxContentProps } from "radix-vue";
|
import type { ListboxContentProps } from "reka-ui";
|
||||||
import { ComboboxContent, useForwardPropsEmits } from "radix-vue";
|
import { ListboxContent, useForwardProps } from "reka-ui";
|
||||||
import { type HTMLAttributes, computed } from "vue";
|
import { type HTMLAttributes, computed } from "vue";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = defineProps<
|
||||||
defineProps<ComboboxContentProps & { class?: HTMLAttributes["class"] }>(),
|
ListboxContentProps & { class?: HTMLAttributes["class"] }
|
||||||
{
|
>();
|
||||||
dismissable: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const emits = defineEmits<ComboboxContentEmits>();
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
const delegatedProps = computed(() => {
|
||||||
const { class: _, ...delegated } = props;
|
const { class: _, ...delegated } = props;
|
||||||
|
|
@ -18,13 +14,13 @@ const delegatedProps = computed(() => {
|
||||||
return delegated;
|
return delegated;
|
||||||
});
|
});
|
||||||
|
|
||||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
const forwarded = useForwardProps(delegatedProps);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<div role="presentation">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</ComboboxContent>
|
</ListboxContent>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { ComboboxSeparatorProps } from "radix-vue";
|
import type { SeparatorProps } from "reka-ui";
|
||||||
import { ComboboxSeparator } from "radix-vue";
|
import { Separator } from "reka-ui";
|
||||||
import { type HTMLAttributes, computed } from "vue";
|
import { type HTMLAttributes, computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
ComboboxSeparatorProps & { class?: HTMLAttributes["class"] }
|
SeparatorProps & { class?: HTMLAttributes["class"] }
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
const delegatedProps = computed(() => {
|
||||||
|
|
@ -16,10 +16,10 @@ const delegatedProps = computed(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ComboboxSeparator
|
<Separator
|
||||||
v-bind="delegatedProps"
|
v-bind="delegatedProps"
|
||||||
:class="cn('-mx-1 h-px bg-border', props.class)"
|
:class="cn('-mx-1 h-px bg-border', props.class)"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</ComboboxSeparator>
|
</Separator>
|
||||||
</template>
|
</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 Command } from "./Command.vue";
|
||||||
export { default as CommandDialog } from "./CommandDialog.vue";
|
export { default as CommandDialog } from "./CommandDialog.vue";
|
||||||
export { default as CommandEmpty } from "./CommandEmpty.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 CommandList } from "./CommandList.vue";
|
||||||
export { default as CommandSeparator } from "./CommandSeparator.vue";
|
export { default as CommandSeparator } from "./CommandSeparator.vue";
|
||||||
export { default as CommandShortcut } from "./CommandShortcut.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 DialogRootEmits,
|
||||||
type DialogRootProps,
|
type DialogRootProps,
|
||||||
useForwardPropsEmits,
|
useForwardPropsEmits,
|
||||||
} from "radix-vue";
|
} from "reka-ui";
|
||||||
|
|
||||||
const props = defineProps<DialogRootProps>();
|
const props = defineProps<DialogRootProps>();
|
||||||
const emits = defineEmits<DialogRootEmits>();
|
const emits = defineEmits<DialogRootEmits>();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DialogClose, type DialogCloseProps } from "radix-vue";
|
import { DialogClose, type DialogCloseProps } from "reka-ui";
|
||||||
|
|
||||||
const props = defineProps<DialogCloseProps>();
|
const props = defineProps<DialogCloseProps>();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
DialogOverlay,
|
DialogOverlay,
|
||||||
DialogPortal,
|
DialogPortal,
|
||||||
useForwardPropsEmits,
|
useForwardPropsEmits,
|
||||||
} from "radix-vue";
|
} from "reka-ui";
|
||||||
import { type HTMLAttributes, computed } from "vue";
|
import { type HTMLAttributes, computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
|
|
@ -32,16 +32,24 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||||
<template>
|
<template>
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
<DialogOverlay
|
<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" />
|
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',
|
<DialogContent
|
||||||
props.class,
|
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
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
<DialogClose v-if="!props.hideClose"
|
<DialogClose
|
||||||
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">
|
v-if="!hideClose"
|
||||||
<X class="size-4" />
|
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>
|
<span class="sr-only">Close</span>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import {
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
type DialogDescriptionProps,
|
type DialogDescriptionProps,
|
||||||
useForwardProps,
|
useForwardProps,
|
||||||
} from "radix-vue";
|
} from "reka-ui";
|
||||||
import { type HTMLAttributes, computed } from "vue";
|
import { type HTMLAttributes, computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const props = defineProps<{ class?: HTMLAttributes["class"] }>();
|
||||||
<div
|
<div
|
||||||
:class="
|
:class="
|
||||||
cn(
|
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,
|
props.class,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
DialogOverlay,
|
DialogOverlay,
|
||||||
DialogPortal,
|
DialogPortal,
|
||||||
useForwardPropsEmits,
|
useForwardPropsEmits,
|
||||||
} from "radix-vue";
|
} from "reka-ui";
|
||||||
import { type HTMLAttributes, computed } from "vue";
|
import { type HTMLAttributes, computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
|
|
@ -29,7 +29,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||||
<template>
|
<template>
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
<DialogOverlay
|
<DialogOverlay
|
||||||
class="fixed inset-0 z-50 grid place-items-center overflow-y-auto 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"
|
class="fixed inset-0 z-50 grid place-items-center overflow-y-auto 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
|
<DialogContent
|
||||||
:class="
|
:class="
|
||||||
|
|
@ -52,7 +52,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||||
<DialogClose
|
<DialogClose
|
||||||
class="absolute top-3 right-3 p-0.5 transition-colors rounded-md hover:bg-secondary"
|
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>
|
<span class="sr-only">Close</span>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cn } from "@/lib/utils";
|
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";
|
import { type HTMLAttributes, computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DialogTrigger, type DialogTriggerProps } from "radix-vue";
|
import { DialogTrigger, type DialogTriggerProps } from "reka-ui";
|
||||||
|
|
||||||
const props = defineProps<DialogTriggerProps>();
|
const props = defineProps<DialogTriggerProps>();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useForwardPropsEmits } from "radix-vue";
|
import { useForwardPropsEmits } from "reka-ui";
|
||||||
import type { DrawerRootEmits, DrawerRootProps } from "vaul-vue";
|
import type { DrawerRootEmits, DrawerRootProps } from "vaul-vue";
|
||||||
import { DrawerRoot } from "vaul-vue";
|
import { DrawerRoot } from "vaul-vue";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { DialogContentEmits, DialogContentProps } from "radix-vue";
|
import type { DialogContentEmits, DialogContentProps } from "reka-ui";
|
||||||
import { useForwardPropsEmits } from "radix-vue";
|
import { useForwardPropsEmits } from "reka-ui";
|
||||||
import { DrawerContent, DrawerPortal } from "vaul-vue";
|
import { DrawerContent, DrawerPortal } from "vaul-vue";
|
||||||
import type { HtmlHTMLAttributes } from "vue";
|
import type { HtmlHTMLAttributes } from "vue";
|
||||||
import DrawerOverlay from "./DrawerOverlay.vue";
|
import DrawerOverlay from "./DrawerOverlay.vue";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { DialogOverlayProps } from "radix-vue";
|
import type { DialogOverlayProps } from "reka-ui";
|
||||||
import { DrawerOverlay } from "vaul-vue";
|
import { DrawerOverlay } from "vaul-vue";
|
||||||
import { type HtmlHTMLAttributes, computed } from "vue";
|
import { type HtmlHTMLAttributes, computed } from "vue";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import {
|
||||||
type DropdownMenuRootEmits,
|
type DropdownMenuRootEmits,
|
||||||
type DropdownMenuRootProps,
|
type DropdownMenuRootProps,
|
||||||
useForwardPropsEmits,
|
useForwardPropsEmits,
|
||||||
} from "radix-vue";
|
} from "reka-ui";
|
||||||
|
|
||||||
const props = defineProps<DropdownMenuRootProps>();
|
const props = defineProps<DropdownMenuRootProps>();
|
||||||
const emits = defineEmits<DropdownMenuRootEmits>();
|
const emits = defineEmits<DropdownMenuRootEmits>();
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
type DropdownMenuCheckboxItemProps,
|
type DropdownMenuCheckboxItemProps,
|
||||||
DropdownMenuItemIndicator,
|
DropdownMenuItemIndicator,
|
||||||
useForwardPropsEmits,
|
useForwardPropsEmits,
|
||||||
} from "radix-vue";
|
} from "reka-ui";
|
||||||
import { type HTMLAttributes, computed } from "vue";
|
import { type HTMLAttributes, computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
|
|
@ -25,15 +25,18 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DropdownMenuCheckboxItem v-bind="forwarded" :class="cn(
|
<DropdownMenuCheckboxItem
|
||||||
'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',
|
v-bind="forwarded"
|
||||||
props.class,
|
: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',
|
||||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
props.class,
|
||||||
<DropdownMenuItemIndicator>
|
)"
|
||||||
<Check class="!mr-0" />
|
>
|
||||||
</DropdownMenuItemIndicator>
|
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
</span>
|
<DropdownMenuItemIndicator>
|
||||||
<slot />
|
<Check class="w-4 h-4" />
|
||||||
</DropdownMenuCheckboxItem>
|
</DropdownMenuItemIndicator>
|
||||||
|
</span>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import {
|
||||||
type DropdownMenuContentProps,
|
type DropdownMenuContentProps,
|
||||||
DropdownMenuPortal,
|
DropdownMenuPortal,
|
||||||
useForwardPropsEmits,
|
useForwardPropsEmits,
|
||||||
} from "radix-vue";
|
} from "reka-ui";
|
||||||
import { type HTMLAttributes, computed } from "vue";
|
import { type HTMLAttributes, computed } from "vue";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DropdownMenuGroup, type DropdownMenuGroupProps } from "radix-vue";
|
import { DropdownMenuGroup, type DropdownMenuGroupProps } from "reka-ui";
|
||||||
|
|
||||||
const props = defineProps<DropdownMenuGroupProps>();
|
const props = defineProps<DropdownMenuGroupProps>();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import {
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
type DropdownMenuItemProps,
|
type DropdownMenuItemProps,
|
||||||
useForwardProps,
|
useForwardProps,
|
||||||
} from "radix-vue";
|
} from "reka-ui";
|
||||||
import { type HTMLAttributes, computed } from "vue";
|
import { type HTMLAttributes, computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
|
|
@ -21,11 +21,16 @@ const forwardedProps = useForwardProps(delegatedProps);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DropdownMenuItem v-bind="forwardedProps" :class="cn(
|
<DropdownMenuItem
|
||||||
'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',
|
v-bind="forwardedProps"
|
||||||
inset && 'pl-8',
|
:class="
|
||||||
props.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
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import {
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
type DropdownMenuLabelProps,
|
type DropdownMenuLabelProps,
|
||||||
useForwardProps,
|
useForwardProps,
|
||||||
} from "radix-vue";
|
} from "reka-ui";
|
||||||
import { type HTMLAttributes, computed } from "vue";
|
import { type HTMLAttributes, computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
|
|
@ -24,8 +24,10 @@ const forwardedProps = useForwardProps(delegatedProps);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DropdownMenuLabel v-bind="forwardedProps"
|
<DropdownMenuLabel
|
||||||
:class="cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)">
|
v-bind="forwardedProps"
|
||||||
<slot />
|
:class="cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)"
|
||||||
</DropdownMenuLabel>
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuLabel>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import {
|
||||||
type DropdownMenuRadioGroupEmits,
|
type DropdownMenuRadioGroupEmits,
|
||||||
type DropdownMenuRadioGroupProps,
|
type DropdownMenuRadioGroupProps,
|
||||||
useForwardPropsEmits,
|
useForwardPropsEmits,
|
||||||
} from "radix-vue";
|
} from "reka-ui";
|
||||||
|
|
||||||
const props = defineProps<DropdownMenuRadioGroupProps>();
|
const props = defineProps<DropdownMenuRadioGroupProps>();
|
||||||
const emits = defineEmits<DropdownMenuRadioGroupEmits>();
|
const emits = defineEmits<DropdownMenuRadioGroupEmits>();
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
type DropdownMenuRadioItemEmits,
|
type DropdownMenuRadioItemEmits,
|
||||||
type DropdownMenuRadioItemProps,
|
type DropdownMenuRadioItemProps,
|
||||||
useForwardPropsEmits,
|
useForwardPropsEmits,
|
||||||
} from "radix-vue";
|
} from "reka-ui";
|
||||||
import { type HTMLAttributes, computed } from "vue";
|
import { type HTMLAttributes, computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
|
|
@ -26,15 +26,18 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DropdownMenuRadioItem v-bind="forwarded" :class="cn(
|
<DropdownMenuRadioItem
|
||||||
'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',
|
v-bind="forwarded"
|
||||||
props.class,
|
: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',
|
||||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
props.class,
|
||||||
<DropdownMenuItemIndicator>
|
)"
|
||||||
<Circle class="size-2 fill-current" />
|
>
|
||||||
</DropdownMenuItemIndicator>
|
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
</span>
|
<DropdownMenuItemIndicator>
|
||||||
<slot />
|
<Circle class="h-2 w-2 fill-current" />
|
||||||
</DropdownMenuRadioItem>
|
</DropdownMenuItemIndicator>
|
||||||
|
</span>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
type DropdownMenuSeparatorProps,
|
type DropdownMenuSeparatorProps,
|
||||||
} from "radix-vue";
|
} from "reka-ui";
|
||||||
import { type HTMLAttributes, computed } from "vue";
|
import { type HTMLAttributes, computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import {
|
||||||
type DropdownMenuSubEmits,
|
type DropdownMenuSubEmits,
|
||||||
type DropdownMenuSubProps,
|
type DropdownMenuSubProps,
|
||||||
useForwardPropsEmits,
|
useForwardPropsEmits,
|
||||||
} from "radix-vue";
|
} from "reka-ui";
|
||||||
|
|
||||||
const props = defineProps<DropdownMenuSubProps>();
|
const props = defineProps<DropdownMenuSubProps>();
|
||||||
const emits = defineEmits<DropdownMenuSubEmits>();
|
const emits = defineEmits<DropdownMenuSubEmits>();
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {
|
||||||
type DropdownMenuSubContentEmits,
|
type DropdownMenuSubContentEmits,
|
||||||
type DropdownMenuSubContentProps,
|
type DropdownMenuSubContentProps,
|
||||||
useForwardPropsEmits,
|
useForwardPropsEmits,
|
||||||
} from "radix-vue";
|
} from "reka-ui";
|
||||||
import { type HTMLAttributes, computed } from "vue";
|
import { type HTMLAttributes, computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
type DropdownMenuSubTriggerProps,
|
type DropdownMenuSubTriggerProps,
|
||||||
useForwardProps,
|
useForwardProps,
|
||||||
} from "radix-vue";
|
} from "reka-ui";
|
||||||
import { type HTMLAttributes, computed } from "vue";
|
import { type HTMLAttributes, computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
|
|
@ -30,6 +30,6 @@ const forwardedProps = useForwardProps(delegatedProps);
|
||||||
)"
|
)"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
<ChevronRight class="ml-auto size-4" />
|
<ChevronRight class="ml-auto h-4 w-4" />
|
||||||
</DropdownMenuSubTrigger>
|
</DropdownMenuSubTrigger>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import {
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
type DropdownMenuTriggerProps,
|
type DropdownMenuTriggerProps,
|
||||||
useForwardProps,
|
useForwardProps,
|
||||||
} from "radix-vue";
|
} from "reka-ui";
|
||||||
|
|
||||||
const props = defineProps<DropdownMenuTriggerProps>();
|
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 DropdownMenuSubContent } from "./DropdownMenuSubContent.vue";
|
||||||
export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue";
|
export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue";
|
||||||
export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.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>
|
<script lang="ts" setup>
|
||||||
import { Slot } from "radix-vue";
|
import { Slot } from "reka-ui";
|
||||||
import { useFormField } from "./useFormField";
|
import { useFormField } from "./useFormField";
|
||||||
|
|
||||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
|
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,19 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { cn } from "@/lib/utils";
|
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 { type HTMLAttributes, provide } from "vue";
|
||||||
import { FORM_ITEM_INJECTION_KEY } from "./injectionKeys";
|
import { FORM_ITEM_INJECTION_KEY } from "./injectionKeys";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = defineProps<{
|
||||||
defineProps<
|
class?: HTMLAttributes["class"];
|
||||||
PrimitiveProps & {
|
}>();
|
||||||
class?: HTMLAttributes["class"];
|
|
||||||
}
|
|
||||||
>(),
|
|
||||||
{
|
|
||||||
as: "div",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const id = useId();
|
const id = useId();
|
||||||
provide(FORM_ITEM_INJECTION_KEY, id);
|
provide(FORM_ITEM_INJECTION_KEY, id);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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 />
|
<slot />
|
||||||
</Primitive>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { LabelProps } from "radix-vue";
|
import type { LabelProps } from "reka-ui";
|
||||||
import type { HTMLAttributes } from "vue";
|
import type { HTMLAttributes } from "vue";
|
||||||
import { Label } from "~/components/ui/label";
|
|
||||||
import { useFormField } from "./useFormField";
|
import { useFormField } from "./useFormField";
|
||||||
|
|
||||||
const props = defineProps<LabelProps & { class?: HTMLAttributes["class"] }>();
|
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 FormLabel } from "./FormLabel.vue";
|
||||||
export { default as FormMessage } from "./FormMessage.vue";
|
export { default as FormMessage } from "./FormMessage.vue";
|
||||||
export { FORM_ITEM_INJECTION_KEY } from "./injectionKeys";
|
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 HoverCardRootEmits,
|
||||||
type HoverCardRootProps,
|
type HoverCardRootProps,
|
||||||
useForwardPropsEmits,
|
useForwardPropsEmits,
|
||||||
} from "radix-vue";
|
} from "reka-ui";
|
||||||
|
|
||||||
const props = defineProps<HoverCardRootProps>();
|
const props = defineProps<HoverCardRootProps>();
|
||||||
const emits = defineEmits<HoverCardRootEmits>();
|
const emits = defineEmits<HoverCardRootEmits>();
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {
|
||||||
type HoverCardContentProps,
|
type HoverCardContentProps,
|
||||||
HoverCardPortal,
|
HoverCardPortal,
|
||||||
useForwardProps,
|
useForwardProps,
|
||||||
} from "radix-vue";
|
} from "reka-ui";
|
||||||
import { type HTMLAttributes, computed } from "vue";
|
import { type HTMLAttributes, computed } from "vue";
|
||||||
|
|
||||||
const props = withDefaults(
|
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