fix(hub): the history kind is a badge, not a fake disclosure toggle (BEA-17) (#67)

* fix(hub): the history kind is a badge, not a fake disclosure toggle (BEA-17)

The +/x in a history row is the kind glyph (added/edited/deleted), but
sitting leftmost inside a role="button" row it read as a tree disclosure
control — clicked, it navigated away instead of expanding. Merge the glyph
and its word into one text badge and vacate the toggle slot: the kind is
now text (no icon shape, no colour-only meaning), and the row's only real
expander stays the note, which keeps its own control and aria-expanded
and now turns its chevron when open.

* fix(hub): fit DELETED in the kind badge and align the row's meta under the path

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Snow W. Lee (Sungwon)
2026-07-28 07:21:38 +09:00
committed by GitHub
co-authored by Claude Fable 5
parent f35d889cfb
commit 411809a785
8 changed files with 63 additions and 29 deletions
+1
View File
@@ -160,6 +160,7 @@ func seedE2E(t *testing.T, state, prefix, projectID string) {
png := "\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89" +
"\x00\x00\x00\nIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82"
put("assets/logo.png", png, 24*time.Hour)
ops[2].Note = "expanded the guide — https://claude.ai/session/e2e" // the one row with a note expander
// A second version of the same binary, so the history diff has a
// predecessor to refuse to diff (the "binary — no diff" path).
put("assets/logo.png", png+"\x00trailing", 3*time.Hour)
@@ -48,6 +48,33 @@ test("folder listing: counts, change feed, heat dot on a read file", async ({ pa
await expect(page.locator('.dl-row[title="notes/readme.md"] .heatdot')).toBeVisible();
});
// BEA-17: the kind glyph read as a disclosure toggle. It is now a text
// badge, the row's only real expander is the note, and clicking the badge
// navigates like the rest of the row — no dead zone, no second behavior.
test("history row: kind is a badge, not a disclosure control", async ({ page }) => {
await login(page);
const pid = await wikiId(page);
await page.goto(`/${pid}/history/guide.md`);
const row = page.locator(".history .hentry").first();
await expect(row).toBeVisible();
// kind is conveyed as text, not by an icon shape
await expect(row.locator(".hkind")).toHaveText("edited");
await expect(row.locator(".hkind .ico")).toHaveCount(0);
// a row announces kind, path and author without the icon
await expect(page.getByRole("button", { name: /edited\s+guide\.md.*alice@x\.io/s })).toHaveCount(1);
// only genuine expanders claim to expand: the note and the diff disclosure
await expect(page.locator(".history .hnote[aria-expanded]")).toHaveCount(1);
await expect(page.locator(".history [aria-expanded]:not(.hnote):not(.hdiff-btn)")).toHaveCount(0);
// ...and it still expands in place, without navigating
await row.locator(".hnote").click({ position: { x: 6, y: 6 } }); // off the note's link
await expect(row.locator(".hnote")).toHaveClass(/open/);
await expect(page).toHaveURL(`/${pid}/history/guide.md`);
// clicking the badge does exactly what clicking the row does: it opens
// the version the row describes (BEA-7), not a dead zone
await row.locator(".hkind").click();
await page.waitForURL(new RegExp(`/${pid}/guide\\.md\\?v=[0-9a-f]{64}$`));
});
test("image file renders an <img>", async ({ page }) => {
await login(page);
const pid = await wikiId(page);
+3 -2
View File
@@ -156,8 +156,9 @@ test("history: whole project, newest first, and per-file versions", async ({ pag
await expect(page.locator("#crumb")).toContainText("History — guide.md");
await expect(page.locator(".history .hentry")).toHaveCount(2);
await expect(page.locator(".history .hentry").first()).toContainText("edited");
// clicking an entry opens THAT version of the file (BEA-7)
await page.click(".history .hentry.clickable >> nth=0");
// clicking an entry opens THAT version of the file (BEA-7); aim at the
// path cell — the row's center can land on its expandable note
await page.click(".history .hentry.clickable >> nth=0 >> .hpath");
await page.waitForURL(new RegExp(`/${pid}/guide\\.md\\?v=[0-9a-f]{64}$`));
});
@@ -5,8 +5,13 @@ import { Icon } from "./shell";
import { DiffView } from "./DiffView";
/* One change as a row: what happened (added / edited / deleted), to which
file, by whom, from where — with the note (session link) expandable. */
const KIND_ICON: Record<string, string> = { add: "plus", edit: "edit", delete: "x" };
file, by whom, from where — with the note (session link) expandable.
The kind is a word in a badge, never a +/✕ glyph in the row's leftmost
slot: sitting there, inside a role="button" row, it read as a disclosure
toggle it never was. The toggle slot stays free for a real per-row
disclosure; the only genuine expansion here is the note, which owns its
own control and its own aria-expanded. */
const KIND_LABEL: Record<string, string> = { add: "added", edit: "edited", delete: "deleted" };
export function HistoryRow({
@@ -50,11 +55,8 @@ export function HistoryRow({
}}
>
<div className="hline">
<span className="hkind">
<Icon name={KIND_ICON[kind] || "dot"} />
</span>
<span className="hkind">{KIND_LABEL[kind] || kind}</span>
<span className="hpath">{e.path}</span>
<span className="htag">{KIND_LABEL[kind] || kind}</span>
<span className="htime">{new Date(e.time).toLocaleString()}</span>
</div>
<div className="hmeta">
+15 -12
View File
@@ -631,27 +631,27 @@ a.ai-main:hover { color: var(--accent); }
/* ---- history ---- */
/* .history width comes from .page (app) */
.hentry { padding: 11px 12px; border-bottom: 1px solid var(--border); }
.hentry { padding: 11px 12px; border-bottom: 1px solid var(--border); --hindent: 72px; }
.hentry:hover { background: rgba(255,255,255,.015); }
.hline { display: flex; gap: 10px; align-items: center; }
.hkind { display: inline-flex; color: var(--add); }
.hkind .ico { width: 13px; height: 13px; }
.hentry.edit .hkind { color: var(--accent); }
.hentry.delete .hkind { color: var(--del); }
.htag { flex: none; font-size: 10px; text-transform: uppercase; letter-spacing: .06em; font-weight: 600; padding: 1px 6px; border-radius: 4px; }
.hentry.add .htag { color: var(--add); background: rgba(76,195,138,.12); }
.hentry.edit .htag { color: var(--accent-bright); background: var(--glow); }
.hentry.delete .htag { color: #ff8b8b; background: rgba(242,109,109,.12); }
/* The kind is a status badge, not a control: a fixed-width pill of text,
so nothing in the row's leftmost slot has the size or shape of a
disclosure toggle. --hindent keeps meta and note aligned under the path. */
.hkind { flex: none; width: 62px; white-space: nowrap; text-align: center; font-size: 10px; text-transform: uppercase; letter-spacing: .06em; font-weight: 600; padding: 2px 6px; border-radius: 4px; color: var(--add); background: rgba(76,195,138,.12); }
.hentry.edit .hkind { color: var(--accent-bright); background: var(--glow); }
.hentry.delete .hkind { color: #ff8b8b; background: rgba(242,109,109,.12); }
.hpath { font-weight: 500; cursor: pointer; color: var(--text); font-size: 13px; }
.hpath:hover { color: var(--accent-bright); }
.htime { margin-left: auto; color: var(--text-faint); font-size: 12px; font-variant-numeric: tabular-nums; }
.hmeta { display: flex; align-items: center; gap: 14px; margin-top: 4px; padding-left: 23px; font-size: 12px; color: var(--text-dim); }
.hmeta { display: flex; align-items: center; gap: 14px; margin-top: 4px; padding-left: var(--hindent); font-size: 12px; color: var(--text-dim); }
.hdev, .hsize { color: var(--text-faint); }
.hsize { font-variant-numeric: tabular-nums; white-space: nowrap; flex: none; }
.hnote { margin-top: 4px; padding-left: 23px; font-size: 12px; color: var(--text-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
.hnote { margin-top: 4px; padding-left: var(--hindent); font-size: 12px; color: var(--text-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
.hnote:hover { color: var(--text); }
.hnote.open { white-space: normal; overflow-wrap: anywhere; }
.hnote::before { content: " "; color: var(--text-ghost); }
/* The one real disclosure in a row — so it gets the chevron, and turns. */
.hnote::before { content: ""; display: inline-block; margin-right: 5px; color: var(--text-ghost); transition: transform .12s; }
.hnote.open::before { transform: rotate(90deg); }
.hnote a { color: var(--accent-bright); text-decoration: none; }
.hnote a:hover { text-decoration: underline; }
@@ -829,6 +829,9 @@ a.ai-main:hover { color: var(--accent); }
.ai-tag { font-size: 11px; }
.htime { white-space: nowrap; font-size: 12px; }
.hline { flex-wrap: wrap; }
/* 78px of indent is a fifth of a 360px row, and the wrapped .hline has
nothing left to align under anyway. */
.hentry { --hindent: 0px; }
/* Four share-dialog buttons don't fit one row at 360 — the destructive
Revoke takes its own line rather than sitting 9px from Done. */
.modal-actions .ai-del { flex: 0 0 100%; justify-content: center; text-align: center; }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BearDrive</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='%23f5a623'><rect x='4' y='4' width='5.6' height='24'/><rect x='11.2' y='4' width='14.4' height='11.2'/><rect x='11.2' y='16.8' width='16.8' height='11.2'/></svg>">
<script type="module" crossorigin src="/assets/index-BSJZ_rnL.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D8NgjNS2.css">
<script type="module" crossorigin src="/assets/index-pwEy71bM.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D_6vQyDA.css">
</head>
<body>
<div id="root"></div>