Add desktop chrome header (#205)

* feat: add desktop chrome header

* fix: address app chrome review feedback

Generated-By: looper 0.2.7 (runner=fixer, agent=codex)

* fix: address app chrome review feedback

Generated-By: looper 0.2.7 (runner=fixer, agent=codex)
This commit is contained in:
Siri-Ray
2026-05-01 19:09:02 +08:00
committed by GitHub
parent 6789dd4c11
commit 82a79c8b23
8 changed files with 442 additions and 183 deletions
+57
View File
@@ -66,6 +66,37 @@ export type DesktopRuntimeOptions = {
discoverUrl(): Promise<string | null>;
};
const MAC_WINDOW_CHROME =
process.platform === "darwin"
? ({
titleBarStyle: "hiddenInset" as const,
trafficLightPosition: { x: 14, y: 12 },
})
: {};
const MAC_WINDOW_CHROME_CSS = `
.app-chrome-header {
--app-chrome-traffic-space: 56px !important;
-webkit-app-region: drag;
}
.app-chrome-traffic-space {
flex: 0 0 56px !important;
width: 56px !important;
}
.app-chrome-header button,
.app-chrome-header [role="button"],
.app-chrome-header [contenteditable],
.app-chrome-actions,
.app-chrome-actions *,
.avatar-popover,
.avatar-popover * {
-webkit-app-region: no-drag;
}
.app-chrome-drag {
-webkit-app-region: drag;
}
`;
function createPendingHtml(): string {
return `data:text/html;charset=utf-8,${encodeURIComponent(`<!doctype html>
<html>
@@ -119,12 +150,31 @@ function mapConsoleLevel(level: number): string {
}
}
async function applyWindowChromeCss(window: BrowserWindow): Promise<void> {
if (process.platform !== "darwin" || window.isDestroyed()) return;
await window.webContents.insertCSS(MAC_WINDOW_CHROME_CSS, { cssOrigin: "user" });
}
function installWindowChromeCssHook(window: BrowserWindow): void {
window.webContents.on("did-finish-load", () => {
void applyWindowChromeCss(window).catch((error: unknown) => {
console.error("desktop window chrome CSS injection failed", error);
});
});
}
function showWindowButtons(window: BrowserWindow): void {
if (process.platform !== "darwin" || window.isDestroyed()) return;
window.setWindowButtonVisibility(true);
}
export async function createDesktopRuntime(options: DesktopRuntimeOptions): Promise<DesktopRuntime> {
const consoleEntries: DesktopConsoleEntry[] = [];
const window = new BrowserWindow({
height: 900,
show: true,
title: "Open Design",
...MAC_WINDOW_CHROME,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
@@ -132,10 +182,15 @@ export async function createDesktopRuntime(options: DesktopRuntimeOptions): Prom
},
width: 1280,
});
installWindowChromeCssHook(window);
showWindowButtons(window);
let currentUrl: string | null = null;
let stopped = false;
let timer: NodeJS.Timeout | null = null;
window.on("focus", () => showWindowButtons(window));
window.on("blur", () => showWindowButtons(window));
(window.webContents as any).on("console-message", (event: { level?: number | string; message?: string }) => {
const level = typeof event.level === "number" ? mapConsoleLevel(event.level) : (event.level ?? "log");
consoleEntries.push({
@@ -149,6 +204,7 @@ export async function createDesktopRuntime(options: DesktopRuntimeOptions): Prom
});
await window.loadURL(createPendingHtml());
showWindowButtons(window);
const schedule = (delayMs: number) => {
if (stopped) return;
@@ -165,6 +221,7 @@ export async function createDesktopRuntime(options: DesktopRuntimeOptions): Prom
if (url != null && url !== currentUrl) {
currentUrl = url;
await window.loadURL(url);
showWindowButtons(window);
}
schedule(url == null ? PENDING_POLL_MS : RUNNING_POLL_MS);
} catch (error) {
+2 -2
View File
@@ -6,11 +6,11 @@ import '../src/index.css';
export const metadata: Metadata = {
title: 'Open Design',
icons: {
icon: '/logo.svg',
icon: '/app-icon.svg',
// Safari pinned-tab mask icon — Next.js's Metadata API doesn't have a
// dedicated `mask` field, so we surface it via the generic `other`
// bucket which renders as a raw <link rel="mask-icon" ...>.
other: [{ rel: 'mask-icon', url: '/logo.svg', color: '#1F1B16' }],
other: [{ rel: 'mask-icon', url: '/app-icon.svg', color: '#363636' }],
},
};
+51
View File
@@ -0,0 +1,51 @@
<svg width="533" height="533" viewBox="0 0 533 533" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_107_105)">
<mask id="mask0_107_105" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="534" height="534">
<path d="M533.006 266.503C533.006 119.317 413.688 0 266.503 0C119.318 0 0.00012207 119.317 0.00012207 266.503V494.417C0.00012207 515.729 17.2769 533.006 38.5888 533.006H266.503C413.688 533.006 533.006 413.688 533.006 266.503Z" fill="url(#paint0_linear_107_105)"/>
</mask>
<g mask="url(#mask0_107_105)">
<g filter="url(#filter0_f_107_105)">
<path d="M559.006 0H-32.4998V541.379C-32.4998 565.474 -12.9674 585.006 11.127 585.006H559.006V0Z" fill="url(#paint1_linear_107_105)"/>
</g>
<g filter="url(#filter1_f_107_105)">
<path d="M241.706 161C241.706 122.422 210.431 91.1475 171.853 91.1475V91.1475C133.274 91.1475 102 122.422 102 161V161C102 199.579 133.274 230.853 171.853 230.853V230.853C210.431 230.853 241.706 199.579 241.706 161V161Z" fill="white" style="fill:white;fill-opacity:1;"/>
</g>
<g opacity="0.7" filter="url(#filter2_f_107_105)">
<path d="M422.555 282.955C436.091 264.202 429.408 237.772 408.585 227.707C403.597 225.296 398.134 224.03 392.595 224.001L368.715 223.877C293.049 223.481 230.694 283.146 227.754 358.755L227.439 366.845C226.585 388.814 237.909 409.466 256.893 420.556C283.361 436.02 317.245 428.853 335.186 403.997L422.555 282.955Z" fill="white" style="fill:white;fill-opacity:1;"/>
</g>
</g>
<path d="M266.503 13C406.509 13.0001 520.005 126.497 520.005 266.503C520.005 406.508 406.509 520.006 266.503 520.006C126.498 520.006 13.0005 406.508 13.0005 266.503C13.0005 126.497 126.498 13 266.503 13ZM200.144 144.795C169.919 135.316 141.442 164.107 151.526 194.357L213.957 381.65C226.127 418.163 278.22 416.846 288.531 379.766L309.793 303.293L386.266 282.03C423.346 271.72 424.663 219.628 388.151 207.457L200.858 145.025L200.144 144.795ZM176.192 186.136C172.804 175.973 182.473 166.304 192.636 169.691L379.929 232.123L380.491 232.322C392.083 236.719 391.467 253.597 379.3 256.98L298.397 279.477C292.366 281.153 287.654 285.866 285.977 291.896L263.481 372.8C260.044 385.16 242.68 385.599 238.623 373.429L176.192 186.136Z" fill="#363636" style="fill:#363636;fill:color(display-p3 0.2121 0.2121 0.2121);fill-opacity:1;"/>
</g>
<defs>
<filter id="filter0_f_107_105" x="-143.001" y="-110.501" width="812.509" height="806.008" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="55.2506" result="effect1_foregroundBlur_107_105"/>
</filter>
<filter id="filter1_f_107_105" x="34.3993" y="23.5468" width="274.907" height="274.907" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="33.8004" result="effect1_foregroundBlur_107_105"/>
</filter>
<filter id="filter2_f_107_105" x="159.793" y="156.274" width="337.406" height="340.017" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="33.8004" result="effect1_foregroundBlur_107_105"/>
</filter>
<linearGradient id="paint0_linear_107_105" x1="13.0004" y1="260.003" x2="295.753" y2="533.006" gradientUnits="userSpaceOnUse">
<stop stop-color="#D999F7" style="stop-color:#D999F7;stop-color:color(display-p3 0.8510 0.6000 0.9686);stop-opacity:1;"/>
<stop offset="0.336538" stop-color="#00A365" style="stop-color:#00A365;stop-color:color(display-p3 0.0000 0.6399 0.3972);stop-opacity:1;"/>
<stop offset="0.673077" stop-color="#F8672F" style="stop-color:#F8672F;stop-color:color(display-p3 0.9725 0.4039 0.1843);stop-opacity:1;"/>
<stop offset="1" stop-color="#9C9C9C" style="stop-color:#9C9C9C;stop-color:color(display-p3 0.6121 0.6121 0.6121);stop-opacity:1;"/>
</linearGradient>
<linearGradient id="paint1_linear_107_105" x1="6.50056" y1="533.006" x2="428.077" y2="4.44429" gradientUnits="userSpaceOnUse">
<stop stop-color="#EEFF00" style="stop-color:#EEFF00;stop-color:color(display-p3 0.9333 1.0000 0.0000);stop-opacity:1;"/>
<stop offset="0.307692" stop-color="#00FFAE" style="stop-color:#00FFAE;stop-color:color(display-p3 0.0000 1.0000 0.6833);stop-opacity:1;"/>
<stop offset="0.668269" stop-color="#00EAFF" style="stop-color:#00EAFF;stop-color:color(display-p3 0.0000 0.9167 1.0000);stop-opacity:1;"/>
<stop offset="1" stop-color="#00FF6A" style="stop-color:#00FF6A;stop-color:color(display-p3 0.0000 1.0000 0.4167);stop-opacity:1;"/>
</linearGradient>
<clipPath id="clip0_107_105">
<rect width="533" height="533" fill="white" style="fill:white;fill-opacity:1;"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

@@ -0,0 +1,64 @@
import type { ReactNode } from 'react';
import { useT } from '../i18n';
import { Icon } from './Icon';
interface Props {
actions?: ReactNode;
children?: ReactNode;
onBack?: () => void;
backLabel?: string;
}
export function AppChromeHeader({ actions, children, onBack, backLabel }: Props) {
const t = useT();
const resolvedBackLabel = backLabel ?? t('project.backToProjects');
return (
<header className="app-chrome-header">
<div className="app-chrome-traffic-space" aria-hidden />
<div className="app-chrome-brand" aria-label={t('app.brand')}>
<span className="app-chrome-mark" aria-hidden>
{/* decorative, parent has aria-label */}
<img src="/app-icon.svg" alt="" className="brand-mark-img" draggable={false} />
</span>
<span className="app-chrome-name">{t('app.brand')}</span>
</div>
{onBack ? (
<button
type="button"
className="app-chrome-back"
onClick={onBack}
title={resolvedBackLabel}
aria-label={resolvedBackLabel}
>
<Icon name="arrow-left" size={15} />
</button>
) : null}
{children ? <div className="app-chrome-content">{children}</div> : null}
<div className="app-chrome-drag" aria-hidden />
{actions ? <div className="app-chrome-actions">{actions}</div> : null}
</header>
);
}
export function SettingsIconButton({
onClick,
title,
ariaLabel,
}: {
onClick: () => void;
title: string;
ariaLabel: string;
}) {
return (
<button
type="button"
className="settings-icon-btn"
onClick={onClick}
title={title}
aria-label={ariaLabel}
>
<Icon name="settings" size={17} />
</button>
);
}
+4 -9
View File
@@ -21,7 +21,7 @@ interface Props {
}
/**
* Compact avatar at the right of the project topbar. Click opens a dropdown
* Compact settings control at the right of the project header. Click opens a dropdown
* with current execution mode, the agent picker (when in daemon mode), and
* a Settings entry — replaces the wide AgentPicker + env-pill row.
*/
@@ -81,19 +81,14 @@ export function AvatarMenu({
<div className="avatar-menu" ref={wrapRef}>
<button
type="button"
className="avatar-btn"
className="settings-icon-btn"
onClick={() => setOpen((v) => !v)}
aria-haspopup="menu"
aria-expanded={open}
title={t('avatar.title')}
aria-label={t('avatar.title')}
>
<img
src="/avatar.png"
alt=""
aria-hidden
draggable={false}
className="avatar-btn-photo"
/>
<Icon name="settings" size={17} />
</button>
{open ? (
<div className="avatar-popover" role="menu">
+120 -138
View File
@@ -20,6 +20,7 @@ import { DesignsTab } from './DesignsTab';
import { DesignSystemPreviewModal } from './DesignSystemPreviewModal';
import { DesignSystemsTab } from './DesignSystemsTab';
import { ExamplesTab } from './ExamplesTab';
import { AppChromeHeader, SettingsIconButton } from './AppChromeHeader';
import { Icon } from './Icon';
import { LanguageMenu } from './LanguageMenu';
import { CenteredLoader } from './Loading';
@@ -168,151 +169,132 @@ export function EntryView({
}, [sidebarWidth]);
return (
<div
className="entry"
style={{ gridTemplateColumns: `${sidebarWidth}px 1fr` }}
>
<aside className="entry-side" style={{ width: sidebarWidth }}>
<div className="entry-brand">
<span className="entry-brand-mark" aria-hidden>
<img src="/logo.svg" alt="" className="brand-mark-img" draggable={false} />
</span>
<div className="entry-brand-text">
<div className="entry-brand-title-row">
<span className="entry-brand-title">{t('app.brand')}</span>
<span className="entry-brand-pill">{t('app.brandPill')}</span>
</div>
<div className="entry-brand-subtitle">{t('app.brandSubtitle')}</div>
</div>
</div>
<NewProjectPanel
skills={skills}
designSystems={designSystems}
defaultDesignSystemId={defaultDesignSystemId}
templates={templates}
onCreate={handleCreate}
onImportClaudeDesign={onImportClaudeDesign}
mediaProviders={config.mediaProviders}
loading={loading}
/>
<div className="entry-side-foot">
<button
type="button"
className="foot-pill"
<div className="entry-shell">
<AppChromeHeader
actions={(
<SettingsIconButton
onClick={onOpenSettings}
title={t('settings.envConfigure')}
>
<Icon name="settings" size={12} />
<span>
{config.mode === 'daemon'
? t('settings.localCli')
: t('settings.anthropicApi')}
</span>
<span style={{ color: 'var(--text-faint)' }}>·</span>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 180 }}>
{envMetaLine}
</span>
</button>
<LanguageMenu />
</div>
<button
type="button"
aria-label={t('entry.resizeAria')}
className={`entry-side-resizer${resizing ? ' dragging' : ''}`}
onMouseDown={(e) => {
e.preventDefault();
startWidthRef.current = sidebarWidth;
startXRef.current = e.clientX;
setResizing(true);
}}
/>
</aside>
<main className="entry-main">
<div className="entry-header">
<div className="entry-tabs" role="tablist">
<TopTabButton current={topTab} value="designs" label={t('entry.tabDesigns')} onClick={setTopTab} />
<TopTabButton current={topTab} value="examples" label={t('entry.tabExamples')} onClick={setTopTab} />
<TopTabButton
current={topTab}
value="design-systems"
label={t('entry.tabDesignSystems')}
onClick={setTopTab}
/>
<TopTabButton
current={topTab}
value="image-templates"
label={t('entry.tabImageTemplates')}
onClick={setTopTab}
/>
<TopTabButton
current={topTab}
value="video-templates"
label={t('entry.tabVideoTemplates')}
onClick={setTopTab}
/>
</div>
<div className="entry-header-right">
{/* Avatar settings live next to tabs to mirror the project view. */}
title={t('entry.openSettingsTitle')}
ariaLabel={t('entry.openSettingsAria')}
/>
)}
/>
<div
className="entry"
style={{ gridTemplateColumns: `${sidebarWidth}px 1fr` }}
>
<aside className="entry-side" style={{ width: sidebarWidth }}>
<NewProjectPanel
skills={skills}
designSystems={designSystems}
defaultDesignSystemId={defaultDesignSystemId}
templates={templates}
onCreate={handleCreate}
onImportClaudeDesign={onImportClaudeDesign}
mediaProviders={config.mediaProviders}
loading={loading}
/>
<div className="entry-side-foot">
<button
type="button"
className="avatar-btn"
className="foot-pill"
onClick={onOpenSettings}
title={t('entry.openSettingsTitle')}
aria-label={t('entry.openSettingsAria')}
title={t('settings.envConfigure')}
>
<img
src="/avatar.png"
alt=""
aria-hidden
draggable={false}
className="avatar-btn-photo"
/>
<Icon name="settings" size={12} />
<span>
{config.mode === 'daemon'
? t('settings.localCli')
: t('settings.anthropicApi')}
</span>
<span style={{ color: 'var(--text-faint)' }}>·</span>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 180 }}>
{envMetaLine}
</span>
</button>
<LanguageMenu />
</div>
</div>
<div className="entry-tab-content">
{loading ? (
<CenteredLoader label={t('entry.loadingWorkspace')} />
) : (
<>
{topTab === 'designs' ? (
<DesignsTab
projects={projects}
skills={skills}
designSystems={designSystems}
onOpen={onOpenProject}
onDelete={onDeleteProject}
/>
) : null}
{topTab === 'examples' ? (
<ExamplesTab skills={skills} onUsePrompt={usePromptFromSkill} />
) : null}
{topTab === 'design-systems' ? (
<DesignSystemsTab
systems={designSystems}
selectedId={defaultDesignSystemId}
onSelect={onChangeDefaultDesignSystem}
onPreview={previewDesignSystem}
/>
) : null}
{topTab === 'image-templates' ? (
<PromptTemplatesTab
surface="image"
templates={promptTemplates}
onPreview={setPreviewPromptTemplate}
/>
) : null}
{topTab === 'video-templates' ? (
<PromptTemplatesTab
surface="video"
templates={promptTemplates}
onPreview={setPreviewPromptTemplate}
/>
) : null}
</>
)}
</div>
</main>
<button
type="button"
aria-label={t('entry.resizeAria')}
className={`entry-side-resizer${resizing ? ' dragging' : ''}`}
onMouseDown={(e) => {
e.preventDefault();
startWidthRef.current = sidebarWidth;
startXRef.current = e.clientX;
setResizing(true);
}}
/>
</aside>
<main className="entry-main">
<div className="entry-header">
<div className="entry-tabs" role="tablist">
<TopTabButton current={topTab} value="designs" label={t('entry.tabDesigns')} onClick={setTopTab} />
<TopTabButton current={topTab} value="examples" label={t('entry.tabExamples')} onClick={setTopTab} />
<TopTabButton
current={topTab}
value="design-systems"
label={t('entry.tabDesignSystems')}
onClick={setTopTab}
/>
<TopTabButton
current={topTab}
value="image-templates"
label={t('entry.tabImageTemplates')}
onClick={setTopTab}
/>
<TopTabButton
current={topTab}
value="video-templates"
label={t('entry.tabVideoTemplates')}
onClick={setTopTab}
/>
</div>
</div>
<div className="entry-tab-content">
{loading ? (
<CenteredLoader label={t('entry.loadingWorkspace')} />
) : (
<>
{topTab === 'designs' ? (
<DesignsTab
projects={projects}
skills={skills}
designSystems={designSystems}
onOpen={onOpenProject}
onDelete={onDeleteProject}
/>
) : null}
{topTab === 'examples' ? (
<ExamplesTab skills={skills} onUsePrompt={usePromptFromSkill} />
) : null}
{topTab === 'design-systems' ? (
<DesignSystemsTab
systems={designSystems}
selectedId={defaultDesignSystemId}
onSelect={onChangeDefaultDesignSystem}
onPreview={previewDesignSystem}
/>
) : null}
{topTab === 'image-templates' ? (
<PromptTemplatesTab
surface="image"
templates={promptTemplates}
onPreview={setPreviewPromptTemplate}
/>
) : null}
{topTab === 'video-templates' ? (
<PromptTemplatesTab
surface="video"
templates={promptTemplates}
onPreview={setPreviewPromptTemplate}
/>
) : null}
</>
)}
</div>
</main>
</div>
{previewSystem ? (
<DesignSystemPreviewModal
system={previewSystem}
+20 -30
View File
@@ -46,10 +46,10 @@ import type {
ProjectTemplate,
SkillSummary,
} from '../types';
import { AppChromeHeader } from './AppChromeHeader';
import { AvatarMenu } from './AvatarMenu';
import { ChatPane } from './ChatPane';
import { FileWorkspace } from './FileWorkspace';
import { Icon } from './Icon';
interface Props {
project: Project;
@@ -1044,20 +1044,24 @@ export function ProjectView({
return (
<div className="app">
<div className="topbar">
<div className="topbar-left">
<button
className="ghost back-btn"
onClick={onBack}
title={t('project.backToProjects')}
aria-label={t('project.backToProjects')}
>
<Icon name="arrow-left" size={14} />
</button>
<span className="brand-mark" aria-hidden>
<img src="/logo.svg" alt="" className="brand-mark-img" draggable={false} />
</span>
<div className="topbar-title">
<AppChromeHeader
onBack={onBack}
backLabel={t('project.backToProjects')}
actions={(
<AvatarMenu
config={config}
agents={agents}
daemonLive={daemonLive}
onModeChange={onModeChange}
onAgentChange={onAgentChange}
onAgentModelChange={onAgentModelChange}
onOpenSettings={onOpenSettings}
onRefreshAgents={onRefreshAgents}
onBack={onBack}
/>
)}
>
<div className="app-project-title">
<span
className="title editable"
data-testid="project-title"
@@ -1076,22 +1080,8 @@ export function ProjectView({
{project.name}
</span>
<span className="meta" data-testid="project-meta">{projectMeta}</span>
</div>
</div>
<div className="topbar-right">
<AvatarMenu
config={config}
agents={agents}
daemonLive={daemonLive}
onModeChange={onModeChange}
onAgentChange={onAgentChange}
onAgentModelChange={onAgentModelChange}
onOpenSettings={onOpenSettings}
onRefreshAgents={onRefreshAgents}
onBack={onBack}
/>
</div>
</div>
</AppChromeHeader>
<div className="split">
<ChatPane
// The conversation id is part of the key so switching conversations
+124 -4
View File
@@ -170,6 +170,114 @@ code {
background: var(--bg-app);
}
.app-chrome-header {
--app-chrome-traffic-space: 0px;
display: flex;
align-items: center;
min-height: 40px;
padding: 4px 14px;
border-bottom: 1px solid var(--border);
background: var(--bg);
gap: 12px;
flex-shrink: 0;
}
.app-chrome-traffic-space {
width: var(--app-chrome-traffic-space);
flex: 0 0 var(--app-chrome-traffic-space);
}
.app-chrome-brand {
display: inline-flex;
align-items: center;
gap: 10px;
min-width: 0;
flex-shrink: 0;
}
.app-chrome-mark {
width: 28px;
height: 28px;
border-radius: 0;
display: inline-flex;
align-items: center;
justify-content: center;
background: transparent;
overflow: visible;
flex-shrink: 0;
}
.app-chrome-mark .brand-mark-img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
padding: 0;
user-select: none;
-webkit-user-drag: none;
}
.app-chrome-name {
color: var(--text-strong);
font-size: 18px;
font-weight: 650;
letter-spacing: -0.01em;
white-space: nowrap;
}
.app-chrome-back,
.settings-icon-btn {
width: 32px;
height: 32px;
padding: 0;
border-color: transparent;
background: transparent;
color: var(--text-muted);
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.app-chrome-back:hover:not(:disabled),
.settings-icon-btn:hover:not(:disabled) {
background: var(--bg-subtle);
border-color: var(--border);
color: var(--text);
}
.app-chrome-content {
min-width: 0;
display: flex;
align-items: center;
gap: 10px;
}
.app-chrome-drag {
min-width: 24px;
flex: 1 1 auto;
align-self: stretch;
}
.app-chrome-actions {
display: inline-flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.app-project-title {
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
}
.app-project-title .title {
color: var(--text-strong);
font-size: 13.5px;
font-weight: 600;
letter-spacing: -0.01em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.app-project-title .meta {
color: var(--text-muted);
font-size: 11.5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.topbar {
display: flex;
align-items: center;
@@ -1286,10 +1394,19 @@ code {
/* ============================================================
Entry view — left sidebar + right tabs
============================================================ */
.entry-shell {
display: grid;
grid-template-rows: auto 1fr;
height: 100vh;
min-height: 0;
background: var(--bg);
}
.entry {
display: grid;
grid-template-columns: 380px 1fr;
height: 100vh;
height: 100%;
min-height: 0;
background: var(--bg);
}
@@ -2284,9 +2401,10 @@ code {
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 28px 0;
padding: 0 28px;
border-bottom: 1px solid var(--border);
min-width: 0;
min-height: 52px;
}
.entry-header-tabs-row {
display: flex;
@@ -4414,13 +4532,15 @@ code {
============================================================ */
/* Editable project title (inline rename in topbar) */
.topbar-title .title.editable {
.topbar-title .title.editable,
.app-project-title .title.editable {
outline: none;
border-radius: 4px;
padding: 1px 4px;
margin: -1px -4px;
}
.topbar-title .title.editable:focus {
.topbar-title .title.editable:focus,
.app-project-title .title.editable:focus {
background: var(--bg-subtle);
box-shadow: 0 0 0 1px var(--accent);
}