Components¶
Every reusable UI primitive in frontend/components/ui/. These are theme-aware, presentation-only — no service calls, no business logic. Feature composites belong in higher-level folders (components/auth/, components/chat/, etc.).
This page is the catalog. For implementation details (the makeStyles(theme) factory pattern, how useTheme() flows in), see Frontend → Architecture.
Foundational¶
md border radius.
surfaceContainerHighest and label to primary — no border swap.
sm radius. Supports campfire-palette tinting for categorical accent.
Layout & containers¶
expo-blur on native, CSS backdrop-filter on web. Use sparingly — every glass surface is a moment.
isNavHidden via SheetContext). Pair with rich content that wants the full screen.
footer element in a standard action bar (surface background, 16 padding, safe-area bottom inset) and pads the body to clear it. footerBare opts out and renders the footer element as-is; footerOptions.keyboardOffset tunes keyboard avoidance. See SheetProps / ScreenSheetProps.
right) or several (rightActions, ~46px each); a single action can full-swipe to fire. Snaps open and holds until tapped or another row opens. Built on Reanimated + gesture-handler.
Platform.OS handles dispatch.
Feedback¶
ToastContext. Single queue. Variants: success, warning, error, info.
useConfirm() (planner) and ConfirmSheet; destructive renders the confirm CTA in the danger tone.
Media & social¶
expo-image with memory-disk caching and a stable cacheKey derived from the URL path so cache entries survive base_url drift (DHCP, CDN swap). Pass cacheKey explicitly when the immutable storage key is available directly (e.g. chat metadata.images.keys).
sm, md, lg.
Decorative & motion¶
Composition rules¶
- Use the primitive as-is. If you need a variant, add it to the primitive — don't fork.
- Stylesheet factories. Every component declares
const makeStyles = (theme) => StyleSheet.create({...})at the top. Components readthemeviauseTheme()once and memoize the resulting sheet — the factory runs on theme change, not on every render. - Compose, don't extend. Build feature components by composing primitives. A
ChatMessageBubbleis aViewwrappingUserAvatar+body-lgtext + (optional)Toast-style reaction tray. It doesn't subclass anything.
Buttons and i18n length¶
Translated labels can be 20–40% longer than English (German for compound nouns, Finnish for case suffixes, Polish for long roots). The button primitive uses a three-tier graceful degradation so non-English labels never overrun siblings and almost never truncate with ….
Tier 1 — Standard row (default)¶
Wrap a row of buttons in <ButtonRow> from frontend/components/ui/Button.tsx. Every child is forced to flex: 1, so widths stay matched and side-by-side buttons render at the same font size. The label uses numberOfLines={1} + ellipsizeMode="tail" + flexShrink: 1. (We deliberately do not use adjustsFontSizeToFit — it shrinks each Text independently and produces mismatched font sizes across buttons in a row.)
Tier 2 — Compact size¶
For rows known to be tight even in English ("Accept" / "Decline" + "Block"; "Join" / "Chat" + "Manage"), pass size="compact" to the children. The primitive shrinks font (15→12) and horizontal padding (20→12), buying ~25% horizontal headroom before any stacking kicks in.
<ButtonRow>
<Button size="compact" title={t('friend_detail.message')} ... />
<Button size="compact" title={t('friend_detail.profile')} ... />
<Button size="compact" title={t('common.share')} ... />
</ButtonRow>
Tier 3 — Auto-stack¶
If the natural label widths still wouldn't fit at the container's available width, <ButtonRow> flips to a vertical column (each child rendered full-width). Triggered only when text truly overflows — a small slack threshold prevents cosmetic stacking when labels are at the edge.
Set autoStack={false} if you've already designed for the worst case and want the row to stay horizontal regardless.
Authoring rule¶
Action-button text in feature components (actionBtnText, cancelBtnText, etc.) should follow the same pattern: add flexShrink: 1 to the style and pass numberOfLines={1} ellipsizeMode="tail" on the <Text>. Prefer <Button> + <ButtonRow> over hand-rolled <TouchableOpacity> for any new action surface.
Adding a primitive¶
- Drop the file in
frontend/components/ui/. - Follow the
makeStyles(theme)pattern. No service calls inside, no business logic — just rendering. - Add an entry to this page in the same PR, under the appropriate grouping above. Include: purpose, key variants, sizing if relevant.
- If the new primitive uses a previously-unused color token, also update Color.
- If it introduces a new motion duration or easing, also update Motion.
Storybook?¶
Not today. The catalog above + the source files are the canonical references. A live Storybook is a candidate future addition; for now, pull the component into a temporary playground screen if you need to iterate visually.