Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .agents/skills/ios-hig/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Glass is the functional layer's material on iOS 26+. Its rules:
- **Few custom surfaces.** System bars, sheets and menus get glass for free. Add `glassEffect` / `buttonStyle('glass')` by hand only to the few app-owned floating controls that need it (today: the tab island, the **+** button, drawer tabs and the drawer title capsule). Glass on glass muddies the hierarchy.
- **Regular, not clear,** unless the surface floats over photos or video; clear glass over bright media may need a ~35% dark dimming layer.
- **Colour sparingly.** Labels and symbols on glass stay monochrome; one prominent (tinted) action per view, coloured by its background, not its label. Keep the accent off most controls; selected tab, unread badge and the primary action are where it belongs. Check legibility at the resting scroll position.
- **Separate controls from content with the scroll edge effect,** not a painted bar background. Leave it on its automatic style; use it only where content scrolls under floating controls; one per view. Our deliberate exception: the chat screen hides the native header and turns scroll edge effects off, using `edgeFade` instead (see `DESIGN.md`, Chat). Do not "fix" that without a Human decision.
- **Separate controls from content with the scroll edge effect,** not a painted bar background. Leave it on its automatic style; use it only where content scrolls under floating controls; one per view. The chat screen uses the native transparent header with the automatic top edge effect over a top-to-bottom (not inverted) list; an inverted list under a transparent header dims on iOS 26 (see `DESIGN.md`, Chat).
- **Fallbacks ship with the glass:** iOS < 26 → bordered/opaque `raised` surface; **Reduce Transparency** → opaque (follow `DrawerMaterial` in `src/ui/primitives/drawer-material.ios.tsx`); **Increase Contrast** → stronger edge; **Reduce Motion** → no morph; accessibility text sizes → an opaque rounded rectangle when text wraps inside a capsule.
- **Hit targets** stay ≥ 44 × 44 pt however small the glass shape looks.

