Files
mantra-kmp/docs/scripts/m3-touch-targets.py

80 lines
3.5 KiB
Python
Raw Normal View History

fix: give every tappable element a real target, and every icon a decided description Phase 3, second step, of docs/material-design-conformance.md. Two accessibility rules the tree had no way to hold: M3's 48x48dp touch target and 44x44dp pointer target, and its requirement that a decorative visual be *annotated* as decorative rather than merely left undescribed. **Nineteen `.clickable` chains had no minimum size, and three were text-sized.** `ArticleCard` and `LiveStreamCardContent` each make an author's name tappable -- a `labelMedium`, around 16dp tall -- and `LinkPreview` does the same to a `bodyLarge` url with 2dp of vertical padding. The other sixteen are cards, rows and full-screen boxes that are already far larger. `minimumInteractiveComponentSize()` is applied to all nineteen rather than to the three, because it is a no-op on anything already 48dp and that makes the rule checkable by a script instead of by measuring. Worth being precise about what it does, since the modifier is easy to describe wrongly: it reserves 48x48dp of **layout**, not of touch handling -- touch expansion happens at the input layer regardless. Layout is what keeps adjacent targets from overlapping, what satisfies M3's 8dp separation, and what a mouse pointer on the desktop build actually has to land on. **`Clickable.kt` had it built in and moved house.** The vendored ACINQ helper defaults to `RectangleShape` and `PaddingValues(0.dp)`, so a `Clickable` is exactly as big as its content -- and its call sites wrap a 20dp emoji and a row of wallet text. It now applies the modifier unconditionally, before `.padding(internalPadding)`, since a size modifier after it would re-impose the smaller constraint. It also stopped declaring `package com.machankura.compose.ui.composable.widgets.buttons` while living under `press/mantra/`. That is the second of the three package namespaces the UI was spread across; `Type.kt` was the first. **Eighteen `contentDescription = null` were indistinguishable from eighteen oversights.** `null` is the *correct* API -- M3 asks that decorative visuals be "annotated as decorative in order to hide them in code", and null is how that annotation is spelled in Compose. The problem is that it reads identically whether somebody decided or never looked. So `Decorative` is introduced -- a `String?` that is null -- and fifteen sites now say `contentDescription = Decorative`. Same bytes, same behaviour, and the difference between a decision and a gap is now visible in the source and countable by the audit. Each of the fifteen has adjacent text saying what the icon says: a lock beside "Private to Ada", a check beside "The group has a shared key.", an icon inside a button whose label is right there. **Three were not decorative and now carry their state.** - `DkgRitualScreen`'s participant list -- a filled or empty circle beside each member. The name says who; only the icon says whether they have contributed. Now "Contributed" / "Not yet contributed". - `DkgRitualScreen`'s round header -- the title says which round and the count says how far along; only the icon says whether it finished. Now "Complete" / "In progress". - `ProposalListScreen`'s leading icon, which is the one this commit could not have left alone: the previous commit took the red away from the failure state on the highlighted card, because `error` is 2.67:1 there. The shape is now the only cue a sighted user gets and the description is the only cue anyone else gets. Now "Awaiting your signature" / "Signed" / "Failed" / "Waiting on others". Descriptions follow M3's rule -- name the purpose, not the picture, and never the role. "Contributed", not "green check", and never "Contributed icon", since the role is added automatically and a screen reader would say it twice. **Two new checks, replacing one that was asking the wrong question.** `docs/scripts/m3-touch-targets.py` finds `.clickable` chains with no minimum size, including chains broken across two lines. The audit used to count `.clickable` outright, which is not a defect count: a clickable `Card` is fine and a clickable `Text` is not, and only the modifier tells them apart. The audit also now separates `contentDescription = null` (untriaged, budget 0) from `Decorative` (decided, reported at 15). Both budgets ratcheted to 0, dated in the file. **Tests.** 944 pass, 595 jvm over 72 classes and 349 android over 44, unchanged -- these are layout and semantics properties, and this repo has no Compose UI test infrastructure to assert them against a running composition. What stands in for it is the two scripts, which check the property that *can* be checked statically: that the modifier and the decision are present at every site. `:composeApp:compileDebugKotlinAndroid` builds, `m3-audit.sh --check` exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 00:52:15 +02:00
#!/usr/bin/env python3
"""Find `.clickable` chains with no minimum interactive size.
M3 asks for touch targets of at least 48x48dp and pointer targets of at least 44x44dp.
`Modifier.clickable` gives an element no minimum of its own, so a clickable `Text` is a
target the size of the text -- around 20dp here.
`minimumInteractiveComponentSize()` is a no-op on anything already 48dp or larger, so the
rule this checks is simply that every `.clickable` chain has it: correct everywhere, and
textual enough to enforce. It reserves *layout* space; touch expansion happens at the
input layer regardless, and the layout is what stops adjacent targets overlapping and what
a pointer on desktop has to land on.
Components that carry their own minimum -- IconButton, Button, Checkbox, ListItem and the
rest of material3 -- are not `.clickable` call sites and never appear here.
fix(scripts): stop m3-touch-targets.py reading comments as code A KDoc line on the dialect picker's row composable, explaining that it used `Card(onClick = …)` rather than a `Modifier.clickable`, was counted as a bare clickable chain. The budget is zero, so a sentence about the rule broke `./gradlew check`. The comment was reworded to get around it, which was the wrong fix -- the sentence was right -- and it is restored here. Not a new rule. m3-spacing-positions.py skips `\s*(//|\*|/\*)` and m3-title-case.py skips the same three prefixes as a startswith tuple; only this checker was missing the two block-comment ones, having been written with `//` alone. m3-audit.sh explains the whole problem at NOT_A_COMMENT, where a note about a hardcoded colour once counted as a colour. The same function had the mirror bug, which is the dangerous direction. It looked for minimumInteractiveComponentSize in a two-line window without stripping comments, so a comment above a real bare `.clickable` that merely mentioned the modifier would have excused it. A false positive is noise; a silently excused offender is a miss. Both lines of the window are now read as code only. Checked against six cases: a bare clickable is still caught, a guard on the same line and a guard on the line above still excuse one, a starred KDoc no longer counts, and a real offender under a comment naming the modifier is now caught. Counts for the tree are unchanged -- 0 before and after -- which was worth establishing, since a broken detector would also report zero. A prefix test rather than block tracking, and the limitation is written on the function: a `/* ... */ `body line not starting with `*` still reads as code. There are 539 block comments under ui/ and not one such line, because every KDoc here stars its body, and three identical rules spelled three ways would be worse than one limitation on the record. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 00:25:16 +02:00
Comments are not code, in either direction. A KDoc line explaining that a row uses a
clickable `Card` *rather than* a `Modifier.clickable` was counted as a call site, which
breaks a zero budget and so breaks `./gradlew check`; and a comment above a real call site
that merely mentions `minimumInteractiveComponentSize` would have excused it. The first is
noise and the second is a miss, so both lines of the window are read as code only. This is
the same thing m3-audit.sh's `NOT_A_COMMENT` exists for, spelled as m3-title-case.py
spells it -- see the note there on a colour comment counting as a colour.
fix: give every tappable element a real target, and every icon a decided description Phase 3, second step, of docs/material-design-conformance.md. Two accessibility rules the tree had no way to hold: M3's 48x48dp touch target and 44x44dp pointer target, and its requirement that a decorative visual be *annotated* as decorative rather than merely left undescribed. **Nineteen `.clickable` chains had no minimum size, and three were text-sized.** `ArticleCard` and `LiveStreamCardContent` each make an author's name tappable -- a `labelMedium`, around 16dp tall -- and `LinkPreview` does the same to a `bodyLarge` url with 2dp of vertical padding. The other sixteen are cards, rows and full-screen boxes that are already far larger. `minimumInteractiveComponentSize()` is applied to all nineteen rather than to the three, because it is a no-op on anything already 48dp and that makes the rule checkable by a script instead of by measuring. Worth being precise about what it does, since the modifier is easy to describe wrongly: it reserves 48x48dp of **layout**, not of touch handling -- touch expansion happens at the input layer regardless. Layout is what keeps adjacent targets from overlapping, what satisfies M3's 8dp separation, and what a mouse pointer on the desktop build actually has to land on. **`Clickable.kt` had it built in and moved house.** The vendored ACINQ helper defaults to `RectangleShape` and `PaddingValues(0.dp)`, so a `Clickable` is exactly as big as its content -- and its call sites wrap a 20dp emoji and a row of wallet text. It now applies the modifier unconditionally, before `.padding(internalPadding)`, since a size modifier after it would re-impose the smaller constraint. It also stopped declaring `package com.machankura.compose.ui.composable.widgets.buttons` while living under `press/mantra/`. That is the second of the three package namespaces the UI was spread across; `Type.kt` was the first. **Eighteen `contentDescription = null` were indistinguishable from eighteen oversights.** `null` is the *correct* API -- M3 asks that decorative visuals be "annotated as decorative in order to hide them in code", and null is how that annotation is spelled in Compose. The problem is that it reads identically whether somebody decided or never looked. So `Decorative` is introduced -- a `String?` that is null -- and fifteen sites now say `contentDescription = Decorative`. Same bytes, same behaviour, and the difference between a decision and a gap is now visible in the source and countable by the audit. Each of the fifteen has adjacent text saying what the icon says: a lock beside "Private to Ada", a check beside "The group has a shared key.", an icon inside a button whose label is right there. **Three were not decorative and now carry their state.** - `DkgRitualScreen`'s participant list -- a filled or empty circle beside each member. The name says who; only the icon says whether they have contributed. Now "Contributed" / "Not yet contributed". - `DkgRitualScreen`'s round header -- the title says which round and the count says how far along; only the icon says whether it finished. Now "Complete" / "In progress". - `ProposalListScreen`'s leading icon, which is the one this commit could not have left alone: the previous commit took the red away from the failure state on the highlighted card, because `error` is 2.67:1 there. The shape is now the only cue a sighted user gets and the description is the only cue anyone else gets. Now "Awaiting your signature" / "Signed" / "Failed" / "Waiting on others". Descriptions follow M3's rule -- name the purpose, not the picture, and never the role. "Contributed", not "green check", and never "Contributed icon", since the role is added automatically and a screen reader would say it twice. **Two new checks, replacing one that was asking the wrong question.** `docs/scripts/m3-touch-targets.py` finds `.clickable` chains with no minimum size, including chains broken across two lines. The audit used to count `.clickable` outright, which is not a defect count: a clickable `Card` is fine and a clickable `Text` is not, and only the modifier tells them apart. The audit also now separates `contentDescription = null` (untriaged, budget 0) from `Decorative` (decided, reported at 15). Both budgets ratcheted to 0, dated in the file. **Tests.** 944 pass, 595 jvm over 72 classes and 349 android over 44, unchanged -- these are layout and semantics properties, and this repo has no Compose UI test infrastructure to assert them against a running composition. What stands in for it is the two scripts, which check the property that *can* be checked statically: that the modifier and the decision are present at every site. `:composeApp:compileDebugKotlinAndroid` builds, `m3-audit.sh --check` exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 00:52:15 +02:00
Usage: m3-touch-targets.py [--list]
"""
import os, re, sys
UI = 'composeApp/src/commonMain/kotlin/press/mantra/compose/ui'
fix(scripts): stop m3-touch-targets.py reading comments as code A KDoc line on the dialect picker's row composable, explaining that it used `Card(onClick = …)` rather than a `Modifier.clickable`, was counted as a bare clickable chain. The budget is zero, so a sentence about the rule broke `./gradlew check`. The comment was reworded to get around it, which was the wrong fix -- the sentence was right -- and it is restored here. Not a new rule. m3-spacing-positions.py skips `\s*(//|\*|/\*)` and m3-title-case.py skips the same three prefixes as a startswith tuple; only this checker was missing the two block-comment ones, having been written with `//` alone. m3-audit.sh explains the whole problem at NOT_A_COMMENT, where a note about a hardcoded colour once counted as a colour. The same function had the mirror bug, which is the dangerous direction. It looked for minimumInteractiveComponentSize in a two-line window without stripping comments, so a comment above a real bare `.clickable` that merely mentioned the modifier would have excused it. A false positive is noise; a silently excused offender is a miss. Both lines of the window are now read as code only. Checked against six cases: a bare clickable is still caught, a guard on the same line and a guard on the line above still excuse one, a starred KDoc no longer counts, and a real offender under a comment naming the modifier is now caught. Counts for the tree are unchanged -- 0 before and after -- which was worth establishing, since a broken detector would also report zero. A prefix test rather than block tracking, and the limitation is written on the function: a `/* ... */ `body line not starting with `*` still reads as code. There are 539 block comments under ui/ and not one such line, because every KDoc here stars its body, and three identical rules spelled three ways would be worse than one limitation on the record. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 00:25:16 +02:00
def _is_comment(line):
"""Whether `line` is a comment line: `//`, or a `/*` block's opener or body.
A prefix test rather than block tracking, because that is what the other two checkers
do and three identical rules spelled three ways is worse than one limitation. The
limitation: a `/* ... */` body line that does not start with `*` reads as code. There
are 539 block comments under `ui/` and not one such line, since every KDoc here stars
its body -- if that ever stops being true, this is the function to make stateful.
"""
return line.lstrip().startswith(('//', '*', '/*'))
fix: give every tappable element a real target, and every icon a decided description Phase 3, second step, of docs/material-design-conformance.md. Two accessibility rules the tree had no way to hold: M3's 48x48dp touch target and 44x44dp pointer target, and its requirement that a decorative visual be *annotated* as decorative rather than merely left undescribed. **Nineteen `.clickable` chains had no minimum size, and three were text-sized.** `ArticleCard` and `LiveStreamCardContent` each make an author's name tappable -- a `labelMedium`, around 16dp tall -- and `LinkPreview` does the same to a `bodyLarge` url with 2dp of vertical padding. The other sixteen are cards, rows and full-screen boxes that are already far larger. `minimumInteractiveComponentSize()` is applied to all nineteen rather than to the three, because it is a no-op on anything already 48dp and that makes the rule checkable by a script instead of by measuring. Worth being precise about what it does, since the modifier is easy to describe wrongly: it reserves 48x48dp of **layout**, not of touch handling -- touch expansion happens at the input layer regardless. Layout is what keeps adjacent targets from overlapping, what satisfies M3's 8dp separation, and what a mouse pointer on the desktop build actually has to land on. **`Clickable.kt` had it built in and moved house.** The vendored ACINQ helper defaults to `RectangleShape` and `PaddingValues(0.dp)`, so a `Clickable` is exactly as big as its content -- and its call sites wrap a 20dp emoji and a row of wallet text. It now applies the modifier unconditionally, before `.padding(internalPadding)`, since a size modifier after it would re-impose the smaller constraint. It also stopped declaring `package com.machankura.compose.ui.composable.widgets.buttons` while living under `press/mantra/`. That is the second of the three package namespaces the UI was spread across; `Type.kt` was the first. **Eighteen `contentDescription = null` were indistinguishable from eighteen oversights.** `null` is the *correct* API -- M3 asks that decorative visuals be "annotated as decorative in order to hide them in code", and null is how that annotation is spelled in Compose. The problem is that it reads identically whether somebody decided or never looked. So `Decorative` is introduced -- a `String?` that is null -- and fifteen sites now say `contentDescription = Decorative`. Same bytes, same behaviour, and the difference between a decision and a gap is now visible in the source and countable by the audit. Each of the fifteen has adjacent text saying what the icon says: a lock beside "Private to Ada", a check beside "The group has a shared key.", an icon inside a button whose label is right there. **Three were not decorative and now carry their state.** - `DkgRitualScreen`'s participant list -- a filled or empty circle beside each member. The name says who; only the icon says whether they have contributed. Now "Contributed" / "Not yet contributed". - `DkgRitualScreen`'s round header -- the title says which round and the count says how far along; only the icon says whether it finished. Now "Complete" / "In progress". - `ProposalListScreen`'s leading icon, which is the one this commit could not have left alone: the previous commit took the red away from the failure state on the highlighted card, because `error` is 2.67:1 there. The shape is now the only cue a sighted user gets and the description is the only cue anyone else gets. Now "Awaiting your signature" / "Signed" / "Failed" / "Waiting on others". Descriptions follow M3's rule -- name the purpose, not the picture, and never the role. "Contributed", not "green check", and never "Contributed icon", since the role is added automatically and a screen reader would say it twice. **Two new checks, replacing one that was asking the wrong question.** `docs/scripts/m3-touch-targets.py` finds `.clickable` chains with no minimum size, including chains broken across two lines. The audit used to count `.clickable` outright, which is not a defect count: a clickable `Card` is fine and a clickable `Text` is not, and only the modifier tells them apart. The audit also now separates `contentDescription = null` (untriaged, budget 0) from `Decorative` (decided, reported at 15). Both budgets ratcheted to 0, dated in the file. **Tests.** 944 pass, 595 jvm over 72 classes and 349 android over 44, unchanged -- these are layout and semantics properties, and this repo has no Compose UI test infrastructure to assert them against a running composition. What stands in for it is the two scripts, which check the property that *can* be checked statically: that the modifier and the decision are present at every site. `:composeApp:compileDebugKotlinAndroid` builds, `m3-audit.sh --check` exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 00:52:15 +02:00
def offenders():
found = []
for root, _, files in os.walk(UI):
for f in sorted(files):
if not f.endswith('.kt'):
continue
path = os.path.join(root, f)
lines = open(path, encoding='utf-8').read().split('\n')
for i, line in enumerate(lines):
if '.clickable' not in line:
continue
fix(scripts): stop m3-touch-targets.py reading comments as code A KDoc line on the dialect picker's row composable, explaining that it used `Card(onClick = …)` rather than a `Modifier.clickable`, was counted as a bare clickable chain. The budget is zero, so a sentence about the rule broke `./gradlew check`. The comment was reworded to get around it, which was the wrong fix -- the sentence was right -- and it is restored here. Not a new rule. m3-spacing-positions.py skips `\s*(//|\*|/\*)` and m3-title-case.py skips the same three prefixes as a startswith tuple; only this checker was missing the two block-comment ones, having been written with `//` alone. m3-audit.sh explains the whole problem at NOT_A_COMMENT, where a note about a hardcoded colour once counted as a colour. The same function had the mirror bug, which is the dangerous direction. It looked for minimumInteractiveComponentSize in a two-line window without stripping comments, so a comment above a real bare `.clickable` that merely mentioned the modifier would have excused it. A false positive is noise; a silently excused offender is a miss. Both lines of the window are now read as code only. Checked against six cases: a bare clickable is still caught, a guard on the same line and a guard on the line above still excuse one, a starred KDoc no longer counts, and a real offender under a comment naming the modifier is now caught. Counts for the tree are unchanged -- 0 before and after -- which was worth establishing, since a broken detector would also report zero. A prefix test rather than block tracking, and the limitation is written on the function: a `/* ... */ `body line not starting with `*` still reads as code. There are 539 block comments under ui/ and not one such line, because every KDoc here stars its body, and three identical rules spelled three ways would be worse than one limitation on the record. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 00:25:16 +02:00
if _is_comment(line) or line.startswith('import '):
fix: give every tappable element a real target, and every icon a decided description Phase 3, second step, of docs/material-design-conformance.md. Two accessibility rules the tree had no way to hold: M3's 48x48dp touch target and 44x44dp pointer target, and its requirement that a decorative visual be *annotated* as decorative rather than merely left undescribed. **Nineteen `.clickable` chains had no minimum size, and three were text-sized.** `ArticleCard` and `LiveStreamCardContent` each make an author's name tappable -- a `labelMedium`, around 16dp tall -- and `LinkPreview` does the same to a `bodyLarge` url with 2dp of vertical padding. The other sixteen are cards, rows and full-screen boxes that are already far larger. `minimumInteractiveComponentSize()` is applied to all nineteen rather than to the three, because it is a no-op on anything already 48dp and that makes the rule checkable by a script instead of by measuring. Worth being precise about what it does, since the modifier is easy to describe wrongly: it reserves 48x48dp of **layout**, not of touch handling -- touch expansion happens at the input layer regardless. Layout is what keeps adjacent targets from overlapping, what satisfies M3's 8dp separation, and what a mouse pointer on the desktop build actually has to land on. **`Clickable.kt` had it built in and moved house.** The vendored ACINQ helper defaults to `RectangleShape` and `PaddingValues(0.dp)`, so a `Clickable` is exactly as big as its content -- and its call sites wrap a 20dp emoji and a row of wallet text. It now applies the modifier unconditionally, before `.padding(internalPadding)`, since a size modifier after it would re-impose the smaller constraint. It also stopped declaring `package com.machankura.compose.ui.composable.widgets.buttons` while living under `press/mantra/`. That is the second of the three package namespaces the UI was spread across; `Type.kt` was the first. **Eighteen `contentDescription = null` were indistinguishable from eighteen oversights.** `null` is the *correct* API -- M3 asks that decorative visuals be "annotated as decorative in order to hide them in code", and null is how that annotation is spelled in Compose. The problem is that it reads identically whether somebody decided or never looked. So `Decorative` is introduced -- a `String?` that is null -- and fifteen sites now say `contentDescription = Decorative`. Same bytes, same behaviour, and the difference between a decision and a gap is now visible in the source and countable by the audit. Each of the fifteen has adjacent text saying what the icon says: a lock beside "Private to Ada", a check beside "The group has a shared key.", an icon inside a button whose label is right there. **Three were not decorative and now carry their state.** - `DkgRitualScreen`'s participant list -- a filled or empty circle beside each member. The name says who; only the icon says whether they have contributed. Now "Contributed" / "Not yet contributed". - `DkgRitualScreen`'s round header -- the title says which round and the count says how far along; only the icon says whether it finished. Now "Complete" / "In progress". - `ProposalListScreen`'s leading icon, which is the one this commit could not have left alone: the previous commit took the red away from the failure state on the highlighted card, because `error` is 2.67:1 there. The shape is now the only cue a sighted user gets and the description is the only cue anyone else gets. Now "Awaiting your signature" / "Signed" / "Failed" / "Waiting on others". Descriptions follow M3's rule -- name the purpose, not the picture, and never the role. "Contributed", not "green check", and never "Contributed icon", since the role is added automatically and a screen reader would say it twice. **Two new checks, replacing one that was asking the wrong question.** `docs/scripts/m3-touch-targets.py` finds `.clickable` chains with no minimum size, including chains broken across two lines. The audit used to count `.clickable` outright, which is not a defect count: a clickable `Card` is fine and a clickable `Text` is not, and only the modifier tells them apart. The audit also now separates `contentDescription = null` (untriaged, budget 0) from `Decorative` (decided, reported at 15). Both budgets ratcheted to 0, dated in the file. **Tests.** 944 pass, 595 jvm over 72 classes and 349 android over 44, unchanged -- these are layout and semantics properties, and this repo has no Compose UI test infrastructure to assert them against a running composition. What stands in for it is the two scripts, which check the property that *can* be checked statically: that the modifier and the decision are present at every site. `:composeApp:compileDebugKotlinAndroid` builds, `m3-audit.sh --check` exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 00:52:15 +02:00
continue
# The modifier may sit on this line or on the one above, since a chain
fix(scripts): stop m3-touch-targets.py reading comments as code A KDoc line on the dialect picker's row composable, explaining that it used `Card(onClick = …)` rather than a `Modifier.clickable`, was counted as a bare clickable chain. The budget is zero, so a sentence about the rule broke `./gradlew check`. The comment was reworded to get around it, which was the wrong fix -- the sentence was right -- and it is restored here. Not a new rule. m3-spacing-positions.py skips `\s*(//|\*|/\*)` and m3-title-case.py skips the same three prefixes as a startswith tuple; only this checker was missing the two block-comment ones, having been written with `//` alone. m3-audit.sh explains the whole problem at NOT_A_COMMENT, where a note about a hardcoded colour once counted as a colour. The same function had the mirror bug, which is the dangerous direction. It looked for minimumInteractiveComponentSize in a two-line window without stripping comments, so a comment above a real bare `.clickable` that merely mentioned the modifier would have excused it. A false positive is noise; a silently excused offender is a miss. Both lines of the window are now read as code only. Checked against six cases: a bare clickable is still caught, a guard on the same line and a guard on the line above still excuse one, a starred KDoc no longer counts, and a real offender under a comment naming the modifier is now caught. Counts for the tree are unchanged -- 0 before and after -- which was worth establishing, since a broken detector would also report zero. A prefix test rather than block tracking, and the limitation is written on the function: a `/* ... */ `body line not starting with `*` still reads as code. There are 539 block comments under ui/ and not one such line, because every KDoc here stars its body, and three identical rules spelled three ways would be worse than one limitation on the record. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 00:25:16 +02:00
# broken across lines is the common shape. Comment lines are dropped from
# the window rather than kept: an exemption has to be in the code.
window = '\n'.join(
l for l in lines[max(0, i - 1):i + 1] if not _is_comment(l)
)
fix: give every tappable element a real target, and every icon a decided description Phase 3, second step, of docs/material-design-conformance.md. Two accessibility rules the tree had no way to hold: M3's 48x48dp touch target and 44x44dp pointer target, and its requirement that a decorative visual be *annotated* as decorative rather than merely left undescribed. **Nineteen `.clickable` chains had no minimum size, and three were text-sized.** `ArticleCard` and `LiveStreamCardContent` each make an author's name tappable -- a `labelMedium`, around 16dp tall -- and `LinkPreview` does the same to a `bodyLarge` url with 2dp of vertical padding. The other sixteen are cards, rows and full-screen boxes that are already far larger. `minimumInteractiveComponentSize()` is applied to all nineteen rather than to the three, because it is a no-op on anything already 48dp and that makes the rule checkable by a script instead of by measuring. Worth being precise about what it does, since the modifier is easy to describe wrongly: it reserves 48x48dp of **layout**, not of touch handling -- touch expansion happens at the input layer regardless. Layout is what keeps adjacent targets from overlapping, what satisfies M3's 8dp separation, and what a mouse pointer on the desktop build actually has to land on. **`Clickable.kt` had it built in and moved house.** The vendored ACINQ helper defaults to `RectangleShape` and `PaddingValues(0.dp)`, so a `Clickable` is exactly as big as its content -- and its call sites wrap a 20dp emoji and a row of wallet text. It now applies the modifier unconditionally, before `.padding(internalPadding)`, since a size modifier after it would re-impose the smaller constraint. It also stopped declaring `package com.machankura.compose.ui.composable.widgets.buttons` while living under `press/mantra/`. That is the second of the three package namespaces the UI was spread across; `Type.kt` was the first. **Eighteen `contentDescription = null` were indistinguishable from eighteen oversights.** `null` is the *correct* API -- M3 asks that decorative visuals be "annotated as decorative in order to hide them in code", and null is how that annotation is spelled in Compose. The problem is that it reads identically whether somebody decided or never looked. So `Decorative` is introduced -- a `String?` that is null -- and fifteen sites now say `contentDescription = Decorative`. Same bytes, same behaviour, and the difference between a decision and a gap is now visible in the source and countable by the audit. Each of the fifteen has adjacent text saying what the icon says: a lock beside "Private to Ada", a check beside "The group has a shared key.", an icon inside a button whose label is right there. **Three were not decorative and now carry their state.** - `DkgRitualScreen`'s participant list -- a filled or empty circle beside each member. The name says who; only the icon says whether they have contributed. Now "Contributed" / "Not yet contributed". - `DkgRitualScreen`'s round header -- the title says which round and the count says how far along; only the icon says whether it finished. Now "Complete" / "In progress". - `ProposalListScreen`'s leading icon, which is the one this commit could not have left alone: the previous commit took the red away from the failure state on the highlighted card, because `error` is 2.67:1 there. The shape is now the only cue a sighted user gets and the description is the only cue anyone else gets. Now "Awaiting your signature" / "Signed" / "Failed" / "Waiting on others". Descriptions follow M3's rule -- name the purpose, not the picture, and never the role. "Contributed", not "green check", and never "Contributed icon", since the role is added automatically and a screen reader would say it twice. **Two new checks, replacing one that was asking the wrong question.** `docs/scripts/m3-touch-targets.py` finds `.clickable` chains with no minimum size, including chains broken across two lines. The audit used to count `.clickable` outright, which is not a defect count: a clickable `Card` is fine and a clickable `Text` is not, and only the modifier tells them apart. The audit also now separates `contentDescription = null` (untriaged, budget 0) from `Decorative` (decided, reported at 15). Both budgets ratcheted to 0, dated in the file. **Tests.** 944 pass, 595 jvm over 72 classes and 349 android over 44, unchanged -- these are layout and semantics properties, and this repo has no Compose UI test infrastructure to assert them against a running composition. What stands in for it is the two scripts, which check the property that *can* be checked statically: that the modifier and the decision are present at every site. `:composeApp:compileDebugKotlinAndroid` builds, `m3-audit.sh --check` exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 00:52:15 +02:00
if 'minimumInteractiveComponentSize' in window:
continue
found.append((path, i + 1, line.strip()[:90]))
return found
def main():
found = offenders()
print(f' {"clickable chains with no minimum target":42s} {len(found):6d}')
if '--list' in sys.argv:
for path, line, ctx in found:
print(f' {path.replace(UI + "/", "")}:{line} {ctx}')
return 1 if found else 0
if __name__ == '__main__':
sys.exit(main())