# Design > Documentation for Sundial 2.0, the design system of the Ash mobile app — warm sand scales, the lavender accent, semantic tokens, components, and Flutter theming. > ## Agent Instructions > This site documents Sundial 2.0, the design system of the Ash mobile app (Flutter). For implementing UI: use the Design System pages — color/typography tokens (foundations/*), Flutter theming (theming/*), and component APIs (components/*) — as your reference; every value there mirrors the Dart source in lib/design_system/. The Philosophy pages (philosophy/*) are design rationale written for humans: consult them when making design decisions, not for API lookups. Golden rules: never hardcode colors or text styles — read tokens via context.colorTheme and context.textTheme; prefer the 2.0 semantic names (contentPrimary, surfaceComponent, accentSolid…) over legacy contrast*/surface* getters and over SundialRef primitives; the accent is a single pale lavender solid that takes dark content — never use accent fills as text, icons, or borders (use contentAccent / borderSelected); brand surfaces are mode-invariant and pair with the contentOnBrand* inks; check lib/design_system/components/ before building custom widgets. # For Agents Source: https://design.slingshotai.com/agents/overview Machine-readable resources and usage guidance for AI agents working with Sundial This section gathers everything an AI agent needs to get up to speed on Sundial quickly and use it correctly. > [!NOTE] > > **One-line orientation.** Implementing UI → use the **Design System** section (tokens, theming, components); every value mirrors `lib/design_system/` in the Ash app. Making design *decisions* → consult **Craft & Foundations** (rationale, not API). Never hardcode colors or text styles. ## Which section to use | Task | Section | | - | - | | Implement a screen or component | Design System | | Look up a color / type / spacing value | Design System → Foundations | | Understand persona or brightness theming | Design System → Theming in Flutter | | Choose a layout, or judge whether an element belongs | Craft & Foundations | | Understand *why* a rule exists | Craft & Foundations | ## Golden rules * Read tokens from context: `context.colorTheme` and `context.textTheme`. Never hardcode `Color(0xFF…)` or a `TextStyle(fontSize: …)`. * Prefer **semantic** tokens ([Semantic Colors](/foundations/colors/semantic)) over `ReferenceColors` primitives. * Use `accent.*` tokens for persona-branded UI ([Accent Colors](/foundations/colors/accent)). * Check the [Components](/components/styled-card) section (and `lib/design_system/components/`) before building a custom widget. ## Machine-readable formats Every page on this site is available as clean Markdown for ingestion: * [**/llms.txt**](/llms.txt) — a compact, priority-ordered index of every page with descriptions (implementation pages first, design rationale last) * [**/llms-full.txt**](/llms-full.txt) — the entire documentation concatenated into a single file * **Any page as Markdown** — append `.md` to its URL (e.g. `/foundations/colors/semantic.md`), or use the **Copy page** button at the top-right of any page to copy or view the Markdown source * **Hosted MCP server** — this site exposes an MCP endpoint at `https://slingshot-ai.mintlify.io/mcp`, so agents can search and fetch pages as tools * **Open in an assistant** — the Copy page dropdown can open the current page directly in ChatGPT, Claude, and other assistants ## Standing agent instructions The site ships an instruction block (`docs.json` → `markdown.instructions`) that Mintlify injects into agent context automatically — it restates the section-routing and golden rules above, so a well-integrated agent receives them without reading this page. > [!NOTE] > > The design [Philosophy](/philosophy/overview) is deliberately kept in its own **Craft & Foundations** section, not mixed into the implementation reference — so agents pulling the Design System pages for an implementation task aren't distracted by design-rationale prose. Pull it in intentionally when you're making a design judgment. # Common Widgets Catalogue Source: https://design.slingshotai.com/appendix/common-widgets App-level reusable widgets in lib/widgets/common/ — adjacent to, but outside, the design system Beyond `lib/design_system/`, the app keeps reusable feature-agnostic widgets in [`lib/widgets/common/`](https://github.com/slingshot-ai/ash-mobile-app/tree/main/lib/widgets/common). They follow Sundial conventions (tokens via context, no hardcoded styles) but are **not** exported from `design_system.dart` — they're app-level building blocks. Check here before writing a new widget. ## Widgets | Widget | Purpose | | - | - | | `AppBarProgressIndicator` | Progress indicator rendered as the bottom of an app bar | | `AshStateLoader` | Loading state displaying the Ash typeface, optionally with text | | `BetaLabel` | Small “beta” pill to mark a feature | | `BlendMask` | Applies a `BlendMode` to its child (CSS mix-blend-mode analog; render object) | | `ElevatedButtonWithLoader` | `ElevatedButton` that swaps in a spinner while `loading` | | `CarouselListView` | Horizontal scrollable carousel (hooks-based) | | `ConversationCard` | Card representing a conversation entry | | `FeatureIntroModal` | Multi-page feature introduction modal (Riverpod consumer) | | `MarkdownText` | Text with markdown-like syntax support | | `OptionSelector` | Generic selectable list-of-options | | `PageIndicator` | Animated pagination dots | | `PageMenu` | Page-level menu | | `ProgressCounter` | Step/progress counter | | `RatingScale` | Numeric rating scale selector | | `ScrollableEdgeFade` | Fades the top/bottom edges of a scrollable | | `SizeReporter` | Reports child's laid-out size and screen position (render object) | | `StarRating` | Star-based rating display | | `TextContent` | Structured text content renderer (headings etc.) | | `TextFieldWithTranscription` | [`StyledTextField`](/components/styled-text-field) + audio-transcription button (Rive) | ## Dialogs — `common/dialogs/` | Item | Purpose | | - | - | | `CenterModalPopUp` | Full-screen blocking modal | | `showConfirmationDialog(…)` | Standard confirm/cancel dialog function | | `ForceUpdateModal` | Blocks the app when the version is unsupported | | `interstitialModalHeader(…)` | Header builder (image/icon) for `CenterModalPopUp` flows | | `showSnackBarMessage(…)` | Themed snackbar function | | `show_toast_message.dart` | Global toast overlay manager + show function | | `UrlInputDialog` | URL input dialog | ## Layout — `common/layout/` | Widget | Purpose | | - | - | | `ScrollViewWithBottomChild` | Scroll view that pins a `bottomChild` to the bottom | ## Related: animations Rive and animation helpers live in `lib/widgets/animations/` (`RiveLoader`, `rive_color_injection`, `AnimatedFade`, `AnimatedTextSequence`, `FadeInText`, `RevealAnimation`, `SlideAndFadeTransition`). Rive artwork, Lottie assets, and the bloom shader system are app-level concerns outside the design system. > [!NOTE] > > **Promotion path**: when a common widget proves broadly useful and stable, consider moving it into `lib/design_system/components/` with the full treatment — `Styled*`/`Ash*` naming, token discipline, docs page. See [Contributing](/patterns/contributing). # Architecture Source: https://design.slingshotai.com/architecture How Sundial is structured: a three-layer token system delivered through Flutter ThemeExtensions Sundial lives entirely in code, in the Ash mobile app repository at [`lib/design_system/`](https://github.com/slingshot-ai/ash-mobile-app/tree/main/lib/design_system). There is no separate token pipeline or generated theme — the Dart source is the single source of truth, and this documentation mirrors it. ## The three-layer color model Sundial's color system is built in three layers. Each layer references the one below it, and app code should almost always consume the **top** layers. ```mermaid theme={null} flowchart TB subgraph L1 ["Layer 1 — ReferenceColors (primitives)"] A["SundialRef: 12-step scales (Radix model)\nwarm sand light/dark · alpha inks · lavender light/dark\n+ 8 brand flats + cherry/amber status stops"] end subgraph L2 ["Layer 2 — DesignSystemColors (semantic)"] B["25 purpose-named tokens\ncontrast* · contrastVariant* · surface* · surfaceOpacity* · disabled* · badge* · error/warning\nlight and dark variants"] end subgraph L3 ["Layer 3 — AccentColors (persona)"] C["Single lavender accent (bright-solid grammar)\nlavenderLight · lavenderDark"] end A --> B A --> C C -->|"nested as colors.accent"| B B --> D["ThemeData extensions\ncontext.colorTheme"] ``` > [card] [Layer 1 — Reference](/foundations/colors/reference) > > Primitive swatches. Raw material — avoid using directly in app code. > [card] [Layer 2 — Semantic](/foundations/colors/semantic) > > Purpose-named tokens that adapt to light/dark mode. Use these. > [card] [Layer 3 — Accent](/foundations/colors/accent) > > Persona-driven accents, nested inside the semantic layer as `colors.accent`. ## Delivery: Flutter ThemeExtensions Sundial does not invent its own theming mechanism. It uses Flutter's built-in [`ThemeExtension`](https://api.flutter.dev/flutter/material/ThemeExtension-class.html) API — the idiomatic way to attach custom design tokens to a Material `ThemeData`: * **`DesignSystemColors extends ThemeExtension`** — the 25 semantic color tokens ([`tokens/semantic_colors.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/tokens/semantic_colors.dart)) * **`DesignSystemTypography extends ThemeExtension`** — the 15 text styles ([`tokens/typography.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/tokens/typography.dart)) Both extensions are installed on every `ThemeData` built by `AppTheme` and read back through two `BuildContext` extension getters: ```dart theme_extensions.dart theme={null} extension ThemeExtensions on BuildContext { /// Access the current design system colors. DesignSystemColors get colorTheme { return Theme.of(this).extension()!; } /// Access the current design system typography. DesignSystemTypography get textTheme { return Theme.of(this).extension()!; } } ``` Because both extensions implement `copyWith` and `lerp` for every field, Flutter can **animate theme changes** — switching brightness or persona smoothly interpolates every token. > [!NOTE] > > Why `ThemeExtension` and not an `InheritedWidget` or Riverpod provider? Tokens ride along with `Theme`, so any subtree can be re-themed with a plain `Theme` widget (see [ThemeOverride](/components/theme-override)), and all of Flutter's theme plumbing — animation, inheritance, `Theme.of` scoping — works for free. The design system itself has **no Riverpod dependency**. ## Directory layout ```text theme={null} lib/design_system/ ├── design_system.dart # Barrel export — import this one file ├── app_theme.dart # Builds ThemeData for light/dark × 5 accent palettes ├── theme_extensions.dart # context.colorTheme / context.textTheme accessors ├── symbols.dart # Generated Material Symbols Rounded glyph map ├── tokens/ │ ├── reference_colors.dart # Layer 1: primitive swatches │ ├── semantic_colors.dart # Layer 2: DesignSystemColors │ ├── accent_colors.dart # Layer 3: AccentColors (lavender light/dark) │ └── typography.dart # DesignSystemTypography (15 styles) └── components/ # Styled* and Ash* widgets ├── styled_card.dart ├── styled_scaffold.dart ├── styled_text_field.dart ├── styled_list_tile.dart ├── styled_tab_bar.dart ├── styled_navigation_bar.dart ├── styled_floating_action_button.dart ├── styled_bottom_sheet.dart ├── semantic_button.dart ├── ash_icon.dart ├── ash_logo.dart ├── ash_wallpaper.dart # + ash_vector_wallpaper.dart, ash_image_wallpaper.dart (parts) ├── conversation_bubble.dart ├── letter_doodle.dart └── theme_override.dart ``` Everything is exported from the barrel file, so consuming code needs a single import: ```dart theme={null} import 'package:ashley/design_system/design_system.dart'; ``` ## Typography system Typography is a flat set of **15 tokens** — 5 categories × 3 sizes — using two typefaces: **Libertinus Serif** for display styles and **Inter** (a variable font) for everything else. Weights are applied via `FontVariation.weight(...)`, which is how the body styles achieve their signature in-between weight of **425**. See [Typography](/foundations/typography). ## What the theme layer adds [`app_theme.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/app_theme.dart) turns the tokens into a complete `ThemeData`: * Styles **every stock Material widget** (buttons, inputs, chips, switches, sliders, dialogs, navigation bar…) with Sundial tokens, so even non-`Styled*` widgets look on-system — see [Material bridge](/theming/material-bridge) * Forces **Cupertino page transitions on all platforms** * Bridges tokens into Material's `ColorScheme` and `TextTheme` for third-party widgets that read them * Installs the two `ThemeExtension`s Entry points: `AppTheme.light` and `AppTheme.dark` — brightness is the only theme axis. # Applications Source: https://design.slingshotai.com/brand/applications The brand in the world — posters, merch, and invitations The Brand Book shows Ash off the screen — the modular kit composed into real things. What stays constant across all of them: the serif [wordmark](/brand/marks), hand-drawn [doodles](/brand/illustration), high-contrast black-and-white with one warm accent, and generous space. ## Posters * **“Where messy minds come to unwind”** — the [positioning line](/brand/overview#positioning) as a hero poster, paired with a doodle figure. * **“Chaos · Mess · Clarity”** — a three-poster set, each carrying its matching stage of the doodle sequence. ![Overlapping posters titled Chaos, Mess, and Clarity with doodle-flower figures, next to white-line flowers on black](https://mintcdn.com/slingshot-ai/bvmER7q-zVy70THj/images/brand/applications-posters.png?fit=max\&auto=format\&n=bvmER7q-zVy70THj\&q=85\&s=16e862a85006518a2546b9e114efedad) The Chaos · Mess · Clarity poster set, stacked — each stage carries its matching doodle figure. ## Merch * **Tote bags** carrying the wordmark and doodles, white on black — the identity at its most confident and simple. ![A sheet of hand-drawn flower sketches next to a photo of a person carrying a natural canvas tote printed with the Ash wordmark and a doodle figure](https://mintcdn.com/slingshot-ai/bvmER7q-zVy70THj/images/brand/applications-tote.png?fit=max\&auto=format\&n=bvmER7q-zVy70THj\&q=85\&s=a229c8be6246d9fb74ce5bbca3cc7421) The kit in the world: sketch-sheet doodles beside the tote — wordmark plus one scribble-headed figure on natural canvas. ## Invitations * **“Party! You're invited to help us celebrate our new digs”** — a save-the-date that shows the playful end of the [tone](/brand/voice) range while staying unmistakably Ash. ## Environments & components The book also covers **environments** (spaces and signage) and reusable **layout components** for composing branded pieces. These share the same kit of parts — nothing new is invented per surface. ## The rule that ties it together Every application is the same handful of parts recomposed — marks, type, color, illustration, voice. When you make something new: start from those parts, keep it high-contrast and spacious, and let a single doodle moment carry the warmth. > [!NOTE] > > The Brand Book also details **Photography** and **Video / motion** direction. Those slides are the canonical reference — see the [Brand Book](https://www.figma.com/design/DUpdqWp9T9djLnWo0Op0yk/Brand-Book) — and can be written up here as dedicated pages when needed. # Color Source: https://design.slingshotai.com/brand/color The brand palette and how it relates to the Sundial tokens Ash's color is quiet by default and warm up close. The identity rests on a **black-and-white core** with **warm beige neutrals** and a small set of **soft pastels** — periwinkle in the lead, backed by green, orange, and yellow. > [!WARNING] > > The Brand Book's color chips predate the current tokens. The **live source of truth for color is the [Sundial design system](/foundations/colors/reference)** — build against those tokens, never hand-picked hexes. ## The palette, in short * **Core:** black and white — with warm beiges standing in for grey. Ash deliberately avoids cold grey. * **Lead accent:** Periwinkle — bold enough to be memorable, soft enough to be safe. It replaced the book's Lavender Blue `#CDCCF0` in October 2026. * **Pastel accents:** green, orange, and yellow — small moments of joy in illustration and applications. **Core & neutrals** **Black**\ `#000000`**White**\ `#FFFFFF`**Light Beige**\ `#F4F2EF`**Dark Beige**\ `#CEC5B8` **Lead accent** **Periwinkle**\ `#DBE1FD` **Pastel accents** **Green**\ `#9ED99D`**Orange**\ `#F6CF98`**Yellow**\ `#F5EAAC` > [!NOTE] > > Swatches are sampled directly from the v1 brand book's chips; the book's printed specs (CMYK / PMS) are still being filled in. For anything built in the product, the [Sundial tokens](/foundations/colors/reference) remain the source of truth. ![A full-bleed lavender-blue slide titled 'Lavender Blue'](https://mintcdn.com/slingshot-ai/bvmER7q-zVy70THj/images/brand/color-lavender-blue.png?fit=max\&auto=format\&n=bvmER7q-zVy70THj\&q=85\&s=476020474300b3998f28c4b44c48320f) Lavender Blue — the book's original lead accent, since replaced by Periwinkle #DBE1FD. ## How we got here The 2026 palette is a correction, and the book is honest about it. The earlier identity leaned on heavy beige and a muted purple — warm, but murky, not punchy, and spread across too many colors to build recognition. What was missing: boldness, a signature tone with depth and appeal across genders, and a sense of joy. The answer was restraint — a black-and-white core with periwinkle in the lead. ![Retrospective slide showing the previous beige, lilac, and multi-hue palette with critique bullets](https://mintcdn.com/slingshot-ai/bvmER7q-zVy70THj/images/brand/color-retrospective.png?fit=max\&auto=format\&n=bvmER7q-zVy70THj\&q=85\&s=f981c222e5106c0a91874e34f379dbd9) The book's retrospective: the old beige-and-muted-purple palette, annotated with why it fell short. ## Where the real values live | Layer | Documented in | | - | - | | Primitive ramps (with hex) | [Reference Colors](/foundations/colors/reference) | | Semantic tokens (light/dark) | [Semantic Colors](/foundations/colors/semantic) | | Persona accents | [Accent Colors](/foundations/colors/accent) | | Flat brand colors (lavender, sunflower, grass, orange, sky, rock, tomato) | [Reference Colors → Brand](/foundations/colors/reference#brand-colors) | ## Principles * **Warm over cold** — parchment, not grey. * **Restraint by default** — color earns its place; the canvas stays calm. * **Never rely on color alone** to carry meaning — see [Robustness & Readability](/philosophy/craft/robustness). # Illustration Source: https://design.slingshotai.com/brand/illustration The hand-drawn doodle-flower system and the Chaos → Mess → Clarity motif Illustration is where Ash's warmth is most visible. The style is **hand-drawn, single-line doodles** — flowers, and little flower-headed characters mid-stride — loose and imperfect on purpose. It is the opposite of slick: a human hand, not a machine. ## The doodle style * **One continuous hand-drawn line.** Wobble and imperfection are features, not flaws. * **Monochrome line** — usually black on a light ground, or white on dark. * **Subjects:** flowers at every stage of tidiness, and small flower-headed figures who walk, carry, and celebrate. The looseness is what lets the brand be playful *and* safe at the same time: it looks like something a person made for you. ![Line slide: a dense single-line scribble specimen](https://mintcdn.com/slingshot-ai/bvmER7q-zVy70THj/images/brand/illustration-line.png?fit=max\&auto=format\&n=bvmER7q-zVy70THj\&q=85\&s=4e01ac6ca14e7af46eb54c75001824fa) Line — one continuous, wobbly hand-drawn stroke. The imperfection is the point. ![Fill slide: three panels of hand-drawn dot texture at increasing dot sizes](https://mintcdn.com/slingshot-ai/bvmER7q-zVy70THj/images/brand/illustration-fill.png?fit=max\&auto=format\&n=bvmER7q-zVy70THj\&q=85\&s=ff362eceb2ff8c39be1183c836b6541b) Fill — hand-dotted textures in three densities, used to shade the doodles without ever going slick. ![Flower-headed walking figures drawn in white line on a black ground](https://mintcdn.com/slingshot-ai/bvmER7q-zVy70THj/images/brand/illustration-dark-set.png?fit=max\&auto=format\&n=bvmER7q-zVy70THj\&q=85\&s=69d149bf906dab28e4143c2d7dfa9b5f) The same line works in reverse — white on black, where the brand is at its most confident. ## Chaos → Mess → Clarity The signature sequence is a dense scribble (**chaos**) that loosens into a rough bloom (**mess**) and resolves into a single open flower (**clarity**). It tells the whole Ash story in one glance, and it recurs across the brand — on posters, on merch, and as the arc of the product itself. See the [brand story](/brand/overview#the-story-chaos-mess-clarity). Why flowers, why scribbles: a scribble is honest about the mess; a flower is hopeful about growth. Together they carry two of the three [brand attributes](/brand/overview#personality) at once. ![Three flower-headed doodle characters walking on a parchment ground, heads progressing from tangled scribble to open flower](https://mintcdn.com/slingshot-ai/bvmER7q-zVy70THj/images/brand/illustration-characters.png?fit=max\&auto=format\&n=bvmER7q-zVy70THj\&q=85\&s=84ceebbf0bb47b0c8b3450e9607c5fe0) The three states as characters, mid-stride: a scribble-headed figure (chaos), a loosening bloom (mess), and a clear open flower (clarity). ## In the app The illustration language lives on in code as: * The [Wallpaper system](/foundations/wallpapers) — flat brand colors behind cards and tiles, and soft gradient “blobs” behind hero surfaces. (The hand-drawn pattern imagery that once backed these surfaces was retired in July 2026; the hand shows up in the doodles below instead.) * The flower and persona marks in [`AshIcons`](/foundations/iconography) (e.g. `ashFlower`). * The [doodle components](/components/doodles) — `ConversationBubble` and `LetterDoodle` are drawn by the same hand. ## Usage > [!TIP] > > **Do** keep it hand-drawn, monochrome, and personal. Use it to add warmth at meaningful moments. > [!WARNING] > > **Don't** redraw it as clean geometric vector, tile it into wallpaper-of-decoration, or dress it up with color gradients and shadows. Expressive moments are *earned, not sprinkled* — see [Craft & Foundations](/philosophy/overview). The canonical illustration library lives in the [Brand Book](https://www.figma.com/design/DUpdqWp9T9djLnWo0Op0yk/Brand-Book). # Logo & Marks Source: https://design.slingshotai.com/brand/marks The Ash wordmark and circular badge, and how to use them Ash's primary mark is the **“Ash” wordmark**, set in the brand serif. A **circular badge** — the wordmark inside a solid disc — is the compact, iconic lockup for tight spaces. ## The wordmark The wordmark is simply *Ash*, set in the brand's warm classical serif (see [Typography](/brand/typography)). It reads confident and book-like — more literary than techy, which is deliberate: Ash is a companion, not a gadget. * **Default lockup is high-contrast and monochrome** — white on black (as on the brand book cover) or black on a light surface. * **Mixed case** — capital *A*, lowercase *sh*. Never all-caps, never restyled in another face. ![The Ash serif wordmark in black on a white slide](https://mintcdn.com/slingshot-ai/bvmER7q-zVy70THj/images/brand/logotype-primary.png?fit=max\&auto=format\&n=bvmER7q-zVy70THj\&q=85\&s=b736c33b4fd6a6afb9f2f668d94cfd5d) The primary mark, from the Brand Book's Marks chapter. ## The badge The circular badge places the wordmark inside a solid circle. Use it where a compact, iconic mark is needed — app icon, avatars, stamps, favicons — anywhere the full wordmark would be too wide. ![Containers slide: the Ash wordmark inside a solid black circle, with auto-centered vs optically-centered comparisons](https://mintcdn.com/slingshot-ai/bvmER7q-zVy70THj/images/brand/logotype-containers-circle.png?fit=max\&auto=format\&n=bvmER7q-zVy70THj\&q=85\&s=9e69e56e416a717f217181db3f2fb2dd) The circular badge. Note the book's rule: the wordmark is optically centered in its container, not auto-centered. ![Containers slide: the Ash wordmark inside a solid black square](https://mintcdn.com/slingshot-ai/bvmER7q-zVy70THj/images/brand/logotype-containers-square.png?fit=max\&auto=format\&n=bvmER7q-zVy70THj\&q=85\&s=51d02f3d449bd362a2459df3137e893e) The same containment rules apply in square containers. ## Clear space & size Give the mark room to breathe. Reserve clear space of at least the wordmark's own cap-height on every side, and never crowd it with other elements. Generous space is itself part of the brand — see [Designing for Ash](/philosophy/designing-for-ash). ![Scale slide: the wordmark on a construction grid next to a full-bleed black poster treatment](https://mintcdn.com/slingshot-ai/bvmER7q-zVy70THj/images/brand/logotype-scale.png?fit=max\&auto=format\&n=bvmER7q-zVy70THj\&q=85\&s=60b71f0b637313564855bfaba3685ca8) Scale guidance from the book — the wordmark sized against a poster grid, and at full bleed on black. ## Usage > [!WARNING] > > **Don't** stretch or condense it, recolor it arbitrarily, add shadows/gradients/effects, set it in a different typeface, or place it on a busy or low-contrast background. Gratuitous effects are an [anti-pattern](/philosophy/anti-patterns). The marks travel with one accent: the hand-drawn doodle flower, in the same monochrome line as the wordmark's ground. ![Accent slide: a single-line doodle flower shown black on white and white on black](https://mintcdn.com/slingshot-ai/bvmER7q-zVy70THj/images/brand/logotype-accent-doodles.png?fit=max\&auto=format\&n=bvmER7q-zVy70THj\&q=85\&s=7f11095bf94e150824edcad023afe529) The doodle-flower accent that accompanies the marks — one hand-drawn line, black on light or white on dark. ## In the app The wordmark ships as the [`AshLogo`](/components/ash-logo) component (width-driven, theme-tinted). The brand's flower and persona marks live in the [`AshIcons`](/foundations/iconography) set. The canonical mark artwork lives in the [Brand Book](https://www.figma.com/design/DUpdqWp9T9djLnWo0Op0yk/Brand-Book). # Brand Source: https://design.slingshotai.com/brand/overview The Ash brand — positioning, personality, and the story the identity tells Ash is a consumer AI companion for mental health and personal growth. The **brand** is the layer above the [Design System](/): the personality, story, and expression that make Ash feel like *Ash* — warm, human, and hopeful — before a single component is placed on a screen. ![Brand book cover: the Ash serif wordmark, white on a black field](https://mintcdn.com/slingshot-ai/bvmER7q-zVy70THj/images/brand/cover.png?fit=max\&auto=format\&n=bvmER7q-zVy70THj\&q=85\&s=19e8d645dc8d9eaa9e174a66635bf596) The cover of the Ash Brand Book — the wordmark at its most confident: white on black. > [!NOTE] > > This section is drawn from the [**Ash Brand Book**](https://www.figma.com/design/DUpdqWp9T9djLnWo0Op0yk/Brand-Book) (the canonical source, in Figma). It captures the brand at the identity level. For the coded tokens and components that implement it, see the [Design System](/); for the design thinking behind it, see [Craft & Foundations](/philosophy/overview). ## Positioning > [!NOTE] > > **Where messy minds come to unwind.** Ash meets people in the mess of real life and helps them move toward clarity — without rushing them. The whole identity is built to feel safe, unhurried, and quietly optimistic. ## Personality Three attributes anchor every expression of the brand. When a design decision is in question, the answer is the one that is most warm, most trustworthy, and most hopeful. > [card] Warm > > Made by someone who cares. Human, never clinical or cold. > [card] Trustworthy > > Calm, honest, dependable. Earns confidence quietly rather than demanding it. > [card] Hopeful > > Optimistic about growth. There is always a way forward. ![Brand book slide 'The Brand Is… Warm, Trustworthy, Hopeful' on a warm parchment ground](https://mintcdn.com/slingshot-ai/bvmER7q-zVy70THj/images/brand/intro-brand-is.png?fit=max\&auto=format\&n=bvmER7q-zVy70THj\&q=85\&s=6bace0e054f52f7c70deac8601a9f0d7) From the book: every visual should ladder back to these three sentiments — through what it calls Human (people aren't perfect, neither are we), Structure (orderly, informed, honest), and Playful (small moments of joy). ## The story: Chaos → Mess → Clarity The core visual narrative runs from a tangled scribble to a clear, open flower — three states that name the emotional arc of using Ash: **1. Chaos** Tangled, knotted thoughts. A dense ball of scribble. **2. Mess** The knot begins to loosen and take shape — still imperfect, but moving. **3. Clarity** A single, clear, open flower. Not tidy perfection — just something you can finally see. This progression shows up everywhere in the brand: in the [illustration system](/brand/illustration), on posters, and as the through-line of Ash's own product experience. It's the reason the identity can be playful and hand-drawn while still feeling safe — the mess is acknowledged, not hidden. ## The brand book is modular The Ash Brand Book is a **modular system**, not a fixed template — a kit of parts (marks, type, color, illustration, voice) meant to be composed for each context, from the app to a tote bag to a party invite. These pages document those parts. ## What's in this section > [card] [Logo & Marks](/brand/marks) > > The "Ash" wordmark and the circular badge — and how to use them. > [card] [Illustration](/brand/illustration) > > The hand-drawn doodle-flower system and the Chaos → Mess → Clarity motif. > [card] [Voice & Tone](/brand/voice) > > How Ash speaks — warm, plain, and unhurried. > [card] [Color](/brand/color) > > The brand palette and how it relates to the Sundial tokens. > [card] [Typography](/brand/typography) > > The serif voice and the working sans. > [card] [Applications](/brand/applications) > > The brand in the world — posters, merch, and invitations. # Typography Source: https://design.slingshotai.com/brand/typography The serif voice and the working sans Ash pairs a **warm classical serif** for expressive, human moments with **Inter** for everything functional. The serif is the brand's voice; the sans is its workhorse. ## The pairing * **Serif (display)** — the *Ash* [wordmark](/brand/marks) and large emotional headlines. Warm, literary, book-like; it's what makes Ash feel like a companion rather than a tool. * **Inter (working sans)** — UI, body copy, labels, and buttons. ![Type specimen slide: Libertinus Serif 'Aa' at display size](https://mintcdn.com/slingshot-ai/bvmER7q-zVy70THj/images/brand/typography-libertinus.png?fit=max\&auto=format\&n=bvmER7q-zVy70THj\&q=85\&s=fb99fc4d94c0ab5fcc736362e7b795b5) Libertinus Serif — the brand's voice. One weight: Regular. ![Type specimen slide: Inter 'Aa' at display size with its three weights](https://mintcdn.com/slingshot-ai/bvmER7q-zVy70THj/images/brand/typography-inter.png?fit=max\&auto=format\&n=bvmER7q-zVy70THj\&q=85\&s=d52676fa5a8ddaeae61876a77870c657) Inter — the workhorse, in Regular, Medium, and Semi Bold. > [!WARNING] > > Brand Book type specs predate the current tokens. The **live type system is Sundial's** — see [Typography](/foundations/typography) for the exact 15-token scale (Libertinus Serif display + Inter, including the unusual 425 body weight). ## Principles * **Contrast through weight and color, not many sizes** — see the [typography craft rules](/philosophy/craft/typography). * **Use the serif sparingly**, for moments that deserve a human voice. Don't set UI chrome in it. * **Constrained scale only** — choose from the system; never a hand-picked size. ## The hierarchy in practice ![Treatment slide: 'Where messy minds come to unwind.' headline over body and subtext levels, with weight and size specs](https://mintcdn.com/slingshot-ai/bvmER7q-zVy70THj/images/brand/typography-treatment.png?fit=max\&auto=format\&n=bvmER7q-zVy70THj\&q=85\&s=1f5a284b8dafcd5712a6b84661c0333d) The book's treatment page: headline, body, and subtext working together in Inter — real product copy, not filler. # Voice & Tone Source: https://design.slingshotai.com/brand/voice How Ash speaks — warm, plain, and unhurried Ash's voice is its personality made audible. It follows directly from the three [brand attributes](/brand/overview#personality) and from who Ash is for: someone who may be tired, anxious, or overwhelmed. Every word should feel safe. ## Principles **Warm, not clinical.** Speak like a person who cares, not a system. Plain, kind language; contractions are welcome. No jargon, no diagnosis-speak. **Trustworthy, not salesy.** Honest and calm. Never hype, never pressure, never a dark pattern. Say what's true, plainly, and under-promise. **Hopeful, not saccharine.** Optimistic and forward-looking, but never dismissive of hard feelings. Acknowledge the mess before pointing toward the light — the same [Chaos → Mess → Clarity](/brand/overview#the-story-chaos-mess-clarity) arc, in words. **Unhurried.** Give people room. Short sentences. Space to breathe. Don't rush anyone toward an action. ![Brand book slide: 'Ash isn't a friend, a coach or a therapist. It's for those in-between moments when something feels… off.' set in the brand serif, white on black](https://mintcdn.com/slingshot-ai/bvmER7q-zVy70THj/images/brand/voice-treatment.png?fit=max\&auto=format\&n=bvmER7q-zVy70THj\&q=85\&s=24062cc42506dd30088b267f3478f8ab) The voice in the brand's own serif, from the book: honest about what Ash is not, warm about what it is. ## Tone shifts with the moment | Moment | Tone | | - | - | | A vulnerable moment | Slower, softer, fewer words | | A milestone or celebration | A little more playful and warm | | Functional UI (settings, errors) | Clear and brief — still kind | ## Do / don't | Say this | Not this | | - | - | | “Take your time.” | “Hurry — your streak is about to end!” | | “That sounds really hard.” | “Don't worry, it's fine!” | | “Want to talk about it?” | “Complete your daily check-in now.” | | Plain, human words | Clinical or diagnostic terms | | Acknowledge first, then gently guide | Jump straight to advice | ## No emoji in UI Ash expresses personality through design and [illustration](/brand/illustration), not emoji — the one exception being user-generated content. This is a hard rule; see [anti-patterns](/philosophy/anti-patterns). > [!NOTE] > > This page articulates Ash's voice from its brand attributes and product context. The **Voice** section of the [Brand Book](https://www.figma.com/design/DUpdqWp9T9djLnWo0Op0yk/Brand-Book) holds the canonical examples — treat this as the working summary and defer to the book where they differ. # Changelog Source: https://design.slingshotai.com/changelog Version history of the Sundial design system — every meaningful change to tokens, components, and theming Sundial is versioned as a system: the version reflects the design system as it ships in the Ash app, not this documentation site. Entries below are backfilled from the git history of [`lib/design_system/`](https://github.com/slingshot-ai/ash-mobile-app/tree/main/lib/design_system) and reference their pull requests. Bump rules live in [Contributing](/patterns/contributing#versioning--changelog). **October 9, 2026** Periwinkle ## The accent moves to periwinkle The brand lead and accent change from Lavender Blue `#CDCCF0` to **Periwinkle `#DBE1FD`** ([#1968](https://github.com/slingshot-ai/ash-mobile-app/pull/1968), in review). Both [lavender ramps](/foundations/colors/reference#lavender) were re-derived from that single anchor in OKLCH: hue rotated −10.6°, chroma eased near the anchor and kept full on the deep steps, lightness held so every step keeps its contrast. Token changes in the same release: * [`contentOnAccent`](/foundations/colors/semantic#content) / `accent.onButton` is now **pure black** (16.2:1 on the solid). * Light-mode [`contentAccent`](/foundations/colors/semantic#content) / `accent.secondary` moves to lavender step 12 — step 11 read royal blue as text. * New [`accentLine`](/foundations/colors/semantic) (light 8 / dark 11) for accent outlines, checkboxes and progress: 3:1, never text. * `brandLavender` follows step 9, along with the native splash screens and the production app icon. This site was rethemed to the new values. **August 7, 2026** Sundial 2.0 ## Sundial 2.0 color system The color foundations are rewritten for **Sundial 2.0**: warm sand 12-step scales on the Radix model with paired alpha inks, the single Lavender Blue accent on a bright-solid grammar (a pale solid that takes dark content), eight mode-invariant brand flats, and cherry/amber status stops. Personas are retired — [Accent Theming](/theming/persona-theming) documents the replacement, and [ThemeOverride](/components/theme-override) is brightness-only. New in the token set: [`contentOnBrandSecondary`](/foundations/colors/semantic#content) — the secondary tier on brand surfaces, an alpha ink (light-alpha step 11) so the surface supplies the hue and one token holds AA on every brand color. Rollout status: the token system is fully specified in Figma and implemented behind draft PRs [#1435](https://github.com/slingshot-ai/ash-mobile-app/pull/1435)–[#1439](https://github.com/slingshot-ai/ash-mobile-app/pull/1439); the old brand flats already resolve to 2.0 values in place. Legacy semantic names survive as forwarding getters until the rename phase completes. This site was rethemed to the 2.0 values: warm sand near-black canvas, lavender ramp accents. **July 29, 2026** Wallpapers ## Pattern wallpapers retired The last five surfaces still backed by hand-drawn pattern PNGs — the notification preview, onboarding notifications, the force-update modal, the voice-migration modal, and the shared interstitial modal header — now use a flat `SolidWallpaper(ReferenceColors.lavender)` ([#1412](https://github.com/slingshot-ai/ash-mobile-app/pull/1412)). `interstitialModalHeader` lost its `image:` parameter along the way. With no callers left, `ImageWallpaper.pattern`, `ImageWallpaper.random`, and the 26-image pattern bank were deleted ([#1413](https://github.com/slingshot-ai/ash-mobile-app/pull/1413)). This completes the move to flat brand color that [`SolidWallpaper`](/foundations/wallpapers#solidwallpaper) began in June 2026. [`ImageWallpaper`](/foundations/wallpapers#imagewallpaper) remains, narrowed to server-supplied imagery — a single URL, one use in the app. Use `SolidWallpaper` for decorative backgrounds. **July 24, 2026** Brand ## Brand section illustrated, site rethemed The [Brand](/brand/overview) section now carries the artwork of the Ash Brand Book: 19 slides exported from the [book's Figma](https://www.figma.com/design/DUpdqWp9T9djLnWo0Op0yk/Brand-Book) across all seven pages — cover, marks, type specimens, illustration system, voice treatment, and applications. [Color](/brand/color) gained the new palette as native swatches (core, beiges, the Lavender Blue lead, and the green/orange/yellow pastels, sampled from the book's chips) plus a "How we got here" retrospective. This site was rethemed to match the brand direction, locked to dark: Lavender Blue accent and a warm near-black background in place of the persona plum and grid decoration. **July 23, 2026** Components ## StyledIconButton joins the system The single-pill conversation input bar redesign ([#1376](https://github.com/slingshot-ai/ash-mobile-app/pull/1376)) introduced [`StyledIconButton`](/components/styled-icon-button) — a 40 px circular icon button with `.accent` and `.surface` variants and a `showDisabledColors` mode for buttons that look unavailable but stay tappable to explain why. **July 14, 2026** v1.0.0 ## Sundial 1.0 Sundial is declared **1.0**: the system as documented on this site, shipping in the Ash app today. The 1.0 snapshot: * **Three-layer color tokens** — 9 reference ramps + 7 brand colors, 25 semantic tokens with light/dark mappings, 5 persona accent palettes * **15 typography tokens** — Libertinus Serif display styles + Inter (variable weight, incl. the 425 body weight) * **14 components** — Styled\* Material wrappers and Ash\* brand primitives * **Theming** — ThemeExtension delivery, context accessors, the Material bridge, subtree persona overrides Also in this release: this documentation site — full token/component reference, Flutter theming guides, the design [Philosophy](/philosophy/overview) section, and this changelog. **June 23, 2026** Accessibility ## Screen reader support in core flows VoiceOver/TalkBack support added across core flows ([#1262](https://github.com/slingshot-ai/ash-mobile-app/pull/1262)). For the design system this established the accessibility conventions documented in [Conventions](/patterns/conventions#accessibility): * [`SemanticButton`](/components/semantic-button) — composite tappables merge into a single announced button * Decorative-by-default icons (`semanticLabel: null` on [`AshIcon`](/components/ash-icon)), labeling at the button layer **June 18, 2026** Wallpapers ## Flat brand colors replace gradient wallpapers Gradient wallpapers were replaced with flat brand colors ([#1283](https://github.com/slingshot-ai/ash-mobile-app/pull/1283)), introducing [`SolidWallpaper`](/foundations/wallpapers#solidwallpaper) with its `cardPalette`/`brandPalette` and the `forWeekday`/`brand`/`forSeed` factories — and with it the solid-wallpaper legibility rule (dark text, no scrim). **June 12, 2026** Typography ## Typography aligned to the Parchment spec Typography tokens and fonts updated to match the Parchment spec ([#1269](https://github.com/slingshot-ai/ash-mobile-app/pull/1269)): **Inter** (variable weight via `FontVariation`, including the 425 body weight) and **Libertinus Serif** for display styles. Display styles set to 100% line height ([#1270](https://github.com/slingshot-ai/ash-mobile-app/pull/1270)). The chat FAB was aligned to its Figma spec the same day ([#1267](https://github.com/slingshot-ai/ash-mobile-app/pull/1267)). Current spec: [Typography](/foundations/typography). **April 11, 2026** Iconography ## Icons optimized for server-driven UI The icon system was optimized for SDUI ([#1155](https://github.com/slingshot-ai/ash-mobile-app/pull/1155)) — the era of [`AshIcon.network`](/components/ash-icon) for server-delivered SVGs alongside the registered `AshIcons` enum. **March 3, 2026** Interaction ## Haptic feedback on navigation Bottom-navigation destinations and home-screen cards gained haptic feedback ([#1102](https://github.com/slingshot-ai/ash-mobile-app/pull/1102)) — now a built-in behavior of [`NavDestination`](/components/styled-navigation-bar) (`HapticFeedback.selectionClick()` before `onTap`). **February 18, 2026** Components ## Navigation components join the system Following the app-navigation overhaul ([#1044](https://github.com/slingshot-ai/ash-mobile-app/pull/1044)), the navigation bar and FAB were moved into the design system ([#1049](https://github.com/slingshot-ai/ash-mobile-app/pull/1049)) as [`StyledNavigationBar`](/components/styled-navigation-bar) + `NavDestination` and [`StyledFloatingActionButton`](/components/styled-floating-action-button). **January 16, 2026** Sundial is born ## Initial Sundial implementation The Sundial Design System landed in the Ash app ([#985](https://github.com/slingshot-ai/ash-mobile-app/pull/985)): * **ReferenceColors** primitive ramps and **DesignSystemColors** semantic tokens as Flutter `ThemeExtension`s * **DesignSystemTypography** — the 5×3 token grid * **AccentColors** with the five persona palettes * `AppTheme` building light/dark `ThemeData` with every Material component themed * The first `Styled*` components and `context.colorTheme` / `context.textTheme` accessors # AshIcon Source: https://design.slingshotai.com/components/ash-icon SVG icon widget for the custom Ash icon set — local assets or network URLs Renders the custom Ash SVG icons (see [Iconography](/foundations/iconography) for the full set) with theme-aware tinting. Source: [`ash_icon.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/components/ash_icon.dart). ```dart theme={null} // Local asset from the AshIcons enum AshIcon(AshIcons.insights, size: 24) // Brand mark keeping its own colors AshIcon(AshIcons.brandSpotify, useOriginalColor: true) // Remote SVG (server-driven UI) AshIcon.network('https://…/icon.svg', size: 32) ``` ## Constructors | Constructor | Source | | - | - | | `AshIcon(AshIcons icon, …)` | `assets/icons/{icon.value}.svg` | | `AshIcon.network(String iconUrl, …)` | Any SVG URL (via `SvgPicture.network`) | ## API | Parameter | Type | Default | Notes | | - | - | - | - | | `size` | `double?` | surrounding `IconTheme.size` | Width & height (`FittedBox` contains the SVG) | | `color` | `Color?` | `IconTheme.color` → `contrastStrong` | Tint color | | `useOriginalColor` | `bool` | `false` | Skip tinting — for multi-color brand marks | | `semanticLabel` | `String?` | `null` | `null` = decorative, excluded from semantics | ## Resolution rules * **Color**: explicit `color` → `IconTheme.of(context).color` → `context.colorTheme.contrastStrong`. Tinting uses `ColorFilter.mode(color, BlendMode.srcIn)`. * **Size**: explicit `size` → `IconTheme.of(context).size`. Because `IconTheme` is respected, `AshIcon` slots into `IconButton`, [`NavDestination`](/components/styled-navigation-bar), and [`StyledFloatingActionButton`](/components/styled-floating-action-button) and picks up their icon styling automatically. * **Semantics**: with `semanticLabel: null` the SVG sets `excludeFromSemantics: true`. Label at the button layer (e.g. `IconButton.tooltip`) instead — avoids double announcements. ## Real usage Sign-in provider buttons (`provider_sign_in_buttons.dart`, brand icons with `useOriginalColor`), activity heroes (`activity_hero_image.dart`), linked-account platform icons (`platform_icon_mapping.dart`, including `.network` for server-driven icons). # AshLogo Source: https://design.slingshotai.com/components/ash-logo The Ash logo widget — width-driven, theme-tinted The Ash brand logo, rendered from `assets/icons/AshLogo.svg`. Source: [`ash_logo.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/components/ash_logo.dart). ```dart theme={null} AshLogo(width: 96) // Fixed color regardless of theme AshLogo(width: 64, color: SundialRef.sandLight[1]) ``` ## API | Parameter | Type | Default | Notes | | - | - | - | - | | `width` | `double` | **required** | Height scales from the SVG's aspect ratio | | `color` | `Color?` | `contrastStrong` | Tint (`BlendMode.srcIn`) | That's the whole API — deliberately minimal. The logo always tints (there is no `useOriginalColor` here); by default it follows the theme's `contrastStrong`, so it's dark on light surfaces and light on dark ones. > [!NOTE] > > `AshLogo.svg` lives in `assets/icons/` but is **not** part of the `AshIcons` enum — the logo is brand lockup, not an icon, and has its own widget. # AshWallpaper Source: https://design.slingshotai.com/components/ash-wallpaper Renders any Wallpaper with an optional scrim, blur, and child content The rendering widget for the [wallpaper system](/foundations/wallpapers): give it any `Wallpaper` (solid, image, or vector) and it paints the background, a legibility scrim, and your content in a `Stack`. Source: [`ash_wallpaper.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/components/ash_wallpaper.dart). ```dart theme={null} AshWallpaper( SolidWallpaper.forSeed(entityId.hashCode), child: Padding( padding: EdgeInsets.all(16), child: content, ), ) ``` ## API | Parameter | Type | Default | Notes | | - | - | - | - | | `wallpaper` | `Wallpaper` | **required** (positional) | `SolidWallpaper`, `ImageWallpaper`, or `VectorWallpaper` | | `scrim` | `Widget?` | `surfaceBg` @ 50% alpha | Layer between wallpaper and child; pass `SizedBox.shrink()` for none | | `fit` | `BoxFit?` | `BoxFit.none` (image) / `BoxFit.cover` (vector) | How the artwork fills the area | | `blur` | `double` | `0` | When > 0, the scrim is wrapped in a `BackdropFilter` gaussian blur (mirror tiling) | | `child` | `Widget?` | — | Foreground content | ## Layer order ```text theme={null} ┌─ child (optional) ───────────┐ top ├─ scrim (+ optional blur) ───┤ └─ wallpaper ─────────────────┘ bottom ``` Dispatch by type: `SolidWallpaper` → `ColoredBox`; `ImageWallpaper` → cached network image (with `surfaceBg` placeholder/error states); `VectorWallpaper` → blurred gradient-blob painter. The stack clips with `Clip.hardEdge` — give the parent the border radius (as [`StyledListTile`](/components/styled-list-tile) does). ## Legibility The default 50% `surfaceBg` scrim is a reasonable general-purpose wash. For content-bearing surfaces follow the [legibility rule](/foundations/wallpapers#legibility-rule): dark text with no scrim over solids; `ThemeOverride(brightness: dark)` + gradient scrim over textures. ## Tokens consumed `surfaceBg` (default scrim, image placeholder). # Doodles Source: https://design.slingshotai.com/components/doodles ConversationBubble and LetterDoodle — hand-drawn illustration marks Two tiny widgets wrap Ash's hand-drawn illustration marks. They have **no parameters** — each renders a PNG asset with `BoxFit.contain`; size them from the outside with a `SizedBox` or `Positioned`. ## ConversationBubble Hand-drawn speech bubble marking **conversation surfaces** — conversation-starter cards, history day tiles, and the daily-summary header. Source: [`conversation_bubble.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/components/conversation_bubble.dart) · asset `assets/images/ConversationStarterBubble.png`. ```dart theme={null} SizedBox(width: 130, child: const ConversationBubble()) ``` [`StyledListTile.conversation`](/components/styled-list-tile) renders it automatically as the tile's right-side decoration. ## LetterDoodle Hand-drawn envelope marking **letter/check-in surfaces** — the letter's history card and the check-in letter detail header. Source: [`letter_doodle.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/components/letter_doodle.dart) · asset `assets/images/Letter.png`. ```dart theme={null} SizedBox(height: 64, child: const LetterDoodle()) ``` ## Why widgets for two images? The wrappers give each mark a single, findable identity: usage is greppable (`ConversationBubble` vs. a raw asset path string), the asset path lives in one place, and future treatment changes (recoloring, animation) have a home. Follow the same pattern for new illustration marks. # SemanticButton Source: https://design.slingshotai.com/components/semantic-button Accessibility wrapper that exposes a custom tappable as a single button to screen readers Raw `GestureDetector`s and bare `InkWell`s don't announce a button role, and widgets composed of several texts read as disjoint nodes. `SemanticButton` merges its subtree into **one semantics node** with `button: true`, so VoiceOver/TalkBack reads e.g. *“Inspired by you, Your own definition of enough, button”* and activates it with the standard double-tap. Source: [`semantic_button.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/components/semantic_button.dart). ```dart theme={null} SemanticButton( child: InkWell( onTap: openEntry, child: Column(children: [titleText, subtitleText]), ), ) ``` Implementation is deliberately tiny: `MergeSemantics` ▸ `Semantics(button: true, …)` ▸ optionally `ExcludeSemantics`. No visuals, no tokens — purely semantics. ## API | Parameter | Type | Default | Notes | | - | - | - | - | | `child` | `Widget` | **required** | The tappable subtree | | `label` | `String?` | `null` | Screen-reader label; null composes from the child's visible text | | `hint` | `String?` | — | Usage hint, e.g. “Double tap to open” | | `selected` | `bool?` | `null` | Selection state for toggle-like buttons | | `enabled` | `bool` | `true` | — | | `excludeChildSemantics` | `bool` | `false` | Hide the child's own semantics, keep only `label`/`hint` | | `onTap` | `VoidCallback?` | — | Semantic tap action — only when the child contributes none | ## Usage rules (from the source doc comment) **1. Visible descriptive text → no label** The merge composes the announcement from the child's text automatically. **2. Decorative or animated child → label + excludeChildSemantics** Custom paint or streaming text reads badly; provide a stable `label` and set `excludeChildSemantics: true`. **3. Never wrap other interactive controls** The merge swallows nested controls' tap actions — one button per wrapper. **4. Pass onTap only when the child has no semantic tap** An `InkWell`/`GestureDetector` inside already merges its handler; duplicating it double-registers. ## Used by [`StyledFloatingActionButton`](/components/styled-floating-action-button) and composite tappables like [`StyledListTile`](/components/styled-list-tile) consumers. Reach for it any time you build a custom tappable that isn't a Material button. # Styled Bottom Sheet Source: https://design.slingshotai.com/components/styled-bottom-sheet showStyledBottomSheet — the floating-card modal bottom sheet Unlike the other components, the styled bottom sheet is a **function**, not a widget class — it wraps `showModalBottomSheet` and builds the styled chrome around your content. Source: [`styled_bottom_sheet.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/components/styled_bottom_sheet.dart). ```dart theme={null} final result = await showStyledBottomSheet( context: context, child: FeedbackForm(), ); ``` ## API ```dart theme={null} Future showStyledBottomSheet({ required BuildContext context, bool hideCloseButton = false, bool enableDrag = true, VoidCallback? onClose, required Widget child, }) ``` | Parameter | Default | Notes | | - | - | - | | `context` | **required** | Modal is pushed on this context's navigator | | `child` | **required** | Sheet content, padded 24 horizontal / 32 vertical | | `hideCloseButton` | `false` | Hide the corner close button | | `enableDrag` | `true` | Drag-to-dismiss | | `onClose` | `Navigator.pop` | Custom close handling | ## Behavior & anatomy * Renders as a **floating card**, not an edge-to-edge sheet: a [`StyledCard`](/components/styled-card) with radius 24 and `surfaceBg` fill, inset 8 px from the sides and 16 px from the bottom (plus the keyboard inset — the sheet rises with the keyboard via `viewInsets`). * `isScrollControlled: true` — the sheet sizes to its content, beyond half-screen if needed. * **`isDismissible: false`** — tapping the barrier does *not* dismiss; users close via the button, drag, or your own actions. Keep the close button visible unless you provide an explicit alternative. * The close button is a rotated `add_circle_sharp` icon (45° → an ×) at the top-right. * Returns the modal's `Future` — resolve it with `Navigator.pop(context, value)`. ## Tokens consumed `surfaceBg` (card fill); the close icon inherits `IconButtonTheme` (`contrastMedium`). ## Real usage `lib/widgets/feedback/feedback_rating_form.dart` presents the session-feedback flow in a styled sheet. # StyledCard Source: https://design.slingshotai.com/components/styled-card Card surface with elevated, outlined, and filled variants A card that mimics Material's `Card` with Sundial colors, borders, and shadows. Source: [`styled_card.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/components/styled_card.dart). ## Variants Variants are **named constructors** (the Sundial pattern — a private `_CardVariant` enum, no public `variant` param): | Constructor | Background | Border | Shadow | | - | - | - | - | | `StyledCard(…)` | `surfaceElevated` | none | subtle, from `elevation` (default 1.0) | | `StyledCard.outlined(…)` | transparent | 1 px `surfaceHigh` | none | | `StyledCard.filled(…)` | `surfaceElevated` | none | none | ```dart theme={null} StyledCard( child: Text('Elevated by default'), ) StyledCard.outlined( child: Text('Transparent with a hairline border'), ) StyledCard.filled( padding: EdgeInsets.all(12), child: Text('Flat filled surface'), ) ``` ## API | Parameter | Type | Default | Notes | | - | - | - | - | | `child` | `Widget?` | — | Card content | | `color` | `Color?` | variant default | Overrides the background | | `padding` | `EdgeInsets` | `kDefaultCardPadding` = `EdgeInsets.all(20)` | Inner padding | | `margin` | `EdgeInsets?` | — | Outer margin | | `border` | `BoxBorder?` | variant default | Overrides the border | | `borderRadius` | `double` | `kDefaultCardBorderRadius` = `16.0` | Corner radius | | `constraints` | `BoxConstraints?` | — | Size constraints | | `elevation` | `double` | `1.0` | **Default constructor only.** Shadow alpha = `elevation.clamp(0,1) × 0.26`, blur = `elevation × 4` | The card wraps its content in `Material(type: MaterialType.transparency)`, so ink effects from children render correctly. ## Tokens consumed * `surfaceElevated` — elevated/filled background * `surfaceHigh` — outlined border ## Real usage ```dart settings_list_tile.dart theme={null} StyledCard.filled( padding: EdgeInsets.zero, child: Column(children: tiles), ) ``` Also the surface behind [styled bottom sheets](/components/styled-bottom-sheet) (radius 24, `surfaceBg`). # StyledFloatingActionButton Source: https://design.slingshotai.com/components/styled-floating-action-button Pill FAB with accent fill, accent-ramp border, icon, and label A pill-shaped extended FAB in the active persona's accent palette. Source: [`styled_floating_action_button.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/components/styled_floating_action_button.dart). ```dart theme={null} StyledFloatingActionButton( icon: AshIcon(AshIcons.conversation), label: 'Talk to Ash', onPressed: startSession, ) ``` ## API | Parameter | Type | Notes | | - | - | - | | `onPressed` | `VoidCallback` | **required** | | `icon` | `Widget` | **required** — tinted `accent.onButton`, 20 px via `IconTheme` | | `label` | `String` | **required** — `buttonLarge` in `accent.onButton` | ## Anatomy * 52 px tall stadium, horizontal padding 24, 4 px icon-label gap * Fill: `accent.button` * Border: 3 px inside stroke of **`accent.base[500]`** — the one place a component reaches into the accent's raw ramp * Ink ripple via `Material` + `InkWell`, anti-aliased clip ## Accessibility Wrapped in [`SemanticButton`](/components/semantic-button) with no explicit label — the visible label merges into a single semantics node, so screen readers announce "‹label›, button". ## Tokens consumed `accent.button`, `accent.onButton`, `accent.base[500]`; typography `buttonLarge`. Changing persona (or wrapping in [ThemeOverride](/components/theme-override)) recolors the FAB automatically — see [Persona theming](/theming/persona-theming). # StyledIconButton Source: https://design.slingshotai.com/components/styled-icon-button 40 px circular icon button in accent or surface palettes, sized for input-bar pills A 40 px circular icon button with two named constructors: `.accent` for a bar's primary action and `.surface` for secondary actions. Introduced with the single-pill conversation input bar, where it is the send/voice button, the mic, and the attachment picker. Source: [`styled_icon_button.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/components/styled_icon_button.dart). ```dart theme={null} // Primary action — accent palette StyledIconButton.accent( icon: const Icon(Symbols.arrow_upward), tooltip: 'Send message', onPressed: onSubmit, ) // Secondary action — surface palette StyledIconButton.surface( icon: const Icon(Symbols.add), tooltip: 'Add attachment', onPressed: openPicker, ) ``` ## API | Parameter | Type | Notes | | - | - | - | | `onPressed` | `VoidCallback?` | **required** — `null` disables the button | | `icon` | `Widget` | **required** — the icon inside the circle | | `tooltip` | `String?` | accessibility / long-press label | | `iconSize` | `double` | icon size within the circle, default 24 | | `showDisabledColors` | `bool` | forces the disabled palette while keeping `onPressed` active, default `false` | ## Variants * **`.accent`** — fill `accent.button`, icon `accent.onButton`. One per bar: the primary action. * **`.surface`** — fill `surfaceHigh`, icon `contrastMedium`. Any number of secondary actions. * **Disabled** — `disabledContainer` / `disabledOnContainer`, from either a `null` `onPressed` or `showDisabledColors: true`. `showDisabledColors` exists for buttons that should *look* unavailable but stay tappable to explain why — the input bar's voice button uses it to show a "voice unavailable" toast instead of going dead. ## Anatomy * Fixed 40 px circle (`fixedSize`/`minimumSize`), zero padding, `shrinkWrap` tap target — sized to sit inside a pill-shaped bar * Built on Material's `IconButton` with `IconButton.styleFrom`, so ripple, focus, and tooltip behavior are stock ## Accessibility Pass `tooltip` — it doubles as the screen-reader label for the icon-only button and appears on long-press. Prefer keeping the button tappable with `showDisabledColors` over hard-disabling when the action's unavailability deserves an explanation. ## Tokens consumed `accent.button`, `accent.onButton`, `surfaceHigh`, `contrastMedium`, `disabledContainer`, `disabledOnContainer`. Changing persona (or wrapping in [ThemeOverride](/components/theme-override)) recolors the accent variant automatically — see [Persona theming](/theming/persona-theming). # StyledListTile Source: https://design.slingshotai.com/components/styled-list-tile Wallpaper-backed list tile with conversation, insights, and activities variants The signature Ash card-tile: a [wallpaper](/foundations/wallpapers) background, a decorative element on the right, and automatic text-legibility handling. Source: [`styled_list_tile.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/components/styled_list_tile.dart). There is **no default constructor** — the tile is variant-only: ## Variants | Constructor | Decoration (right side) | Content layout | | - | - | - | | `.conversation(…)` | `ConversationBubble` doodle (130 px wide) | Optional icon, title (`bodyLarge`), subtitle (`bodySmall`) | | `.insights(…)` | `AshIcons.insights` at 128 px, 26% black — or a custom `backgroundImage` | Subtitle label (`bodyMedium`) **above** title (`buttonLarge`) | | `.activities(…)` | Your `backgroundIcon` at 128 px, 26% black | Icon + subtitle header row (`overlineSmall`, 20 px icon) above title (`bodyLarge`) | ```dart theme={null} StyledListTile.conversation( wallpaper: SolidWallpaper.forWeekday(DateTime.now().weekday), title: 'What made today feel heavy?', onTap: () => startConversation(), ) StyledListTile.activities( // Seeded from the activity so the tile and the submission page's // hero image always land on the same brand color. wallpaper: SolidWallpaper.forSeed(activity.hashCode), backgroundIcon: AshIcons.activitiesPies, icon: AshIcon(AshIcons.activitiesPies), subtitle: 'ACTIVITY', title: 'The Pies', onTap: openActivity, ) ``` > [!NOTE] > > Every tile in the app now passes a [`SolidWallpaper`](/foundations/wallpapers#solidwallpaper). Seed it from the entity's id so a tile and its detail page share one color. ## API | Parameter | Type | Notes | | - | - | - | | `wallpaper` | `Wallpaper` | **required** — solid, image, or vector | | `title` | `String` | **required**; auto-sizes, max 2 lines | | `subtitle` | `String?` | Placement varies by variant | | `icon` | `Widget?` | insights/activities (and conversation renders none) | | `backgroundIcon` | `AshIcons` | **required** for `.activities`; fixed for others | | `backgroundImage` | `Widget?` | `.insights` only — replaces the background icon | | `onTap` | `VoidCallback?` | **required** (nullable to allow disabled tiles) | Structure: 16 px radius, 1 px `surfaceElevated` outside-stroke border, min height 96, content padded `(16, 16, 120, 16)` to clear the decoration, `InkWell` ripple, `AutoSizeText` for overflow. ## The legibility rule (implemented here) ```dart theme={null} final isSolid = wallpaper is SolidWallpaper; ``` * **Solid wallpaper** → dark sand-12 ink (`SundialRef.sandLight[12]`) directly on the brand color; **no scrim**. * **Image/vector wallpaper** → a left-to-right black gradient scrim (75% → 0%) is painted, and the content subtree is wrapped in `ThemeOverride(brightness: Brightness.dark)` so all tokens resolve to light-on-dark. This is the canonical example of the [wallpaper legibility convention](/foundations/wallpapers#legibility-rule) — follow the same pattern for any new wallpaper-backed surface. ## Real usage `lib/widgets/history/day_entries_list.dart` renders conversation and insights tiles for the history feed; `lib/widgets/conversation/therapist_artifact.dart` uses `.insights` with a custom `backgroundImage`. # StyledNavigationBar Source: https://design.slingshotai.com/components/styled-navigation-bar Floating pill navigation bar with NavDestination items, badges, and haptics The app's floating bottom navigation: a pill (stadium) bar that hovers above the content with an optional FAB beside it. Source: [`styled_navigation_bar.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/components/styled_navigation_bar.dart). ```dart theme={null} StyledNavigationBar( destinations: [ NavDestination( activeIcon: Icon(Symbols.home_filled), inactiveIcon: Icon(Symbols.home), label: 'Home', isActive: tab == 0, onTap: () => switchTab(0), ), // … ], fab: StyledFloatingActionButton( icon: AshIcon(AshIcons.conversation), label: 'Talk', onPressed: startSession, ), ) ``` ## StyledNavigationBar | Parameter | Type | Notes | | - | - | - | | `destinations` | `List` | **required** — spread evenly across the pill | | `fab` | `Widget?` | Rendered in a `Row` beside the bar (8 px gap), typically a [StyledFloatingActionButton](/components/styled-floating-action-button) | Anatomy: 64 px tall, stadium shape, `surfaceBg` fill with a 3 px inside `surfaceElevated` stroke, strong drop shadow (black-54, blur 12), 24 px outer padding, bottom `SafeArea`. **Callers own show/hide animation** — the bar itself doesn't animate visibility. ## NavDestination | Parameter | Type | Notes | | - | - | - | | `activeIcon` / `inactiveIcon` | `Widget` | **required** — swapped by state, tinted via `IconTheme` (24 px) | | `label` | `String` | **required** — `buttonSmall`, single line | | `isActive` | `bool` | **required** — `contrastStrong` when active, `contrastWeak` when not | | `showBadge` | `bool` | default `false` — 9 px `badgeRed` dot on the icon | | `onTap` | `VoidCallback` | **required** — fires **after**`HapticFeedback.selectionClick()` | ## Tokens consumed `surfaceBg`, `surfaceElevated`, `contrastStrong`, `contrastWeak`, `badgeRed`; typography `buttonSmall`. ## Real usage `lib/widgets/navigation/navigation_shell.dart` — the app's main shell renders the bar under an `IndexedStack` of tabs and passes the session FAB. # StyledScaffold Source: https://design.slingshotai.com/components/styled-scaffold The app's screen shell: StyledAppBar, back handling, and system UI overlay control A wrapper around `Scaffold` that gives every main screen consistent structure: a compact custom app bar, unified back-button behavior, and status-bar brightness control. Source: [`styled_scaffold.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/components/styled_scaffold.dart) — which exports four things: * **`StyledScaffold`** — the screen shell * **`StyledAppBar`** — the compact app bar (usable standalone) * **`ScaffoldCloseButton`** — themed circular close button * **`SystemUiOverlayOverride`** — status-bar icon brightness for a subtree ```dart theme={null} StyledScaffold( title: Text('Preferences'), body: PreferencesList(), ) ``` ## Back-button behavior The scaffold wraps its `Scaffold` in a `PopScope` with **`canPop: false` by default**: system back gestures and the app-bar back button are intercepted and routed through `onLeadingPressed`, which defaults to `Beamer.of(context).beamToHome()`. Set `canPop: true` for screens stacked over another route (e.g. the session screen over home) so iOS edge-swipe and system back perform a native pop revealing the route beneath. ## StyledScaffold API | Parameter | Type | Default | Notes | | - | - | - | - | | `body` | `Widget` | **required** | Main content, wrapped in `SystemUiOverlayOverride` | | `title` / `leading` / `trailing` | `Widget?` | — | Passed to `StyledAppBar` | | `appBarBottom` | `PreferredSizeWidget?` | — | Banner/controls under the toolbar | | `hideAppBar` | `bool` | `false` | Remove the app bar entirely | | `hideBackButton` | `bool` | `false` | Suppress the default back button | | `showAppBarBottomBorder` | `bool` | `false` | 2 px `surfaceElevated` border | | `bottomNavigationBar` | `Widget?` | — | e.g. [StyledNavigationBar](/components/styled-navigation-bar) | | `floatingActionButton` (+`Location`) | `Widget?` | — | Standard Material FAB slot | | `extendBodyBehindAppBar` | `bool` | `false` | Full-bleed behind the app bar | | `extendBodyBehindNavigationBar` | `bool` | `false` | Maps to `Scaffold.extendBody` | | `resizeToAvoidBottomInset` | `bool` | `true` | Keyboard avoidance | | `onLeadingPressed` | `VoidCallback?` | beam home | Back/leading action | | `backgroundColor` | `Color?` | theme `surfaceBg` | — | | `uiOverlayBrightness` | `Brightness?` | theme brightness | Status-bar icon color | | `canPop` | `bool` | `false` | See back-button behavior above | ## StyledAppBar A compact toolbar (**`ashToolbarHeight = 40`**, vs. Material's 56) built on `NavigationToolbar` inside a `SafeArea`. Title renders in `titleMedium` / `contrastMedium`, centered. When `onBackButtonPressed` is provided and no custom `leading`, a `BackButton` appears. Padding knobs: `topPadding` (default `kToolbarHeight - 40`), `bottomPadding` (0), `horizontalPadding` (16). Implements `preferredSize` from its paddings + border + `appBarBottom`. ## ScaffoldCloseButton 40×40 `IconButton` with `Icons.close`, `surfaceElevated` background, `contrastMedium` foreground — the standard dismiss affordance for modal-style screens. ```dart theme={null} StyledScaffold( hideBackButton: true, trailing: ScaffoldCloseButton(onPressed: () => Navigator.pop(context)), body: …, ) ``` ## SystemUiOverlayOverride Sets status-bar icon brightness via `AnnotatedRegion` — dark icons on light themes, light icons on dark. Pass `brightness` explicitly when a screen mixes light content with a dark header (or vice versa). Also exported: `context.systemUiOverlayStyle` for manual use. ## Tokens consumed `surfaceBg` (via theme), `surfaceElevated` (border, close button), `contrastMedium` (title, icons); typography `titleMedium`. > [!NOTE] > > `StyledScaffold` depends on the app's Beamer router for its default back action — it's the one design-system component with a navigation dependency. # StyledTabBar Source: https://design.slingshotai.com/components/styled-tab-bar Pill-shaped segmented tab bar with a sliding indicator A pill-shaped segmented control wrapping Material's `TabBar`: a rounded `surfaceElevated` track with a `surfaceBg` indicator that slides between tabs. Source: [`styled_tab_bar.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/components/styled_tab_bar.dart). ```dart theme={null} StyledTabBar( tabs: const ['Conversations', 'Insights'], selectedIndex: selectedTab, onChanged: (index) => setSelectedTab(index), ) ``` ## API | Parameter | Type | Notes | | - | - | - | | `tabs` | `List` | **required** — tab labels | | `selectedIndex` | `int` | **required** — controlled selection | | `onChanged` | `ValueChanged` | **required** — fires on user taps | This is a **controlled component**: it doesn't own selection state. Internally (it's a `HookWidget`) a `useTabController` plus two `useEffect`s keep things in sync — one animates the controller when `selectedIndex` changes externally, the other listens to the controller and calls `onChanged` when the user taps (skipping intermediate `indexIsChanging` frames). ## Visual anatomy * Track: `surfaceElevated`, radius 32, 4 px padding * Indicator: `surfaceBg`, radius 24, soft black-26 shadow, sized to the tab * Labels: `buttonMedium` / `contrastBold` for **both** states — selection is communicated by the indicator, not label color * 40 px tab height; no splash/overlay (selection is the feedback) ## Tokens consumed `surfaceElevated`, `surfaceBg`, `contrastBold`; typography `buttonMedium`. > [!NOTE] > > One of only two hook-based widgets in the design system (the other is app-level `CarouselListView`) — components stay `StatelessWidget` unless they genuinely need local state. See [Conventions](/patterns/conventions). # StyledTextField Source: https://design.slingshotai.com/components/styled-text-field Themed TextField wrapper with a built-in character counter — filled, outlined, and underline variants A wrapper around Flutter's `TextField` that applies Sundial styling and adds a **threshold-based character counter**. Source: [`styled_text_field.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/components/styled_text_field.dart). ```dart theme={null} StyledTextField( controller: controller, hintText: 'Enter text', maxLength: 250, // default showCounterAfterChars: 200, // counter appears at 200 chars ) ``` ## Variants | Constructor | Look | | - | - | | `StyledTextField(…)` | **Filled** — `surfaceElevated` fill, radius 16, no stroke | | `StyledTextField.outlined(…)` | `surfaceBg` fill with a 2 px `surfaceHigh` stroke; focus recolors the stroke to `contrastSubtle` | | `StyledTextField.underline(…)` | No fill; 3 px `contrastWeak` bottom underline; compact 8 px vertical padding | ## Character counter The default Material counter is suppressed; Sundial's own counter renders **only once the text reaches `showCounterAfterChars`** (default 200 of `maxLength` 250), as `current/max` in `bodySmall` / `contrastMedium`. Position it with `counterAlignment`: ```dart theme={null} enum TextFieldCharCounterAlignment { topLeft, topRight, bottomLeft, bottomRight } ``` ## API | Parameter | Type | Default | | - | - | - | | `controller` | `TextEditingController?` | — | | `hintText` | `String?` | — | | `maxLength` | `int` | `250` | | `showCounterAfterChars` | `int` | `200` | | `counterAlignment` | `TextFieldCharCounterAlignment` | `bottomRight` | | `minLines` / `maxLines` | `int?` | — | | `textCapitalization` | `TextCapitalization` | `sentences` | | `textInputType` | `TextInputType` | `text` | | `textAlign` | `TextAlign` | `left` | | `textInputAction` | `TextInputAction?` | — | | `closeKeyboardOnTapOutside` | `bool` | `true` — tapping outside unfocuses | | `onChanged` / `onSubmitted` | callbacks | — | | `focusNode` / `autofocus` / `enabled` | — | `false` / `true` | | `prefixIcon` / `suffixIcon` | `Widget?` | — | | `fillColor` | `Color?` | variant default (filled only) | | `cursorColor` | `Color?` | theme accent | | `borderRadius` | `double` | `16` (0 for underline) | | `borderSide` | `BorderSide?` | — (default ctor only) | | `style` | `TextStyle?` | `bodyLarge` | Other built-in behavior: hint in `contrastWeak`, prefix icon tinted `contrastSubtle`, and `scrollPadding` of 300 px so the field scrolls clear of the keyboard. ## Tokens consumed `surfaceElevated`, `surfaceBg`, `surfaceHigh`, `contrastWeak`, `contrastSubtle`, `contrastMedium`; typography `bodyLarge`, `bodySmall`. ## Real usage ```dart profile_settings.dart theme={null} StyledTextField( controller: monikerController, hintText: 'What should Ash call you?', textCapitalization: TextCapitalization.words, ) ``` For voice input, `TextFieldWithTranscription` (in `lib/widgets/common/`) wraps this component with a transcription button — see the [common widgets catalogue](/appendix/common-widgets). # ThemeOverride Source: https://design.slingshotai.com/components/theme-override Rebuild the Material theme for a subtree — brightness-only in Sundial 2.0 A utility widget that rebuilds the Material `Theme` for a subtree using `AppTheme.light` or `AppTheme.dark` — the sanctioned way to locally flip brightness (dark chrome over imagery, a dark-locked sheet). Source: [`theme_override.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/components/theme_override.dart). ```dart theme={null} ThemeOverride( brightness: Brightness.dark, child: ..., ) ``` ## Props | Prop | Type | Default | Notes | | - | - | - | - | | `brightness` | `Brightness` | required | Theme for the subtree | | `child` | `Widget` | required | | > [!NOTE] > > The `accentPalette` parameter was removed with the persona system in Sundial 2.0 — brightness is the only axis. See [Accent Theming](/theming/persona-theming). ## Behavior The override installs a **complete** theme — ThemeExtensions, Material component themes, ColorScheme, TextTheme — not a partial patch. And since all Sundial tokens implement `lerp`, entering/changing an override animates via `AnimatedTheme` when it replaces an existing theme in place. The canonical use is textured or dark surfaces whose content must read light regardless of the app theme: `StyledListTile` wraps its content in `ThemeOverride(brightness: Brightness.dark)` over textured wallpapers, and the feature-intro and force-update modals lock their chrome the same way. # Accent Colors Source: https://design.slingshotai.com/foundations/colors/accent Layer 3 — the single lavender accent and its bright-solid grammar (personas retired) Sundial 2.0 has **one accent: Periwinkle** (`#DBE1FD`), the brand lead — it replaced Lavender Blue `#CDCCF0` in October 2026. The code keeps the `lavender` names. The persona system — five palettes recoloring the app per selected persona — was retired with 2.0; `AccentPalette`, `AppTheme.fromPalette`, and the palette machinery are gone. Defined in [`tokens/accent_colors.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/tokens/accent_colors.dart) as two instances, `AccentColors.lavenderLight` and `.lavenderDark`, installed by the matching theme. ## The bright-accent grammar Lavender is a Radix **bright** scale: the accent solid is *pale* and takes *dark* content — the inverse of 1.0's dark plum grammar. | Field | 2.0 token | Value | | - | - | - | | `accent.button` | `accentSolid` | Lavender 9 `#DBE1FD`, invariant | | `accent.onButton` | `contentOnAccent` | Pure black `#000000` (`brandBlack`), invariant — the solid is invariant, so its content is too | | `accent.secondary` | `contentAccent` | Lavender-light 12 `#252C4A` / lavender-dark 11 `#8F9BD2` — light step 11 reads royal blue as text | | — | `accentLine` | Lavender-light 8 `#6977B5` / lavender-dark 11 `#8F9BD2` — outlines, checkboxes, progress; 3:1, never text | | `accent.background` | — | Lavender 3 tint — selection fills, chip tints | | `accent.base` | — | A `ColorSwatch` for legacy call sites; its **default value is the accent content tier** (light 12, dark 11), so bare `accent.base` reads (spinners, progress) stay legible | > [!WARNING] > > `accent.button` is a **surface**, not a foreground. Using it for text, icons, borders, or rings on light surfaces yields \~1.2:1 contrast — this was the largest class of bugs found in the 2.0 audit. Text and glyphs take `contentAccent`; accent line work (outlines, checkboxes, progress) takes `accentLine`; selection borders take `borderSelected`. ## Migration path The `accent.*` fields are the legacy API. During the rename phase they migrate to the semantic tokens directly (`accentSolid`, `contentOnAccent`, `contentAccent`), after which `AccentColors` is deleted. New code should use the semantic names now. # Reference Colors Source: https://design.slingshotai.com/foundations/colors/reference Layer 1 — Sundial 2.0 primitives: warm sand scales, alpha inks, lavender accent scales, brand flats, and status stops Reference colors are Sundial's **primitives** — the raw material every other color token is built from. Sundial 2.0's primitives live in [`tokens/sundial_reference_colors.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/tokens/sundial_reference_colors.dart) as `abstract final class SundialRef`, structured on the [Radix Colors](https://www.radix-ui.com/colors) 12-step model: **same step = same job**, in light and dark alike. > [!WARNING] > > Don't use reference colors directly in feature code. Use the [semantic tokens](/foundations/colors/semantic) (`context.colorTheme.*`), which pick the right primitive for the current brightness. Legitimate direct uses are rare and deliberate — wallpaper palettes, pinned sign-in chrome, and the token definitions themselves. ## The 12-step model Each scale is a `ColorSwatch` keyed `1`–`12`. Steps are grouped into bands, and the band tells you the job: | Steps | Band | Jobs | | - | - | - | | 1–2 | Canvas | App background, subtle tints | | 3–5 | Components | Fills for bars, chips, inputs; resting → selected states | | 6–8 | Borders | Separators, input borders, strong outlines | | 9–10 | Solids | Bounded object fills — never text | | 11–12 | Text | Secondary (lowest AA-safe) and primary content | Movement **within** a band is a state change; crossing a band is a change of identity. One step off the canvas is a tint, not a boundary. ## Warm sand The working neutral — sand warmed by an absolute Lab chroma delta (+1.06 at hue 85°) so mid steps stay grey-beige rather than tan. Step 3 is the spacing-true stop (`#F2F0ED`); the brand book's light beige lives separately as a [brand flat](#brand-flats). **Light** (`SundialRef.sandLight`): **Dark** (`SundialRef.sandDark`): ## Alpha inks Each mode also carries a translucent **ink** scale (`sandAlphaLight`, `sandAlphaDark`): dark ink for light mode, light ink for dark mode. An alpha step composited over the mode's canvas reproduces the matching solid step — which means one ink works on *any* surface, picking up the surface's own hue in the composite. This is what powers pressed states (`ink/pressed`), scrims (`overlay/scrim` = light-alpha 11 in both modes), and the [on-brand secondary tier](/foundations/colors/semantic#content). ## Lavender The accent — a Radix **bright** scale (sky-templated), tuned periwinkle: step 9 is the *pale* brand solid `#DBE1FD`, anchored identically in both modes, and takes **dark** content. Steps 11–12 are the deep text tiers. The ramp was derived from that single anchor: each step is the previous lavender step moved by the same OKLCH hue shift (−10.6°), with chroma eased near the anchor and kept full on the deep steps, and lightness held so every step keeps its contrast. > [!WARNING] > > This scale is non-monotonic by design: step 9 is lighter than step 8. Never assume step+1 = darker. Line and text stops (light 8, dark 11) are tuned to hold their contrast minimums on the step-5 *selected* surface, not just the canvas. **Light** (`SundialRef.lavenderLight`): **Dark** (`SundialRef.lavenderDark`): ## Brand flats Mode-invariant spotlight colors from the Brand Guidelines, exposed as constants (`SundialRef.brandLavender`, `.brandGreen`, …): Periwinkle `#DBE1FD` · Green `#9ED99D` · Orange `#F6CF98` · Yellow `#F5EAAC` · Dark Beige `#CEC5B8` · Light Beige `#F4F2EF` · White · Black. > [!NOTE] > > Light beige is deliberately **not** warm sand step 3 (`#F2F0ED`): the scale keeps its spacing-true step while the brand color keeps the brand-book hex. Nearby, never interchangeable. ## Status stops Cherry (error) and amber (warning) carry five stops each — `100 / 400 / 500 / 600 / 800` — enough for content, solid, border, and surface roles without a full ramp: ## The 1.0 `ReferenceColors` during migration The 1.0 primitives ([`tokens/reference_colors.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/tokens/reference_colors.dart)) still exist while call sites migrate. The old brand flats (`sunflower`, `grass`, `tomato`, `sky`, `rock`, …) have been remapped **in place** to the nearest 2.0 brand color, so existing consumers already render 2.0 values; the parchment/wood/plum ramps are scheduled for deletion at the end of the migration. # Semantic Colors Source: https://design.slingshotai.com/foundations/colors/semantic Layer 2 — the 46 purpose-named tokens of DesignSystemColors, with the band grammar that governs them Semantic colors define color **by purpose**, not by value. They live in [`tokens/semantic_colors.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/tokens/semantic_colors.dart) as `DesignSystemColors`, a Flutter `ThemeExtension` with two static instances — `DesignSystemColors.light` and `DesignSystemColors.dark` — built from a single brightness factory so the modes cannot drift, installed by `AppTheme`, and read via `context.colorTheme`. ```dart theme={null} final colors = context.colorTheme; Text('Hello', style: TextStyle(color: colors.contentPrimary)); Container(color: colors.surfaceComponent); ``` ## The grammar Four rules govern every token below: 1. **Same step, same job.** Light and dark use the same scale position for the same role — optical relationships survive the mode switch. 2. **Containers set loudness; glyphs set legibility.** Surfaces are planes content sits on; content tokens guarantee contrast against the whole surface band they're legal on. 3. **Persistent states get tokens, transient states get ink.** Selection is a token (`stateSelected`); a press is an alpha ink composited over whatever it touches. 4. **Disabled is a collapse.** Container drops to step 5, content to step 8 — deliberately washy, WCAG-exempt. ## Surfaces | Token | Step | Job | | - | - | - | | `surfaceBg` | 1 | The canvas | | `surfaceSubtle` | 2 | Region tints, resting inputs — a tint, not a boundary | | `surfaceComponent` | 3 | Bars, tracks, chips, bubbles | | `surfaceComponentStrong` | 5 | Chips nested on components, notice banners | ## Solids & accent | Token | Value | Job | | - | - | - | | `solidInverse` | step 12 | The full-span labeled primary (CTA discs, send) — pairs with `contentOnInverse` | | `solidDefault` | step 9 | Toggles, sliders, meters, dots — never text | | `solidSubtle` | step 8 | Inert small solids: grabbers, scroll indicators | | `accentSolid` | lavender 9, invariant | Primary accent actions — a **pale** solid that takes **dark** content | | `accentLine` | lavender-light 8 / lavender-dark 11 | Accent line work and small non-text graphics: selected outlines, checkboxes, progress — 3:1, never text | ## Content | Token | Value | Job | | - | - | - | | `contentPrimary` | step 12 | Titles, values, body emphasis | | `contentSecondary` | step 11 | Labels, helper text, UI icons — lowest AA-safe tier | | `contentPlaceholder` | step 10 | Placeholder text only — below AA by design | | `contentDisabled` | step 8 | Disabled labels — WCAG-exempt | | `contentOnInverse` | step 1 | Content on `solidInverse` | | `contentOnAccent` | brand black, invariant | Content on `accentSolid` — pure black in both modes | | `contentAccent` | lavender-light 12 / lavender-dark 11 | Accent-colored text and selected glyphs | | `contentOnBrand` | sand-light 12, invariant | Primary ink on light/pastel brand surfaces | | `contentOnBrandSecondary` | light-alpha 11, invariant | Secondary ink on brand surfaces — an **alpha ink**, so the surface supplies the hue and one token harmonizes on every brand color (a solid grey fails AA on four of five). AA 4.91–5.63:1 across the brand set | | `contentOnBrandDark` | sand-light 1, invariant | Ink on dark brand surfaces (black) | ## Borders | Token | Value | Job | | - | - | - | | `borderSubtle` | step 6 | Separators, hairlines | | `borderDefault` | step 7 | Inputs, outlined chips — quiet by design, no 3:1 promise | | `borderStrong` | step 8 | Emphasized outlines | | `borderSelected` | lavender-light 8 / lavender-dark 9 | Selection rings — holds ≥3:1 on the step-5 *selected* surface, not just the canvas | | `focusRing` | same values as `borderSelected` | Where typing goes vs. what you chose — same values today, distinct meaning | | `borderOnBrand` | dark alpha ink | Rings on brand surfaces | ## States, inks, overlay | Token | Value | Job | | - | - | - | | `stateSelected` | step 5 | Selected surface state | | `stateDisabledContainer` | step 5 | The disabled collapse — pair with `contentDisabled` | | `inkPressed` | alpha 4, mode's own ink | The pressed state for surface-polarity fills — maps to `InkWell.highlightColor` | | `inkPressedOnInverse` | alpha 4, opposite ink | Pressed ink for `solidInverse` and pale solids on dark — the mirror, same step | | `overlayScrim` | light-alpha 11, invariant | Sheet/modal scrim — always-darkening ink | ## Brand surfaces Mode-invariant spotlight planes, chosen per card: `surfaceBrandLavender`, `surfaceBrandGreen`, `surfaceBrandOrange`, `surfaceBrandYellow`, `surfaceBrandDarkBeige`, `surfaceBrandLightBeige`, `surfaceBrandWhite`, `surfaceBrandBlack`. Elements sitting on a mode-invariant surface must themselves be invariant — hence the pinned `contentOnBrand*` and `solidOnBrand` inks above. ## Status Error and warning each carry four roles from the [cherry/amber stops](/foundations/colors/reference#status-stops): `statusErrorContent` (600 light / 400 dark — AA on the whole surface band), `statusErrorSolid` and `statusErrorBorder` (500 both modes — ≥3:1 non-text), `statusErrorSurface` (100 light / 800 dark — banner tints that pair with status content, never neutral content). Same shape for `statusWarning*`. > [!NOTE] > > Destructive fills borrow the inverse pattern: `statusErrorContent` as the fill with `surfaceBg` as its ink gives an AA pair in both modes (deep red + near-white in light; pale red + near-black in dark). ## Legacy names The 1.0 names still compile as **forwarding getters** while call sites migrate — `contrastStrong → contentPrimary`, `contrastMedium → contentSecondary`, `contrastWeak → contentPlaceholder`, `surfaceElevated → surfaceComponent`, `surfaceHigh → surfaceComponentStrong`, `disabledContainer → stateDisabledContainer`, `badgeRed → statusErrorSolid`, `badgePurple → accentSolid`, `errorColor → statusErrorContent`, `warningColor → statusWarningContent`, and the `surfaceOpacity01–05` tiers pin to the light alpha scale in both modes (1.0 scrims are always dark ink). New code uses the 2.0 names; the getters are removed once migration completes. # Iconography Source: https://design.slingshotai.com/foundations/iconography Two icon systems: custom Ash SVG icons and the Material Symbols Rounded font Sundial has **two icon systems**, used side by side: > [card] AshIcons — custom SVGs > > 21 brand-specific icons (personas, activities, third-party brand marks) rendered by the `AshIcon` widget from `assets/icons/*.svg`. > [card] Symbols — Material Symbols Rounded > > The full Material Symbols Rounded set as icon fonts, in outlined and filled variants, exposed as `IconData` constants. ## AshIcons (custom SVG) Defined in [`components/ash_icon.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/components/ash_icon.dart). Each enum value maps to `assets/icons/{value}.svg`: | Group | Icons | | - | - | | Brand mark | `ashFlower` | | Activities | `activitiesFiveWhys`, `activitiesPies`, `activitiesValuesSort`, `activitiesRelationshipMapper` | | Personas | `personaAsh`, `personaCircle`, `personaDiamond`, `personaHeart`, `personaTriangle` | | Third-party brands | `brandApple`, `brandGoogle`, `brandOpenAI`, `brandReddit`, `brandSpotify`, `brandSteam`, `brandXTwitter`, `brandYouTube` | | Features | `conversation`, `insights`, `weeklyInsights` | ```dart theme={null} // Local asset icon, tinted with the current icon color AshIcon(AshIcons.conversation, size: 24) // Keep the SVG's own colors (brand logos) AshIcon(AshIcons.brandGoogle, useOriginalColor: true) // Remote SVG AshIcon.network('https://…/icon.svg', size: 32) ``` Color and size resolve in this order: explicit parameter → surrounding `IconTheme` → `context.colorTheme.contrastStrong`. Unless `useOriginalColor: true`, the SVG is tinted via `ColorFilter.mode(color, BlendMode.srcIn)`. > [!NOTE] > > **Adding an icon:** drop the SVG into `assets/icons/` and register it in the `AshIcons` enum — unregistered files can't be used. See [Contributing](/patterns/contributing). ### Accessibility convention `semanticLabel` defaults to `null`, which **hides the icon from screen readers** (decorative). Icon-only buttons should label at the button layer (e.g. `IconButton.tooltip`) instead of the icon, to avoid double announcements. ## Symbols (Material Symbols Rounded) [`symbols.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/symbols.dart) is a **generated** glyph map (\~600 KB — don't edit by hand) exposing the complete Material Symbols Rounded set as `IconData`: ```dart theme={null} // Outlined (default) Icon(Symbols.settings) // Filled variant — same name with a _filled suffix Icon(Symbols.settings_filled) ``` Two icon fonts back it, declared in `pubspec.yaml`: `MaterialSymbolsRounded` (outlined) and `MaterialSymbolsRoundedFilled` (filled). Use `Symbols.*` for general UI iconography; reserve `AshIcons` for brand-specific artwork that doesn't exist in Material Symbols. ## Related widgets * [`AshIcon`](/components/ash-icon) — the SVG icon widget * [`AshLogo`](/components/ash-logo) — the Ash wordmark/logo (`assets/icons/AshLogo.svg`), separate from the icon enum # Shape & Spacing Source: https://design.slingshotai.com/foundations/shape-and-spacing Radii, padding, and elevation conventions — and what is (deliberately) not tokenized > [!NOTE] > > Unlike colors and typography, Sundial has **no central spacing/radius/shadow token file**. Shape and spacing are conventions enforced by the component themes in [`app_theme.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/app_theme.dart) and by the `Styled*` components themselves. This page documents those conventions as they exist in code. ## Corner radii | Radius | Where | | - | - | | **16** | Elevated / outlined / text buttons, `StyledCard` (`kDefaultCardBorderRadius = 16.0`), `StyledTextField` | | **8** | Filled buttons, dialogs | | **12** | Menus | | **24** | Styled bottom sheet card | | **32** | Chips (fully pill at typical heights) | | **Pill / stadium** | `StyledNavigationBar`, `StyledFloatingActionButton`, `StyledTabBar` | The only *exported* radius constant is `kDefaultCardBorderRadius = 16.0` in [`styled_card.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/components/styled_card.dart). Everything else is inline in the component themes. **Rule of thumb:** primary tappable surfaces and cards are 16; dense system surfaces (dialogs, filled buttons) are 8; anything navigational is a pill. ## Padding Recurring values from the component themes: | Padding | Where | | - | - | | `EdgeInsets.symmetric(vertical: 17, horizontal: 20)` | All button styles (elevated, outlined, text, filled) | | `EdgeInsets.all(16)` | Input fields (`contentPadding`) | | `EdgeInsets.all(20)` | `StyledCard` default content padding (`kDefaultCardPadding`) | | `EdgeInsets.symmetric(vertical: 10, horizontal: 16)` | Chips | | `EdgeInsets.all(8)` | Icon buttons (within a fixed 40×40 hit target) | ## Elevation & shadows Sundial is intentionally **flat**: buttons set `elevation: 0`, the navigation bar sets `elevation: 0`, and surfaces communicate hierarchy through the `surfaceBg → surfaceElevated → surfaceHigh` color ladder instead of shadows. The exception is `StyledCard`'s default (elevated) variant, which draws a subtle shadow derived from its `elevation` parameter (default `1.0`). The `.outlined()` and `.filled()` variants are shadowless. ## Sizing | Value | Where | | - | - | | 40 × 40 | Icon button fixed size (24 px icon) | | 24 | Standard icon size in buttons | | 330 | Drawer width | | 32 × 4 | Bottom-sheet drag handle | | 4 | Slider track height (12 px thumb radius) | | 40 | `ashToolbarHeight` — the `StyledAppBar` toolbar height | ## Layout spacing Gaps between siblings use Flutter's modern `spacing` parameter rather than `SizedBox` separators: ```dart theme={null} Column( spacing: 8, children: [/* … */], ) Row( spacing: 12, children: [/* … */], ) ``` There is no enforced spacing scale; common values in the codebase are 4, 8, 12, 16, 20. ## Animation durations Also not centrally tokenized — durations are declared where they're used (e.g. wallpaper image fade-in is 500 ms). When adding motion, match the durations of neighboring components rather than inventing new ones. > [!NOTE] > > If a central spacing/radius/duration token file is ever introduced, it should live in `lib/design_system/tokens/` alongside the color and typography tokens — and this page should become a spec table like the others. # Typography Source: https://design.slingshotai.com/foundations/typography 15 text styles in 5 categories, set in Libertinus Serif and Inter (variable weight) Sundial's type system is **15 tokens** — 5 categories × 3 sizes — defined in [`tokens/typography.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/tokens/typography.dart) as `DesignSystemTypography`, a `ThemeExtension` accessed via `context.textTheme`. ```dart theme={null} final typography = context.textTheme; Text('Big heading', style: typography.displayLarge); Text('Section title', style: typography.titleMedium); Text('Body text', style: typography.bodyLarge); Text('LABEL', style: typography.overlineSmall); ``` > [!WARNING] > > The fonts are **Libertinus Serif** and **Inter**. (Some older internal docs mention GoogleSans — that's stale; the shipped app uses Inter.) ## The two typefaces | Typeface | Role | Notes | | - | - | - | | **Libertinus Serif** | Display styles — large expressive headings | Regular (400). Asset: `LibertinusSerif-Regular.otf` | | **Inter** | Everything else — titles, body, overlines, buttons | Variable font; weights applied via `FontVariation.weight(…)`, plus an italic asset | Weights are set with `fontVariations: [FontVariation.weight(n)]` rather than `fontWeight`. That's what enables the body styles' signature **weight 425** — an in-between weight only a variable font can render. ## Full spec | Token | Family | Weight | Size | Line height | Letter spacing | | - | - | - | - | - | - | | `displayLarge` | Libertinus Serif | 400 | 40 | 1.0 | −1.0 | | `displayMedium` | Libertinus Serif | 400 | 36 | 1.0 | −0.75 | | `displaySmall` | Libertinus Serif | 400 | 32 | 1.0 | −0.5 | | `titleLarge` | Inter | 600 | 23 | 1.2 | −0.25 | | `titleMedium` | Inter | 600 | 20 | 1.2 | −0.1 | | `titleSmall` | Inter | 600 | 18 | 1.2 | −0.1 | | `bodyLarge` | Inter | 425 | 16 | 1.5 | −0.25 | | `bodyMedium` | Inter | 425 | 14 | 1.5 | −0.25 | | `bodySmall` | Inter | 425 | 12 | 1.5 | −0.25 | | `overlineLarge` | Inter | 500 | 16 | 1.2 | +1.5 | | `overlineMedium` | Inter | 500 | 14 | 1.2 | +1.25 | | `overlineSmall` | Inter | 500 | 12 | 1.2 | +1.25 | | `buttonLarge` | Inter | 600 | 18 | 1.2 | −0.25 | | `buttonMedium` | Inter | 600 | 14 | 1.2 | −0.25 | | `buttonSmall` | Inter | 600 | 10 | 1.2 | −0.25 | Notable details: * **Overlines are the only styles with positive (wide) letter spacing** — they're for small uppercase-feel labels. * Display styles have a tight **1.0 line height**; body styles breathe at **1.5**. * Sizes are logical pixels; `height` is a multiplier of `fontSize` (Flutter semantics). > [!NOTE] > > The samples on this site render in the docs theme's fonts, not the app's — refer to the spec table for exact metrics, or run the app to see the real thing. ## Category guidance | Category | Use for | | - | - | | **Display** | Hero moments and emotional headings — Ash's serif voice (letters, onboarding, insights) | | **Title** | Screen and section titles, app bar titles | | **Body** | Paragraphs, list content, general UI text | | **Overline** | Small labels, eyebrow text, metadata | | **Button** | Buttons, tabs, nav bar labels | ## Color variants Three static instances: * `DesignSystemTypography.base` — **colorless**; the styles as pure metrics * `DesignSystemTypography.light` — every style colored **parchment 800** (`272725`) — migrating to `contentPrimary` when the 1.0 ramps are deleted * `DesignSystemTypography.dark` — every style colored **parchment 200** (`DDDAD1`) `AppTheme` installs `.light`/`.dark` as the `ThemeExtension`, so `context.textTheme` styles arrive pre-colored for the current mode. Override color per-use with `copyWith`: ```dart theme={null} Text( 'Welcome', style: context.textTheme.titleLarge.copyWith( color: context.colorTheme.contrastBold, ), ) ``` ## Font assets Declared in [`pubspec.yaml`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/pubspec.yaml) under `fonts:`: | Family | Assets | In the type system? | | - | - | - | | `Inter` | `Inter.ttf`, `Inter-Italic.ttf` (italic) | ✅ titles, body, overlines, buttons | | `LibertinusSerif` | `LibertinusSerif-Regular.otf` | ✅ display | | `LaBelleAurore` | `LaBelleAurore-Regular.ttf` | Decorative only (handwritten accents) | | `RockSalt` | `RockSalt-Regular.ttf` | Decorative only | | `MaterialSymbolsRounded` | `MaterialSymbolsRounded.ttf` | Icon font — see [Iconography](/foundations/iconography) | | `MaterialSymbolsRoundedFilled` | `MaterialSymbolsRounded_Filled.ttf` | Icon font (filled variant) | ## Material TextTheme bridge `AppTheme` also maps these tokens into Flutter's stock `TextTheme` so Material widgets and third-party code pick them up: DS display/title/body map to their Material namesakes, and **DS overline styles fill Material's `label*` slots** — all colored `contrastStrong`. See [Material bridge](/theming/material-bridge). # Wallpapers Source: https://design.slingshotai.com/foundations/wallpapers The Wallpaper system: solid brand colors, server-supplied imagery, and vector gradient blobs Wallpapers are Sundial's decorative background system — used behind cards, list tiles, and hero surfaces. The model is a sealed class hierarchy in [`components/ash_wallpaper.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/components/ash_wallpaper.dart) rendered by the [`AshWallpaper`](/components/ash-wallpaper) widget: ```dart theme={null} sealed class Wallpaper {} class SolidWallpaper implements Wallpaper { … } // flat brand color class ImageWallpaper implements Wallpaper { … } // remote image by URL class VectorWallpaper implements Wallpaper { … } // gradient "blobs" ``` ## SolidWallpaper A flat, single-color wallpaper built on the [brand colors](/foundations/colors/reference#brand-colors). Introduced to replace gradients with solid brand colors as part of removing gradients from the app. **Palettes:** | Palette | Colors | Used by | | - | - | - | | `cardPalette` | orange, lavender, sky, grass, sunflower | Conversation-starter cards, weekly insights | | `brandPalette` | cardPalette + tomato, rock | Full cycling/seeding set | **Factories:** ```dart theme={null} // Fixed color SolidWallpaper(ReferenceColors.sky) // One brand color per weekday (Mon=sky, Tue=lavender, Wed=grass, // Thu=sunflower, Fri=orange, Sat=tomato, Sun=rock) SolidWallpaper.forWeekday(DateTime.now().weekday) // Cycle through brandPalette by index — lists of cards SolidWallpaper.brand(index) // Deterministic pick from a seed — same entity, same color every time SolidWallpaper.forSeed(weekNumber, palette: SolidWallpaper.cardPalette) ``` ## ImageWallpaper A single remote image by URL, cached via `cached_network_image`. Reach for it only when the artwork is **supplied by the backend** — it's the one wallpaper type whose content isn't decided in code. ```dart theme={null} ImageWallpaper(imageSrc: 'https://…/header.png') ``` Props: `imageSrc` (required), `scale` (default 1.0), `alignment` (default center), `fadeInDuration` (default 500 ms). While loading (or on error) the wallpaper shows a `surfaceBg` placeholder. It has exactly one use in the app today: the [feature intro modal](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/widgets/common/feature_intro_modal.dart) header, whose image URL arrives from the server. > [!NOTE] > > **The pattern bank is gone (July 29, 2026).** `ImageWallpaper` used to also serve a bank of 26 hand-drawn pattern PNGs through `.pattern(image:)` and `.random(random:)` factories. The five surfaces still on those patterns moved to `SolidWallpaper(ReferenceColors.lavender)` ([#1412](https://github.com/slingshot-ai/ash-mobile-app/pull/1412)), then both factories and the pattern-bank plumbing were deleted ([#1413](https://github.com/slingshot-ai/ash-mobile-app/pull/1413)). > > For a decorative brand background, use [`SolidWallpaper`](#solidwallpaper) — don't reintroduce a remote image for decoration. ## VectorWallpaper Blurred gradient "blobs" over a background color — the most atmospheric option. Each named constructor is a fruit/persona theme with a `bgColor` and three blob colors whose positions/sizes are deterministically randomized from a string seed: | Constructor | Background | Blobs | | - | - | - | | `.home()` | `CA57F4` | 3 × `8B0CAE` | | `.apple()` | `5DB996` | 3 × `118B50` | | `.blackberry()` | `4227B9` | 3 × `A546F3` | | `.blueberry()` | `0A0E86` | `2C6FD9187BCC0B71D1` | | `.cherry()` | `860A19` | `C22CD9D40F6BD10BB3` | | `.dragonfruit()` | `FDD7EF` | 3 × `FC2BAC` | | `.grape()` | `8D68FD` | `DC96E8D44DE0F85BF0` | | `.orange()` | `FF5F00` | 3 × `FF9F66` | | `.peach()` | `FDDF68` | `FC7D58FF7C24FCC358` | | `.pear()` | `C0C000` | `97B61B8ADA00FBEA51` | | `.plum()` | `49243E` | 3 × `704264` | homeappleblackberryblueberrycherrydragonfruitgrapeorangepeachpearplum *(Approximate CSS previews — the app renders soft blurred blobs at deterministic seeded positions.)* `VectorWallpaper.lerp(a, b, t)` interpolates two vector wallpapers — background color and each blob — enabling animated wallpaper transitions. > [!NOTE] > > Vector wallpaper colors are **intentionally off-token** — vivid one-off values for atmosphere, not part of the reference ramps. ## Legibility rule Content over a wallpaper must stay readable. The convention (implemented in [StyledListTile](/components/styled-list-tile)): * **Solid wallpapers** — dark sand-12 ink (`contentOnBrand`), no scrim; the brand colors are light enough. Secondary lines take `contentOnBrandSecondary`, the alpha ink. * **Image/vector wallpapers** — force a **dark-brightness subtree** via [ThemeOverride](/components/theme-override) and add a bottom gradient scrim, so light text sits on a darkened backdrop. The `AshWallpaper` widget's default scrim is `surfaceBg` at 50% alpha; pass a custom `scrim` (or `blur`) as needed. See [AshWallpaper](/components/ash-wallpaper) for the rendering API. # Introduction Source: https://design.slingshotai.com/index Sundial — the design system of the Ash mobile app **Sundial** is the design system that powers Ash, Slingshot AI's mobile app. It defines the colors, typography, iconography, and reusable components used across every screen — with full light/dark mode support and persona-based accent theming. Sundial is **code-first**: the system lives in the Flutter app at [`lib/design_system/`](https://github.com/slingshot-ai/ash-mobile-app/tree/main/lib/design_system), and this site documents exactly what ships in the app. Every hex value, type spec, and API on these pages is drawn from the Dart source. ## What's in the system > [card] [Colors](/foundations/colors/reference) > > A three-layer token system: 9 primitive color ramps, 25 semantic tokens with light/dark mappings, and 5 persona accent palettes. > [card] [Typography](/foundations/typography) > > 15 text styles across Display, Title, Body, Overline, and Button categories — set in Libertinus Serif and Inter. > [card] [Components](/components/styled-card) > > Styled\* wrappers around Material widgets and Ash\* brand primitives — cards, text fields, scaffolds, nav bars, wallpapers, and more. > [card] [Flutter theming](/theming/theme-construction) > > How the tokens become a Material ThemeData: ThemeExtensions, context accessors, persona theming, and the Material bridge. > [card] [Craft & Foundations](/philosophy/overview) > > The why behind the system — essentialism, respect for the user, and the craft rules that make UI feel designed. > [card] [Changelog](/changelog) > > Version history of the system. The navbar version (v1.0.0) tracks Sundial as it ships in the app. ## The one-sentence version Widgets read design tokens from the ambient `Theme` via two context extensions — `context.colorTheme` and `context.textTheme` — and never hardcode colors or text styles: ```dart theme={null} final colors = context.colorTheme; // DesignSystemColors final typography = context.textTheme; // DesignSystemTypography Text( 'Welcome', style: typography.titleLarge.copyWith(color: colors.contrastBold), ) ``` Because tokens are delivered as Flutter `ThemeExtension`s, everything adapts automatically when the platform switches between light and dark mode, or when a subtree is re-themed to a different persona palette. ## Where things live | Area | Source of truth | | - | - | | Color primitives | [`tokens/reference_colors.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/tokens/reference_colors.dart) | | Semantic colors | [`tokens/semantic_colors.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/tokens/semantic_colors.dart) | | Accent palettes | [`tokens/accent_colors.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/tokens/accent_colors.dart) | | Typography | [`tokens/typography.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/tokens/typography.dart) | | Theme construction | [`app_theme.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/app_theme.dart) | | Components | [`components/`](https://github.com/slingshot-ai/ash-mobile-app/tree/main/lib/design_system/components) | > [!NOTE] > > Start with the [Quickstart](/quickstart) if you're building UI today, or the [Architecture](/architecture) page for the mental model behind the system. # Contributing Source: https://design.slingshotai.com/patterns/contributing How to extend Sundial — tokens, icons, components — and keep these docs true Sundial's source of truth is the code in [`lib/design_system/`](https://github.com/slingshot-ai/ash-mobile-app/tree/main/lib/design_system). Changes land there; these docs mirror it. ## Adding a color token **1. Decide the layer** New primitive shade? → `reference_colors.dart`. New purpose? → a semantic token in `semantic_colors.dart` referencing existing primitives. Persona-dependent? → a field on `AccentColors`. **2. Update all the places** For `DesignSystemColors`: the field, the constructor, `.light`, `.dark`, `copyWith`, **and `lerp`**. A token missing from `lerp` snaps instead of animating on theme changes. **3. Wire Material if relevant** If stock Material widgets should use it, add it to the component themes in `app_theme.dart` (see [Material bridge](/theming/material-bridge)). **4. Document it** Add the token to the [Semantic Colors](/foundations/colors/semantic) (or relevant) page with its light/dark mapping. ## Adding a typography token Same drill in `typography.dart`: field + constructor + `base` + `copyWith` + `lerp` (colors come from `light`/`dark` via the blanket `copyWith`). Stick to the 5-category × 3-size grid — a new size within a category beats a new one-off style. ## Adding an icon 1. Drop the SVG in `assets/icons/` (subfolders like `activities/`, `personas/` are fine). 2. Register it in the `AshIcons` enum in `ash_icon.dart` — the enum value's string is the path fragment: `myIcon('MyIcon')` → `assets/icons/MyIcon.svg`. 3. Prefer [Material Symbols](/foundations/iconography#symbols-material-symbols-rounded) for generic glyphs — `AshIcons` is for artwork that's ours. ## Adding a component * **Location**: foundational UI → `lib/design_system/components/` + export from `design_system.dart`; feature-specific reusable → `lib/widgets/common/`. * **Naming**: `Styled*` for Material wrappers, `Ash*` for brand primitives. * **Variants**: private enum + named constructors ([the pattern](/patterns/conventions#the-variant-pattern)). * **Tokens only**: all colors/text via `context.colorTheme` / `context.textTheme`. * **Stateless** unless it genuinely needs local state; no Riverpod in the design system. * **Accessibility**: composite tappables wrapped in `SemanticButton`; decorative icons unlabeled. * **Doc comments**: every public class and non-obvious parameter — doc comments are where these pages come from. * **Docs**: add a component page here following the shared template (purpose → variants → API table → tokens → example → source link). ## Versioning & changelog Sundial is versioned as a system — the navbar version on this site tracks the design system as it ships in the app, not the docs. When you change the system: 1. **Add an `` entry** to the [Changelog](/changelog) describing the change, dated, with the PR link. 2. **Bump the version** when the change warrants it: * **Major** — breaking renames or removals of tokens/components, palette restructuring * **Minor** — new tokens, new components, new component variants * **Patch** — value tweaks (a hex adjustment, a radius change), bug fixes 3. **Update the navbar badge** — the `v…` link in `docs.json`'s `navbar.links` is set by hand; keep it matching the newest changelog entry. Not every commit needs an entry — cosmetic refactors with no visible change can skip the changelog. Every *visible* change to tokens, components, or theming gets one. ## Keeping these docs accurate This site is hand-maintained against the Dart source. When you change tokens or component APIs, update the corresponding page in the same working session. Rule of thumb: **every hex value, metric, and parameter on this site must be traceable to a line of code** — if you can't point to the line, don't write it. # Conventions Source: https://design.slingshotai.com/patterns/conventions The naming, structure, and usage rules that keep Sundial coherent ## Naming prefixes | Prefix | Meaning | Examples | | - | - | - | | `Styled*` | Design-system wrapper around a Material widget, injecting Sundial styling and behavior | `StyledCard`, `StyledScaffold`, `StyledTextField`, `showStyledBottomSheet` | | `Ash*` | Brand-specific visual primitive with no Material counterpart | `AshIcon`, `AshLogo`, `AshWallpaper` | ## The variant pattern Components encode variants as a **private enum + named constructors**, never a public `variant` parameter: ```dart theme={null} enum _CardVariant { elevated, filled, outlined } // private StyledCard(…) // default variant StyledCard.outlined(…) // named constructor sets _variant StyledCard.filled(…) ``` Benefits: invalid parameter combinations are unrepresentable (each constructor only exposes what its variant supports — e.g. `elevation` exists only on the elevated `StyledCard`), and call sites read naturally. `StyledListTile` takes it further: variant-only, no default constructor. ## Token discipline 1. **Never hardcode colors or text styles** — everything comes from `context.colorTheme` / `context.textTheme`. 2. **Semantic over primitive** — `colors.surfaceBg`, not `SundialRef.sandLight[1]`. Direct primitive use is reserved for deliberate cases (wallpaper palettes, the solid-wallpaper dark-text rule). 3. **Accent for persona branding** — persona-responsive UI reads `colors.accent.*`; never a specific palette's colors. 4. **Test both brightness modes** — tokens make it work; you still verify contrast. ## Component reuse Before building a custom widget: ```bash theme={null} ls lib/design_system/components/ # design-system components ls lib/widgets/common/ # app-specific reusable widgets ``` And check how existing call sites use a component: `grep -r "StyledTextField" lib/`. ## State management in components Design-system components are `StatelessWidget` wherever possible. Only [`StyledTabBar`](/components/styled-tab-bar) uses hooks (`HookWidget`) — and only to bridge a `TabController`. **No Riverpod inside `lib/design_system/`** — providers belong to app-level widgets. ## Accessibility * Composite tappables get [`SemanticButton`](/components/semantic-button) — one semantics node, `button: true`. * Icons are decorative by default (`semanticLabel: null` on [`AshIcon`](/components/ash-icon)); label at the button layer. * Nav taps give haptic feedback (`HapticFeedback.selectionClick()` in `NavDestination`). ## Wallpaper legibility Solid wallpaper → dark sand-12 ink (`contentOnBrand`), no scrim. Textured wallpaper → `ThemeOverride(brightness: dark)` + gradient scrim. Canonical implementation: [`StyledListTile`](/components/styled-list-tile). ## Modern Flutter idioms The codebase tracks recent Flutter/Dart features — patterns you'll see inside components: * `Column(spacing: 8)` / `Row(spacing: 12)` instead of `SizedBox` separators * `switch` expressions for variant dispatch * Null-aware collection elements (`?child` inside children lists) * Dot-shorthands for enum/static access (e.g. `mainAxisSize: .min`) * `Color.withValues(alpha: …)` instead of deprecated `withOpacity` Match these idioms when contributing — and check `pubspec.yaml`'s Flutter constraint before assuming an API's availability. # Anti-Patterns Source: https://design.slingshotai.com/philosophy/anti-patterns Design rationale: the never-do-these list — dark patterns, emoji UI, arbitrary values, border overuse, and more These are hard rules, not preferences. If a design does any of these, it's wrong — fix it. * **Dark patterns of any kind.** No manipulative modals, no guilt-tripping copy, no artificial urgency, no buried opt-outs. * **Emojis in UI** unless they are user-generated content. The product chooses its personality through design, not emoji. * **Bad typography.** Inconsistent hierarchy, too many font sizes, decorative type without purpose. See [Typography](/philosophy/craft/typography). * **Uncomfortable spacing.** Generous, consistent whitespace. When something feels wrong, it's usually cramped or inconsistently spaced. Err on the side of breathing room. * **Unjustified elements.** Every button, label, section, and interaction must have a reason for existing. If you can't articulate why it's there, remove it. * **Gratuitous visual complexity.** No glassmorphism, gradients, shadows, or animation unless they serve a specific functional or emotional purpose. * **Designing for the business instead of the user.** If a feature exists primarily to move a metric rather than help the person, flag it. * **Border overuse.** Don't reach for borders by default to separate content. Spacing, background color changes, and subtle shadows are almost always cleaner. See [Color & Depth](/philosophy/craft/color-and-depth). * **Stretching content to fill space.** On large screens, content should have a comfortable max-width. Don't let it stretch edge-to-edge just because the viewport allows it. * **Neglected empty states.** Never leave a blank state undesigned. “No results” or “Nothing here yet” without intention is a missed opportunity. * **Arbitrary values.** Never use a spacing value, font size, or color that isn't from the design system's defined scale. If it feels “close enough,” use the nearest system value instead. In Sundial terms: tokens from [Semantic Colors](/foundations/colors/semantic) and [Typography](/foundations/typography), never hand-picked values. > [!NOTE] > > The system-level enforcement of several of these lives in [Conventions](/patterns/conventions) — e.g. “never hardcode colors or text styles” is the code-level form of the arbitrary-values rule. # Color & Depth Source: https://design.slingshotai.com/philosophy/craft/color-and-depth Design rationale: building palettes, purposeful shadows, borders as a last resort, and finished edges ## Color When building or extending a palette: * **You need more colors than you think.** A real UI needs 8–10 shades of grey, 5–9 shades of the primary, and multiple shades per semantic color. (Sundial's answer: 11-shade ramps for every family — see [Reference Colors](/foundations/colors/reference).) * **Use HSL** — it maps to human perception far better than hex or RGB. * **Don't just lighten and darken — rotate the hue.** Lighter shades shift toward the nearest bright hue (60°, 180°, 300°), darker shades toward the nearest dark hue (0°, 120°, 240°). This produces richer, more natural ramps. * **Define the edges first:** pick the darkest and lightest shades, then fill the gaps. The middle shade should work as a button background. * **Never rely on color alone to communicate meaning.** Supplement with icons, weight, or position — critical for accessibility. * **No grey text on colored backgrounds** — it looks washed out. Use a lighter or darker shade of the background's own hue. (This is the `onButton` = accent-200 rule — see [Accent Colors](/foundations/colors/accent).) ## Depth & shadows Shadows serve a purpose or don't exist: * **One light source, from above.** Every shadow in the UI agrees on where light comes from. * **A small elevation system** (3–5 levels), subtle to pronounced: tight shadows for low elevation (buttons), larger diffused ones for high elevation (modals, dropdowns). * **Combine two shadows** for realism: a tight dark one at the contact edge plus a soft ambient one. * **Shadows communicate interactivity** — raised feels clickable, flat feels static. Use that intentionally. * In flat contexts, create depth **through color**: lighter surfaces read as closer. This is Sundial's primary mechanism — the `surfaceBg → surfaceElevated → surfaceHigh` ladder — with real shadows reserved for genuinely floating things (the nav bar, the elevated card). ## Borders & separation Borders are a **last resort** for separating content. In order, try: 1. More spacing 2. A different background color 3. A subtle shadow 4. …and only then a border — light and low-contrast, so it separates without shouting Too many borders make an interface feel busy; every border competes for attention. ## Images & user content * User-uploaded content (avatars, photos) lives in **fixed-size containers** with cover-fit cropping. Never let user content dictate layout dimensions. * Text over images needs **guaranteed** legibility: overlays, lowered image contrast, or text scrims — don't hope the image is dark enough. (Sundial's wallpaper scrim rules encode this — see [Wallpapers](/foundations/wallpapers#legibility-rule).) * Icons and images have an intended display size — don't scale a 16px icon to 64px. Use the right asset, or enclose a small icon in a larger shape. * Prevent background bleed on light-edged images with a subtle inset shadow rather than a border. ## Empty states & finishing touches * **Empty states are design opportunities, not afterthoughts.** A blank screen with “No data yet” is a missed chance to guide, reassure, or delight. * **Replace defaults with intentional choices:** custom bullet icons, styled checkboxes, accent borders on cards and alerts. Small upgrades to defaults signal craft. * Accent borders or colored top-edges add personality without complexity. * Break up monotonous layouts by alternating section background colors. # Robustness & Readability Source: https://design.slingshotai.com/philosophy/craft/robustness Design rationale: line length, accessible contrast, content overflow, and alignment — where generated UIs fail These rules target the places where UIs — especially generated ones — consistently fail with real content and real users. ## Respect line length Body text sits between **45–75 characters per line**; the sweet spot is 60–70. Under 40, the eye develops a jittery rhythm; over 80, it loses its place on the return sweep. This is a documented perceptual constraint, not an aesthetic preference. Watch for the classic failures: full-width card descriptions, hero subtitles spanning the viewport, helper text stretching under a wide form. Give prose its own max-width, independent of its container. Longer lines also need more generous line height — see [Typography](/philosophy/craft/typography) for the pairing table. ## Never sacrifice accessibility for hierarchy De-emphasis (lighter, muted text) taken too far produces text real humans can't read. | Text type | WCAG AA minimum | | - | - | | Normal text (under 18px, or under 14px bold) | **4.5:1** | | Large text (18px+, or 14px+ bold) | **3:1** | | Non-text UI (icons, borders, focus rings) | **3:1** | Your “tertiary” color is the lightest color that *still passes 4.5:1* at body sizes. When in doubt, err toward slightly too much contrast: a secondary color that's a touch dark still reads as secondary through size and weight; a color that fails contrast is actively harmful. Rules of thumb: * Never use color as the **only** differentiator — vary size or weight too. * Test the extremes: lightest text on lightest background is the most dangerous pair. In Sundial terms: verify `contrastWeak` on `surfaceBg` in **both** brightness modes before using it for anything that must be read. * Dark mode reverses the math but not the ratios — light-on-dark still needs 4.5:1. * Placeholder text usually fails by default; ensure contrast or don't rely on it for essential information. ## Design for content overflow and edge cases Sample data is always perfectly sized; real data never is. * **Truncate supplementary content, wrap primary content.** Single-line ellipsis for names in constrained spaces; 2–3-line clamping for previews; never truncate an error message. (Sundial components use `AutoSizeText` with `maxLines` + ellipsis — see [StyledListTile](/components/styled-list-tile).) * **Empty states are mandatory.** Every list, table, or feed will eventually be empty. Each empty state needs: what would be here, why it's empty, and an action to resolve it. * **Stress-test the extremes** before calling a layout done: | Scenario | Check | | - | - | | Very long text (40+ char name) | Overflow? Pushes elements off-screen? | | Very short text (1 char) | Component collapse? Alignment break? | | Empty / null content | Blank void? Layout collapse? | | 1 item in a list | Still looks intentional? No “showing 1 of 1” noise? | | 100+ items | Pagination or lazy loading? | | Very large numbers | Column overflow? Abbreviate? | | User-generated content | Sanitized? URLs and line breaks handled? | When writing sample data, include at least one stress-test data point — it forces edge-case handling during the build, not after. ## Maintain alignment consistency Center-aligning everything produces a wedding invitation: decorative, impossible to scan. The eye scans by returning to a consistent starting edge — left-aligned text creates that edge. | Content | Alignment | | - | - | | Body text, paragraphs, lists, form labels | Left | | Table text / table numbers | Left / **right, tabular figures** | | Short hero text, empty states | Center acceptable | | Card content | Left internally — even when the card is centered | **Pick an edge and stick to it** within any section. And the key distinction: **center the container, left-align the content within it.** # Spacing & Layout Source: https://design.slingshotai.com/philosophy/craft/spacing-and-layout Design rationale: generous whitespace, a constrained spacing scale, and unambiguous grouping ## Start with too much white space Most developers add white space reactively — something looks cramped, so they add margin until it stops looking bad. The result is the *minimum* breathing room: functional, never great. **Do the opposite:** start with *way too much* space, then remove until you're happy. What feels like “a little too much” focused on a single element ends up “just enough” in a complete UI. Cramped interfaces feel stressful — the opposite of what Ash should communicate. Dense UIs (dashboards, data tables) are legitimate — but density should be a **deliberate choice**, never the default. ## Follow a spacing scale exactly Agonizing over 120px vs 125px is slow and produces inconsistency. Use a constrained scale where **no two adjacent values are closer than \~25% apart** — at small sizes a 4px jump is significant; at large sizes it's imperceptible. The reference scale (4px base): **4, 8, 12, 16, 24, 32, 48, 64, 96, 128**. Need space? Grab a value from the scale. Not enough? Jump to the *next* value — don't nudge by 2px. The constraint is the point. Never use arbitrary values; if a value feels “close enough,” use the nearest scale value instead. > [!NOTE] > > Sundial has no exported spacing-token class — the scale is a convention. Common values in the codebase are 4, 8, 12, 16, 20; see [Shape & Spacing](/foundations/shape-and-spacing) for what components actually use, and prefer Flutter's `Column(spacing: …)` / `Row(spacing: …)` over `SizedBox` separators. ## Avoid ambiguous spacing One of the most damaging spacing mistakes. When spacing alone defines grouping, equal spacing everywhere destroys relationships — a form label equidistant between two inputs belongs to neither. **The rule: space within a group \< space between groups. Always. Without exception.** * Form labels sit tight to their inputs; field groups sit apart * More space *above* a heading than below it — it belongs to the section it introduces * Gap between icon and its label \< gap between one icon-label pair and the next * List item spacing > line height within an item If two elements are closer to each other than to their surroundings, they are implicitly grouped — make sure that grouping is intentional. ## You don't have to fill the screen If content needs 600px, give it 600px — extra space around the edges is visual relief, and stretched content hurts readability. Give content a comfortable max-width on large screens. If something feels too narrow, don't stretch it — think in columns and put supporting context beside it. Design mobile-first (\~400px), then expand — you'll change less than you expect. ## Grids are a tool, not a religion 12-column grids outsource decisions that deserve judgment, and percentage widths make everything fluid when not everything should be: * **Sidebars should be fixed-width** — 25% is too wide on a big screen and too narrow on a small one. * **Use max-width, not column counts, for centered content** — a card that looks best at 500px should be 500px everywhere. * A consistent spacing system produces better results than forcing content into rigid columns. ## Relative sizing doesn't scale Don't make everything proportional to a single base (“heading is 2.5× body”). Large elements on large screens must shrink *faster* than small elements as the screen shrinks — the ratio between largest and smallest text should be less extreme on mobile. The same goes for component internals: large buttons get disproportionately generous padding, small buttons disproportionately tight. Components at different sizes need **independent design decisions**. ## The human test Every spacing decision should look like a human designer made it. If a layout looks “off” but you can't pinpoint why, check for inconsistent margins, misaligned elements, or uneven padding — it's almost always one of those. # Typography Source: https://design.slingshotai.com/philosophy/craft/typography Design rationale: typography is non-negotiable — bad type kills otherwise good design Typography is non-negotiable. Bad typography will kill an otherwise good design. **Reference:** Paco Coursey's work. Minimal and focused. Hierarchy is achieved through the fewest possible changes — font weight and color contrast, nothing more. No decorative type treatments unless they serve a clear purpose. ## Rules * Establish a clear typographic hierarchy and follow it consistently. * Achieve contrast through **weight and color**, not by multiplying font sizes or styles. * Every typographic choice must be intentional and justifiable. * When in doubt, do less. * **Define a constrained type scale and use only values from it.** Don't hand-pick arbitrary font sizes. In Sundial that scale is the [15 typography tokens](/foundations/typography) — 5 categories × 3 sizes, full stop. * Keep body text line length between **45–75 characters**. Wider degrades readability. * **Line-height is contextual**: tighter for large headings (1–1.2), looser for body (1.5–2). One line-height for everything will always feel wrong somewhere. Sundial encodes this: display styles are 1.0, body is 1.5, titles/overlines/buttons 1.2. * When mixing font sizes on the same line, **align by baseline**, not center. * Not every link needs a color treatment. In navigation or obviously interactive contexts, weight or underline-on-hover is cleaner than link-blue. * Right-align numbers in tables and use **tabular figures** so columns line up. ## Line length and line height, precisely | Context | Line height | Notes | | - | - | - | | Body text (45–75ch) | 1.5–1.75 | Standard readable prose | | Short-line text (under 40ch) | 1.4–1.5 | Captions, sidebars | | Large headings | 1.1–1.3 | Headings read as units — Sundial display styles sit at 1.0 | | UI labels and buttons | 1–1.25 | Single-line, compact | Headings have line-length limits too — roughly 20–30 characters for large display sizes. Don't run any text the full width of a large viewport. ## In Sundial The type system already embodies these rules — your job is to not fight it: * Use `context.textTheme` tokens only; never construct a `TextStyle` with a hand-picked size. * The two-typeface split is intentional: **Libertinus Serif** display styles are Ash's warm voice for hero moments; **Inter** does everything functional. Don't use display styles for UI chrome. * The body weight of **425** exists precisely to tune contrast between body and semibold titles — respect it rather than bolding body text. # Visual Hierarchy Source: https://design.slingshotai.com/philosophy/craft/visual-hierarchy Design rationale: hierarchy is the single most important factor in making UI feel designed Visual hierarchy is the single most important factor in making a UI feel well-designed. It refers to how important elements appear *in relation to one another*. When everything competes for attention equally, the interface feels noisy and chaotic. When you deliberately emphasize primary content and de-emphasize secondary content, the result is immediately more pleasing. Most interfaces have three levels: **primary** (what should the user notice first), **secondary** (supporting information), **tertiary** (background details). Every element must sit clearly at one level. ## Size isn't the only lever Relying solely on font size for hierarchy leads to comically large primary content and unreadably small secondary content. Use **weight and color** alongside size — a semibold 18px heading communicates importance as well as a regular 24px one, and takes less space. In Sundial, the contrast ramp is the color half of this system: | Tier | Purpose | Sundial token | | - | - | - | | Primary | Headlines, key data, names | `contrastStrong` | | Secondary | Dates, metadata, descriptions | `contrastMedium` / `contrastSubtle` | | Tertiary | Hints, placeholders, inactive states | `contrastWeak` | Two weights cover almost everything: normal (Sundial body is Inter 425) for most text, semibold (600) for headings and emphasized data. **Never use weights below 400 in UI** — to de-emphasize, use a lighter color or smaller size, not a thinner weight. ## Don't use gray text on colored backgrounds Gray-on-white works because it *reduces contrast*. Gray on a colored background doesn't — it just looks muddy and disabled. White at reduced opacity looks ghostly and lets the background bleed through. **The right approach:** pick a color from the *same hue* as the background — in Sundial terms, a lighter shade of the same ramp. This is exactly why `AccentColors.onButton` is shade 200 of the accent's own family, not white or gray — see [Accent Colors](/foundations/colors/accent). ## Emphasize by de-emphasizing When an element won't “pop” no matter what you do to it, the fix is usually to *turn down the volume on everything else*: * Active vs. inactive nav → soften the inactive items (Sundial's nav bar: `contrastStrong` vs `contrastWeak`) * Selected vs. unselected tabs → lighten the unselected ones * Primary vs. secondary actions → mute the secondary button ## Labels are a last resort `Label: Value` for everything flattens hierarchy — label and value compete equally. 1. **You may not need one.** `janedoe@email.com` is obviously an email; `$19.99` is obviously a price. 2. **Combine into natural language.** `12 left in stock` beats `In stock: 12`. 3. **When needed, make the label secondary** — smaller, lighter, or both. The data is what matters. (Sundial's `overlineSmall` in `contrastMedium` is the house style for this — see `StyledListTile.activities`.) 4. *Exception:* on scan-heavy spec pages where users hunt for the label itself, slightly emphasize labels over values — gently. ## Separate visual hierarchy from document hierarchy Don't let semantic structure dictate visual weight — a section title often *functions* as a label, not a focal point. Style based on the role the content plays, not its heading level. Keep the semantics for accessibility; choose the text style independently. ## Balance weight and contrast Bold elements feel emphasized because they cover more surface area — and **icons are surface-area-heavy** by nature. An icon the same color as adjacent text will dominate it; give icons a softer color (this is why `StyledTextField` tints prefix icons `contrastSubtle`, not the text color). Similarly, when a hairline border is too subtle, make it *thicker*, not darker — dark borders are harsh. ## Semantics are secondary to hierarchy A destructive action is not automatically a primary action. Don't give “Delete” a big red button just because it's dangerous: | Tier | Treatment | Sundial | | - | - | - | | Primary | Solid, high contrast | `ElevatedButton` (accent fill) | | Secondary | Outline / muted | `OutlinedButton`, `FilledButton` | | Tertiary | Text-only | `TextButton` | If deleting isn't the page's main job, style it secondary or tertiary. In the *confirmation dialog* — where the destructive action IS primary — apply the bold destructive styling. # Designing for Ash Source: https://design.slingshotai.com/philosophy/designing-for-ash Design rationale: what designing for mental health demands — safety, warmth, and unhurried interfaces Ash (produced by Slingshot AI) is a consumer AI companion for mental health and personal growth. The audience is broad, the stakes are personal, and retention and meaningful impact are the primary success metrics. ## What designing for mental health means * **Someone may be using this app in a vulnerable emotional state.** The interface must feel safe, warm, and unhurried. * **“Respecting the user's time” doesn't mean “make everything fast.”** Sometimes it means giving people space to reflect. * **Never ask users to do work that only benefits the business.** No surveys disguised as features. No engagement mechanics that exploit vulnerability. * **The experience should feel like talking to someone who genuinely cares** — not interacting with a system. ## How this shows up in Sundial The design system encodes these values in concrete decisions: | Value | System expression | | - | - | | Warmth over sterility | Parchment/wood neutral palette instead of grays — see [Reference Colors](/foundations/colors/reference) | | A companion, not a dashboard | Libertinus Serif display type for Ash's voice — see [Typography](/foundations/typography) | | Personal, not corporate | Persona accent palettes the user chooses — see [Accent Colors](/foundations/colors/accent) | | Calm surfaces | Flat elevation, color-ladder hierarchy instead of shadows — see [Shape & Spacing](/foundations/shape-and-spacing) | | Safety for vulnerable states | Generous whitespace, unhurried motion, no urgency mechanics | > [!WARNING] > > If a proposed feature exists primarily to move a metric rather than help the person, flag it. That's not a design compromise to negotiate — it's an [anti-pattern](/philosophy/anti-patterns). # Design Philosophy Source: https://design.slingshotai.com/philosophy/overview Design rationale: the beliefs behind Sundial — essentialism, respect for the user, and earned expressiveness > [!NOTE] > > **Who this section is for.** The Craft & Foundations pages explain *why* Ash looks and feels the way it does — the values and taste behind the [Design System](/). Humans: read this before designing anything new. Agents: for implementing UI, the Design System section (tokens, theming, components) is your reference; consult these pages when making *design decisions* — choosing layouts, adding elements, judging whether something belongs — not for API lookups. ## Core belief People's time is precious. Your app has to earn the right to be on someone's phone. Every element on screen must justify its existence — not to the business, but to the person using it. If something is there to serve a metric rather than a human need, remove it. This extends from **Self-Determination Theory**. Every design decision should support: * **Autonomy** — Give users real choices and control. Never manipulate, trick, or funnel people into actions that serve the business at their expense. No dark patterns. Ever. * **Competency** — Make users feel capable, not confused. Interfaces should be immediately legible. If someone has to “figure out” how to use a feature, the design has failed. * **Relatedness** — The product should feel like it was made by someone who cares. Warmth, craft, and attention to detail communicate respect. Cold efficiency is not the goal. ## The tightrope: essentialism + humanity The philosophy walks a deliberate line between two traditions. **The essentialist side** — Dieter Rams, Victor Papanek, Don Norman. Solve the problem. Don't waste the user's time. Strip away everything that isn't serving a clear purpose. *Design for the Real World* is the foundational text. **The human side** — Buck Design's narrative worldbuilding, Sekani Solomon's transition from motion graphics to product, Claudio Guglieri moving between 3D and software. Design should leave people with something memorable and meaningful — art, delight, personality. Flat and bland is not the same as minimal and focused. How to navigate this in practice: * **V0 releases prioritize structural soundness.** Get the UX right. Follow the design system. Use simple, proven patterns. A solid V0 should feel comfortable and correct even if it never gets a visual polish pass. * **Expressive moments are earned, not sprinkled.** Don't add delight for its own sake. Identify the specific moments where personality, animation, or visual richness will meaningfully improve the experience — then invest deeply in those moments. * **If forced to choose, choose function.** A sound UX with plain visuals is always preferable to a beautiful interface that confuses people. ## How to apply this This is a permanent design context layer — it applies to every design task regardless of the specific PRD. **1. Start from the design system** Use existing components first — see [Quickstart](/quickstart) and [Components](/components/styled-card). **2. Apply the essentialism test** Does every element justify its existence to the user? **3. Check typography and spacing** These are the most common failure points — see [Typography](/philosophy/craft/typography) and [Spacing & Layout](/philosophy/craft/spacing-and-layout). **4. Consider the emotional context** Someone may be vulnerable when using Ash — see [Designing for Ash](/philosophy/designing-for-ash). **5. Add expressive elements only with purpose** Decoration must serve a functional or emotional goal. **6. When uncertain, choose simpler, warmer, more respectful** Of two viable approaches, pick the one that respects the user's time most. ## Where to go next > [card] [Process](/philosophy/process) > > Reference-first design, building from parts, and spending your deepest thinking on unknown unknowns. > [card] [Designing for Ash](/philosophy/designing-for-ash) > > What designing for mental health demands of an interface. > [card] [Craft rules](/philosophy/craft/visual-hierarchy) > > Hierarchy, typography, spacing, color, depth — the concrete rules that make UI feel designed. > [card] [Anti-patterns](/philosophy/anti-patterns) > > The never-do-these list. # Process Source: https://design.slingshotai.com/philosophy/process Design rationale: reference-first design, building from parts, and iterating to a testable skeleton ## Start with reference, not from scratch When approaching a new feature, the first step is always to look at successful solutions from respected companies and designers on **Mobbin** and **Are.na**. Good design stands on the shoulders of people who came before. Don't reinvent the wheel — study what works, understand *why* it works, then adapt it. ## Build from parts Take specific patterns — a nav system from one app, a card layout from another — deconstruct why they work, then rebuild them for our context. This is not copying; it's informed composition. ## Dedicate your deepest thinking to unknown unknowns Templates, macros, and proven patterns handle the known. The real design work is revealing what you haven't considered yet. When new knowledge surfaces during the design process, don't be afraid to abandon existing work and rebuild on a fresh foundation. ## Iterate quickly Move fast through the structural decisions. Get to something testable. The goal is not a perfect first draft — it's a sound skeleton that can be validated and refined. > [!NOTE] > > This maps directly onto the V0 principle from the [overview](/philosophy/overview): structural soundness first, expressive investment second — and only where it's earned. # Quickstart Source: https://design.slingshotai.com/quickstart Use Sundial tokens and components in a widget in five minutes ## 1. Import the barrel file Everything — tokens, theme, components, context extensions — is exported from one file: ```dart theme={null} import 'package:ashley/design_system/design_system.dart'; ``` ## 2. Read tokens from context Inside any `build` method, grab the current theme's tokens via the two `BuildContext` extensions: ```dart theme={null} @override Widget build(BuildContext context) { final colors = context.colorTheme; // DesignSystemColors final typography = context.textTheme; // DesignSystemTypography return Container( color: colors.surfaceElevated, child: Text( 'Hello from Sundial', style: typography.bodyLarge.copyWith(color: colors.contrastBold), ), ); } ``` These resolve against the ambient `Theme`, so the same code renders correctly in light mode, dark mode, and under any persona accent palette. ## 3. Reach for existing components first Before building a custom widget, check what already exists: ```bash theme={null} ls lib/design_system/components/ # design system components ls lib/widgets/common/ # app-specific reusable widgets ``` ```dart Don't — reinvent with raw Material theme={null} TextField( maxLength: 250, decoration: InputDecoration(/* custom styling… */), // …custom counter logic… ) ``` ```dart Do — use the Styled wrapper theme={null} StyledTextField( controller: controller, hintText: 'Enter text', maxLength: 250, // default is 250 showCounterAfterChars: 200, // counter appears after this threshold ) ``` Variants use **named constructors**, not parameters: `StyledCard.outlined()`, `StyledCard.filled()`, `StyledTextField.underline()`. See the [Components](/components/styled-card) section for every widget. ## 4. Accent-colored (persona) UI Persona-branded elements use the accent tokens nested in the color theme: ```dart theme={null} ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: colors.accent.button, foregroundColor: colors.accent.onButton, ), onPressed: () {}, child: Text('Primary Action'), ) ``` (In practice, a plain `ElevatedButton` already gets these colors from the app theme — see [Material bridge](/theming/material-bridge).) ## The golden rules **1. Never hardcode colors or text styles** Every color comes from `context.colorTheme`, every text style from `context.textTheme`. No `Color(0xFF…)`, no `TextStyle(fontSize: …)` in feature code. **2. Semantic over primitive** Use `colors.surfaceBg`, not `SundialRef.sandLight[1]`. Semantic tokens adapt to light/dark automatically; primitives don't. **3. Use accent tokens for persona branding** Anything that should follow the user's persona uses `colors.accent.*` — never a specific palette's colors. **4. Test both brightness modes** Every screen must hold up in light and dark. If you need to force a subtree, use [ThemeOverride](/components/theme-override). # Material Bridge Source: https://design.slingshotai.com/theming/material-bridge How stock Material widgets stay on-system without Styled wrappers Not every widget in the app is a `Styled*` component — plenty of code uses plain `ElevatedButton`, `TextField`, `Chip`, `Switch`, or third-party packages that read Material's `ColorScheme`/`TextTheme`. The **Material bridge** is the part of [`app_theme.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/app_theme.dart) that keeps all of that on-system. It has three parts: ## 1. Per-component themes Every relevant `*ThemeData` on `ThemeData` is wired to Sundial tokens: | Material widget | Key token wiring | | - | - | | `AppBar` | bg `surfaceBg`, fg `contrastMedium`, title `titleMedium`, centered | | `ElevatedButton` | fill `accent.button`, content `accent.onButton`, radius 16, elevation 0, padding 17/20, `buttonLarge`; disabled → `disabledContainer`/`disabledOnContainer` | | `OutlinedButton` | border + content `accent.secondary`, radius 16, `buttonLarge` | | `TextButton` | content `accent.secondary`, radius 16, `buttonLarge` | | `FilledButton` | fill `surfaceElevated`, content `contrastMedium`, radius 8 — the "neutral" button | | `IconButton` | 40×40, 24 px icon, fg `contrastMedium`, bg `surfaceElevated` | | `TextField` (InputDecoration) | filled `surfaceElevated`, hint `bodyLarge`/`contrastWeak`, content padding 16, no border strokes | | `DropdownMenu` | text `buttonMedium`, shares the input decoration theme | | `Chip` | bg `surfaceElevated`, selected `surfaceHigh`, label `buttonMedium`/`contrastBold`, radius 32, no border | | `MenuButton` | bg `surfaceElevated`, fg `contrastBold` | | `Switch` | selected: `accent.onButton` thumb on `accent.button` track · unselected: `contrastWeak` on `surfaceElevated` with 2 px `surfaceHigh` outline · disabled: `disabled*` | | `Slider` | active `accent.button`, inactive `surfaceHigh`, thumb `accent.button` (12 px), overlay `accent.background`, 4 px track | | `BottomSheet` | bg `surfaceBg`, drag handle 32×4 `surfaceHigh` | | `Divider` | `surfaceElevated` | | `Dialog` | bg `surfaceBg`, radius 8, title `titleSmall`/`contrastStrong`, content `bodyLarge`/`contrastMedium` | | `NavigationBar` | bg `surfaceBg`, transparent indicator, selected `contrastStrong` / unselected `contrastWeak`, labels `buttonSmall` | | `Drawer` | width 330, bg `surfaceElevated` | | `TextSelection` | cursor `accent.button`, handles `accent.secondary`, selection `accent.background` | > [!NOTE] > > Because accent tokens feed the button/switch/slider/selection themes, **persona theming applies to stock Material widgets automatically** — no Styled wrapper needed for accent-correct UI. ## 2. ColorScheme mapping For widgets (and packages) that read `Theme.of(context).colorScheme`: | ColorScheme slot | Sundial token | | - | - | | `primary` | `contrastStrong` | | `secondary` | `contrastMedium` | | `surface` | `surfaceBg` | | `error` | `badgeRed` | | `surfaceContainerHighest` | `surfaceHigh` | | `surfaceTint` | `surfaceHigh` | The rest of the scheme comes from Flutter's default for the brightness (`ThemeData(brightness: …).colorScheme`), overridden with the slots above. ## 3. TextTheme mapping For code that reads Material's `TextTheme` (`Theme.of(context).textTheme`): | Material slot | Sundial token | | - | - | | `displayLarge/Medium/Small` | DS `display*` | | `titleLarge/Medium/Small` | DS `title*` | | `bodyLarge/Medium/Small` | DS `body*` | | `labelLarge/Medium/Small` | **DS `overline*`** | All slots are colored `contrastStrong`. Note the deliberate remap: Material's `label*` slots carry Sundial's overline styles — Material has no overline category, and its label slots serve the same role. ## When to rely on the bridge vs. a Styled component * **Bridge is enough**: standard Material behavior with Sundial looks — a plain button, switch, slider, dialog. * **Use the Styled component**: when Sundial adds behavior or structure beyond styling — `StyledTextField`'s character counter, `StyledScaffold`'s back handling, `StyledCard`'s variants. See [Components](/components/styled-card). # Accent Theming Source: https://design.slingshotai.com/theming/persona-theming What replaced persona theming in Sundial 2.0 — one lavender accent, brightness-only overrides > [!NOTE] > > **Personas were retired in Sundial 2.0.** This page documents what replaced the system this URL used to describe. Sundial 1.0 let a selected persona recolor the app through five accent palettes (`plum`, `apricot`, `olive`, `damson`, `cherry`). Sundial 2.0 removes that axis entirely: the app has **one accent — brand Lavender Blue** — and themes vary only by brightness. ## What changed * `AccentPalette` and the ten per-persona `AccentColors` statics are deleted. Two instances remain: `AccentColors.lavenderLight` / `.lavenderDark`, wired into `AppTheme.light` / `.dark`. * `AppTheme.fromPalette(palette, brightness)` is gone — construct with `AppTheme.light` or `AppTheme.dark`. * `ThemeOverride` is **brightness-only**: it flips a subtree light/dark (dark chrome over a photo, a dark-locked sheet) and no longer accepts a palette. ```dart theme={null} // Flip a subtree to dark regardless of the app theme: ThemeOverride( brightness: Brightness.dark, child: ..., ) ``` * Server-driven UI ignores palette hints: the SDUI `themeOverride` component honors brightness and drops `ACCENT_PALETTE_*` values (the proto enum survives — resolver cases are never deleted — but every palette resolves to lavender). ## Why Persona expression moves to content — voice, illustration, conversation — rather than repainting the interface. One accent keeps the bright-solid grammar coherent (a pale solid taking dark content only works if the solid is *known*), makes contrast guarantees provable once, and lets brand color carry recognition consistently across every surface. ## If you need per-surface color Brand expression on cards and spotlight surfaces uses the mode-invariant [brand surface tokens](/foundations/colors/semantic#brand-surfaces) (`surfaceBrandGreen`, `surfaceBrandOrange`, …) with their paired on-brand inks — a per-card choice, not a theme axis. # Theme Construction Source: https://design.slingshotai.com/theming/theme-construction How AppTheme builds a complete Material ThemeData from Sundial tokens [`app_theme.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/app_theme.dart) is where tokens become a Flutter theme. A single private builder produces every theme the app uses: ```dart theme={null} class AppTheme { static ThemeData _buildTheme(Brightness brightness) { … } static final light = _buildTheme(Brightness.light); static final dark = _buildTheme(Brightness.dark); } ``` Brightness is the only axis — the persona palette parameter was retired with Sundial 2.0. ## Build sequence **1. Pick base tokens** `DesignSystemTypography.base` (colorless metrics) and `DesignSystemColors.light` or `.dark` by brightness. **2. Resolve the accent** `AccentColors.lavenderLight` or `.lavenderDark` by brightness — the single lavender accent, nested into the color tokens. **3. Style every Material component** Buttons, inputs, chips, menus, switch, slider, bottom sheet, divider, dialog, navigation bar, drawer, text selection — all wired to tokens. See [Material bridge](/theming/material-bridge). **4. Install the ThemeExtensions** `extensions: [colorThemeExtension, DesignSystemTypography.light|dark]` — what `context.colorTheme` / `context.textTheme` read. ## App wiring `MaterialApp` is configured in `lib/main.dart`: ```dart main.dart theme={null} theme: AppTheme.light, darkTheme: AppTheme.dark, ``` Flutter selects `theme` or `darkTheme` from the platform brightness; every token, component theme, and extension flips with it. ## Global behaviors baked into the theme ### Cupertino transitions everywhere ```dart theme={null} pageTransitionsTheme: PageTransitionsTheme( builders: { for (final platform in TargetPlatform.values) platform: const CupertinoPageTransitionsBuilder(), }, ), ``` Every `Navigator.push` slides in from the right, iOS-style, **on Android too** — one consistent motion language. ### Consistent back icon `actionIconTheme` forces `Icons.arrow_back` as the back button icon on all platforms. ### Scaffold background `scaffoldBackgroundColor: colorTheme.surfaceBg` — screens start on the correct surface without any per-screen setup. ## Notable component wiring A few representative mappings (the full set is in [Material bridge](/theming/material-bridge)): | Component | Token wiring | | - | - | | `ElevatedButton` | fill `accent.button`, content `accent.onButton`, radius 16, elevation 0, `buttonLarge` | | `Switch` | selected: thumb `accent.onButton` / track `accent.button`; unselected: thumb `contrastWeak` / track `surfaceElevated` with `surfaceHigh` outline; disabled: `disabled*` | | `Slider` | active track & thumb `accent.button`, inactive `surfaceHigh`, overlay `accent.background` | | `Dialog` | bg `surfaceBg`, radius 8, title `titleSmall`/`contrastStrong`, content `bodyLarge`/`contrastMedium` | | `NavigationBar` | bg `surfaceBg`, selected `contrastStrong` vs unselected `contrastWeak`, labels `buttonSmall` | | `Drawer` | width 330, bg `surfaceElevated` | | Text selection | cursor `accent.button`, handles `accent.secondary`, selection `accent.background` | ## Theme instances are cached `AppTheme.light`/`.dark` are `static final` — built once, pure functions of tokens. Either is safe to install anywhere — which is exactly what [ThemeOverride](/components/theme-override) does per subtree. Note the static-final consequence: token edits need a full restart, not a hot reload. # ThemeExtensions & Context Accessors Source: https://design.slingshotai.com/theming/theme-extensions How tokens ride on ThemeData and why theme changes animate for free Sundial delivers tokens through Flutter's `ThemeExtension` API — the platform-native way to attach custom data to a `ThemeData`. ## The two extensions | Extension | Contents | Static instances | | - | - | - | | `DesignSystemColors` | 25 semantic color tokens (incl. nested `AccentColors`) | `.light`, `.dark` | | `DesignSystemTypography` | 15 text styles | `.base` (colorless), `.light`, `.dark` | Both are installed by `AppTheme`: ```dart theme={null} extensions: brightness == Brightness.dark ? [colorThemeExtension, DesignSystemTypography.dark] : [colorThemeExtension, DesignSystemTypography.light], ``` ## Context accessors [`theme_extensions.dart`](https://github.com/slingshot-ai/ash-mobile-app/blob/main/lib/design_system/theme_extensions.dart) provides the two getters used throughout the app: ```dart theme={null} extension ThemeExtensions on BuildContext { DesignSystemColors get colorTheme => Theme.of(this).extension()!; DesignSystemTypography get textTheme => Theme.of(this).extension()!; } ``` Two properties worth noting: * **They resolve against the nearest `Theme`** — inside a [ThemeOverride](/components/theme-override) subtree you get the overridden tokens, not the app-level ones. * **They are non-null asserted (`!`)** — any `Theme` a Sundial widget lives under must carry both extensions. In practice that means themes must come from `AppTheme.*`; a bare `ThemeData()` would crash token consumers. When overriding a subtree theme, always go through `AppTheme.light`/`.dark` or `ThemeOverride`. > [!WARNING] > > Don't confuse `context.textTheme` (Sundial's extension getter returning `DesignSystemTypography`) with Flutter's `Theme.of(context).textTheme` (Material's `TextTheme`). The names are close; the types differ. ## Why copyWith and lerp matter Every `ThemeExtension` subclass must implement `copyWith` and `lerp`. Sundial implements both **for every field** (all 25 colors, all 15 text styles, and the 5 accent fields): * **`copyWith`** is how `AppTheme` nests the resolved accent: `baseColorTheme.copyWith(accent: accent)`. * **`lerp`** is what makes theme changes **animate**. When `ThemeData` changes (brightness flip, persona change, `ThemeOverride` boundary), Flutter's `AnimatedTheme` interpolates between old and new extensions — every color cross-fades and every text style transitions smoothly, with zero widget-level work. ```dart theme={null} // From DesignSystemColors.lerp — every token interpolates return DesignSystemColors( contrastWeak: Color.lerp(contrastWeak, other.contrastWeak, t)!, // … 24 more fields … accent: accent.lerp(other.accent, t), // nested extension lerps too ); ``` ## Adding a token (checklist) Adding a field to either extension means updating **four places** in the token file — the field, the constructor, `copyWith`, and `lerp` — plus the `.light`/`.dark` (or `.base`) instances. Missing `lerp` won't fail to compile for nullable fields, so review carefully; a token that doesn't lerp will snap during animated theme changes. See [Contributing](/patterns/contributing).