Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- The task name column in the table can now be resized

### Fixed

- Fixed table columns shifting left instead of widening when dragging a column edge to the right
- Fixed resized table columns snapping back narrower when the table is wider than the view

## [2.6.0] - 2026-10-06

### Highlights
Expand Down
4 changes: 3 additions & 1 deletion packages/ui/src/views/snapshotTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ export function renderSnapshotTable(container: HTMLElement, model: ViewModel): H
for (const id of columns) {
const th = hrow.createEl('th', { text: viewFieldLabel(id, customFields) })
const width = columnWidth(model.fields, id)
th.setCssStyles({ width: width === undefined ? 'auto' : `${width}px` })
const titleMin = id === 'title' ? model.fields.table?.widths?.title : undefined
if (width !== undefined) th.setCssStyles({ width: `${width}px`, minWidth: `${width}px` })
else th.setCssStyles({ width: 'auto', minWidth: titleMin === undefined ? '' : `${titleMin}px` })
const rank = model.sort.findIndex((rule) => rule.key === id)
if (rank >= 0) {
const indicator = th.createSpan({
Expand Down
28 changes: 22 additions & 6 deletions src/views/table/TableRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export interface TableContext {

/** A column dragged narrower than this hides instead. */
const HIDE_BELOW = 24
/** Matches the title cell's `min-width` in table.css. */
const TITLE_MIN_WIDTH = 180

export function renderTable(ctx: TableContext): void {
const wrapper = ctx.container.createDiv('pm-table-wrapper')
Expand Down Expand Up @@ -134,7 +136,9 @@ export function renderTable(ctx: TableContext): void {
const label = viewFieldLabel(id, customFields)
const th = hrow.createEl('th')
const width = columnWidth(ctx.fields, id)
th.setCssStyles({ width: width === undefined ? 'auto' : `${width}px` })
const titleMin = id === 'title' ? ctx.fields.table?.widths?.title : undefined
if (width !== undefined) th.setCssStyles({ width: `${width}px`, minWidth: `${width}px` })
else th.setCssStyles({ width: 'auto', minWidth: titleMin === undefined ? '' : `${titleMin}px` })
const key = SORT_KEYS.find((k) => k === id)
if (key) {
th.addClass('pm-table-th-sortable')
Expand All @@ -160,7 +164,7 @@ export function renderTable(ctx: TableContext): void {
} else {
th.setText(label)
}
if (width !== undefined) attachColumnResize(th, id, ctx)
attachColumnResize(th, id, ctx)
}
paintSortIndicators()

Expand All @@ -187,7 +191,8 @@ export function renderTable(ctx: TableContext): void {

/**
* Dragging a header's right edge sets that column's width in the view; letting go below
* `HIDE_BELOW` hides the column instead.
* `HIDE_BELOW` hides the column instead. The title's width is only a minimum, since it
* fills whatever the other columns leave, and it never hides.
*/
function attachColumnResize(th: HTMLElement, id: string, ctx: TableContext): void {
th.addClass('pm-table-th-resizable')
Expand All @@ -197,24 +202,35 @@ function attachColumnResize(th: HTMLElement, id: string, ctx: TableContext): voi
down.preventDefault()
down.stopPropagation()
handle.setPointerCapture(down.pointerId)
const table = th.closest('table')
const headers = Array.from(th.parentElement?.children ?? []).filter((cell) => cell.instanceOf(HTMLElement))
const startStyles = headers.map((cell) => ({ width: cell.style.width, minWidth: cell.style.minWidth }))
const startTableWidth = table?.offsetWidth ?? 0
for (const cell of headers) cell.setCssStyles({ width: `${cell.offsetWidth}px`, minWidth: `${cell.offsetWidth}px` })
const startWidth = th.offsetWidth
const isTitle = id === 'title'
let width = startWidth
th.addClass('is-resizing')
const move = (e: PointerEvent): void => {
width = Math.max(0, Math.round(startWidth + e.clientX - down.clientX))
th.setCssStyles({ width: `${width}px` })
width = Math.max(isTitle ? TITLE_MIN_WIDTH : 0, Math.round(startWidth + e.clientX - down.clientX))
th.setCssStyles({ width: `${width}px`, minWidth: `${width}px` })
table?.setCssStyles({ width: `${startTableWidth + width - startWidth}px` })
th.toggleClass('is-hiding', width < HIDE_BELOW)
}
const up = (): void => {
handle.removeEventListener('pointermove', move)
handle.removeEventListener('pointerup', up)
handle.removeEventListener('pointercancel', up)
th.removeClass('is-resizing', 'is-hiding')
headers.forEach((cell, i) => cell.setCssStyles(startStyles[i]))
table?.setCssStyles({ width: '' })
if (width === startWidth) return
const list = (ctx.fields.table ??= { visible: [...ctx.columns] })
if (width < HIDE_BELOW) {
if (!isTitle && width < HIDE_BELOW) {
list.visible = ctx.columns.filter((column) => column !== id)
if (list.widths) Reflect.deleteProperty(list.widths, id)
} else if (isTitle && width === TITLE_MIN_WIDTH) {
if (list.widths) Reflect.deleteProperty(list.widths, id)
} else {
list.widths = { ...list.widths, [id]: width }
}
Expand Down
Loading