Spaces:
Running
Sit a task checkbox on its own line, at any type size
Browse filesThe box was aligned with `margin-top: 2px` on the label. That number can only be
right at one size: the line it has to meet is 1em * --doc-line, and both halves
move — --doc-line is 1.15 in Docs style and 1.8 in Reading, and the font size
carries --doc-text-scale and --doc-zoom on top. 2px was tuned at Reading 1x, so:
docs 1.15 1.0x box 4.5px low
reading 1.8 1.0x box 0.5px low <- the one it was picked in
reading 1.8 1.6x box 3.5px low
docs 1.15 1.6x box 2.5px low
Same cause, second symptom: an inline label makes the line box taller than the
text needs, so every task row was 23.9px against 16.9px for a bullet row holding
the same words — the checkbox pushed down inside it and task lists out of step
with every other list. Making the label exactly one line tall and centring the
box inside it fixes both: a task row is now 16.9px, the same as a bullet row.
The insertion panel drew the same list from its own markup and drifted the other
way (+2.5px at 1x, -2.5px at 1.6x, so no constant fixes both). Its box carries
the offset itself, derived the same way, since that markup has no label.
Browser test measures the gap between the box's centre and the centre of the
first line of its label, over both doc styles at 1x and 1.6x, in the editor and
in the panel. Reverting the CSS fails it: "panel checkbox sits on its line: off
by [2.5]".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- client/app.css +16 -1
- test/browser.js +40 -1
|
@@ -1202,13 +1202,28 @@ body.suggesting .tiptap { caret-color: transparent; cursor: default; }
|
|
| 1202 |
/* --- task lists --- */
|
| 1203 |
.tiptap ul[data-type='taskList'] { list-style: none; padding-left: 4px; margin: 0.4em 0; }
|
| 1204 |
.tiptap ul[data-type='taskList'] li { display: flex; align-items: flex-start; gap: 8px; margin: 3px 0; }
|
| 1205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1206 |
.tiptap ul[data-type='taskList'] li > div { flex: 1; }
|
| 1207 |
.tiptap ul[data-type='taskList'] input[type='checkbox'] { width: 15px; height: 15px; accent-color: var(--accent); cursor: pointer; }
|
| 1208 |
.tiptap ul[data-type='taskList'] li[data-checked='true'] > div { color: var(--text-3); text-decoration: line-through; }
|
| 1209 |
/* task lists rendered in suggestion insertion panels */
|
| 1210 |
.sugg-ins-block ul[data-type='taskList'] { list-style: none; padding-left: 0; }
|
| 1211 |
.sugg-ins-block .task-item { display: flex; align-items: flex-start; gap: 8px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1212 |
.sugg-ins-block .task-item.checked > span { color: var(--text-3); text-decoration: line-through; }
|
| 1213 |
|
| 1214 |
/* --- tables --- */
|
|
|
|
| 1202 |
/* --- task lists --- */
|
| 1203 |
.tiptap ul[data-type='taskList'] { list-style: none; padding-left: 4px; margin: 0.4em 0; }
|
| 1204 |
.tiptap ul[data-type='taskList'] li { display: flex; align-items: flex-start; gap: 8px; margin: 3px 0; }
|
| 1205 |
+
/* Centre the box on the first line of its own label, whatever that line is.
|
| 1206 |
+
A fixed nudge cannot: the line box is 1em * --doc-line, and both halves move
|
| 1207 |
+
— --doc-line is 1.15 in Docs style and 1.8 in Reading, and the font size
|
| 1208 |
+
carries --doc-text-scale and --doc-zoom on top. 2px happened to be right at
|
| 1209 |
+
Reading 1x and was up to 4.5px out everywhere else. Making the label exactly
|
| 1210 |
+
one line tall and centring inside it is correct at every size, and keeps the
|
| 1211 |
+
box on the FIRST line when the label wraps. */
|
| 1212 |
+
.tiptap ul[data-type='taskList'] li > label {
|
| 1213 |
+
display: flex; align-items: center; height: calc(1em * var(--doc-line)); flex: 0 0 auto;
|
| 1214 |
+
}
|
| 1215 |
.tiptap ul[data-type='taskList'] li > div { flex: 1; }
|
| 1216 |
.tiptap ul[data-type='taskList'] input[type='checkbox'] { width: 15px; height: 15px; accent-color: var(--accent); cursor: pointer; }
|
| 1217 |
.tiptap ul[data-type='taskList'] li[data-checked='true'] > div { color: var(--text-3); text-decoration: line-through; }
|
| 1218 |
/* task lists rendered in suggestion insertion panels */
|
| 1219 |
.sugg-ins-block ul[data-type='taskList'] { list-style: none; padding-left: 0; }
|
| 1220 |
.sugg-ins-block .task-item { display: flex; align-items: flex-start; gap: 8px; }
|
| 1221 |
+
/* Same first-line centring as the editor's task list. This markup has no label
|
| 1222 |
+
to make one line tall, so the box carries the offset itself — derived, not a
|
| 1223 |
+
nudge: it drifted +2.5px at 1x and -2.5px at 1.6x, so no constant fixes both. */
|
| 1224 |
+
.sugg-ins-block .task-item > input[type='checkbox'] {
|
| 1225 |
+
flex: 0 0 auto; margin: calc((1em * var(--doc-line) - 15px) / 2) 0 0;
|
| 1226 |
+
}
|
| 1227 |
.sugg-ins-block .task-item.checked > span { color: var(--text-3); text-decoration: line-through; }
|
| 1228 |
|
| 1229 |
/* --- tables --- */
|
|
@@ -636,6 +636,21 @@ async function main() {
|
|
| 636 |
}, listSugg.suggestion_id)
|
| 637 |
console.log('✓ list suggestion: only the added item renders as inserted')
|
| 638 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 639 |
// --- new blocks (heading + list) render FORMATTED in the insertion panel ---
|
| 640 |
const blockSugg = await page.evaluate(async () => {
|
| 641 |
const id = location.pathname.split('/').pop()
|
|
@@ -646,7 +661,7 @@ async function main() {
|
|
| 646 |
headers: { 'content-type': 'application/json' },
|
| 647 |
body: JSON.stringify({
|
| 648 |
block_index: li,
|
| 649 |
-
replacement_markdown: snap.blocks[li].markdown + '\n\n## Sources\n\n- one **bold** source',
|
| 650 |
}),
|
| 651 |
})).json())
|
| 652 |
})
|
|
@@ -654,6 +669,9 @@ async function main() {
|
|
| 654 |
await page.waitForSelector('.tiptap .sugg-ins-block', { timeout: 5000 })
|
| 655 |
assert.ok(await page.$('.tiptap .sugg-ins-block h2'), 'inserted heading renders as a heading')
|
| 656 |
assert.ok(await page.$('.tiptap .sugg-ins-block ul li strong'), 'inserted list renders as bullets with marks')
|
|
|
|
|
|
|
|
|
|
| 657 |
await page.evaluate(async sid => {
|
| 658 |
const id = location.pathname.split('/').pop()
|
| 659 |
await fetch(`/api/docs/${id}/suggestions/${sid}/reject`, { method: 'POST' })
|
|
@@ -1449,6 +1467,27 @@ async function main() {
|
|
| 1449 |
await waitFor(async () => !!(await page.$('.tiptap iframe.html-embed')), 'seeded HTML embed renders in the editor')
|
| 1450 |
console.log('✓ task lists + tables + HTML embed render in the editor')
|
| 1451 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1452 |
// --- table columns resize by dragging a cell border, and the width sticks ---
|
| 1453 |
const colWidths = () => page.evaluate(() =>
|
| 1454 |
[...document.querySelectorAll('.tiptap table:first-of-type tr:first-child > *')]
|
|
|
|
| 636 |
}, listSugg.suggestion_id)
|
| 637 |
console.log('✓ list suggestion: only the added item renders as inserted')
|
| 638 |
|
| 639 |
+
// Distance from a checkbox's centre to the centre of the FIRST line of its own
|
| 640 |
+
// label. A fixed margin-top cannot hold this: the line box is 1em * --doc-line,
|
| 641 |
+
// and both the line height (1.15 in Docs style, 1.8 in Reading) and the font
|
| 642 |
+
// size (--doc-text-scale, --doc-zoom) move underneath it.
|
| 643 |
+
const checkboxDrift = (rowSel, labelSel) => page.evaluate(([rows, label]) =>
|
| 644 |
+
[...document.querySelectorAll(rows)].map(li => {
|
| 645 |
+
const box = li.querySelector('input[type="checkbox"]').getBoundingClientRect()
|
| 646 |
+
const node = document.createTreeWalker(li.querySelector(label), NodeFilter.SHOW_TEXT).nextNode()
|
| 647 |
+
const r = document.createRange()
|
| 648 |
+
r.setStart(node, 0)
|
| 649 |
+
r.setEnd(node, Math.min(4, node.length))
|
| 650 |
+
const line = r.getClientRects()[0]
|
| 651 |
+
return Math.abs((box.top + box.height / 2) - (line.top + line.height / 2))
|
| 652 |
+
}), [rowSel, labelSel])
|
| 653 |
+
|
| 654 |
// --- new blocks (heading + list) render FORMATTED in the insertion panel ---
|
| 655 |
const blockSugg = await page.evaluate(async () => {
|
| 656 |
const id = location.pathname.split('/').pop()
|
|
|
|
| 661 |
headers: { 'content-type': 'application/json' },
|
| 662 |
body: JSON.stringify({
|
| 663 |
block_index: li,
|
| 664 |
+
replacement_markdown: snap.blocks[li].markdown + '\n\n## Sources\n\n- one **bold** source\n\n- [ ] and a task in the panel',
|
| 665 |
}),
|
| 666 |
})).json())
|
| 667 |
})
|
|
|
|
| 669 |
await page.waitForSelector('.tiptap .sugg-ins-block', { timeout: 5000 })
|
| 670 |
assert.ok(await page.$('.tiptap .sugg-ins-block h2'), 'inserted heading renders as a heading')
|
| 671 |
assert.ok(await page.$('.tiptap .sugg-ins-block ul li strong'), 'inserted list renders as bullets with marks')
|
| 672 |
+
const panelDrift = await checkboxDrift('.sugg-ins-block .task-item', 'span')
|
| 673 |
+
assert.ok(panelDrift.length, 'panel renders a task item')
|
| 674 |
+
assert.ok(Math.max(...panelDrift) <= 1, 'panel checkbox sits on its line: off by ' + JSON.stringify(panelDrift))
|
| 675 |
await page.evaluate(async sid => {
|
| 676 |
const id = location.pathname.split('/').pop()
|
| 677 |
await fetch(`/api/docs/${id}/suggestions/${sid}/reject`, { method: 'POST' })
|
|
|
|
| 1467 |
await waitFor(async () => !!(await page.$('.tiptap iframe.html-embed')), 'seeded HTML embed renders in the editor')
|
| 1468 |
console.log('✓ task lists + tables + HTML embed render in the editor')
|
| 1469 |
|
| 1470 |
+
// 2px was tuned at Reading 1x and was 4.5px out in Docs style, so measure the
|
| 1471 |
+
// combinations rather than the one the number was picked in
|
| 1472 |
+
const priorStyle = await page.evaluate(() => document.documentElement.getAttribute('data-doc-style'))
|
| 1473 |
+
const priorScale = await page.evaluate(() => document.documentElement.style.getPropertyValue('--doc-text-scale'))
|
| 1474 |
+
for (const [style, scale] of [['docs', '1'], ['reading', '1'], ['reading', '1.6'], ['docs', '1.6']]) {
|
| 1475 |
+
await page.evaluate(([s_, z]) => {
|
| 1476 |
+
document.documentElement.dataset.docStyle = s_
|
| 1477 |
+
document.documentElement.style.setProperty('--doc-text-scale', z)
|
| 1478 |
+
}, [style, scale])
|
| 1479 |
+
const drift = await checkboxDrift('.tiptap ul[data-type="taskList"] li:not(.task-item)', 'div')
|
| 1480 |
+
assert.ok(drift.length, `task rows to measure in ${style} at ${scale}x`)
|
| 1481 |
+
assert.ok(Math.max(...drift) <= 1, `checkbox centred in ${style} at ${scale}x: off by ${JSON.stringify(drift)}`)
|
| 1482 |
+
}
|
| 1483 |
+
await page.evaluate(([s_, z]) => {
|
| 1484 |
+
if (s_ === null) delete document.documentElement.dataset.docStyle
|
| 1485 |
+
else document.documentElement.dataset.docStyle = s_
|
| 1486 |
+
if (z) document.documentElement.style.setProperty('--doc-text-scale', z)
|
| 1487 |
+
else document.documentElement.style.removeProperty('--doc-text-scale')
|
| 1488 |
+
}, [priorStyle, priorScale])
|
| 1489 |
+
console.log('✓ task checkboxes stay on the first line across doc styles and text sizes')
|
| 1490 |
+
|
| 1491 |
// --- table columns resize by dragging a cell border, and the width sticks ---
|
| 1492 |
const colWidths = () => page.evaluate(() =>
|
| 1493 |
[...document.querySelectorAll('.tiptap table:first-of-type tr:first-child > *')]
|