lvwerra HF Staff Claude Opus 5 commited on
Commit
2c03eaf
·
1 Parent(s): 7ef4ebf

Sit a task checkbox on its own line, at any type size

Browse files

The 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>

Files changed (2) hide show
  1. client/app.css +16 -1
  2. test/browser.js +40 -1
client/app.css CHANGED
@@ -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
- .tiptap ul[data-type='taskList'] li > label { margin-top: 2px; flex: 0 0 auto; }
 
 
 
 
 
 
 
 
 
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 --- */
test/browser.js CHANGED
@@ -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 > *')]