fix(web): restore release header layout (#1519)

* fix(web): restore release header layout

* fix(web): disambiguate entry settings button

Generated-By: looper 0.7.4 (runner=fixer, agent=codex)
This commit is contained in:
Siri-Ray 2026-05-13 14:57:25 +08:00 committed by GitHub
parent eda182c8a1
commit 026e13b347
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 24 additions and 250 deletions

View file

@ -39,6 +39,7 @@ import { PetRail } from './pet/PetRail';
import { PromptTemplatePreviewModal } from './PromptTemplatePreviewModal';
import { PromptTemplatesTab } from './PromptTemplatesTab';
import { apiProtocolLabel } from '../utils/apiProtocol';
import { AppChromeHeader, SettingsIconButton } from './AppChromeHeader';
type TopTab = 'designs' | 'templates' | 'design-systems' | 'image-templates' | 'video-templates';
@ -473,6 +474,15 @@ export function EntryView({
return (
<div className="entry-shell">
<AppChromeHeader
actions={(
<SettingsIconButton
onClick={() => onOpenSettings()}
title={t('settings.title')}
ariaLabel={t('settings.title')}
/>
)}
/>
<div
className={`entry${petRailHidden ? '' : ' has-pet-rail'}`}
style={{
@ -482,16 +492,6 @@ export function EntryView({
}}
>
<aside className="entry-side" style={{ width: sidebarWidth }}>
<div className="entry-brand">
<span className="entry-brand-mark" aria-hidden>
<img src="/app-icon.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>
</div>
</div>
</div>
<NewProjectPanel
skills={skills}
designSystems={designSystems}

View file

@ -79,7 +79,6 @@ import type {
PreviewComment,
PreviewCommentTarget,
ProjectFile,
ProjectPlatform,
ProjectTemplate,
LiveArtifactEventItem,
LiveArtifactSummary,
@ -240,56 +239,6 @@ function projectEventToAgentEvent(evt: ProjectEvent): LiveArtifactEventItem['eve
};
}
const PLATFORM_LABELS: Record<ProjectPlatform, string> = {
auto: 'Auto',
responsive: 'Responsive web',
'web-desktop': 'Desktop web',
'mobile-ios': 'iOS app',
'mobile-android': 'Android app',
tablet: 'Tablet app',
'desktop-app': 'Desktop app',
};
function labelProjectPlatform(platform: ProjectPlatform | string): string {
return PLATFORM_LABELS[platform as ProjectPlatform] ?? platform;
}
function projectTargetPlatforms(project: Project): string[] {
const targets = project.metadata?.platformTargets;
if (Array.isArray(targets) && targets.length > 0) {
return [...new Set(targets)].map(labelProjectPlatform);
}
if (project.metadata?.platform) {
return [labelProjectPlatform(project.metadata.platform)];
}
return [];
}
type ProjectFeatureChip = {
label: string;
title: string;
tone: 'landing' | 'widgets';
};
function projectFeatureChips(project: Project): ProjectFeatureChip[] {
const chips: ProjectFeatureChip[] = [];
if (project.metadata?.includeLandingPage) {
chips.push({
label: 'Landing page',
title: 'Landing page companion surface is enabled for this project',
tone: 'landing',
});
}
if (project.metadata?.includeOsWidgets) {
chips.push({
label: 'OS widgets',
title: 'Home-screen, lock-screen, or quick-access OS widget surfaces are enabled',
tone: 'widgets',
});
}
return chips;
}
export function ProjectView({
project,
routeFileName,
@ -1987,13 +1936,6 @@ export function ProjectView({
return [skill, ds].filter(Boolean).join(' · ') || t('project.metaFreeform');
}, [skills, designTemplates, designSystems, project.skillId, project.designSystemId, t]);
const targetPlatforms = useMemo(() => projectTargetPlatforms(project), [project]);
const targetPlatformsLabel = targetPlatforms.join(', ');
const visibleTargetPlatforms = targetPlatforms.slice(0, 5);
const hiddenTargetPlatformCount = Math.max(0, targetPlatforms.length - visibleTargetPlatforms.length);
const featureChips = useMemo(() => projectFeatureChips(project), [project]);
const featureChipsLabel = featureChips.map((chip) => chip.label).join(', ');
const isDeck = useMemo(
() =>
(skills.find((s) => s.id === project.skillId) ??
@ -2343,43 +2285,6 @@ export function ProjectView({
</span>
<span className="meta" data-testid="project-meta">{projectMeta}</span>
</span>
{targetPlatforms.length > 0 ? (
<span
className="project-target-platforms"
data-testid="project-target-platforms"
title={`Target platforms: ${targetPlatformsLabel}`}
>
<span className="project-target-platforms-label">Targets</span>
{visibleTargetPlatforms.map((platform) => (
<span className="project-target-platform-chip" key={platform}>
{platform}
</span>
))}
{hiddenTargetPlatformCount > 0 ? (
<span className="project-target-platform-chip is-count">
+{hiddenTargetPlatformCount}
</span>
) : null}
</span>
) : null}
{featureChips.length > 0 ? (
<span
className="project-feature-chips"
data-testid="project-feature-chips"
title={`Enabled design outputs: ${featureChipsLabel}`}
>
<span className="project-feature-chips-label">Includes</span>
{featureChips.map((chip) => (
<span
className={`project-feature-chip is-${chip.tone}`}
key={chip.tone}
title={chip.title}
>
{chip.label}
</span>
))}
</span>
) : null}
</div>
</AppChromeHeader>
<div

View file

@ -583,69 +583,6 @@ code {
min-width: 0;
flex: 0 1 auto;
}
.project-target-platforms,
.project-feature-chips {
display: inline-flex;
align-items: center;
gap: 4px;
min-width: 0;
max-width: min(100%, 280px);
height: 22px;
overflow: hidden;
white-space: nowrap;
flex: 0 1 auto;
}
.project-feature-chips {
max-width: min(100%, 260px);
}
.project-target-platforms-label,
.project-feature-chips-label {
color: var(--text-muted);
font-size: 10px;
line-height: 18px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
flex-shrink: 0;
}
.project-target-platform-chip,
.project-feature-chip {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 0;
max-width: 92px;
height: 20px;
padding: 0 8px;
border: 1px solid color-mix(in srgb, var(--border) 78%, transparent);
border-radius: 999px;
color: var(--text-muted);
background: color-mix(in srgb, var(--bg-subtle) 88%, transparent);
font-size: 11px;
line-height: 18px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 0 1 auto;
}
.project-feature-chip.is-landing {
color: color-mix(in srgb, var(--accent) 72%, var(--text-strong));
border-color: color-mix(in srgb, var(--accent) 26%, transparent);
background: color-mix(in srgb, var(--accent) 10%, var(--bg-subtle));
}
.project-feature-chip.is-widgets {
color: color-mix(in srgb, #0891b2 72%, var(--text-strong));
border-color: color-mix(in srgb, #0891b2 26%, transparent);
background: color-mix(in srgb, #0891b2 10%, var(--bg-subtle));
}
.project-target-platform-chip.is-count {
min-width: 28px;
max-width: 36px;
flex: 0 0 auto;
color: var(--text-strong);
background: color-mix(in srgb, var(--accent, #7c5cff) 12%, var(--bg-subtle));
}
.topbar {
display: flex;
align-items: center;
@ -3908,7 +3845,7 @@ code {
============================================================ */
.entry-shell {
display: grid;
grid-template-rows: 1fr;
grid-template-rows: auto minmax(0, 1fr);
height: 100vh;
min-height: 0;
background: var(--bg);
@ -3929,67 +3866,6 @@ code {
flex-direction: column;
}
.entry-brand {
display: flex;
align-items: center;
gap: 12px;
padding: 24px 20px 18px;
}
.entry-brand-actions {
margin-left: auto;
display: inline-flex;
align-items: center;
}
.entry-brand-mark {
width: 34px;
height: 34px;
border-radius: 50%;
background: linear-gradient(135deg, var(--accent-tint) 0%, var(--accent-soft) 100%);
color: var(--accent);
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
overflow: hidden;
}
.entry-brand-mark .brand-mark-img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
padding: 2px;
user-select: none;
-webkit-user-drag: none;
}
.entry-brand-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.entry-brand-title-row {
display: flex;
align-items: center;
gap: 8px;
}
.entry-brand-title {
font-family: var(--serif);
font-weight: 450;
font-size: 22px;
letter-spacing: -0.015em;
line-height: 1;
color: var(--text);
}
.entry-brand-pill {
font-size: 10.5px;
letter-spacing: 0.02em;
padding: 2px 8px;
border-radius: var(--radius-pill);
background: var(--bg-subtle);
border: 1px solid var(--border);
color: var(--text-muted);
}
.entry-brand-subtitle {
font-size: 11.5px;
color: var(--text-muted);
letter-spacing: 0.01em;
}
/* ============================================================
NEW PROJECT PANEL design standards
This block applies to every tab inside `.newproj` (prototype,
@ -4020,6 +3896,7 @@ code {
flex: 1;
min-height: 0;
overflow: hidden;
padding-top: 24px;
}
.newproj-tabs-shell {
position: relative;
@ -12513,13 +12390,6 @@ button.ghost.mcp-copy-btn:hover:not(:disabled) {
.entry-side-resizer.dragging { background: var(--accent-soft); }
body.entry-resizing { cursor: col-resize; user-select: none; }
/* Branded header title + research-preview pill on a single line, with
the "by …" subtitle underneath. */
.entry-brand-title-row {
flex-wrap: wrap;
row-gap: 4px;
}
/* ============================================================
Composer Import popover (coming-soon menu)
============================================================ */

View file

@ -41,9 +41,9 @@ test.beforeEach(async ({ page }) => {
test('pet pill toggle hides and shows the pet rail', async ({ page }) => {
await page.goto('/');
await expect(page.getByTestId('new-project-panel')).toBeVisible();
await expect(page.locator('.entry-brand')).toBeVisible();
await expect(page.locator('.entry-brand .entry-brand-title')).toHaveText('Open Design');
await expect(page.locator('.app-chrome-header')).toHaveCount(0);
await expect(page.locator('.app-chrome-header')).toBeVisible();
await expect(page.locator('.app-chrome-header .app-chrome-name')).toHaveText('Open Design');
await expect(page.locator('.entry-brand')).toHaveCount(0);
await expect(page.locator('.pet-rail')).toBeVisible();
const hideToggle = page.locator('.pet-pill-toggle');
@ -82,18 +82,17 @@ test('entry chrome avoids horizontal overflow on compact desktop width', async (
await page.setViewportSize({ width: 820, height: 900 });
await page.goto('/');
await expect(page.getByTestId('new-project-panel')).toBeVisible();
await expect(page.locator('.entry-brand')).toBeVisible();
await expect(page.locator('.app-chrome-header')).toBeVisible();
// The brand row replaced the old global chrome header; if it overflows
// horizontally on a compact desktop, the logo/title/settings cog will
// wrap or push the layout sideways. Keep it pinned to no-overflow.
const brandOverflow = await page.evaluate(() => {
const brand = document.querySelector('.entry-brand');
if (!(brand instanceof HTMLElement)) return null;
return Math.max(0, brand.scrollWidth - brand.clientWidth);
// The shared app chrome header should stay one row and avoid pushing
// the entry layout sideways on compact desktop widths.
const headerOverflow = await page.evaluate(() => {
const header = document.querySelector('.app-chrome-header');
if (!(header instanceof HTMLElement)) return null;
return Math.max(0, header.scrollWidth - header.clientWidth);
});
expect(brandOverflow).not.toBeNull();
expect(brandOverflow!).toBeLessThanOrEqual(2);
expect(headerOverflow).not.toBeNull();
expect(headerOverflow!).toBeLessThanOrEqual(2);
const pageOverflow = await page.evaluate(() =>
Math.max(0, document.documentElement.scrollWidth - document.documentElement.clientWidth),