238 lines
12 KiB
Markdown
238 lines
12 KiB
Markdown
# Design
|
|
|
|
```
|
|
Status: Current
|
|
Owner: _null
|
|
Last reviewed: 2026-08-18
|
|
Governs: docs/design/**, core/designsystem/**, app/src/main/kotlin/**,
|
|
app/src/main/res/values/** — the design tokens, every user-facing
|
|
screen, 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`](../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, typography, motion | §37, §38, §40, §41 |
|
|
| Colour | §39 — **superseded by [`BRAND_GUIDE.md`](BRAND_GUIDE.md)**; see [Colour](#colour) below |
|
|
| 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](../planning/PRODUCT_PLAN.md). 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". Every **screen** that shows a
|
|
fertility estimate carries the not-contraception line, which is where
|
|
[§18](../planning/PRODUCT_PLAN.md) scopes it — "near fertility features and in
|
|
About". Notification copy is the deliberate exception: it says "Estimated
|
|
fertile window" and stops, because a disclaimer is a long sentence about
|
|
fertility on a lock screen.
|
|
|
|
**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.
|
|
|
|
**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](../planning/PRODUCT_PLAN.md)).
|
|
|
|
**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.
|
|
|
|
## Colour
|
|
|
|
The palette is [`BRAND_GUIDE.md`](BRAND_GUIDE.md), supplied by the project owner
|
|
on 2026-08-18. It **supersedes** the sketch in
|
|
[`../planning/PRODUCT_PLAN.md` §39](../planning/PRODUCT_PLAN.md) where the two
|
|
differ, and one difference is worth stating rather than leaving to be discovered:
|
|
|
|
> **§39 asks for a muted teal or sage fertile window. The brand guide is plum
|
|
> and rose throughout, and gives fertility its own pink (`#E891AE`).**
|
|
|
|
The guide wins — it is newer, it is the owner's, and a teal accent inside this
|
|
identity would look borrowed from another app. What §39 was protecting is kept
|
|
by other means, and this is the part that matters: **the calendar's states
|
|
differ in shape, not only in colour.** A solid disc, a dotted ring, a continuous
|
|
ring and a star stay tellable apart in greyscale and to a colourblind user, which
|
|
is what §43 actually requires. Colour is the second signal here, never the only
|
|
one.
|
|
|
|
The two rules from §39 that survive unchanged, because they are product
|
|
decisions rather than colour preferences:
|
|
|
|
- **Period state** is a sophisticated rose or plum — never graphic blood-red.
|
|
- **Nothing reads as "safe".** The fertile window is pink rather than green for
|
|
the same reason the labels say "Lower likelihood": this app never gives
|
|
permission.
|
|
|
|
Everything goes through Material 3 colour roles and centralized tokens in
|
|
`core/designsystem`. **No hard-coded colours in a Composable** — 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.
|
|
|
|
## A row for an unbuilt feature is absent, not disabled
|
|
|
|
Settings is built as `PRODUCT_PLAN.md` §36 structures it, and it shows only the
|
|
sections that have something behind them. Privacy & Security arrives with the
|
|
issues that fill it; Appearance and Premium with their batches.
|
|
|
|
The alternative — every §36 row present and greyed out — is worse here than in
|
|
most apps. A disabled *Export My Data* is a promise with no delivery date, and a
|
|
disabled *Delete My Data* tells a user that the control over their own health
|
|
data exists and that they may not have it. In a product whose entire argument is
|
|
that the data is theirs, that is a bad first thing to say.
|
|
|
|
Absent is honest, and the tracker is where the plan lives — which is the same
|
|
rule [`../data/README.md`](../data/README.md) applies to placeholder images, for
|
|
the same reason: something that looks finished outlives the issue that would
|
|
have replaced 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 real now, and the replaceable names are why that was cheap
|
|
|
|
Every illustration in the app is the owner's own drawing, in a matched light and
|
|
dark pair, in
|
|
[`core/designsystem/src/main/res/`](../../core/designsystem/src/main/res/). The
|
|
sources and the decisions behind them are in [`dist/`](dist/).
|
|
|
|
**That swap changed one function body per illustration and no screen's layout**,
|
|
because PRODUCT_PLAN.md §42's advice was followed literally when there was
|
|
nothing to draw with: polished placeholders behind replaceable names. The names
|
|
did not change when the drawings did. That is the whole argument for the rule,
|
|
and it is worth keeping for the artwork still to come.
|
|
|
|
**These are raster, and §42 still holds.** Its preference list ends "avoid
|
|
*unnecessary* raster imagery", and it offers placeholder vectors as the fallback
|
|
for artwork that does not exist. The artwork exists, and it is gradient
|
|
landscapes with glow and depth — there is no honest `VectorDrawable` of one. The
|
|
whole set costs 130 KB of the release APK.
|
|
|
|
**The calendar markers stay vectors**, and so does `CycleProgressMark`. They are
|
|
drawn *from data* — a marker's shape is what a day is, and the progress arc is
|
|
where the user actually stands in their cycle. No amount of good artwork makes
|
|
those images.
|
|
|
|
**The theme swap is resource resolution, not a branch.** Each drawable has a
|
|
`drawable-night/` twin, so Android picks it and the artwork follows a device
|
|
theme change with no code of ours involved. The cost is that a missing night
|
|
asset is invisible in light mode, which is why the preview pair in
|
|
`OnboardingPreviews.kt` has one entry per illustration.
|
|
|
|
Still placeholder: **the launcher icon**, which keeps its simplified vector
|
|
because the supplied emblem has content close to its edges and an adaptive icon
|
|
masks about a quarter of the canvas away, so dropping it in unmodified would crop
|
|
the shield. Fitting it to the safe zone is issue #30.
|
|
|
|
### What the artwork has to do
|
|
|
|
Two things at once, stated by the owner and recorded here because it is the brief
|
|
every remaining art issue is judged against: **welcoming, and visibly private.**
|
|
Not alternating between them screen by screen — a warm opening followed by a bare
|
|
form is how onboarding reads today, and the privacy idea currently appears on
|
|
exactly one screen out of seven.
|
|
|
|
Welcoming is the harder half to get right without breaking [Tone](#tone): warmth
|
|
here means an app that looks like it was made with care, never one that
|
|
celebrates a period or decorates it. Private is already half-solved in the
|
|
vocabulary — `PrivacyIllustration` encloses a ring in a shield, and enclosure
|
|
reads as protection without reaching for a padlock, which would read as a
|
|
security product rather than a calm one.
|
|
|
|
The onboarding brief has been answered: eight illustrations, both themes, now
|
|
shipping in the app. [`dist/`](dist/) holds the sources and the reasoning — no
|
|
text painted into the pixels, no step numbers, and a forecast illustration that
|
|
names no fields so it cannot contradict the live one beside it.
|
|
|
|
The art still outstanding is tracked as issues #30 to #32, not listed here.
|
|
|
|
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`](../architecture/README.md)
|
|
- Scope and audience — [`../planning/PROJECT_PLAN.md`](../planning/PROJECT_PLAN.md)
|