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`
7.7 KiB
Phase 156 — tables: sticky headers + ellipsis on clipped labels
Source: owner UX audit 2026-10-03 (live interactive-browser pass): "sticky table headers (RAG/Sources/History — the tables are long; the header scrolls away)" + "At ~1,009px window width the rightmost table columns start clipped with no ellipsis hint on the 'Hidden'/'Edit' labels (the wrapper scrolls fine — just subtle)".
Story: n/a (audit-derived phase; the table machinery is phase 97's 97_kb_tree_catalog + phase 86's 86_history_page_width, both complete)
Context: styles.css:: .table-wrap (L2528–2534: position: relative; background; border; radius; box-shadow; overflow-x: auto) wraps .docs-table (width: 100%; min-width: 640px) — the RAG folder table (the kb-folders-table class), the Sources table (#git-sources-table-wrap, L3390), and the History table (the phase-50/86 width contract, L5583). The thead already has a TINTED background (L530 comment: "the thead tinted") — the sticky design needs it OPAQUE (rows must not show through on scroll). The clipped cells: the Sources table's Hidden checkbox label + Ignore paths/Remove buttons, the RAG table's Edit button, the History table's Copy/Unshare/Create link/Delete buttons — today a clipped label hard-cuts (no text-overflow: ellipsis). The sticky constraint (the design's load-bearing fact): a position: sticky th sticks to its NEAREST SCROLLING ANCESTOR — with overflow-x: auto, the wrapper's computed overflow-y is auto too, so the wrapper IS the scroll container; but it has no height bound, so it never scrolls vertically — the PAGE does. A sticky header in an unbounded wrapper therefore scrolls away with the page. The header sticks only if the wrapper itself is the vertical scroll region (a bounded max-height) — the standard scrollable-table-with-sticky-header pattern. That is the UX trade this phase makes (ASSUMPTION A1): the tables become bounded internal-scroll panels; the page no longer carries their height.
Objective
On the three catalogue tables (RAG folders, Sources, History): the header row stays pinned while the user scrolls the table's own (now bounded) vertical scroll, and a clipped cell label shows … instead of a hard cut — the full text remains reachable (title/aria, the established idiom).
Dependencies
97_kb_tree_catalog(complete) — the RAG folder table + the.table-wrapcard.86_history_page_width(complete) — the History table's width contract (the sticky design must keep it — the table stays full-width inside the bounded panel).63_unambiguous_document_listing(complete) — the Sources table's column idiom (theHiddenlabel + per-row actions).
Design (shared by all tasks — the executor reads this, not the chat)
- A1 (owner-locked): the bounded-panel design —
.table-wrapgainsmax-height(ASSUMPTION A2:min(70vh, 64rem)— the phase picks this; it must keep a 15-row table scrolling while a 3-row one fits without a scrollbar: themin()of a viewport fraction and a fixed cap does exactly that) +overflow: auto(supersedingoverflow-x: auto— the horizontal scroll contract is UNCHANGED: the table'smin-width: 640pxstill triggers the wrapper's horizontal scroll at narrow widths;overflow: autokeeps both axes). Thethsticky:position: sticky; top: 0; z-index: 1;on.table-wrap table thead thwith an OPAQUE background (the existing tint made opaque —background: var(--surface)or the existing tint token, whichever the L530 comment names; check the current value and make it non-transparent; do NOT add a new palette variable — B3: the 17-variable contract is untouched, and an all-default theme stays byte-identical in the served CSS — the rule changes apply to BOTH themes equally). - A3 (owner-locked): the ellipsis hint — the clipped cell LABELS get
max-width+overflow: hidden; text-overflow: ellipsis; white-space: nowrap;(a.cell-clipclass or a direct selector on the action-cell text nodes — the executor's choice; thedocs-table's existing cell padding is untouched). Coverage: the SourcesHiddencheckbox label +Ignore paths+Remove; the RAGEdit; the History action buttons (Copy/Unshare/Create link/Delete). The FULL text stays reachable: every clipped label's control already carries its accessible name (theRemovebutton'saria-labelnames the full source — the phase-89 idiom; CHECK each of the other controls and add atitle/aria-labelwhere one is missing — the audit: the RAGEditbutton, the History action buttons, theIgnore pathsbutton — add the missing labels in the same task). - Scope guard: the CHAT view's
.md-table-wrap(L539 — the markdown-rendered tables) is a DIFFERENT class — UNTOUCHED (it has no card, no height bound; the design is for the catalogue tables only). The mobile ≤640px contract (PLAN §7.1) is re-verified in the E2E (the hamburger nav + the tables' horizontal scroll at ≤640px still work — themax-heightmust not break the mobile scroll model).
Tasks
01_sticky_header.md— the bounded-panelmax-height/overflow: auto+ the opaque stickyth(all three catalogue tables share the class — one rule).02_ellipsis_hints.md— the clipped-label ellipsis + the missing title/aria audit (the five control families).03_unit_pins.md— the CSS contract pins (the existing styles-pinning idiom — the:root-drift test parsesstyles.css; extend the same suite).04_e2e_table_polish.md— the dedicated Playwright suite (narrow viewport: the sticky offset + the computed ellipsis style + the mobile contract).
Testing & Quality
- Unit: the CSS pins —
.table-wrapcarriesmax-height: min(70vh, 64rem)+overflow: auto;.table-wrap table thead thcarriesposition: sticky; top: 0; z-index: 1+ an opaquebackground(parse the stylesheet — the styles-pinning idiom, the:root-drift test's precedent); the.md-table-wraprule is byte-UNCHANGED (the scope guard). - Integration: none (CSS-only phase — the server pins stay byte-identical as regression guards).
- Coverage: >90% on modified code (CSS pinned by the source-level suite + the E2E).
- The phase is "Complete" only when the entire project test suite passes 100% (every test in the project, not just this phase's).
- E2E:
uv run pytest tests/e2e/test_table_ux_polish.py -v --no-covin isolation (DB up:podman compose up -d db).
Documentation & Consistency
- The final pass reviews
README.mdandAGENTS.mdin full against the project as it stands after this phase — fixing every inconsistency found, including pre-existing ones not caused by the phase. - Tasks update
README.md/AGENTS.mdimmediately when their own changes are user-visible (the bounded-panel scroll is a visible UX change — name it where the pages' layout is described, if anywhere).
Completion Criteria
- all three catalogue tables: after scrolling the wrapper's vertical scroll, the
theadstays at the panel top (the E2E asserts theth's offsetTop ≈ 0 while the wrapper's scrollTop > 0) - the horizontal-scroll contract is unchanged (narrow viewport: the table's
min-width: 640pxstill scrolls inside the wrapper — the E2E) - a clipped action label shows
text-overflow: ellipsis(the E2E asserts the COMPUTED style on a forced-narrow cell — deterministic, no pixel assertions) and its full text is reachable (title/aria present on every clipped control) - the mobile ≤640px contract holds (the hamburger nav + the table scroll — the E2E)
uv run pytestgreen; the dedicated E2E green in isolation;uv run ruff check . && uv run pyrightclean;app/coverage >90%- one atomic commit per AGENTS.md rule 8 (
--no-gpg-sign)