Merge pull request #2 from recurse-chat/ui-improvements
UI improvements
This commit is contained in:
@@ -6,6 +6,7 @@ use tauri::{AppHandle, Manager};
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Account {
|
pub struct Account {
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
|
pub avatar: Option<String>,
|
||||||
pub private_key: String,
|
pub private_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-3
@@ -29,18 +29,26 @@ export default function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider attribute="class" defaultTheme="system">
|
<ThemeProvider attribute="class" defaultTheme="system">
|
||||||
<main className="size-screen flex">
|
<main className="size-screen flex select-none cursor-default">
|
||||||
{appRef.current?.accounts &&
|
{appRef.current?.accounts &&
|
||||||
appRef.current.accounts.accounts.length === 0 ? (
|
appRef.current.accounts.accounts.length === 0 ? (
|
||||||
<NewProfilePage appRef={appRef} />
|
<NewProfilePage appRef={appRef} />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ServerList appRef={appRef} />
|
|
||||||
<ResizablePanelGroup orientation="horizontal" className="h-screen">
|
<ResizablePanelGroup orientation="horizontal" className="h-screen">
|
||||||
<ResizablePanel defaultSize="420px">
|
<ResizablePanel
|
||||||
|
defaultSize="600px"
|
||||||
|
maxSize="400px"
|
||||||
|
minSize="4rem"
|
||||||
|
groupResizeBehavior="preserve-pixel-size"
|
||||||
|
>
|
||||||
|
<div className="flex">
|
||||||
|
<ServerList appRef={appRef} />
|
||||||
<Sidebar appRef={appRef} />
|
<Sidebar appRef={appRef} />
|
||||||
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle withHandle />
|
<ResizableHandle withHandle />
|
||||||
|
|
||||||
<ResizablePanel defaultSize="100%">
|
<ResizablePanel defaultSize="100%">
|
||||||
<div className="h-screen"></div>
|
<div className="h-screen"></div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
|
|||||||
+6
-2
@@ -5,7 +5,7 @@ import { sha512 } from "@noble/hashes/sha2.js";
|
|||||||
import { base58 } from "@scure/base";
|
import { base58 } from "@scure/base";
|
||||||
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { AccountsFile, getAccounts } from "@/lib/accounts";
|
import { Account, AccountsFile, getAccounts } from "@/lib/accounts";
|
||||||
|
|
||||||
ed.hashes.sha512 = sha512;
|
ed.hashes.sha512 = sha512;
|
||||||
|
|
||||||
@@ -38,6 +38,10 @@ export default class Enclave {
|
|||||||
this.accounts = await getAccounts();
|
this.accounts = await getAccounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getAccount(): Account | null {
|
||||||
|
return this.accounts?.accounts[this.accounts.activeAccount] || null;
|
||||||
|
}
|
||||||
|
|
||||||
public async connectToServer(hostname: string, isSecure: boolean) {
|
public async connectToServer(hostname: string, isSecure: boolean) {
|
||||||
if (this.server) {
|
if (this.server) {
|
||||||
this.server.disconnect();
|
this.server.disconnect();
|
||||||
@@ -87,7 +91,7 @@ export default class Enclave {
|
|||||||
public getClientSecretKey() {
|
public getClientSecretKey() {
|
||||||
if (!this.accounts) return null;
|
if (!this.accounts) return null;
|
||||||
|
|
||||||
const account = this.accounts?.accounts[this.accounts.activeAccount];
|
const account = this.getAccount();
|
||||||
|
|
||||||
return account ? base58.decode(account.privateKey) : null;
|
return account ? base58.decode(account.privateKey) : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Avatar({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: AvatarPrimitive.Root.Props & {
|
||||||
|
size?: "default" | "sm" | "lg"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
data-slot="avatar"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
data-slot="avatar-image"
|
||||||
|
className={cn(
|
||||||
|
"aspect-square size-full rounded-full object-cover",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarFallback({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: AvatarPrimitive.Fallback.Props) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
data-slot="avatar-fallback"
|
||||||
|
className={cn(
|
||||||
|
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="avatar-badge"
|
||||||
|
className={cn(
|
||||||
|
"absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none",
|
||||||
|
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
|
||||||
|
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
|
||||||
|
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="avatar-group"
|
||||||
|
className={cn(
|
||||||
|
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarGroupCount({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="avatar-group-count"
|
||||||
|
className={cn(
|
||||||
|
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Avatar,
|
||||||
|
AvatarImage,
|
||||||
|
AvatarFallback,
|
||||||
|
AvatarGroup,
|
||||||
|
AvatarGroupCount,
|
||||||
|
AvatarBadge,
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Card({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn(
|
||||||
|
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-(--card-spacing)", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Card } from "../ui/card";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||||
|
import { CopyIcon, Settings2Icon } from "lucide-react";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import Enclave from "@/app/app";
|
||||||
|
import { base58 } from "@scure/base";
|
||||||
|
import * as ed from "@noble/ed25519";
|
||||||
|
|
||||||
|
export default function AccountCard({
|
||||||
|
appRef,
|
||||||
|
}: {
|
||||||
|
appRef: React.RefObject<Enclave | null>;
|
||||||
|
}) {
|
||||||
|
const account = appRef.current?.getAccount();
|
||||||
|
|
||||||
|
if (!account) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute -left-16 right-16 bottom-0 h-20 w-[calc(100%+4rem-2rem)] z-10 ml-4 mb-4 @max-[150px]:hidden">
|
||||||
|
<Card className="px-3 py-3 h-full w-full flex flex-row">
|
||||||
|
<div className="flex gap-2.5 items-center">
|
||||||
|
<Avatar className="h-full w-auto aspect-square">
|
||||||
|
<AvatarImage src={account.avatar} />
|
||||||
|
<AvatarFallback>{account.displayName[0]}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span
|
||||||
|
className="flex items-center gap-1.5 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
base58.encode(
|
||||||
|
ed.getPublicKey(base58.decode(account.privateKey)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{account.displayName}
|
||||||
|
<CopyIcon className="text-muted-foreground size-3" />
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground">Online</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ml-auto flex items-center text-muted-foreground">
|
||||||
|
<Button variant="ghost" className="size-10">
|
||||||
|
<Settings2Icon className="size-5.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,6 +7,16 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { saveAccounts } from "@/lib/accounts";
|
import { saveAccounts } from "@/lib/accounts";
|
||||||
import Enclave from "@/app/app";
|
import Enclave from "@/app/app";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||||
|
|
||||||
|
function readFileAsDataUrl(file: File): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => resolve(reader.result as string);
|
||||||
|
reader.onerror = () => reject(new Error("Failed to read image"));
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function NewProfilePage({
|
export function NewProfilePage({
|
||||||
appRef,
|
appRef,
|
||||||
@@ -14,9 +24,28 @@ export function NewProfilePage({
|
|||||||
appRef: React.RefObject<Enclave | null>;
|
appRef: React.RefObject<Enclave | null>;
|
||||||
}) {
|
}) {
|
||||||
const [displayName, setDisplayName] = useState("");
|
const [displayName, setDisplayName] = useState("");
|
||||||
|
const [avatar, setAvatar] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
async function handleAvatarChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
if (!file.type.startsWith("image/")) {
|
||||||
|
setError("Please select an image file");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dataUrl = await readFileAsDataUrl(file);
|
||||||
|
setAvatar(dataUrl);
|
||||||
|
setError(null);
|
||||||
|
} catch {
|
||||||
|
setError("Failed to load image");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleCreate() {
|
async function handleCreate() {
|
||||||
const name = displayName.trim();
|
const name = displayName.trim();
|
||||||
if (!name) {
|
if (!name) {
|
||||||
@@ -42,6 +71,7 @@ export function NewProfilePage({
|
|||||||
const newAccount = {
|
const newAccount = {
|
||||||
displayName: name,
|
displayName: name,
|
||||||
privateKey: base58.encode(secretKey),
|
privateKey: base58.encode(secretKey),
|
||||||
|
avatar: avatar ?? undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
appRef.current.accounts.activeAccount =
|
appRef.current.accounts.activeAccount =
|
||||||
@@ -54,11 +84,13 @@ export function NewProfilePage({
|
|||||||
setError(e instanceof Error ? e.message : "Failed to create profile");
|
setError(e instanceof Error ? e.message : "Failed to create profile");
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen items-center justify-center">
|
<div className="flex h-screen w-full items-center justify-center">
|
||||||
<div className="w-full max-w-sm space-y-6">
|
<div className="w-full max-w-sm space-y-6">
|
||||||
<div className="space-y-2 text-center">
|
<div className="space-y-2 text-center">
|
||||||
<h1 className="text-2xl font-semibold">Create your profile</h1>
|
<h1 className="text-2xl font-semibold">Create your profile</h1>
|
||||||
@@ -68,6 +100,24 @@ export function NewProfilePage({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
<label htmlFor="avatar-upload" className="cursor-pointer">
|
||||||
|
<div className="h-20 w-20 overflow-hidden rounded-full border border-border bg-muted flex items-center justify-center hover:opacity-80 transition-opacity">
|
||||||
|
<Avatar className="h-full w-auto aspect-square">
|
||||||
|
{avatar && <AvatarImage src={avatar} />}
|
||||||
|
<AvatarFallback>{displayName[0] || "J"}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="avatar-upload"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleAvatarChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="display-name">Display name</Label>
|
<Label htmlFor="display-name">Display name</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import Enclave from "@/app/app";
|
import Enclave from "@/app/app";
|
||||||
import { Channel, ChannelKind } from "@/app/protocol";
|
import { Channel, ChannelKind } from "@/app/protocol";
|
||||||
import { HashIcon } from "lucide-react";
|
import { ChevronDown, ChevronUp, HashIcon } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import AccountCard from "./AccountCard";
|
||||||
|
|
||||||
export function ChannelIcon({ kind }: { kind: ChannelKind["kind"] }) {
|
export function ChannelIcon({ kind }: { kind: ChannelKind["kind"] }) {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
@@ -11,19 +13,39 @@ export function ChannelIcon({ kind }: { kind: ChannelKind["kind"] }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function RenderCategory({ channel }: { channel: Channel }) {
|
||||||
|
if (channel.kind !== "category") return null;
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="w-full px-2 py-1 rounded-md cursor-default text-sm text-foreground/70 flex items-center"
|
||||||
|
onClick={() => setOpen((v) => !v)}
|
||||||
|
>
|
||||||
|
{channel.name}
|
||||||
|
{open ? (
|
||||||
|
<ChevronDown className="p-1 shrink" />
|
||||||
|
) : (
|
||||||
|
<ChevronUp className="p-1 shrink" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{open && (
|
||||||
|
<div className="pt-0.5 w-full flex flex-col gap-1">
|
||||||
|
<RenderChannels channels={channel.channels} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function RenderChannels({ channels }: { channels: Channel[] }) {
|
export function RenderChannels({ channels }: { channels: Channel[] }) {
|
||||||
return channels.map((channel) =>
|
return channels.map((channel) =>
|
||||||
channel.kind === "category" ? (
|
channel.kind === "category" ? (
|
||||||
<div>
|
<RenderCategory channel={channel} />
|
||||||
<div className="w-full px-0.5 py-1 rounded-md select-none cursor-default text-sm text-foreground/70">
|
|
||||||
{channel.name}
|
|
||||||
</div>
|
|
||||||
<div className="pl-2 pr-1.5 pt-0.5 w-full flex flex-col gap-1">
|
|
||||||
<RenderChannels channels={channel.channels} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full hover:bg-accent px-2.5 py-1.5 rounded-md flex gap-2.5 items-center select-none cursor-default text-sm text-foreground/70">
|
<div className="w-full hover:bg-accent px-2.5 py-1.5 rounded-md flex gap-2.5 items-center select-none cursor-default text-sm text-muted-foreground">
|
||||||
<ChannelIcon kind={channel.kind} />
|
<ChannelIcon kind={channel.kind} />
|
||||||
{channel.name}
|
{channel.name}
|
||||||
</div>
|
</div>
|
||||||
@@ -37,8 +59,9 @@ export default function Sidebar({
|
|||||||
appRef: React.RefObject<Enclave | null>;
|
appRef: React.RefObject<Enclave | null>;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
appRef.current?.server?.meta && (
|
<div className="h-screen relative w-full @container">
|
||||||
<div className="h-screen">
|
{appRef.current?.server?.meta && (
|
||||||
|
<>
|
||||||
<header className="px-3 pt-2.5 pb-2.5 text-lg font-semibold border-b border-b-border">
|
<header className="px-3 pt-2.5 pb-2.5 text-lg font-semibold border-b border-b-border">
|
||||||
<h1>{appRef.current.server.meta.name}</h1>
|
<h1>{appRef.current.server.meta.name}</h1>
|
||||||
</header>
|
</header>
|
||||||
@@ -46,7 +69,10 @@ export default function Sidebar({
|
|||||||
<section className="px-1.5 pt-3.5 w-full flex flex-col gap-1">
|
<section className="px-1.5 pt-3.5 w-full flex flex-col gap-1">
|
||||||
<RenderChannels channels={appRef.current.server.meta.channels} />
|
<RenderChannels channels={appRef.current.server.meta.channels} />
|
||||||
</section>
|
</section>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AccountCard appRef={appRef} />
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default function ServerList({
|
|||||||
if (!appRef.current) return null;
|
if (!appRef.current) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="bg-card w-16 h-screen p-2 flex flex-col gap-2.5 border-r border-r-border">
|
<aside className="w-16 h-screen p-2 flex flex-col gap-2.5 border-r border-r-border shrink-0">
|
||||||
{Object.entries(appRef.current.serverList).map(([hostname, server]) => {
|
{Object.entries(appRef.current.serverList).map(([hostname, server]) => {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { invoke } from "@tauri-apps/api/core";
|
|||||||
|
|
||||||
export interface Account {
|
export interface Account {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
avatar?: string;
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user