§21 and §22, each state its own screen rather than a variant of one. Which one applies is decided by CycleStatusRules in a pure module with twelve tests on its boundaries — and the boundaries are the point, because they are the days this screen is most read: the day a period is due, the day after one ends, the day a forecast slips. The number dominates (§38): displayLarge at 72sp, in the primary colour, with the unit as a separate quiet line so "4" reads instantly and "DAYS" is there if you look. Its content description carries the whole sentence, so TalkBack says "Period likely in: 4 days" rather than reading a bare numeral. The state this screen exists to get right is the last one. Past the forecast the app NEVER says late — late implies a schedule the user failed to keep, and the truth is that an estimate was imprecise. It shows what it originally said, what it says now, and asks. A UX DEFECT FOUND BY DRIVING IT Tapping "Period ended" changed nothing on screen. The logic was right — a period that ends today still includes today, so the state does not change — but the button looked broken, which is worse than being broken somewhere visible. DuringPeriod now carries the end date, so the screen shows "Ended 18 August" and offers only the useful action (undo) rather than a button that visibly does nothing. §24's "Updated ✓" acknowledgement is there too. No test would have caught this; it needed somebody to tap the button and look. The banner slot is reserved and empty. §48 wants no layout jump when an ad loads and a graceful gap when one fails, and both are properties of the space existing whether or not it is filled — reserving it in Batch 07 instead means shipping the jump first. Deliberately not a "your ad here" box, which would be a placeholder for the thing a user pays to remove. Fertility lines are absent rather than faked: §22 shows them and Batch 04 estimates them, and a placeholder number there would be inventing a fertility estimate, which is the one thing this screen must not do. Preview pairs for every state, light and dark. 129 tests, all passing. closes #17 |
||
|---|---|---|
| .. | ||
| README.md | ||
README.md
Design
Status: Current
Owner: _null
Last reviewed: 2026-08-18
Governs: docs/design/**, the design tokens in core/designsystem, and the product's
tone and interface copy
Review trigger: Any new user-facing screen or state; any change to the colour or
type tokens; any change to notification copy or to a privacy or
fertility disclaimer
Where the detail is
The screen-by-screen specification and the actual words are
../planning/PRODUCT_PLAN.md, and they are not
repeated here — a second copy of interface copy is how two versions of a
disclaimer come to exist.
| Subject | Section it owns |
|---|---|
| Onboarding, seven screens, with copy | §19, §56 |
| Navigation — four tabs | §20 |
| Today screen and its six dynamic states | §21, §22 |
| Period logging, period end, spotting | §23, §24, §25 |
| Calendar states and markers | §26 |
| Insights | §27 |
| Notification modes, types and flow | §28, §29, §30 |
| Incognito launcher | §32 |
| Look and feel, visual direction, colour, typography, motion | §37–§41 |
| Artwork | §42 |
| Accessibility | §43 |
This document holds what governs those: the tone, and the rules that decide an argument the specification did not anticipate.
Tone
Calm, private, intelligent, adult. The app is a good utility with warmth — it is not clinical, not childish, not gamified, and not stereotypically feminine as an identity. It never celebrates a period and never alarms about one.
One paragraph, and it settles most small arguments: the app speaks like someone competent who is not making a fuss. "Your period is late!" is out. "Not yet?" with an updated forecast is in.
Four rules that decide the arguments
1. The number is the hero. The forecast dominates the Today screen — §38. Anything competing with it for attention is wrong, including anything of ours.
2. Nothing asserts certainty the model does not have. A window and a confidence label, never a bare exact date presented as fact. "Estimated ovulation", never "you are ovulating today". Fertility copy carries the not-contraception line wherever it appears.
3. State is never colour alone. Confirmed period is a solid fill, predicted is dotted or outlined, fertile window is a ring, ovulation is its own small marker — distinguishable in greyscale, because that is also what makes them distinguishable to a colourblind user and to a screenshot in a bug report. Predicted and confirmed days must never look identical.
The banner space is reserved before there is an ad to put in it. §48 wants no layout jump when one loads and a graceful gap when one fails, and both are properties of the space existing whether or not it is filled. Retrofitting the reservation in Batch 07 means shipping the jump first and finding it in a QA round. It is empty and invisible now — deliberately not a "your ad here" box, which would be a placeholder for the thing a user pays to remove.
4. Ads never touch a health action. No banner in onboarding, in the period-start confirmation, in the period-end confirmation, or between steps of a health workflow — and never an interstitial after logging (§33). Banner space is reserved in the layout so a failed ad does not move the content.
Colour, in one line each
The palette is §39; the constraints on it are:
- Not pink as the whole identity. Deep plum, muted berry, soft lavender, warm cream, charcoal, muted sage/teal.
- Period state is a sophisticated berry or plum — never graphic blood-red.
- Fertile window is muted teal or sage — never bright green, which reads as safe and this app must never say that.
- Everything goes through Material 3 colour roles and centralized tokens in
core/designsystem. No hard-coded colours in a Composable — the guard is that a colour literal outside the token file is a review failure.
Both themes, every time
Dark mode was broken for the whole of Batch 01 and nothing said so. PeriodTheme
was not wrapping its content in a Surface, so any Text without an explicit
colour inherited Material's default — black — and the app's own background never
painted at all. In light mode that looked correct by accident, because dark
text on cream is what was wanted anyway; in dark mode the onboarding headings
were near-black on charcoal.
No test caught it and no test easily would. It was found by opening the app.
Two rules follow. Every screen gets a preview pair, light and dark — cheap,
and it puts the failure in front of whoever is editing. And the Surface lives
in the theme rather than in each screen, so a screen without a Scaffold cannot
forget it.
The states most often left undesigned
Designed here on purpose, because they are the two most people meet first:
- Empty — no periods logged yet. It has to make the next action obvious rather than apologise.
- Learning — one or two cycles recorded. The app says it is still learning rather than showing a confident forecast it has not earned. "Getting to know your pattern", not a percentage.
The artwork is placeholder, and says so here
Every illustration and calendar marker in the app is a Compose vector path
in core/designsystem/src/main/kotlin/dev/privacyllc/period/designsystem/art/, written as PRODUCT_PLAN.md §42 asks: polished
placeholders behind replaceable names, so real artwork changes one function body
and no call site.
The visual language is deliberately narrow — overlapping circular forms and nothing else. §42's forbidden list (blood drops, tampons, pads, uterus imagery, gender symbols, anatomy) is a product decision rather than squeamishness: this app gets opened in public, and somebody glancing over a shoulder should learn nothing. That is a property of the pictures as much as of the notification text.
Note the deliberate inconsistency with docs/data/img/, where a placeholder
is forbidden. The rule there is that an image which looks finished outlives the
issue that would have replaced it, because nobody files a ticket against
something that appears done. In-app assets escape that because §42 asks for them
explicitly and the replaceable-name mechanism is what keeps them replaceable —
and because a screen with no illustration cannot be evaluated at all, while a
project card with no icon simply shows initials.
Include the rejected version
For any decision that was genuinely close, record what was not chosen and why.
Without it the same option is proposed again every few months and re-argued from
nothing. The first entries belong to whoever makes those calls; the
specification's Avoid: lists are already a partial record of them.
What does not belong here
- How it is built —
../architecture/README.md - Scope and audience —
../planning/PROJECT_PLAN.md