Added mobile

This commit is contained in:
Alois 2026-04-04 16:50:23 +02:00
commit 80114d1d66
125 changed files with 8200 additions and 542 deletions

View file

@ -6,7 +6,7 @@
"scripts": {
"format": "bunx prettier --write .",
"lint": "eslint src",
"dev": "vite --port 3000",
"dev": "vite --port 3000 --host 0.0.0.0",
"test": "bun test --pass-with-no-tests",
"build": "bun run test && tsc -b && vite build",
"preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .."
@ -25,6 +25,7 @@
"@tensamin/ttp": "workspace:*",
"@tensamin/ui": "workspace:*",
"@tensamin/user": "workspace:*",
"@tensamin/mobile": "workspace:*",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"comlink": "^4.4.2",

View file

@ -1,5 +1,5 @@
import { Button } from "@tensamin/ui/cmp/button";
import { House, Phone } from "lucide-react";
import { House, Phone, User } from "lucide-react";
import { useLocation, useNavigate, useSearch } from "@tanstack/react-router";
import { useCall } from "@tensamin/call/context";
import Wrapper from "@tensamin/user/wrapper";
@ -13,6 +13,7 @@ import {
} from "@tensamin/ui/cmp/select";
import { displayCallId } from "@tensamin/call/utils";
import { useState } from "react";
import { SidebarTrigger, useSidebar } from "@tensamin/ui/cmp/sidebar";
export default function Navbar() {
const navigate = useNavigate();
@ -28,7 +29,7 @@ export default function Navbar() {
const [selectOpen, setSelectOpen] = useState(false);
return (
<div className="w-full h-13.5 flex items-center justify-center">
<div className="w-full gap-2 h-13.5 flex items-center justify-center">
<Button
onClick={() => navigate({ to: "/" })}
className="w-9 h-9 aspect-square rounded-lg"
@ -40,7 +41,7 @@ export default function Navbar() {
<Wrapper
userId={id}
component={(user) => (
<p className="font-medium pl-3 text-md">{user?.display}</p>
<p className="font-medium text-md">{user?.display}</p>
)}
loading={<Skeleton className="ml-3 w-40 h-5" />}
/>
@ -100,3 +101,29 @@ export default function Navbar() {
</div>
);
}
export function MobileNavbar() {
const navigate = useNavigate();
const { setOpenMobile } = useSidebar();
return (
<div className="z-100 w-full h-16 flex justify-center items-center gap-8 bg-card border-t">
<SidebarTrigger
className="text-foreground w-12! h-12! aspect-square rounded-xl"
variant="link"
>
<User className="size-5.5" />
</SidebarTrigger>
<Button
onClick={() => {
navigate({ to: "/" });
setOpenMobile(false);
}}
className="text-foreground w-12 h-12 aspect-square rounded-xl"
variant="link"
>
<House className="size-5.5" />
</Button>
</div>
);
}

View file

@ -4,6 +4,11 @@ import * as React from "react";
import { Basic, Loading } from "./modals/basic";
import List from "@/features/conversation/list/body";
import {
Sidebar as SidebarRoot,
SidebarContent,
} from "@tensamin/ui/cmp/sidebar";
/**
* Renders the conversation sidebar with account summary and conversation list.
* @returns Sidebar JSX.
@ -17,17 +22,21 @@ export default function Sidebar() {
}, [load]);
return (
<div className="w-65 h-full flex flex-col gap-3 p-2">
<div>
<Wrapper
loading={<Loading />}
userId={userId}
component={(user) => <Basic user={user} />}
/>
</div>
<div className="h-full">
<List />
</div>
</div>
<SidebarRoot>
<SidebarContent>
<div className="h-full w-full flex flex-col gap-3 p-2">
<div>
<Wrapper
loading={<Loading />}
userId={userId}
component={(user) => <Basic user={user} />}
/>
</div>
<div className="h-full">
<List />
</div>
</div>
</SidebarContent>
</SidebarRoot>
);
}

View file

@ -1,5 +1,3 @@
import * as React from "react";
type Category = "conversations" | "communities";
/**
@ -11,28 +9,6 @@ export default function Switch(props: {
category: Category;
setCategory: (category: Category) => void;
}) {
const conversationsRef = React.useRef<HTMLButtonElement | null>(null);
const communitiesRef = React.useRef<HTMLButtonElement | null>(null);
const [indicator, setIndicator] = React.useState({ left: 0, width: 0 });
const updateIndicator = React.useCallback(() => {
const active =
props.category === "conversations"
? conversationsRef.current
: communitiesRef.current;
if (!active) return;
setIndicator({
left: active.offsetLeft,
width: active.offsetWidth,
});
}, [props.category]);
React.useLayoutEffect(() => {
updateIndicator();
}, [updateIndicator]);
/**
* Executes toggleCategory.
* @param none This function has no parameters.
@ -47,20 +23,19 @@ export default function Switch(props: {
return (
<div
role="tablist"
className="border relative inline-flex rounded-full bg-card p-1 select-none"
className="border relative grid grid-cols-2 rounded-full bg-card p-1 select-none"
>
<div
className="absolute top-1 bottom-1 rounded-full bg-input shadow-sm transition-all duration-300 ease-in-out"
style={{
left: `${indicator.left}px`,
width: `${indicator.width}px`,
left: props.category === "conversations" ? "0.25rem" : "50%",
width: "calc(50% - 0.25rem)",
}}
/>
<button
ref={conversationsRef}
role="tab"
aria-selected={props.category === "conversations"}
className={`relative z-10 cursor-pointer px-2.5 py-1.5 rounded-full text-sm font-medium transition-colors duration-300 ${
className={`relative z-10 w-full cursor-pointer rounded-full px-2.5 py-1.5 text-center text-sm font-medium transition-colors duration-300 ${
props.category === "conversations"
? "text-foreground"
: "text-ring/50 hover:text-ring"
@ -70,10 +45,9 @@ export default function Switch(props: {
Conversations
</button>
<button
ref={communitiesRef}
role="tab"
aria-selected={props.category === "communities"}
className={`relative z-10 cursor-pointer px-2.5 py-1.5 rounded-full text-sm font-medium transition-colors duration-300 ${
className={`relative z-10 w-full cursor-pointer rounded-full px-2.5 py-1.5 text-center text-sm font-medium transition-colors duration-300 ${
props.category === "communities"
? "text-foreground"
: "text-ring/50 hover:text-ring"

View file

@ -14,18 +14,20 @@ export default function ConversationModal({ userId }: { userId: number }) {
return (
<ContextMenu>
<ContextMenuTrigger asChild>
<div
className="select-none cursor-pointer"
onClick={() => navigate({ to: "/chat", search: { id: userId } })}
>
<Wrapper
loading={<Loading />}
userId={userId}
component={(user) => <Basic user={user} />}
/>
</div>
</ContextMenuTrigger>
<ContextMenuTrigger
render={
<div
className="select-none cursor-pointer"
onClick={() => navigate({ to: "/chat", search: { id: userId } })}
>
<Wrapper
loading={<Loading />}
userId={userId}
component={(user) => <Basic user={user} />}
/>
</div>
}
/>
<ContextMenuContent>
<ContextMenuGroup>
<ContextMenuItem>Test</ContextMenuItem>

View file

@ -70,9 +70,7 @@ function AddConversationButton() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Add Conversation</Button>
</DialogTrigger>
<DialogTrigger render={<Button>Add Conversation</Button>} />
<DialogContent>
<DialogHeader>
<DialogTitle>New Conversation</DialogTitle>
@ -98,9 +96,7 @@ function AddConversationButton() {
placeholder="Enter username..."
/>
<div className="flex w-full justify-end gap-1">
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<DialogClose render={<Button variant="outline">Cancel</Button>} />
<Button disabled={loading} type="submit">
{loading && <Loader2 className="animate-spin" />} Continue
</Button>

View file

@ -1,7 +1,10 @@
import { type ReactNode } from "react";
import Sidebar from "@/components/sidebar";
import Navbar from "@/components/navbar";
import Navbar, { MobileNavbar } from "@/components/navbar";
import { SidebarProvider } from "@tensamin/ui/cmp/sidebar";
import { useIsMobile, cn } from "@tensamin/ui/utils";
import { motion } from "framer-motion";
@ -11,6 +14,8 @@ import { motion } from "framer-motion";
* @returns unknown.
*/
export default function Layout(props: { children: ReactNode }) {
const isMobile = useIsMobile();
return (
<motion.div
className="w-full h-full flex bg-sidebar"
@ -26,13 +31,21 @@ export default function Layout(props: { children: ReactNode }) {
},
}}
>
<Sidebar />
<div className="w-full h-full flex flex-col">
<Navbar />
<div className="bg-background h-full w-full rounded-tl-3xl border-t border-l">
{props.children}
<SidebarProvider>
<Sidebar />
<div className="w-full h-full flex flex-col">
{!isMobile && <Navbar />}
<div
className={cn(
"bg-background h-full w-full",
!isMobile && "rounded-tl-3xl border-t border-l",
)}
>
{props.children}
</div>
{isMobile && <MobileNavbar />}
</div>
</div>
</SidebarProvider>
</motion.div>
);
}

View file

@ -2,6 +2,7 @@ import { useEffect, useState, type ReactNode } from "react";
import Storage from "@tensamin/storage/context";
import Crypto from "@tensamin/crypto/context";
import Mobile from "@tensamin/mobile/context";
import LegalWrapper from "@/features/legal/screen";
@ -10,6 +11,7 @@ import { TooltipProvider } from "@tensamin/ui/cmp/tooltip";
import { useStorage } from "@tensamin/storage/context";
import { useLocation, useNavigate } from "@tanstack/react-router";
import { useIsMobile } from "@tensamin/ui/utils";
/**
* Executes Layout.
@ -20,6 +22,8 @@ export default function Layout(props: { children: ReactNode }) {
const [pixelRatio, setPixelRatio] = useState(devicePixelRatio);
const [visible, setVisible] = useState(false);
const isMobile = useIsMobile();
useEffect(() => {
const handleResize = () => {
setPixelRatio(devicePixelRatio);
@ -35,7 +39,9 @@ export default function Layout(props: { children: ReactNode }) {
<>
<div className="fixed top-0 left-0 z-100 w-screen h-screen flex flex-col justify-center items-center pointer-events-none select-none">
{visible && (
<div className="bg-card border p-1 text-xs">{pixelRatio}</div>
<div className="bg-card border p-1 text-xs">
{Math.round(pixelRatio)}
</div>
)}
{/* Todo: Make this nicer */}
@ -45,12 +51,14 @@ export default function Layout(props: { children: ReactNode }) {
</div>
)}
</div>
<Toaster />
<Toaster position={isMobile ? "top-center" : "bottom-right"} />
<TooltipProvider>
<Storage>
<LoginWrapper>
<LegalWrapper>
<Crypto>{props.children}</Crypto>
<Crypto>
<Mobile>{props.children}</Mobile>
</Crypto>
</LegalWrapper>
</LoginWrapper>
</Storage>