Close a card by pressing its words, and let a selection be put away

A markdown paragraph took every tap that landed on its glyphs, so an opened
peer message or memory note could be shut anywhere except on the text --
which is most of it, and reads as a card that has stopped working. Measured
on the emulator: with a handler on the text the tap did nothing at all, and
with the handler removed the same tap shut the card. The words now do the
shutting, through a composition local, since the renderer composes those
paragraphs out of its own component table and there is nothing between the
card and them to pass a parameter through. The link handler is bounded by
the long-press timeout, so holding to select is not a tap.

The other half is the tap that puts a selection away, which used to shut
whatever card the words were in. The container clears the selection from
that same press, milliseconds before the card reads it, so the answer is
taken at composition instead -- what was true when the reader touched the
screen.

Selection colours are the app's own. Material's 40% of primary is a tint of
whatever is behind it, and over the near-black a code block sits on it
composited to a smudge, so selecting a line of code looked like nothing had
happened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-03 21:03:15 -04:00
1 parent 359649bc73
commit acdf00ab1d
10 files changed
+336 -69

No files matched your search

@@ -0,0 +1,56 @@
package com.example.aiapp
import java.time.Duration
import kotlin.test.Test
import kotlin.test.assertEquals
/**
* The two ways a span of time is written here, and the rule each of them follows.
*
* Both are read off a screen to make a decision -- how long a tool call may take, how long a quota
* has left -- so what matters is that the shortest form that answers the question is what appears.
*/
class DurationsTest {
@Test
fun `under a minute is the largest unit alone`() {
assertEquals("30ms", formatMillis(30))
assertEquals("999ms", formatMillis(999))
assertEquals("1s", formatMillis(1000))
assertEquals("2.5s", formatMillis(2500))
// One decimal, rounded rather than cut: 2.46s is nearer two and a half than two and four.
assertEquals("2.5s", formatMillis(2460))
assertEquals("59.9s", formatMillis(59_900))
}
@Test
fun `a minute or more is every unit that has something in it`() {
// The figure this rule was written for: a tool timeout, which arrives as milliseconds and
// is unreadable as 480000.
assertEquals("8m", formatMillis(480_000))
assertEquals("1m", formatMillis(60_000))
assertEquals("1m 30s", formatMillis(90_000))
assertEquals("5d 12h 4m", formatMillis(475_440_000))
// Empty units are left out rather than written as zero: the labels say which is which,
// and "5d 0h 4m" is only longer.
assertEquals("5d 4m", formatMillis(432_240_000))
}
@Test
fun `only a whole number of milliseconds is rewritten`() {
assertEquals("8m", formatMillisText(" 480000 "))
// A timeout a tool expressed some other way is its own words, passed through rather than
// guessed at.
assertEquals("2 minutes", formatMillisText("2 minutes"))
assertEquals("", formatMillisText(""))
}
@Test
fun `a countdown rounds up, so it never reports a minute already spent`() {
assertEquals("3h 13m", formatSpan(Duration.ofMinutes(192).plusSeconds(50)))
// Exactly on a minute is already the answer and is not pushed past it.
assertEquals("3h 12m", formatSpan(Duration.ofMinutes(192)))
assertEquals("12m", formatSpan(Duration.ofMinutes(12)))
// Rounding up carries, so a day's worth of minutes reads as a day.
assertEquals("1d 0h", formatSpan(Duration.ofHours(23).plusMinutes(59).plusSeconds(30)))
}
}