Cleanup: scope drawer clip-path ID with useId(), fix indentation, add layout comments

This commit is contained in:
Lemon
2026-03-20 23:33:42 -07:00
parent ae0c90e1ce
commit b94ef0a46c
4 changed files with 55 additions and 45 deletions
+3
View File
@@ -280,6 +280,9 @@ export function Feed({ kinds, tagFilters, header, hideCompose, emptyMessage, fee
/>
)}
{/* Compose box and header render *after* the tab bar so that tabs stay
at the top as the primary navigation element on every feed page.
This is intentional — see commits 3e7edf65, 82fe6f2f. */}
{!hideCompose && <ComposeBox compact />}
{header}
+6
View File
@@ -88,6 +88,12 @@ function MainLayoutInner() {
{/* Main content + right sidebar: inside Suspense so the left sidebar persists while lazy pages load */}
<Suspense fallback={<PageSkeleton />}>
{/* -mt-mobile-bar pulls content up behind the mobile top bar so the
transparent SVG header arc and page content overlap seamlessly.
The corresponding padding-top (set in CSS) prevents content from
being hidden. This depends on MobileTopBar having a transparent /
semi-transparent background — a solid top bar would obscure the
content underneath. Only active below the sidebar breakpoint. */}
<div className={cn("relative flex-1 min-w-0 sidebar:border-l sidebar:border-r border-border bg-background/85 -mt-mobile-bar", !noMaxWidth && "sidebar:max-w-[600px]", !noOverscroll && "pb-overscroll")}>
<Outlet />
{showFAB && (
+10 -9
View File
@@ -1,4 +1,4 @@
import { useState, useMemo } from 'react';
import { useState, useId, useMemo } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { ChevronDown, ChevronUp, LogOut, UserPlus, Loader2 } from 'lucide-react';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
@@ -32,11 +32,10 @@ import { resolveTheme, resolveThemeConfig } from '@/themes';
/** Total width of the drawer background layer: 300px drawer + 36px arc overhang. */
const DRAWER_BG_WIDTH = 336;
/** Shared clip-path style for the drawer arc background layers. */
const drawerClipStyle: React.CSSProperties = {
width: DRAWER_BG_WIDTH,
clipPath: 'url(#drawer-arc-clip)',
};
/** Build the shared clip-path style for the drawer arc background layers. */
function drawerClipStyle(clipId: string): React.CSSProperties {
return { width: DRAWER_BG_WIDTH, clipPath: `url(#${clipId})` };
}
interface MobileDrawerProps {
open: boolean;
@@ -44,6 +43,8 @@ interface MobileDrawerProps {
}
export function MobileDrawer({ open, onOpenChange }: MobileDrawerProps) {
const clipId = `${useId()}-drawer-arc-clip`;
const clipStyle = drawerClipStyle(clipId);
const location = useLocation();
const navigate = useNavigate();
const { user, metadata, event: currentUserEvent } = useCurrentUser();
@@ -115,7 +116,7 @@ export function MobileDrawer({ open, onOpenChange }: MobileDrawerProps) {
drawer while the extra 36px overflows for the curved bulge. */}
<svg className="absolute" width="0" height="0" aria-hidden="true">
<defs>
<clipPath id="drawer-arc-clip" clipPathUnits="objectBoundingBox">
<clipPath id={clipId} clipPathUnits="objectBoundingBox">
<path d="M0,0 L0.893,0 Q1,0.5 0.893,1 L0,1 Z" />
</clipPath>
</defs>
@@ -125,12 +126,12 @@ export function MobileDrawer({ open, onOpenChange }: MobileDrawerProps) {
seamlessly through both regions. */}
<div
className="absolute top-0 left-0 bottom-0 pointer-events-none bg-background"
style={{ ...bgStyle, ...drawerClipStyle }}
style={{ ...bgStyle, ...clipStyle }}
/>
{hasBgImage && (
<div
className="absolute top-0 left-0 bottom-0 bg-background/70 pointer-events-none"
style={drawerClipStyle}
style={clipStyle}
/>
)}
<SheetTitle className="sr-only">Navigation menu</SheetTitle>
+36 -36
View File
@@ -419,43 +419,43 @@ export function AIChatPage() {
<div className="shrink-0 px-4 py-3 flex flex-col sidebar:flex-row sidebar:items-center sidebar:justify-between gap-2 sidebar:gap-3">
<PageHeader title="AI Chat" icon={<Bot className="size-5" />} className="px-0 mt-0 mb-0" />
<div className="flex items-center gap-2">
{/* Model selector */}
<Select value={selectedModel} onValueChange={setSelectedModel} disabled={modelsLoading}>
<SelectTrigger className="w-full sidebar:w-44 h-8 text-base md:text-xs">
<SelectValue placeholder={modelsLoading ? 'Loading models...' : 'Select model'} />
</SelectTrigger>
<SelectContent>
{models.map((model) => {
const totalCost = parseFloat(model.pricing.prompt) + parseFloat(model.pricing.completion);
const isFree = totalCost === 0;
return (
<SelectItem key={model.id} value={model.id}>
<span className="flex items-center gap-1.5">
{model.name}
{isFree && (
<span className="text-[10px] font-medium text-green-600 dark:text-green-400 bg-green-500/10 px-1 rounded">
FREE
</span>
)}
</span>
</SelectItem>
);
})}
</SelectContent>
</Select>
<div className="flex items-center gap-2">
{/* Model selector */}
<Select value={selectedModel} onValueChange={setSelectedModel} disabled={modelsLoading}>
<SelectTrigger className="w-full sidebar:w-44 h-8 text-base md:text-xs">
<SelectValue placeholder={modelsLoading ? 'Loading models...' : 'Select model'} />
</SelectTrigger>
<SelectContent>
{models.map((model) => {
const totalCost = parseFloat(model.pricing.prompt) + parseFloat(model.pricing.completion);
const isFree = totalCost === 0;
return (
<SelectItem key={model.id} value={model.id}>
<span className="flex items-center gap-1.5">
{model.name}
{isFree && (
<span className="text-[10px] font-medium text-green-600 dark:text-green-400 bg-green-500/10 px-1 rounded">
FREE
</span>
)}
</span>
</SelectItem>
);
})}
</SelectContent>
</Select>
<Button
variant="ghost"
size="icon"
className="size-8"
onClick={handleClear}
disabled={messages.length === 0}
title="Clear conversation"
>
<Trash2 className="size-4" />
</Button>
</div>
<Button
variant="ghost"
size="icon"
className="size-8"
onClick={handleClear}
disabled={messages.length === 0}
title="Clear conversation"
>
<Trash2 className="size-4" />
</Button>
</div>
</div>
{/* Messages Area */}