section ticking covers the section

This commit is contained in:
2026-08-27 15:43:00 -04:00
parent 6dcff467d7
commit 8e02235a71
4 changed files with 97 additions and 12 deletions

View File

@@ -12,7 +12,8 @@
Reading only: in the editor a tick would be an edit.
-->
<h2 v-if="block.type === 'heading'" :id="anchorFor(block)" class="gb-heading"
:class="{ 'gb-sectiondone': isTicked(block.id) }">
:class="['gb-h' + levelOf(block),
{ 'gb-sectiondone': isTicked(block.id) || inDoneSection(index) }]">
<input v-if="interactive" type="checkbox" class="gb-sectiontick"
:checked="isTicked(block.id)"
:aria-label="'Mark this section done'"
@@ -21,10 +22,11 @@
</h2>
<p v-else-if="block.type === 'paragraph'" class="gb-para"
:class="{ 'gb-sectiondone': inDoneSection(index) }"
v-html="inline(block.text)"></p>
<div v-else-if="block.type === 'step'" class="gb-step"
:class="{ 'gb-sectiondone': isTicked(block.id) }">
:class="{ 'gb-sectiondone': isTicked(block.id) || inDoneSection(index) }">
<label class="gb-stepnum">
<input v-if="interactive" type="checkbox" class="gb-sectiontick"
:checked="isTicked(block.id)"
@@ -41,7 +43,8 @@
editor a tick would be an edit, and an author clicking around their own
draft would quietly change what the next reader sees as "done".
-->
<ul v-else-if="block.type === 'checklist'" class="gb-checklist">
<ul v-else-if="block.type === 'checklist'" class="gb-checklist"
:class="{ 'gb-sectiondone': inDoneSection(index) }">
<li v-for="item in itemsOf(block)" :key="item.id" class="gb-check">
<label>
<input type="checkbox"
@@ -54,21 +57,25 @@
</li>
</ul>
<ul v-else-if="block.type === 'bullets'" class="gb-list">
<ul v-else-if="block.type === 'bullets'" class="gb-list"
:class="{ 'gb-sectiondone': inDoneSection(index) }">
<li v-for="item in itemsOf(block)" :key="item.id" v-html="inline(item.text)"></li>
</ul>
<ol v-else-if="block.type === 'numbers'" class="gb-list">
<ol v-else-if="block.type === 'numbers'" class="gb-list"
:class="{ 'gb-sectiondone': inDoneSection(index) }">
<li v-for="item in itemsOf(block)" :key="item.id" v-html="inline(item.text)"></li>
</ol>
<div v-else-if="block.type === 'callout'"
class="gb-callout" :class="'gb-' + (block.variant || 'info')">
<div v-else-if="block.type === 'callout'" class="gb-callout"
:class="['gb-' + (block.variant || 'info'),
{ 'gb-sectiondone': inDoneSection(index) }]">
<strong v-if="block.title">{{ block.title }}</strong>
<div v-html="inline(block.text)"></div>
</div>
<div v-else-if="block.type === 'code'" class="gb-code">
<div v-else-if="block.type === 'code'" class="gb-code"
:class="{ 'gb-sectiondone': inDoneSection(index) }">
<div class="gb-codehead">
<span class="gb-lang">{{ block.language || 'text' }}</span>
<span v-if="block.caption" class="gb-caption">{{ block.caption }}</span>
@@ -108,7 +115,8 @@
A failure says so in words. A guide whose screenshot is missing must not
look like a guide that never had one.
-->
<figure v-else-if="block.type === 'image'" class="gb-figure">
<figure v-else-if="block.type === 'image'" class="gb-figure"
:class="{ 'gb-sectiondone': inDoneSection(index) }">
<img v-if="imageState(block).src" :src="imageState(block).src"
:alt="block.alt || ''">
<p v-else-if="imageState(block).failed" class="gb-imagefailed">
@@ -118,7 +126,8 @@
<figcaption v-if="block.caption">{{ block.caption }}</figcaption>
</figure>
<div v-else-if="block.type === 'table'" class="gb-tablewrap">
<div v-else-if="block.type === 'table'" class="gb-tablewrap"
:class="{ 'gb-sectiondone': inDoneSection(index) }">
<table class="gb-table">
<thead v-if="block.header && block.header.length">
<tr><th v-for="(cell, c) in block.header" :key="c" v-html="inline(cell)"></th></tr>
@@ -179,6 +188,42 @@ function toggle (itemid, checked) {
emit('toggle', itemid, checked)
}
// Which heading governs each block: the nearest one above it. Ticking a
// heading crosses off everything under it down to the next heading, because
// that is what "this section is done" means to somebody reading - striking the
// title alone leaves the wall of text it was introducing looking untouched.
// Headings nest. A block belongs to every heading above it that has not been
// closed by a heading of the same or higher rank, so ticking a top-level
// section crosses off its sub-sections too - which is what "this section is
// done" means to somebody who ticked the thing that contained them.
//
// A guide written before levels existed has none; those all read as level 2,
// which is the flat behaviour they already had.
function levelOf (block) {
const level = Number(block.level)
return Number.isFinite(level) && level >= 2 && level <= 6 ? level : 2
}
const ancestors = computed(() => {
const map = {}
const stack = []
props.blocks.forEach((block, index) => {
if (block.type === 'heading') {
const level = levelOf(block)
while (stack.length && stack[stack.length - 1].level >= level) stack.pop()
map[index] = stack.map(entry => entry.id)
stack.push({ id: block.id, level })
} else {
map[index] = stack.map(entry => entry.id)
}
})
return map
})
function inDoneSection (index) {
return (ancestors.value[index] || []).some(id => isTicked(id))
}
// Steps number themselves by position among steps, so inserting a paragraph
// between two of them does not renumber anything by hand.
const stepIndex = computed(() => {
@@ -215,11 +260,24 @@ function select (block) {
dark mode work with no dark-mode code. */
.gb-blocks { line-height: 1.65; }
.gb-heading { margin: 1.6rem 0 0.6rem; font-size: 1.35rem; color: var(--text); }
/* Rank is visible, so a reader can see what contains what - and so ticking a
section off looks like it covered the right amount of page. */
.gb-h3 { font-size: 1.15rem; margin-top: 1.2rem; }
.gb-h4, .gb-h5, .gb-h6 { font-size: 1.02rem; margin-top: 1rem; }
/* A ticked section is struck through and faded, not hidden: somebody scanning
back up the page has to be able to read what they already did. */
back up the page has to be able to read what they already did.
The strike lands on the heading and on everything under it. A code block is
faded but NOT struck - a line through a command makes it unreadable, and the
reason to look back at a finished step is usually to re-read the command. */
.gb-sectiondone { text-decoration: line-through; opacity: 0.55; }
.gb-sectiondone .gb-stepnum { text-decoration: none; }
.gb-code.gb-sectiondone,
.gb-sectiondone pre,
.gb-sectiondone code,
.gb-figure.gb-sectiondone,
.gb-tablewrap.gb-sectiondone { text-decoration: none; }
.gb-sectiontick {
/* Touch-sized: a guide is read on a shopfloor PC, often with gloves on. */
width: 1.1rem;

View File

@@ -244,7 +244,12 @@ class _Builder:
if tag in ('h2', 'h3', 'h4', 'h5'):
text = _inline(node)
if text:
self._add({'type': 'heading', 'text': text})
# The LEVEL travels with it. Without it every heading is a
# sibling, and a reader who ticks a section off crosses out its
# title and nothing underneath - the sub-headings that belong to
# it look untouched, because nothing recorded that they belong.
self._add({'type': 'heading', 'level': int(tag[1]),
'text': text})
return
if tag == 'p':