Skip to content

Commit 3e5302f

Browse files
committed
fix: stabilize tasks wrap+expand layout
1 parent 55603c4 commit 3e5302f

2 files changed

Lines changed: 166 additions & 65 deletions

File tree

client/app.js

Lines changed: 157 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -20446,11 +20446,11 @@ class ClaudeOrchestrator {
2044620446
<div class="task-card-meta">${escapeHtml(meta)}</div>
2044720447
<div class="task-card-top-right">${quickLaunchHtml}</div>
2044820448
</div>
20449-
<div class="task-card-title">${title}</div>
20450-
</div>
20451-
`;
20452-
})
20453-
.join('')}
20449+
<div class="task-card-title" title="${escapeHtml(String(c?.name || ''))}">${title}</div>
20450+
</div>
20451+
`;
20452+
})
20453+
.join('')}
2045420454
</div>
2045520455
</div>
2045620456
`;
@@ -20482,41 +20482,91 @@ class ClaudeOrchestrator {
2048220482
return;
2048320483
}
2048420484

20485-
const containerHeight = cardsContainer.clientHeight;
20486-
if (!containerHeight || containerHeight < 40) return;
20485+
const containerHeight = cardsContainer.clientHeight;
20486+
if (!containerHeight || containerHeight < 40) {
20487+
col.style.setProperty('--tasks-card-columns', '1');
20488+
col.style.setProperty('--tasks-card-rows', '1');
20489+
col.style.width = '';
20490+
col.style.minWidth = '';
20491+
20492+
const tries = Number(col.dataset.tasksWrapExpandRetry || '0') || 0;
20493+
if (tries < 4) {
20494+
col.dataset.tasksWrapExpandRetry = String(tries + 1);
20495+
window.requestAnimationFrame(() => computeForColumn(col));
20496+
} else {
20497+
delete col.dataset.tasksWrapExpandRetry;
20498+
}
20499+
return;
20500+
}
20501+
delete col.dataset.tasksWrapExpandRetry;
20502+
20503+
const styles = window.getComputedStyle(cardsContainer);
20504+
const rowGap = Number.parseFloat(styles.rowGap || styles.gap || '0') || 0;
20505+
const columnGap = Number.parseFloat(styles.columnGap || styles.gap || '0') || 0;
20506+
const padLeft = Number.parseFloat(styles.paddingLeft || '0') || 0;
20507+
const padRight = Number.parseFloat(styles.paddingRight || '0') || 0;
20508+
20509+
const measureSampleHeights = () => {
20510+
const maxSamples = Math.min(24, cardCount);
20511+
if (maxSamples <= 0) return [];
20512+
const step = Math.max(1, Math.floor(cardCount / maxSamples));
20513+
const heights = [];
20514+
for (let i = 0; i < cardCount && heights.length < maxSamples; i += step) {
20515+
const h = cards[i]?.getBoundingClientRect?.().height;
20516+
if (h) heights.push(h);
20517+
}
20518+
return heights;
20519+
};
2048720520

20488-
const styles = window.getComputedStyle(cardsContainer);
20489-
const rowGap = Number.parseFloat(styles.rowGap || styles.gap || '0') || 0;
20490-
const sample = cards.slice(0, Math.min(6, cardCount));
20491-
const heights = sample.map(el => el.getBoundingClientRect().height).filter(Boolean);
20492-
const avg = heights.length ? (heights.reduce((a, b) => a + b, 0) / heights.length) : 80;
20493-
const denom = Math.max(1, avg + rowGap);
20494-
let rowsFit = Math.max(1, Math.floor((containerHeight + rowGap) / denom));
20495-
rowsFit = Math.min(rowsFit, 12);
20521+
const heights = measureSampleHeights().sort((a, b) => a - b);
20522+
const median = heights.length ? heights[Math.floor(heights.length / 2)] : 80;
20523+
const denom = Math.max(1, median + rowGap);
20524+
let rowsFit = Math.max(1, Math.floor((containerHeight + rowGap) / denom));
20525+
rowsFit = Math.min(rowsFit, cardCount);
2049620526

2049720527
const apply = (rows) => {
2049820528
const r = Math.max(1, Number(rows) || 1);
2049920529
const cols = Math.max(1, Math.ceil(cardCount / r));
2050020530
col.style.setProperty('--tasks-card-rows', String(r));
2050120531
col.style.setProperty('--tasks-card-columns', String(cols));
20502-
if (cols <= 1) {
20503-
col.style.width = '';
20504-
col.style.minWidth = '';
20505-
} else {
20506-
const target = Math.max(baseWidth, baseWidth * cols);
20507-
col.style.width = `${Math.round(target)}px`;
20508-
col.style.minWidth = `${Math.round(target)}px`;
20509-
}
20510-
};
20532+
if (cols <= 1) {
20533+
col.style.width = '';
20534+
col.style.minWidth = '';
20535+
} else {
20536+
// Match CSS `minmax(180px, 1fr)` for card columns; expand only as much as needed.
20537+
const minCardWidth = 180;
20538+
const cardsWidth = (cols * minCardWidth) + Math.max(0, cols - 1) * columnGap;
20539+
const target = Math.max(baseWidth, cardsWidth + padLeft + padRight);
20540+
col.style.width = `${Math.round(target)}px`;
20541+
col.style.minWidth = `${Math.round(target)}px`;
20542+
}
20543+
};
2051120544

20512-
apply(rowsFit);
20513-
for (let attempt = 0; attempt < 6; attempt++) {
20514-
void cardsContainer.offsetHeight;
20515-
if (cardsContainer.scrollHeight <= cardsContainer.clientHeight + 1) break;
20516-
rowsFit = Math.max(1, rowsFit - 1);
20517-
apply(rowsFit);
20518-
}
20519-
};
20545+
apply(rowsFit);
20546+
const fits = () => (cardsContainer.scrollHeight <= cardsContainer.clientHeight + 1);
20547+
20548+
// If we overflow vertically, reduce rows (creating more columns) until we fit.
20549+
for (let attempt = 0; attempt < 24; attempt++) {
20550+
void cardsContainer.offsetHeight;
20551+
if (fits()) break;
20552+
rowsFit = Math.max(1, rowsFit - 1);
20553+
apply(rowsFit);
20554+
}
20555+
20556+
// If we fit, try to maximize rows (minimize columns) by filling vertically first.
20557+
for (let attempt = 0; attempt < 24; attempt++) {
20558+
if (rowsFit >= cardCount) break;
20559+
const next = rowsFit + 1;
20560+
apply(next);
20561+
void cardsContainer.offsetHeight;
20562+
if (fits()) {
20563+
rowsFit = next;
20564+
continue;
20565+
}
20566+
apply(rowsFit);
20567+
break;
20568+
}
20569+
};
2052020570

2052120571
window.requestAnimationFrame(() => {
2052220572
columns.forEach(computeForColumn);
@@ -20929,11 +20979,11 @@ class ClaudeOrchestrator {
2092920979
</div>
2093020980
</div>
2093120981
</div>
20932-
<div class="task-card-title">${title}</div>
20933-
<div class="task-card-meta">${due ? `<span class="task-card-due" title="Due">${escapeHtml(due)}</span> • ` : ''}${last}</div>
20934-
</div>
20935-
`;
20936-
}).join('')}
20982+
<div class="task-card-title" title="${escapeHtml(String(c?.name || ''))}">${title}</div>
20983+
<div class="task-card-meta">${due ? `<span class="task-card-due" title="Due">${escapeHtml(due)}</span> • ` : ''}${last}</div>
20984+
</div>
20985+
`;
20986+
}).join('')}
2093720987
</div>
2093820988
</div>
2093920989
`;
@@ -20978,24 +21028,51 @@ class ClaudeOrchestrator {
2097821028
col.style.setProperty('--tasks-card-columns', '1');
2097921029
col.style.setProperty('--tasks-card-rows', '1');
2098021030
return;
20981-
}
21031+
}
21032+
21033+
const containerHeight = cardsContainer.clientHeight;
21034+
if (!containerHeight || containerHeight < 40) {
21035+
col.style.setProperty('--tasks-card-columns', '1');
21036+
col.style.setProperty('--tasks-card-rows', '1');
21037+
col.style.width = '';
21038+
col.style.minWidth = '';
21039+
21040+
const tries = Number(col.dataset.tasksWrapExpandRetry || '0') || 0;
21041+
if (tries < 4) {
21042+
col.dataset.tasksWrapExpandRetry = String(tries + 1);
21043+
window.requestAnimationFrame(() => computeForColumn(col));
21044+
} else {
21045+
delete col.dataset.tasksWrapExpandRetry;
21046+
}
21047+
return;
21048+
}
21049+
delete col.dataset.tasksWrapExpandRetry;
21050+
21051+
const styles = window.getComputedStyle(cardsContainer);
21052+
const rowGap = Number.parseFloat(styles.rowGap || styles.gap || '0') || 0;
21053+
const columnGap = Number.parseFloat(styles.columnGap || styles.gap || '0') || 0;
21054+
const padLeft = Number.parseFloat(styles.paddingLeft || '0') || 0;
21055+
const padRight = Number.parseFloat(styles.paddingRight || '0') || 0;
21056+
21057+
const measureSampleHeights = () => {
21058+
const maxSamples = Math.min(24, cardCount);
21059+
if (maxSamples <= 0) return [];
21060+
const step = Math.max(1, Math.floor(cardCount / maxSamples));
21061+
const heights = [];
21062+
for (let i = 0; i < cardCount && heights.length < maxSamples; i += step) {
21063+
const h = cards[i]?.getBoundingClientRect?.().height;
21064+
if (h) heights.push(h);
21065+
}
21066+
return heights;
21067+
};
2098221068

