Files
multica/packages/views/package.json
Naiyuan Qing e70e71fea1 fix(issues): repair the issue Table's column interactions, loading states and scroll rendering (#6036)
* fix(ui): make table column resizing land where the pointer is (MUL-5166)

Pressing the resize handle committed a width before any drag: it wrote the
rendered width, which fixed table-layout had stretched past the configured
one, so a stray click on a column edge silently rewrote and persisted that
column and stopped it adapting to the window. Dragging then ran ahead of the
pointer, because committing one width changes how much leftover space the
layout has to share out and rescales every other column mid-gesture. And the
gesture never ended if the pointer came up outside the window or the user
switched apps — the column kept tracking the pointer on return, with the page
stuck unselectable under a col-resize cursor.

- Require 4px of travel before anything is committed, matching the
  column-reorder sensor's activation distance on the same header.
- Pin every column to its rendered width on the frame the drag starts, so
  there is no leftover left to redistribute and the drag maps 1:1.
- Capture the pointer and end on blur / pointercancel / lostpointercapture,
  the same four-part contract the sidebar rail already uses.
- Own the cursor from a portaled full-viewport layer for the duration of the
  drag. document.body.style.cursor loses to any descendant that declares its
  own, which is why the cursor flickered over rows and text.

Resizing also re-rendered every cell each frame, and each cell carries a
popover, so a frame cost ~143ms. Widths now travel as custom properties
published once on the <table>, and the body is swapped for a memoized copy
while a drag is live — the browser applies each new width with no React work.

Visually the table had no column rules at all, which left the pinned columns'
trailing shadow as the only vertical line and made it read as a stray border
on one column. Every column now carries a rule, the pinned shadow appears
only once the surface is actually scrolled sideways, and the resize handle
brightens its rule to brand rather than matching the faint resting colour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(issues): drag a table column by its header, not by a hidden grip

Reordering columns was advertised by a grip that only appeared once the
pointer was already on the header, and dragging it moved a wrapper the height
of its own line of text while the <th> around it clipped anything that
travelled outside the cell — so the column being moved looked like it had
vanished rather than slid.

The header is now the handle. There is no grip: the cursor carries the
affordance (grab, then grabbing), the whole cell responds, and only the
sort/hide button opts out, since a press there is always the menu. The
wrapper spans the cell's box at all times — the negative margins undo the
<th>'s padding and put it back inside — so what travels is a header-sized
block and going translucent is the entire drag state, matching how a desktop
tab behaves. Overflow is lifted for the length of a reorder and the column in
hand is raised over its neighbours.

Columns are restricted to the horizontal axis, the same constraint the
desktop tab bar puts on tab reordering, which is why packages/views now
declares @dnd-kit/modifiers directly.

Transforming the <th> itself would carry the header's height along for free,
but `transform` on a table cell is a corner of the spec browsers take
liberties with — Chromium lifts the cell out of the table's box model and its
geometry stops matching the row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(issues): stop a dragged table column stretching into its neighbour's width

Dragging a column header applied dnd-kit's full transform, which carries
scaleX/scaleY alongside the translation. Those come from its layout animation:
old rect over new rect, tweening an item into the shape of the slot it lands
in. Between two tabs of equal width the ratio is 1 and never shows. Between
two columns it is not — swapping a 174px column with a 96px one stretched the
header to 1.8x on the way across. Only the horizontal translation is applied
now; reordering changes no column's width, so there is no shape for a tween to
describe. The travel and the settle stay animated through `transition`.

The travelling wrapper was also fixed at h-8 while the header strip measures
39px once row borders are counted, leaving the block short at both edges and
misaligned by 3px — which read as the same flattening. Its height is derived
from the cell now.

The reorder grip returns as the drag handle, appearing on hover as before and
staying visible for the length of a drag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ui): mark the frozen column boundary while the table is scrolled sideways

The edge where the frozen columns end had a shadow drawn inside the pinned
cell with `--border`. That token is tuned for hairlines between adjacent
surfaces — oklch .945 in light mode — and all but disappears once spread into
a shadow, so the boundary read as unmarked while content slid underneath it.
Darkening it in place only made the frozen column look outlined: an inset
shadow can shade that cell's own edge and nothing else, while the depth being
described belongs to the content passing beneath.

Split into the two things MUI X's data grid separates, a permanent border for
where the boundary is and a shadow for something crossing it right now:

- the rule between the frozen columns and the rest stays put, as before;
- a 12px gradient is cast past the frozen block, mounted outside the scroll
  container and shown only while scrolled sideways.

Its position is measured off the DOM — the trailing frozen header carries a
`data-pinned-edge` marker — rather than summed from column sizes. Fixed
table-layout stretches columns past their configured widths whenever the
container is wider than the table's min-width, so a sum lands the marker in
the middle of visible content.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(ui): size a table column to its content on double-click

Double-clicking a column's resize handle called column.resetSize, which
cleared the stored width and let TanStack fall back to its generic default of
150 — a number unrelated to any width this table was designed with. "Reset"
therefore widened priority from 130, collapsed labels from 220, and clamped
title to its 260 minimum. It restored nothing.

