fix(mobile): knowledge panel tabs, autocomplete height floor, status row gap
The project knowledge panel's section sidebar becomes a horizontal pill strip on mobile, where a half-width side column left no room for content. The autocomplete popups get their 120px minimum height back — browser keyboard panning could put the anchor above the measured boundary and collapse them to nothing. The status row's 24px footer-swap reservation now applies only to the assistant-status instance; the accessory-only row above the composer (pending changes) takes the normal 8px instead of floating a stray gap.
This commit is contained in:
@@ -313,7 +313,10 @@ export const StatusRow: React.FC<StatusRowProps> = ({
|
|||||||
// carries ~12px more structure BELOW its footer than this row has — so
|
// carries ~12px more structure BELOW its footer than this row has — so
|
||||||
// the swap used to lift the line up. mb-6 (24px) reserves that space
|
// the swap used to lift the line up. mb-6 (24px) reserves that space
|
||||||
// under this row instead (verified: row top 636 == footer top 636).
|
// under this row instead (verified: row top 636 == footer top 636).
|
||||||
className={cn("mb-6", !hasLeftAccessory && "chat-column")}
|
// The reservation belongs to the assistant-status swap only: a row that
|
||||||
|
// renders just an accessory (the pending-changes bar) takes the normal
|
||||||
|
// 8px, or it floats a stray gap above the composer.
|
||||||
|
className={cn(showAssistantStatus ? "mb-6" : "mb-2", !hasLeftAccessory && "chat-column")}
|
||||||
style={STATUS_ROW_CONTAINER_STYLE}
|
style={STATUS_ROW_CONTAINER_STYLE}
|
||||||
>
|
>
|
||||||
{/* h-8 matches the turn footer's real row height: its h-8 action
|
{/* h-8 matches the turn footer's real row height: its h-8 action
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ export const useMobileAutocompleteMaxHeight = (
|
|||||||
// The popup's bottom edge is its anchor (composer top) and does not
|
// The popup's bottom edge is its anchor (composer top) and does not
|
||||||
// depend on its current height.
|
// depend on its current height.
|
||||||
const available = Math.max(0, Math.floor(el.getBoundingClientRect().bottom - boundaryTop - 8));
|
const available = Math.max(0, Math.floor(el.getBoundingClientRect().bottom - boundaryTop - 8));
|
||||||
const next = available < normalMaxHeight ? available : undefined;
|
// Floor: browser keyboard panning can put the anchor above the
|
||||||
|
// measured boundary for a frame (or for the whole pan), which
|
||||||
|
// would collapse the popup to zero height. A short popup that
|
||||||
|
// slightly overlaps the header beats an invisible one.
|
||||||
|
const next = available < normalMaxHeight ? Math.max(120, available) : undefined;
|
||||||
setMaxHeight((prev) => (prev === next ? prev : next));
|
setMaxHeight((prev) => (prev === next ? prev : next));
|
||||||
};
|
};
|
||||||
measure();
|
measure();
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
|
|||||||
const globalMemory = useAgentMemoryStore((state) => state.global);
|
const globalMemory = useAgentMemoryStore((state) => state.global);
|
||||||
const projectMemory = useAgentMemoryStore((state) => state.project);
|
const projectMemory = useAgentMemoryStore((state) => state.project);
|
||||||
|
|
||||||
|
const isMobile = useUIStore((state) => state.isMobile);
|
||||||
const storedTab = useUIStore((state) => state.projectContextTab);
|
const storedTab = useUIStore((state) => state.projectContextTab);
|
||||||
const setStoredTab = useUIStore((state) => state.setProjectContextTab);
|
const setStoredTab = useUIStore((state) => state.setProjectContextTab);
|
||||||
const requestedTab = TAB_ORDER.includes(storedTab as ProjectContextTab)
|
const requestedTab = TAB_ORDER.includes(storedTab as ProjectContextTab)
|
||||||
@@ -413,6 +414,42 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile: a half-width panel has no room for a side column, so the
|
||||||
|
sections become the same pill strip the mobile drawer's surface
|
||||||
|
tabs use — the active pill carries the label, the rest collapse to
|
||||||
|
icon and count. */}
|
||||||
|
{isMobile ? (
|
||||||
|
<nav
|
||||||
|
className="flex flex-shrink-0 items-center gap-1.5 overflow-x-auto px-3 pb-2"
|
||||||
|
aria-label={t('rightSidebar.contextNotesTodo.sections.label')}
|
||||||
|
>
|
||||||
|
{sections.map((section) => {
|
||||||
|
const isActive = activeTab === section.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={section.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setStoredTab(section.id)}
|
||||||
|
aria-current={isActive ? 'page' : undefined}
|
||||||
|
className={cn(
|
||||||
|
'flex flex-shrink-0 items-center gap-1.5 rounded-full border px-3 py-1.5 transition-colors',
|
||||||
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
|
||||||
|
isActive
|
||||||
|
? 'border-transparent bg-interactive-active text-foreground'
|
||||||
|
: 'border-[var(--interactive-border)] text-muted-foreground',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon name={section.icon} className="h-4 w-4 flex-shrink-0" />
|
||||||
|
{isActive ? (
|
||||||
|
<span className="whitespace-nowrap typography-meta">{section.label}</span>
|
||||||
|
) : null}
|
||||||
|
<span className="typography-micro text-muted-foreground">{section.count}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{/* Content first, sidebar on the right — the same order and the same
|
{/* Content first, sidebar on the right — the same order and the same
|
||||||
drag-to-resize edge the files surface uses, so the two panels do not
|
drag-to-resize edge the files surface uses, so the two panels do not
|
||||||
disagree about where navigation lives. */}
|
disagree about where navigation lives. */}
|
||||||
@@ -472,6 +509,7 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isMobile ? null : (
|
||||||
<nav
|
<nav
|
||||||
className="relative flex flex-shrink-0 flex-col gap-0.5 overflow-y-auto border-l border-[var(--interactive-border)] p-2"
|
className="relative flex flex-shrink-0 flex-col gap-0.5 overflow-y-auto border-l border-[var(--interactive-border)] p-2"
|
||||||
style={{ width: `${sidebarWidth}px` }}
|
style={{ width: `${sidebarWidth}px` }}
|
||||||
@@ -514,6 +552,7 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TodoSendDialog
|
<TodoSendDialog
|
||||||
|
|||||||
Reference in New Issue
Block a user