Expand All @@ -67,7 +67,7 @@ Lessons from a production Expo 58 app with iOS 26 glass (t3code; notes in `/mnt/
- Tab bar: navigation only, always visible on top-level screens, labelled, never disabled.
- Glass only on the functional layer; every glass surface has the iOS < 26, Reduce Transparency, Increase Contrast and large-text fallbacks.
- At most one tinted primary action per view; labels on glass monochrome.
- Scroll edge effect automatic, only under floating controls, one per view (chat's documented exception aside).
- Scroll edge effect automatic, only under floating controls, one per view.
- Drift from `DESIGN.md` named in the PR, not silently resolved.

## Keeping current
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/ios-hig/references/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Build: `SheetScreen` + `sheetScreenOptions` (`src/ui/primitives/sheet-screen.tsx

Build: native stack headers and `NativeTabs` apply it automatically; React Native Screens exposes per-edge `scrollEdgeEffects` for a screen. Do not paint a solid or translucent background under a bar instead of using it.

Today: chat turns all edges `hidden` and fades with `edgeFade` because a transparent native header dims the inverted list (`DESIGN.md` → Chat header). Other screens keep the system default.
Today: chat keeps the automatic top edge effect under a transparent native header; its list runs top to bottom because an inverted list under that header dims on iOS 26 (`DESIGN.md` → Chat header). Other screens keep the system default.

## Tab bars

Expand Down
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.
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.
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.
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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 10 additions & 10 deletions apps/mobile/DESIGN.md

Large diffs are not rendered by default.

5 changes: 0 additions & 5 deletions apps/mobile/src/ui/chat/bot-tone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,3 @@ export function botStateTone(bot: RosterBot): ColorRole {
export function botBusy(bot: RosterBot): boolean {
return !bot.paused && (bot.state === 'thinking' || bot.state === 'working');
}

export function botDotTone(bot: RosterBot): ColorRole {
if (!bot.paused && bot.state === 'idle') return 'success';
return botStateTone(bot);
}
21 changes: 8 additions & 13 deletions apps/mobile/src/ui/chat/channel-title.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { SourceName } from './source-name';
import { Pressable } from 'react-native';

import type { RosterChannel } from '../../host/client';
import { frosted, useDesignTheme } from '../design/theme';
import { edge, radius, space } from '../design/tokens';
import { useDesignTheme } from '../design/theme';
import { radius, space } from '../design/tokens';
import { Text } from '../primitives/text';
import { t } from '../strings';
import { barLabelStyle } from './native-header';
import { GroupMark } from './group-mark';

const MARK = 28;
Expand All @@ -30,26 +30,21 @@ export function ChannelTitle({
disabled={channel === undefined}
onPress={onPress}
style={({ pressed }) => ({
maxWidth: '100%',
flexShrink: 1,
flexDirection: 'row',
alignItems: 'center',
gap: space.x3,
minHeight: Math.max(theme.touch, theme.type.body.line * theme.fontScale + space.x3),
paddingLeft: (theme.touch - MARK) / 2,
paddingRight: theme.touch / 2,
borderRadius: radius.round,
borderWidth: edge.hairline,
borderColor: theme.layer.raised.edge,
backgroundColor: pressed ? theme.layer.raised.pressed : frosted(theme.layer.raised.fill),
backgroundColor: pressed ? theme.layer.raised.pressed : undefined,
})}
>
<GroupMark avatar={channel?.avatar} size={MARK} corner={radius.round} />
<SourceName name={name} source="computer" />
{members === undefined ? null : (
<Text variant="meta" tone="muted" numberOfLines={1}>
{members}
</Text>
)}
<Text strong numberOfLines={1} style={[{ flexShrink: 1 }, barLabelStyle]}>
{name}
</Text>
</Pressable>
);
}
32 changes: 9 additions & 23 deletions apps/mobile/src/ui/chat/chat-title.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import { SourceName } from './source-name';
import { Pressable, View } from 'react-native';
import { Pressable } from 'react-native';

import type { RosterBot } from '../../host/client';
import { BotAvatar } from '../bot-avatar';
import { useDesignTheme } from '../design/theme';
import { radius, space } from '../design/tokens';
import { DrawerMaterial, useLiquidGlass } from '../primitives/drawer-material';
import { Text } from '../primitives/text';
import { t } from '../strings';
import { botDotTone } from './bot-tone';
import { headerGlassFill } from './header-chrome';
import { barLabelStyle } from './native-header';

const AVATAR = 28;
const DOT = space.x3;

export function ChatTitle({
bot,
Expand All @@ -23,7 +20,6 @@ export function ChatTitle({
onPress(): void;
}) {
const theme = useDesignTheme();
const glass = useLiquidGlass();
const status = bot === undefined ? undefined : bot.paused ? t.paused : t.state[bot.state];
const name = bot?.displayName ?? fallback;
return (
Expand All @@ -34,31 +30,21 @@ export function ChatTitle({
disabled={bot === undefined}
onPress={onPress}
style={({ pressed }) => ({
maxWidth: '100%',
flexShrink: 1,
flexDirection: 'row',
alignItems: 'center',
gap: space.x3,
minHeight: Math.max(theme.touch, theme.type.body.line * theme.fontScale + space.x3),
paddingLeft: bot === undefined ? theme.touch / 2 : (theme.touch - AVATAR) / 2,
paddingRight: bot === undefined ? theme.touch / 2 : (theme.touch - DOT) / 2,
paddingRight: theme.touch / 2,
borderRadius: radius.round,
...headerGlassFill(theme, glass, pressed),
backgroundColor: pressed ? theme.layer.raised.pressed : undefined,
})}
>
{glass ? <DrawerMaterial /> : null}
{bot === undefined ? null : <BotAvatar bot={bot} size={AVATAR} ring={false} />}
<SourceName name={name} source="computer" />
{bot === undefined ? null : (
<View
testID="chat-title-state"
style={{
width: DOT,
height: DOT,
borderRadius: radius.round,
backgroundColor: theme.color[botDotTone(bot)],
}}
/>
)}
<Text strong numberOfLines={1} style={[{ flexShrink: 1 }, barLabelStyle]}>
{name}
</Text>
</Pressable>
);
}
Loading
Loading