# Column Resize + Reorder Implementation Plan

## Objective

Allow users to resize and move columns in `praxis-table` without conflicting with sorting, while keeping behavior predictable, accessible, and persistent.

## Guiding Principles

- `sort` belongs to a dedicated sortable label/container, not to the entire `th`
- `reorder` belongs to the draggable header cell, without a permanent move icon
- `resize` belongs to a dedicated handle on the right edge of the header cell
- runtime mutations should reuse the existing persistence pipeline through a shared column-mutation coordinator
- width persistence should normalize to `px`
- keep the existing config model and avoid introducing parallel API knobs

## Current Architecture Notes

Today the table header combines multiple behaviors on the same `th`:

- `mat-sort-header`
- `cdkDrag`
- reorder affordance/indicator
- sticky start/end support
- horizontal scroll modes already used in production

Relevant files:

- `/mnt/d/Developer/praxis-plataform/praxis-ui-angular/projects/praxis-table/src/lib/praxis-table.html`
- `/mnt/d/Developer/praxis-plataform/praxis-ui-angular/projects/praxis-table/src/lib/praxis-table.scss`
- `/mnt/d/Developer/praxis-plataform/praxis-ui-angular/projects/praxis-table/src/lib/praxis-table.ts`

This is the main reason resize must not be introduced as another gesture on the same interaction zone. It also means sticky and horizontal scroll behavior must be treated as first-order implementation concerns, not deferred work.

Additional constraints from the current implementation:

- reorder keyboard parity is attached to the dedicated header sort trigger through Ctrl/Alt + arrows
- runtime reorder does not render a visible move icon/handle, matching SAP Fiori and MUI Data Grid patterns
- persistence currently saves the full `config` snapshot for each runtime mutation
- width styling may already come from `column.width`, `headerStyle`, or `style`

Current reorder affordance note:

- reorder is implicit on the draggable header cell, matching enterprise grids such as SAP Fiori and MUI Data Grid
- the current `cdkDrag` root remains on the `th` so preview, placeholder sizing, and index semantics stay stable
- `mat-sort-header` is mounted on `.praxis-header-sort-trigger`, not on the full `th`, so sort remains a focused label interaction while header drag remains available
- keyboard reorder belongs to the focused header sort trigger via Ctrl/Alt + arrow keys

The plan must explicitly unwind those couplings instead of treating them as incidental details.

## Implementation Phases

### Phase 1: Split Header Interaction Zones

Refactor the header so each interaction has an explicit zone:

- sortable label container: sort
- draggable header cell: reorder, with no permanent move icon
- right-edge handle: resize

Changes in `praxis-table.html`:

- keep `.praxis-header-label-text`
- add a sortable wrapper `.praxis-header-sort-trigger`
- keep `cdkDrag` on the `th`
- do not render a visible reorder icon/handle in the runtime header
- add `.praxis-column-resize-handle`
- keep keyboard reorder on the header sort trigger without blocking sort Enter/Space behavior
- ensure the resize handle stops `click`, `pointerdown`, and keyboard bubbling into the sort trigger

Changes in `praxis-table.scss`:

- make the sortable label container the only area that shows sort cursor/hover treatment
- add resize handle positioned at the far right edge
- use `cursor: col-resize` for resize
- use a hit area around `8px` to `12px` for resize

Sorting strategy:

- do not keep the full `th` as the primary sort interaction target
- move the sort trigger to the label/container inside the header cell
- preserve keyboard sort semantics on that dedicated sort target
- treat suppression of `click` on resize handles as a hard requirement, not a best-effort safeguard

### Phase 2: Add Runtime Resize State

Add state to `praxis-table.ts`:

- `activeResizeColumnField: string | null`
- `resizeStartX: number`
- `resizeStartWidthPx: number`
- `isResizingColumn: boolean`
- `resizePointerId: number | null`

Add internal defaults:

- derive defaults from the existing config model first
- use `behavior.resizing.minColumnWidth` and `behavior.resizing.maxColumnWidth` when present
- fall back to sane internal defaults only when config is absent

Add handlers:

- `onColumnResizePointerDown(event, column)`
- `onColumnResizePointerMove(event)`
- `onColumnResizePointerUp(event)`
- `onColumnResizePointerCancel(event)`
- `cancelColumnResize()`

Add runtime width state:

