coderef

Bear

Bear's iOS app is a writer's instrument disguised as a design system. The whole product is built around one idea: the prose is the interface.
DownloadRaw
Bear

Build with the Bear look.

Palette, type and components from this DESIGN.md, applied to a sample hero.

Palette

At a glance

Theme
light
Typefaces
Inter, JetBrains Mono, Lora
Sections
7
Length
4,412 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · Bear

Light and dark previews, tokens and a ready-to-use package, generated from this DESIGN.md. No AI involved: the same file always produces the same output.

Generating design system…
DownloadRaw

Bear (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports Bear’s visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, the live-Markdown renderer, paste-ready @Composables, navigation, motion, and haptics.

Why a Compose guide for an iOS-referenced app? The DESIGN.md tokens are platform-neutral. This file keeps the visual identity (Bear’s calm charcoal canvas, the single red→orange brand gradient, inline #hashtags as the filing system, live in-place Markdown) while making everything idiomatic Android — a ModalNavigationDrawer for the tag sidebar, a FloatingActionButton for compose, sp/dp instead of pt.

Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, and Coil 2.6+ if you render embedded images. No color extraction — Bear’s palette is fixed. Bear is theme-driven; the default dark “Charcoal” scheme is provided, never true black.

1. Color Tokens

// ui/theme/BearColors.kt
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Brush

object BearColors {
    // Canvas & Surfaces (Light)
    val Canvas       = Color(0xFFFFFFFF)
    val SurfaceLight = Color(0xFFF6F6F4)
    val PressedLight = Color(0xFFECECE8)
    val DividerLight = Color(0xFFE6E6E2)

    // Canvas & Surfaces (Dark — "Charcoal")
    val CanvasDark = Color(0xFF21252B) // Bear's signature charcoal — NOT pure black
    val Surface1   = Color(0xFF282C34)
    val Surface2   = Color(0xFF2F343D)
    val Divider    = Color(0xFF353A42)

    // Text
    val TextPrimary    = Color(0xFFE8EAED) // dark
    val TextPrimaryLt  = Color(0xFF2A2C33) // light — warm near-black, NOT pure black
    val TextSecondary  = Color(0xFF9DA3AD)
    val TextTertiary   = Color(0xFF6B7280)

    // Brand (the single accent)
    val BearRed        = Color(0xFFE0566F)
    val BearOrange     = Color(0xFFFF8A65)
    val BearRedPressed = Color(0xFFC8485E)
    val Link           = Color(0xFF5AAFEF)

    // Tag / highlight accents
    val TagBlue   = Color(0xFF5AAFEF)
    val TagGreen  = Color(0xFF57C98B)
    val TagYellow = Color(0xFFF2C14E)
    val TagPurple = Color(0xFFB08CF0)
    val Highlight = Color(0xFF4A431F) // dark

    // Semantic
    val Error   = Color(0xFFF2545B)
    val Success = Color(0xFF57C98B)

    // The one brand gradient — icon, FAB, checkbox fill, selection
    val BrandGradient = Brush.linearGradient(listOf(BearRed, BearOrange))
}

Wire it into both schemes. Bear is theme-driven; the dark scheme uses the signature #21252B charcoal, never true black.

// ui/theme/Theme.kt
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable

private val BearLight = lightColorScheme(
    primary        = BearColors.BearOrange,   // interactive glyph color
    onPrimary      = Color(0xFFFFFFFF),
    background      = BearColors.Canvas,
    onBackground    = BearColors.TextPrimaryLt,
    surface         = BearColors.SurfaceLight,
    onSurface       = BearColors.TextPrimaryLt,
    surfaceVariant  = BearColors.PressedLight,
    outline         = BearColors.DividerLight,
    error           = BearColors.Error,
)

private val BearDark = darkColorScheme(
    primary        = BearColors.BearOrange,
    onPrimary      = Color(0xFFFFFFFF),
    background      = BearColors.CanvasDark,
    onBackground    = BearColors.TextPrimary,
    surface         = BearColors.Surface1,
    onSurface       = BearColors.TextPrimary,
    surfaceVariant  = BearColors.Surface2,
    outline         = BearColors.Divider,
    error           = BearColors.Error,
)

@Composable
fun BearTheme(
    dark: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit,
) = MaterialTheme(
    colorScheme = if (dark) BearDark else BearLight,
    typography = BearTypography,
    content = content,
)

2. Typography

Bear’s prose family is user-switchable: Inter (default sans), Lora (serif), JetBrains Mono (mono theme + all code) — all SIL OFL, drop the TTFs in res/font/. Respect the user’s choice globally via a CompositionLocal; fenced code always renders JetBrains Mono. Body 400, headings 700; reading rhythm, not UI rhythm.

// ui/theme/BearType.kt
import androidx.compose.material3.Typography
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp

val Inter = FontFamily(
    Font(R.font.inter_regular,   FontWeight.Normal),
    Font(R.font.inter_medium,    FontWeight.Medium),
    Font(R.font.inter_semibold,  FontWeight.SemiBold),
    Font(R.font.inter_bold,      FontWeight.Bold),
    Font(R.font.inter_extrabold, FontWeight.ExtraBold),
)
val Lora = FontFamily(
    Font(R.font.lora_regular, FontWeight.Normal),
    Font(R.font.lora_italic,  FontWeight.Normal, FontStyle.Italic),
    Font(R.font.lora_bold,    FontWeight.Bold),
)
val JetBrainsMono = FontFamily(
    Font(R.font.jetbrains_mono_regular, FontWeight.Normal),
    Font(R.font.jetbrains_mono_medium,  FontWeight.Medium),
)

enum class BearFontFamily(val family: FontFamily) {
    Sans(Inter), Serif(Lora), Mono(JetBrainsMono)
}
// Provide at the app root; switch from Settings — applies to all prose
val LocalBearFont = compositionLocalOf { BearFontFamily.Sans }

// Named ramp — mirrors DESIGN.md §3 exactly (pt → sp 1:1). Default = Inter.
object BearText {
    val LargeTitle = TextStyle(Inter, fontWeight = FontWeight.ExtraBold, fontSize = 32.sp, lineHeight = 38.sp, letterSpacing = (-0.6).sp)
    val NoteTitle  = TextStyle(Inter, fontWeight = FontWeight.Bold,      fontSize = 24.sp, lineHeight = 30.sp, letterSpacing = (-0.3).sp)
    val H2         = TextStyle(Inter, fontWeight = FontWeight.Bold,      fontSize = 19.sp, lineHeight = 25.sp, letterSpacing = (-0.2).sp)
    val H3         = TextStyle(Inter, fontWeight = FontWeight.Bold,      fontSize = 17.sp, lineHeight = 23.sp, letterSpacing = (-0.1).sp)
    val Subhead    = TextStyle(Inter, fontWeight = FontWeight.SemiBold,  fontSize = 17.sp, lineHeight = 23.sp)
    val Body       = TextStyle(Inter, fontWeight = FontWeight.Normal,    fontSize = 16.sp, lineHeight = 25.sp)
    val BodyDense  = TextStyle(Inter, fontWeight = FontWeight.Normal,    fontSize = 15.sp, lineHeight = 22.sp)
    val Quote      = TextStyle(Lora,  fontWeight = FontWeight.Normal,    fontSize = 17.sp, lineHeight = 26.sp, fontStyle = FontStyle.Italic)
    val Preview    = TextStyle(Inter, fontWeight = FontWeight.Normal,    fontSize = 14.sp, lineHeight = 20.sp)
    val Meta       = TextStyle(Inter, fontWeight = FontWeight.Normal,    fontSize = 14.sp, lineHeight = 20.sp)
    val Tag        = TextStyle(Inter, fontWeight = FontWeight.Medium,    fontSize = 14.sp, lineHeight = 20.sp)
    val CodeInline = TextStyle(JetBrainsMono, fontWeight = FontWeight.Normal, fontSize = 14.sp, lineHeight = 20.sp)
    val CodeBlock  = TextStyle(JetBrainsMono, fontWeight = FontWeight.Normal, fontSize = 13.sp, lineHeight = 20.sp)
    val Caption    = TextStyle(Inter, fontWeight = FontWeight.Medium,    fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.1.sp)
    val TabLabel   = TextStyle(Inter, fontWeight = FontWeight.Medium,    fontSize = 13.sp, lineHeight = 17.sp, letterSpacing = 0.1.sp)
}

// Helper: rebind a base style onto the user's chosen prose family
fun TextStyle.withBearFamily(f: BearFontFamily): TextStyle = copy(fontFamily = f.family)

// Map onto Material 3 slots so stock components inherit the brand
val BearTypography = Typography(
    headlineLarge = BearText.LargeTitle,
    headlineMedium = BearText.NoteTitle,
    titleMedium   = BearText.Subhead,
    bodyMedium    = BearText.Body,
    labelSmall    = BearText.TabLabel,
)

3. Signature Components

Live Markdown Renderer

Bear’s signature: render Markdown in-place using an AnnotatedString, dimming the syntax marker rather than hiding structure.

import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle

@Composable
fun MarkdownLine(raw: String) {
    when {
        raw.startsWith("# ")  -> Text(buildAnnotatedString {
            withStyle(SpanStyle(color = BearColors.TextTertiary)) { append("# ") }
            append(raw.drop(2))
        }, style = BearText.NoteTitle, color = BearColors.TextPrimary)

        raw.startsWith("## ") -> Text(buildAnnotatedString {
            withStyle(SpanStyle(color = BearColors.TextTertiary)) { append("## ") }
            append(raw.drop(3))
        }, style = BearText.H2, color = BearColors.TextPrimary)

        raw.startsWith("> ")  -> QuoteBlock(raw.drop(2))
        raw.startsWith("- [") -> CheckboxRow(
            initiallyChecked = raw.startsWith("- [x]"),
            label = raw.replace(Regex("^- \\[.\\]\\s*"), ""),
        )
        else -> Text(inlineStyled(raw), style = BearText.Body, color = BearColors.TextPrimary)
    }
}

/** Tints #hashtags red and [[wiki]] links blue inline. */
fun inlineStyled(s: String): AnnotatedString = buildAnnotatedString {
    val token = Regex("#[\\w/]+|\\[\\[[^\\]]+\\]\\]")
    var last = 0
    token.findAll(s).forEach { m ->
        append(s.substring(last, m.range.first))
        val t = m.value
        val style = if (t.startsWith("#")) SpanStyle(color = BearColors.BearRed)
                    else SpanStyle(color = BearColors.Link)
        withStyle(style) { append(t) }
        last = m.range.last + 1
    }
    append(s.substring(last))
}

Note Editor (Title + Body)

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun NoteEditor(title: String, onTitleChange: (String) -> Unit, lines: List<String>) {
    LazyColumn(
        Modifier.fillMaxSize(),
        contentPadding = PaddingValues(horizontal = 22.dp, vertical = 4.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp),
    ) {
        item {
            BasicTextField(
                value = title, onValueChange = onTitleChange,
                textStyle = BearText.NoteTitle.copy(color = BearColors.TextPrimary),
                decorationBox = { inner ->
                    if (title.isEmpty()) Text("Title", style = BearText.NoteTitle, color = BearColors.TextTertiary)
                    inner()
                },
            )
        }
        items(lines) { MarkdownLine(it) }
    }
}

Checkbox Row

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Icon
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.draw.clip
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextDecoration

@Composable
fun CheckboxRow(initiallyChecked: Boolean, label: String) {
    var checked by remember { mutableStateOf(initiallyChecked) }
    val haptics = LocalHapticFeedback.current

    Row(
        Modifier
            .fillMaxWidth()
            .clickable {
                haptics.performHapticFeedback(HapticFeedbackType.LongPress)
                checked = !checked
            },
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(10.dp),
    ) {
        Box(
            Modifier
                .size(19.dp)
                .clip(RoundedCornerShape(5.dp))
                .then(if (!checked) Modifier.border(2.dp, BearColors.TextTertiary, RoundedCornerShape(5.dp)) else Modifier),
            contentAlignment = Alignment.Center,
        ) {
            AnimatedVisibility(checked, enter = fadeIn(tween(150)), exit = fadeOut(tween(150))) {
                Box(Modifier.matchParentSize().background(BearColors.BrandGradient), contentAlignment = Alignment.Center) {
                    Icon(Icons.Filled.Check, null, tint = Color(0xFFFFFFFF), modifier = Modifier.size(11.dp))
                }
            }
        }
        Text(
            label,
            style = BearText.Body,
            color = if (checked) BearColors.TextTertiary else BearColors.TextPrimary,
            textDecoration = if (checked) TextDecoration.LineThrough else TextDecoration.None,
        )
    }
}

Quote Block

@Composable
fun QuoteBlock(text: String) {
    Row(Modifier.fillMaxWidth()) {
        Box(Modifier.width(3.dp).fillMaxHeight().background(BearColors.BearOrange))
        Text(
            text,
            style = BearText.Quote,
            color = BearColors.TextSecondary,
            modifier = Modifier.padding(start = 14.dp, top = 2.dp, bottom = 2.dp),
        )
    }
}

Code Block

import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.rememberScrollState

@Composable
fun CodeBlock(language: String?, code: String) {
    Column(
        Modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(8.dp))
            .background(BearColors.Surface1),
    ) {
        if (language != null) {
            Text(
                language, style = BearText.Caption, color = BearColors.TextTertiary,
                modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp, top = 8.dp),
            )
        }
        Text(
            code,
            style = BearText.CodeBlock,
            color = BearColors.TextPrimary,
            modifier = Modifier.horizontalScroll(rememberScrollState()).padding(14.dp),
        )
    }
}

