refactor(ui): trim header trigger labels and fix dropdown anchoring
OpenInAppButton and ProjectActionsButton now render icon-only triggers in the header instead of icon + text. Padding is tightened accordingly and the now-unused selectedButtonLabel / formatActionButtonLabel helpers are gone. With narrower triggers the previous center-aligned dropdown with translate -30px started overflowing the viewport on the left edge for ProjectActions. Switched ProjectActions dropdown to align="start" and OpenInApp dropdown to align="end" (matching their positions in the header) and removed the manual translate offset. Also fixed a long-standing inconsistency where clicking an already-running action in the ProjectActions dropdown re-ran it instead of stopping; the non-compact branch was missing toggleStopIfRunning=true that the compact branch already passed. MiniChat header now uses bg-sidebar to match the main desktop header palette.
This commit is contained in:
@@ -142,8 +142,9 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => void handleOpen(selectedApp)}
|
onClick={() => void handleOpen(selectedApp)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex h-full items-center gap-2 px-3 typography-ui-label font-medium',
|
'inline-flex h-full items-center px-2.5 typography-ui-label font-medium',
|
||||||
'text-foreground hover:bg-interactive-hover transition-colors'
|
'text-foreground hover:bg-interactive-hover transition-colors',
|
||||||
|
isScanning && 'animate-pulse'
|
||||||
)}
|
)}
|
||||||
aria-label={t('openInApp.actions.openInAria', { app: selectedApp.label })}
|
aria-label={t('openInApp.actions.openInAria', { app: selectedApp.label })}
|
||||||
>
|
>
|
||||||
@@ -152,9 +153,6 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
|
|||||||
iconDataUrl={selectedApp.iconDataUrl}
|
iconDataUrl={selectedApp.iconDataUrl}
|
||||||
fallbackIconDataUrl={selectedApp.fallbackIconDataUrl}
|
fallbackIconDataUrl={selectedApp.fallbackIconDataUrl}
|
||||||
/>
|
/>
|
||||||
<span className={cn('header-open-label', isScanning ? 'animate-pulse text-muted-foreground' : undefined)}>
|
|
||||||
{t('openInApp.actions.open')}
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@@ -171,9 +169,8 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
|
|||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="center"
|
align="end"
|
||||||
className="w-56 max-h-[70vh] overflow-y-auto"
|
className="w-56 max-h-[70vh] overflow-y-auto"
|
||||||
style={{ translate: '-30px 0' }}
|
|
||||||
>
|
>
|
||||||
<DropdownMenuItem className="flex items-center gap-2" onClick={() => void handleCopyPath()}>
|
<DropdownMenuItem className="flex items-center gap-2" onClick={() => void handleCopyPath()}>
|
||||||
<Icon name="file-copy" className="h-4 w-4" />
|
<Icon name="file-copy" className="h-4 w-4" />
|
||||||
|
|||||||
@@ -146,26 +146,6 @@ const extractBestUrl = (value: string): string | null => {
|
|||||||
return normalized[0] ?? null;
|
return normalized[0] ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatActionButtonLabel = (value: string, fallbackLabel: string): string => {
|
|
||||||
const trimmed = value.trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
return fallbackLabel;
|
|
||||||
}
|
|
||||||
|
|
||||||
const words = trimmed.split(/\s+/).filter(Boolean);
|
|
||||||
if (words.length >= 2) {
|
|
||||||
const first = words[0];
|
|
||||||
const second = words[1].slice(0, 3);
|
|
||||||
const shortTwoWord = `${first} ${second}`.trim();
|
|
||||||
if (words.length > 2 || shortTwoWord.length < trimmed.length) {
|
|
||||||
return `${shortTwoWord}...`;
|
|
||||||
}
|
|
||||||
return shortTwoWord;
|
|
||||||
}
|
|
||||||
|
|
||||||
return trimmed.length > 12 ? `${trimmed.slice(0, 9).trimEnd()}...` : trimmed;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ProjectActionsButton = ({
|
export const ProjectActionsButton = ({
|
||||||
projectRef,
|
projectRef,
|
||||||
directory,
|
directory,
|
||||||
@@ -732,10 +712,6 @@ export const ProjectActionsButton = ({
|
|||||||
const selectedIconName = resolvedSelected.id === AUTO_DISCOVER_ACTION_ID
|
const selectedIconName = resolvedSelected.id === AUTO_DISCOVER_ACTION_ID
|
||||||
? 'search'
|
? 'search'
|
||||||
: PROJECT_ACTION_ICON_MAP[selectedIconKey] || 'play';
|
: PROJECT_ACTION_ICON_MAP[selectedIconKey] || 'play';
|
||||||
const selectedButtonLabel = formatActionButtonLabel(
|
|
||||||
resolvedSelected.name,
|
|
||||||
t('projectActions.label.fallbackAction'),
|
|
||||||
);
|
|
||||||
const selectedRunKey = toProjectActionRunKey(normalizedDirectory, resolvedSelected.id);
|
const selectedRunKey = toProjectActionRunKey(normalizedDirectory, resolvedSelected.id);
|
||||||
const selectedRunning = projectActionRuns[selectedRunKey];
|
const selectedRunning = projectActionRuns[selectedRunKey];
|
||||||
const isStoppingSelected = selectedRunning?.status === 'stopping';
|
const isStoppingSelected = selectedRunning?.status === 'stopping';
|
||||||
@@ -855,8 +831,8 @@ export const ProjectActionsButton = ({
|
|||||||
onClick={handlePrimaryClick}
|
onClick={handlePrimaryClick}
|
||||||
disabled={isLoading || isStoppingSelected}
|
disabled={isLoading || isStoppingSelected}
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex h-full items-center typography-ui-label font-medium text-foreground hover:bg-interactive-hover',
|
'inline-flex h-full items-center justify-center typography-ui-label font-medium text-foreground hover:bg-interactive-hover',
|
||||||
compact ? 'w-9 justify-center px-0' : 'gap-2 px-3',
|
compact ? 'w-9 px-0' : 'px-2.5',
|
||||||
'transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:cursor-not-allowed'
|
'transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:cursor-not-allowed'
|
||||||
)}
|
)}
|
||||||
aria-label={selectedRunning
|
aria-label={selectedRunning
|
||||||
@@ -870,7 +846,6 @@ export const ProjectActionsButton = ({
|
|||||||
? <Icon name="stop" className="h-4 w-4 text-[var(--status-warning)]" />
|
? <Icon name="stop" className="h-4 w-4 text-[var(--status-warning)]" />
|
||||||
: <Icon name={selectedIconName} className="h-4 w-4" />}
|
: <Icon name={selectedIconName} className="h-4 w-4" />}
|
||||||
</span>
|
</span>
|
||||||
{!compact ? <span className="header-open-label whitespace-nowrap">{selectedButtonLabel}</span> : null}
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{showSelectedPreviewButton ? (
|
{showSelectedPreviewButton ? (
|
||||||
@@ -907,7 +882,7 @@ export const ProjectActionsButton = ({
|
|||||||
<Icon name="arrow-down-s" className="h-4 w-4" />
|
<Icon name="arrow-down-s" className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="center" className="w-52 max-h-[70vh] overflow-y-auto" style={{ translate: '-30px 0' }}>
|
<DropdownMenuContent align="start" className="w-52 max-h-[70vh] overflow-y-auto">
|
||||||
<DropdownMenuItem className="flex items-center gap-2" onClick={openProjectActionsSettings}>
|
<DropdownMenuItem className="flex items-center gap-2" onClick={openProjectActionsSettings}>
|
||||||
<Icon name="add" className="h-4 w-4" />
|
<Icon name="add" className="h-4 w-4" />
|
||||||
<span className="typography-ui-label text-foreground">{t('projectActions.actions.addNewAction')}</span>
|
<span className="typography-ui-label text-foreground">{t('projectActions.actions.addNewAction')}</span>
|
||||||
@@ -928,7 +903,7 @@ export const ProjectActionsButton = ({
|
|||||||
key={entry.id}
|
key={entry.id}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleSelectAction(entry);
|
handleSelectAction(entry, true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name={iconName} className="h-4 w-4" />
|
<Icon name={iconName} className="h-4 w-4" />
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ const MiniChatHeader: React.FC<{ mode: MiniChatMode }> = ({ mode }) => {
|
|||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-3 border-b border-[var(--interactive-border)] bg-[var(--surface-background)] pr-3',
|
'flex items-center gap-3 border-b border-[var(--interactive-border)] bg-sidebar pr-3',
|
||||||
hasMacTrafficLights ? 'pl-[5.5rem]' : 'pl-3',
|
hasMacTrafficLights ? 'pl-[5.5rem]' : 'pl-3',
|
||||||
macosHeaderSizeClass || 'min-h-14',
|
macosHeaderSizeClass || 'min-h-14',
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user