Skip to content

Commit 7af2145

Browse files
fix(plugin-kanban, plugin-gantt): phone kanban opens on its first non-empty column; narrow gantt bars show their title beside the bar (#27435)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8UjW5hY4zzDTaMsfZutCvCm)_ Fixes #27418 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:手机上的看板首次加载时,自动滚到第一个有卡片的列(列顺序不变,用户一动手就不再移动);甘特图上放不下标题的窄条,把完整标题画在条的右侧,宽条照旧在条内带省略号显示。 - 为什么改:HotCRM 18.2.0 手机走查第 12–13 行:管道看板打开停在空的第一阶段;月视图的条只有 18px,标题一个字都显示不出,连省略号都没有。 - 风险与回滚:只动 `plugin-kanban` 的平铺看板滚动器和 `plugin-gantt` 的条标题;桌面宽看板第一个有卡片的列已在视野内时不滚动,宽条(可用宽度 ≥96px)标题位置不变。回滚 = revert 本 PR。 - 先看:`KanbanImpl.tsx` 的 `initialColumnShownRef` 两个 effect;`GanttView.tsx` 的 `barLabelPlacement` 与两处标题渲染。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/plugin-kanban/src/__tests__/phoneOpensOnFirstFilledColumn-27418.test.tsx packages/plugin-gantt/src/GanttView.barLabelPlacement-27418.test.tsx`,预期 11 passed。 - 需要你做的:无,席位落地。 ## What changes **Row 12, `ui/packages/plugin-kanban/src/KanbanImpl.tsx` (flat board only).** - On its first settled load (`recordsSettled`, and `boardWidth` measured so columns have their final width), the flat scroller brings the first column that holds a card into view. It moves by the distance between that column and the first one, from wherever the snap rests the board (Chromium rests a phone board at `scrollLeft: 16`). - It is one-shot. It ends on the first settled load that holds a card, whether or not it scrolled, and on any `pointerdown` / `touchstart` / `wheel` / `keydown` on the scroller. A board that settles empty waits for its first card unless the viewer moved first. - A column already fully in view is never scrolled to, so a wide desktop board is unchanged. Column order is unchanged. The swimlane layout is unchanged. **Row 13, `ui/packages/plugin-gantt/src/GanttView.tsx` (bar titles).** - `barLabelPlacement(barWidth, title)` puts the title inside the bar when the whole title fits after the bar's `px-2`, or when the bar has at least 96px of room for a truncated title. Otherwise the title is drawn beside the bar's end (`BESIDE_BAR_LABEL_CLASS`: `left-full`, one line, `text-foreground`, never truncated). The fit uses a per-character estimate at the label's 10px font (CJK 10px, other 5.5px, matching Chromium's measured widths). It only chooses the placement and never cuts the title. - Applies to leaf bars and summary bars. Each label now carries `data-testid="gantt-bar-label-ID"` and `data-placement`. ## Why Rows 12–13 of #27355 (HotCRM 18.2.0 at phone width), carded as #27418. Measured in real Chromium (`/opt/pw-browsers/chromium`) at 390x844 on a fixture page that renders `KanbanBoard` and `GanttView` from source with the console's Tailwind build. HotCRM's metadata was not used. | Reading | Before (`45c9be5f`) | After | |---|---|---| | Kanban, stages 1–2 empty: column in view on first paint | `scrollLeft` 16, "Prospecting" (empty) | `scrollLeft` 756, "Proposal" (first with cards) | | Kanban, stage 1 has cards | `scrollLeft` 16, "Prospecting" | `scrollLeft` 16, "Prospecting" (unchanged) | | Gantt month, 18px bar, long title | label box 0px wide inside the bar: no text, no ellipsis | whole title beside the bar, 297px | | Gantt week, 31px bar, "Holiday Email Nurture" | 13px inside: an ellipsis and a letter | whole title beside the bar, 104px | | Gantt week, 63px bar, long title | 45px inside: "Q4 Ent…" | whole title beside the bar | | Gantt month, 112px bar, "Trade Show Booth" | inside, whole | inside, whole (unchanged) | | Task-list title cell, long title | `text-overflow: ellipsis`, `scrollWidth` 356 vs `clientWidth` 91 | unchanged | ## Acceptance notes - **The task-list title already ellipsizes (premise not reproduced).** At 390px the title cell measured `text-overflow: ellipsis` with `scrollWidth` 356 over `clientWidth` 91, and it ellipsized latin, CJK and unbroken titles in week and month view. A phone opens the gantt with the task list collapsed (`width: 0`), so the bar was the only place a title showed; the "cut with no ellipsis" title in the report matches the bar label (0px of room in an 18px bar). The title cell is not changed. A test pins the two classes the reading rests on. - Milestones (diamonds) draw no title before or after; not changed. The PNG/PDF export's `fitText` labels are not changed either. - A beside title can sit under a bar's end connector dot while that dot shows on hover. The label is `pointer-events-none`, so the dot stays grabbable. - The kanban's mobile dot indicator still estimates the active column as `scrollLeft / clientWidth`; it follows the new scroll position like any other scroll. - Out of scope per the order: a gantt column-title config key (#27386 row 2). ## Verification Head `0db321c7`. - New tests: `phoneOpensOnFirstFilledColumn-27418.test.tsx` (6) and `GanttView.barLabelPlacement-27418.test.tsx` (5), all pass. - Ablations (each through `scripts/ablation-replace.mjs`, restored to the HEAD blob after): - Delete the kanban `scrollLeft` move: 3 tests fail (opens on third stage, waits then moves, never moves again). - Make the kanban `viewerMoved` handler a no-op: 1 test fails (never overrides a viewer who scrolled first). - Make `barLabelPlacement` always return `'inside'`: 2 tests fail (narrow leaf bar, narrow summary bar). - `pnpm --dir ui exec vitest run packages/plugin-kanban/ packages/plugin-gantt/` under the lock: 186 files passed, 1 skipped; 1320 tests passed, 114 skipped. - `pnpm --filter @object-ui/plugin-kanban run type-check` and `@object-ui/plugin-gantt`: exit 0. eslint on the four changed files: 0 errors. `pnpm --dir ui run check:new-line-citations`: 0 new. <details><summary>Derived gates (dispatch-gates --commands, 35)</summary> Taken on head `0db321c7`; every command's exit was captured before any pipe. ```text node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-sdui-manifest.mjs :: exit 0 node scripts/check-sdui-manifest.mjs --self-test :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 node scripts/release-pending-publish.mjs --self-test :: exit 0 pnpm --dir ui exec vitest run scripts/ :: exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:dispatcher-error-vocabulary :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:error-status-conformance :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:pm-changeset-deadline-census :: exit 0 pnpm check:published-files :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:skill-identifier-liveness :: exit 0 pnpm check:test-source-alias :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 ``` `dispatch-gates --ran`: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes (touched files linted; repo-wide lint is CI's). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset (`@object-ui/plugin-kanban` and `@object-ui/plugin-gantt` patch). - [x] 6. `packages/spec` touched: n/a. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 block is the dispatch order's required Chinese summary). - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JQwxbZnXDgvp4mp2dYYyJg --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbZnXDgvp4mp2dYYyJg)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9229738 commit 7af2145

5 files changed

Lines changed: 387 additions & 7 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/plugin-kanban': patch
3+
'@object-ui/plugin-gantt': patch
4+
---
5+
6+
fix(plugin-kanban, plugin-gantt): a phone kanban opens on its first column that holds a card, and a narrow gantt bar shows its title beside the bar
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
- **Kanban.** At phone width the board shows one column at a time, so a pipeline whose first stages were empty opened on an empty column. On its first settled load the board now brings the first column that holds a card into view. The column order does not change, a board whose first column holds a card opens as before, and a column already in view is never scrolled to. The board never moves after the viewer touches, swipes, scrolls or types on it. Swimlane boards are unchanged.
12+
- **Gantt.** A bar too narrow to show its title (a week or month bar on a phone, 18 to 63px wide) drew an ellipsis or nothing. It now draws the whole title beside its end. A bar with room keeps its title inside, cut with an ellipsis as before. This applies to task bars and summary bars; milestones and the PNG/PDF export are unchanged.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/**
2+
* #27418 — at phone width a gantt bar carries its title.
3+
*
4+
* Measured in Chromium at 390x844 before the fix: a month-view bar is 18px
5+
* wide, its in-bar title got 0px of room after the bar's `px-2`, and drew
6+
* nothing — not even an ellipsis; a 31–63px week bar showed an ellipsis and a
7+
* letter or two. A phone opens with the task list collapsed, so the bar was
8+
* the only place the title could appear. Now a bar too narrow for its title
9+
* draws it beside its end, whole; a bar with room keeps the in-bar label.
10+
*
11+
* jsdom does no layout, but the bar's width is an inline style computed from
12+
* the view's column width (`window.innerWidth` stands in for the container),
13+
* so placement is readable here without a browser.
14+
*/
15+
import React from 'react';
16+
import { render, cleanup } from '@testing-library/react';
17+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
18+
import { GanttView, type GanttTask } from './GanttView';
19+
20+
const LONG_TITLE = 'Q4 Enterprise Product Launch Webinar Series — APAC Region';
21+
22+
function setWidth(width: number) {
23+
Object.defineProperty(window, 'innerWidth', { value: width, configurable: true });
24+
}
25+
26+
beforeEach(() => window.localStorage.clear());
27+
afterEach(cleanup);
28+
29+
function makeTask(id: string, title: string, start: string, end: string, extra: Partial<GanttTask> = {}): GanttTask {
30+
return { id, title, start: new Date(start), end: new Date(end), progress: 0, ...extra };
31+
}
32+
33+
function renderView(tasks: GanttTask[], viewMode: 'day' | 'week' | 'month') {
34+
return render(
35+
<div style={{ width: window.innerWidth, height: 600 }}>
36+
<GanttView
37+
tasks={tasks}
38+
viewMode={viewMode}
39+
startDate={new Date('2026-10-01T00:00:00.000Z')}
40+
endDate={new Date('2026-12-31T00:00:00.000Z')}
41+
/>
42+
</div>,
43+
);
44+
}
45+
46+
function barWidth(container: HTMLElement, id: string) {
47+
const bar = container.querySelector<HTMLElement>(`[data-testid="gantt-task-bar-${id}"], [data-testid="gantt-summary-bar-${id}"]`);
48+
return parseFloat(bar?.style.width ?? 'NaN');
49+
}
50+
51+
function label(container: HTMLElement, id: string) {
52+
const el = container.querySelector<HTMLElement>(`[data-testid="gantt-bar-label-${id}"]`);
53+
expect(el, `bar ${id} draws a label`).not.toBeNull();
54+
return el!;
55+
}
56+
57+
describe('a gantt bar carries its title at phone width (#27418)', () => {
58+
it('draws a narrow bar’s title beside it, whole', () => {
59+
setWidth(390);
60+
const { container } = renderView([makeTask('c1', LONG_TITLE, '2026-10-05T00:00:00', '2026-10-09T00:00:00')], 'month');
61+
// The fixture really is the measured narrow bar, not a wide one.
62+
expect(barWidth(container, 'c1')).toBeLessThan(40);
63+
const el = label(container, 'c1');
64+
expect(el.dataset.placement).toBe('beside');
65+
expect(el.textContent).toBe(LONG_TITLE);
66+
// Beside the bar it is never cut: no truncation, one line.
67+
expect(el.className).not.toMatch(/\btruncate\b/);
68+
expect(el.className).toMatch(/\bwhitespace-nowrap\b/);
69+
expect(el.className).toMatch(/\bleft-full\b/);
70+
});
71+
72+
it('keeps a short title inside a narrow bar it fits in', () => {
73+
setWidth(1280);
74+
const { container } = renderView([makeTask('s', 'QA', '2026-10-05T00:00:00', '2026-10-09T00:00:00')], 'week');
75+
expect(barWidth(container, 's')).toBeLessThan(16 + 96);
76+
expect(label(container, 's').dataset.placement).toBe('inside');
77+
});
78+
79+
it('keeps a long title inside a bar with room, ending in an ellipsis', () => {
80+
setWidth(1280);
81+
const { container } = renderView([makeTask('w', LONG_TITLE, '2026-10-05T00:00:00', '2026-10-09T00:00:00')], 'day');
82+
expect(barWidth(container, 'w')).toBeGreaterThanOrEqual(16 + 96);
83+
const el = label(container, 'w');
84+
expect(el.dataset.placement).toBe('inside');
85+
expect(el.className).toMatch(/\btruncate\b/);
86+
});
87+
88+
it('draws a narrow summary bar’s title beside it too', () => {
89+
setWidth(390);
90+
const { container } = renderView(
91+
[
92+
makeTask('p', LONG_TITLE, '2026-10-05T00:00:00', '2026-10-06T00:00:00'),
93+
makeTask('k', 'Child', '2026-10-05T00:00:00', '2026-10-07T00:00:00', { parent: 'p' }),
94+
],
95+
'month',
96+
);
97+
const el = label(container, 'p');
98+
expect(el.dataset.placement).toBe('beside');
99+
expect(el.textContent).toBe(LONG_TITLE);
100+
});
101+
102+
it('ellipsizes a long title in the task list (measured in Chromium, unchanged)', () => {
103+
// At 390px the title cell measured text-overflow: ellipsis with
104+
// scrollWidth 356 over clientWidth 91: the cell already ellipsizes. This
105+
// pins the two classes that reading rests on.
106+
setWidth(390);
107+
const { getByTestId } = renderView([makeTask('c1', LONG_TITLE, '2026-10-05T00:00:00', '2026-10-09T00:00:00')], 'month');
108+
const title = getByTestId('gantt-row-title-c1');
109+
expect(title.className).toMatch(/\btruncate\b/);
110+
expect(title.parentElement!.className).toMatch(/\bmin-w-0\b/);
111+
});
112+
});

‎ui/packages/plugin-gantt/src/GanttView.tsx‎

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,50 @@ function rowHeightForContainer(width: number) {
187187
return width < 640 ? 32 : 40;
188188
}
189189

190+
/** The horizontal padding a bar spends before its in-bar title (`px-2`). */
191+
const BAR_LABEL_PADDING_W = 16;
192+
193+
/**
194+
* The least in-bar room worth a truncated title: below it the title is moved
195+
* beside the bar instead. 96px holds about as much of a title as the phone
196+
* task list's title cell does, so a bar never shows less of its title than
197+
* the list beside it.
198+
*/
199+
const BAR_LABEL_MIN_INSIDE_W = 96;
200+
201+
/**
202+
* A title's width in the bar's 10px label font, estimated per character: a
203+
* wide (CJK) character at 10px, any other at 5.5px, which is what Chromium
204+
* measured for latin titles at that size. An estimate, not a measurement, so
205+
* it may only decide where the title goes, never cut the title itself.
206+
*/
207+
function estimateBarLabelWidth(title: string) {
208+
let width = 0;
209+
for (const ch of title) width += (ch.codePointAt(0) ?? 0) > 0x2e7f ? 10 : 5.5;
210+
return width;
211+
}
212+
213+
/**
214+
* Where a bar draws its title (#27418). Inside when the whole title fits, or
215+
* when the bar has room for a readable part of it (truncated with an ellipsis,
216+
* as before). Otherwise BESIDE the bar, after its end: at phone width a week
217+
* or month bar is 18 to 63px wide, and a title squeezed into that showed an
218+
* ellipsis or nothing at all — and with the task list collapsed, as it opens
219+
* on a phone, the bar is the only place the title appears.
220+
*/
221+
function barLabelPlacement(barWidth: number, title: string): 'inside' | 'beside' {
222+
const room = barWidth - BAR_LABEL_PADDING_W;
223+
return room >= BAR_LABEL_MIN_INSIDE_W || estimateBarLabelWidth(title) <= room ? 'inside' : 'beside';
224+
}
225+
226+
/**
227+
* The title drawn beside a bar too narrow to hold it: after the bar's end, on
228+
* the row's own background, never truncated (the row has the whole timeline
229+
* to its right). Shared by the leaf and summary bars.
230+
*/
231+
const BESIDE_BAR_LABEL_CLASS =
232+
"absolute left-full top-1/2 -translate-y-1/2 ml-1.5 whitespace-nowrap text-[10px] font-medium text-foreground pointer-events-none";
233+
190234
/**
191235
* Dependency link types, MS-Project style:
192236
* - `fs` finish-to-start (default): predecessor must finish before this task starts
@@ -4469,9 +4513,19 @@ export function GanttView({
44694513
/>
44704514
</>
44714515
)}
4472-
<span className="relative text-[10px] text-white font-medium truncate pointer-events-none">
4473-
{task.title}
4474-
</span>
4516+
{barLabelPlacement(liveStyle.width, task.title) === 'inside' ? (
4517+
<span
4518+
className="relative text-[10px] text-white font-medium truncate pointer-events-none"
4519+
data-testid={`gantt-bar-label-${task.id}`}
4520+
data-placement="inside"
4521+
>
4522+
{task.title}
4523+
</span>
4524+
) : (
4525+
<span className={BESIDE_BAR_LABEL_CLASS} data-testid={`gantt-bar-label-${task.id}`} data-placement="beside">
4526+
{task.title}
4527+
</span>
4528+
)}
44754529
{/* Connector dots — same drag-to-link affordance as leaf
44764530
bars, so dependencies (3.4.5) can be drawn between
44774531
summary plans, not only via the right-click menu. */}
@@ -4843,10 +4897,22 @@ export function GanttView({
48434897

48444898
{/* Bar label — the task title, shown like summary bars so
48454899
leaf bars aren't blank. Fades out on hover to reveal the
4846-
progress / drag overlay below. */}
4847-
<span className="relative text-[10px] text-white font-medium truncate pointer-events-none group-hover:opacity-0 transition-opacity">
4848-
{task.title}
4849-
</span>
4900+
progress / drag overlay below. A bar too narrow for it
4901+
draws it beside its end instead (#27418), where the
4902+
overlay does not reach, so that one stays put. */}
4903+
{barLabelPlacement(liveStyle.width, task.title) === 'inside' ? (
4904+
<span
4905+
className="relative text-[10px] text-white font-medium truncate pointer-events-none group-hover:opacity-0 transition-opacity"
4906+
data-testid={`gantt-bar-label-${task.id}`}
4907+
data-placement="inside"
4908+
>
4909+
{task.title}
4910+
</span>
4911+
) : (
4912+
<span className={BESIDE_BAR_LABEL_CLASS} data-testid={`gantt-bar-label-${task.id}`} data-placement="beside">
4913+
{task.title}
4914+
</span>
4915+
)}
48504916
{/* Hover Details / drag tooltip — overlays the title so the
48514917
bar's text width never shifts on hover. */}
48524918
<span className="absolute inset-0 flex items-center px-2 text-[10px] text-white font-medium truncate opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">

‎ui/packages/plugin-kanban/src/KanbanImpl.tsx‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,6 +1354,51 @@ function KanbanBoardInner({ columns, onCardMove, onCardClick, className, dnd, qu
13541354
return () => el.removeEventListener('scroll', handle)
13551355
}, [boardColumns.length])
13561356

1357+
/**
1358+
* Open the board on its first column that holds a card (#27418).
1359+
*
1360+
* A phone shows one column at a time, so a pipeline whose first stages are
1361+
* empty opened on an empty column and read as an empty board. On the FIRST
1362+
* load only, the flat scroller brings the first non-empty column into view.
1363+
* The column order is untouched, and a column already in view (the first
1364+
* column holds cards, or a wide board shows several) is never scrolled to.
1365+
*
1366+
* ⛔ It never moves the board after the viewer did: any pointer, touch,
1367+
* wheel or key input on the scroller ends it, and so does the first settled
1368+
* load that holds a card, whether or not it scrolled. A board that settles
1369+
* empty waits for its first card, unless the viewer moved first.
1370+
*
1371+
* It waits for `recordsSettled` (a column's cards are only a fact once the
1372+
* rows have arrived) and for `boardWidth` (the columns take their final width
1373+
* from it, so an offset read before it would land on the wrong column).
1374+
*/
1375+
const initialColumnShownRef = React.useRef(false)
1376+
React.useEffect(() => {
1377+
const el = flatScrollRef.current
1378+
if (!el) return
1379+
const viewerMoved = () => { initialColumnShownRef.current = true }
1380+
const events = ['pointerdown', 'touchstart', 'wheel', 'keydown'] as const
1381+
for (const name of events) el.addEventListener(name, viewerMoved, { passive: true })
1382+
return () => { for (const name of events) el.removeEventListener(name, viewerMoved) }
1383+
}, [swimlaneField])
1384+
React.useLayoutEffect(() => {
1385+
if (initialColumnShownRef.current || !recordsSettled || !boardWidth) return
1386+
const el = flatScrollRef.current
1387+
if (!el) return
1388+
const firstFilled = boardColumns.findIndex((col) => col.cards.length > 0)
1389+
if (firstFilled < 0) return
1390+
initialColumnShownRef.current = true
1391+
const first = el.children[0]
1392+
const target = el.children[firstFilled]
1393+
if (firstFilled === 0 || !first || !target) return
1394+
const box = el.getBoundingClientRect()
1395+
const column = target.getBoundingClientRect()
1396+
if (column.left >= box.left && column.right <= box.right) return
1397+
// Move by the distance between the two columns, from wherever the snap
1398+
// rests the first one (Chromium rests a phone board at `scrollLeft: 16`).
1399+
el.scrollLeft += column.left - first.getBoundingClientRect().left
1400+
}, [recordsSettled, boardWidth, boardColumns])
1401+
13571402
/**
13581403
* The swimlane board's single horizontal scroll position (objectui#8448).
13591404
*

0 commit comments

Comments
 (0)