chore: monorepo
This commit is contained in:
32
packages/ui/package.json
Normal file
32
packages/ui/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@lume/ui",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"dependencies": {
|
||||
"@lume/ark": "workspace:^",
|
||||
"@lume/icons": "workspace:^",
|
||||
"@lume/utils": "workspace:^",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-hover-card": "^1.0.7",
|
||||
"@tanstack/react-query": "^5.14.2",
|
||||
"@tauri-apps/api": "2.0.0-alpha.11",
|
||||
"@tauri-apps/plugin-http": "2.0.0-alpha.3",
|
||||
"@tauri-apps/plugin-os": "2.0.0-alpha.4",
|
||||
"minidenticons": "^4.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-router-dom": "^6.21.0",
|
||||
"sonner": "^1.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lume/tailwindcss": "workspace:^",
|
||||
"@lume/tsconfig": "workspace:^",
|
||||
"@lume/types": "workspace:^",
|
||||
"@types/react": "^18.2.45",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
49
packages/ui/src/account/active.tsx
Normal file
49
packages/ui/src/account/active.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useProfile, useStorage } from "@lume/ark";
|
||||
import { useNetworkStatus } from "@lume/utils";
|
||||
import * as Avatar from "@radix-ui/react-avatar";
|
||||
import { minidenticon } from "minidenticons";
|
||||
import { Link } from "react-router-dom";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { AccountMoreActions } from "./more";
|
||||
|
||||
export function ActiveAccount() {
|
||||
const storage = useStorage();
|
||||
const isOnline = useNetworkStatus();
|
||||
|
||||
const { user } = useProfile(storage.account.pubkey);
|
||||
|
||||
const svgURI = `data:image/svg+xml;utf8,${encodeURIComponent(
|
||||
minidenticon(storage.account.pubkey, 90, 50),
|
||||
)}`;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1 rounded-xl bg-black/10 p-1 ring-1 ring-transparent hover:bg-black/20 hover:ring-blue-500 dark:bg-white/10 dark:hover:bg-white/20">
|
||||
<Link to="/settings/" className="relative inline-block">
|
||||
<Avatar.Root>
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={storage.account.pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
style={{ contentVisibility: "auto" }}
|
||||
className="aspect-square h-auto w-full rounded-lg object-cover"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={150}>
|
||||
<img
|
||||
src={svgURI}
|
||||
alt={storage.account.pubkey}
|
||||
className="aspect-square h-auto w-full rounded-lg bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<span
|
||||
className={twMerge(
|
||||
"absolute bottom-0 right-0 block h-2 w-2 rounded-full ring-2 ring-neutral-100 dark:ring-neutral-900",
|
||||
isOnline ? "bg-teal-500" : "bg-red-500",
|
||||
)}
|
||||
/>
|
||||
</Link>
|
||||
<AccountMoreActions />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
75
packages/ui/src/account/logout.tsx
Normal file
75
packages/ui/src/account/logout.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { useStorage } from "@lume/ark";
|
||||
import * as AlertDialog from "@radix-ui/react-alert-dialog";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function Logout() {
|
||||
const storage = useStorage();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
// logout
|
||||
await storage.logout();
|
||||
|
||||
// clear cache
|
||||
queryClient.clear();
|
||||
|
||||
// redirect to welcome screen
|
||||
navigate("/auth/welcome");
|
||||
} catch (e) {
|
||||
toast.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialog.Root>
|
||||
<AlertDialog.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-10 items-center rounded-lg px-2 text-sm font-medium text-white hover:bg-blue-600 focus:outline-none"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</AlertDialog.Trigger>
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Overlay className="fixed inset-0 z-50 bg-black/20 backdrop-blur-sm dark:bg-black/20" />
|
||||
<AlertDialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
||||
<div className="relative h-min w-full max-w-md rounded-xl bg-neutral-100 dark:bg-neutral-900">
|
||||
<div className="flex flex-col gap-1 border-b border-white/5 px-5 py-4">
|
||||
<AlertDialog.Title className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Are you sure!
|
||||
</AlertDialog.Title>
|
||||
<AlertDialog.Description className="text-sm leading-tight text-neutral-600 dark:text-neutral-400">
|
||||
You can always log back in at any time. If you just want to
|
||||
switch accounts, you can do that by adding an existing account.
|
||||
</AlertDialog.Description>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 px-5 py-3">
|
||||
<AlertDialog.Cancel asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-9 items-center justify-center rounded-lg px-4 text-sm font-medium text-neutral-900 outline-none hover:bg-neutral-200 dark:text-neutral-100 dark:hover:bg-neutral-800"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</AlertDialog.Cancel>
|
||||
<AlertDialog.Action asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => logout()}
|
||||
className="inline-flex h-9 items-center justify-center rounded-lg bg-red-500 px-4 text-sm font-medium text-white outline-none hover:bg-red-600"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</AlertDialog.Action>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Portal>
|
||||
</AlertDialog.Root>
|
||||
);
|
||||
}
|
||||
32
packages/ui/src/account/more.tsx
Normal file
32
packages/ui/src/account/more.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { HorizontalDotsIcon } from "@lume/icons";
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Logout } from "./logout";
|
||||
|
||||
export function AccountMoreActions() {
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center justify-center rounded-md"
|
||||
>
|
||||
<HorizontalDotsIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content className="ml-2 flex w-[200px] flex-col overflow-hidden rounded-xl bg-blue-500 p-2 focus:outline-none">
|
||||
<DropdownMenu.Item asChild>
|
||||
<Link
|
||||
to="/settings/"
|
||||
className="inline-flex h-10 items-center rounded-lg px-2 text-sm font-medium text-white hover:bg-blue-600 focus:outline-none"
|
||||
>
|
||||
Settings
|
||||
</Link>
|
||||
</DropdownMenu.Item>
|
||||
<Logout />
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
);
|
||||
}
|
||||
12
packages/ui/src/index.ts
Normal file
12
packages/ui/src/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export * from "./account/active";
|
||||
export * from "./account/logout";
|
||||
export * from "./account/more";
|
||||
export * from "./navigation";
|
||||
export * from "./nip05";
|
||||
export * from "./user";
|
||||
export * from "./titlebar";
|
||||
export * from "./layouts/app";
|
||||
export * from "./layouts/auth";
|
||||
export * from "./layouts/composer";
|
||||
export * from "./layouts/home";
|
||||
export * from "./layouts/settings";
|
||||
28
packages/ui/src/layouts/app.tsx
Normal file
28
packages/ui/src/layouts/app.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { type Platform } from "@tauri-apps/plugin-os";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Navigation } from "../navigation";
|
||||
import { WindowTitleBar } from "../titlebar";
|
||||
|
||||
export function AppLayout({ platform }: { platform: Platform }) {
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex h-screen w-screen flex-col",
|
||||
platform !== "macos" ? "bg-blue-50 dark:bg-blue-950" : "",
|
||||
)}
|
||||
>
|
||||
{platform !== "macos" ? (
|
||||
<WindowTitleBar platform={platform} />
|
||||
) : (
|
||||
<div data-tauri-drag-region className="h-9 shrink-0" />
|
||||
)}
|
||||
<div className="flex h-full min-h-0 w-full">
|
||||
<Navigation />
|
||||
<div className="h-full flex-1 px-1 pb-1">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
19
packages/ui/src/layouts/auth.tsx
Normal file
19
packages/ui/src/layouts/auth.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { type Platform } from "@tauri-apps/plugin-os";
|
||||
import { Outlet, ScrollRestoration } from "react-router-dom";
|
||||
import { WindowTitleBar } from "../titlebar";
|
||||
|
||||
export function AuthLayout({ platform }: { platform: Platform }) {
|
||||
return (
|
||||
<div className="flex h-screen w-screen flex-col bg-neutral-50 dark:bg-neutral-950">
|
||||
{platform !== "macos" ? (
|
||||
<WindowTitleBar platform={platform} />
|
||||
) : (
|
||||
<div data-tauri-drag-region className="h-9 shrink-0" />
|
||||
)}
|
||||
<div className="h-full w-full">
|
||||
<Outlet />
|
||||
<ScrollRestoration />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
52
packages/ui/src/layouts/composer.tsx
Normal file
52
packages/ui/src/layouts/composer.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { NavLink, Outlet, useLocation } from 'react-router-dom';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function ComposerLayout() {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<div className="container mx-auto h-full px-8 pt-8">
|
||||
<div className="mb-8 flex h-10 shrink-0 items-center gap-3">
|
||||
{location.pathname !== '/new/privkey' ? (
|
||||
<div className="flex h-10 items-center gap-2 rounded-lg bg-neutral-100 px-0.5 dark:bg-neutral-800">
|
||||
<NavLink
|
||||
to="/new/"
|
||||
end
|
||||
className={({ isActive }) =>
|
||||
twMerge(
|
||||
'inline-flex h-9 w-20 items-center justify-center rounded-lg text-sm font-medium',
|
||||
isActive ? 'bg-white shadow dark:bg-black' : 'bg-transparent'
|
||||
)
|
||||
}
|
||||
>
|
||||
Post
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/new/article"
|
||||
className={({ isActive }) =>
|
||||
twMerge(
|
||||
'inline-flex h-9 w-20 items-center justify-center rounded-lg text-sm font-medium',
|
||||
isActive ? 'bg-white shadow dark:bg-black' : 'bg-transparent'
|
||||
)
|
||||
}
|
||||
>
|
||||
Article
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/new/file"
|
||||
className={({ isActive }) =>
|
||||
twMerge(
|
||||
'inline-flex h-9 w-28 items-center justify-center rounded-lg text-sm font-medium',
|
||||
isActive ? 'bg-white shadow dark:bg-black' : 'bg-transparent'
|
||||
)
|
||||
}
|
||||
>
|
||||
File Sharing
|
||||
</NavLink>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
9
packages/ui/src/layouts/home.tsx
Normal file
9
packages/ui/src/layouts/home.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
export function HomeLayout() {
|
||||
return (
|
||||
<div className="h-full w-full rounded-xl overflow-hidden bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-[inset_0_0_0.5px_1px_hsla(0,0%,100%,0.075),0_0_0_1px_hsla(0,0%,0%,0.05),0_0.3px_0.4px_hsla(0,0%,0%,0.02),0_0.9px_1.5px_hsla(0,0%,0%,0.045),0_3.5px_6px_hsla(0,0%,0%,0.09)]">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
92
packages/ui/src/layouts/settings.tsx
Normal file
92
packages/ui/src/layouts/settings.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
AdvancedSettingsIcon,
|
||||
InfoIcon,
|
||||
SecureIcon,
|
||||
SettingsIcon,
|
||||
UserIcon,
|
||||
} from "@lume/icons";
|
||||
import { NavLink, Outlet } from "react-router-dom";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function SettingsLayout() {
|
||||
return (
|
||||
<div className="flex h-full min-h-0 w-full flex-col gap-8 overflow-y-auto">
|
||||
<div className="flex h-24 w-full items-center justify-center border-b border-neutral-200 px-2 dark:border-neutral-900">
|
||||
<div className="flex items-center gap-0.5">
|
||||
<NavLink
|
||||
to="/settings/"
|
||||
end
|
||||
className={({ isActive }) =>
|
||||
twMerge(
|
||||
"flex w-20 shrink-0 flex-col items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-900",
|
||||
isActive
|
||||
? "bg-neutral-100 text-blue-500 hover:bg-neutral-100 dark:bg-neutral-900 dark:hover:bg-neutral-900"
|
||||
: "",
|
||||
)
|
||||
}
|
||||
>
|
||||
<UserIcon className="h-6 w-6" />
|
||||
<p className="text-sm font-medium">User</p>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/settings/general"
|
||||
className={({ isActive }) =>
|
||||
twMerge(
|
||||
"flex w-20 shrink-0 flex-col items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-900",
|
||||
isActive
|
||||
? "bg-neutral-100 text-blue-500 hover:bg-neutral-100 dark:bg-neutral-900 dark:hover:bg-neutral-900"
|
||||
: "",
|
||||
)
|
||||
}
|
||||
>
|
||||
<SettingsIcon className="h-6 w-6" />
|
||||
<p className="text-sm font-medium">General</p>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/settings/backup"
|
||||
className={({ isActive }) =>
|
||||
twMerge(
|
||||
"flex w-20 shrink-0 flex-col items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-900",
|
||||
isActive
|
||||
? "bg-neutral-100 text-blue-500 hover:bg-neutral-100 dark:bg-neutral-900 dark:hover:bg-neutral-900"
|
||||
: "",
|
||||
)
|
||||
}
|
||||
>
|
||||
<SecureIcon className="h-6 w-6" />
|
||||
<p className="text-sm font-medium">Backup</p>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/settings/advanced"
|
||||
className={({ isActive }) =>
|
||||
twMerge(
|
||||
"flex w-20 shrink-0 flex-col items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-900",
|
||||
isActive
|
||||
? "bg-neutral-100 text-blue-500 hover:bg-neutral-100 dark:bg-neutral-900 dark:hover:bg-neutral-900"
|
||||
: "",
|
||||
)
|
||||
}
|
||||
>
|
||||
<AdvancedSettingsIcon className="h-6 w-6" />
|
||||
<p className="text-sm font-medium">Advanced</p>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/settings/about"
|
||||
className={({ isActive }) =>
|
||||
twMerge(
|
||||
"flex w-20 shrink-0 flex-col items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-900",
|
||||
isActive
|
||||
? "bg-neutral-100 text-blue-500 hover:bg-neutral-100 dark:bg-neutral-900 dark:hover:bg-neutral-900"
|
||||
: "",
|
||||
)
|
||||
}
|
||||
>
|
||||
<InfoIcon className="h-6 w-6" />
|
||||
<p className="text-sm font-medium">About</p>
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
155
packages/ui/src/navigation.tsx
Normal file
155
packages/ui/src/navigation.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import {
|
||||
DepotIcon,
|
||||
HomeIcon,
|
||||
NwcIcon,
|
||||
PlusIcon,
|
||||
RelayIcon,
|
||||
SearchIcon,
|
||||
} from "@lume/icons";
|
||||
import { Link, NavLink } from "react-router-dom";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { ActiveAccount } from "./account/active";
|
||||
|
||||
export function Navigation() {
|
||||
return (
|
||||
<div className="flex h-full w-20 shrink-0 flex-col justify-between px-4 py-3">
|
||||
<div className="flex flex-1 flex-col gap-5">
|
||||
<NavLink
|
||||
to="/"
|
||||
preventScrollReset={true}
|
||||
className="inline-flex flex-col items-center justify-center"
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<div
|
||||
className={twMerge(
|
||||
"inline-flex aspect-square h-auto w-full items-center justify-center rounded-xl",
|
||||
isActive
|
||||
? "bg-black/10 text-black dark:bg-white/10 dark:text-white"
|
||||
: "text-black/50 dark:text-neutral-400",
|
||||
)}
|
||||
>
|
||||
<HomeIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"text-sm",
|
||||
isActive
|
||||
? "font-semibold text-black dark:text-white"
|
||||
: "font-medium text-black/50 dark:text-white/50",
|
||||
)}
|
||||
>
|
||||
Home
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/relays"
|
||||
preventScrollReset={true}
|
||||
className="inline-flex flex-col items-center justify-center"
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<div
|
||||
className={twMerge(
|
||||
"inline-flex aspect-square h-auto w-full items-center justify-center rounded-xl",
|
||||
isActive
|
||||
? "bg-black/10 text-black dark:bg-white/10 dark:text-white"
|
||||
: "text-black/50 dark:text-neutral-400",
|
||||
)}
|
||||
>
|
||||
<RelayIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"text-sm",
|
||||
isActive
|
||||
? "font-semibold text-black dark:text-white"
|
||||
: "font-medium text-black/50 dark:text-white/50",
|
||||
)}
|
||||
>
|
||||
Relays
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/depot"
|
||||
preventScrollReset={true}
|
||||
className="inline-flex flex-col items-center justify-center"
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<div
|
||||
className={twMerge(
|
||||
"inline-flex aspect-square h-auto w-full items-center justify-center rounded-xl",
|
||||
isActive
|
||||
? "bg-black/10 text-black dark:bg-white/10 dark:text-white"
|
||||
: "text-black/50 dark:text-neutral-400",
|
||||
)}
|
||||
>
|
||||
<DepotIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"text-sm",
|
||||
isActive
|
||||
? "font-semibold text-black dark:text-white"
|
||||
: "font-medium text-black/50 dark:text-white/50",
|
||||
)}
|
||||
>
|
||||
Depot
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/nwc"
|
||||
preventScrollReset={true}
|
||||
className="inline-flex flex-col items-center justify-center"
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<div
|
||||
className={twMerge(
|
||||
"inline-flex aspect-square h-auto w-full items-center justify-center rounded-xl",
|
||||
isActive
|
||||
? "bg-black/10 text-black dark:bg-white/10 dark:text-white"
|
||||
: "text-black/50 dark:text-neutral-400",
|
||||
)}
|
||||
>
|
||||
<NwcIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"text-sm",
|
||||
isActive
|
||||
? "font-semibold text-black dark:text-white"
|
||||
: "font-medium text-black/50 dark:text-white/50",
|
||||
)}
|
||||
>
|
||||
Wallet
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-col gap-3 p-1">
|
||||
<Link
|
||||
to="/new/"
|
||||
className="flex aspect-square h-auto w-full items-center justify-center rounded-xl bg-black/10 text-black hover:bg-blue-500 hover:text-white dark:bg-white/10 dark:text-white dark:hover:bg-blue-500"
|
||||
>
|
||||
<PlusIcon className="h-5 w-5" />
|
||||
</Link>
|
||||
<Link
|
||||
to="/nwc"
|
||||
className="flex aspect-square h-auto w-full items-center justify-center rounded-xl bg-black/10 hover:bg-blue-500 hover:text-white dark:bg-white/10 dark:hover:bg-blue-500"
|
||||
>
|
||||
<SearchIcon className="h-5 w-5" />
|
||||
</Link>
|
||||
<ActiveAccount />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
74
packages/ui/src/nip05.tsx
Normal file
74
packages/ui/src/nip05.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { UnverifiedIcon, VerifiedIcon } from "@lume/icons";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import { memo } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface NIP05 {
|
||||
names: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const NIP05 = memo(function NIP05({
|
||||
pubkey,
|
||||
nip05,
|
||||
className,
|
||||
}: {
|
||||
pubkey: string;
|
||||
nip05: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ["nip05", nip05],
|
||||
queryFn: async ({ signal }: { signal: AbortSignal }) => {
|
||||
try {
|
||||
const localPath = nip05.split("@")[0];
|
||||
const service = nip05.split("@")[1];
|
||||
const verifyURL = `https://${service}/.well-known/nostr.json?name=${localPath}`;
|
||||
|
||||
const res = await fetch(verifyURL, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!res.ok)
|
||||
throw new Error(`Failed to fetch NIP-05 service: ${nip05}`);
|
||||
|
||||
const data: NIP05 = await res.json();
|
||||
if (data.names) {
|
||||
if (data.names[localPath.toLowerCase()] === pubkey) return true;
|
||||
if (data.names[localPath] === pubkey) return true;
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to verify NIP-05, error: ${e}`);
|
||||
}
|
||||
},
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
if (status === "pending") {
|
||||
<div className="h-4 w-4 animate-pulse rounded-full bg-neutral-100 dark:bg-neutral-900" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="inline-flex items-center gap-1">
|
||||
<p className={twMerge("text-sm font-medium", className)}>
|
||||
{nip05.startsWith("_@") ? nip05.replace("_@", "") : nip05}
|
||||
</p>
|
||||
{data === true ? (
|
||||
<VerifiedIcon className="h-4 w-4 text-teal-500" />
|
||||
) : (
|
||||
<UnverifiedIcon className="h-4 w-4 text-red-500" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
21
packages/ui/src/titlebar/components/button.tsx
Normal file
21
packages/ui/src/titlebar/components/button.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { ButtonHTMLAttributes } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function WindowButton({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={twMerge(
|
||||
"inline-flex cursor-default items-center justify-center",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
140
packages/ui/src/titlebar/components/icons.tsx
Normal file
140
packages/ui/src/titlebar/components/icons.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import type { SVGProps } from 'react';
|
||||
|
||||
export const WindowIcons = {
|
||||
minimizeWin: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
width="10"
|
||||
height="1"
|
||||
viewBox="0 0 10 1"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M0.498047 1.00098C0.429688 1.00098 0.364583 0.987956 0.302734 0.961914C0.244141 0.935872 0.192057 0.900065 0.146484 0.854492C0.100911 0.808919 0.0651042 0.756836 0.0390625 0.698242C0.0130208 0.636393 0 0.571289 0 0.50293C0 0.43457 0.0130208 0.371094 0.0390625 0.3125C0.0651042 0.250651 0.100911 0.19694 0.146484 0.151367C0.192057 0.102539 0.244141 0.0651042 0.302734 0.0390625C0.364583 0.0130208 0.429688 0 0.498047 0H9.50195C9.57031 0 9.63379 0.0130208 9.69238 0.0390625C9.75423 0.0651042 9.80794 0.102539 9.85352 0.151367C9.89909 0.19694 9.9349 0.250651 9.96094 0.3125C9.98698 0.371094 10 0.43457 10 0.50293C10 0.571289 9.98698 0.636393 9.96094 0.698242C9.9349 0.756836 9.89909 0.808919 9.85352 0.854492C9.80794 0.900065 9.75423 0.935872 9.69238 0.961914C9.63379 0.987956 9.57031 1.00098 9.50195 1.00098H0.498047Z"
|
||||
fill="currentColor"
|
||||
fillOpacity="0.8956"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
maximizeWin: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 10 10"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M1.47461 10.001C1.2793 10.001 1.09212 9.96191 0.913086 9.88379C0.734049 9.80241 0.576172 9.69499 0.439453 9.56152C0.30599 9.4248 0.198568 9.26693 0.117188 9.08789C0.0390625 8.90885 0 8.72168 0 8.52637V1.47559C0 1.28027 0.0390625 1.0931 0.117188 0.914062C0.198568 0.735026 0.30599 0.578776 0.439453 0.445312C0.576172 0.308594 0.734049 0.201172 0.913086 0.123047C1.09212 0.0416667 1.2793 0.000976562 1.47461 0.000976562H8.52539C8.7207 0.000976562 8.90788 0.0416667 9.08691 0.123047C9.26595 0.201172 9.4222 0.308594 9.55566 0.445312C9.69238 0.578776 9.7998 0.735026 9.87793 0.914062C9.95931 1.0931 10 1.28027 10 1.47559V8.52637C10 8.72168 9.95931 8.90885 9.87793 9.08789C9.7998 9.26693 9.69238 9.4248 9.55566 9.56152C9.4222 9.69499 9.26595 9.80241 9.08691 9.88379C8.90788 9.96191 8.7207 10.001 8.52539 10.001H1.47461ZM8.50098 9C8.56934 9 8.63281 8.98698 8.69141 8.96094C8.75326 8.9349 8.80697 8.89909 8.85254 8.85352C8.89811 8.80794 8.93392 8.75586 8.95996 8.69727C8.986 8.63542 8.99902 8.57031 8.99902 8.50195V1.5C8.99902 1.43164 8.986 1.36816 8.95996 1.30957C8.93392 1.24772 8.89811 1.19401 8.85254 1.14844C8.80697 1.10286 8.75326 1.06706 8.69141 1.04102C8.63281 1.01497 8.56934 1.00195 8.50098 1.00195H1.49902C1.43066 1.00195 1.36556 1.01497 1.30371 1.04102C1.24512 1.06706 1.19303 1.10286 1.14746 1.14844C1.10189 1.19401 1.06608 1.24772 1.04004 1.30957C1.014 1.36816 1.00098 1.43164 1.00098 1.5V8.50195C1.00098 8.57031 1.014 8.63542 1.04004 8.69727C1.06608 8.75586 1.10189 8.80794 1.14746 8.85352C1.19303 8.89909 1.24512 8.9349 1.30371 8.96094C1.36556 8.98698 1.43066 9 1.49902 9H8.50098Z"
|
||||
fill="currentColor"
|
||||
fillOpacity="0.8956"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
maximizeRestoreWin: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
width="10"
|
||||
height="11"
|
||||
viewBox="0 0 10 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M8.99902 2.98096C8.99902 2.71077 8.94531 2.45687 8.83789 2.21924C8.73047 1.97835 8.58398 1.77002 8.39844 1.59424C8.21615 1.4152 8.00293 1.27523 7.75879 1.17432C7.5179 1.07015 7.264 1.01807 6.99707 1.01807H2.08496C2.13704 0.868327 2.21029 0.731608 2.30469 0.60791C2.39909 0.484212 2.50814 0.378418 2.63184 0.290527C2.75553 0.202637 2.89062 0.135905 3.03711 0.090332C3.18685 0.0415039 3.34147 0.0170898 3.50098 0.0170898H6.99707C7.41048 0.0170898 7.79948 0.0968424 8.16406 0.256348C8.52865 0.412598 8.84603 0.625814 9.11621 0.895996C9.38965 1.16618 9.60449 1.48356 9.76074 1.84814C9.92025 2.21273 10 2.60173 10 3.01514V6.51611C10 6.67562 9.97559 6.83024 9.92676 6.97998C9.88118 7.12646 9.81445 7.26156 9.72656 7.38525C9.63867 7.50895 9.53288 7.618 9.40918 7.7124C9.28548 7.8068 9.14876 7.88005 8.99902 7.93213V2.98096ZM1.47461 10.0171C1.2793 10.0171 1.09212 9.97803 0.913086 9.8999C0.734049 9.81852 0.576172 9.7111 0.439453 9.57764C0.30599 9.44092 0.198568 9.28304 0.117188 9.104C0.0390625 8.92497 0 8.73779 0 8.54248V3.49365C0 3.29508 0.0390625 3.10791 0.117188 2.93213C0.198568 2.75309 0.30599 2.59684 0.439453 2.46338C0.576172 2.32666 0.732422 2.21924 0.908203 2.14111C1.08724 2.05973 1.27604 2.01904 1.47461 2.01904H6.52344C6.72201 2.01904 6.91081 2.05973 7.08984 2.14111C7.26888 2.21924 7.42513 2.32503 7.55859 2.4585C7.69206 2.59196 7.79785 2.74821 7.87598 2.92725C7.95736 3.10628 7.99805 3.29508 7.99805 3.49365V8.54248C7.99805 8.74105 7.95736 8.92985 7.87598 9.10889C7.79785 9.28467 7.69043 9.44092 7.55371 9.57764C7.42025 9.7111 7.264 9.81852 7.08496 9.8999C6.90918 9.97803 6.72201 10.0171 6.52344 10.0171H1.47461ZM6.49902 9.01611C6.56738 9.01611 6.63086 9.00309 6.68945 8.97705C6.7513 8.95101 6.80501 8.9152 6.85059 8.86963C6.89941 8.82406 6.93685 8.77197 6.96289 8.71338C6.98893 8.65153 7.00195 8.58643 7.00195 8.51807V3.51807C7.00195 3.44971 6.98893 3.3846 6.96289 3.32275C6.93685 3.2609 6.90104 3.20719 6.85547 3.16162C6.8099 3.11605 6.75618 3.08024 6.69434 3.0542C6.63249 3.02816 6.56738 3.01514 6.49902 3.01514H1.49902C1.43066 3.01514 1.36556 3.02816 1.30371 3.0542C1.24512 3.08024 1.19303 3.11768 1.14746 3.1665C1.10189 3.21208 1.06608 3.26579 1.04004 3.32764C1.014 3.38623 1.00098 3.44971 1.00098 3.51807V8.51807C1.00098 8.58643 1.014 8.65153 1.04004 8.71338C1.06608 8.77197 1.10189 8.82406 1.14746 8.86963C1.19303 8.9152 1.24512 8.95101 1.30371 8.97705C1.36556 9.00309 1.43066 9.01611 1.49902 9.01611H6.49902Z"
|
||||
fill="currentColor"
|
||||
fillOpacity="0.8956"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
closeWin: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 10 10"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M5 5.70898L0.854492 9.85449C0.756836 9.95215 0.639648 10.001 0.50293 10.001C0.359701 10.001 0.239258 9.95378 0.141602 9.85938C0.0472005 9.76172 0 9.64128 0 9.49805C0 9.36133 0.0488281 9.24414 0.146484 9.14648L4.29199 5.00098L0.146484 0.855469C0.0488281 0.757812 0 0.638997 0 0.499023C0 0.430664 0.0130208 0.36556 0.0390625 0.303711C0.0651042 0.241862 0.100911 0.189779 0.146484 0.147461C0.192057 0.101888 0.245768 0.0660807 0.307617 0.0400391C0.369466 0.0139974 0.43457 0.000976562 0.50293 0.000976562C0.639648 0.000976562 0.756836 0.0498047 0.854492 0.147461L5 4.29297L9.14551 0.147461C9.24316 0.0498047 9.36198 0.000976562 9.50195 0.000976562C9.57031 0.000976562 9.63379 0.0139974 9.69238 0.0400391C9.75423 0.0660807 9.80794 0.101888 9.85352 0.147461C9.89909 0.193034 9.9349 0.246745 9.96094 0.308594C9.98698 0.367188 10 0.430664 10 0.499023C10 0.638997 9.95117 0.757812 9.85352 0.855469L5.70801 5.00098L9.85352 9.14648C9.95117 9.24414 10 9.36133 10 9.49805C10 9.56641 9.98698 9.63151 9.96094 9.69336C9.9349 9.75521 9.89909 9.80892 9.85352 9.85449C9.8112 9.90007 9.75911 9.93587 9.69727 9.96191C9.63542 9.98796 9.57031 10.001 9.50195 10.001C9.36198 10.001 9.24316 9.95215 9.14551 9.85449L5 5.70898Z"
|
||||
fill="currentColor"
|
||||
fillOpacity="0.8956"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
closeMac: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
width="6"
|
||||
height="6"
|
||||
viewBox="0 0 16 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M15.7522 4.44381L11.1543 9.04165L15.7494 13.6368C16.0898 13.9771 16.078 14.5407 15.724 14.8947L13.8907 16.728C13.5358 17.0829 12.9731 17.0938 12.6328 16.7534L8.03766 12.1583L3.44437 16.7507C3.10402 17.091 2.54132 17.0801 2.18645 16.7253L0.273257 14.8121C-0.0807018 14.4572 -0.0925004 13.8945 0.247845 13.5542L4.84024 8.96087L0.32499 4.44653C-0.0153555 4.10619 -0.00355681 3.54258 0.350402 3.18862L2.18373 1.35529C2.53859 1.00042 3.1013 0.989533 3.44164 1.32988L7.95689 5.84422L12.5556 1.24638C12.8951 0.906035 13.4587 0.917833 13.8126 1.27179L15.7267 3.18589C16.0807 3.53985 16.0925 4.10346 15.7522 4.44381Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
minMac: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
width="8"
|
||||
height="8"
|
||||
viewBox="0 0 17 6"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g clipPath="url(#clip0_20_2051)">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M1.47211 1.18042H15.4197C15.8052 1.18042 16.1179 1.50551 16.1179 1.90769V3.73242C16.1179 4.13387 15.8052 4.80006 15.4197 4.80006H1.47211C1.08665 4.80006 0.773926 4.47497 0.773926 4.07278V1.90769C0.773926 1.50551 1.08665 1.18042 1.47211 1.18042Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
fullMac: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
width="6"
|
||||
height="6"
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g clipPath="url(#clip0_20_2057)">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.53068 0.433838L15.0933 12.0409C15.0933 12.0409 15.0658 5.35028 15.0658 4.01784C15.0658 1.32095 14.1813 0.433838 11.5378 0.433838C10.6462 0.433838 3.53068 0.433838 3.53068 0.433838ZM12.4409 15.5378L0.87735 3.93073C0.87735 3.93073 0.905794 10.6214 0.905794 11.9538C0.905794 14.6507 1.79024 15.5378 4.43291 15.5378C5.32535 15.5378 12.4409 15.5378 12.4409 15.5378Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
plusMac: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
width="8"
|
||||
height="8"
|
||||
viewBox="0 0 17 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g clipPath="url(#clip0_20_2053)">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15.5308 9.80147H10.3199V15.0095C10.3199 15.3949 9.9941 15.7076 9.59265 15.7076H7.51555C7.11337 15.7076 6.78828 15.3949 6.78828 15.0095V9.80147H1.58319C1.19774 9.80147 0.88501 9.47638 0.88501 9.07419V6.90619C0.88501 6.50401 1.19774 6.17892 1.58319 6.17892H6.78828V1.06183C6.78828 0.676375 7.11337 0.363647 7.51555 0.363647H9.59265C9.9941 0.363647 10.3199 0.676375 10.3199 1.06183V6.17892H15.5308C15.9163 6.17892 16.229 6.50401 16.229 6.90619V9.07419C16.229 9.47638 15.9163 9.80147 15.5308 9.80147Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
115
packages/ui/src/titlebar/context.tsx
Normal file
115
packages/ui/src/titlebar/context.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { type Window, getCurrent } from "@tauri-apps/api/window";
|
||||
import { type } from "@tauri-apps/plugin-os";
|
||||
import React, { createContext, useCallback, useEffect, useState } from "react";
|
||||
|
||||
interface AppWindowContextType {
|
||||
appWindow: Window | null;
|
||||
isWindowMaximized: boolean;
|
||||
minimizeWindow: () => Promise<void>;
|
||||
maximizeWindow: () => Promise<void>;
|
||||
fullscreenWindow: () => Promise<void>;
|
||||
closeWindow: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const AppWindowContext = createContext<AppWindowContextType>({
|
||||
appWindow: null,
|
||||
isWindowMaximized: false,
|
||||
minimizeWindow: () => Promise.resolve(),
|
||||
maximizeWindow: () => Promise.resolve(),
|
||||
fullscreenWindow: () => Promise.resolve(),
|
||||
closeWindow: () => Promise.resolve(),
|
||||
});
|
||||
|
||||
interface AppWindowProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AppWindowProvider: React.FC<AppWindowProviderProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [appWindow, setAppWindow] = useState<Window | null>(null);
|
||||
const [isWindowMaximized, setIsWindowMaximized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const window = getCurrent();
|
||||
setAppWindow(window);
|
||||
}, []);
|
||||
|
||||
const updateIsWindowMaximized = useCallback(async () => {
|
||||
if (appWindow) {
|
||||
const _isWindowMaximized = await appWindow.isMaximized();
|
||||
setIsWindowMaximized(_isWindowMaximized);
|
||||
}
|
||||
}, [appWindow]);
|
||||
|
||||
useEffect(() => {
|
||||
let unlisten: () => void = () => {};
|
||||
|
||||
async function getOsType() {
|
||||
const osname = await type();
|
||||
|
||||
if (osname !== "macos") {
|
||||
updateIsWindowMaximized();
|
||||
|
||||
const listen = async () => {
|
||||
if (appWindow) {
|
||||
unlisten = await appWindow.onResized(() => {
|
||||
updateIsWindowMaximized();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
listen();
|
||||
}
|
||||
}
|
||||
|
||||
getOsType();
|
||||
|
||||
// Cleanup the listener when the component unmounts
|
||||
return () => unlisten?.();
|
||||
}, [appWindow, updateIsWindowMaximized]);
|
||||
|
||||
const minimizeWindow = async () => {
|
||||
if (appWindow) {
|
||||
await appWindow.minimize();
|
||||
}
|
||||
};
|
||||
|
||||
const maximizeWindow = async () => {
|
||||
if (appWindow) {
|
||||
await appWindow.toggleMaximize();
|
||||
}
|
||||
};
|
||||
|
||||
const fullscreenWindow = async () => {
|
||||
if (appWindow) {
|
||||
const fullscreen = await appWindow.isFullscreen();
|
||||
if (fullscreen) {
|
||||
await appWindow.setFullscreen(false);
|
||||
} else {
|
||||
await appWindow.setFullscreen(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const closeWindow = async () => {
|
||||
if (appWindow) {
|
||||
await appWindow.close();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AppWindowContext.Provider
|
||||
value={{
|
||||
appWindow,
|
||||
isWindowMaximized,
|
||||
minimizeWindow,
|
||||
maximizeWindow,
|
||||
fullscreenWindow,
|
||||
closeWindow,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AppWindowContext.Provider>
|
||||
);
|
||||
};
|
||||
43
packages/ui/src/titlebar/controls/gnome.tsx
Normal file
43
packages/ui/src/titlebar/controls/gnome.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { HTMLProps, useContext } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { WindowButton } from "../components/button";
|
||||
import { WindowIcons } from "../components/icons";
|
||||
import { AppWindowContext } from "../context";
|
||||
|
||||
export function Gnome({ className, ...props }: HTMLProps<HTMLDivElement>) {
|
||||
const { isWindowMaximized, minimizeWindow, maximizeWindow, closeWindow } =
|
||||
useContext(AppWindowContext);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"mr-[10px] h-auto items-center space-x-[13px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<WindowButton
|
||||
onClick={minimizeWindow}
|
||||
className="m-0 aspect-square h-6 w-6 cursor-default rounded-full bg-[#dadada] p-0 text-[#3d3d3d] hover:bg-[#d1d1d1] active:bg-[#bfbfbf] dark:bg-[#373737] dark:text-white dark:hover:bg-[#424242] dark:active:bg-[#565656]"
|
||||
>
|
||||
<WindowIcons.minimizeWin className="h-[9px] w-[9px]" />
|
||||
</WindowButton>
|
||||
<WindowButton
|
||||
onClick={maximizeWindow}
|
||||
className="m-0 aspect-square h-6 w-6 cursor-default rounded-full bg-[#dadada] p-0 text-[#3d3d3d] hover:bg-[#d1d1d1] active:bg-[#bfbfbf] dark:bg-[#373737] dark:text-white dark:hover:bg-[#424242] dark:active:bg-[#565656]"
|
||||
>
|
||||
{!isWindowMaximized ? (
|
||||
<WindowIcons.maximizeWin className="h-2 w-2" />
|
||||
) : (
|
||||
<WindowIcons.maximizeRestoreWin className="h-[9px] w-[9px]" />
|
||||
)}
|
||||
</WindowButton>
|
||||
<WindowButton
|
||||
onClick={closeWindow}
|
||||
className="m-0 aspect-square h-6 w-6 cursor-default rounded-full bg-[#dadada] p-0 text-[#3d3d3d] hover:bg-[#d1d1d1] active:bg-[#bfbfbf] dark:bg-[#373737] dark:text-white dark:hover:bg-[#424242] dark:active:bg-[#565656]"
|
||||
>
|
||||
<WindowIcons.closeWin className="h-2 w-2" />
|
||||
</WindowButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
79
packages/ui/src/titlebar/controls/macos.tsx
Normal file
79
packages/ui/src/titlebar/controls/macos.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { HTMLProps, useContext, useEffect, useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { WindowButton } from "../components/button";
|
||||
import { WindowIcons } from "../components/icons";
|
||||
import { AppWindowContext } from "../context";
|
||||
|
||||
export function MacOS({ className, ...props }: HTMLProps<HTMLDivElement>) {
|
||||
const { minimizeWindow, maximizeWindow, fullscreenWindow, closeWindow } =
|
||||
useContext(AppWindowContext);
|
||||
|
||||
const [isAltKeyPressed, setIsAltKeyPressed] = useState(false);
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
|
||||
const last = isAltKeyPressed ? (
|
||||
<WindowIcons.plusMac />
|
||||
) : (
|
||||
<WindowIcons.fullMac />
|
||||
);
|
||||
const key = "Alt";
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsHovering(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsHovering(false);
|
||||
};
|
||||
|
||||
const handleAltKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === key) {
|
||||
setIsAltKeyPressed(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAltKeyUp = (e: KeyboardEvent) => {
|
||||
if (e.key === key) {
|
||||
setIsAltKeyPressed(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Attach event listeners when the component mounts
|
||||
window.addEventListener("keydown", handleAltKeyDown);
|
||||
window.addEventListener("keyup", handleAltKeyUp);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"space-x-2 px-3 text-black active:text-black dark:text-black",
|
||||
className,
|
||||
)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
{...props}
|
||||
>
|
||||
<WindowButton
|
||||
onClick={closeWindow}
|
||||
className="aspect-square h-3 w-3 cursor-default content-center items-center justify-center self-center rounded-full border border-black/[.12] bg-[#ff544d] text-center text-black/60 hover:bg-[#ff544d] active:bg-[#bf403a] active:text-black/60 dark:border-none"
|
||||
>
|
||||
{isHovering && <WindowIcons.closeMac />}
|
||||
</WindowButton>
|
||||
<WindowButton
|
||||
onClick={minimizeWindow}
|
||||
className="aspect-square h-3 w-3 cursor-default content-center items-center justify-center self-center rounded-full border border-black/[.12] bg-[#ffbd2e] text-center text-black/60 hover:bg-[#ffbd2e] active:bg-[#bf9122] active:text-black/60 dark:border-none"
|
||||
>
|
||||
{isHovering && <WindowIcons.minMac />}
|
||||
</WindowButton>
|
||||
<WindowButton
|
||||
// onKeyDown={handleAltKeyDown}
|
||||
// onKeyUp={handleAltKeyUp}
|
||||
onClick={isAltKeyPressed ? maximizeWindow : fullscreenWindow}
|
||||
className="aspect-square h-3 w-3 cursor-default content-center items-center justify-center self-center rounded-full border border-black/[.12] bg-[#28c93f] text-center text-black/60 hover:bg-[#28c93f] active:bg-[#1e9930] active:text-black/60 dark:border-none"
|
||||
>
|
||||
{isHovering && last}
|
||||
</WindowButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
41
packages/ui/src/titlebar/controls/windows.tsx
Normal file
41
packages/ui/src/titlebar/controls/windows.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { HTMLProps, useContext } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { WindowButton } from "../components/button";
|
||||
import { WindowIcons } from "../components/icons";
|
||||
import { AppWindowContext } from "../context";
|
||||
|
||||
export function Windows({ className, ...props }: HTMLProps<HTMLDivElement>) {
|
||||
const { isWindowMaximized, minimizeWindow, maximizeWindow, closeWindow } =
|
||||
useContext(AppWindowContext);
|
||||
|
||||
return (
|
||||
<div className={twMerge("h-8", className)} {...props}>
|
||||
<WindowButton
|
||||
onClick={minimizeWindow}
|
||||
className="max-h-8 w-[46px] cursor-default rounded-none bg-transparent text-black/90 hover:bg-black/[.05] active:bg-black/[.03] dark:text-white dark:hover:bg-white/[.06] dark:active:bg-white/[.04]"
|
||||
>
|
||||
<WindowIcons.minimizeWin />
|
||||
</WindowButton>
|
||||
<WindowButton
|
||||
onClick={maximizeWindow}
|
||||
className={twMerge(
|
||||
"max-h-8 w-[46px] cursor-default rounded-none bg-transparent",
|
||||
"text-black/90 hover:bg-black/[.05] active:bg-black/[.03] dark:text-white dark:hover:bg-white/[.06] dark:active:bg-white/[.04]",
|
||||
// !isMaximizable && "text-white/[.36]",
|
||||
)}
|
||||
>
|
||||
{!isWindowMaximized ? (
|
||||
<WindowIcons.maximizeWin />
|
||||
) : (
|
||||
<WindowIcons.maximizeRestoreWin />
|
||||
)}
|
||||
</WindowButton>
|
||||
<WindowButton
|
||||
onClick={closeWindow}
|
||||
className="max-h-8 w-[46px] cursor-default rounded-none bg-transparent text-black/90 hover:bg-[#c42b1c] hover:text-white active:bg-[#c42b1c]/90 dark:text-white"
|
||||
>
|
||||
<WindowIcons.closeWin />
|
||||
</WindowButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
7
packages/ui/src/titlebar/index.ts
Normal file
7
packages/ui/src/titlebar/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './context';
|
||||
export * from './components/button';
|
||||
export * from './components/icons';
|
||||
export * from './controls/gnome';
|
||||
export * from './controls/windows';
|
||||
export * from './controls/macos';
|
||||
export * from './titleBar';
|
||||
31
packages/ui/src/titlebar/titleBar.tsx
Normal file
31
packages/ui/src/titlebar/titleBar.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Platform } from "@tauri-apps/plugin-os";
|
||||
import { AppWindowProvider } from "./context";
|
||||
import { Gnome } from "./controls/gnome";
|
||||
import { MacOS } from "./controls/macos";
|
||||
import { Windows } from "./controls/windows";
|
||||
|
||||
export function WindowTitleBar({ platform }: { platform: Platform }) {
|
||||
const ControlsComponent = () => {
|
||||
switch (platform) {
|
||||
case "windows":
|
||||
return <Windows className="ml-auto flex" />;
|
||||
case "macos":
|
||||
return <MacOS className="ml-0 flex" />;
|
||||
case "linux":
|
||||
return <Gnome className="ml-auto flex" />;
|
||||
default:
|
||||
return <Windows className="ml-auto flex" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AppWindowProvider>
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="bg-background flex select-none flex-row overflow-hidden"
|
||||
>
|
||||
<ControlsComponent />
|
||||
</div>
|
||||
</AppWindowProvider>
|
||||
);
|
||||
}
|
||||
619
packages/ui/src/user.tsx
Normal file
619
packages/ui/src/user.tsx
Normal file
@@ -0,0 +1,619 @@
|
||||
import { useProfile } from "@lume/ark";
|
||||
import { RepostIcon } from "@lume/icons";
|
||||
import { displayNpub, formatCreatedAt } from "@lume/utils";
|
||||
import * as Avatar from "@radix-ui/react-avatar";
|
||||
import * as HoverCard from "@radix-ui/react-hover-card";
|
||||
import { minidenticon } from "minidenticons";
|
||||
import { memo, useMemo } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { NIP05 } from "./nip05";
|
||||
|
||||
export const User = memo(function User({
|
||||
pubkey,
|
||||
time,
|
||||
variant = "default",
|
||||
subtext,
|
||||
}: {
|
||||
pubkey: string;
|
||||
time?: number;
|
||||
variant?:
|
||||
| "default"
|
||||
| "simple"
|
||||
| "mention"
|
||||
| "notify"
|
||||
| "repost"
|
||||
| "chat"
|
||||
| "large"
|
||||
| "thread"
|
||||
| "miniavatar"
|
||||
| "avatar"
|
||||
| "stacked"
|
||||
| "ministacked"
|
||||
| "childnote";
|
||||
subtext?: string;
|
||||
}) {
|
||||
const { isLoading, user } = useProfile(pubkey);
|
||||
|
||||
const createdAt = useMemo(
|
||||
() => formatCreatedAt(time, variant === "chat"),
|
||||
[time, variant],
|
||||
);
|
||||
const fallbackName = useMemo(() => displayNpub(pubkey, 16), [pubkey]);
|
||||
const fallbackAvatar = useMemo(
|
||||
() =>
|
||||
`data:image/svg+xml;utf8,${encodeURIComponent(
|
||||
minidenticon(pubkey, 90, 50),
|
||||
)}`,
|
||||
[pubkey],
|
||||
);
|
||||
|
||||
if (variant === "mention") {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar.Root className="shrink-0">
|
||||
<Avatar.Image
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
className="h-6 w-6 rounded-md bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Root>
|
||||
<div className="flex flex-1 items-baseline gap-2">
|
||||
<h5 className="max-w-[10rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
{fallbackName}
|
||||
</h5>
|
||||
<span className="text-neutral-600 dark:text-neutral-400">·</span>
|
||||
<span className="text-neutral-600 dark:text-neutral-400">
|
||||
{createdAt}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-6 items-center gap-2">
|
||||
<Avatar.Root className="shrink-0">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-6 w-6 rounded-md"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
className="h-6 w-6 rounded-md bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div className="flex flex-1 items-baseline gap-2">
|
||||
<h5 className="max-w-[10rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
{user?.name ||
|
||||
user?.display_name ||
|
||||
user?.displayName ||
|
||||
fallbackName}
|
||||
</h5>
|
||||
<span className="text-neutral-600 dark:text-neutral-400">·</span>
|
||||
<span className="text-neutral-600 dark:text-neutral-400">
|
||||
{createdAt}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === "notify") {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar.Root className="h-8 w-8 shrink-0">
|
||||
<Avatar.Image
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
className="h-8 w-8 rounded-md bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Root>
|
||||
<h5 className="max-w-[10rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
{fallbackName}
|
||||
</h5>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar.Root className="h-8 w-8 shrink-0">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-8 w-8 rounded-md"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
className="h-8 w-8 rounded-md bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<h5 className="max-w-[10rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
{user?.name ||
|
||||
user?.display_name ||
|
||||
user?.displayName ||
|
||||
fallbackName}
|
||||
</h5>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === "large") {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="h-14 w-14 shrink-0 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="h-3.5 w-36 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
||||
<div className="h-4 w-24 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col gap-2.5">
|
||||
<Avatar.Root className="shrink-0">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-11 w-11 rounded-lg object-cover"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
className="h-11 w-11 rounded-lg bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div className="flex flex-col items-start text-start">
|
||||
<p className="max-w-[15rem] truncate text-lg font-semibold">
|
||||
{user?.name || user?.display_name || user?.displayName}
|
||||
</p>
|
||||
<p className="break-p prose prose-neutral max-w-none select-text whitespace-pre-line leading-normal dark:prose-invert">
|
||||
{user?.about || user?.bio || "No bio"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === "simple") {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="h-10 w-10 shrink-0 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
|
||||
<div className="flex w-full flex-col items-start gap-1">
|
||||
<div className="h-4 w-36 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
||||
<div className="h-4 w-24 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Avatar.Root className="h-10 w-10 shrink-0">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-10 w-10 rounded-lg object-cover"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
className="h-10 w-10 rounded-lg bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div className="flex w-full flex-col items-start">
|
||||
<h3 className="max-w-[15rem] truncate text-base font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
{user?.name || user?.display_name || user?.displayName}
|
||||
</h3>
|
||||
<p className="max-w-[10rem] truncate text-sm text-neutral-900 dark:text-neutral-100/70">
|
||||
{user?.nip05 || user?.username || fallbackName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === "avatar") {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-12 w-12 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Avatar.Root>
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-12 w-12 rounded-lg"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
className="h-12 w-12 rounded-lg bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === "miniavatar") {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-10 w-10 shrink-0 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Avatar.Root className="h-10 w-10 shrink-0">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-10 w-10 rounded-lg"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
className="h-10 w-10 rounded-lg bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === "childnote") {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<>
|
||||
<Avatar.Root className="h-10 w-10 shrink-0">
|
||||
<Avatar.Image
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
className="h-10 w-10 rounded-lg bg-black object-cover dark:bg-white"
|
||||
/>
|
||||
</Avatar.Root>
|
||||
<div className="absolute left-2 top-2 inline-flex items-center gap-1.5 font-semibold leading-tight">
|
||||
<div className="w-full max-w-[10rem] truncate">{fallbackName} </div>
|
||||
<div className="font-normal text-neutral-700 dark:text-neutral-300">
|
||||
{subtext}:
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Avatar.Root className="h-10 w-10 shrink-0">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-10 w-10 rounded-lg object-cover"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
className="h-10 w-10 rounded-lg bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div className="absolute left-2 top-2 inline-flex items-center gap-1.5 font-semibold leading-tight">
|
||||
<div className="w-full max-w-[10rem] truncate">
|
||||
{user?.display_name ||
|
||||
user?.name ||
|
||||
user?.displayName ||
|
||||
fallbackName}{" "}
|
||||
</div>
|
||||
<div className="font-normal text-neutral-700 dark:text-neutral-300">
|
||||
{subtext}:
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === "stacked") {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="inline-block h-8 w-8 animate-pulse rounded-full bg-neutral-300 ring-1 ring-neutral-200 dark:bg-neutral-700 dark:ring-neutral-800" />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Avatar.Root>
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="inline-block h-8 w-8 rounded-full ring-1 ring-neutral-200 dark:ring-neutral-800"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
className="inline-block h-8 w-8 rounded-full bg-black ring-1 ring-neutral-200 dark:bg-white dark:ring-neutral-800"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === "ministacked") {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="inline-block h-6 w-6 animate-pulse rounded-full bg-neutral-300 ring-1 ring-white dark:ring-black" />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Avatar.Root>
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="inline-block h-6 w-6 rounded-full ring-1 ring-white dark:ring-black"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
className="inline-block h-6 w-6 rounded-full bg-black ring-1 ring-white dark:bg-white dark:ring-black"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === "repost") {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
<div className="inline-flex h-10 w-10 items-center justify-center">
|
||||
<RepostIcon className="h-5 w-5 text-blue-500" />
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<div className="h-6 w-6 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
||||
<div className="h-4 w-24 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 px-3">
|
||||
<div className="inline-flex w-10 items-center justify-center">
|
||||
<RepostIcon className="h-5 w-5 text-blue-500" />
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<Avatar.Root className="shrink-0">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-6 w-6 rounded object-cover"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
className="h-6 w-6 rounded bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div className="inline-flex items-baseline gap-1">
|
||||
<h5 className="max-w-[10rem] truncate font-medium text-neutral-900 dark:text-neutral-100/80">
|
||||
{user?.name ||
|
||||
user?.display_name ||
|
||||
user?.displayName ||
|
||||
fallbackName}
|
||||
</h5>
|
||||
<span className="text-blue-500">reposted</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === "thread") {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-16 items-center gap-3 px-3">
|
||||
<div className="h-10 w-10 shrink-0 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
|
||||
<div className="flex flex-1 flex-col gap-1">
|
||||
<div className="h-4 w-36 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
||||
<div className="h-3 w-24 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-16 items-center gap-3 px-3">
|
||||
<Avatar.Root className="h-10 w-10 shrink-0">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-10 w-10 rounded-lg object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
className="h-10 w-10 rounded-lg bg-black ring-1 ring-neutral-200/50 dark:bg-white dark:ring-neutral-800/50"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<h5 className="max-w-[15rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
{user?.name || user?.display_name || user?.displayName || "Anon"}
|
||||
</h5>
|
||||
<div className="inline-flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
<span>{createdAt}</span>
|
||||
<span>·</span>
|
||||
<span>{fallbackName}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 px-3">
|
||||
<Avatar.Root className="h-9 w-9 shrink-0">
|
||||
<Avatar.Image
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
className="h-9 w-9 rounded-lg bg-black ring-1 ring-neutral-200/50 dark:bg-white dark:ring-neutral-800/50"
|
||||
/>
|
||||
</Avatar.Root>
|
||||
<div className="h-6 flex-1">
|
||||
<div className="max-w-[15rem] truncate font-semibold text-neutral-950 dark:text-neutral-50">
|
||||
{fallbackName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<HoverCard.Root>
|
||||
<div className="flex items-center gap-3 px-3">
|
||||
<HoverCard.Trigger asChild>
|
||||
<Avatar.Root className="h-9 w-9 shrink-0">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-9 w-9 rounded-lg bg-white object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
className="h-9 w-9 rounded-lg bg-black ring-1 ring-neutral-200/50 dark:bg-white dark:ring-neutral-800/50"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
</HoverCard.Trigger>
|
||||
<div className="flex h-6 flex-1 items-start gap-2">
|
||||
<div className="max-w-[15rem] truncate font-semibold text-neutral-950 dark:text-neutral-50">
|
||||
{user?.name ||
|
||||
user?.display_name ||
|
||||
user?.displayName ||
|
||||
fallbackName}
|
||||
</div>
|
||||
<div className="ml-auto inline-flex items-center gap-3">
|
||||
<div className="text-neutral-500 dark:text-neutral-400">
|
||||
{createdAt}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<HoverCard.Portal>
|
||||
<HoverCard.Content
|
||||
className="ml-4 w-[300px] overflow-hidden rounded-xl border border-neutral-200 bg-neutral-100 shadow-lg data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=top]:animate-slideDownAndFade data-[state=open]:transition-all focus:outline-none dark:border-neutral-800 dark:bg-neutral-900"
|
||||
sideOffset={5}
|
||||
>
|
||||
<div className="flex gap-2.5 border-b border-neutral-200 px-3 py-3 dark:border-neutral-800">
|
||||
<Avatar.Root className="shrink-0">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-10 w-10 rounded-lg object-cover"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
className="h-10 w-10 rounded-lg bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<div className="inline-flex flex-col">
|
||||
<h5 className="text-sm font-semibold">
|
||||
{user?.name ||
|
||||
user?.display_name ||
|
||||
user?.displayName ||
|
||||
user?.username}
|
||||
</h5>
|
||||
{user?.nip05 ? (
|
||||
<NIP05
|
||||
pubkey={pubkey}
|
||||
nip05={user.nip05}
|
||||
className="max-w-[15rem] truncate text-sm text-neutral-500 dark:text-neutral-300"
|
||||
/>
|
||||
) : (
|
||||
<span className="max-w-[15rem] truncate text-sm text-neutral-500 dark:text-neutral-300">
|
||||
{fallbackName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="line-clamp-3 break-all text-sm leading-tight text-neutral-900 dark:text-neutral-100">
|
||||
{user?.about}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-3">
|
||||
<Link
|
||||
to={`/users/${pubkey}`}
|
||||
className="inline-flex h-9 flex-1 items-center justify-center rounded-lg bg-neutral-200 text-sm font-semibold hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
View profile
|
||||
</Link>
|
||||
<Link
|
||||
to={`/chats/${pubkey}`}
|
||||
className="inline-flex h-9 flex-1 items-center justify-center rounded-lg bg-neutral-200 text-sm font-semibold hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Message
|
||||
</Link>
|
||||
</div>
|
||||
</HoverCard.Content>
|
||||
</HoverCard.Portal>
|
||||
</HoverCard.Root>
|
||||
);
|
||||
});
|
||||
8
packages/ui/tailwind.config.js
Normal file
8
packages/ui/tailwind.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import sharedConfig from "@lume/tailwindcss";
|
||||
|
||||
const config = {
|
||||
content: ["./src/**/*.{js,ts,jsx,tsx}"],
|
||||
presets: [sharedConfig],
|
||||
};
|
||||
|
||||
export default config;
|
||||
8
packages/ui/tsconfig.json
Normal file
8
packages/ui/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@lume/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user