Feat: UI fixes in graph

This commit is contained in:
Harshith Mullapudi 2025-07-11 09:10:23 +05:30
parent 26040ffb74
commit 50c4e2bcce
25 changed files with 276 additions and 229 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,13 +1,6 @@
import { useFetcher, useNavigate } from "@remix-run/react"; import { useFetcher, useNavigate } from "@remix-run/react";
import { useEffect, useState, useCallback, useRef } from "react"; import { useEffect, useState, useCallback, useRef } from "react";
import { import { AutoSizer, List, type ListRowRenderer } from "react-virtualized";
List,
AutoSizer,
InfiniteLoader,
type ListRowRenderer,
} from "react-virtualized";
import { format } from "date-fns";
import { MessageSquare, Clock, Plus } from "lucide-react";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { Button } from "../ui"; import { Button } from "../ui";
@ -40,10 +33,8 @@ type ConversationListResponse = {
export const ConversationList = ({ export const ConversationList = ({
currentConversationId, currentConversationId,
showNewConversationCTA,
}: { }: {
currentConversationId?: string; currentConversationId?: string;
showNewConversationCTA?: boolean;
}) => { }) => {
const fetcher = useFetcher<ConversationListResponse>(); const fetcher = useFetcher<ConversationListResponse>();
const navigate = useNavigate(); const navigate = useNavigate();
@ -51,8 +42,9 @@ export const ConversationList = ({
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [hasNextPage, setHasNextPage] = useState(true); const [hasNextPage, setHasNextPage] = useState(true);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
// const [searchTerm, setSearchTerm] = useState("");
// const searchTimeoutRef = useRef<NodeJS.Timeout>(); // Prevent duplicate conversations when paginating
const loadedConversationIds = useRef<Set<string>>(new Set());
const loadMoreConversations = useCallback( const loadMoreConversations = useCallback(
(page: number) => { (page: number) => {
@ -61,89 +53,75 @@ export const ConversationList = ({
setIsLoading(true); setIsLoading(true);
const searchParams = new URLSearchParams({ const searchParams = new URLSearchParams({
page: page.toString(), page: page.toString(),
limit: "25", limit: "5", // Increased for better density
}); });
fetcher.load(`/api/v1/conversations?${searchParams}`, { fetcher.load(`/api/v1/conversations?${searchParams}`, {
flushSync: true, flushSync: true,
}); });
}, },
[isLoading, fetcher, currentPage], [isLoading, fetcher],
); );
// Initial load
useEffect(() => { useEffect(() => {
loadMoreConversations(1); loadMoreConversations(1);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
// Handle fetcher response
useEffect(() => { useEffect(() => {
if (fetcher.data && fetcher.state === "idle") { if (fetcher.data && fetcher.state === "idle") {
setIsLoading(false); setIsLoading(false);
const response = fetcher.data; const response = fetcher.data;
if (currentPage === 1) { // Prevent duplicate conversations
setConversations(response.conversations); const newConversations = response.conversations.filter(
} else { (c) => !loadedConversationIds.current.has(c.id),
setConversations((prev) => [...prev, ...response.conversations]); );
} newConversations.forEach((c) => loadedConversationIds.current.add(c.id));
setConversations((prev) => [...prev, ...newConversations]);
setHasNextPage(response.pagination.hasNext); setHasNextPage(response.pagination.hasNext);
setCurrentPage(response.pagination.page); setCurrentPage(response.pagination.page);
} }
}, [fetcher.data, fetcher.state, currentPage]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetcher.data, fetcher.state]);
// const handleSearch = useCallback( // The row count is conversations.length + 1 if hasNextPage, else just conversations.length
// (term: string) => { const rowCount = hasNextPage
// setSearchTerm(term); ? conversations.length + 1
// setCurrentPage(1); : conversations.length;
// setConversations([]);
// setHasNextPage(true);
// if (searchTimeoutRef.current) {
// clearTimeout(searchTimeoutRef.current);
// }
// searchTimeoutRef.current = setTimeout(() => {
// loadMoreConversations(1);
// }, 300);
// },
// [loadMoreConversations],
// );
const isRowLoaded = useCallback(
({ index }: { index: number }) => {
return !!conversations[index];
},
[conversations],
);
const loadMoreRows = useCallback(() => {
if (!hasNextPage || isLoading) {
return Promise.resolve();
}
return new Promise<void>((resolve) => {
if (conversations.length === 25) {
const nextPage = currentPage + 1;
loadMoreConversations(nextPage);
const checkLoaded = () => {
if (!isLoading) {
resolve();
} else {
setTimeout(checkLoaded, 100);
}
};
checkLoaded();
}
});
}, [
hasNextPage,
isLoading,
currentPage,
loadMoreConversations,
conversations,
]);
const rowRenderer: ListRowRenderer = useCallback( const rowRenderer: ListRowRenderer = useCallback(
({ index, key, style }) => { ({ index, key, style }) => {
// If this is the last row and hasNextPage, show the Load More button
if (hasNextPage && index === conversations.length) {
return (
<div
key={key}
style={style}
className="-ml-1 flex items-center justify-start p-2 py-0 text-sm"
>
<Button
variant="link"
onClick={() => loadMoreConversations(currentPage + 1)}
disabled={isLoading}
className="w-fit underline underline-offset-4"
>
{isLoading ? (
<>
<div className="border-primary mr-2 h-4 w-4 animate-spin rounded-full border-2 border-t-transparent" />
Loading...
</>
) : (
"Load More"
)}
</Button>
</div>
);
}
const conversation = conversations[index]; const conversation = conversations[index];
if (!conversation) { if (!conversation) {
@ -158,16 +136,21 @@ export const ConversationList = ({
return ( return (
<div key={key} style={style}> <div key={key} style={style}>
<div className="p-1"> <div className="px-1 pr-2">
<Button <Button
variant="ghost" variant="ghost"
className={cn( className={cn(
"border-border h-auto w-full justify-start p-2 text-left", "border-border h-auto w-full justify-start rounded p-2 py-1 text-left",
currentConversationId === conversation.id && "bg-grayAlpha-100", currentConversationId === conversation.id &&
"bg-accent text-accent-foreground font-semibold",
)} )}
onClick={() => { onClick={() => {
window.location.href = `/home/conversation/${conversation.id}`; navigate(`/home/conversation/${conversation.id}`);
}} }}
tabIndex={0}
aria-current={
currentConversationId === conversation.id ? "page" : undefined
}
> >
<div className="flex w-full items-start space-x-3"> <div className="flex w-full items-start space-x-3">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
@ -175,11 +158,6 @@ export const ConversationList = ({
<p className={cn("truncate font-normal")}> <p className={cn("truncate font-normal")}>
{conversation.title || "Untitled Conversation"} {conversation.title || "Untitled Conversation"}
</p> </p>
<div className="text-muted-foreground flex items-center space-x-1 text-xs">
<span>
{format(new Date(conversation.updatedAt), "MMM d")}
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -188,62 +166,32 @@ export const ConversationList = ({
</div> </div>
); );
}, },
[conversations], [
conversations,
currentConversationId,
hasNextPage,
isLoading,
currentPage,
loadMoreConversations,
navigate,
],
); );
const rowCount = hasNextPage
? conversations.length + 1
: conversations.length;
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col pt-1 pl-1">
{showNewConversationCTA && ( <div className="grow overflow-hidden">
<div className="flex items-center justify-start p-1 pb-0"> <AutoSizer>
<Button {({ height, width }) => (
variant="ghost" <List
className="w-full justify-start gap-2 py-4" height={height}
onClick={() => { width={width}
navigate("/home/conversation"); rowCount={rowCount}
}} rowHeight={36} // Slightly taller for better click area
> rowRenderer={rowRenderer}
<Plus size={14} /> New conversation overscanRowCount={5}
</Button> />
</div>
)}
{/* <div className="border-b">
<Input
type="text"
placeholder="Search conversations..."
className="focus:ring-primary w-full rounded-none px-3 py-2 focus:ring-2 focus:outline-none"
value={searchTerm}
onChange={(e) => handleSearch(e.target.value)}
/>
</div> */}
<div className="flex-1 overflow-hidden">
<InfiniteLoader
isRowLoaded={isRowLoaded}
loadMoreRows={loadMoreRows}
rowCount={rowCount}
threshold={5}
>
{({ onRowsRendered, registerChild }) => (
<AutoSizer>
{({ height, width }) => (
<List
ref={registerChild}
height={height}
width={width}
rowCount={rowCount}
rowHeight={40}
onRowsRendered={onRowsRendered}
rowRenderer={rowRenderer}
overscanRowCount={5}
/>
)}
</AutoSizer>
)} )}
</InfiniteLoader> </AutoSizer>
</div> </div>
{isLoading && conversations.length === 0 && ( {isLoading && conversations.length === 0 && (

View File

@ -8,7 +8,7 @@ import { EditorContent, Placeholder, EditorRoot } from "novel";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { Button } from "../ui"; import { Button } from "../ui";
import { Loader } from "lucide-react"; import { Loader, LoaderCircle } from "lucide-react";
import { Form, useSubmit } from "@remix-run/react"; import { Form, useSubmit } from "@remix-run/react";
interface ConversationTextareaProps { interface ConversationTextareaProps {
@ -159,7 +159,7 @@ export function ConversationTextarea({
> >
{isLoading ? ( {isLoading ? (
<> <>
<Loader size={18} className="mr-1 animate-spin" /> <LoaderCircle size={18} className="mr-1 animate-spin" />
Stop Stop
</> </>
) : ( ) : (

View File

@ -46,17 +46,7 @@ export const ConversationNew = ({
); );
return ( return (
<ResizablePanelGroup direction="horizontal" className="bg-background-2"> <ResizablePanelGroup direction="horizontal" className="rounded-md">
<ResizablePanel
maxSize={50}
defaultSize={16}
minSize={16}
collapsible
collapsedSize={16}
className="border-border h-[calc(100vh_-_60px)] min-w-[200px] border-r-1"
>
<ConversationList />
</ResizablePanel>
<ResizableHandle className="w-1" /> <ResizableHandle className="w-1" />
<ResizablePanel <ResizablePanel
@ -70,7 +60,7 @@ export const ConversationNew = ({
onSubmit={(e) => submitForm(e)} onSubmit={(e) => submitForm(e)}
className="pt-2" className="pt-2"
> >
<div className={cn("flex h-[calc(100vh_-_60px)] flex-col")}> <div className={cn("flex h-[calc(100vh_-_56px)] flex-col")}>
<div className="flex h-full w-full flex-col items-start justify-start overflow-y-auto p-4"> <div className="flex h-full w-full flex-col items-start justify-start overflow-y-auto p-4">
<div className="flex w-full flex-col items-center"> <div className="flex w-full flex-col items-center">
<div className="w-full max-w-[90ch]"> <div className="w-full max-w-[90ch]">

View File

@ -134,8 +134,6 @@ export const extensionsForConversation = [
tiptapLink, tiptapLink,
horizontalRule, horizontalRule,
heading, heading,
AIHighlight,
HighlightExtension,
Table.configure({ Table.configure({
resizable: true, resizable: true,
}), }),

View File

@ -310,7 +310,7 @@ export const Graph = forwardRef<GraphRef, GraphProps>(
...settings, ...settings,
barnesHutOptimize: true, barnesHutOptimize: true,
strongGravityMode: false, strongGravityMode: false,
gravity: 0.05, gravity: 0.1,
scalingRatio: 10, scalingRatio: 10,
slowDown: 5, slowDown: 5,
}, },

View File

@ -13,6 +13,7 @@ import { NavMain } from "./nav-main";
import { useUser } from "~/hooks/useUser"; import { useUser } from "~/hooks/useUser";
import { NavUser } from "./nav-user"; import { NavUser } from "./nav-user";
import Logo from "../logo/logo"; import Logo from "../logo/logo";
import { ConversationList } from "../conversation";
const data = { const data = {
navMain: [ navMain: [
@ -44,24 +45,29 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return ( return (
<Sidebar <Sidebar
collapsible="none" variant="inset"
{...props} {...props}
className="bg-background h-[100vh] w-[calc(var(--sidebar-width-icon)+1px)]! py-2" className="bg-background h-[100vh] py-2"
> >
<SidebarHeader> <SidebarHeader>
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem> <SidebarMenuItem>
<div className="mt-1 flex w-full items-center justify-center"> <div className="mt-1 flex w-full items-center justify-start gap-2">
<Logo width={20} height={20} /> <Logo width={20} height={20} />
C.O.R.E.
</div> </div>
</SidebarMenuItem> </SidebarMenuItem>
</SidebarMenu> </SidebarMenu>
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent>
<NavMain items={data.navMain} /> <NavMain items={data.navMain} />
<div className="mt-4 flex h-full flex-col">
<h2 className="text-muted-foreground px-4 text-sm"> History </h2>
<ConversationList />
</div>
</SidebarContent> </SidebarContent>
<SidebarFooter className="p-0"> <SidebarFooter className="px-2">
<NavUser user={user} /> <NavUser user={user} />
</SidebarFooter> </SidebarFooter>
</Sidebar> </Sidebar>

View File

@ -7,6 +7,7 @@ import {
SidebarMenuItem, SidebarMenuItem,
} from "../ui/sidebar"; } from "../ui/sidebar";
import { useLocation, useNavigate } from "@remix-run/react"; import { useLocation, useNavigate } from "@remix-run/react";
import { Button } from "../ui";
export const NavMain = ({ export const NavMain = ({
items, items,
@ -23,20 +24,22 @@ export const NavMain = ({
return ( return (
<SidebarGroup> <SidebarGroup>
<SidebarGroupContent className="flex flex-col gap-2"> <SidebarGroupContent className="flex flex-col gap-2">
<SidebarMenu> <SidebarMenu className="gap-0.5">
{items.map((item) => ( {items.map((item) => (
<SidebarMenuItem key={item.title}> <SidebarMenuItem key={item.title}>
<SidebarMenuButton <Button
tooltip={item.title}
isActive={location.pathname.includes(item.url)} isActive={location.pathname.includes(item.url)}
className={cn( className={cn(
"bg-grayAlpha-100 w-fit gap-1 !rounded-md",
location.pathname.includes(item.url) && location.pathname.includes(item.url) &&
"!bg-grayAlpha-100 hover:bg-grayAlpha-100!", "!bg-accent !text-accent-foreground",
)} )}
onClick={() => navigate(item.url)} onClick={() => navigate(item.url)}
variant="ghost"
> >
{item.icon && <item.icon />} {item.icon && <item.icon size={16} />}
</SidebarMenuButton> {item.title}
</Button>
</SidebarMenuItem> </SidebarMenuItem>
))} ))}
</SidebarMenu> </SidebarMenu>

View File

@ -26,26 +26,10 @@ export function NavUser({ user }: { user: User }) {
return ( return (
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem className="mb-2 flex justify-center"> <SidebarMenuItem className="flex justify-between">
<Button
variant="ghost"
isActive={location.pathname.includes("settings")}
className={cn(
location.pathname.includes("settings") &&
"!bg-grayAlpha-100 hover:bg-grayAlpha-100!",
)}
onClick={() => navigate("/settings")}
>
<Settings size={18} />
</Button>
</SidebarMenuItem>
<SidebarMenuItem>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button variant="link" className="mb-2 ml-2 gap-2 px-0">
variant="link"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground mb-2 gap-2 px-3"
>
<AvatarText <AvatarText
text={user.name ?? "User"} text={user.name ?? "User"}
className="h-6 w-6 rounded" className="h-6 w-6 rounded"
@ -55,7 +39,7 @@ export function NavUser({ user }: { user: User }) {
<DropdownMenuContent <DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg" className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
side={isMobile ? "bottom" : "top"} side={isMobile ? "bottom" : "top"}
align="end" align="start"
sideOffset={4} sideOffset={4}
> >
<DropdownMenuLabel className="p-0 font-normal"> <DropdownMenuLabel className="p-0 font-normal">
@ -71,7 +55,14 @@ export function NavUser({ user }: { user: User }) {
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
className="flex gap-2" className="flex gap-2"
onClick={() => (window.location.href = "/logout")} onClick={() => navigate("/settings")}
>
<Settings size={16} />
Settings
</DropdownMenuItem>
<DropdownMenuItem
className="flex gap-2"
onClick={() => navigate("/logout")}
> >
<LogOut size={16} /> <LogOut size={16} />
Log out Log out

View File

@ -1,4 +1,7 @@
import { useLocation } from "@remix-run/react"; import { useLocation, useNavigate } from "@remix-run/react";
import { Button } from "./button";
import { Plus } from "lucide-react";
import { SidebarTrigger } from "./sidebar";
const PAGE_TITLES: Record<string, string> = { const PAGE_TITLES: Record<string, string> = {
"/home/dashboard": "Memory graph", "/home/dashboard": "Memory graph",
@ -18,14 +21,37 @@ function getHeaderTitle(pathname: string): string {
return "Documents"; return "Documents";
} }
function isConversationDetail(pathname: string): boolean {
// Matches /home/conversation/<something> but not /home/conversation exactly
return /^\/home\/conversation\/[^/]+$/.test(pathname);
}
export function SiteHeader() { export function SiteHeader() {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate();
const title = getHeaderTitle(location.pathname); const title = getHeaderTitle(location.pathname);
const showNewConversationButton = isConversationDetail(location.pathname);
return ( return (
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)"> <header className="border-border flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
<div className="flex w-full items-center gap-1 px-4 lg:gap-2"> <div className="flex w-full items-center justify-between gap-1 px-4 pr-2 lg:gap-2">
<h1 className="text-base">{title}</h1> <div className="flex items-center gap-1">
<SidebarTrigger className="-ml-1" />
<h1 className="text-base">{title}</h1>
</div>
<div>
{showNewConversationButton && (
<Button
onClick={() => navigate("/home/conversation")}
variant="secondary"
className="gap-2"
>
<Plus size={14} />
New conversation
</Button>
)}
</div>
</div> </div>
</header> </header>
); );

View File

@ -227,7 +227,7 @@ function Sidebar({
<div <div
data-slot="sidebar-container" data-slot="sidebar-container"
className={cn( className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex", "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) !px-0 transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left" side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]" ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]", : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
@ -514,7 +514,11 @@ function SidebarMenuButton({
data-sidebar="menu-button" data-sidebar="menu-button"
data-size={size} data-size={size}
data-active={isActive} data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)} className={cn(
sidebarMenuButtonVariants({ variant, size }),
className,
"rounded-md",
)}
{...props} {...props}
/> />
); );

View File

@ -129,29 +129,16 @@ export default function SingleConversation() {
} }
return ( return (
<ResizablePanelGroup direction="horizontal" className="bg-background-2"> <ResizablePanelGroup direction="horizontal" className="!rounded-md">
<ResizablePanel
maxSize={50}
defaultSize={16}
minSize={16}
collapsible
collapsedSize={16}
className="border-border h-[calc(100vh_-_60px)] min-w-[200px] border-r-1"
>
<ConversationList
currentConversationId={conversationId}
showNewConversationCTA
/>
</ResizablePanel>
<ResizableHandle className="w-1" /> <ResizableHandle className="w-1" />
<ResizablePanel <ResizablePanel
collapsible collapsible
collapsedSize={0} collapsedSize={0}
className="flex h-[calc(100vh_-_24px)] w-full flex-col" className="flex w-full flex-col"
> >
<div className="relative flex h-[calc(100vh_-_70px)] w-full flex-col items-center justify-center overflow-auto"> <div className="relative flex h-[calc(100vh_-_70px)] w-full flex-col items-center justify-center overflow-auto">
<div className="flex h-[calc(100vh_-_60px)] w-full flex-col justify-end overflow-hidden"> <div className="flex h-[calc(100vh_-_56px)] w-full flex-col justify-end overflow-hidden">
<ScrollAreaWithAutoScroll> <ScrollAreaWithAutoScroll>
{getConversations()} {getConversations()}
{conversationResponse && ( {conversationResponse && (

View File

@ -13,6 +13,7 @@ import { useTypedLoaderData } from "remix-typedjson";
import { SearchBodyRequest } from "./search"; import { SearchBodyRequest } from "./search";
import { SearchService } from "~/services/search.server"; import { SearchService } from "~/services/search.server";
import { GraphVisualizationClient } from "~/components/graph/graph-client"; import { GraphVisualizationClient } from "~/components/graph/graph-client";
import { LoaderCircle } from "lucide-react";
export async function action({ request }: ActionFunctionArgs) { export async function action({ request }: ActionFunctionArgs) {
const userId = await requireUserId(request); const userId = await requireUserId(request);
@ -83,11 +84,11 @@ export default function Dashboard() {
}, [userId]); }, [userId]);
return ( return (
<div className="home flex h-[calc(100vh_-_60px)] flex-col overflow-y-auto p-3 text-base"> <div className="home flex h-[calc(100vh_-_56px)] flex-col overflow-y-auto p-3 text-base">
<div className="flex grow items-center justify-center rounded"> <div className="flex grow items-center justify-center rounded">
{loading ? ( {loading ? (
<div className="flex h-full w-full flex-col items-center justify-center"> <div className="flex h-full w-full flex-col items-center justify-center">
<div className="mb-2 h-8 w-8 animate-spin rounded-full border-b-2 border-gray-400" /> <LoaderCircle size={18} className="mr-1 animate-spin" />
<span className="text-muted-foreground">Loading graph...</span> <span className="text-muted-foreground">Loading graph...</span>
</div> </div>
) : ( ) : (

View File

@ -33,19 +33,17 @@ export default function Home() {
{ {
"--sidebar-width": "calc(var(--spacing) * 54)", "--sidebar-width": "calc(var(--spacing) * 54)",
"--header-height": "calc(var(--spacing) * 12)", "--header-height": "calc(var(--spacing) * 12)",
background: "var(--background-2)", background: "var(--background)",
} as React.CSSProperties } as React.CSSProperties
} }
> >
<AppSidebar variant="inset" /> <AppSidebar variant="inset" />
<SidebarInset className="bg-background h-[100vh] py-2 pr-2"> <SidebarInset className="bg-background-2 h-full rounded pr-0">
<div className="bg-background-2 h-full rounded-md"> <SiteHeader />
<SiteHeader /> <div className="flex h-[calc(100vh_-_56px)] flex-col rounded">
<div className="flex h-[calc(100vh_-_60px)] flex-col"> <div className="@container/main flex h-full flex-col gap-2">
<div className="@container/main flex h-full flex-col gap-2"> <div className="flex h-full flex-col">
<div className="flex h-full flex-col"> <Outlet />
<Outlet />
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -322,7 +322,7 @@
@layer base { @layer base {
* { * {
@apply border-border outline-ring/50; @apply border-border outline-ring/50;
--header-height: 44px; --header-height: 40px;
} }
body { body {
@apply bg-background-2 text-foreground text-base; @apply bg-background-2 text-foreground text-base;
@ -467,3 +467,74 @@
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10' style='fill: rgba(255,255,255, 0.5)'%3E%3Cpath d='M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7.55228475,8 8,8.44771525 8,9 C8,9.55228475 7.55228475,10 7,10 Z'%3E%3C/path%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10' style='fill: rgba(255,255,255, 0.5)'%3E%3Cpath d='M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7.55228475,8 8,8.44771525 8,9 C8,9.55228475 7.55228475,10 7,10 Z'%3E%3C/path%3E%3C/svg%3E");
} }
@layer base {
.tiptap {
:first-child {
margin-top: 0;
}
pre {
@apply bg-grayAlpha-100 text-foreground p-4 rounded-md w-fit;
margin: 1.5rem 0;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
/* Code styling */
.hljs-comment,
.hljs-quote {
@apply text-muted-foreground;
}
.hljs-variable,
.hljs-template-variable,
.hljs-attribute,
.hljs-tag,
.hljs-regexp,
.hljs-link,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: var(--custom-color-4); /* #886dbc */
}
.hljs-number,
.hljs-meta,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params {
color: var(--custom-color-2); /* #7b8a34 */
}
.hljs-string,
.hljs-symbol,
.hljs-bullet {
color: var(--custom-color-3); /* #1c91a8 */
}
.hljs-title,
.hljs-section {
color: var(--custom-color-1); /* #b56455 */
}
.hljs-keyword,
.hljs-selector-tag {
color: var(--custom-color-5); /* #ad6e30 */
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 79 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 79 KiB

BIN
apps/webapp/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB