diff --git a/mobile/app/h/[hostId]/index.tsx b/mobile/app/h/[hostId]/index.tsx index 2673b57e527..3b62b787216 100644 --- a/mobile/app/h/[hostId]/index.tsx +++ b/mobile/app/h/[hostId]/index.tsx @@ -44,6 +44,7 @@ import { import type { RpcSuccess } from '../../../src/transport/types' import { StatusDot } from '../../../src/components/StatusDot' import { NewWorktreeModalController } from '../../../src/components/NewWorktreeModalController' +import { NewWorkspaceFab, FAB_SIZE } from '../../../src/components/NewWorkspaceFab' import { MobileRepoIcon } from '../../../src/components/MobileRepoIcon' import { WorktreeListRow } from '../../../src/components/WorktreeListRow' import { useNow } from '../../../src/hooks/use-now' @@ -1099,17 +1100,6 @@ export function HostScreen({ /> - - - - setShowSearch((s) => !s)}> {showSearch ? ( @@ -1187,7 +1177,9 @@ export function HostScreen({ // above the Samsung 3-button nav / iOS home indicator. contentContainerStyle={[ styles.list, - { paddingBottom: spacing.lg + insets.bottom }, + // Phone shows a floating "+" button bottom-right; reserve room so the + // last row stays tappable above it. Embedded sidebars keep the toolbar +. + { paddingBottom: (embedded ? spacing.lg : FAB_SIZE + spacing.xl) + insets.bottom }, isWideLayout && !embedded && { maxWidth: contentMaxWidth, width: '100%', alignSelf: 'center' } ]} @@ -1245,6 +1237,11 @@ export function HostScreen({ /> )} + {/* Floating "new workspace" button — phone only; embedded sidebars keep the toolbar +. */} + {!embedded && ( + + )} + void + disabled?: boolean +} + +// Phone-only floating "+" for creating a workspace. Absolutely positioned so it +// never intercepts list row taps, and lifted above the home indicator. +export function NewWorkspaceFab({ onPress, disabled }: NewWorkspaceFabProps): React.JSX.Element { + const insets = useSafeAreaInsets() + return ( + [ + styles.fab, + { bottom: spacing.xl + insets.bottom }, + pressed && styles.fabPressed, + disabled && styles.fabDisabled + ]} + onPress={onPress} + disabled={disabled} + accessibilityRole="button" + accessibilityLabel="New workspace" + hitSlop={8} + > + + + ) +} + +const styles = StyleSheet.create({ + fab: { + position: 'absolute', + right: spacing.lg, + width: FAB_SIZE, + height: FAB_SIZE, + borderRadius: FAB_SIZE / 2, + alignItems: 'center', + justifyContent: 'center', + // Crisp near-white surface + dark icon: high contrast against the dark canvas + // reads as the primary action while staying monochrome (STYLEGUIDE: color is + // for state). Tight shadow avoids the muddy halo that made it look disabled. + backgroundColor: colors.surfaceBright, + shadowColor: '#000', + shadowOpacity: 0.25, + shadowRadius: 4, + shadowOffset: { width: 0, height: 2 }, + elevation: 4 + }, + fabPressed: { + backgroundColor: colors.textPrimary + }, + fabDisabled: { + opacity: 0.5 + } +}) diff --git a/mobile/src/theme/mobile-theme.ts b/mobile/src/theme/mobile-theme.ts index 5583b73f078..76b14415a32 100644 --- a/mobile/src/theme/mobile-theme.ts +++ b/mobile/src/theme/mobile-theme.ts @@ -12,6 +12,11 @@ export const colors = { textSecondary: '#888888', textMuted: '#555555', + // Crisp near-white surface for the single primary action on a screen (the + // worktree FAB). Brighter than textPrimary so it reads as a solid button, not + // disabled chrome, while staying monochrome (STYLEGUIDE: color is for state). + surfaceBright: '#f5f5f5', + accentBlue: '#3b82f6', statusGreen: '#22c55e',