20983-
const containerHeight = cardsContainer.clientHeight;
20984-
if (!containerHeight || containerHeight < 40) return;
20985-
20986-
const styles = window.getComputedStyle(cardsContainer);
20987-
const rowGap = Number.parseFloat(styles.rowGap || styles.gap || '0') || 0;
20988-
const columnGap = Number.parseFloat(styles.columnGap || styles.gap || '0') || 0;
20989-
const padLeft = Number.parseFloat(styles.paddingLeft || '0') || 0;
20990-
const padRight = Number.parseFloat(styles.paddingRight || '0') || 0;
20991-
const sample = cards.slice(0, Math.min(6, cardCount));
20992-
const heights = sample.map(el => el.getBoundingClientRect().height).filter(Boolean);
20993-
const avg = heights.length ? (heights.reduce((a, b) => a + b, 0) / heights.length) : 80;
20994-
const denom = Math.max(1, avg + rowGap);
20995-
let rowsFit = Math.max(1, Math.floor((containerHeight + rowGap) / denom));
20996-
// In wrap+expand mode, prefer minimizing the number of columns by filling vertically first
20997-
// (as long as we still avoid vertical scrolling).
20998-
rowsFit = Math.min(rowsFit, cardCount);
21069+
const heights = measureSampleHeights().sort((a, b) => a - b);
21070+
const median = heights.length ? heights[Math.floor(heights.length / 2)] : 80;
21071+
const denom = Math.max(1, median + rowGap);
21072+
let rowsFit = Math.max(1, Math.floor((containerHeight + rowGap) / denom));
21073+
// In wrap+expand mode, prefer minimizing the number of columns by filling vertically first
21074+
// (as long as we still avoid vertical scrolling).
21075+
rowsFit = Math.min(rowsFit, cardCount);
2099921076

