mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
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:
parent
eda182c8a1
commit
026e13b347
4 changed files with 24 additions and 250 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
============================================================ */
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Reference in a new issue