Tag Sidebar Row + Compose FAB

import androidx.compose.material.icons.filled.Tag
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.FloatingActionButton

@Composable
fun TagRow(title: String, indent: Int = 0, active: Boolean = false) {
    Row(
        Modifier
            .fillMaxWidth()
            .height(36.dp)
            .background(if (active) BearColors.Surface2 else Color.Transparent)
            .padding(start = (14 + indent * 18).dp, end = 12.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(8.dp),
    ) {
        Icon(Icons.Filled.Tag, null, tint = BearColors.BearRed, modifier = Modifier.size(16.dp))
        Text(title, style = BearText.Tag, color = BearColors.TextPrimary)
    }
}

@Composable
fun ComposeFab(onClick: () -> Unit) {
    FloatingActionButton(
        onClick = onClick,
        containerColor = Color.Transparent,
        contentColor = Color(0xFFFFFFFF),
        modifier = Modifier
            .size(56.dp)
            .clip(RoundedCornerShape(28.dp))
            .background(BearColors.BrandGradient),
    ) {
        Icon(Icons.Filled.Edit, contentDescription = "New note", modifier = Modifier.size(24.dp))
    }
}

4. Navigation

Bear has minimal chrome: a slide-in tag sidebar and a bottom info bar (no Material tab bar). Model the sidebar as a ModalNavigationDrawer (SurfaceGray, 280.dp) and the editor’s bottom strip as a custom BottomAppBar. There is no tint pill — interactive glyphs are Bear Orange.

import androidx.compose.material3.BottomAppBar
import androidx.compose.material.icons.filled.*

@Composable
fun EditorInfoBar(words: Int, chars: Int) {
    BottomAppBar(
        containerColor = BearColors.Surface1.copy(alpha = 0.92f),
        tonalElevation = 0.dp,
    ) {
        Text(
            "$words words · $chars chars",
            style = BearText.Caption,
            color = BearColors.TextTertiary,
            modifier = Modifier.padding(start = 22.dp),
        )
        Spacer(Modifier.weight(1f))
        Row(horizontalArrangement = Arrangement.spacedBy(22.dp), modifier = Modifier.padding(end = 22.dp)) {
            listOf(Icons.Filled.GridOn, Icons.Filled.Visibility, Icons.Filled.IosShare, Icons.Filled.Info)
                .forEach { Icon(it, null, tint = BearColors.BearOrange, modifier = Modifier.size(20.dp)) }
        }
    }
}

The tag sidebar (ModalDrawerSheet, 280.dp, Surface1 background) holds smart filters (Notes, Untagged, Todo, Today, Archive, Trash) as 40.dp rows, then the #hashtag tree — each tag row 36.dp with a leading 16.dp Tag glyph tinted BearRed, nested children indented 18.dp behind a disclosure chevron. Active row uses Surface2. Material’s drawerState gives the edge-swipe + 250ms open/close for free.

5. Motion

Bear motion is quiet — 120–300ms ease-out, never aggressive. The Markdown marker fade never causes a layout jump.

Moment Compose recipe
Checkbox toggle AnimatedVisibility of the gradient fill fadeIn(tween(150)); label TextDecoration.LineThrough flips with it
Markdown marker fade recompose AnnotatedString; animate marker SpanStyle alpha via animateColorAsState(tween(120)) — opacity only
Tag tap → list filter swap list state inside Crossfade(tween(200))
Nested tag disclosure chevron animateFloatAsState 0 → 90° tween(150); children expandVertically(tween(200)) + fadeIn
Compose FAB press scale animateFloatAsState → 0.92; then nav push tween(280)
Page navigation Nav NavHost slide push tween(300)
Sidebar swipe ModalNavigationDrawer drawerState — 1:1 drag, 50% threshold, spring(dampingRatio = 0.8f)
Text selection brand-red @ ~15% overlay appears instantly (no animation)
// Checkbox fill — the canonical Bear toggle
AnimatedVisibility(
    visible = checked,
    enter = fadeIn(tween(150)),
    exit = fadeOut(tween(150)),
) { /* gradient box + check */ }

Haptics: prefer LocalHapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) for the soft impact on checkbox toggle, tag tap, and FAB press. Use HapticFeedbackConstants.CONFIRM for export-complete. Auto-save is silent — no motion, no haptic; only a “Saved” snackbar on manual save or error.

