mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-19 04:06:43 +02:00
better eq profile assigning
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Pressable, StyleSheet, View } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Text } from '@/components/Text';
|
||||
import { EqSheet } from '@/components/eq/EqSheet';
|
||||
import type { KnownEQOutputDevice } from '@/audio/eqDevicePresets';
|
||||
import type { EQPreset } from '@/types/audio';
|
||||
import { radius, spacing } from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
|
||||
import { playHaptic } from '@/lib/haptics';
|
||||
|
||||
interface PresetDeviceAssignmentSheetProps {
|
||||
preset: EQPreset;
|
||||
devices: KnownEQOutputDevice[];
|
||||
assignments: Readonly<Record<string, string>>;
|
||||
presets: EQPreset[];
|
||||
currentDeviceKey: string | null;
|
||||
onSave: (deviceKeys: string[]) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function kindLabel(device: KnownEQOutputDevice): string {
|
||||
switch (device.kind) {
|
||||
case 'speaker':
|
||||
return 'Phone speaker';
|
||||
case 'wired':
|
||||
return 'Wired output';
|
||||
case 'bluetooth':
|
||||
return 'Bluetooth';
|
||||
case 'usb':
|
||||
return 'USB audio';
|
||||
case 'hdmi':
|
||||
return 'HDMI audio';
|
||||
default:
|
||||
return 'Audio output';
|
||||
}
|
||||
}
|
||||
|
||||
/** Optional Poweramp-style automation: one saved preset may own many devices. */
|
||||
export function PresetDeviceAssignmentSheet({
|
||||
preset,
|
||||
devices,
|
||||
assignments,
|
||||
presets,
|
||||
currentDeviceKey,
|
||||
onSave,
|
||||
onClose,
|
||||
}: PresetDeviceAssignmentSheetProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const ripple = useRipple();
|
||||
const [selected, setSelected] = useState<Set<string>>(
|
||||
() => new Set(devices.filter((device) => assignments[device.key] === preset.id).map((device) => device.key))
|
||||
);
|
||||
const presetNames = useMemo(
|
||||
() => new Map(presets.map((candidate) => [candidate.id, candidate.name])),
|
||||
[presets]
|
||||
);
|
||||
const sortedDevices = useMemo(
|
||||
() => [...devices].sort((a, b) => {
|
||||
if (a.key === currentDeviceKey) return -1;
|
||||
if (b.key === currentDeviceKey) return 1;
|
||||
return b.lastSeenAt - a.lastSeenAt || a.label.localeCompare(b.label);
|
||||
}),
|
||||
[currentDeviceKey, devices]
|
||||
);
|
||||
|
||||
const toggleDevice = (deviceKey: string) => {
|
||||
setSelected((current) => {
|
||||
const next = new Set(current);
|
||||
if (next.has(deviceKey)) next.delete(deviceKey);
|
||||
else next.add(deviceKey);
|
||||
return next;
|
||||
});
|
||||
playHaptic('selection');
|
||||
};
|
||||
|
||||
return (
|
||||
<EqSheet onClose={onClose} scrollable>
|
||||
<Text variant="heading" style={styles.title}>
|
||||
Assign {preset.name}
|
||||
</Text>
|
||||
<Text variant="caption" color={colors.textSecondary} style={styles.description}>
|
||||
This preset will load automatically when a selected output becomes active.
|
||||
</Text>
|
||||
|
||||
{sortedDevices.length === 0 ? (
|
||||
<Text variant="body" color={colors.textTertiary} style={styles.empty}>
|
||||
No audio outputs have been observed yet.
|
||||
</Text>
|
||||
) : (
|
||||
<View style={styles.list}>
|
||||
{sortedDevices.map((device) => {
|
||||
const checked = selected.has(device.key);
|
||||
const assignedPresetId = assignments[device.key];
|
||||
const assignedPresetName = assignedPresetId ? presetNames.get(assignedPresetId) : null;
|
||||
const subtitle = assignedPresetId === preset.id
|
||||
? 'Assigned to this preset'
|
||||
: assignedPresetName
|
||||
? `Currently assigned to ${assignedPresetName}`
|
||||
: kindLabel(device);
|
||||
return (
|
||||
<Pressable
|
||||
key={device.key}
|
||||
android_ripple={ripple.bounded}
|
||||
unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
style={styles.deviceRow}
|
||||
onPress={() => toggleDevice(device.key)}
|
||||
accessibilityRole="checkbox"
|
||||
accessibilityState={{ checked }}
|
||||
accessibilityLabel={`${device.label}${device.key === currentDeviceKey ? ', current output' : ''}`}
|
||||
>
|
||||
<View style={styles.deviceMeta}>
|
||||
<View style={styles.deviceTitleRow}>
|
||||
<Text variant="body" numberOfLines={1} style={styles.deviceTitle}>
|
||||
{device.label}
|
||||
</Text>
|
||||
{device.key === currentDeviceKey ? (
|
||||
<Text variant="caption" color={colors.accentTextStrong} style={styles.currentBadge}>
|
||||
CURRENT
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
<Text variant="caption" color={colors.textSecondary} numberOfLines={1}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons
|
||||
name={checked ? 'checkbox' : 'square-outline'}
|
||||
size={22}
|
||||
color={checked ? colors.accent : colors.textTertiary}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.actions}>
|
||||
<Pressable android_ripple={ripple.bounded} style={[styles.button, styles.cancel]} onPress={onClose}>
|
||||
<Text variant="label" color={colors.textSecondary}>Cancel</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
android_ripple={ripple.bounded}
|
||||
style={[styles.button, styles.save]}
|
||||
onPress={() => {
|
||||
onSave([...selected]);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<Text variant="label" color={colors.accentTextStrong}>Save assignments</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</EqSheet>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
title: {
|
||||
marginTop: spacing.xs,
|
||||
},
|
||||
description: {
|
||||
lineHeight: 17,
|
||||
marginTop: spacing.xs,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
empty: {
|
||||
paddingVertical: spacing.xl,
|
||||
},
|
||||
list: {
|
||||
gap: spacing.xs,
|
||||
},
|
||||
deviceRow: {
|
||||
minHeight: 60,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.md,
|
||||
borderRadius: radius.md,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
backgroundColor: colors.glassBg,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.glassBorder,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
deviceMeta: {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
gap: 2,
|
||||
},
|
||||
deviceTitleRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
},
|
||||
deviceTitle: {
|
||||
flexShrink: 1,
|
||||
},
|
||||
currentBadge: {
|
||||
fontSize: 10,
|
||||
letterSpacing: 0.7,
|
||||
},
|
||||
actions: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
gap: spacing.sm,
|
||||
marginTop: spacing.lg,
|
||||
},
|
||||
button: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingVertical: spacing.md,
|
||||
borderRadius: radius.pill,
|
||||
},
|
||||
cancel: {
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.glassBorder,
|
||||
},
|
||||
save: {
|
||||
backgroundColor: colors.accentGlow,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.accent,
|
||||
},
|
||||
}));
|
||||
|
||||
export default PresetDeviceAssignmentSheet;
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Pressable, StyleSheet } from 'react-native';
|
||||
import { Pressable, StyleSheet, View } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Text } from '@/components/Text';
|
||||
import { spacing } from '@/theme';
|
||||
import { radius, spacing } from '@/theme';
|
||||
import { useColors } from '@/theme/themed';
|
||||
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
|
||||
import type { KnownEQOutputDevice } from '@/audio/eqDevicePresets';
|
||||
import type { EQPreset } from '@/types/audio';
|
||||
import {
|
||||
EqSheet,
|
||||
@@ -14,8 +15,11 @@ import {
|
||||
interface PresetSheetProps {
|
||||
presets: EQPreset[];
|
||||
activePresetId: string | null;
|
||||
knownDevices: KnownEQOutputDevice[];
|
||||
assignments: Readonly<Record<string, string>>;
|
||||
onApply: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
onAssign: (preset: EQPreset) => void;
|
||||
onDelete: (preset: EQPreset) => void;
|
||||
onSaveNew: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
@@ -32,7 +36,10 @@ function modeIcon(preset: EQPreset): 'options-outline' | 'analytics-outline' {
|
||||
export function PresetSheet({
|
||||
presets,
|
||||
activePresetId,
|
||||
knownDevices,
|
||||
assignments,
|
||||
onApply,
|
||||
onAssign,
|
||||
onDelete,
|
||||
onSaveNew,
|
||||
onClose,
|
||||
@@ -41,25 +48,69 @@ export function PresetSheet({
|
||||
const ripple = useRipple();
|
||||
const builtIn = presets.filter((p) => !p.isCustom);
|
||||
const custom = presets.filter((p) => p.isCustom);
|
||||
const devicesByKey = new Map(knownDevices.map((device) => [device.key, device]));
|
||||
|
||||
const renderPreset = (preset: EQPreset) => {
|
||||
const assignedDeviceKeys = Object.entries(assignments)
|
||||
.filter(([, presetId]) => presetId === preset.id)
|
||||
.map(([deviceKey]) => deviceKey);
|
||||
const assignmentSubtitle = assignedDeviceKeys.length === 1
|
||||
? `Assigned to ${devicesByKey.get(assignedDeviceKeys[0])?.label ?? '1 device'}`
|
||||
: assignedDeviceKeys.length > 1
|
||||
? `Assigned to ${assignedDeviceKeys.length} devices`
|
||||
: undefined;
|
||||
return (
|
||||
<EqSheetItem
|
||||
key={preset.id}
|
||||
label={preset.name}
|
||||
subtitle={assignmentSubtitle}
|
||||
icon={preset.isCustom ? modeIcon(preset) : undefined}
|
||||
selected={preset.id === activePresetId}
|
||||
onPress={() => {
|
||||
onApply(preset.id);
|
||||
onClose();
|
||||
}}
|
||||
trailing={
|
||||
<View style={styles.trailingActions}>
|
||||
<Pressable
|
||||
android_ripple={ripple.bounded}
|
||||
unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
hitSlop={6}
|
||||
onPress={() => onAssign(preset)}
|
||||
style={styles.assignButton}
|
||||
accessibilityLabel={`Assign devices to ${preset.name}`}
|
||||
>
|
||||
<Text variant="label" color={colors.textSecondary}>
|
||||
Assign
|
||||
</Text>
|
||||
<Ionicons name="chevron-forward" size={14} color={colors.textTertiary} />
|
||||
</Pressable>
|
||||
{preset.isCustom ? (
|
||||
<Pressable
|
||||
android_ripple={ripple.bounded}
|
||||
unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
hitSlop={8}
|
||||
onPress={() => onDelete(preset)}
|
||||
style={styles.deleteButton}
|
||||
accessibilityLabel={`Delete preset ${preset.name}`}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={18} color={colors.textTertiary} />
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<EqSheet onClose={onClose}>
|
||||
<EqSheet onClose={onClose} scrollable>
|
||||
<Text variant="heading" style={styles.title}>
|
||||
Presets
|
||||
</Text>
|
||||
|
||||
<EqSheetSection label="BUILT-IN" />
|
||||
{builtIn.map((p) => (
|
||||
<EqSheetItem
|
||||
key={p.id}
|
||||
label={p.name}
|
||||
selected={p.id === activePresetId}
|
||||
onPress={() => {
|
||||
onApply(p.id);
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{builtIn.map(renderPreset)}
|
||||
|
||||
<EqSheetSection label="CUSTOM" />
|
||||
{custom.length === 0 ? (
|
||||
@@ -67,28 +118,7 @@ export function PresetSheet({
|
||||
No saved presets yet.
|
||||
</Text>
|
||||
) : (
|
||||
custom.map((p) => (
|
||||
<EqSheetItem
|
||||
key={p.id}
|
||||
label={p.name}
|
||||
icon={modeIcon(p)}
|
||||
selected={p.id === activePresetId}
|
||||
onPress={() => {
|
||||
onApply(p.id);
|
||||
onClose();
|
||||
}}
|
||||
trailing={
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
hitSlop={10}
|
||||
onPress={() => onDelete(p.id)}
|
||||
style={styles.delete}
|
||||
accessibilityLabel={`Delete preset ${p.name}`}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={18} color={colors.textTertiary} />
|
||||
</Pressable>
|
||||
}
|
||||
/>
|
||||
))
|
||||
custom.map(renderPreset)
|
||||
)}
|
||||
|
||||
<EqSheetItem
|
||||
@@ -110,7 +140,21 @@ const styles = StyleSheet.create({
|
||||
empty: {
|
||||
paddingVertical: spacing.sm,
|
||||
},
|
||||
delete: {
|
||||
trailingActions: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.xs,
|
||||
},
|
||||
assignButton: {
|
||||
minHeight: 40,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
paddingHorizontal: spacing.sm,
|
||||
borderRadius: radius.pill,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
deleteButton: {
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: spacing.sm,
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from 'react-native';
|
||||
import { BottomSheetTextInput } from '@gorhom/bottom-sheet';
|
||||
import { Text } from '@/components/Text';
|
||||
import { HapticSwitch } from '@/components/HapticSwitch';
|
||||
import {
|
||||
fonts,
|
||||
radius,
|
||||
@@ -17,16 +18,23 @@ import { EqSheet } from './EqSheet';
|
||||
|
||||
interface SavePresetSheetProps {
|
||||
defaultName: string;
|
||||
onSave: (name: string) => void;
|
||||
currentDeviceLabel?: string | null;
|
||||
onSave: (name: string, assignToCurrentDevice: boolean) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/** Name + save a custom preset from the current bands/preamp. */
|
||||
export function SavePresetSheet({ defaultName, onSave, onClose }: SavePresetSheetProps) {
|
||||
export function SavePresetSheet({
|
||||
defaultName,
|
||||
currentDeviceLabel,
|
||||
onSave,
|
||||
onClose,
|
||||
}: SavePresetSheetProps) {
|
||||
const styles = useStyles();
|
||||
const ripple = useRipple();
|
||||
const colors = useColors();
|
||||
const [name, setName] = useState(defaultName);
|
||||
const [assignToCurrentDevice, setAssignToCurrentDevice] = useState(false);
|
||||
const trimmed = name.trim();
|
||||
|
||||
return (
|
||||
@@ -46,11 +54,27 @@ export function SavePresetSheet({ defaultName, onSave, onClose }: SavePresetShee
|
||||
returnKeyType="done"
|
||||
onSubmitEditing={() => {
|
||||
if (trimmed) {
|
||||
onSave(trimmed);
|
||||
onSave(trimmed, assignToCurrentDevice);
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{currentDeviceLabel ? (
|
||||
<View style={styles.assignmentRow}>
|
||||
<View style={styles.assignmentText}>
|
||||
<Text variant="body">Assign to current output</Text>
|
||||
<Text variant="caption" color={colors.textSecondary} numberOfLines={1}>
|
||||
{currentDeviceLabel}
|
||||
</Text>
|
||||
</View>
|
||||
<HapticSwitch
|
||||
value={assignToCurrentDevice}
|
||||
onValueChange={setAssignToCurrentDevice}
|
||||
trackColor={{ false: colors.glassBorder, true: colors.accent }}
|
||||
thumbColor={colors.textPrimary}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
<View style={styles.actions}>
|
||||
<Pressable android_ripple={ripple.bounded} style={[styles.btn, styles.cancel]} onPress={onClose}>
|
||||
<Text variant="label" color={colors.textSecondary}>
|
||||
@@ -61,7 +85,7 @@ export function SavePresetSheet({ defaultName, onSave, onClose }: SavePresetShee
|
||||
style={[styles.btn, styles.save, !trimmed && styles.saveDisabled]}
|
||||
disabled={!trimmed}
|
||||
onPress={() => {
|
||||
onSave(trimmed);
|
||||
onSave(trimmed, assignToCurrentDevice);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
@@ -90,6 +114,22 @@ const useStyles = createThemedStyles((colors) => ({
|
||||
borderColor: colors.glassBorder,
|
||||
backgroundColor: colors.glassBg,
|
||||
},
|
||||
assignmentRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.md,
|
||||
marginTop: spacing.md,
|
||||
padding: spacing.md,
|
||||
borderRadius: radius.md,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.glassBorder,
|
||||
backgroundColor: colors.glassBg,
|
||||
},
|
||||
assignmentText: {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
gap: 2,
|
||||
},
|
||||
actions: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Ionicons } from '@expo/vector-icons';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import BottomSheet, {
|
||||
BottomSheetBackdrop,
|
||||
BottomSheetScrollView,
|
||||
BottomSheetView,
|
||||
type BottomSheetBackdropProps
|
||||
} from '@gorhom/bottom-sheet';
|
||||
@@ -19,7 +20,15 @@ import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
|
||||
import { playHaptic } from '@/lib/haptics';
|
||||
|
||||
export function AppSheet({ onClose, children }: { onClose: () => void; children: ReactNode }) {
|
||||
export function AppSheet({
|
||||
onClose,
|
||||
children,
|
||||
scrollable = false,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
scrollable?: boolean;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const insets = useSafeAreaInsets();
|
||||
const renderBackdrop = useCallback(
|
||||
@@ -45,9 +54,20 @@ export function AppSheet({ onClose, children }: { onClose: () => void; children:
|
||||
backgroundStyle={styles.sheetBg}
|
||||
handleIndicatorStyle={styles.handle}
|
||||
>
|
||||
<BottomSheetView style={[styles.content, { paddingBottom: insets.bottom + spacing.md }]}>
|
||||
{children}
|
||||
</BottomSheetView>
|
||||
{scrollable ? (
|
||||
<BottomSheetScrollView
|
||||
contentContainerStyle={[styles.content, { paddingBottom: insets.bottom + spacing.md }]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{children}
|
||||
</BottomSheetScrollView>
|
||||
) : (
|
||||
<BottomSheetView
|
||||
style={[styles.content, { paddingBottom: insets.bottom + spacing.md }]}
|
||||
>
|
||||
{children}
|
||||
</BottomSheetView>
|
||||
)}
|
||||
</BottomSheet>
|
||||
);
|
||||
}
|
||||
@@ -80,6 +100,7 @@ export function AppSheetTitle({ title, subtitle }: { title: string; subtitle?: s
|
||||
|
||||
export interface AppSheetItemProps {
|
||||
label: string;
|
||||
subtitle?: string;
|
||||
icon?: keyof typeof Ionicons.glyphMap;
|
||||
selected?: boolean;
|
||||
destructive?: boolean;
|
||||
@@ -89,6 +110,7 @@ export interface AppSheetItemProps {
|
||||
|
||||
export function AppSheetItem({
|
||||
label,
|
||||
subtitle,
|
||||
icon,
|
||||
selected,
|
||||
destructive,
|
||||
@@ -118,9 +140,16 @@ export function AppSheetItem({
|
||||
{icon ? (
|
||||
<Ionicons name={icon} size={20} color={destructive ? colors.warning : colors.textSecondary} />
|
||||
) : null}
|
||||
<Text variant="body" numberOfLines={1} style={styles.itemLabel} color={tint}>
|
||||
{label}
|
||||
</Text>
|
||||
<View style={styles.itemMeta}>
|
||||
<Text variant="body" numberOfLines={1} style={styles.itemLabel} color={tint}>
|
||||
{label}
|
||||
</Text>
|
||||
{subtitle ? (
|
||||
<Text variant="caption" numberOfLines={1} color={colors.textSecondary}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
{selected ? <Ionicons name="checkmark" size={18} color={colors.accent} /> : null}
|
||||
</Pressable>
|
||||
{trailing}
|
||||
@@ -168,7 +197,12 @@ const useStyles = createThemedStyles((colors) => ({
|
||||
paddingVertical: spacing.md,
|
||||
},
|
||||
itemLabel: {
|
||||
flexShrink: 1,
|
||||
},
|
||||
itemMeta: {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
gap: 2,
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user