2100021077
const apply = (rows) => {
2100121078
const r = Math.max(1, Number(rows) || 1);
@@ -21015,18 +21092,33 @@ class ClaudeOrchestrator {
2101521092
col.style.minWidth = `${Math.round(target)}px`;
2101621093
}
2101721094
};
21095+
21096+
apply(rowsFit);
21097+
const fits = () => (cardsContainer.scrollHeight <= cardsContainer.clientHeight + 1);
21098+
21099+
// If we still overflow vertically, reduce rows (creating more columns) until we fit.
21100+
for (let attempt = 0; attempt < 24; attempt++) {
21101+
// Force reflow and then check overflow.
21102+
void cardsContainer.offsetHeight;
21103+
if (fits()) break;
21104+
rowsFit = Math.max(1, rowsFit - 1);
21105+
apply(rowsFit);
21106+
}
2101821107

21019-
apply(rowsFit);
21020-
21021-
// If we still overflow vertically, reduce rows (creating more columns) until we fit.
21022-
for (let attempt = 0; attempt < 24; attempt++) {
21023-
// Force reflow and then check overflow.
21024-
void cardsContainer.offsetHeight;
21025-
if (cardsContainer.scrollHeight <= cardsContainer.clientHeight + 1) break;
21026-
rowsFit = Math.max(1, rowsFit - 1);
21027-
apply(rowsFit);
21028-
}
21029-
};
21108+
// If we fit, try to maximize rows (minimize columns) by filling vertically first.
21109+
for (let attempt = 0; attempt < 24; attempt++) {
21110+
if (rowsFit >= cardCount) break;
21111+
const next = rowsFit + 1;
21112+
apply(next);
21113+
void cardsContainer.offsetHeight;
21114+
if (fits()) {
21115+
rowsFit = next;
21116+
continue;
21117+
}
21118+
apply(rowsFit);
21119+
break;
21120+
}
21121+
};
2103021122

2103121123
// Clear variables for collapsed columns to avoid stale widths.
2103221124
for (const col of columns) {

client/styles.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10509,6 +10509,15 @@ header h1 {
1050910509
min-height: 0;
1051010510
}
1051110511

10512+
.tasks-board.tasks-board-expand .task-card-title {
10513+
display: -webkit-box;
10514+
-webkit-line-clamp: 3;
10515+
-webkit-box-orient: vertical;
10516+
overflow: hidden;
10517+
overflow-wrap: anywhere;
10518+
word-break: break-word;
10519+
}
10520+
1051210521
.tasks-board.tasks-board-grid .task-card-board {
1051310522
height: fit-content;
1051410523
}

0 commit comments

Comments
 (0)