- keep transient width changes separate from persisted `config.columns[]` until commit
- resolve whether a column is resizable from both `behavior.resizing.enabled` and `column.resizable !== false`
- exclude non-data columns (`_select`, `_expander`, `_actions`) from resize by design

### Phase 3: Prevent Interaction Conflicts

On resize start:

- call `preventDefault()`
- call `stopPropagation()`
- capture the active pointer
- temporarily block sort and reorder while `isResizingColumn === true`

Behavioral rule:

- dragging starts from the header cell, except the resize handle and sortable label interactions
- sorting only starts from the dedicated sortable label/container
- resizing only starts from the right-edge resize handle

During resize:

- apply a class such as `pfx-column-resizing`
- temporarily disable `cdkDropList`
- ignore sort triggers from the header
- ensure sticky columns and resize handles preserve usable hit areas and z-index ordering
- preserve horizontal scroll behavior while pointer capture is active
- support abort paths: `Escape`, `pointercancel`, lost capture, and pointerup outside the viewport

### Phase 4: Normalize and Apply Widths

Add width utilities to `praxis-table.ts`:

- `resolveColumnWidthPx(column, headerEl): number`
- `clampColumnWidthPx(width: number): number`
- `formatColumnWidthPx(width: number): string`

Width rules:

- if `column.width` is already in `px`, use it directly
- if width is `%`, `auto`, or empty, measure the current header width at first resize and convert to `px`
- persist resized widths as `px`
- resizing changes the internal table content width, not the host/card/viewport width
- when the summed column widths exceed the viewport, the scroll container owns horizontal overflow
- when the summed column widths are smaller than the viewport, empty space may remain after the last column instead of redistributing widths implicitly
- do not resize neighboring columns in `px` mode; the resized column changes and following columns move accordingly
- when resize is enabled, persisted `px` widths below the effective readable minimum are normalized back into `config.columns[]`, not only corrected during rendering
- the effective readable minimum includes the column header text, the resize separator gutter, and the sort gutter only when that concrete column is sortable
- explicitly define precedence when `headerStyle` or `style` also carry width declarations
- document that first user resize converts responsive width intent into fixed runtime width intent
- validate width behavior under sticky start/end columns and horizontal scroll mode

Keep `getColumnWidthStyle(column)` as the main rendering output, now backed by updated runtime values.

### Phase 5: Persistence

Reuse the existing runtime mutation persistence flow in `praxis-table.ts`, but do not let resize and reorder persist independently against unrelated config snapshots.

Add a shared column runtime mutation coordinator:

- monotonic `columnMutationOperationId`
- mutation kind: `column-reorder` | `column-resize`
- per-operation snapshot merge before `saveConfig`
- stale-callback rejection shared by reorder and resize
- serialized commit semantics when reorder and resize overlap in time

Refactor `persistTableConfigAfterRuntimeMutation(...)` usage behind a narrower helper such as:

- `persistColumnRuntimeMutation(trigger, mutateConfig, options)`

Add a new trigger:

- `column-resize`

Persist width by updating the matching entry in `config.columns[]`:

- locate by `field`
- set `width: "${px}px"`

Resize persistence policy:

- persist on commit only, not continuously during pointer move
- commit point is `pointerup` for pointer interactions
- keyboard resize commits stepwise on each handled key event
- keyboard resize uses `ArrowLeft`, `ArrowRight`, `Shift + Arrow`, `Home`, and `End`
- define stale-callback protection for resize and reorder through the same shared coordinator
- derived table width used during/after resize must be cleared when the visible column contract changes externally
- do not add undo in the first slice unless a concrete UX requirement appears, but keep the runtime contract compatible with future undo support

Emit a resize event after persistence:

- `columnResize`

Suggested payload:

- `tableId`
- `field`
- `header`
- `previousWidth`
- `currentWidth`
- `persisted`

### Phase 6: Public Configuration API

Align strictly with the existing config model. Do not introduce a second naming scheme.

Use the existing behavior flags in the core model:

- `behavior.dragging.columns`
- `behavior.resizing.enabled`
- `behavior.resizing.minColumnWidth`
- `behavior.resizing.maxColumnWidth`
- `behavior.resizing.persistWidths`
- `ColumnDefinition.resizable`

Optional future additions should only happen if the current model proves insufficient.

Recommended rollout:

