Sleek Coach Docs

Design Conventions

The presentation rulebook for the Sleek Coach mobile app: how screens and components look, how they use the theme, and what keeps the app feeling like one product. Treat this file as the source of truth for design review: the design-review skill walks it, and the pre-commit design agent checks UI-touching changes against it. Every rule below was earned by a real cleanup pass (the token sweep that removed hardcoded rgba() borders, the double-divider fix above the chat input, the semantic palette that sat unused while components hardcoded #F59E0B).

Prime directive

Every visual value flows from src/theme/. Components consume tokens; they never define them. A color, radius, spacing, or font size written inline in a component is a fork of the design system, and forks are how the app stops feeling like one product.

Boundaries: docs/AI_CONVENTIONS.md owns the coach AI behavior and the chat streaming contract; this doc owns how things look and feel. Cross-reference, never duplicate.

1. Colors come only from the theme

Use theme.colors via useAppTheme() (screens) or useTheme<AppTheme>() (components, when the custom semantic colors are needed). No hex or rgba() literals outside src/theme/.

Why: the theme guarantees light/dark parity and Material 3 contrast ratios; a hardcoded rgba(0,0,0,0.1) border reads fine in light mode and disappears in dark mode.

Specific assignments, so nobody has to guess:

2. Spacing and radii come from the scales

Padding, margin, and gap values come from the spacing scale (xs 4, sm 8, md 16, lg 24, xl 32, xxl 48). Corner radii come from the borderRadius scale (xs 4, sm 8, md 12, lg 16, xl 24, full), both in src/theme/spacing.ts. Fixed-size pills and circles use borderRadius.full, not a hand-computed half-height. This applies to relocated code too: a refactor that moves an old off-scale value into a new file is the moment to fix it, not to carry it over.

Why: one rhythm knob. When every gap is a multiple from one scale, screens align without anyone measuring, and a future density change is a one-file edit.

3. Type comes from Paper variants or the typography presets

Use Paper’s <Text variant="..."> first. When a raw style is unavoidable (a TextInput, a third-party component), compose from the typography presets in src/theme/typography.ts. No literal fontSize in components. Markdown styling comes from createMarkdownStyles in src/theme/markdown.ts, never a per-component style object.

Why: a single type ramp. Literal font sizes drift one screen at a time until headings disagree across tabs.

4. Reuse the primitives before writing one-offs

Reach for react-native-paper components and the app’s own primitives in src/components/ui/ (Button, Card, Chip, Input, EmptyState, LoadingSpinner, ErrorBoundary, TargetProgressRow) before writing a new component. If a pattern appears a third time, extract it into src/components/ui/.

Why: reuse carries accessibility, theming, and dark mode for free; one-offs carry none of them.

5. Static styles in StyleSheet.create, theme values in style arrays

StyleSheet.create holds static layout (flex, dimensions, static spacing). Theme-dependent values are applied inline via style arrays: style={[styles.header, { borderBottomColor: theme.colors.outlineVariant }]}. Do not put theme colors inside StyleSheet.create, and do not rebuild whole style objects per render.

Why: StyleSheet gives render performance for the static part; the array keeps the dynamic part reactive to theme changes. This is the pattern ChatInput established.

6. Shared UX constants live in src/constants/

Tuned values that shape behavior (chat message length limit, scroll thresholds, stream throttles, history caps) live in src/constants/ (chat.ts, config.ts), imported where used. Never inline them in a component.

Why: the chat scroll threshold and throttle were tuned deliberately in PRs #47 through #49; a value with history behind it must be findable and must not be re-tuned by accident in a copy.

7. Dark mode is a first-class theme

Every new surface is verified in dark mode before it ships (theme.dark is the switch; the ThemeContext supports light, dark, and system). If a color pair looks wrong in dark mode, the fix is choosing the right token, not adding a theme.dark ? ternary with literals.

Why: roughly half of real sessions run dark. Rule 1 makes this nearly automatic; this rule makes it checked.

8. Accessibility is part of done

Touch targets are at least 44pt. Icon-only controls carry an accessibilityLabel. Text is not embedded in images. Interactive states (disabled, loading) are visually distinct using theme tokens.

Why: a coaching app is used mid-workout, tired, and one-handed; generous targets and labeled controls are function, not polish.

Review checklist

Walk this in order against the diff. Run the mechanical greps as written (scope them to changed files when the diff is small); the judgment items require reading the full post-change files, not just the hunks.

Mechanical:

  1. Hex literals outside the theme (expect no hits): grep -rEn "'#[0-9A-Fa-f]{3,8}'" apps/mobile/src --include='*.tsx' --include='*.ts' | grep -v __tests__ | grep -v src/theme
  2. rgb/rgba literals outside the theme (expect no hits): grep -rn 'rgba(\|rgb(' apps/mobile/src --include='*.tsx' | grep -v __tests__ | grep -v src/theme
  3. Raw font sizes (the grep is repo-wide; judge only hits in files the diff changed, each of which needs a justification or a preset; hits in untouched files are legacy context, not findings): grep -rEn 'fontSize: [0-9]' apps/mobile/src --include='*.tsx' | grep -v __tests__ | grep -v src/theme
  4. New raw radii or spacing in the diff (legacy raw values exist throughout the app, including coach and checkin surfaces; the rule is that a diff must not ADD new ones, and relocations count as additions). The grep covers shorthand and longhand properties; treat it as a floor, not a census: git diff origin/main... -- apps/mobile/src | grep -E '^\+.*(borderRadius|border[A-Za-z]*Radius|padding[A-Za-z]*|margin[A-Za-z]*|gap|rowGap|columnGap): [0-9]'

Judgment:

  1. Colors in the diff map to the assignments in rule 1 (dividers to outlineVariant, overlays to backdrop, semantics to success/warning/info).
  2. New components checked against src/components/ui/ and Paper for an existing primitive; third occurrences extracted.
  3. Theme-dependent values applied via style arrays, not baked into StyleSheet.create.
  4. Tuned constants imported from src/constants/, not inlined.
  5. Dark mode considered: no light-only assumptions, no literal-based theme.dark ternaries.
  6. Icon-only controls labeled; touch targets at least 44pt.