phase: 156_table_ux_polish
Everything is verified. All gates green on the current working tree. Final report: **Phase 156 — final verification pass (all 4 task files present in `complete/` — no outstanding work)** - **Verified (not re-implemented):** bounded-panel `.table-wrap` (`max-height: min(70vh, 64rem)`, `overflow: auto`, opaque sticky `th` via `var(--brand-soft)`), `.cell-clip` ellipsis rule + the five label families in the three renderers, the added aria-labels, unit pins (incl. `.md-table-wrap` byte-unchanged scope guard, `#git-sources-table-wrap` harmony — no divergent rule), dedicated E2E suite (sticky ×3 tables, horizontal contract, computed-style ellipsis + reachable full text, mobile hamburger + scroll + pin) - **Tests/lint/coverage:** `uv run pytest --cov=app --cov-report=term-missing` → **3570 passed, 99% on `app/`** (gate >90%); `uv run pytest tests/e2e/test_table_ux_polish.py -v --no-cov` (DB up) → **6/6 passed in isolation**; `uv run ruff check . && uv run pyright` → **clean** (0 errors) - **Completion criteria:** sticky header all 3 tables (exact-0 offset while `scrollTop > 0`) ✅ · horizontal scroll unchanged ✅ · ellipsis computed style + actual clip + title/aria reachable ✅ · mobile ≤640px contract ✅ · full suite/E2E/lint/coverage ✅ · atomic commit: **harness commits this phase after the pass** (per pipeline protocol — no git history commands run by me) - **Documentation:** AGENTS.md phase-156 wording (bounded panels, sticky headers, `.cell-clip` + aria-label idiom) matches the implementation exactly — verified consistent; README.md reviewed in full (pages/URLs vs registered routes, all `.env.example` vars documented, Quick Start, eval/llm scripts, compose prod profile, E2E examples) — verified consistent, no changes needed (README describes no table layout/scroll, so no phase-156 mention required) - **Deviations:** none — all work was already in the working tree; this pass re-verified every gate and the docs - **Next pending phase:** `157_chat_chip_cap`
This commit is contained in:
@@ -35,6 +35,11 @@
|
||||
* "Delete? [Yes] [No]" (focus moves to Yes, so the confirm is
|
||||
* keyboard-reachable), Yes fires the DELETE and removes the row,
|
||||
* No (or a failed request) keeps it.
|
||||
* Phase 156 (task 02, A3): the Create link / Copy / Unshare /
|
||||
* Delete visible texts are .cell-clip spans — a squeezed
|
||||
* Actions/Share cell shows "…" instead of a hard cut; every one
|
||||
* of these buttons' aria-label names the chat title (the
|
||||
* phase-66 idiom), so the full text stays reachable.
|
||||
*
|
||||
* The share copy has the owner-locked inline-link fallback: a
|
||||
* non-secure (http) homelab origin rejects navigator.clipboard, so the
|
||||
@@ -214,7 +219,14 @@ export async function mount(root) {
|
||||
del.type = "button";
|
||||
del.className = "history-delete";
|
||||
del.setAttribute("aria-label", `Delete saved chat: ${chat.title}`);
|
||||
del.textContent = "Delete";
|
||||
/* Phase 156 (task 02, A3): the visible text is the .cell-clip
|
||||
span — a squeezed Actions cell shows "…" instead of a hard
|
||||
cut; the aria-label above names the chat title (the
|
||||
phase-66 idiom), so the full text stays reachable. */
|
||||
const delClip = document.createElement("span");
|
||||
delClip.className = "cell-clip";
|
||||
delClip.textContent = "Delete"; // textContent only (XSS contract)
|
||||
del.append(delClip);
|
||||
|
||||
function restoreDelete() {
|
||||
cell.replaceChildren(del);
|
||||
@@ -326,7 +338,13 @@ export async function mount(root) {
|
||||
create.type = "button";
|
||||
create.className = "history-share-create";
|
||||
create.setAttribute("aria-label", `Create share link: ${chat.title}`);
|
||||
create.textContent = "Create link";
|
||||
/* Phase 156 (task 02, A3): the visible text is the .cell-clip
|
||||
span (the aria-label above names the chat title — the
|
||||
phase-66 idiom, the full text stays reachable). */
|
||||
const createClip = document.createElement("span");
|
||||
createClip.className = "cell-clip";
|
||||
createClip.textContent = "Create link"; // textContent only (XSS contract)
|
||||
create.append(createClip);
|
||||
create.addEventListener("click", () => void createShareLink(chat, cell, create));
|
||||
cell.replaceChildren(create);
|
||||
}
|
||||
@@ -340,13 +358,22 @@ export async function mount(root) {
|
||||
copy.type = "button";
|
||||
copy.className = "history-share-copy";
|
||||
copy.setAttribute("aria-label", `Copy share link: ${chat.title}`);
|
||||
copy.textContent = "Copy";
|
||||
/* Phase 156 (task 02, A3): the visible texts are .cell-clip
|
||||
spans (the aria-labels above name the chat title — the
|
||||
phase-66 idiom, the full text stays reachable). */
|
||||
const copyClip = document.createElement("span");
|
||||
copyClip.className = "cell-clip";
|
||||
copyClip.textContent = "Copy"; // textContent only (XSS contract)
|
||||
copy.append(copyClip);
|
||||
copy.addEventListener("click", () => void copyRowShareLink(chat, cell));
|
||||
const unshare = document.createElement("button");
|
||||
unshare.type = "button";
|
||||
unshare.className = "history-unshare";
|
||||
unshare.setAttribute("aria-label", `Unshare saved chat: ${chat.title}`);
|
||||
unshare.textContent = "Unshare";
|
||||
const unshareClip = document.createElement("span");
|
||||
unshareClip.className = "cell-clip";
|
||||
unshareClip.textContent = "Unshare"; // textContent only (XSS contract)
|
||||
unshare.append(unshareClip);
|
||||
function restoreShared() {
|
||||
cell.replaceChildren(copy, unshare);
|
||||
unshare.focus({ preventScroll: true }); // focus returns to the (restored) control
|
||||
|
||||
Reference in New Issue
Block a user