6. Icons

Bear ships custom glyphs; the closest first-party set is androidx.compose.material:material-icons-extended. The #hashtag glyph maps to Tag; the page-emoji concept does not exist in Bear (it is text-only Markdown).

Purpose SF Symbol (iOS) Material Icon (Compose)
Back (“Notes”) chevron.left Icons.Filled.ArrowBack
Info (top nav) info.circle Icons.Filled.Info
Format / Markdown textformat Icons.Filled.TextFields
More ellipsis Icons.Filled.MoreHoriz
Compose FAB square.and.pencil Icons.Filled.Edit
Checkbox (checked) checkmark Icons.Filled.Check
Tag / #hashtag number Icons.Filled.Tag
Smart filter — Notes note.text Icons.Filled.Description
Smart filter — Todo checkmark.circle Icons.Filled.CheckCircle
Smart filter — Today calendar Icons.Filled.CalendarToday
Smart filter — Archive archivebox Icons.Filled.Archive
Smart filter — Trash trash Icons.Filled.Delete
Search magnifyingglass Icons.Filled.Search
Info-bar — Table tablecells Icons.Filled.GridOn
Info-bar — Preview eye Icons.Filled.Visibility
Info-bar — Export square.and.arrow.up Icons.Filled.IosShare
Disclosure (nested tag) chevron.right Icons.Filled.ChevronRight (rotate 90°)
Pin note pin Icons.Filled.PushPin
Link / wiki-link link Icons.Filled.Link