It now sizes the column to its widest rendered cell, the convention Excel,
Sheets, AG Grid and Notion all share for that gesture.

Fixed table-layout ignores content and the cells truncate their own text, so
nothing on screen reports the width the content wants. The measurement lifts
both constraints across the column's cells, reads them, and restores
everything within the same task, so the browser paints once — after the
restore — and the intermediate layout is never seen. Only the rows inside the
virtual window are measured.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ui): stop the table header flickering black during scroll

The sticky header carried backdrop-blur over a translucent fill. A
backdrop-filter on a sticky element with content scrolling beneath it is a
known Chromium compositing fault — the blur layer recomputes its backdrop
every frame, and virtualisation is adding and removing the very rows it reads
from, so the strip flickers black under fast scrolling. Chromium's fix for the
same symptom was reverted, so upgrading does not carry it (electron#12906,
electron#45854, chromium#339841685).

The fill is now the opaque colour that bg-muted/30 was compositing to, which
is the mix the pinned header cells already use. A header the content scrolls
behind has no reason to show it through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(issues): give the table's title column back the width its hover actions held

The sub-issue and rename buttons sat inline in the title cell at opacity 0.
Hidden is not absent: their boxes reserved around 40px of the column at all
times, in the one column with the least room to spare, for controls that only
appear on hover. They are positioned over the cell's trailing edge now, the
way SidebarMenuAction is, with a gradient fading the title running underneath
rather than icons sitting on top of it — the sidebar needs no gradient because
its labels are short, a title runs to the cell's edge. focus-within keeps them
reachable from the keyboard, where hover never fires.

Reordering a column also scrolled the table vertically. Modifiers constrain a
drag's movement but not its auto-scrolling, which reads raw pointer
coordinates, so a few pixels of vertical drift sent the rows moving under a
gesture that cannot act on them. Auto-scroll's y threshold is zero now; x
keeps it, since a table wider than its viewport needs it to reach a distant
slot, but at 0.05 rather than the default 0.2, which arms it a fifth of the
way in from either edge — most of a wide header.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(issues): show the table's own grid while its first page loads, and end its columns like every other surface

Two loading-state gaps, both in how the table's non-issue rows are modelled.

A cold load rendered a single "Loading…" line under a full header, which reads
as an empty table rather than a loading one. The surface-level skeleton meant
for it was unreachable: use-issue-surface-data hardcodes isLoading to false for
table, so the `mode === "table"` branch never ran — and it would not have
fitted, drawing rounded bars at p-2 where the table draws an edge-to-edge grid,
so switching it on would have traded one wrong state for a layout jump.

Placeholders are rows now, rendered through the ordinary cell renderer so they
inherit the real column widths, pinning and borders. Everything the table knows
before its data — header, toolbar, column layout — is up immediately, and the
rows swap in without moving anything. The unreachable surface branch is gone.

The end of a column was hand-rolled too, leaving the table the one surface
where a failed page read as muted body text rather than an error, where the
prompt sat left-aligned against three centred ones, and where reaching the end
of a paginated branch said nothing at all. The row carries its state rather
than a finished label and renders through ListLoadMoreFooter, the footer Board,
List and Swimlane already share — which exists, per its own comment, so those
states and their wording stay consistent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ui): measure table rows instead of assuming they are all one height

Virtualisation sized every row at the same 41px estimate, but the table does
not have one kind of row: group headers are 37px, an end-of-column footer
shorter still, and placeholders different again. Each one put the estimate a
few pixels out, and the error accumulates — with grouping on, the rendered
window drifts off the rows it should be showing and the scrollbar overshoots
the bottom.

Rows report their own height through the virtualizer's measureElement now, and
the estimate only covers rows that have never been mounted. Rows built by
renderRow are cloned to carry data-index and the measuring ref, so callers keep
returning a plain <tr> and still take part.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(issues): scope the title cell's actions to the title cell

The sub-issue and rename buttons keyed off the row's hover state, so pointing
anywhere along a row — a status chip, a date, empty space — put controls that
act on the title under a pointer that was somewhere else. They follow the
title cell's own hover now. The gradient behind them still tracks the row
colour, since that is what the cell is painted with while they are showing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ui): let group rows report their height, and re-measure the frozen edge on resize

Self-review of the branch turned up two places where a change did not reach as
far as it was supposed to.

Group rows never took part in the row measurement. DataTable clones the
virtualizer's ref and data-index onto whatever renderRow returns, but
IssueTableGroupRow declared only its own three props and absorbed them — and a
group header, being shorter than a data row, is exactly the row the
measurement was added for.

The frozen-column shadow measured its boundary from the scroll handler alone.
Resizing a frozen column while the surface is scrolled sideways moves that
boundary with no scroll event to follow, leaving the shadow behind at the old
position.

Also drops a dependency left behind when the load-more rows stopped building
their own labels.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 15:13:20 +08:00

136 lines
4.9 KiB
JSON

