phase: 156_table_ux_polish
Build and Push Containers / build-and-push-app (push) Successful in 2m15s
Build and Push Containers / build-and-push-db (push) Successful in 11s

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:
2026-10-04 20:46:57 -04:00
parent 742eef955b
commit 1513981c1c
33 changed files with 1790 additions and 58 deletions
+103 -22
View File
@@ -2524,14 +2524,74 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
Tokens view carried the identical defect). Zero-offset positioning
changes no layout: the spans stay clipped by their own
clip: rect(0 0 0 0) + 1px box, and cards without such spans (the
RAG .docs-table, the git-sources table) render unchanged. */
RAG .docs-table, the git-sources table) render unchanged.
Phase 156 (task 01, owner-locked A1): the card is ALSO the
vertical scroll region — max-height: min(70vh, 64rem) bounds it
(a 15-row table scrolls, a 3-row one fits without a scrollbar)
and overflow: auto (superseding the phase-86 overflow-x pin —
the tables' min-width still triggers the in-card horizontal
scroll at narrow widths) makes the card the sticky th's
scrolling ancestor; an unbounded card never scrolls vertically,
so a sticky header in it would scroll away with the page. */
.table-wrap {
position: relative;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow-x: auto;
overflow: auto;
max-height: min(70vh, 64rem);
}
/* Phase 156 (task 01): the ONE sticky-header rule for every
catalogue table inside a .table-wrap card (the RAG .docs-table,
the .git-sources-table, the .history-table, the .tokens-table —
the per-table th rules keep the text styling only). top: 0 pins
the header row to the card top while the card's own vertical
scroll moves; z-index: 1 keeps the pinned th above the scrolled
cells. OPAQUE background: --brand-soft is a solid hex (no alpha),
so rows never show through the pinned header (B3: no new palette
variable; a themed page's #bor-theme override of the token
handles the color — same behavior, B4). */
.table-wrap table thead th {
position: sticky;
top: 0;
z-index: 1;
background: var(--brand-soft);
}
/* Phase 156 (task 02, owner-locked A3): the ellipsis hint on the
catalogue tables' CLIPPED action labels. The class sits on the
LABEL SPAN, never the cell (the docs-table cell padding is
untouched). The five families: the Sources "Hidden" checkbox
text + "Ignore paths" + "Remove" (git-sources.js makeRow), the
RAG folder table's "Edit" (sources.js makeDescCell), the
History "Copy" / "Unshare" / "Create link" / "Delete" (history.js).
max-width: 12rem is GENEROUS — every one of the five labels
("Edit" ~2.4rem … "Ignore paths" ~6.3rem at 0.78–0.82rem)
fits it, so a cell that FITS shows its full text (the clamp
engages only in the CLIPPED case — the audit's ~1,009px
rightmost columns), where the label shows "…" instead of the
card edge's hard cut. Mechanism: the block container's
overflow hidden + text-overflow ellipsis + nowrap is the
house .sync-label shrinkable-item pattern; the SPAN's
min-width: 0 lets the flex item shrink below its text width —
but a flex item shrinks only when its FLEX PARENT is
constrained, so the hosting controls carry the supporting width
rules (the max-width: 100% + min-width: 0 added to each block
below): without them the inline-flex buttons keep their content
size, overflow the squeezed cell, and the card edge hard-cuts
the label again (the pre-phase defect). The FULL text stays
reachable — every one of these controls' aria-label names the
row's identity (the source's URL/path — the phase-89/105 idiom;
the chat title — the phase-66 idiom; the source/folder name —
the phase-97 idiom), so the clip is visual only. B5 idiom
intact: the labels are text (text + color, never color alone). */
.cell-clip {
display: block;
min-width: 0;
max-width: 12rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.docs-table {
width: 100%;
@@ -2546,13 +2606,10 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
white-space: nowrap;
}
.docs-table th {
background: var(--brand-soft);
color: var(--brand-ink);
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.04em;
position: sticky;
top: 0;
}
.docs-table td:nth-child(2) { font-family: var(--mono); font-size: 0.82rem; max-width: 30rem; overflow: hidden; text-overflow: ellipsis; }
.docs-table tbody tr:hover { background: var(--bg); }
@@ -2720,6 +2777,14 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
flex: 0 0 auto;
display: inline-flex;
align-items: center;
/* Phase 156 (task 02, A3): the width support for the label span's
clamp — the flex item is clamped to the cell + shrinkable
(min-width: 0), so a squeezed Description cell ellipsizes the
"Edit" label instead of overflowing the card edge. The level
block's use (the non-flex .kb-level-body) is unaffected: its
natural width never reaches the clamp. */
max-width: 100%;
min-width: 0;
min-height: 24px;
padding: 0.15rem 0.7rem;
border: 1px solid var(--line);
@@ -3384,16 +3449,15 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
.ignore-editor-save:hover:not(:disabled) { background: var(--brand-hover); }
/* The list: the Sources page's table pattern — full width in the
72rem frame, surface card, horizontally scrollable wrapper (the
URL column never wraps or ellipsizes: long URLs, credentials
included, scroll the wrapper instead of truncating). */
#git-sources-table-wrap {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow-x: auto;
}
72rem frame. The URL column never wraps or ellipsizes: long
URLs, credentials included, scroll the wrapper instead of
truncating (the .git-sources-table 560px min-width keeps the
in-wrapper horizontal scroll). Phase 156 (task 01): the id
card's duplicate background/border/radius/shadow/overflow
declarations were REMOVED — the wrapper carries
class="table-wrap", so it inherits the base card + the
bounded-panel max-height + the sticky rule (the one-rule
contract: no divergent unbounded override). */
.git-sources-table {
width: 100%;
border-collapse: collapse;
@@ -3407,13 +3471,10 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
white-space: nowrap;
}
.git-sources-table th {
background: var(--brand-soft);
color: var(--brand-ink);
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.04em;
position: sticky;
top: 0;
}
/* Location cell: mono at the Sources-table size; the <code> is plain
(no chip background — the cell IS the mono readout). Phase 38:
@@ -3448,6 +3509,10 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
align-items: center;
justify-content: center;
gap: 0.35rem;
/* Phase 156 (task 02, A3): the width support for the "Remove"
label span's clamp (same rationale as .git-source-hidden
above). */
max-width: 100%;
min-height: 44px;
min-width: 44px;
flex: 0 0 auto;
@@ -3476,6 +3541,10 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
display: inline-flex;
align-items: center;
justify-content: center;
/* Phase 156 (task 02, A3): the width support for the "Ignore
paths" label span's clamp (same rationale as .git-source-hidden
above). */
max-width: 100%;
min-height: 44px;
min-width: 44px;
flex: 0 0 auto;
@@ -3525,6 +3594,11 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
display: inline-flex;
align-items: center;
gap: 0.4rem;
/* Phase 156 (task 02, A3): the width support for the "Hidden"
label span's clamp (the label is an inline-flex child of the
cell — clamped to the cell so the span can ellipsize in a
squeezed column instead of overflowing the card edge). */
max-width: 100%;
height: 44px;
padding: 0 0.5rem;
font-size: 0.8rem;
@@ -3678,7 +3752,6 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
vertical-align: middle;
}
.history-table th {
background: var(--brand-soft);
color: var(--brand-ink);
font-size: 0.82rem;
text-transform: uppercase;
@@ -3730,9 +3803,17 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
}
/* Actions: the Delete ghost button (the tuning row-action language)
+ the inline two-step confirm pair (phase 50 task 04). */
.history-actions { display: inline-flex; align-items: center; gap: 0.4rem; }
/* Phase 156 (task 02, A3): the width support for the action
labels' clamp — both wrappers are clamped to the cell (max-width
100% of the td's content box), and the flex-item buttons are
shrinkable (min-width: 0), so a squeezed Actions/Share column
ellipsizes the label instead of the card edge hard-cutting it.
Mobile (≤640px) is unaffected in kind: the flex-wrap there wraps
the buttons onto their own lines before the clamp engages. */
.history-actions { display: inline-flex; align-items: center; gap: 0.4rem; max-width: 100%; }
.history-delete {
min-height: 44px;
min-width: 0;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
@@ -3785,12 +3866,13 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
row-action language of .history-delete: --line border, transparent
fill, ink-soft (>=5.1:1), ≥44px comfortable target, focus-visible
via the global 3px rule; hover takes the brand pair (6.9:1). */
.history-share { display: inline-flex; align-items: center; gap: 0.4rem; flex-wrap: nowrap; }
.history-share { display: inline-flex; align-items: center; gap: 0.4rem; flex-wrap: nowrap; max-width: 100%; }
.history-share-cell { white-space: nowrap; }
.history-share-create,
.history-share-copy,
.history-unshare {
min-height: 44px;
min-width: 0; /* phase 156 (task 02, A3) — the shrinkable flex item (the clamp support above) */
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
@@ -3984,7 +4066,6 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
vertical-align: middle;
}
.tokens-table th {
background: var(--brand-soft);
color: var(--brand-ink);
font-size: 0.82rem;
text-transform: uppercase;