7.9 KiB
spec, created, status
| spec | created | status |
|---|---|---|
| docs/specs/ui/requirements/resizable-markdown-tables.md | 2026-08-05 | complete |
Implementation Plan: Resizable Markdown Table Columns
Overview
Add ephemeral, accessible column resizing to the shared Markdown table renderer without weakening the wrapping repair already covered by chat browser tests. Desktop fine-pointer users receive full-height inline separators; phone and coarse-pointer layouts retain automatic wrapping and local scrolling with no resize controls.
Frontend design
Width geometry
Add a small pure geometry module under apps/web/lib/markdown/ that adjusts an
adjacent width pair while preserving its sum and enforcing the 64-pixel minimum.
Use the same function for pointer and keyboard input so clamping behavior has one
testable source of truth. If the pair totals less than 128 pixels, leave both
widths unchanged because the total-width and dual-minimum constraints cannot
otherwise both hold.
Resizable table component
Extract the shared renderer's current table wrapper into a client component,
likely apps/web/components/shared/resizable-markdown-table.tsx.
- Render the semantic
<table>unchanged in automatic mode. - Measure the table and its header cells after layout. A
ResizeObserverkeeps separator positions aligned while automatic content or container geometry changes, and controlled column-width updates explicitly trigger another measurement even when the table's outer box is unchanged. Observe character data as well as child-list mutations so streamed text updates also remeasure. - Place focusable
role="separator"controls in an overlay within the table's scroll content. Each control is centered on an internal boundary, spans the full table height, and scrolls with the table. Omit a boundary while either adjacent measured column is below the 64-pixel minimum. - Give each separator an adjacent-column accessible name, vertical orientation, a 64-pixel minimum, the rounded current left-column width, and a maximum equal to the pair total minus the right-column minimum. Verify those rendered ARIA semantics in Playwright alongside keyboard behavior.
- On first adjustment, snapshot measured widths and apply them through a
<colgroup>with fixed table layout. Keep the measured table width constant so one adjacent column grows exactly as the other shrinks. Do not activate fixed widths until pointer movement has a non-zero delta. - Use pointer capture for dragging. Restore the drag-start snapshot on
pointercancel, and clean up cursor and selection overrides on every exit path. - Double-click or
Enterclears the custom width state and returns the table to CSS automatic layout. A changed column count does the same. - Render controls only when the responsive capability is non-phone and fine-pointer. Tables that cannot provide valid multi-column geometry stay automatic. A capability change during a drag also clears the active drag and document-level cursor and selection overrides.
Styling and localization
Add narrowly scoped Markdown-table classes in apps/web/app/globals.css for the
relative stage, separator hit area, and hover/focus/active line. Preserve the
existing overflow-x-auto wrapper and the two-/three-column wrapping and
four-plus-column minimum-width policies.
Add the separator's accessible name to the shared English locale and access it
through t(); no visible copy or public documentation is added.
Responsive and mobile implementation
The nearest mobile exemplar is the current task-chat Markdown table tested by
mobile-markdown-wrap.spec.ts. The component shares parsing, measurement-safe
markup, wrapping, and the local scroll owner at every viewport. It suppresses
the separator overlay on phones and coarse pointers because a usable touch hit
target would obstruct narrow table content. Mobile capability remains complete
for reading and scrolling; only the optional precision adjustment is desktop
specific.
Tests
Unit tests
- RED: adjacent-pair resizing grows and shrinks by equal amounts.
- RED: both drag directions clamp at 64 pixels without changing the pair total.
- RED: keyboard deltas use the same geometry behavior.
- Cover the resize hook's keyboard branches, capability-disable reset, and column-count-change reset in a focused hook test.
- GREEN: implement the geometry module and table component, then wire it into
markdown-components.tsx. - The repository does not add isolated React component tests for pure UI. Playwright covers separator semantics, rendered geometry, reset behavior, and responsive capability gating through the real shared renderer.
Likely files:
apps/web/lib/markdown/table-resize.tsapps/web/lib/markdown/table-resize.test.tsapps/web/components/shared/resizable-markdown-table.tsxapps/web/components/shared/use-markdown-table-resize.tsapps/web/components/shared/use-markdown-table-resize.test.tsapps/web/components/shared/markdown-components.tsxapps/web/app/globals.cssapps/web/src/locales/en/common.json
Browser tests
Extend the existing chat Markdown wrapping suites rather than creating a second fixture path.
- Desktop: render a three-column table, start a drag at a body-row point on the full-height boundary, and assert adjacent widths change, the untouched column and total width remain stable, and minimum-width clamping works.
- Desktop: assert double-click returns near the initial automatic proportions;
assert arrow keys resize and
Enterresets. - Desktop wide table: scroll its local wrapper and assert separators remain aligned and no chat/document overflow appears.
- Mobile Chrome: assert no separators are exposed while the existing ordinary wrapping and wide-table local scrolling checks still pass.
Likely files:
apps/web/e2e/tests/chat/markdown-wrap.spec.tsapps/web/e2e/tests/chat/mobile-markdown-wrap.spec.ts
Implementation waves
Execution is sequential in the primary conversation. The E2E work depends on the renderer contract, and this package does not authorize subagent use.
Verification
(cd apps && pnpm install --frozen-lockfile)
(cd apps && pnpm --filter @kandev/web exec vitest run \
lib/markdown/table-resize.test.ts \
components/shared/use-markdown-table-resize.test.ts \
components/shared/markdown-components.test.tsx)
(cd apps/web && pnpm run typecheck)
(cd apps && pnpm --filter @kandev/web lint)
make build-web
(cd apps/web && pnpm e2e:run tests/chat/markdown-wrap.spec.ts)
(cd apps/web && pnpm e2e:run --project mobile-chrome tests/chat/mobile-markdown-wrap.spec.ts)
Documentation impact
The durable UI spec and plan cover the new behavior. No public CLI, config, API, or workflow documentation changes are required.
Risks
- Separator geometry must use the scroll content's coordinate system; viewport coordinates would drift after horizontal scrolling.
- Streaming updates can change row height or column count after initial render. Observation must update positions, while structure changes must discard custom widths.
- Fixed widths must activate only after user adjustment. Applying them on first render would regress responsive automatic wrapping.
- Pointer cancellation and unmount must restore global cursor and text-selection state to avoid a sticky resize cursor elsewhere in the app.
- Component tests run in jsdom, so deterministic width math belongs in a pure helper and real geometry assertions belong in Playwright.
Open questions
None.
Verification results
vitest: 42 focused geometry/hook/shared-renderer tests passed.pnpm run typecheck: passed.pnpm --filter @kandev/web lint: passed with zero warnings.make build-web: passed.- Desktop
markdown-wrap.spec.ts: 8 tests passed. - Mobile Chrome
mobile-markdown-wrap.spec.ts: 2 tests passed. git diff --check: passed.