S3.4: pixel shell, navigation and global search

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-09 12:29:12 -04:00
1 parent 08ff96eec8
commit c97adc14cb
4 files changed
+61 -55

No files matched your search

+5 -35
View File
@@ -118,15 +118,15 @@ export function SearchDialog({
return (
<Modal label={t("Search")} onEscape={onClose} onBackdrop={onClose} backdropStyle={overlayStyle}>
<form onSubmit={submit} style={panelStyle}>
<div className="row" style={{ padding: "10px 12px", borderBottom: "1px solid var(--border-soft)" }}>
<form onSubmit={submit} className="modal search-panel">
<div className="row search-query">
<Icon name="search" color="var(--text-3)" />
<input
autoFocus
aria-label={t("Search query")}
value={query}
onChange={(event) => setQuery(event.target.value)}
style={inputStyle}
className="search-query-input"
/>
<select
aria-label={t("Search filter")}
@@ -140,7 +140,7 @@ export function SearchDialog({
</select>
<Button size="small" onClick={onClose}>{t("Close")}</Button>
</div>
<div style={{ maxHeight: 420, overflow: "auto", padding: 8 }} aria-busy={searching}>
<div className="search-results" aria-busy={searching}>
{searching && <div role="status" style={{ padding: 8, fontSize: 13, color: "var(--text-3)" }}>{t("Searching…")}</div>}
{current?.error && <div className="form-error" role="alert">{t("Search failed: {reason}", { reason: current.error })}</div>}
{visibleResults.map((result) => (
@@ -148,7 +148,7 @@ export function SearchDialog({
key={`${result.entityType}:${result.entityId}`}
type="button"
onClick={() => openResult(result)}
style={rowStyle}
className="search-row"
>
<Pill>{LABELS[result.entityType]}</Pill>
<div style={{ flex: 1, minWidth: 0 }}>
@@ -210,33 +210,3 @@ const overlayStyle = {
placeItems: "start center",
paddingTop: "12vh",
} satisfies CSSProperties;
const panelStyle = {
width: "min(680px, calc(100vw - 32px))",
border: "1px solid var(--border)",
borderRadius: 12,
background: "var(--panel)",
boxShadow: "var(--shadow-md)",
overflow: "hidden",
} satisfies CSSProperties;
const inputStyle = {
flex: 1,
minWidth: 0,
border: 0,
outline: "none",
background: "transparent",
fontSize: 14,
} satisfies CSSProperties;
const rowStyle = {
width: "100%",
border: 0,
borderRadius: 8,
background: "transparent",
display: "flex",
alignItems: "center",
gap: 10,
padding: "9px 10px",
textAlign: "left",
} satisfies CSSProperties;
+24 -17
View File
@@ -139,7 +139,7 @@ a { color: inherit; }
display: flex;
flex-direction: column;
padding: var(--sp-4) var(--sp-3);
border-right: 1px solid var(--border-soft);
border-right: 2px solid var(--border-strong);
background: var(--bg);
}
/* Clear the native macOS traffic lights that overlap the sidebar top. */
@@ -177,10 +177,10 @@ a { color: inherit; }
border-radius: 0;
display: grid;
place-items: center;
color: #fff;
background: linear-gradient(135deg, #1a1a1d 0%, #3a3a40 100%);
color: var(--btn-primary-fg);
background: var(--btn-primary-bg);
box-shadow: 2px 2px 0 0 var(--border-strong);
font-size: var(--fs-body);
font-weight: 600;
letter-spacing: 0;
}
.brand-name {
@@ -197,22 +197,27 @@ a { color: inherit; }
align-items: center;
gap: var(--sp-2);
padding: var(--sp-2) var(--sp-3);
border-radius: var(--r-sm);
background: var(--panel-2);
border-radius: 0;
background: var(--panel);
color: var(--text-3);
margin-bottom: var(--sp-4);
font-size: var(--fs-body);
cursor: text;
border: 0;
border: 2px solid var(--border-strong);
box-shadow: inset 2px 2px 0 0 var(--border-soft);
width: 100%;
transition: background var(--motion-fast) var(--ease-out), color var(--motion-fast) var(--ease-out);
}
.search-bar:hover { background: var(--panel-3); color: var(--text-2); }
.search-bar:hover { background: var(--hover); color: var(--text-2); }
/* Shortcut as a key cap: a deeper bottom edge reads as a raised key. */
.search-kbd {
margin-left: auto;
padding: 0 4px;
font-size: var(--fs-caption);
color: var(--text-3);
font-family: var(--font-mono);
color: var(--text-2);
background: var(--panel-2);
border: 2px solid var(--border);
border-bottom-width: 4px;
}
.nav { display: flex; flex-direction: column; gap: 2px; }
.nav-label {
@@ -239,17 +244,19 @@ a { color: inherit; }
transition: background var(--motion-fast) var(--ease-out), color var(--motion-fast) var(--ease-out), transform var(--motion-fast) var(--ease-out);
}
.nav-item:hover { background: var(--hover); color: var(--text-1); }
.nav-item:active { transform: scale(0.985); }
.nav-item.active { background: var(--panel-3); color: var(--text-1); font-weight: 560; }
.nav-item.active svg { color: var(--text-1); }
.nav-item.active { background: var(--btn-primary-bg); color: var(--btn-primary-fg); box-shadow: 2px 2px 0 0 var(--border-strong); }
.nav-item.active svg { color: var(--btn-primary-fg); }
.nav-item svg { color: var(--text-3); transition: color var(--motion-fast) var(--ease-out); }
.nav-item .icon-box { color: var(--text-3); }
.nav-item.active .icon-box { color: var(--text-1); }
.nav-item.active .icon-box { color: var(--btn-primary-fg); }
.nav-item span:not(.nav-badge):not(.icon-box) { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-badge {
margin-left: auto;
padding: 0 4px;
font-size: var(--fs-caption);
color: var(--text-3);
color: var(--accent);
background: var(--accent-soft);
clip-path: polygon(2px 0, calc(100% - 2px) 0, 100% 2px, 100% calc(100% - 2px), calc(100% - 2px) 100%, 2px 100%, 0 calc(100% - 2px), 0 2px);
font-variant-numeric: tabular-nums;
}
@@ -434,7 +441,7 @@ a { color: inherit; }
.sidebar-foot {
padding: var(--sp-3) var(--sp-2) var(--sp-1);
margin-top: var(--sp-2);
border-top: 1px solid var(--border-soft);
border-top: 2px solid var(--border-soft);
display: flex;
align-items: center;
gap: var(--sp-2);
@@ -462,7 +469,7 @@ a { color: inherit; }
align-items: center;
gap: var(--sp-3);
padding: 0 var(--sp-5);
border-bottom: 1px solid var(--border-soft);
border-bottom: 2px solid var(--border-strong);
-webkit-app-region: drag;
}
.topbar button,
+11 -3
View File
@@ -38,8 +38,8 @@
}
/* Menu arrow: the chosen or pointed-at nav row shows a blinking right-pointing pixel arrow. */
.nav > .nav-item { position: relative; padding-left: 18px; }
.nav > .nav-item::before {
.nav > .nav-item, .search-results > .search-row { position: relative; padding-left: 18px; }
:is(.nav > .nav-item, .search-results > .search-row)::before {
content: "";
position: absolute;
left: 4px;
@@ -51,7 +51,7 @@
clip-path: polygon(0 0, 34% 0, 34% 20%, 67% 20%, 67% 40%, 100% 40%, 100% 60%, 67% 60%, 67% 80%, 34% 80%, 34% 100%, 0 100%);
visibility: hidden;
}
.nav > .nav-item:is(:hover, :focus-visible, [aria-current], .active)::before {
:is(.nav > .nav-item, .search-results > .search-row):is(:hover, :focus-visible, [aria-current], .active)::before {
visibility: visible;
animation: px-blink 0.8s steps(1, end) infinite;
}
@@ -85,3 +85,11 @@
.inventory { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 8px; padding: 8px; background: var(--panel-3); box-shadow: inset 2px 2px 0 0 var(--border-soft); }
.inventory-slot { display: flex; flex-direction: column; gap: 10px; min-width: 0; padding: 12px; background: var(--panel); border: 2px solid var(--border-strong); box-shadow: inset -2px -2px 0 0 var(--border-soft), inset 2px 2px 0 0 #ffffff40; }
.inventory-slot:hover { background: var(--hover); }
/* Global search: the framed panel, a sunken query line, and menu rows. */
.search-panel { width: min(680px, calc(100vw - 32px)); max-height: none; padding: 0; overflow: hidden; }
.search-query { padding: 10px 12px; border-bottom: 2px solid var(--border-strong); }
.search-query-input { flex: 1; min-width: 0; border: 0; outline: none; background: transparent; font: inherit; color: var(--text-1); }
.search-results { max-height: 420px; overflow: auto; padding: 8px; }
.search-row { width: 100%; border: 0; background: transparent; display: flex; align-items: center; gap: 10px; padding-top: 8px; padding-bottom: 8px; padding-right: 10px; text-align: left; color: var(--text-1); }
.search-row:hover, .search-row:focus-visible { background: var(--hover); outline: none; }
+21
View File
@@ -73,6 +73,27 @@ test("text uses the bundled pixel font, and the palette follows the day part, se
const views = page.getByRole("radiogroup", { name: "View" });
expect(await views.locator("[aria-checked=true]").evaluate((tab) => getComputedStyle(tab).borderBottomColor === getComputedStyle(tab).backgroundColor)).toBe(true);
// Shell: the chosen page is an inverse block, rules are 2px, and search opens in the framed panel whose rows take the menu arrow.
await expect.poll(() => page.evaluate(() => {
const active = getComputedStyle(document.querySelector(".nav > .nav-item.active")!);
const accent = getComputedStyle(document.documentElement).getPropertyValue("--btn-primary-bg").trim();
const probe = document.createElement("div");
probe.style.color = accent;
document.body.append(probe);
const accentRgb = getComputedStyle(probe).color;
probe.remove();
return [active.backgroundColor === accentRgb, getComputedStyle(document.querySelector(".topbar")!).borderBottomWidth, getComputedStyle(document.querySelector(".sidebar")!).borderRightWidth];
})).toEqual([true, "2px", "2px"]);
await page.getByRole("button", { name: "Search", exact: true }).click();
const search = page.getByRole("dialog", { name: "Search" });
expect(await search.locator("form").evaluate((panel) => getComputedStyle(panel).borderImageSource.startsWith('url("data:image/svg+xml'))).toBe(true);
await search.getByLabel("Search query").fill("CEO");
const row = search.locator(".search-row").first();
await row.hover();
expect(await row.evaluate((node) => getComputedStyle(node, "::before").animationName)).toBe("px-blink");
await page.keyboard.press("Escape");
await expect(search).toHaveCount(0);
await page.getByRole("button", { name: "Settings" }).click();
expect(await page.getByRole("combobox", { name: "Theme" }).evaluate((field) => [getComputedStyle(field).borderTopWidth, getComputedStyle(field).borderRadius])).toEqual(["2px", "0px"]);
await page.getByRole("combobox", { name: "Theme" }).selectOption("dark");