S3.4: pixel shell, navigation and global search
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
08ff96eec8
commit
c97adc14cb
4 files changed
+61
-55
No files matched your search
@@ -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
@@ -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,
|
||||
|
||||
@@ -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; }
|
||||
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user