mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 08:06:36 +00:00
feat: draw buttons, switches, selects, inputs and badges like the webmail
This commit is contained in:
+17
-11
@@ -1,20 +1,25 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, ViewStyle } from 'react-native';
|
||||
import { radius, typography, type ThemePalette } from '../theme/tokens';
|
||||
import { radius, type ThemePalette } from '../theme/tokens';
|
||||
import { useColors } from '../theme/colors';
|
||||
|
||||
interface BadgeProps {
|
||||
/** Sentence case ("Not on this server"); badges are never upper case. */
|
||||
children: string;
|
||||
variant?: 'default' | 'primary' | 'destructive' | 'outline' | 'muted';
|
||||
style?: ViewStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* A small status pill. The default is the webmail's outline badge: hairline
|
||||
* border, muted 12px text. The tinted variants keep the same size.
|
||||
*/
|
||||
export default function Badge({ children, variant = 'default', style }: BadgeProps) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
return (
|
||||
<View style={[styles.base, styles[`bg_${variant}` as const], style]}>
|
||||
<Text style={[styles.text, styles[`text_${variant}` as const]]}>
|
||||
<Text style={[styles.text, styles[`text_${variant}` as const]]} numberOfLines={1}>
|
||||
{children}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -25,23 +30,24 @@ function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
base: {
|
||||
borderRadius: radius.full,
|
||||
borderWidth: 1,
|
||||
borderColor: 'transparent',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
paddingVertical: 1,
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
text: { ...typography.caption, fontWeight: '500' },
|
||||
text: { fontSize: 12, lineHeight: 16 },
|
||||
|
||||
bg_default: { backgroundColor: c.surface },
|
||||
bg_primary: { backgroundColor: c.primaryBg },
|
||||
bg_default: { borderColor: c.border },
|
||||
bg_outline: { borderColor: c.border },
|
||||
bg_primary: { backgroundColor: c.accent },
|
||||
bg_destructive: { backgroundColor: c.errorBg },
|
||||
bg_outline: { backgroundColor: 'transparent', borderWidth: 1, borderColor: c.border },
|
||||
bg_muted: { backgroundColor: c.muted },
|
||||
|
||||
text_default: { color: c.text },
|
||||
text_primary: { color: c.primary },
|
||||
text_default: { color: c.mutedForeground },
|
||||
text_outline: { color: c.mutedForeground },
|
||||
text_primary: { color: c.accentForeground },
|
||||
text_destructive: { color: c.error },
|
||||
text_outline: { color: c.text },
|
||||
text_muted: { color: c.mutedForeground },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+43
-27
@@ -1,9 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Pressable, Text, StyleSheet, ViewStyle, ActivityIndicator } from 'react-native';
|
||||
import { spacing, radius, typography, componentSizes, type ThemePalette } from '../theme/tokens';
|
||||
import { spacing, radius, componentSizes, type ThemePalette } from '../theme/tokens';
|
||||
import { useColors } from '../theme/colors';
|
||||
|
||||
type ButtonVariant = 'default' | 'ghost' | 'outline' | 'destructive';
|
||||
/**
|
||||
* The webmail's standard-theme buttons (components/ui/button.tsx):
|
||||
* - default: bg-primary text-primary-foreground
|
||||
* - secondary: bg-muted text-foreground
|
||||
* - outline: border, transparent
|
||||
* - ghost: no border or fill until pressed
|
||||
* - destructive: red fill, white text
|
||||
* - destructiveOutline: red border and text, for a reset in a settings row
|
||||
* All 36 dp tall (lg 44 for a page's main action), radius 6, 14px medium.
|
||||
*/
|
||||
type ButtonVariant = 'default' | 'secondary' | 'ghost' | 'outline' | 'destructive' | 'destructiveOutline';
|
||||
type ButtonSize = 'sm' | 'md' | 'lg' | 'icon';
|
||||
|
||||
interface ButtonProps {
|
||||
@@ -19,8 +29,8 @@ interface ButtonProps {
|
||||
accessibilityLabel?: string;
|
||||
}
|
||||
|
||||
// Extra touch area so even the small sizes reach a 48 dp target.
|
||||
const HIT_SLOP: Record<ButtonSize, number> = { sm: 6, md: 4, lg: 2, icon: 4 };
|
||||
// Extra touch area so every size reaches a 48 dp target.
|
||||
const HIT_SLOP: Record<ButtonSize, number> = { sm: 6, md: 6, lg: 2, icon: 4 };
|
||||
|
||||
export default function Button({
|
||||
variant = 'default',
|
||||
@@ -36,6 +46,9 @@ export default function Button({
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const isDisabled = disabled || loading;
|
||||
const spinnerColor = variant === 'default'
|
||||
? c.primaryForeground
|
||||
: variant === 'destructive' ? '#ffffff' : c.text;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
@@ -55,15 +68,12 @@ export default function Button({
|
||||
accessibilityState={{ disabled: isDisabled, busy: loading }}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={variant === 'default' ? c.primaryForeground : c.primary}
|
||||
/>
|
||||
<ActivityIndicator size="small" color={spinnerColor} />
|
||||
) : (
|
||||
<>
|
||||
{icon}
|
||||
{typeof children === 'string' ? (
|
||||
<Text style={[styles.text, styles[`text_${variant}` as const], isDisabled && styles.textDisabled]}>
|
||||
<Text style={[styles.text, styles[`text_${variant}` as const]]} numberOfLines={1}>
|
||||
{children}
|
||||
</Text>
|
||||
) : (
|
||||
@@ -79,30 +89,36 @@ function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
base: {
|
||||
flexDirection: 'row', alignItems: 'center', justifyContent: 'center',
|
||||
borderRadius: radius.md, gap: spacing.sm,
|
||||
borderRadius: radius.sm, gap: spacing.sm,
|
||||
},
|
||||
text: { ...typography.bodyMedium },
|
||||
text: { fontSize: 14, fontWeight: '500', lineHeight: 20 },
|
||||
disabled: { opacity: 0.5 },
|
||||
textDisabled: { opacity: 0.5 },
|
||||
|
||||
size_sm: { height: componentSizes.buttonSm, paddingHorizontal: spacing.md },
|
||||
size_md: { height: componentSizes.buttonMd, paddingHorizontal: spacing.lg, paddingVertical: spacing.sm },
|
||||
size_lg: { height: componentSizes.buttonLg, paddingHorizontal: spacing.xxxl },
|
||||
// minHeight, so a larger system font grows the button instead of clipping.
|
||||
size_sm: { minHeight: componentSizes.buttonSm, paddingHorizontal: spacing.md },
|
||||
size_md: { minHeight: componentSizes.buttonSm, paddingHorizontal: 14 },
|
||||
size_lg: { minHeight: componentSizes.buttonLg, paddingHorizontal: spacing.xxl },
|
||||
size_icon: { height: componentSizes.buttonMd, width: componentSizes.buttonMd, paddingHorizontal: 0 },
|
||||
|
||||
bg_default: { backgroundColor: c.primary, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.05, shadowRadius: 1, elevation: 1 },
|
||||
bg_ghost: { backgroundColor: 'transparent' },
|
||||
bg_outline: { backgroundColor: c.background, borderWidth: 1, borderColor: c.border },
|
||||
bg_destructive: { backgroundColor: c.error, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.05, shadowRadius: 1, elevation: 1 },
|
||||
bg_default: { backgroundColor: c.primary },
|
||||
bg_secondary: { backgroundColor: c.muted },
|
||||
bg_ghost: { backgroundColor: 'transparent' },
|
||||
bg_outline: { backgroundColor: 'transparent', borderWidth: 1, borderColor: c.border },
|
||||
bg_destructive: { backgroundColor: c.error },
|
||||
bg_destructiveOutline: { backgroundColor: 'transparent', borderWidth: 1, borderColor: c.error },
|
||||
|
||||
pressed_default: { backgroundColor: c.primaryDark, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 2, elevation: 2 },
|
||||
pressed_ghost: { backgroundColor: c.accent },
|
||||
pressed_outline: { backgroundColor: c.accent },
|
||||
pressed_destructive: { opacity: 0.9 },
|
||||
pressed_default: { opacity: 0.9 },
|
||||
pressed_secondary: { opacity: 0.8 },
|
||||
pressed_ghost: { backgroundColor: c.muted },
|
||||
pressed_outline: { backgroundColor: c.muted },
|
||||
pressed_destructive: { opacity: 0.9 },
|
||||
pressed_destructiveOutline: { backgroundColor: c.errorBg },
|
||||
|
||||
text_default: { color: c.primaryForeground },
|
||||
text_ghost: { color: c.text },
|
||||
text_outline: { color: c.text },
|
||||
text_destructive: { color: c.errorForeground },
|
||||
text_default: { color: c.primaryForeground },
|
||||
text_secondary: { color: c.text },
|
||||
text_ghost: { color: c.text },
|
||||
text_outline: { color: c.text },
|
||||
text_destructive: { color: '#ffffff' },
|
||||
text_destructiveOutline: { color: c.error },
|
||||
});
|
||||
}
|
||||
|
||||
+12
-13
@@ -14,13 +14,12 @@ interface InputProps extends TextInputProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches webmail input.tsx:
|
||||
* - h-10 w-full rounded-md border border-input bg-background
|
||||
* - px-3 py-2 text-sm text-foreground
|
||||
* - placeholder:text-muted-foreground
|
||||
* - hover:border-muted-foreground
|
||||
* - focus:ring-2 ring-ring border-ring
|
||||
* - disabled:opacity-50
|
||||
* Matches webmail input.tsx in the standard theme:
|
||||
* - h-10 rounded-md border border-input, transparent background
|
||||
* - px-3 text-sm text-foreground, placeholder in muted-foreground
|
||||
* - focus: border-ring
|
||||
* - disabled: opacity-50
|
||||
* - label above in text-sm font-medium text-foreground
|
||||
*/
|
||||
export default function Input({
|
||||
label,
|
||||
@@ -50,7 +49,7 @@ export default function Input({
|
||||
ref={ref}
|
||||
{...props}
|
||||
style={[styles.input, style]}
|
||||
placeholderTextColor={c.textMuted}
|
||||
placeholderTextColor={c.mutedForeground}
|
||||
onFocus={(e) => { setFocused(true); props.onFocus?.(e); }}
|
||||
onBlur={(e) => { setFocused(false); props.onBlur?.(e); }}
|
||||
/>
|
||||
@@ -65,8 +64,8 @@ function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
label: {
|
||||
...typography.bodyMedium,
|
||||
color: c.textSecondary,
|
||||
marginBottom: spacing.sm,
|
||||
color: c.text,
|
||||
marginBottom: 6,
|
||||
},
|
||||
inputWrapper: {
|
||||
flexDirection: 'row',
|
||||
@@ -75,12 +74,12 @@ function makeStyles(c: ThemePalette) {
|
||||
borderWidth: 1,
|
||||
borderColor: c.border, // border-input
|
||||
borderRadius: radius.sm, // rounded-md = 6px
|
||||
backgroundColor: c.background, // bg-background
|
||||
backgroundColor: 'transparent',
|
||||
paddingHorizontal: spacing.md, // px-3 = 12px
|
||||
},
|
||||
// Colour only, so focusing doesn't shift the text by a pixel.
|
||||
inputFocused: {
|
||||
borderColor: c.borderFocus, // border-ring (blue-500)
|
||||
borderWidth: 2,
|
||||
borderColor: c.borderFocus,
|
||||
},
|
||||
inputError: {
|
||||
borderColor: c.error,
|
||||
|
||||
@@ -12,11 +12,9 @@ interface ToggleSwitchProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches webmail ToggleSwitch:
|
||||
* - h-6 w-11 rounded-full
|
||||
* - checked: bg-primary, unchecked: bg-muted
|
||||
* - thumb: h-4 w-4 rounded-full bg-background
|
||||
* - translate-x-6 (checked) / translate-x-1 (unchecked)
|
||||
* The webmail switch in the standard theme: a 44 x 24 track, bg-primary when
|
||||
* on (knob in primary-foreground), bg-muted when off (knob in background).
|
||||
* The knob is 20 dp with 2 dp of track around it.
|
||||
*/
|
||||
export default function ToggleSwitch({ value, onValueChange, disabled = false, accessibilityLabel }: ToggleSwitchProps) {
|
||||
const c = useColors();
|
||||
@@ -38,7 +36,8 @@ export default function ToggleSwitch({ value, onValueChange, disabled = false, a
|
||||
<View
|
||||
style={[
|
||||
styles.thumb,
|
||||
{ transform: [{ translateX: value ? 24 : 4 }] },
|
||||
value ? styles.thumbOn : styles.thumbOff,
|
||||
{ transform: [{ translateX: value ? 22 : 2 }] },
|
||||
]}
|
||||
/>
|
||||
</Pressable>
|
||||
@@ -60,9 +59,14 @@ function makeStyles(c: ThemePalette) {
|
||||
backgroundColor: c.muted,
|
||||
},
|
||||
thumb: {
|
||||
width: componentSizes.toggleThumb, // h-4 w-4 = 16
|
||||
width: componentSizes.toggleThumb,
|
||||
height: componentSizes.toggleThumb,
|
||||
borderRadius: radius.full,
|
||||
},
|
||||
thumbOn: {
|
||||
backgroundColor: c.primaryForeground,
|
||||
},
|
||||
thumbOff: {
|
||||
backgroundColor: c.background,
|
||||
},
|
||||
disabled: {
|
||||
|
||||
@@ -179,7 +179,7 @@ function makeStyles(c: ThemePalette) {
|
||||
backgroundColor: c.muted,
|
||||
gap: 2,
|
||||
},
|
||||
previewLabel: { fontSize: 11, fontWeight: '600', color: c.mutedForeground, textTransform: 'uppercase' },
|
||||
previewLabel: { fontSize: 12, fontWeight: '500', color: c.mutedForeground },
|
||||
previewValue: { ...typography.captionMedium, color: c.text },
|
||||
tokenWrap: { flexDirection: 'row', flexWrap: 'wrap', gap: 4, marginTop: spacing.md },
|
||||
tokenPill: {
|
||||
|
||||
@@ -317,6 +317,7 @@ function makeStyles(c: ThemePalette) {
|
||||
},
|
||||
bottomActions: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: spacing.sm,
|
||||
paddingTop: spacing.sm,
|
||||
},
|
||||
|
||||
@@ -267,7 +267,7 @@ function makeStyles(c: ThemePalette) {
|
||||
backgroundColor: c.muted,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
notesTitle: { ...typography.caption, color: c.mutedForeground, textTransform: 'uppercase' },
|
||||
notesTitle: { ...typography.captionMedium, color: c.mutedForeground },
|
||||
notesBody: { ...typography.body, color: c.text, lineHeight: 20 },
|
||||
progressBox: {
|
||||
paddingHorizontal: spacing.md,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ScrollView,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import { FlaskConical, Lock, ChevronDown, Check } from '../icons';
|
||||
import { FlaskConical, Lock, ChevronDown, ChevronRight, Check, ExternalLink, type AppIcon } from '../icons';
|
||||
import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens';
|
||||
import { useColors } from '../../theme/colors';
|
||||
import ToggleSwitchComponent from '../ToggleSwitch';
|
||||
@@ -17,12 +17,14 @@ import { useSearchHighlight } from './search-highlight';
|
||||
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
||||
|
||||
/**
|
||||
* Mirrors webmail settings-section.tsx:
|
||||
* - SettingsSection: space-y-4, with experimental banner, h3 title + description
|
||||
* - SettingItem: flex items-start justify-between py-3 border-b border-border
|
||||
* - ToggleSwitch: h-6 w-11
|
||||
* - Select: rounded-md bg-muted border text-sm px-3 py-1.5
|
||||
* Mirrors webmail settings-section.tsx in the standard theme:
|
||||
* - SettingsSection: title 18px medium, description 14px muted, then rows
|
||||
* - SettingItem: label 14px medium, description 12px muted, 12 dp vertical
|
||||
* padding, a 1 dp border under each row, the control on the right
|
||||
* - ToggleSwitch: 44 x 24
|
||||
* - Select: a 36 dp outline button with a chevron, radius 6
|
||||
* - RadioGroup: flex gap-1.5, pill buttons
|
||||
* Plus SettingsRow, the 56 dp row of the Settings list (icon, title, value).
|
||||
*/
|
||||
|
||||
interface SettingsSectionProps {
|
||||
@@ -76,6 +78,9 @@ interface SettingItemProps {
|
||||
children?: React.ReactNode;
|
||||
locked?: boolean;
|
||||
noBorder?: boolean;
|
||||
/** Makes the whole row a link: to a page (chevron) or out of the app. */
|
||||
onPress?: () => void;
|
||||
link?: 'page' | 'external';
|
||||
}
|
||||
|
||||
export function SettingItem({
|
||||
@@ -84,16 +89,15 @@ export function SettingItem({
|
||||
children,
|
||||
locked,
|
||||
noBorder,
|
||||
onPress,
|
||||
link = 'page',
|
||||
}: SettingItemProps) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const highlight = useSearchHighlight(label);
|
||||
return (
|
||||
<View
|
||||
ref={highlight.ref}
|
||||
onLayout={highlight.onLayout}
|
||||
style={[styles.settingItem, !noBorder && styles.settingItemBorder, locked && styles.locked, highlight.highlighted && styles.searchHighlight]}
|
||||
>
|
||||
const rowStyle = [styles.settingItem, !noBorder && styles.settingItemBorder, locked && styles.locked, highlight.highlighted && styles.searchHighlight];
|
||||
const body = (
|
||||
<>
|
||||
<View style={styles.settingContent}>
|
||||
<View style={styles.settingLabelRow}>
|
||||
<Text style={styles.settingLabel}>{label}</Text>
|
||||
@@ -106,10 +110,81 @@ export function SettingItem({
|
||||
<View style={styles.settingRight}>{children}</View>
|
||||
</SettingLabelContext.Provider>
|
||||
)}
|
||||
{onPress && (link === 'external'
|
||||
? <ExternalLink size={16} color={c.mutedForeground} />
|
||||
: <ChevronRight size={16} color={c.mutedForeground} />)}
|
||||
</>
|
||||
);
|
||||
if (onPress) {
|
||||
return (
|
||||
<Pressable
|
||||
ref={highlight.ref}
|
||||
onLayout={highlight.onLayout}
|
||||
onPress={onPress}
|
||||
accessibilityRole={link === 'external' ? 'link' : 'button'}
|
||||
style={({ pressed }) => [...rowStyle, pressed && styles.settingItemPressed]}
|
||||
>
|
||||
{body}
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View ref={highlight.ref} onLayout={highlight.onLayout} style={rowStyle}>
|
||||
{body}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
interface SettingsRowProps {
|
||||
icon: AppIcon;
|
||||
title: string;
|
||||
/** The current value, e.g. "Dark · Standard theme". */
|
||||
value?: string | null;
|
||||
onPress: () => void;
|
||||
disabled?: boolean;
|
||||
/** The page open beside the list on a wide window. */
|
||||
selected?: boolean;
|
||||
/** Sits after the title, e.g. a Badge. */
|
||||
badge?: React.ReactNode;
|
||||
}
|
||||
|
||||
/** A row of the Settings list: muted icon, 15px title, 13px current value. */
|
||||
export function SettingsRow({ icon: Icon, title, value, onPress, disabled, selected, badge }: SettingsRowProps) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={value ? `${title}, ${value}` : title}
|
||||
accessibilityState={{ disabled, selected }}
|
||||
style={({ pressed }) => [
|
||||
styles.row,
|
||||
disabled && styles.rowDisabled,
|
||||
!disabled && pressed && styles.rowPressed,
|
||||
selected && styles.rowSelected,
|
||||
]}
|
||||
>
|
||||
<Icon size={20} color={c.mutedForeground} />
|
||||
<View style={styles.rowText}>
|
||||
<View style={styles.rowTitleLine}>
|
||||
<Text style={styles.rowTitle} numberOfLines={1}>{title}</Text>
|
||||
{badge}
|
||||
</View>
|
||||
{value ? <Text style={styles.rowValue} numberOfLines={1}>{value}</Text> : null}
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
/** A group label in the Settings list: title case, 13px medium, muted. */
|
||||
export function SettingsGroupLabel({ children }: { children: string }) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
return <Text style={styles.groupLabel} accessibilityRole="header">{children}</Text>;
|
||||
}
|
||||
|
||||
interface ToggleSwitchProps {
|
||||
checked: boolean;
|
||||
onChange: (v: boolean) => void;
|
||||
@@ -151,6 +226,7 @@ export function RadioGroup({ value, onChange, options, style }: RadioGroupProps)
|
||||
accessibilityRole="radio"
|
||||
accessibilityState={{ selected, checked: selected }}
|
||||
accessibilityLabel={opt.label}
|
||||
hitSlop={{ top: 10, bottom: 10 }}
|
||||
style={[
|
||||
styles.radioOption,
|
||||
selected ? styles.radioSelected : styles.radioUnselected,
|
||||
@@ -185,14 +261,15 @@ export function Select({ value, onChange, options, style }: SelectProps) {
|
||||
return (
|
||||
<>
|
||||
<Pressable
|
||||
style={[styles.select, style]}
|
||||
style={({ pressed }) => [styles.select, pressed && styles.selectPressed, style]}
|
||||
onPress={() => setOpen(true)}
|
||||
hitSlop={6}
|
||||
accessibilityRole="combobox"
|
||||
accessibilityLabel={current?.label ?? ''}
|
||||
accessibilityState={{ expanded: open }}
|
||||
>
|
||||
<Text style={styles.selectText}>{current?.label ?? ''}</Text>
|
||||
<ChevronDown size={14} color={c.mutedForeground} />
|
||||
<ChevronDown size={14} color={c.text} />
|
||||
</Pressable>
|
||||
<Modal supportedOrientations={MODAL_ORIENTATIONS}
|
||||
visible={open}
|
||||
@@ -235,19 +312,20 @@ export function Select({ value, onChange, options, style }: SelectProps) {
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
section: {
|
||||
gap: spacing.lg,
|
||||
},
|
||||
sectionBody: {
|
||||
gap: spacing.lg,
|
||||
gap: spacing.xs,
|
||||
},
|
||||
// Rows sit flush; the border under each one separates them.
|
||||
sectionBody: { marginTop: spacing.xs },
|
||||
sectionTitle: {
|
||||
...typography.h3,
|
||||
fontSize: 18,
|
||||
fontWeight: '500',
|
||||
lineHeight: 26,
|
||||
color: c.text,
|
||||
},
|
||||
sectionDescription: {
|
||||
...typography.body,
|
||||
color: c.mutedForeground,
|
||||
marginTop: 4,
|
||||
marginTop: 2,
|
||||
},
|
||||
experimentalBanner: {
|
||||
flexDirection: 'row',
|
||||
@@ -269,11 +347,15 @@ function makeStyles(c: ThemePalette) {
|
||||
},
|
||||
settingItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
minHeight: 48,
|
||||
paddingVertical: spacing.md,
|
||||
gap: spacing.md,
|
||||
},
|
||||
settingItemPressed: {
|
||||
backgroundColor: c.surfaceHover,
|
||||
},
|
||||
settingItemBorder: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: c.border,
|
||||
@@ -283,7 +365,6 @@ function makeStyles(c: ThemePalette) {
|
||||
},
|
||||
settingContent: {
|
||||
flex: 1,
|
||||
paddingRight: spacing.lg,
|
||||
},
|
||||
settingLabelRow: {
|
||||
flexDirection: 'row',
|
||||
@@ -297,7 +378,7 @@ function makeStyles(c: ThemePalette) {
|
||||
settingDescription: {
|
||||
...typography.caption,
|
||||
color: c.mutedForeground,
|
||||
marginTop: 2,
|
||||
marginTop: 3,
|
||||
},
|
||||
settingRight: {
|
||||
flexShrink: 0,
|
||||
@@ -340,18 +421,44 @@ function makeStyles(c: ThemePalette) {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: 6,
|
||||
minHeight: 36,
|
||||
paddingHorizontal: 10,
|
||||
borderRadius: radius.sm,
|
||||
backgroundColor: c.muted,
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: c.border,
|
||||
minWidth: 120,
|
||||
},
|
||||
selectPressed: {
|
||||
backgroundColor: c.muted,
|
||||
},
|
||||
selectText: {
|
||||
...typography.body,
|
||||
color: c.text,
|
||||
flex: 1,
|
||||
flexShrink: 1,
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 14,
|
||||
minHeight: 56,
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingVertical: spacing.sm,
|
||||
},
|
||||
rowPressed: { backgroundColor: c.surfaceHover },
|
||||
rowSelected: { backgroundColor: c.muted },
|
||||
rowDisabled: { opacity: 0.55 },
|
||||
rowText: { flex: 1, gap: 2 },
|
||||
rowTitleLine: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm },
|
||||
rowTitle: { fontSize: 15, lineHeight: 20, color: c.text, flexShrink: 1 },
|
||||
rowValue: { fontSize: 13, lineHeight: 18, color: c.mutedForeground },
|
||||
groupLabel: {
|
||||
fontSize: 13,
|
||||
fontWeight: '500',
|
||||
lineHeight: 18,
|
||||
color: c.mutedForeground,
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingTop: spacing.lg,
|
||||
paddingBottom: spacing.xs,
|
||||
},
|
||||
modalBackdrop: {
|
||||
flex: 1,
|
||||
|
||||
+1
-1
@@ -326,6 +326,6 @@ export const componentSizes = {
|
||||
headerHeight: 56, inputHeight: 40,
|
||||
buttonSm: 36, buttonMd: 40, buttonLg: 44,
|
||||
tagDot: 6, eventDot: 6,
|
||||
toggleHeight: 24, toggleWidth: 44, toggleThumb: 16,
|
||||
toggleHeight: 24, toggleWidth: 44, toggleThumb: 20,
|
||||
fab: 56, badgeSize: 16,
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user