- keep `dragging.columns = true`
- ship resize behind `behavior.resizing.enabled`
- honor `behavior.resizing.persistWidths` when deciding whether to save width runtime mutations
- keep `column.resizable !== false` as the per-column opt-out
- do not add new public API until the current model proves insufficient under real usage

### Phase 7: Accessibility

Resize handle:

- `role="separator"`
- `aria-orientation="vertical"`
- `aria-label="Resize column X"`
- `tabindex="0"`
- expose current/min/max width semantics explicitly when implemented
- announce committed width changes through the existing live-region pattern or an equivalent status channel

Keyboard behavior for resize handle:

- `ArrowLeft` and `ArrowRight` resize by the default step
- `Shift + ArrowLeft` and `Shift + ArrowRight` resize by the larger step
- `Home` applies the configured minimum width
- `End` applies the configured maximum width when one is configured
- `Escape` cancels an active pointer resize before commit
- keyboard resize commits stepwise on each handled key event

Reorder accessibility:

- keep keyboard reorder on the focused header sort trigger with Alt/Ctrl + arrow keys
- do not add a permanent focusable move icon unless a future accessibility review proves the header-trigger model insufficient
- preserve visible focus styling and screen-reader parity on the header label/sort trigger

### Phase 8: Testing

Add focused tests for interaction conflicts:

- resize does not trigger reorder
- resize does not trigger sort
- resize handle click does not bubble into sort click
- reorder does not alter width
- clicking the sortable label still sorts
- reorder still works through the draggable header cell
- keyboard reorder still works after moving intent to the handle
- sort keyboard interaction still works after sort leaves the `th`

Add persistence tests:

- width updates `config.columns`
- width persists through the shared column mutation coordinator
- width survives rerender/reload flow
- width persistence respects `behavior.resizing.persistWidths`
- resize persists only on commit, not on every pointer move
- concurrent resize/reorder persistence does not leave stale width/order state
- out-of-order async persistence callbacks do not overwrite the latest width/order snapshot

Add limit tests:

- `minWidthPx` is respected
- `maxWidthPx` is respected

Add pointer/cancel coverage:

- resize aborts cleanly on `Escape`
- resize aborts cleanly on `pointercancel`
- resize completes correctly when pointerup happens outside the immediate handle area

Add visual/e2e coverage later:

- resize with horizontal scroll enabled
- resize under `scroll-auto`
- resize under `scroll-wrap`
- reorder still works after resize
- sticky start column resize
- sticky end column resize
- sticky columns do not produce overlap/z-index regression while resizing
- adjacent non-data columns (`_select`, `_expander`, `_actions`) do not regress
- responsive scroll modes remain aligned between header and body

## Recommended Delivery Sequence

1. Refactor header structure so sort is isolated to a dedicated sortable label/container
2. Keep reorder implicit on the header and remove stale move-icon guidance from docs/editor copy
3. Add resize handle with pointer support, keyboard support, and explicit cancel paths
4. Validate sticky and horizontal-scroll behavior in a follow-up hardening slice
5. Introduce shared column runtime mutation coordination for reorder + resize persistence
6. Persist column width on commit only
7. Add focused conflict and persistence tests
8. Add broader sticky, horizontal-scroll, and non-data-column E2E coverage
9. Enable only through config/feature flag

## Definition of Done

- users can reorder from the header without a permanent move icon
- users can resize only from the right-edge handle
- clicking the sortable label still sorts
- reorder, resize, and sort do not interfere with each other
- width and order persist correctly
- stale async persistence callbacks cannot revert the latest width/order mutation
- existing horizontal scroll and sticky behavior remain intact after first resize

## Risk Notes

- gesture ambiguity is not solved by event suppression alone; sort must be structurally isolated from reorder/resize handles
- supporting `%` and `auto` widths requires careful first-resize normalization
- first resize may intentionally convert responsive width behavior into fixed pixel width behavior
- sticky start/end columns and horizontal scroll are immediate implementation concerns
- resize and reorder mutations need a shared commit/stale-callback policy, not parallel ad hoc logic
- table-level width is runtime-derived state; it must not outlive the column contract that produced it
- the canonical enterprise default follows the SAP/MUI model for fixed-width columns: resize the column and the internal content width, keep the outer viewport stable, and use horizontal overflow when needed

## Suggested Follow-Up

After review, break this plan into small implementation tasks by file:

- `praxis-table.html`
- `praxis-table.scss`
- `praxis-table.ts`
- relevant core config model files
- dedicated unit/integration specs
