Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8b95b68
feat(mobile): use Material 3 selection controls on Android
claude Oct 19, 2026
c169a65
chore(mobile): add #2342 change fragment and drop prohibited comments
claude Oct 19, 2026
39511c9
fix(mobile): rebuild Android segmented row when its options change
claude Oct 19, 2026
46e5c9b
feat(mobile): use Material 3 segmented lists for Android grouped rows
claude Oct 19, 2026
171edce
chore(mobile): add #2348 change fragment
claude Oct 19, 2026
a2c1a80
fix(mobile): fall back to a radio list when Android segments cannot fit
claude Oct 19, 2026
fac5397
docs(mobile): add Android emulator screenshots for #2342
DoodleBears Oct 19, 2026
e08f89b
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Oct 19, 2026
fa2eda5
Merge remote-tracking branch 'origin/claude/project-thread-m7tsnr' in…
claude Oct 19, 2026
3b6c16e
fix(mobile): outline unselected Android segments and chips on a trans…
claude Oct 19, 2026
bb95e86
Merge #2342 into the segmented list branch; segmented radio fallback …
claude Oct 19, 2026
0556fa7
fix(mobile): keep segmentCorners in a pure design module so the root …
claude Oct 19, 2026
e807a9b
docs(mobile): recapture Android emulator screenshots for #2342
DoodleBears Oct 19, 2026
ff99f81
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Oct 19, 2026
3070309
Merge remote-tracking branch 'origin/claude/project-thread-m7tsnr' in…
claude Oct 19, 2026
5cae4ea
docs(mobile): add physical-phone screenshots for #2348
DoodleBears Oct 20, 2026
9902f4c
Merge origin/main into claude/project-thread-m7tsnr-shape
claude Oct 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions apps/mobile/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ The shell is set by [ADR-0211](../../docs/adr/0211-the-mobile-app-has-two-tabs-w
- A floating header follows the chat chrome: title on the left, sorting, filtering and Activity search on the right. Filters live in a sheet, with category, Bot, Channel, unread-message and clear controls; no permanent filter chips. A nonblank Activity search hides sections with no matches; an entirely empty result shows one no-results message. Clearing the query restores normal empty sections. Sections with another page remain reachable. Search uses safe Activity summaries and Channel/Bot labels on the current Host before pagination. Handled history stays excluded unless selected. Default ordering is newest action first, mentions before other personal messages, then newest unread conversation; explicit newest/oldest overrides personal-message priority. Each section can load another bounded page. Hosts without query version 1 show an update error. Complete tab totals remain a subsequent slice of [#1954](https://github.com/BotHarness/DeepSeekBot/issues/1954); when another page exists, the screen identifies counts as loaded items.
- **Settings** opens from the gear as an immediate-settings sheet: one handle, fixed header and scrolling body; swipe or activation of the handle dismisses while completed settings remain. Child pages use Back within the same sheet. Android retains its fixed accessible handle, large detent and 28dp top corners. On iOS, native modal presentation keeps nested content visible; the shared frame supplies one handle in the native vertical dismissal region, and the native header owns only the root title. The handle is also a Close button with a platform-sized touch target, explicit accessibility activation and escape; no extra X is shown. Child Back still pops only one page. Actual screen-reader focus and gestures require separate native acceptance. There is no Done/save step for immediate preferences. Settings reuses row and section components, in groups. Each navigable or action row leads with a system icon (`RowIcon`: an SF Symbol on iOS, a Material Symbol on Android), monochrome `fg`, or `accent`/`danger` to match the row's tone, so a row's purpose reads before its label:
- **Models and presets** configures this Host's existing global default model and shared Model Preset library. Bots without an override follow that default. Presets can be created, renamed and edited for Orchestrator, Assignment default, allowed Assignment models and reasoning effort, through the existing Host methods. Editing a preset does not rewrite Bots that already applied a copy. Model selection pushes inside the same drawer, searches model/provider names and IDs, virtualizes the catalog, and keeps missing-key models visible but unavailable for selection. Saved settings and read errors are scoped to the Host and Bot/preset; cached content remains visible during refresh or failure, with retry.
- Android groups use `androidSettings` tokens: 28dp corners, 16dp row padding, 56dp minimum single-line rows and 72dp minimum supporting rows that expand with system text. Section headings use the accent role, supporting text uses the body ramp and muted role, and separators are inset 16dp. Selected choices use the layer's chosen fill plus accent text and the existing checkmark. Explicit navigation rows show a chevron; destructive and copy actions do not imply navigation. Android switches use SDK 58 Expo UI's native Material 3 control inside a bounded Host; state and persistence remain owned by existing preferences. iOS retains its existing rows and switch adapter.
- Android groups use `androidSettings` tokens and the Material 3 segmented list: each row is its own `raised` tile, 2dp apart, with 16dp outer corners on the group's first and last rows and 4dp inner corners between rows (`segmentCorners`), and no divider lines. Rows have 16dp padding, 56dp minimum single-line height and 72dp minimum supporting height, and expand with system text. Section headings use the accent role and supporting text uses the body ramp and muted role. Selected choices use the layer's chosen fill plus accent text and the existing checkmark. Explicit navigation rows show a chevron; destructive and copy actions do not imply navigation. Android switches use SDK 58 Expo UI's native Material 3 control inside a bounded Host; state and persistence remain owned by existing preferences. iOS retains its existing rows and switch adapter.
- **Usage** comes first, as one card for the current Host, like a fitness app's week: "Usage", "Last 7 days" and a chevron, the 7-day token total in `display`, and a bar per day (`DayBars`). An idle Host shows a quiet week of stubs, and a failed read shows a danger line with Try again under the card. Tapping it pushes the **Usage** screen:
- A Today / 7 days / 4 weeks / 12 weeks `Segmented` switch; switching reads the Host again. A background 4-week read checks support on entry; until it succeeds, and on older Hosts, only Today and 7 days appear. An `invalid-input` refusal of a longer period returns to 7 days without showing an error. The Settings card does not probe or change its 7-day range.
- The period's dates, its total, daily bars for 7 and 28 days, and 12 weekly bars for 84 days. Each week starts at the range's first day, not a calendar-week boundary; a selected weekly bar shows its date range and partial-aware total. Longer charts omit cramped weekday labels, retaining their dates in the chart's accessibility label. A muted note explains when some usage could not be counted or is still being counted.
Expand Down Expand Up @@ -293,7 +293,7 @@ Each layer has `fill`, `pressed`, `selected` (neutral, such as the current tab),
- `chosen` is the accent over the fill, 13% in light and 30% in dark. On `well` it equals DSH `state-business-tertiary`, and it follows a Bot's accent.
- A state is pushed further from its fill until it stands apart from the fill, the layer under it and the other states (`SEPARATION` in `layers.ts`; `test/design-tokens.test.ts` checks every scheme and accent). DSH's dark `interactive-bg-active` over `bg-base` equals `bg-module-platform`, so without this a selected row in a card looks like the card.
- Buttons are solid pills (`radius.round`) with no border, and take their colours from `buttonColors(theme, tone, on)` (`controls.ts`) for the layer they sit `on`. Primary is the neutral `fg` fill with a `base` label, never the Bot accent. Secondary is the layer's `selected`, or the next layer's fill when `selected` is too close to the layer. Danger is the layer's `danger` tint with a red label. Each fill stands apart from the layer and from the other tones, and each label reaches 4.5:1.
- Bottom sheets (`chat/sheet.tsx`) follow the [overlay contract](./OVERLAYS.md), with one functional handle, task-specific header actions and `theme.overlay.fill`. The existing fallback Close/grabber implementation requires caller-by-caller migration. Their content is iOS-style inset groups on `raised` (`SettingsGroupItem`): rows separated by `divider` hairlines, a muted heading above each group and a muted footnote below it. A warning goes in a `warningSoft` callout, not orange text. A selected option shows a `fg` check, and switches use `Toggle` (`accent` track when on).
- Bottom sheets (`chat/sheet.tsx`) follow the [overlay contract](./OVERLAYS.md), with one functional handle, task-specific header actions and `theme.overlay.fill`. The existing fallback Close/grabber implementation requires caller-by-caller migration. Their content is grouped rows on `raised` (`SettingsGroupItem`): on iOS inset groups with `divider` hairlines, on Android the segmented list above, a muted heading above each group and a muted footnote below it. A warning goes in a `warningSoft` callout, not orange text. A selected option shows a `fg` check, and switches use `Toggle` (`accent` track when on).
- Translucent roles (`hover`, `selected`, `divider`, `border*`, `dangerSoft`) are never a `backgroundColor`; `test/style-guard.test.ts` enforces this. Translucent edges stay allowed.

## Components
Expand Down
12 changes: 12 additions & 0 deletions apps/mobile/src/ui/design/segment-corners.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { androidSettings } from './tokens';

export function segmentCorners(first: boolean, last: boolean) {
const top = first ? androidSettings.listRadius : androidSettings.innerRadius;
const bottom = last ? androidSettings.listRadius : androidSettings.innerRadius;
return {
borderTopLeftRadius: top,
borderTopRightRadius: top,
borderBottomLeftRadius: bottom,
borderBottomRightRadius: bottom,
};
}
3 changes: 3 additions & 0 deletions apps/mobile/src/ui/design/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export const pairingScan = { maxSize: 200, widthRatio: 0.5 } as const;

export const androidSettings = {
radius: radius.xl,
listRadius: radius.lg,
innerRadius: radius.xs,
segmentGap: 2,
rowHeight: 56,
supportingRowHeight: 72,
inset: space.x5,
Expand Down
16 changes: 8 additions & 8 deletions apps/mobile/src/ui/primitives/segmented.android.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import { fillMaxWidth } from '@expo/ui/jetpack-compose/modifiers';
import { Pressable, View, useWindowDimensions } from 'react-native';

import { pressTint, useDesignTheme } from '../design/theme';
import { radius, space } from '../design/tokens';
import { androidSettings, space } from '../design/tokens';
import { RadioMark } from './radio-mark';
import { segmentCorners } from '../design/segment-corners';
import type { SegmentedProps } from './segmented';
import { Text } from './text';

Expand Down Expand Up @@ -46,11 +47,13 @@ export function Segmented<T extends string>({
)
)
return (
<View accessibilityRole="radiogroup" accessibilityLabel={label} style={{ gap: space.x1 }}>
<View
accessibilityRole="radiogroup"
accessibilityLabel={label}
style={{ gap: androidSettings.segmentGap }}
>
{options.map((option, index) => {
const selected = option.value === value;
const top = index === 0 ? radius.lg : radius.xs;
const bottom = index === options.length - 1 ? radius.lg : radius.xs;
return (
<Pressable
key={option.value}
Expand All @@ -69,10 +72,7 @@ export function Segmented<T extends string>({
paddingHorizontal: space.x5,
paddingVertical: space.x3,
overflow: 'hidden',
borderTopLeftRadius: top,
borderTopRightRadius: top,
borderBottomLeftRadius: bottom,
borderBottomRightRadius: bottom,
...segmentCorners(index === 0, index === options.length - 1),
backgroundColor: selected ? layer.chosen : layer.fill,
}}
>
Expand Down
82 changes: 46 additions & 36 deletions apps/mobile/src/ui/primitives/settings-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { androidSettings, edge, radius, space } from '../design/tokens';
import { pressTint, useDesignTheme } from '../design/theme';
import { ChevronRightIcon } from '../shell/icons';
import { Text } from './text';
import { segmentCorners } from '../design/segment-corners';

function SettingsFrame({
onPress,
Expand Down Expand Up @@ -147,22 +148,32 @@ export function SettingsSection({
return (
<View style={{ gap: android ? androidSettings.sectionGap : space.x2 }}>
{title === undefined ? null : <SettingsHeading>{title}</SettingsHeading>}
<View
style={{
backgroundColor: theme.layer[on].fill,
borderRadius: android ? androidSettings.radius : radius.md,
overflow: 'hidden',
}}
>
{android
? rows.map((child, index) => (
<View key={isValidElement(child) ? child.key : index}>
{index === 0 ? null : <SettingsDivider on={on} />}
{child}
</View>
))
: children}
</View>
{android ? (
<View style={{ gap: androidSettings.segmentGap }}>
{rows.map((child, index) => (
<View
key={isValidElement(child) ? child.key : index}
style={{
backgroundColor: theme.layer[on].fill,
overflow: 'hidden',
...segmentCorners(index === 0, index === rows.length - 1),
}}
>
{child}
</View>
))}
</View>
) : (
<View
style={{
backgroundColor: theme.layer[on].fill,
borderRadius: radius.md,
overflow: 'hidden',
}}
>
{children}
</View>
)}
{footer === undefined ? null : <SettingsFooter>{footer}</SettingsFooter>}
</View>
);
Expand All @@ -187,19 +198,6 @@ export function SettingsHeading({ children }: { children: string }) {
);
}

function SettingsDivider({ on }: { on: LayerName }) {
const theme = useDesignTheme();
return (
<View
style={{
marginHorizontal: androidSettings.inset,
borderTopWidth: edge.hairline,
borderColor: theme.layer[on].divider,
}}
/>
);
}

export function SettingsFooter({ children }: { children: string }) {
return (
<Text variant="meta" tone="muted" style={{ paddingHorizontal: space.x5 }}>
Expand All @@ -222,22 +220,34 @@ export function SettingsGroupItem({
const theme = useDesignTheme();
const layer = theme.layer[on];
const android = Platform.OS === 'android';
const corners = android ? androidSettings.radius : radius.md;
if (android)
return (
<View
style={{
marginHorizontal: space.x5,
marginTop: first ? 0 : androidSettings.segmentGap,
backgroundColor: layer.fill,
overflow: 'hidden',
...segmentCorners(first, last),
}}
>
{children}
</View>
);
return (
<View
style={{
marginHorizontal: space.x5,
backgroundColor: layer.fill,
borderTopLeftRadius: first ? corners : 0,
borderTopRightRadius: first ? corners : 0,
borderBottomLeftRadius: last ? corners : 0,
borderBottomRightRadius: last ? corners : 0,
borderTopWidth: android || first ? 0 : edge.hairline,
borderTopLeftRadius: first ? radius.md : 0,
borderTopRightRadius: first ? radius.md : 0,
borderBottomLeftRadius: last ? radius.md : 0,
borderBottomRightRadius: last ? radius.md : 0,
borderTopWidth: first ? 0 : edge.hairline,
borderTopColor: layer.divider,
overflow: 'hidden',
}}
>
{android && !first ? <SettingsDivider on={on} /> : null}
{children}
</View>
);
Expand Down
25 changes: 17 additions & 8 deletions apps/mobile/src/ui/shell/activity-filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { INBOX_SECTIONS, type InboxSection, type InboxSetAside } from '../../host/inbox';
import { Sheet } from '../chat/sheet';
import { useDesignTheme } from '../design/theme';
import { edge, radius, space } from '../design/tokens';
import { androidSettings, edge, radius, space } from '../design/tokens';
import { DrawerHeader } from '../primitives/drawer-header';
import { RadioMark } from '../primitives/radio-mark';
import { segmentCorners } from '../design/segment-corners';
import { Text } from '../primitives/text';
import { Toggle } from '../primitives/toggle';
import { CheckIcon } from './icons';
Expand Down Expand Up @@ -203,7 +204,7 @@ function OptionsEditor({ bots, channels, filter, sort, onFilter, onSort, onClose
marginHorizontal: space.x5,
paddingHorizontal: space.x5,
paddingVertical: space.x3,
borderRadius: ios ? radius.md : 0,
borderRadius: ios ? radius.md : androidSettings.listRadius,
backgroundColor: theme.layer.raised.fill,
}}
>
Expand Down Expand Up @@ -233,12 +234,20 @@ function OptionsEditor({ bots, channels, filter, sort, onFilter, onSort, onClose
marginHorizontal: space.x5,
paddingHorizontal: space.x5,
paddingVertical: space.x3,
borderTopLeftRadius: ios && first ? radius.md : 0,
borderTopRightRadius: ios && first ? radius.md : 0,
borderBottomLeftRadius: ios && last ? radius.md : 0,
borderBottomRightRadius: ios && last ? radius.md : 0,
borderBottomWidth: last ? 0 : edge.hairline,
borderBottomColor: theme.layer.raised.divider,
...(ios
? {
borderTopLeftRadius: first ? radius.md : 0,
borderTopRightRadius: first ? radius.md : 0,
borderBottomLeftRadius: last ? radius.md : 0,
borderBottomRightRadius: last ? radius.md : 0,
borderBottomWidth: last ? 0 : edge.hairline,
borderBottomColor: theme.layer.raised.divider,
}
: {
...segmentCorners(first, last),
marginTop: first ? 0 : androidSettings.segmentGap,
overflow: 'hidden',
}),
backgroundColor: pressed ? theme.layer.raised.pressed : theme.layer.raised.fill,
})}
>
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/test/material-controls-android.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ vi.mock('@expo/ui/jetpack-compose', async () => {
createElement('i', { 'data-testid': 'native-radio', 'data-selected': props.selected }),
};
});
vi.mock('../src/ui/shell/icons', () => ({ ChevronRightIcon: () => null }));
vi.mock('../src/ui/primitives/radio-mark', () => import('../src/ui/primitives/radio-mark.android'));
vi.mock('@expo/ui/jetpack-compose/modifiers', () => ({ fillMaxWidth: () => ({}) }));

Expand Down
19 changes: 19 additions & 0 deletions apps/mobile/test/segmented-list-android.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { expect, it } from 'vitest';

import { androidSettings } from '../src/ui/design/tokens';
import { segmentCorners } from '../src/ui/design/segment-corners';

it('rounds the outer corners of a segmented list and keeps inner corners soft', () => {
expect(androidSettings.listRadius).toBe(16);
expect(androidSettings.innerRadius).toBe(4);
expect(androidSettings.segmentGap).toBe(2);
expect(segmentCorners(true, false)).toEqual({
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
borderBottomLeftRadius: 4,
borderBottomRightRadius: 4,
});
expect(segmentCorners(false, false).borderTopLeftRadius).toBe(4);
expect(segmentCorners(false, true).borderBottomRightRadius).toBe(16);
expect(new Set(Object.values(segmentCorners(true, true)))).toEqual(new Set([16]));
});
11 changes: 11 additions & 0 deletions changes/2348-android-segmented-lists.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
section: Changed
---

## en

- On Android, grouped lists in Settings and sheets now use the Material 3 segmented list: soft 16dp outer corners, 4dp corners between rows and a 2dp gap instead of divider lines ([#2348](https://github.com/BotHarness/DeepSeekBot/pull/2348)).

## zh

- Android 上设置页和底部面板里的分组列表改用 Material 3 分段列表:外侧 16dp 柔和圆角,行与行之间 4dp 圆角加 2dp 间隙,不再用分隔线([#2348](https://github.com/BotHarness/DeepSeekBot/pull/2348))。
Loading