Checklists in the body, colour from tags, and commit-derived CalVer #4

Merged
bvandeusen merged 73 commits from dev into main 2026-08-29 13:39:45 -04:00
Showing only changes of commit 68f851110f - Show all commits
@@ -14,9 +14,11 @@ import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -136,38 +138,54 @@ private fun TaskBlock(
onEnter: () -> Unit,
onDelete: () -> Unit,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = block.checked == true,
onCheckedChange = { onChange(block.copy(checked = it)) },
enabled = !readOnly,
)
BlockField(
value = block.value,
onValueChange = { onChange(block.copy(value = it)) },
modifier = Modifier.weight(1f).focusRequester(requester),
enabled = !readOnly,
singleLine = true,
textStyle =
MaterialTheme.typography.bodyLarge.copy(
// Struck through when done, matching the card and the web.
textDecoration =
if (block.checked == true) TextDecoration.LineThrough else null,
),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = { onEnter() }),
)
if (!readOnly) {
IconButton(onClick = onDelete) {
Icon(
Icons.Filled.Close,
contentDescription = stringResource(R.string.editor_remove_item),
)
// Material sizes every interactive component to a 48dp touch target, and on a
// checklist that IS the row height — which is why six items filled a phone screen
// even after the field's own padding came off.
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides ROW_TOUCH) {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = block.checked == true,
onCheckedChange = { onChange(block.copy(checked = it)) },
enabled = !readOnly,
)
BlockField(
value = block.value,
onValueChange = { onChange(block.copy(value = it)) },
modifier = Modifier.weight(1f).focusRequester(requester),
enabled = !readOnly,
singleLine = true,
textStyle =
MaterialTheme.typography.bodyLarge.copy(
// Struck through when done, matching the card and the web.
textDecoration =
if (block.checked == true) TextDecoration.LineThrough else null,
),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = { onEnter() }),
)
if (!readOnly) {
IconButton(onClick = onDelete) {
Icon(
Icons.Filled.Close,
contentDescription = stringResource(R.string.editor_remove_item),
)
}
}
}
}
}
/**
* The touch target for a checklist row's controls.
*
* Material's floor is 48dp and this is deliberately under it. That floor is sized for
* a control somebody has to find; a checklist box sits in a predictable column with an
* identical box directly above and below, and the cost of a near miss is ticking the
* neighbouring item — visible, and undone by tapping again. Trading twelve of those
* dp for a list that fits on a screen is what was asked for, twice.
*/
private val ROW_TOUCH = 36.dp
/**
* The field a block is typed into.
*