chore: ⬆️ Upgrade to the latest Shadcn-Vue version
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 2m30s
Deploy to GitHub Pages / build (push) Failing after 6s
Deploy to GitHub Pages / deploy (push) Has been skipped
Docker / build (push) Failing after 5s
Mirror to Codeberg / Mirror (push) Failing after 0s

This commit is contained in:
Jesse Wierzbinski 2025-03-28 01:16:24 +01:00
parent 7649ecfb80
commit 092bce0f24
No known key found for this signature in database
169 changed files with 1860 additions and 1088 deletions

18
app.vue
View file

@ -18,9 +18,9 @@ import "~/styles/index.css";
import { convert } from "html-to-text";
import ConfirmationModal from "./components/modals/confirm.vue";
import { Toaster } from "./components/ui/sonner";
import { TooltipProvider } from "./components/ui/tooltip";
import { overwriteGetLocale } from "./paraglide/runtime";
import { type EnumSetting, SettingIds } from "./settings";
import { TooltipProvider } from "./components/ui/tooltip";
// Sin
//import "~/styles/mcdonalds.css";
@ -58,7 +58,7 @@ useSeoMeta({
ogImage: computed(() => instance.value?.banner?.url),
twitterTitle: computed(() => instance.value?.title ?? ""),
twitterDescription: computed(() =>
convert(description.value?.content ?? "")
convert(description.value?.content ?? ""),
),
twitterImage: computed(() => instance.value?.banner?.url),
description: computed(() => convert(description.value?.content ?? "")),
@ -76,7 +76,7 @@ useHead({
if (code && origin && appData.value && route.path !== "/oauth/code") {
const newOrigin = new URL(
URL.canParse(origin) ? origin : `https://${origin}`
URL.canParse(origin) ? origin : `https://${origin}`,
);
signInWithCode(code, appData.value, newOrigin);
@ -84,7 +84,7 @@ if (code && origin && appData.value && route.path !== "/oauth/code") {
if (origin && !code) {
const newOrigin = new URL(
URL.canParse(origin) ? origin : `https://${origin}`
URL.canParse(origin) ? origin : `https://${origin}`,
);
signIn(appData, newOrigin);
@ -108,4 +108,14 @@ html.theme-changing * {
transition: background-color 1s ease, border 1s ease, color 1s ease,
box-shadow 1s ease !important;
}
.slide-down-enter-active,
.slide-down-leave-active {
transition: transform 0.3s ease;
}
.slide-down-enter-from,
.slide-down-leave-to {
transform: translateY(-100%);
}
</style>

View file

@ -38,7 +38,7 @@
"nanoid": "^5.1.5",
"nuxt": "^3.16.1",
"nuxt-security": "^2.2.0",
"radix-vue": "^1.9.17",
"reka-ui": "^2.1.1",
"shadcn-nuxt": "1.0.3",
"tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7",
@ -1768,8 +1768,6 @@
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"radix-vue": ["radix-vue@1.9.17", "", { "dependencies": { "@floating-ui/dom": "^1.6.7", "@floating-ui/vue": "^1.1.0", "@internationalized/date": "^3.5.4", "@internationalized/number": "^3.5.3", "@tanstack/vue-virtual": "^3.8.1", "@vueuse/core": "^10.11.0", "@vueuse/shared": "^10.11.0", "aria-hidden": "^1.2.4", "defu": "^6.1.4", "fast-deep-equal": "^3.1.3", "nanoid": "^5.0.7" }, "peerDependencies": { "vue": ">= 3.2.0" } }, "sha512-mVCu7I2vXt1L2IUYHTt0sZMz7s1K2ZtqKeTIxG3yC5mMFfLBG4FtE1FDeRMpDd+Hhg/ybi9+iXmAP1ISREndoQ=="],
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
"randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="],
@ -2426,10 +2424,6 @@
"prosemirror-trailing-node/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"radix-vue/@vueuse/core": ["@vueuse/core@10.11.1", "", { "dependencies": { "@types/web-bluetooth": "^0.0.20", "@vueuse/metadata": "10.11.1", "@vueuse/shared": "10.11.1", "vue-demi": ">=0.14.8" } }, "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww=="],
"radix-vue/@vueuse/shared": ["@vueuse/shared@10.11.1", "", { "dependencies": { "vue-demi": ">=0.14.8" } }, "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA=="],
"randombytes/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"readdir-glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
@ -2590,10 +2584,6 @@
"nypm/pkg-types/confbox": ["confbox@0.2.1", "", {}, "sha512-hkT3yDPFbs95mNCy1+7qNKC6Pro+/ibzYxtM2iqEigpf0sVw+bg4Zh9/snjsBcf990vfIsg5+1U7VyiyBb3etg=="],
"radix-vue/@vueuse/core/@types/web-bluetooth": ["@types/web-bluetooth@0.0.20", "", {}, "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="],
"radix-vue/@vueuse/core/@vueuse/metadata": ["@vueuse/metadata@10.11.1", "", {}, "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw=="],
"reka-ui/@vueuse/core/@vueuse/metadata": ["@vueuse/metadata@12.8.2", "", {}, "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A=="],
"resolve-path/http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="],

View file

@ -2,7 +2,6 @@
"$schema": "https://shadcn-vue.com/schema.json",
"style": "default",
"typescript": true,
"tsConfigPath": ".nuxt/tsconfig.json",
"tailwind": {
"config": "tailwind.config.ts",
"css": "styles/index.css",
@ -10,9 +9,11 @@
"cssVariables": true,
"prefix": ""
},
"framework": "nuxt",
"aliases": {
"components": "~/components",
"utils": "@/lib/utils"
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"composables": "@/composables"
}
}

View file

@ -44,8 +44,8 @@
<Toggle
variant="default"
size="sm"
:pressed="state.contentType === 'text/html'"
@update:pressed="
:model-value="state.contentType === 'text/html'"
@update:model-value="
(i) =>
(state.contentType = i ? 'text/html' : 'text/plain')
"
@ -61,6 +61,8 @@
<SelectTrigger
:as-child="true"
:disabled="relation?.type === 'edit'"
:disable-default-classes="true"
:disable-select-icon="true"
>
<Button variant="ghost" size="icon">
<component
@ -110,11 +112,7 @@
</Tooltip>
<Tooltip>
<TooltipTrigger as="div">
<Toggle
variant="default"
size="sm"
v-model:pressed="state.sensitive"
>
<Toggle variant="default" size="sm" v-model="state.sensitive">
<TriangleAlert class="!size-5" />
</Toggle>
</TooltipTrigger>
@ -153,19 +151,23 @@ import {
Smile,
TriangleAlert,
} from "lucide-vue-next";
import { SelectTrigger } from "radix-vue";
import { toast } from "vue-sonner";
import Note from "~/components/notes/note.vue";
import { Select, SelectContent, SelectItem } from "~/components/ui/select";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from "~/components/ui/select";
import * as m from "~/paraglide/messages.js";
import { SettingIds } from "~/settings";
import EditorContent from "../editor/content.vue";
import { Button } from "../ui/button";
import { DialogFooter } from "../ui/dialog";
import { Input } from "../ui/input";
import { Toggle } from "../ui/toggle";
import { DialogFooter } from "../ui/dialog";
import Files from "./files.vue";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import Files from "./files.vue";
const { Control_Enter, Command_Enter } = useMagicKeys();
const ctrlEnterSend = useSetting(SettingIds.CtrlEnterToSend);
@ -221,7 +223,7 @@ const state = reactive({
contentType: "text/html" as "text/html" | "text/plain",
visibility: (relation?.type === "edit"
? relation.note.visibility
: defaultVisibility.value.value ?? "public") as Status["visibility"],
: (defaultVisibility.value.value ?? "public")) as Status["visibility"],
files: (relation?.type === "edit"
? relation.note.media_attachments.map((a) => ({
apiId: a.id,

View file

@ -60,7 +60,7 @@ const relation = ref(
type: "reply" | "quote" | "edit";
note: Status;
source?: StatusSource;
} | null
} | null,
);
</script>

View file

@ -1,13 +1,23 @@
<template>
<DropdownMenu>
<DropdownMenuTrigger as="button"
<DropdownMenuTrigger
as="button"
:disabled="file.uploading || file.updating"
class="block bg-card text-card-foreground shadow-sm h-28 overflow-hidden rounded relative min-w-28 *:disabled:opacity-50">
class="block bg-card text-card-foreground shadow-sm h-28 overflow-hidden rounded relative min-w-28 *:disabled:opacity-50"
>
<Avatar class="h-28 w-full" shape="square">
<AvatarImage class="!object-contain" :src="createObjectURL(file.file)" />
<AvatarImage
class="!object-contain"
:src="createObjectURL(file.file)"
/>
</Avatar>
<Badge v-if="!file.uploading && !file.updating" class="absolute bottom-1 right-1" variant="default">{{ formatBytes(file.file.size) }}</Badge>
<Badge v-else class="absolute bottom-1 right-1 rounded px-1 !opacity-100" variant="default"><Loader class="animate-spin size-4" /></Badge>
<Badge
v-if="file.uploading && !file.updating"
class="absolute bottom-1 right-1"
variant="default"
>{{ formatBytes(file.file.size) }}</Badge
>
<Spinner v-else class="absolute bottom-1 right-1 size-8 p-1.5" />
</DropdownMenuTrigger>
<DropdownMenuContent class="min-w-48">
<DropdownMenuLabel>{{ file.file.name }}</DropdownMenuLabel>
@ -32,6 +42,7 @@
<script lang="ts" setup>
import { Captions, Delete, Loader, TextCursorInput } from "lucide-vue-next";
import Spinner from "~/components/graphics/spinner.vue";
import { confirmModalService } from "~/components/modals/composable.ts";
import { Avatar, AvatarImage } from "~/components/ui/avatar";
import { Badge } from "~/components/ui/badge";
@ -121,6 +132,8 @@ const formatBytes = (bytes: number) => {
const digitsAfterPoint = 2;
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${Number.parseFloat((bytes / k ** i).toFixed(digitsAfterPoint))} ${sizes[i]}`;
return `${Number.parseFloat((bytes / k ** i).toFixed(digitsAfterPoint))} ${
sizes[i]
}`;
};
</script>

View 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>

View 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>

View 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>

View 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>

View 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>

View file

@ -1,19 +1,27 @@
<template>
<div
class="fixed md:hidden bottom-0 inset-x-0 border-t h-16 bg-background z-10 flex items-center justify-around *:h-full *:w-full gap-6 px-4 py-2 [&>a>svg]:size-5 [&>button>svg]:size-5">
class="fixed md:hidden bottom-0 inset-x-0 border-t h-16 bg-background z-10 flex items-center justify-around *:h-full *:w-full gap-6 px-4 py-2 [&>a>svg]:size-5 [&>button>svg]:size-5"
>
<Button :as="NuxtLink" href="/" variant="ghost" size="icon">
<Home />
</Button>
<Button :as="NuxtLink" href="/notifications" variant="ghost" size="icon">
<Button
:as="NuxtLink"
href="/notifications"
variant="ghost"
size="icon"
>
<Bell />
</Button>
<AccountSwitcher>
<Button variant="ghost" size="icon">
<User />
</Button>
</AccountSwitcher>
<Button variant="default" size="icon" :title="m.salty_aloof_turkey_nudge()"
@click="useEvent('composer:open')">
<Button
variant="default"
size="icon"
:title="m.salty_aloof_turkey_nudge()"
@click="useEvent('composer:open')"
>
<Pen />
</Button>
</div>
@ -23,6 +31,5 @@
import { Bell, Home, Pen, User } from "lucide-vue-next";
import * as m from "~/paraglide/messages.js";
import { NuxtLink } from "#components";
import AccountSwitcher from "../sidebars/account-switcher.vue";
import { Button } from "../ui/button";
</script>

View file

@ -1,29 +1,74 @@
<template>
<Card as="article" class="rounded-none border-0 duration-200 shadow- max-w-full relative">
<CardHeader class="pb-4" as="header">
<ReblogHeader v-if="note.reblog" :avatar="note.account.avatar" :display-name="note.account.display_name"
:url="reblogAccountUrl" :emojis="note.account.emojis" />
<Header :author="noteToUse.account" :author-url="accountUrl"
:corner-avatar="note.reblog ? note.account.avatar : undefined" :note-url="url"
:visibility="noteToUse.visibility" :created-at="new Date(noteToUse.created_at)"
:small-layout="smallLayout" class="z-[1]" />
<div v-if="topAvatarBar" :class="cn('shrink-0 bg-border w-0.5 absolute top-0 h-7 left-[3rem]')"></div>
<div v-if="bottomAvatarBar" :class="cn('shrink-0 bg-border w-0.5 absolute bottom-0 h-[calc(100%-1.5rem)] left-[3rem]')"></div>
<Card as="article" class="relative gap-4 items-stretch">
<CardHeader as="header">
<ReblogHeader
v-if="note.reblog"
:avatar="note.account.avatar"
:display-name="note.account.display_name"
:url="reblogAccountUrl"
:emojis="note.account.emojis"
/>
<Header
:author="noteToUse.account"
:author-url="accountUrl"
:corner-avatar="note.reblog ? note.account.avatar : undefined"
:note-url="url"
:visibility="noteToUse.visibility"
:created-at="new Date(noteToUse.created_at)"
:small-layout="smallLayout"
class="z-[1]"
/>
<div
v-if="topAvatarBar"
:class="
cn(
'shrink-0 bg-border w-0.5 absolute top-0 h-7 left-[3rem]'
)
"
></div>
<div
v-if="bottomAvatarBar"
:class="
cn(
'shrink-0 bg-border w-0.5 absolute bottom-0 h-[calc(100%-1.5rem)] left-[3rem]'
)
"
></div>
</CardHeader>
<!-- Simply offset by the size of avatar + 0.75rem (the gap) -->
<CardContent :class="contentUnderUsername && (smallLayout ? 'ml-11' : 'ml-[4.25rem]')">
<Content :content="noteToUse.content" :quote="note.quote ?? undefined"
:attachments="noteToUse.media_attachments" :plain-content="noteToUse.plain_content ?? undefined"
:emojis="noteToUse.emojis" :sensitive="noteToUse.sensitive" :content-warning="noteToUse.spoiler_text" />
<CardContent
:class="
contentUnderUsername && (smallLayout ? 'ml-11' : 'ml-[4.25rem]')
"
>
<Content
:content="noteToUse.content"
:quote="note.quote ?? undefined"
:attachments="noteToUse.media_attachments"
:plain-content="noteToUse.plain_content ?? undefined"
:emojis="noteToUse.emojis"
:sensitive="noteToUse.sensitive"
:content-warning="noteToUse.spoiler_text"
/>
</CardContent>
<CardFooter v-if="!hideActions" class="p-4 pt-0">
<Actions :reply-count="noteToUse.replies_count" :like-count="noteToUse.favourites_count" :url="url"
:api-note-string="JSON.stringify(note, null, 4)" :reblog-count="noteToUse.reblogs_count"
:remote-url="noteToUse.url" :is-remote="isRemote" :author-id="noteToUse.account.id"
@edit="useEvent('composer:edit', note)" @reply="useEvent('composer:reply', note)"
@quote="useEvent('composer:quote', note)" @delete="useEvent('note:delete', note)"
:note-id="noteToUse.id" :liked="noteToUse.favourited ?? false"
:reblogged="noteToUse.reblogged ?? false" />
<CardFooter v-if="!hideActions">
<Actions
:reply-count="noteToUse.replies_count"
:like-count="noteToUse.favourites_count"
:url="url"
:api-note-string="JSON.stringify(note, null, 4)"
:reblog-count="noteToUse.reblogs_count"
:remote-url="noteToUse.url"
:is-remote="isRemote"
:author-id="noteToUse.account.id"
@edit="useEvent('composer:edit', note)"
@reply="useEvent('composer:reply', note)"
@quote="useEvent('composer:quote', note)"
@delete="useEvent('note:delete', note)"
:note-id="noteToUse.id"
:liked="noteToUse.favourited ?? false"
:reblogged="noteToUse.reblogged ?? false"
/>
</CardFooter>
</Card>
</template>

View file

@ -1,7 +1,18 @@
<template>
<div>
<Note v-if="parent" :note="parent" :hide-actions="true" :content-under-username="true" :bottom-avatar-bar="true" />
<Note :note="note" :top-avatar-bar="!!parent" />
<Note
v-if="parent"
:note="parent"
:hide-actions="true"
:content-under-username="true"
:bottom-avatar-bar="true"
class="border-b-0 rounded-b-none"
/>
<Note
:note="note"
:class="parent && 'border-t-0 rounded-t-none'"
:top-avatar-bar="!!parent"
/>
</div>
</template>

View file

@ -1,17 +1,30 @@
<template>
<Card class="rounded-none border-0">
<Card class="*:w-full p-2">
<Collapsible :default-open="true" v-slot="{ open }">
<Tooltip>
<TooltipTrigger :as-child="true">
<CardHeader v-if="notification.account"
class="flex-row items-center gap-2 px-4 py-2 border-b border-border">
<CardHeader
v-if="notification.account"
class="flex-row items-center gap-2 px-2"
>
<component :is="icon" class="size-5 shrink-0" />
<Avatar class="size-6 border border-card" :src="notification.account.avatar" :name="notification.account.display_name" />
<span class="font-semibold" v-render-emojis="notification.account.emojis">{{
notification.account.display_name
}}</span>
<Avatar
class="size-5 border border-card"
:src="notification.account.avatar"
:name="notification.account.display_name"
/>
<span
class="font-semibold text-sm"
v-render-emojis="notification.account.emojis"
>{{ notification.account.display_name }}</span
>
<CollapsibleTrigger :as-child="true">
<Button variant="ghost" size="icon" class="ml-auto [&_svg]:data-[state=open]:-rotate-180" :title="open ? 'Collapse' : 'Expand'">
<Button
variant="ghost"
size="icon"
class="ml-auto [&_svg]:data-[state=open]:-rotate-180"
:title="open ? 'Collapse' : 'Expand'"
>
<ChevronDown class="duration-200" />
</Button>
</CollapsibleTrigger>
@ -23,9 +36,19 @@
</Tooltip>
<CollapsibleContent :as-child="true">
<CardContent class="p-0">
<Note v-if="notification.status" :note="notification.status" :small-layout="true"
:hide-actions="true" />
<FollowRequest v-else-if="notification.type === 'follow_request' && notification.account" :follower="notification.account" />
<Note
v-if="notification.status"
:note="notification.status"
:small-layout="true"
:hide-actions="true"
/>
<FollowRequest
v-else-if="
notification.type === 'follow_request' &&
notification.account
"
:follower="notification.account"
/>
</CardContent>
</CollapsibleContent>
</Collapsible>

View file

@ -82,7 +82,11 @@ const issuerRedirectUrl = (issuerId: string) => {
<template>
<div class="grid gap-6">
<form @submit="form.submitForm" method="post" :action="redirectUrl.toString()">
<form
@submit="form.submitForm"
method="post"
:action="redirectUrl.toString()"
>
<div class="grid gap-6">
<FormField v-slot="{ componentField }" name="identifier">
<FormItem>
@ -90,9 +94,15 @@ const issuerRedirectUrl = (issuerId: string) => {
{{ m.fluffy_soft_wolf_cook() }}
</FormLabel>
<FormControl>
<Input placeholder="petergriffin" type="text" auto-capitalize="none"
auto-complete="idenfifier" auto-correct="off" :disabled="isLoading"
v-bind="componentField" />
<Input
placeholder="petergriffin"
type="text"
auto-capitalize="none"
auto-complete="idenfifier"
auto-correct="off"
:disabled="isLoading"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
@ -103,9 +113,15 @@ const issuerRedirectUrl = (issuerId: string) => {
{{ m.livid_bright_wallaby_quiz() }}
</FormLabel>
<FormControl>
<Input placeholder="hunter2" type="password" auto-capitalize="none"
auto-complete="password" auto-correct="off" :disabled="isLoading"
v-bind="componentField" />
<Input
placeholder="hunter2"
type="password"
auto-capitalize="none"
auto-complete="password"
auto-correct="off"
:disabled="isLoading"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
@ -116,7 +132,10 @@ const issuerRedirectUrl = (issuerId: string) => {
</Button>
</div>
</form>
<div v-if="ssoConfig && ssoConfig.providers.length > 0" class="relative">
<div
v-if="ssoConfig && ssoConfig.providers.length > 0"
class="relative"
>
<div class="absolute inset-0 flex items-center">
<span class="w-full border-t" />
</div>
@ -126,11 +145,25 @@ const issuerRedirectUrl = (issuerId: string) => {
</span>
</div>
</div>
<div v-if="ssoConfig && ssoConfig.providers.length > 0" class="flex flex-col gap-2">
<Button as="a" :href="issuerRedirectUrl(provider.id)" variant="outline" type="button" :disabled="isLoading" v-for="provider of ssoConfig.providers">
<div
v-if="ssoConfig && ssoConfig.providers.length > 0"
class="flex flex-col gap-2"
>
<Button
as="a"
:href="issuerRedirectUrl(provider.id)"
variant="outline"
type="button"
:disabled="isLoading"
v-for="provider of ssoConfig.providers"
>
<Loader v-if="isLoading" class="mr-2 animate-spin" />
<img crossorigin="anonymous" :src="provider.icon" :alt="`${provider.name}'s logo`"
class="size-4 mr-2" />
<img
crossorigin="anonymous"
:src="provider.icon"
:alt="`${provider.name}'s logo`"
class="size-4 mr-2"
/>
{{ provider.name }}
</Button>
</div>

View file

@ -1,5 +1,9 @@
<template>
<Collapsible :as="Card" class="grid items-center px-6 py-4 gap-4" v-slot="{ open }">
<Collapsible
:as="Card"
class="grid justify-normal items-center px-6 py-4 gap-4"
v-slot="{ open }"
>
<div class="grid grid-cols-[1fr,auto] items-center gap-4">
<CardHeader class="space-y-0.5 p-0">
<CardTitle class="text-base">
@ -10,16 +14,27 @@
</CardDescription>
</CardHeader>
<CollapsibleTrigger :as-child="true">
<Button variant="outline" size="icon" class="ml-auto [&_svg]:data-[state=open]:-rotate-180"
:title="open ? 'Collapse' : 'Expand'">
<Button
variant="outline"
size="icon"
class="ml-auto [&_svg]:data-[state=open]:-rotate-180"
:title="open ? 'Collapse' : 'Expand'"
>
<ChevronDown class="duration-200" />
</Button>
</CollapsibleTrigger>
</div>
<CollapsibleContent :as-child="true">
<CardFooter class="p-1">
<Textarea :rows="10" :model-value="setting.value"
@update:model-value="v => { setting.value = String(v) }" />
<Textarea
:rows="10"
:model-value="setting.value"
@update:model-value="
(v) => {
setting.value = String(v);
}
"
/>
</CardFooter>
</CollapsibleContent>
</Collapsible>

View file

@ -107,7 +107,7 @@ const editName = async () => {
toast.success(m.gaudy_lime_bison_adore());
identity.value.emojis = identity.value.emojis.map((e) =>
e.id === emoji.id ? data : e
e.id === emoji.id ? data : e,
);
} catch {
toast.dismiss(id);
@ -134,7 +134,7 @@ const _delete = async () => {
toast.success(m.crisp_whole_canary_tear());
identity.value.emojis = identity.value.emojis.filter(
(e) => e.id !== emoji.id
(e) => e.id !== emoji.id,
);
} catch {
toast.dismiss(id);

View file

@ -227,7 +227,7 @@ const formSchema = toTypedSchema(
count:
identity.value?.instance.configuration.emojis
.emoji_size_limit ?? Number.POSITIVE_INFINITY,
})
}),
),
shortcode: z
.string()
@ -240,7 +240,7 @@ const formSchema = toTypedSchema(
identity.value?.instance.configuration.emojis
.max_emoji_shortcode_characters ??
Number.POSITIVE_INFINITY,
})
}),
)
.regex(emojiValidator),
global: z.boolean().default(false),
@ -250,7 +250,7 @@ const formSchema = toTypedSchema(
64,
m.home_cool_orangutan_hug({
count: 64,
})
}),
)
.optional(),
alt: z
@ -264,10 +264,10 @@ const formSchema = toTypedSchema(
identity.value?.instance.configuration.emojis
.max_emoji_description_characters ??
Number.POSITIVE_INFINITY,
})
}),
)
.optional(),
})
}),
);
const { isSubmitting, handleSubmit, values, setFieldValue } = useForm({
validationSchema: formSchema,
@ -288,7 +288,7 @@ const submit = handleSubmit(async (values) => {
alt: values.alt,
category: values.category,
global: values.global,
}
},
);
toast.dismiss(id);

View file

@ -1,5 +1,5 @@
<template>
<Card v-if="identity" class="w-full max-h-full overflow-auto">
<Card v-if="identity" class="w-full max-h-full block overflow-y-auto">
<form class="p-4 grid gap-6" ref="formRef" @submit="handleSubmit">
<FormField v-slot="{ handleChange, handleBlur }" name="banner">
<FormItem>
@ -156,6 +156,7 @@
v-slot="{ componentField, value, handleChange }"
name="bot"
:as="Card"
class="block"
>
<FormItem
class="grid grid-cols-[1fr,auto] items-center p-6 gap-2"
@ -183,6 +184,7 @@
v-slot="{ componentField, value, handleChange }"
name="locked"
:as="Card"
class="block"
>
<FormItem
class="grid grid-cols-[1fr,auto] items-center p-6 gap-2"
@ -210,6 +212,7 @@
v-slot="{ componentField, value, handleChange }"
name="discoverable"
:as="Card"
class="block"
>
<FormItem
class="grid grid-cols-[1fr,auto] items-center p-6 gap-2"
@ -234,10 +237,6 @@
</FormField>
</form>
</Card>
<Profile
:account="account"
class="max-w-lg overflow-auto hidden xl:block"
/>
</template>
<script lang="ts" setup>
@ -247,7 +246,6 @@ import { Trash } from "lucide-vue-next";
import { useForm } from "vee-validate";
import { toast } from "vue-sonner";
import { z } from "zod";
import Profile from "~/components/profiles/profile.vue";
import { Button } from "~/components/ui/button";
import {
Card,
@ -287,7 +285,7 @@ const formSchema = toTypedSchema(
m.civil_icy_ant_mend({
size: identity.value?.instance.configuration.accounts
.header_size_limit,
})
}),
)
.optional(),
avatar: z
@ -300,7 +298,7 @@ const formSchema = toTypedSchema(
m.zippy_caring_raven_edit({
size: identity.value?.instance.configuration.accounts
.avatar_size_limit,
})
}),
)
.or(z.string().url())
.optional(),
@ -308,26 +306,26 @@ const formSchema = toTypedSchema(
.string()
.max(
identity.value.instance.configuration.accounts
.max_displayname_characters
.max_displayname_characters,
),
username: z
.string()
.regex(/^[a-z0-9_-]+$/, m.still_upper_otter_dine())
.max(
identity.value.instance.configuration.accounts
.max_username_characters
.max_username_characters,
),
bio: z
.string()
.max(
identity.value.instance.configuration.accounts
.max_note_characters
.max_note_characters,
),
bot: z.boolean(),
locked: z.boolean(),
discoverable: z.boolean(),
fields: z.array(z.object({ name: z.string(), value: z.string() })),
})
}),
);
const form = useForm({
@ -367,8 +365,8 @@ const handleSubmit = form.handleSubmit(async (values) => {
// Can't compare two arrays directly in JS, so we need to check if all fields are the same
fields_attributes: values.fields.every((field) =>
account.value.source?.fields?.some(
(f) => f.name === field.name && f.value === field.value
)
(f) => f.name === field.name && f.value === field.value,
),
)
? undefined
: values.fields,
@ -387,8 +385,8 @@ const handleSubmit = form.handleSubmit(async (values) => {
try {
const { data } = await client.value.updateCredentials(
Object.fromEntries(
Object.entries(changedData).filter(([, v]) => v !== undefined)
)
Object.entries(changedData).filter(([, v]) => v !== undefined),
),
);
toast.dismiss(id);
@ -399,6 +397,12 @@ const handleSubmit = form.handleSubmit(async (values) => {
}
account.value = data;
form.resetForm({
values: {
...form.values,
...values,
},
});
} catch (e) {
const error = e as ResponseError<{ error: string }>;

View file

@ -193,25 +193,25 @@ const schema = toTypedSchema(
(v) => v.size <= (maxSize ?? Number.MAX_SAFE_INTEGER),
m.zippy_caring_raven_edit({
size: maxSize ?? Number.MAX_SAFE_INTEGER,
})
}),
),
})
.or(
z.object({
url: z.string().url(),
})
}),
)
.or(
z.object({
email: z.string().email(),
})
)
}),
),
);
const emailToGravatar = async (email: string) => {
const sha256 = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(email)
new TextEncoder().encode(email),
);
return `https://www.gravatar.com/avatar/${Array.from(new Uint8Array(sha256))
@ -234,7 +234,7 @@ const submit = handleSubmit(async (values) => {
} else if ((values as { email: string }).email) {
emit(
"submitUrl",
await emailToGravatar((values as { email: string }).email)
await emailToGravatar((values as { email: string }).email),
);
}

View file

@ -1,5 +1,5 @@
<template>
<Avatar :shape="(shape.value as 'circle' | 'square')">
<Avatar :shape="(shape.value as 'circle' | 'square')" :size="size">
<AvatarFallback v-if="name">
{{ getInitials(name) }}
</AvatarFallback>
@ -11,9 +11,10 @@
import { SettingIds } from "~/settings";
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
const { name } = defineProps<{
const { name, size = "base" } = defineProps<{
src?: string;
name?: string;
size?: "base" | "sm" | "lg";
}>();
/**

View 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>

View file

@ -1,12 +1,26 @@
<template>
<CardHeader class="p-0 relative">
<CardHeader class="relative w-full">
<div class="bg-muted rounded overflow-hidden h-48 md:h-72 w-full">
<img :src="header" alt="" class="object-cover w-full h-full" />
<img
v-if="header"
:src="header"
alt=""
class="object-cover w-full h-full"
/>
<!-- Shadow overlay at the bottom -->
<div class="absolute bottom-0 w-full h-1/3 bg-gradient-to-b from-black/0 to-black/40"></div>
<div
class="absolute bottom-0 w-full h-1/3 bg-gradient-to-b from-black/0 to-black/40"
></div>
</div>
<div class="absolute bottom-0 translate-y-1/3 left-4 flex flex-row items-start gap-2">
<Avatar size="lg" class="border" :src="avatar" :name="displayName" />
<div
class="absolute bottom-0 translate-y-1/3 left-4 flex flex-row items-start gap-2"
>
<Avatar
size="lg"
class="border"
:src="avatar"
:name="displayName"
/>
</div>
</CardHeader>
</template>

View file

@ -1,13 +1,27 @@
<template>
<Card>
<ProfileHeader :header="account.header" :avatar="account.avatar" :display-name="account.display_name" />
<CardContent class="pt-3 gap-4 flex flex-col">
<Card class="*:w-full">
<ProfileHeader
:header="account.header"
:avatar="account.avatar"
:display-name="account.display_name"
/>
<CardContent>
<div class="flex flex-row justify-end gap-2">
<Button variant="secondary" :disabled="isLoading || relationship?.requested" v-if="!isMe && identity"
@click="relationship?.following ? unfollow() : follow()">
<Button
variant="secondary"
:disabled="isLoading || relationship?.requested"
v-if="!isMe && identity"
@click="relationship?.following ? unfollow() : follow()"
>
<Loader v-if="isLoading" class="animate-spin" />
<span v-else>
{{ relationship?.following ? m.brief_upper_otter_cuddle() : relationship?.requested ? m.weak_bright_larva_grasp() : m.lazy_major_loris_grasp() }}
{{
relationship?.following
? m.brief_upper_otter_cuddle()
: relationship?.requested
? m.weak_bright_larva_grasp()
: m.lazy_major_loris_grasp()
}}
</span>
</Button>
<ProfileActions :account="account">
@ -22,27 +36,31 @@
</CardTitle>
<CopyableText :text="account.acct">
<span
class="font-semibold bg-gradient-to-tr from-pink-700 dark:from-indigo-400 via-purple-700 dark:via-purple-400 to-indigo-700 dark:to-indigo-400 text-transparent bg-clip-text">
class="font-semibold bg-gradient-to-tr from-pink-700 dark:from-indigo-400 via-purple-700 dark:via-purple-400 to-indigo-700 dark:to-indigo-400 text-transparent bg-clip-text"
>
@{{ username }}
</span>
<span class="text-muted-foreground">{{ instance && "@" }}{{ instance }}</span>
<span class="text-muted-foreground"
>{{ instance && "@" }}{{ instance }}</span
>
</CopyableText>
</div>
<div class="flex flex-row flex-wrap gap-2 -mx-2" v-if="isDeveloper || account.bot || roles.length > 0">
<ProfileBadge v-if="isDeveloper" :name="m.nice_bad_grizzly_coax()" :description="m.honest_jolly_shell_blend()"
:verified="true" />
<ProfileBadge v-if="account.bot" :name="m.merry_red_shrimp_bump()"
:description="m.sweet_mad_jannes_create()" />
<ProfileBadge v-for="role in roles" :key="role.id" :name="role.name" :description="role.description"
:icon="role.icon" />
</div>
<ProfileBadges :account="account" />
<ProfileContent :content="account.note" :emojis="account.emojis" />
</CardContent>
<CardFooter class="flex-col items-start gap-4">
<ProfileStats :creation-date="new Date(account.created_at || 0)" :follower-count="account.followers_count"
:following-count="account.following_count" :note-count="account.statuses_count" />
<CardFooter>
<ProfileStats
:creation-date="new Date(account.created_at || 0)"
:follower-count="account.followers_count"
:following-count="account.following_count"
:note-count="account.statuses_count"
/>
<Separator v-if="account.fields.length > 0" />
<ProfileFields v-if="account.fields.length > 0" :fields="account.fields" :emojis="account.emojis" />
<ProfileFields
v-if="account.fields.length > 0"
:fields="account.fields"
:emojis="account.emojis"
/>
</CardFooter>
</Card>
</template>
@ -59,7 +77,7 @@ import * as m from "~/paraglide/messages.js";
import { SettingIds } from "~/settings";
import { confirmModalService } from "../modals/composable";
import ProfileActions from "./profile-actions.vue";
import ProfileBadge from "./profile-badge.vue";
import ProfileBadges from "./profile-badges.vue";
import ProfileContent from "./profile-content.vue";
import ProfileFields from "./profile-fields.vue";
import ProfileHeader from "./profile-header.vue";
@ -69,16 +87,9 @@ const { account } = defineProps<{
account: Account;
}>();
const config = useConfig();
const { relationship, isLoading } = useRelationship(client, account.id);
const isMe = identity.value?.account.id === account.id;
const [username, instance] = account.acct.split("@");
const roles = account.roles.filter((r) => r.visible);
// Get user handle in username@instance format
const handle = account.acct.includes("@")
? account.acct
: `${account.acct}@${identity.value?.instance.domain ?? window.location.host}`;
const isDeveloper = config.DEVELOPER_HANDLES.includes(handle);
const confirmFollows = useSetting(SettingIds.ConfirmFollow);

View file

@ -1,12 +1,25 @@
<template>
<div class="relative">
<div class="bg-muted rounded overflow-hidden h-32 w-full">
<img :src="account.header" alt="" class="object-cover w-full h-full" />
<img
:src="account.header"
alt=""
class="object-cover w-full h-full"
/>
<!-- Shadow overlay at the bottom -->
<div class="absolute bottom-0 w-full h-1/3 bg-gradient-to-b from-black/0 to-black/40"></div>
<div
class="absolute bottom-0 w-full h-1/3 bg-gradient-to-b from-black/0 to-black/40"
></div>
</div>
<div class="absolute bottom-0 left-1/2 translate-y-1/3 -translate-x-1/2 flex flex-row items-start gap-2">
<Avatar size="base" class="border" :src="account.avatar" :name="account.display_name" />
<div
class="absolute bottom-0 left-1/2 translate-y-1/3 -translate-x-1/2 flex flex-row items-start gap-2"
>
<Avatar
size="base"
class="border"
:src="account.avatar"
:name="account.display_name"
/>
</div>
</div>
<div class="flex flex-col justify-center items-center mt-8">
@ -15,19 +28,32 @@
</span>
<CopyableText :text="account.acct" class="text-sm">
<span
class="font-semibold bg-gradient-to-tr from-pink-700 dark:from-indigo-400 via-purple-700 dark:via-purple-400 to-indigo-700 dark:to-indigo-400 text-transparent bg-clip-text">
class="font-semibold bg-gradient-to-tr from-pink-700 dark:from-indigo-400 via-purple-700 dark:via-purple-400 to-indigo-700 dark:to-indigo-400 text-transparent bg-clip-text"
>
@{{ username }}
</span>
<span class="text-muted-foreground">{{ instance && "@" }}{{ instance }}</span>
<span class="text-muted-foreground"
>{{ instance && "@" }}{{ instance }}</span
>
</CopyableText>
</div>
<ProfileContent :content="account.note" :emojis="account.emojis" class="mt-4 max-h-72 overflow-y-auto" />
<ProfileContent
:content="account.note"
:emojis="account.emojis"
class="mt-4 max-h-72 overflow-y-auto"
/>
<Separator v-if="account.fields.length > 0" class="mt-4" />
<ProfileFields v-if="account.fields.length > 0" :fields="account.fields" :emojis="account.emojis" class="mt-4 max-h-48 overflow-y-auto" />
<ProfileFields
v-if="account.fields.length > 0"
:fields="account.fields"
:emojis="account.emojis"
class="mt-4 max-h-48 overflow-y-auto"
/>
</template>
<script lang="ts" setup>
import type { Account } from "@versia/client/types";
import { Separator } from "~/components/ui/separator";
import CopyableText from "../notes/copyable-text.vue";
import Avatar from "./avatar.vue";
import ProfileContent from "./profile-content.vue";

View 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>

View file

@ -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>

View 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>

View 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>

View 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>

View file

@ -1,209 +1,43 @@
<template>
<Sidebar collapsible="none" class="hidden md:flex">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<NuxtLink href="/">
<SidebarMenuButton size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
<Avatar class="size-8" :src="instance?.thumbnail.url ??
'https://cdn.versia.pub/branding/icon.svg'
" :name="instance?.title" />
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ instance?.title ?? m.short_zippy_felix_kick()
}}</span>
<span class="truncate text-xs">{{ instance?.description ?? m.top_active_ocelot_cure() }}</span>
</div>
<!-- <ChevronsUpDown class="ml-auto" /> -->
</SidebarMenuButton>
</NuxtLink>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<Sidebar collapsible="offcanvas">
<InstanceHeader />
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>{{ m.trite_real_sawfish_drum() }}</SidebarGroupLabel>
<SidebarMenu>
<SidebarMenuItem v-for="item in data.other.filter(
i => i.requiresLogin ? !!identity : true,
)" :key="item.name">
<SidebarMenuButton as-child>
<NuxtLink :href="item.url">
<component :is="item.icon" />
<span>{{ item.name }}</span>
</NuxtLink>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
<SidebarGroupLabel>{{
m.trite_real_sawfish_drum()
}}</SidebarGroupLabel>
<NavItems
:items="
sidebarConfig.other.filter((i) =>
i.requiresLogin ? !!identity : true
)
"
/>
</SidebarGroup>
<SidebarGroup v-if="identity" class="mt-auto">
<SidebarGroupLabel>{{ m.close_short_kitten_coax() }}</SidebarGroupLabel>
<SidebarMenu>
<Collapsible v-for="item in data.navMain" :key="item.title" as-child class="group/collapsible">
<SidebarMenuItem>
<CollapsibleTrigger as-child>
<SidebarMenuButton :tooltip="item.title">
<component :is="item.icon" />
<span>{{ item.title }}</span>
<ChevronRight
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
<SidebarMenuSubItem v-for="subItem in item.items" :key="subItem.title">
<SidebarMenuSubButton as-child>
<NuxtLink :href="subItem.url">
<span>{{ subItem.title }}</span>
</NuxtLink>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
</SidebarMenu>
<SidebarGroupLabel>{{
m.close_short_kitten_coax()
}}</SidebarGroupLabel>
<NavGroup :items="sidebarConfig.navMain" />
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu class="gap-3">
<SidebarMenuItem>
<AccountSwitcher>
<SidebarMenuButton size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
<Avatar v-if="identity" class="size-8" :src="identity.account.avatar"
:name="identity.account.display_name" />
<Avatar v-else class="size-8" name="AB" />
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold" v-render-emojis="identity?.account.emojis">{{
identity?.account.display_name ?? "Not signed in"
}}</span>
<span class="truncate text-xs" v-if="identity">@{{ identity?.account.acct }}</span>
</div>
<ChevronsUpDown class="ml-auto size-4" />
</SidebarMenuButton>
</AccountSwitcher>
</SidebarMenuItem>
<SidebarMenuItem class="flex flex-col gap-2">
<Button variant="default" size="lg" class="w-full group-data-[collapsible=icon]:px-4"
v-if="identity" @click="useEvent('composer:open')">
<Pen />
<span class="group-data-[collapsible=icon]:hidden">{{ m.salty_aloof_turkey_nudge() }}</span>
</Button>
<Button variant="destructive" size="lg" class="w-full group-data-[collapsible=icon]:px-4"
v-if="$pwa?.needRefresh" @click="$pwa?.updateServiceWorker(true)">
<DownloadCloud />
<span class="group-data-[collapsible=icon]:hidden">{{ m.quaint_low_felix_pave() }}</span>
</Button>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
<FooterActions />
<SidebarRail />
</Sidebar>
</template>
<script lang="ts" setup>
import {
BedSingle,
Bell,
ChevronRight,
ChevronsUpDown,
DownloadCloud,
Globe,
House,
MapIcon,
Pen,
Settings2,
} from "lucide-vue-next";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "~/components/ui/collapsible";
import { sidebarConfig } from "~/components/sidebars/sidebar";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarRail,
} from "~/components/ui/sidebar";
import * as m from "~/paraglide/messages.js";
import Avatar from "../profiles/avatar.vue";
import { Button } from "../ui/button";
import AccountSwitcher from "./account-switcher.vue";
const data = {
navMain: [
{
title: m.patchy_seemly_hound_grace(),
url: "/preferences",
icon: Settings2,
items: [
{
title: m.factual_arable_jurgen_endure(),
url: "/preferences/account",
},
{
title: m.tough_clean_wolf_gleam(),
url: "/preferences/appearance",
},
{
title: m.legal_best_tadpole_rise(),
url: "/preferences/behaviour",
},
{
title: m.novel_trite_sloth_adapt(),
url: "/preferences/emojis",
},
{
title: m.safe_green_mink_cook(),
url: "/preferences/roles",
},
],
},
],
other: [
{
name: m.bland_chunky_sparrow_propel(),
url: "/home",
icon: House,
requiresLogin: true,
},
{
name: m.lost_trick_dog_grace(),
url: "/public",
icon: MapIcon,
requiresLogin: false,
},
{
name: m.crazy_game_parrot_pave(),
url: "/local",
icon: BedSingle,
requiresLogin: false,
},
{
name: m.real_tame_moose_greet(),
url: "/global",
icon: Globe,
requiresLogin: false,
},
{
name: m.that_patchy_mare_snip(),
url: "/notifications",
icon: Bell,
requiresLogin: true,
},
],
};
const instance = useInstance();
const { $pwa } = useNuxtApp();
import FooterActions from "./footer/footer-actions.vue";
import InstanceHeader from "./instance/instance-header.vue";
import NavGroup from "./navigation/nav-group.vue";
import NavItems from "./navigation/nav-items.vue";
</script>

View 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>

View 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>

View file

@ -1,13 +1,17 @@
<template>
<Sidebar side="right" collapsible="none" class="w-96 hidden lg:flex">
<SidebarContent class="p-2 overflow-y-auto">
<Sidebar
side="right"
collapsible="none"
class="hidden md:flex"
style="--sidebar-width: 24rem; --sidebar-width-mobile: 18rem"
>
<SidebarContent class="overflow-y-auto *:p-2 *:gap-2">
<NotificationsTimeline />
</SidebarContent>
<SidebarRail />
</Sidebar>
</template>
<script lang="ts" setup>
import NotificationsTimeline from "../timelines/notifications.vue";
import { Sidebar, SidebarContent, SidebarRail } from "../ui/sidebar";
<script setup lang="ts">
import NotificationsTimeline from "~/components/timelines/notifications.vue";
import { Sidebar, SidebarContent } from "~/components/ui/sidebar";
</script>

View 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,
},
],
};

View file

@ -1,8 +1,8 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import { SidebarInset, SidebarProvider } from "~/components/ui/sidebar";
import Timelines from "~/components/navigation/timelines.vue";
import { SidebarInset } from "~/components/ui/sidebar";
import { SettingIds } from "~/settings";
import Timelines from "../navigation/timelines.vue";
import LeftSidebar from "./left-sidebar.vue";
import RightSidebar from "./right-sidebar.vue";
@ -18,14 +18,15 @@ const showTimelines = computed(
</script>
<template>
<SidebarProvider>
<LeftSidebar />
<SidebarInset :class="cn('relative overflow-y-auto overflow-x-hidden', !isMd && 'pt-4')">
<header v-if="showTimelines" class="flex h-16 items-center bg-background/80 backdrop-blur-2xl sticky top-0 inset-x-0 z-10 p-4">
<main class="grow h-dvh overflow-y-auto">
<header
v-if="showTimelines"
class="flex h-16 items-center bg-background/80 backdrop-blur-2xl sticky top-0 inset-x-0 z-10 p-4"
>
<Timelines />
</header>
<slot />
</SidebarInset>
</main>
<RightSidebar v-if="identity" v-show="showRightSidebar.value" />
</SidebarProvider>
</template>

View file

@ -1,59 +1,52 @@
<!-- Timeline.vue -->
<template>
<TransitionGroup name="timeline-item" tag="div" class="timeline-items first:*:rounded-t divide-y divide-border *:border-x first:*:border-t *:overflow-hidden last:*:rounded-b last:*:!border-b *:shadow-none">
<TimelineItem :type="type" v-for="item in items" :key="item.id" :item="item" @update="updateItem"
@delete="removeItem" />
</TransitionGroup>
<div
role="status"
class="flex flex-col gap-4 items-center *:max-w-2xl *:w-full p-4"
>
<TimelineItem
:type="type"
v-for="item in items"
:key="item.id"
:item="item"
@update="updateItem"
@delete="removeItem"
/>
<div v-if="isLoading" class="p-4 flex items-center justify-center h-48">
<Loader class="size-8 animate-spin" />
</div>
<Spinner v-if="isLoading" />
<div v-if="error" class="timeline-error">
{{ error.message }}
</div>
<!-- If there are some posts, but the user scrolled to the end -->
<Card v-if="hasReachedEnd && items.length > 0" class="shadow-none bg-transparent border-none p-4">
<CardHeader class="text-center gap-y-4">
<CardTitle>{{ m.steep_suave_fish_snap() }}</CardTitle>
<CardDescription>
{{ m.muddy_bland_shark_accept() }}
</CardDescription>
</CardHeader>
</Card>
<ReachedEnd v-if="hasReachedEnd && items.length > 0" />
<!-- If there are no posts at all -->
<Card v-else-if="hasReachedEnd && items.length === 0" class="shadow-none bg-transparent border-none p-4">
<CardHeader class="text-center gap-y-4">
<CardTitle>{{ m.fine_arable_lemming_fold() }}</CardTitle>
<CardDescription>
{{ m.petty_honest_fish_stir() }}
</CardDescription>
</CardHeader>
</Card>
<NoPosts v-else-if="hasReachedEnd && items.length === 0" />
<div v-else-if="!infiniteScroll.value" class="py-10 px-4">
<Button variant="secondary" @click="loadNext" :disabled="isLoading" class="w-full">
<Button
variant="secondary"
@click="loadNext"
:disabled="isLoading"
class="w-full"
>
{{ m.gaudy_bland_gorilla_talk() }}
</Button>
</div>
<div v-else ref="loadMoreTrigger" class="h-20"></div>
</div>
</template>
<script lang="ts" setup>
import type { Notification, Status } from "@versia/client/types";
import { useIntersectionObserver } from "@vueuse/core";
import { Loader } from "lucide-vue-next";
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import * as m from "~/paraglide/messages.js";
import { SettingIds } from "~/settings";
import NoPosts from "../errors/NoPosts.vue";
import ReachedEnd from "../errors/ReachedEnd.vue";
import Spinner from "../graphics/spinner.vue";
import { Button } from "../ui/button";
import TimelineItem from "./timeline-item.vue";
@ -92,16 +85,3 @@ onMounted(() => {
props.loadNext();
});
</script>
<style scoped>
.timeline-item-enter-active,
.timeline-item-leave-active {
transition: all 0.5s ease;
}
.timeline-item-enter-from,
.timeline-item-leave-to {
opacity: 0;
scale: 0.99;
}
</style>

View file

@ -4,7 +4,7 @@ import {
type AlertDialogProps,
AlertDialogRoot,
useForwardPropsEmits,
} from "radix-vue";
} from "reka-ui";
const props = defineProps<AlertDialogProps>();
const emits = defineEmits<AlertDialogEmits>();

View file

@ -1,8 +1,8 @@
<script setup lang="ts">
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { AlertDialogAction, type AlertDialogActionProps } from "radix-vue";
import { AlertDialogAction, type AlertDialogActionProps } from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
import { buttonVariants } from "~/components/ui/button";
const props = defineProps<
AlertDialogActionProps & { class?: HTMLAttributes["class"] }

View file

@ -1,8 +1,8 @@
<script setup lang="ts">
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { AlertDialogCancel, type AlertDialogCancelProps } from "radix-vue";
import { AlertDialogCancel, type AlertDialogCancelProps } from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
import { buttonVariants } from "~/components/ui/button";
const props = defineProps<
AlertDialogCancelProps & { class?: HTMLAttributes["class"] }

View file

@ -7,7 +7,7 @@ import {
AlertDialogOverlay,
AlertDialogPortal,
useForwardPropsEmits,
} from "radix-vue";
} from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
const props = defineProps<
@ -33,7 +33,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
v-bind="forwarded"
:class="
cn(
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded',
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
props.class,
)
"

View file

@ -3,7 +3,7 @@ import { cn } from "@/lib/utils";
import {
AlertDialogDescription,
type AlertDialogDescriptionProps,
} from "radix-vue";
} from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
const props = defineProps<

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import { AlertDialogTitle, type AlertDialogTitleProps } from "radix-vue";
import { AlertDialogTitle, type AlertDialogTitleProps } from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
const props = defineProps<

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { AlertDialogTrigger, type AlertDialogTriggerProps } from "radix-vue";
import { AlertDialogTrigger, type AlertDialogTriggerProps } from "reka-ui";
const props = defineProps<AlertDialogTriggerProps>();
</script>

View file

@ -5,7 +5,7 @@ export { default as AlertDescription } from "./AlertDescription.vue";
export { default as AlertTitle } from "./AlertTitle.vue";
export const alertVariants = cva(
"relative w-full rounded border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg]:size-4",
{
variants: {
variant: {

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import { AvatarRoot } from "radix-vue";
import { AvatarRoot } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { type AvatarVariants, avatarVariant } from ".";

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { AvatarFallback, type AvatarFallbackProps } from "radix-vue";
import { AvatarFallback, type AvatarFallbackProps } from "reka-ui";
const props = defineProps<AvatarFallbackProps>();
</script>

View file

@ -1,9 +1,12 @@
<script setup lang="ts">
import { AvatarImage, type AvatarImageProps } from "radix-vue";
import type { AvatarImageProps } from "reka-ui";
import { AvatarImage } from "reka-ui";
const props = defineProps<AvatarImageProps>();
</script>
<template>
<AvatarImage v-bind="props" class="h-full w-full object-cover" />
<AvatarImage v-bind="props" class="h-full w-full object-cover">
<slot />
</AvatarImage>
</template>

View file

@ -15,7 +15,7 @@ export const avatarVariant = cva(
},
shape: {
circle: "rounded-full",
square: "rounded",
square: "rounded-md",
},
},
},

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import { Primitive, type PrimitiveProps } from "radix-vue";
import { Primitive, type PrimitiveProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { type ButtonVariants, buttonVariants } from ".";
@ -20,7 +20,6 @@ const props = withDefaults(defineProps<Props>(), {
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
data-component="button"
>
<slot />
</Primitive>

View file

@ -1,4 +1,6 @@
import { type VariantProps, cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
export { default as Button } from "./Button.vue";
export const buttonVariants = cva(
@ -7,21 +9,21 @@ export const buttonVariants = cva(
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
"bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 px-3 text-xs",
lg: "h-10 px-8",
icon: "size-9",
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {

View file

@ -1,22 +1,21 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import { Primitive, type PrimitiveProps } from "radix-vue";
import type { HTMLAttributes } from "vue";
const props = withDefaults(
defineProps<PrimitiveProps & { class?: HTMLAttributes["class"] }>(),
{
as: "div",
},
);
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<Primitive :as="props.as" :as-child="props.asChild" :class="cn(
'rounded-lg border bg-card/90 backdrop-blur-xl text-card-foreground shadow-sm',
props.class,
<div
:class="
cn(
'rounded-lg border bg-card text-card-foreground shadow-sm flex flex-col gap-6 p-4 items-center justify-center',
props.class
)
" data-component="card">
"
>
<slot />
</Primitive>
</div>
</template>

View file

@ -8,7 +8,7 @@ const props = defineProps<{
</script>
<template>
<div :class="cn('p-6 pt-0', props.class)">
<div :class="cn('flex flex-col gap-2', props.class)">
<slot />
</div>
</template>

View file

@ -8,7 +8,7 @@ const props = defineProps<{
</script>
<template>
<div :class="cn('flex items-center p-6 pt-0', props.class)">
<div :class="cn('flex items-center', props.class)">
<slot />
</div>
</template>

View file

@ -1,18 +1,14 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import { Primitive, type PrimitiveProps } from "radix-vue";
import type { HTMLAttributes } from "vue";
const props = withDefaults(
defineProps<PrimitiveProps & { class?: HTMLAttributes["class"] }>(),
{
as: "div",
},
);
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<Primitive :as="props.as" :as-child="props.asChild" :class="cn('flex flex-col gap-y-1.5 p-6', props.class)">
<div :class="cn('flex flex-col gap-y-1.5', props.class)">
<slot />
</Primitive>
</div>
</template>

View file

@ -1,12 +1,8 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import { Check } from "lucide-vue-next";
import type { CheckboxRootEmits, CheckboxRootProps } from "radix-vue";
import {
CheckboxIndicator,
CheckboxRoot,
useForwardPropsEmits,
} from "radix-vue";
import type { CheckboxRootEmits, CheckboxRootProps } from "reka-ui";
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
const props = defineProps<
@ -24,11 +20,15 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<CheckboxRoot v-bind="forwarded" :class="cn('peer size-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
props.class)">
<CheckboxRoot
v-bind="forwarded"
:class="
cn('peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
props.class)"
>
<CheckboxIndicator class="flex h-full w-full items-center justify-center text-current">
<slot>
<Check class="size-4" />
<Check class="h-4 w-4" />
</slot>
</CheckboxIndicator>
</CheckboxRoot>

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { CollapsibleRootEmits, CollapsibleRootProps } from "radix-vue";
import { CollapsibleRoot, useForwardPropsEmits } from "radix-vue";
import type { CollapsibleRootEmits, CollapsibleRootProps } from "reka-ui";
import { CollapsibleRoot, useForwardPropsEmits } from "reka-ui";
const props = defineProps<CollapsibleRootProps>();
const emits = defineEmits<CollapsibleRootEmits>();

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { CollapsibleContent, type CollapsibleContentProps } from "radix-vue";
import { CollapsibleContent, type CollapsibleContentProps } from "reka-ui";
const props = defineProps<CollapsibleContentProps>();
</script>

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { CollapsibleTrigger, type CollapsibleTriggerProps } from "radix-vue";
import { CollapsibleTrigger, type CollapsibleTriggerProps } from "reka-ui";
const props = defineProps<CollapsibleTriggerProps>();
</script>

View file

@ -1,18 +1,18 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import type { ComboboxRootEmits, ComboboxRootProps } from "radix-vue";
import { ComboboxRoot, useForwardPropsEmits } from "radix-vue";
import { type HTMLAttributes, computed } from "vue";
import type { ListboxRootEmits, ListboxRootProps } from "reka-ui";
import { ListboxRoot, useFilter, useForwardPropsEmits } from "reka-ui";
import { type HTMLAttributes, computed, reactive, ref, watch } from "vue";
import { provideCommandContext } from ".";
const props = withDefaults(
defineProps<ComboboxRootProps & { class?: HTMLAttributes["class"] }>(),
defineProps<ListboxRootProps & { class?: HTMLAttributes["class"] }>(),
{
open: true,
modelValue: "",
},
);
const emits = defineEmits<ComboboxRootEmits>();
const emits = defineEmits<ListboxRootEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
@ -21,11 +21,84 @@ const delegatedProps = computed(() => {
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const allItems = ref<Map<string, string>>(new Map());
const allGroups = ref<Map<string, Set<string>>>(new Map());
const { contains } = useFilter({ sensitivity: "base" });
const filterState = reactive({
search: "",
filtered: {
/** The count of all visible items. */
count: 0,
/** Map from visible item id to its search score. */
items: new Map() as Map<string, number>,
/** Set of groups with at least one visible item. */
groups: new Set() as Set<string>,
},
});
function filterItems() {
if (!filterState.search) {
filterState.filtered.count = allItems.value.size;
// Do nothing, each item will know to show itself because search is empty
return;
}
// Reset the groups
filterState.filtered.groups = new Set();
let itemCount = 0;
// Check which items should be included
for (const [id, value] of allItems.value) {
const score = contains(value, filterState.search);
filterState.filtered.items.set(id, score ? 1 : 0);
if (score) {
itemCount++;
}
}
// Check which groups have at least 1 item shown
for (const [groupId, group] of allGroups.value) {
for (const itemId of group) {
if (filterState.filtered.items.get(itemId)) {
filterState.filtered.groups.add(groupId);
break;
}
}
}
filterState.filtered.count = itemCount;
}
function handleSelect() {
filterState.search = "";
}
watch(
() => filterState.search,
() => {
filterItems();
},
);
provideCommandContext({
allItems,
allGroups,
filterState,
});
</script>
<template>
<ComboboxRoot v-bind="forwarded"
:class="cn('flex h-full w-full flex-col overflow-hidden rounded bg-popover text-popover-foreground', props.class)">
<ListboxRoot
v-bind="forwarded"
:class="
cn(
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
props.class
)
"
>
<slot />
</ComboboxRoot>
</ListboxRoot>
</template>

View file

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { DialogRootEmits, DialogRootProps } from "radix-vue";
import { useForwardPropsEmits } from "radix-vue";
import { Dialog, DialogContent } from "~/components/ui/dialog";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import type { DialogRootEmits, DialogRootProps } from "reka-ui";
import { useForwardPropsEmits } from "reka-ui";
import Command from "./Command.vue";
const props = defineProps<DialogRootProps>();

View file

@ -1,11 +1,12 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import type { ComboboxEmptyProps } from "radix-vue";
import { ComboboxEmpty } from "radix-vue";
import type { PrimitiveProps } from "reka-ui";
import { Primitive } from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
import { useCommand } from ".";
const props = defineProps<
ComboboxEmptyProps & { class?: HTMLAttributes["class"] }
PrimitiveProps & { class?: HTMLAttributes["class"] }
>();
const delegatedProps = computed(() => {
@ -13,10 +14,15 @@ const delegatedProps = computed(() => {
return delegated;
});
const { filterState } = useCommand();
const isRender = computed(
() => !!filterState.search && filterState.filtered.count === 0,
);
</script>
<template>
<ComboboxEmpty v-bind="delegatedProps" :class="cn('py-6 text-center text-sm', props.class)">
<Primitive v-if="isRender" v-bind="delegatedProps" :class="cn('py-6 text-center text-sm', props.class)">
<slot />
</ComboboxEmpty>
</Primitive>
</template>

View file

@ -1,11 +1,12 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import type { ComboboxGroupProps } from "radix-vue";
import { ComboboxGroup, ComboboxLabel } from "radix-vue";
import { type HTMLAttributes, computed } from "vue";
import type { ListboxGroupProps } from "reka-ui";
import { ListboxGroup, ListboxGroupLabel, useId } from "reka-ui";
import { type HTMLAttributes, computed, onMounted, onUnmounted } from "vue";
import { provideCommandGroupContext, useCommand } from ".";
const props = defineProps<
ComboboxGroupProps & {
ListboxGroupProps & {
class?: HTMLAttributes["class"];
heading?: string;
}
@ -16,16 +17,43 @@ const delegatedProps = computed(() => {
return delegated;
});
const { allGroups, filterState } = useCommand();
const id = useId();
const isRender = computed(() =>
filterState.search ? filterState.filtered.groups.has(id) : true,
);
provideCommandGroupContext({ id });
onMounted(() => {
if (!allGroups.value.has(id)) {
allGroups.value.set(id, new Set());
}
});
onUnmounted(() => {
allGroups.value.delete(id);
});
</script>
<template>
<ComboboxGroup
<ListboxGroup
v-bind="delegatedProps"
:class="cn('overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground', props.class)"
:id="id"
:class="
cn(
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
props.class
)
"
:hidden="isRender ? undefined : true"
>
<ListboxGroupLabel
v-if="heading"
class="px-2 py-1.5 text-xs font-medium text-muted-foreground"
>
<ComboboxLabel v-if="heading" class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
{{ heading }}
</ComboboxLabel>
</ListboxGroupLabel>
<slot />
</ComboboxGroup>
</ListboxGroup>
</template>

View file

@ -2,18 +2,19 @@
import { cn } from "@/lib/utils";
import { Search } from "lucide-vue-next";
import {
ComboboxInput,
type ComboboxInputProps,
ListboxFilter,
type ListboxFilterProps,
useForwardProps,
} from "radix-vue";
} from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
import { useCommand } from ".";
defineOptions({
inheritAttrs: false,
});
const props = defineProps<
ComboboxInputProps & {
ListboxFilterProps & {
class?: HTMLAttributes["class"];
}
>();
@ -25,12 +26,18 @@ const delegatedProps = computed(() => {
});
const forwardedProps = useForwardProps(delegatedProps);
const { filterState } = useCommand();
</script>
<template>
<div class="flex items-center border-b px-3" cmdk-input-wrapper>
<Search class="mr-2 size-4 shrink-0 opacity-50" />
<ComboboxInput v-bind="{ ...forwardedProps, ...$attrs }" auto-focus
:class="cn('flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50', props.class)" />
<Search class="mr-2 h-4 w-4 shrink-0 opacity-50" />
<ListboxFilter
v-bind="{ ...forwardedProps, ...$attrs }"
v-model="filterState.search"
auto-focus
:class="cn('flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50', props.class)"
/>
</div>
</template>

View file

@ -1,13 +1,21 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import type { ComboboxItemEmits, ComboboxItemProps } from "radix-vue";
import { ComboboxItem, useForwardPropsEmits } from "radix-vue";
import { type HTMLAttributes, computed } from "vue";
import { useCurrentElement } from "@vueuse/core";
import type { ListboxItemEmits, ListboxItemProps } from "reka-ui";
import { ListboxItem, useForwardPropsEmits, useId } from "reka-ui";
import {
type HTMLAttributes,
computed,
onMounted,
onUnmounted,
ref,
} from "vue";
import { useCommand, useCommandGroup } from ".";
const props = defineProps<
ComboboxItemProps & { class?: HTMLAttributes["class"] }
ListboxItemProps & { class?: HTMLAttributes["class"] }
>();
const emits = defineEmits<ComboboxItemEmits>();
const emits = defineEmits<ListboxItemEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
@ -16,13 +24,72 @@ const delegatedProps = computed(() => {
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const id = useId();
const { filterState, allItems, allGroups } = useCommand();
const groupContext = useCommandGroup();
const isRender = computed(() => {
if (filterState.search) {
const filteredCurrentItem = filterState.filtered.items.get(id);
// If the filtered items is undefined means not in the all times map yet
// Do the first render to add into the map
if (filteredCurrentItem === undefined) {
return true;
}
// Check with filter
return filteredCurrentItem > 0;
}
return true;
});
const itemRef = ref();
const currentElement = useCurrentElement(itemRef);
onMounted(() => {
if (!(currentElement.value instanceof HTMLElement)) {
return;
}
// textValue to perform filter
allItems.value.set(
id,
currentElement.value.textContent ?? props.value?.toString() ?? "",
);
const groupId = groupContext?.id;
if (groupId) {
if (allGroups.value.has(groupId)) {
allGroups.value.get(groupId)?.add(id);
} else {
allGroups.value.set(groupId, new Set([id]));
}
}
});
onUnmounted(() => {
allItems.value.delete(id);
});
</script>
<template>
<ComboboxItem
<ListboxItem
v-if="isRender"
v-bind="forwarded"
:class="cn('relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', props.class)"
:id="id"
ref="itemRef"
:class="
cn(
'relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0',
props.class
)
"
@select="
() => {
filterState.search = '';
}
"
>
<slot />
</ComboboxItem>
</ListboxItem>
</template>

View file

@ -1,16 +1,12 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import type { ComboboxContentEmits, ComboboxContentProps } from "radix-vue";
import { ComboboxContent, useForwardPropsEmits } from "radix-vue";
import type { ListboxContentProps } from "reka-ui";
import { ListboxContent, useForwardProps } from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
const props = withDefaults(
defineProps<ComboboxContentProps & { class?: HTMLAttributes["class"] }>(),
{
dismissable: false,
},
);
const emits = defineEmits<ComboboxContentEmits>();
const props = defineProps<
ListboxContentProps & { class?: HTMLAttributes["class"] }
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
@ -18,13 +14,13 @@ const delegatedProps = computed(() => {
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const forwarded = useForwardProps(delegatedProps);
</script>
<template>
<ComboboxContent v-bind="forwarded" :class="cn('max-h-[300px] overflow-y-auto overflow-x-hidden', props.class)">
<ListboxContent v-bind="forwarded" :class="cn('max-h-[300px] overflow-y-auto overflow-x-hidden', props.class)">
<div role="presentation">
<slot />
</div>
</ComboboxContent>
</ListboxContent>
</template>

View file

@ -1,11 +1,11 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import type { ComboboxSeparatorProps } from "radix-vue";
import { ComboboxSeparator } from "radix-vue";
import type { SeparatorProps } from "reka-ui";
import { Separator } from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
const props = defineProps<
ComboboxSeparatorProps & { class?: HTMLAttributes["class"] }
SeparatorProps & { class?: HTMLAttributes["class"] }
>();
const delegatedProps = computed(() => {
@ -16,10 +16,10 @@ const delegatedProps = computed(() => {
</script>
<template>
<ComboboxSeparator
<Separator
v-bind="delegatedProps"
:class="cn('-mx-1 h-px bg-border', props.class)"
>
<slot />
</ComboboxSeparator>
</Separator>
</template>

View file

@ -1,3 +1,6 @@
import { createContext } from "reka-ui";
import type { Ref } from "vue";
export { default as Command } from "./Command.vue";
export { default as CommandDialog } from "./CommandDialog.vue";
export { default as CommandEmpty } from "./CommandEmpty.vue";
@ -7,3 +10,20 @@ export { default as CommandItem } from "./CommandItem.vue";
export { default as CommandList } from "./CommandList.vue";
export { default as CommandSeparator } from "./CommandSeparator.vue";
export { default as CommandShortcut } from "./CommandShortcut.vue";
export const [useCommand, provideCommandContext] = createContext<{
allItems: Ref<Map<string, string>>;
allGroups: Ref<Map<string, Set<string>>>;
filterState: {
search: string;
filtered: {
count: number;
items: Map<string, number>;
groups: Set<string>;
};
};
}>("Command");
export const [useCommandGroup, provideCommandGroupContext] = createContext<{
id?: string;
}>("CommandGroup");

View file

@ -4,7 +4,7 @@ import {
type DialogRootEmits,
type DialogRootProps,
useForwardPropsEmits,
} from "radix-vue";
} from "reka-ui";
const props = defineProps<DialogRootProps>();
const emits = defineEmits<DialogRootEmits>();

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { DialogClose, type DialogCloseProps } from "radix-vue";
import { DialogClose, type DialogCloseProps } from "reka-ui";
const props = defineProps<DialogCloseProps>();
</script>

View file

@ -9,7 +9,7 @@ import {
DialogOverlay,
DialogPortal,
useForwardPropsEmits,
} from "radix-vue";
} from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
const props = defineProps<
@ -32,16 +32,24 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<template>
<DialogPortal>
<DialogOverlay
class="fixed inset-0 z-50 bg-black/80 backdrop-blur data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
<DialogContent v-bind="forwarded" :class="cn(
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
/>
<DialogContent
v-bind="forwarded"
:class="
cn(
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
props.class,
)">
props.class
)
"
>
<slot />
<DialogClose v-if="!props.hideClose"
class="absolute right-4 top-4 rounded opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X class="size-4" />
<DialogClose
v-if="!hideClose"
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
>
<X class="w-4 h-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>

View file

@ -4,7 +4,7 @@ import {
DialogDescription,
type DialogDescriptionProps,
useForwardProps,
} from "radix-vue";
} from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
const props = defineProps<

View file

@ -9,7 +9,7 @@ const props = defineProps<{ class?: HTMLAttributes["class"] }>();
<div
:class="
cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2 gap-y-2',
'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2',
props.class,
)
"

View file

@ -9,7 +9,7 @@ import {
DialogOverlay,
DialogPortal,
useForwardPropsEmits,
} from "radix-vue";
} from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
const props = defineProps<
@ -52,7 +52,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<DialogClose
class="absolute top-3 right-3 p-0.5 transition-colors rounded-md hover:bg-secondary"
>
<X class="size-4" />
<X class="w-4 h-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import { DialogTitle, type DialogTitleProps, useForwardProps } from "radix-vue";
import { DialogTitle, type DialogTitleProps, useForwardProps } from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
const props = defineProps<

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { DialogTrigger, type DialogTriggerProps } from "radix-vue";
import { DialogTrigger, type DialogTriggerProps } from "reka-ui";
const props = defineProps<DialogTriggerProps>();
</script>

View file

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { useForwardPropsEmits } from "radix-vue";
import { useForwardPropsEmits } from "reka-ui";
import type { DrawerRootEmits, DrawerRootProps } from "vaul-vue";
import { DrawerRoot } from "vaul-vue";

View file

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { cn } from "@/lib/utils";
import type { DialogContentEmits, DialogContentProps } from "radix-vue";
import { useForwardPropsEmits } from "radix-vue";
import type { DialogContentEmits, DialogContentProps } from "reka-ui";
import { useForwardPropsEmits } from "reka-ui";
import { DrawerContent, DrawerPortal } from "vaul-vue";
import type { HtmlHTMLAttributes } from "vue";
import DrawerOverlay from "./DrawerOverlay.vue";

View file

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { cn } from "@/lib/utils";
import type { DialogOverlayProps } from "radix-vue";
import type { DialogOverlayProps } from "reka-ui";
import { DrawerOverlay } from "vaul-vue";
import { type HtmlHTMLAttributes, computed } from "vue";

View file

@ -4,7 +4,7 @@ import {
type DropdownMenuRootEmits,
type DropdownMenuRootProps,
useForwardPropsEmits,
} from "radix-vue";
} from "reka-ui";
const props = defineProps<DropdownMenuRootProps>();
const emits = defineEmits<DropdownMenuRootEmits>();

View file

@ -7,7 +7,7 @@ import {
type DropdownMenuCheckboxItemProps,
DropdownMenuItemIndicator,
useForwardPropsEmits,
} from "radix-vue";
} from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
const props = defineProps<
@ -25,13 +25,16 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuCheckboxItem v-bind="forwarded" :class="cn(
<DropdownMenuCheckboxItem
v-bind="forwarded"
:class=" cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class,
)">
)"
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<Check class="!mr-0" />
<Check class="w-4 h-4" />
</DropdownMenuItemIndicator>
</span>
<slot />

View file

@ -6,7 +6,7 @@ import {
type DropdownMenuContentProps,
DropdownMenuPortal,
useForwardPropsEmits,
} from "radix-vue";
} from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
const props = withDefaults(

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { DropdownMenuGroup, type DropdownMenuGroupProps } from "radix-vue";
import { DropdownMenuGroup, type DropdownMenuGroupProps } from "reka-ui";
const props = defineProps<DropdownMenuGroupProps>();
</script>

View file

@ -4,7 +4,7 @@ import {
DropdownMenuItem,
type DropdownMenuItemProps,
useForwardProps,
} from "radix-vue";
} from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
const props = defineProps<
@ -21,11 +21,16 @@ const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DropdownMenuItem v-bind="forwardedProps" :class="cn(
'relative flex cursor-default select-none items-center rounded-sm gap-2 px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:mr-0.5 w-full',
<DropdownMenuItem
v-bind="forwardedProps"
:class="
cn(
'relative flex w-full cursor-default select-none items-center rounded-sm gap-2 px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
inset && 'pl-8',
props.class,
)">
props.class
)
"
>
<slot />
</DropdownMenuItem>
</template>

View file

@ -4,7 +4,7 @@ import {
DropdownMenuLabel,
type DropdownMenuLabelProps,
useForwardProps,
} from "radix-vue";
} from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
const props = defineProps<
@ -24,8 +24,10 @@ const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DropdownMenuLabel v-bind="forwardedProps"
:class="cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)">
<DropdownMenuLabel
v-bind="forwardedProps"
:class="cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)"
>
<slot />
</DropdownMenuLabel>
</template>

View file

@ -4,7 +4,7 @@ import {
type DropdownMenuRadioGroupEmits,
type DropdownMenuRadioGroupProps,
useForwardPropsEmits,
} from "radix-vue";
} from "reka-ui";
const props = defineProps<DropdownMenuRadioGroupProps>();
const emits = defineEmits<DropdownMenuRadioGroupEmits>();

View file

@ -7,7 +7,7 @@ import {
type DropdownMenuRadioItemEmits,
type DropdownMenuRadioItemProps,
useForwardPropsEmits,
} from "radix-vue";
} from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
const props = defineProps<
@ -26,13 +26,16 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuRadioItem v-bind="forwarded" :class="cn(
<DropdownMenuRadioItem
v-bind="forwarded"
:class="cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class,
)">
)"
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<Circle class="size-2 fill-current" />
<Circle class="h-2 w-2 fill-current" />
</DropdownMenuItemIndicator>
</span>
<slot />

View file

@ -3,7 +3,7 @@ import { cn } from "@/lib/utils";
import {
DropdownMenuSeparator,
type DropdownMenuSeparatorProps,
} from "radix-vue";
} from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
const props = defineProps<

View file

@ -4,7 +4,7 @@ import {
type DropdownMenuSubEmits,
type DropdownMenuSubProps,
useForwardPropsEmits,
} from "radix-vue";
} from "reka-ui";
const props = defineProps<DropdownMenuSubProps>();
const emits = defineEmits<DropdownMenuSubEmits>();

View file

@ -5,7 +5,7 @@ import {
type DropdownMenuSubContentEmits,
type DropdownMenuSubContentProps,
useForwardPropsEmits,
} from "radix-vue";
} from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
const props = defineProps<

View file

@ -5,7 +5,7 @@ import {
DropdownMenuSubTrigger,
type DropdownMenuSubTriggerProps,
useForwardProps,
} from "radix-vue";
} from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
const props = defineProps<
@ -30,6 +30,6 @@ const forwardedProps = useForwardProps(delegatedProps);
)"
>
<slot />
<ChevronRight class="ml-auto size-4" />
<ChevronRight class="ml-auto h-4 w-4" />
</DropdownMenuSubTrigger>
</template>

View file

@ -3,7 +3,7 @@ import {
DropdownMenuTrigger,
type DropdownMenuTriggerProps,
useForwardProps,
} from "radix-vue";
} from "reka-ui";
const props = defineProps<DropdownMenuTriggerProps>();

View file

@ -13,4 +13,4 @@ export { default as DropdownMenuSub } from "./DropdownMenuSub.vue";
export { default as DropdownMenuSubContent } from "./DropdownMenuSubContent.vue";
export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue";
export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue";
export { DropdownMenuPortal } from "radix-vue";
export { DropdownMenuPortal } from "reka-ui";

View file

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { Slot } from "radix-vue";
import { Slot } from "reka-ui";
import { useFormField } from "./useFormField";
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();

View file

@ -1,26 +1,19 @@
<script lang="ts" setup>
import { cn } from "@/lib/utils";
import { Primitive, type PrimitiveProps, useId } from "radix-vue";
import { useId } from "reka-ui";
import { type HTMLAttributes, provide } from "vue";
import { FORM_ITEM_INJECTION_KEY } from "./injectionKeys";
const props = withDefaults(
defineProps<
PrimitiveProps & {
const props = defineProps<{
class?: HTMLAttributes["class"];
}
>(),
{
as: "div",
},
);
}>();
const id = useId();
provide(FORM_ITEM_INJECTION_KEY, id);
</script>
<template>
<Primitive :as="props.as" :as-child="props.asChild" :class="cn('space-y-2', props.class)">
<div :class="cn('space-y-2', props.class)">
<slot />
</Primitive>
</div>
</template>

View file

@ -1,8 +1,8 @@
<script lang="ts" setup>
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import type { LabelProps } from "radix-vue";
import type { LabelProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { Label } from "~/components/ui/label";
import { useFormField } from "./useFormField";
const props = defineProps<LabelProps & { class?: HTMLAttributes["class"] }>();

View file

@ -4,4 +4,4 @@ export { default as FormItem } from "./FormItem.vue";
export { default as FormLabel } from "./FormLabel.vue";
export { default as FormMessage } from "./FormMessage.vue";
export { FORM_ITEM_INJECTION_KEY } from "./injectionKeys";
export { Field as FormField, Form } from "vee-validate";
export { Form, Field as FormField } from "vee-validate";

View file

@ -4,7 +4,7 @@ import {
type HoverCardRootEmits,
type HoverCardRootProps,
useForwardPropsEmits,
} from "radix-vue";
} from "reka-ui";
const props = defineProps<HoverCardRootProps>();
const emits = defineEmits<HoverCardRootEmits>();

View file

@ -5,7 +5,7 @@ import {
type HoverCardContentProps,
HoverCardPortal,
useForwardProps,
} from "radix-vue";
} from "reka-ui";
import { type HTMLAttributes, computed } from "vue";
const props = withDefaults(

Some files were not shown because too many files have changed in this diff Show more