7. Minimum SDK & Accessibility Notes

  • minSdk 24 (Compose floor is 21; modern motion is comfortable at 24). compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+.
  • Edge-to-edge: call enableEdgeToEdge(); the dark “Charcoal” canvas wants light-content system bars (dark-content on the light theme). The editor must keep the keyboard accessory row visible — use Modifier.imePadding() on the editor LazyColumn.
  • Font scaling: sp honors the user’s font scale — keep it on note title, headings, body, quote, preview, meta, code (it stays monospace as it scales). Pin layout-sensitive text (tag tokens, 12sp info-bar caption, sidebar filter labels, 13sp tab labels) via dp-derived sizes or a fixed-fontScale LocalDensity.
  • User font choice: read LocalBearFont everywhere for prose and apply style.withBearFamily(...) — never hard-code Inter; Lora/JetBrains Mono must apply globally. Fenced code is always JetBrains Mono.
  • TalkBack: label checkbox rows with Modifier.semantics { role = Role.Checkbox; stateDescription = if (checked) "Completed" else "Not completed" }; label #hashtag spans “Tag: {name}, double-tap to filter” via ClickableText + LinkAnnotation; mark note titles as heading(). The marker dim is visual only — expose the styled text, not the raw #/**.
  • Touch targets: Material guidance is 48.dp. Give the 19.dp checkbox, 16.dp tag glyph, and 14.dp disclosure chevron a 48.dp hit area via padding; sidebar rows are 36.dp tall but full-row tappable; the FAB is 56.dp.
  • Contrast: #2A2C33 on #FFFFFF and #E8EAED on #21252B pass WCAG AA for body at 16sp; #E0566F hashtags pass AA on canvas. Validate any custom tag color against its surface.
  • Reduce motion: when Settings.Global.ANIMATOR_DURATION_SCALE == 0, disable the disclosure rotation and checkbox cross-fade — substitute an instant state change; keep the selection highlight (it conveys state).
  • Dark mode: invert via the Dark* palette — #21252B, NOT true black; #2A2C33 text becomes #E8EAED. Shadows are nearly invisible on dark, so floating panels add a 1dp Divider border as the elevation cue. Do not enable Material You dynamicColorScheme() — Bear’s calm-canvas + single-red-gradient identity must hold regardless of wallpaper. Model each shipped Bear theme as a token object; the brand gradient is the one constant across all of them.

Brand names describe the visual style only; coderef is not affiliated with or endorsed by any brand listed. Licenses