diff --git a/.github/pr-screenshots/2349-plain-titles/1-settings-root-dark.png b/.github/pr-screenshots/2349-plain-titles/1-settings-root-dark.png new file mode 100644 index 0000000000..a0077ccfba Binary files /dev/null and b/.github/pr-screenshots/2349-plain-titles/1-settings-root-dark.png differ diff --git a/.github/pr-screenshots/2349-plain-titles/1-settings-root-light.png b/.github/pr-screenshots/2349-plain-titles/1-settings-root-light.png new file mode 100644 index 0000000000..c79e3fefa2 Binary files /dev/null and b/.github/pr-screenshots/2349-plain-titles/1-settings-root-light.png differ diff --git a/.github/pr-screenshots/2349-plain-titles/2-3-chat-background-theme-dark.png b/.github/pr-screenshots/2349-plain-titles/2-3-chat-background-theme-dark.png new file mode 100644 index 0000000000..332189198c Binary files /dev/null and b/.github/pr-screenshots/2349-plain-titles/2-3-chat-background-theme-dark.png differ diff --git a/.github/pr-screenshots/2349-plain-titles/2-3-chat-background-theme-light.png b/.github/pr-screenshots/2349-plain-titles/2-3-chat-background-theme-light.png new file mode 100644 index 0000000000..9d2dafa6cf Binary files /dev/null and b/.github/pr-screenshots/2349-plain-titles/2-3-chat-background-theme-light.png differ diff --git a/.github/pr-screenshots/2349-plain-titles/3-image-fill-fit-dark.png b/.github/pr-screenshots/2349-plain-titles/3-image-fill-fit-dark.png new file mode 100644 index 0000000000..cc3618aae8 Binary files /dev/null and b/.github/pr-screenshots/2349-plain-titles/3-image-fill-fit-dark.png differ diff --git a/.github/pr-screenshots/2349-plain-titles/3-image-fill-fit-light.png b/.github/pr-screenshots/2349-plain-titles/3-image-fill-fit-light.png new file mode 100644 index 0000000000..3c790875e3 Binary files /dev/null and b/.github/pr-screenshots/2349-plain-titles/3-image-fill-fit-light.png differ diff --git a/.github/pr-screenshots/2349-plain-titles/4-activity-filter-sheet-dark.png b/.github/pr-screenshots/2349-plain-titles/4-activity-filter-sheet-dark.png new file mode 100644 index 0000000000..48eed94679 Binary files /dev/null and b/.github/pr-screenshots/2349-plain-titles/4-activity-filter-sheet-dark.png differ diff --git a/.github/pr-screenshots/2349-plain-titles/4-activity-filter-sheet-light.png b/.github/pr-screenshots/2349-plain-titles/4-activity-filter-sheet-light.png new file mode 100644 index 0000000000..add727ee5f Binary files /dev/null and b/.github/pr-screenshots/2349-plain-titles/4-activity-filter-sheet-light.png differ diff --git a/.github/pr-screenshots/2349-plain-titles/5-usage-dark.png b/.github/pr-screenshots/2349-plain-titles/5-usage-dark.png new file mode 100644 index 0000000000..03ab720544 Binary files /dev/null and b/.github/pr-screenshots/2349-plain-titles/5-usage-dark.png differ diff --git a/.github/pr-screenshots/2349-plain-titles/5-usage-light.png b/.github/pr-screenshots/2349-plain-titles/5-usage-light.png new file mode 100644 index 0000000000..b00eb37201 Binary files /dev/null and b/.github/pr-screenshots/2349-plain-titles/5-usage-light.png differ diff --git a/.github/pr-screenshots/2349-plain-titles/6-chat-background-ax5-dark.png b/.github/pr-screenshots/2349-plain-titles/6-chat-background-ax5-dark.png new file mode 100644 index 0000000000..387ad1c117 Binary files /dev/null and b/.github/pr-screenshots/2349-plain-titles/6-chat-background-ax5-dark.png differ diff --git a/.github/pr-screenshots/2349-plain-titles/6-chat-background-ax5-light.png b/.github/pr-screenshots/2349-plain-titles/6-chat-background-ax5-light.png new file mode 100644 index 0000000000..4d37a5c920 Binary files /dev/null and b/.github/pr-screenshots/2349-plain-titles/6-chat-background-ax5-light.png differ diff --git a/.github/pr-screenshots/2349-plain-titles/7-dm-mid-dark.png b/.github/pr-screenshots/2349-plain-titles/7-dm-mid-dark.png new file mode 100644 index 0000000000..a0d02bc0f8 Binary files /dev/null and b/.github/pr-screenshots/2349-plain-titles/7-dm-mid-dark.png differ diff --git a/.github/pr-screenshots/2349-plain-titles/7-dm-mid-light.png b/.github/pr-screenshots/2349-plain-titles/7-dm-mid-light.png new file mode 100644 index 0000000000..5be03d0692 Binary files /dev/null and b/.github/pr-screenshots/2349-plain-titles/7-dm-mid-light.png differ diff --git a/.github/pr-screenshots/2349-plain-titles/7b-dm-mid-color-bg-dark.png b/.github/pr-screenshots/2349-plain-titles/7b-dm-mid-color-bg-dark.png new file mode 100644 index 0000000000..eeb233560e Binary files /dev/null and b/.github/pr-screenshots/2349-plain-titles/7b-dm-mid-color-bg-dark.png differ diff --git a/.github/pr-screenshots/2349-plain-titles/7b-dm-mid-color-bg-light.png b/.github/pr-screenshots/2349-plain-titles/7b-dm-mid-color-bg-light.png new file mode 100644 index 0000000000..7f1967414f Binary files /dev/null and b/.github/pr-screenshots/2349-plain-titles/7b-dm-mid-color-bg-light.png differ diff --git a/apps/mobile/DESIGN.md b/apps/mobile/DESIGN.md index 458570e4b9..2956755db2 100644 --- a/apps/mobile/DESIGN.md +++ b/apps/mobile/DESIGN.md @@ -33,7 +33,7 @@ Older rules below that prescribe a custom surface where a native component exist This applies [Native first](#native-first) to iOS menus, following the Apple HIG (the `ios-hig` skill maps each component to our stack). Build them with `@expo/ui` SwiftUI views or native navigation, not with React Native views drawn to look like them. On Android the same jobs use Material 3 components: a dropdown menu for ⋯ actions, an exposed dropdown menu or radio dialog for a single choice, and a Material dialog for confirmations. - **Action menus are native pull-down menus.** A ⋯ button opens a SwiftUI `Menu` anchored to it; a long press on a row opens a native context menu with the same actions. Items are commands with title-case labels, most-used first, related items grouped, at most one submenu level. Destructive items carry the destructive role and still open a confirmation describing the consequence. -- **Choices of one value are native pop-up menus.** A short, fixed set of mutually exclusive options shows the current value on the control and opens a menu-style picker with a checkmark on the selection. Two to five peer modes that should stay visible use a segmented control. Options that need explanation, search or a long list use a sheet. +- **Choices of one value are native pop-up menus.** A short, fixed set of mutually exclusive options shows the current value on the control and opens a menu-style picker with a checkmark on the selection. Two to five peer modes that should stay visible use a segmented control (`Segmented`), with a one-word label per segment; a label that needs more words means the choice belongs in a menu or a list instead. Options that need explanation, search or a long list use a sheet. - **Alerts are for confirmations and errors only.** A system alert asks one question with a Cancel and at most one or two actions; it never lists a menu of unrelated commands. - **Exceptions stay as recorded.** The chat bubble menu (reaction island and action card, see Chat) and app-owned tab presentation (ADR-0235) are deliberate custom surfaces. Any new custom surface where the system has a control needs the reason written here first. - Native menus inherit Liquid Glass, Dynamic Type, VoiceOver and Reduce Transparency from the system; do not restyle them. Every action in a context menu is also reachable from a visible control or an accessibility action. @@ -161,7 +161,7 @@ The shell is set by [ADR-0211](../../docs/adr/0211-the-mobile-app-has-two-tabs-w - The chip is a full-round pill on a frosted `raised` (the fill at `glass.tint` opacity) with a hairline `edge`, so messages show faintly through it as they pass under. In one row it holds the Bot's avatar (no state ring), its name, and a state dot: `success` when idle, `accent` while thinking or working, `warning` when it waits for the Human, `danger` when blocked, `muted` when paused. The state is read out in the chip's accessibility label. - The chip's padding is concentric with its corners: it is `theme.touch` tall, so its corner radius is half that, and the avatar and the dot each sit that same distance from their end of the pill (padding = corner radius − the item's own radius). - Back and the computer button are round buttons of `theme.touch`, on the same frosted `raised` with a hairline `edge`, on every platform. Both icons are `fg`, never the Bot's accent. - - Messages run under the header and the status bar and fade there: a light static gradient from `base` to transparent (`edgeFade.stops`, at most 80%) covers the header and `edgeFade.top` below it, on every platform. At rest the list keeps the header's height free above the oldest message. UIKit's scroll edge effects stay off (`scrollEdgeEffects` all `hidden`), because the header is our own. On iOS 26 with Reduce Transparency off, both fades also carry a soft scroll edge like HIG Scroll views: a `thin` system material (`EdgeMaterial`, drawn past the safe area) at full strength under the status bar, header and floating composer, fading out over `edgeFade.top` / `edgeFade.bottom` beyond them, so messages blur as well as fade and no sharp text shows through those bars. The bottom fade spans the composer's measured height plus `edgeFade.bottom`. The blur is composited by the system, so the list does not re-render. The header's back button, title capsule and trailing button sit on Liquid Glass there instead of the frosted `raised` fill. Older iOS, Reduce Transparency and Android keep the colour fade and frosted fill only. + - Messages run under the header and the status bar and fade there: a light static gradient from `base` to transparent (`edgeFade.stops`, at most 80%) covers the header and `edgeFade.top` below it, on every platform. At rest the list keeps the header's height free above the oldest message. UIKit's scroll edge effects stay off (`scrollEdgeEffects` all `hidden`), because the header is our own. On iOS 26 with Reduce Transparency off, both fades also carry a soft scroll edge like HIG Scroll views: an `ultraThin` system material (`EdgeMaterial`, its hosting view set to `ignoreSafeArea="all"` so it reaches the screen edge) strongest at the screen edge and easing out quickly (`edgeFade.blur`, at most 80%), so it is nearly clear by the bottom of the header (top) or the top of the composer (bottom). Like Grok's chat, text under the status bar is softened, not hidden; messages stay readable right up to the edge. On iOS 26 the colour fade is not drawn, because a `base` tint does not match a custom chat background. The bottom fade spans the composer's measured height plus `edgeFade.bottom`. The blur is composited by the system, so the list does not re-render. The header's back button, title capsule and trailing button sit on Liquid Glass there instead of the frosted `raised` fill. Older iOS, Reduce Transparency and Android keep the colour fade and frosted fill only. - The list runs to the bottom of the screen, and the composer floats over it with no bar of its own; its pill keeps an opaque `surface` fill, so nothing shows through where the Human types; messages pass around and under it. The bottom of the screen fades out with the same light gradient over the composer and `edgeFade.bottom` above it. The list keeps that much space below the newest message, so at rest it sits just above the composer; scrolled up, messages pass behind the composer. - Messages: - Activity rows show Bot DMs, posts elsewhere, memory commits, Bot creation, departures and stopped turns as one left-aligned meta line with a kind icon and tail ellipsis. They use the same `bubbleRow(true)` inset and single `space.x4` (12pt) top gap as a new message group, with no bottom padding or vertically centred minimum-height box: bubble → activity, activity → activity and activity → new message group keep the same boundary gap. Icons and avatars match the meta line height. In groups, the actor avatar occupies the existing speaker gutter, followed by the icon at the bubble's left edge and the description. In a Human DM, omit the current Bot's avatar and redundant name; Bot-DM actions show the other Bot's avatar after the icon. Missing avatars use the person icon. @@ -283,7 +283,7 @@ Each layer has `fill`, `pressed`, `selected` (neutral, such as the current tab), - A drawer owns one `SheetStack`: its handle stays fixed while secondary pages push/back inside the same native sheet. Global usage, App icon and notifications, profile model/workspace/notifications, routine detail/create/edit, Token detail and memory files share this navigation. Child headers consume no second status-bar inset; headers and bodies use `theme.overlay.fill`, with raised content groups. Editors reuse their existing validation and Host mutation ports. - Profile token totals retain reported usage when other calls are unknown, using the global Settings count format (`674.7K+`, for example). Completely unreported usage remains unknown; an idle, fully reported window may show zero. - `src/ui/design/` holds the tokens and theme (ADR-0201). -- `src/ui/primitives/` holds `Text` (variant `meta` / `body` / `title`, and a tone that is a colour role), `Button`, `Toggle`, `SettingsRow`, `SettingsSection` and `SettingsGroupItem`, `CountBadge`, `Segmented` (an iOS-style two- or three-way switch on the layer's `inset`), `DayBars` (one bar per day or week, scaled to the busiest, optionally stacked in tones from `design/chart.ts`; an empty day keeps a stub on the layer's `inset`), `DayHeat` (a weeks × weekdays heatmap), and later `Icon`, `IconButton`, `Button`, `Avatar`, `Card`, `SettingsRow` and `EmptyState`. +- `src/ui/primitives/` holds `Text` (variant `meta` / `body` / `title`, and a tone that is a colour role), `Button`, `Toggle`, `SettingsRow`, `SettingsSection` and `SettingsGroupItem`, `CountBadge`, `Segmented` (the native segmented control: a SwiftUI segmented `Picker` on iOS, a Material 3 `SingleChoiceSegmentedButtonRow` on Android; each label is one short word so no segment wraps), `DayBars` (one bar per day or week, scaled to the busiest, optionally stacked in tones from `design/chart.ts`; an empty day keeps a stub on the layer's `inset`), `DayHeat` (a weeks × weekdays heatmap), and later `Icon`, `IconButton`, `Button`, `Avatar`, `Card`, `SettingsRow` and `EmptyState`. - Forms use `TextField` from `primitives/text-field`: the `field` layer, a hairline `field` edge, `radius.md`, a touch-height minimum (two lines when multiline) and the system clear button on single-line fields. Give every field a placeholder or a visible label, and the keyboard and content type its value needs (`url`, `emailAddress`). `SearchField` leads with a magnifying glass and relies on the native iOS clear button; Android, which has none, adds its own. - Follow the HIG for alerts and action sheets. An action that can't be undone (unpair, remove access, revoke, sign out, reset) asks first through `confirmDestructive`: a title that names the action, Cancel first, then the destructive button. A short list of actions on one item uses `showActionMenu`, which on iOS is an action sheet with the destructive action last and Cancel set apart; never show such a list as a centered alert. - `src/ui/chat/` will hold the conversation row, bubble, message group, time separator, action cards, composer, working status and scroll-to-bottom. @@ -384,7 +384,7 @@ Every editable field on iOS and Android belongs to a layout that responds to the ### Shared drawer title treatment -The workspace drawer starts with its category tabs. `DrawerHeader` supplies generic bottom `Sheet` headers in the iOS 26 sheet layout: an optional leading close (×), a centred semantic title with an optional decorative icon, and an optional trailing confirm (✓) for sheets that commit a change. Empty slots keep their width so the title stays centred, and close and confirm are each independently accessible. The iOS title uses a content-sized capsule with native glass on iOS 26; older iOS and Reduce Transparency use an opaque raised capsule. At accessibility text sizes, the title uses an opaque rounded rectangle so multiline text stays within a readable surface. Android keeps a plain centred title on the opaque overlay. The close target is at least the platform touch minimum. Long titles wrap without changing the font scale; subtitles remain outside the capsule. +The workspace drawer starts with its category tabs. `DrawerHeader` supplies generic bottom `Sheet` headers in the iOS 26 sheet layout: an optional leading close (×), a centred semantic title with an optional decorative icon, and an optional trailing confirm (✓) for sheets that commit a change. Empty slots keep their width so the title stays centred, and close and confirm are each independently accessible. The title is plain bold text on the sheet's opaque overlay, with no capsule, on both platforms; close, back and confirm are quiet filled circles on the overlay's `raised` fill, matching the native iOS 26 back button of a sheet's child page. A sheet is already a clean overlay above the screen, so it gets no glass: Liquid Glass is only for controls that float over content that must stay visible beneath them, such as the chat header over messages or the navigation island over a list. Child pages of a sheet stack (`SheetStack`) use the native iOS navigation bar with its minimal back button and centred title; Android draws the same row with `DrawerHeader` and `onBack`, so a child page's back sits where the root page's close sits. The close target is at least the platform touch minimum. Long titles wrap without changing the font scale; subtitles remain outside the capsule. Material is a decorative background only: text, focus, hit testing and navigation remain in the existing React Native tree. A color-scheme change recreates native glass backdrops and the bottom navigation button presentations so they cannot retain the previous appearance. The owning React page, selected route, source scope and menu state remain mounted; native accessibility focus after an appearance change requires separate device verification. It must not capture gestures or become another accessibility element. Drawer body and grabber stay opaque and continuous. Header styling does not add dismissal controls to Settings or other native `SheetStack` routes; those retain their established handle/back policy until separately verified. diff --git a/apps/mobile/src/ui/chat/chat-background-editor.tsx b/apps/mobile/src/ui/chat/chat-background-editor.tsx index 5c9270e01e..6bac04a359 100644 --- a/apps/mobile/src/ui/chat/chat-background-editor.tsx +++ b/apps/mobile/src/ui/chat/chat-background-editor.tsx @@ -198,18 +198,24 @@ export function ChatBackgroundEditor({ {t.chatBackground.failed} ) : null} -