mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-18 06:10:43 -07:00
Mobile companion app + paired-device backend
New iOS-first companion (Expo SDK 54 + NativeWind v4) with three tabs: Captures (the hero — floating gold mic, live mic-meter waveform, expand-row playback), Generate (profile picker + speak + autoplay + recent), and Voices (searchable profile list). Pairing (V0): backend mints a one-time token, mobile scans/pastes the voicebox:// URL, server returns a long-lived bearer it stores only as a SHA-256 hash. Bearer-or-loopback auth applied to every user-data router so binding 0.0.0.0 doesn't leak existing endpoints. Loopback callers (the desktop app) keep their friction-free access. Desktop Settings → Mobile pane: live host picker (LAN / Tailscale auto- detected via the App-bundle binary path on macOS), QR rendering, 5-minute expiry countdown, copyable URL fallback, paired-device list with revoke. Auto-closes when a new device pairs. just dev now binds the backend to 0.0.0.0 so paired phones can reach it — and just setup-python pins mlx-audio==0.4.1 + mlx-lm so fresh Apple Silicon worktrees get a working STT path on first install.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { forwardRef } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Pressable,
|
||||
type PressableProps,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { colors } from '@/lib/colors';
|
||||
|
||||
type Variant = 'primary' | 'secondary' | 'ghost';
|
||||
type Size = 'sm' | 'md' | 'lg';
|
||||
|
||||
type Props = Omit<PressableProps, 'children'> & {
|
||||
label: string;
|
||||
variant?: Variant;
|
||||
size?: Size;
|
||||
loading?: boolean;
|
||||
leftSlot?: React.ReactNode;
|
||||
rightSlot?: React.ReactNode;
|
||||
};
|
||||
|
||||
const containerBase =
|
||||
'flex-row items-center justify-center rounded-full active:opacity-90';
|
||||
|
||||
const variantContainer: Record<Variant, string> = {
|
||||
primary: 'bg-accent',
|
||||
secondary: 'bg-card border border-border',
|
||||
ghost: 'bg-transparent',
|
||||
};
|
||||
|
||||
const variantLabel: Record<Variant, string> = {
|
||||
primary: 'text-accent-foreground',
|
||||
secondary: 'text-foreground',
|
||||
ghost: 'text-muted-foreground',
|
||||
};
|
||||
|
||||
const sizeContainer: Record<Size, string> = {
|
||||
sm: 'px-4 py-2 gap-1.5',
|
||||
md: 'px-6 py-3 gap-2',
|
||||
lg: 'px-8 py-4 gap-2.5',
|
||||
};
|
||||
|
||||
const sizeLabel: Record<Size, string> = {
|
||||
sm: 'text-xs',
|
||||
md: 'text-sm',
|
||||
lg: 'text-base',
|
||||
};
|
||||
|
||||
// Mirrors the desktop CTA in landing/src/components/SponsorPromo.tsx —
|
||||
// rounded-full, gold accent, uppercase semibold label with wide tracking.
|
||||
export const Button = forwardRef<View, Props>(function Button(
|
||||
{
|
||||
label,
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
loading = false,
|
||||
leftSlot,
|
||||
rightSlot,
|
||||
disabled,
|
||||
style,
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const isPrimary = variant === 'primary';
|
||||
const isDisabled = disabled || loading;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
ref={ref}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ disabled: isDisabled, busy: loading }}
|
||||
disabled={isDisabled}
|
||||
style={[
|
||||
isPrimary && {
|
||||
shadowColor: colors.accent,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.35,
|
||||
shadowRadius: 16,
|
||||
elevation: 6,
|
||||
},
|
||||
isDisabled && { opacity: 0.5 },
|
||||
style as object,
|
||||
]}
|
||||
className={`${containerBase} ${variantContainer[variant]} ${sizeContainer[size]}`}
|
||||
{...rest}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={isPrimary ? colors.accentForeground : colors.foreground}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{leftSlot}
|
||||
<Text
|
||||
className={`font-semibold uppercase ${variantLabel[variant]} ${sizeLabel[size]}`}
|
||||
style={{ letterSpacing: 1.2 }}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
{rightSlot}
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user