{
"name": "@multica/views",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"lint": "eslint .",
"test": "vitest run"
},
"exports": {
"./navigation": "./navigation/index.ts",
"./common/actor-avatar": "./common/actor-avatar.tsx",
"./editor": "./editor/index.ts",
"./issues/components": "./issues/components/index.ts",
"./issues/hooks": "./issues/hooks/index.ts",
"./issues/utils/filter": "./issues/utils/filter.ts",
"./issues/utils/sort": "./issues/utils/sort.ts",
"./common/task-transcript": "./common/task-transcript/index.ts",
"./projects/components": "./projects/components/index.ts",
"./autopilots/components": "./autopilots/components/index.ts",
"./modals/registry": "./modals/registry.tsx",
"./modals/create-issue": "./modals/create-issue.tsx",
"./my-issues": "./my-issues/index.ts",
"./skills": "./skills/index.ts",
"./agents": "./agents/index.ts",
"./members": "./members/index.ts",
"./inbox": "./inbox/index.ts",
"./runtimes": "./runtimes/index.ts",
"./billing": "./billing/index.ts",
"./dashboard": "./dashboard/index.ts",
"./squads": "./squads/index.ts",
"./squads/components": "./squads/components/index.ts",
"./workspace/workspace-avatar": "./workspace/workspace-avatar.tsx",
"./workspace/create-workspace-form": "./workspace/create-workspace-form.tsx",
"./workspace/no-access-page": "./workspace/no-access-page.tsx",
"./workspace/new-workspace-page": "./workspace/new-workspace-page.tsx",
"./workspace/welcome-after-onboarding": "./workspace/welcome-after-onboarding.tsx",
"./workspace/use-workspace-seen": "./workspace/use-workspace-seen.ts",
"./layout": "./layout/index.ts",
"./layout/route-icon-components": "./layout/route-icon-components.tsx",
"./auth": "./auth/index.ts",
"./search": "./search/index.ts",
"./chat": "./chat/index.ts",
"./settings": "./settings/index.ts",
"./settings/lark-tab": "./settings/components/lark-tab.tsx",
"./lark": "./lark/index.ts",
"./slack": "./slack/index.ts",
"./invite": "./invite/index.ts",
"./invitations": "./invitations/index.ts",
"./onboarding": "./onboarding/index.ts",
"./platform": "./platform/index.ts",
"./i18n": "./i18n/index.ts",
"./attachments": "./attachments/index.ts",
"./locales": "./locales/index.ts",
"./locales/*": "./locales/*"
},
"dependencies": {
"@base-ui/react": "^1.3.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@floating-ui/dom": "^1.7.6",
"@multica/core": "workspace:*",
"@multica/ui": "workspace:*",
"@tanstack/react-virtual": "catalog:",
"@tiptap/core": "3.27.1",
"@tiptap/extension-code-block-lowlight": "3.27.1",
"@tiptap/extension-document": "3.27.1",
"@tiptap/extension-highlight": "3.27.1",
"@tiptap/extension-image": "3.27.1",
"@tiptap/extension-link": "3.27.1",
"@tiptap/extension-list": "3.27.1",
"@tiptap/extension-mention": "3.27.1",
"@tiptap/extension-paragraph": "3.27.1",
"@tiptap/extension-placeholder": "3.27.1",
"@tiptap/extension-table": "3.27.1",
"@tiptap/extension-table-cell": "3.27.1",
"@tiptap/extension-table-header": "3.27.1",
"@tiptap/extension-table-row": "3.27.1",
"@tiptap/extension-text": "3.27.1",
"@tiptap/extension-typography": "3.27.1",
"@tiptap/markdown": "3.27.1",
"@tiptap/pm": "3.27.1",
"@tiptap/react": "3.27.1",
"@tiptap/starter-kit": "3.27.1",
"@tiptap/suggestion": "3.27.1",
"cmdk": "^1.1.1",
"hast-util-to-html": "^9.0.5",
"katex": "catalog:",
"lowlight": "^3.3.0",
"mdast-util-from-markdown": "^2.0.3",
"mermaid": "catalog:",
"motion": "^12.38.0",
"pinyin-pro": "3.26.0",
"react-easy-crop": "^6.2.0",
"react-markdown": "^10.1.0",
"react-qr-code": "catalog:",
"react-resizable-panels": "^4.7.5",
"react-virtuoso": "catalog:",
"recharts": "3.8.0",
"rehype-katex": "catalog:",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"remark-math": "catalog:",
"sonner": "^2.0.7"
},
"peerDependencies": {
"@tanstack/react-query": "catalog:",
"@tanstack/react-table": "catalog:",
"i18next": "catalog:",
"lucide-react": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"react-i18next": "catalog:",
"zustand": "catalog:"
},
"devDependencies": {
"@multica/eslint-config": "workspace:*",
"@multica/tsconfig": "workspace:*",
"@testing-library/jest-dom": "catalog:",
"@testing-library/react": "catalog:",
"@testing-library/user-event": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@vitejs/plugin-react": "catalog:",
"eslint-plugin-i18next": "catalog:",
"jsdom": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
}
}