Cleanup: scope drawer clip-path ID with useId(), fix indentation, add layout comments
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user