update
This commit is contained in:
@@ -6,8 +6,10 @@ import { useRouter } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { usePresetStore } from '@store/presetStore';
|
||||
import { useAuthStore } from '@store/authStore';
|
||||
import { isUserOwnedPreset } from '@domain/SpoolPreset';
|
||||
import { colors, typography, spacing, radius } from '@shared/theme';
|
||||
import { deletePresetUseCase } from '@infrastructure/container';
|
||||
|
||||
/**
|
||||
* Tela de Presets de Carretéis — M4-0
|
||||
@@ -22,7 +24,23 @@ export default function ConfigScreen(): React.ReactElement {
|
||||
`Deseja excluir o preset "${name}"?`,
|
||||
[
|
||||
{ text: 'Cancelar', style: 'cancel' },
|
||||
{ text: 'Excluir', style: 'destructive', onPress: () => removePreset(id) },
|
||||
{
|
||||
text: 'Excluir',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
const user = useAuthStore.getState().user;
|
||||
if (!user) {
|
||||
Alert.alert('Erro', 'Usuário não autenticado');
|
||||
return;
|
||||
}
|
||||
await deletePresetUseCase.execute(id, user.id);
|
||||
removePreset(id);
|
||||
} catch (error) {
|
||||
Alert.alert('Erro', `Falha ao deletar: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,9 +9,12 @@ import { z } from 'zod';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { usePresetStore } from '@store/presetStore';
|
||||
import { useAuthStore } from '@store/authStore';
|
||||
import { Input } from '@presentation/components/ui/Input';
|
||||
import { Button } from '@presentation/components/ui/Button';
|
||||
import { colors, typography, spacing, radius } from '@shared/theme';
|
||||
import type { UpdateSpoolPresetInput } from '@domain/SpoolPreset';
|
||||
import { updatePresetUseCase, deletePresetUseCase } from '@infrastructure/container';
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().min(1, 'Nome obrigatório'),
|
||||
@@ -64,9 +67,20 @@ export default function EditPresetScreen(): React.ReactElement {
|
||||
{
|
||||
text: 'Excluir',
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
removePreset(preset!.id);
|
||||
router.replace('/(app)/(tabs)/config' as never);
|
||||
onPress: async () => {
|
||||
try {
|
||||
const user = useAuthStore.getState().user;
|
||||
if (!user) {
|
||||
Alert.alert('Erro', 'Usuário não autenticado');
|
||||
return;
|
||||
}
|
||||
|
||||
await deletePresetUseCase.execute(preset!.id, user.id);
|
||||
removePreset(preset!.id);
|
||||
router.replace('/(app)/(tabs)/config' as never);
|
||||
} catch (error) {
|
||||
Alert.alert('Erro', `Falha ao deletar: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -76,15 +90,22 @@ export default function EditPresetScreen(): React.ReactElement {
|
||||
async function onSubmit(data: FormData): Promise<void> {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// TODO: UpdatePresetUseCase via DI container
|
||||
updatePreset({
|
||||
...preset!,
|
||||
const user = useAuthStore.getState().user;
|
||||
if (!user) {
|
||||
Alert.alert('Erro', 'Usuário não autenticado');
|
||||
return;
|
||||
}
|
||||
|
||||
const input: UpdateSpoolPresetInput = {
|
||||
name: data.name,
|
||||
spoolWeightG: Number(data.spoolWeightG),
|
||||
});
|
||||
};
|
||||
|
||||
const updated = await updatePresetUseCase.execute(preset!.id, user.id, input);
|
||||
updatePreset(updated);
|
||||
router.back();
|
||||
} catch {
|
||||
Alert.alert('Erro', 'Não foi possível salvar as alterações.');
|
||||
} catch (error) {
|
||||
Alert.alert('Erro', `Não foi possível salvar as alterações: ${(error as Error).message}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@@ -9,10 +9,12 @@ import { z } from 'zod';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { usePresetStore } from '@store/presetStore';
|
||||
import { useAuthStore } from '@store/authStore';
|
||||
import { Input } from '@presentation/components/ui/Input';
|
||||
import { Button } from '@presentation/components/ui/Button';
|
||||
import { colors, typography, spacing, radius } from '@shared/theme';
|
||||
import type { SpoolPreset } from '@domain/SpoolPreset';
|
||||
import type { SpoolPreset, CreateSpoolPresetInput } from '@domain/SpoolPreset';
|
||||
import { createPresetUseCase } from '@infrastructure/container';
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().min(1, 'Nome obrigatório'),
|
||||
@@ -40,19 +42,22 @@ export default function NewPresetScreen(): React.ReactElement {
|
||||
async function onSubmit(data: FormData): Promise<void> {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// TODO: CreatePresetUseCase via DI container
|
||||
const preset: SpoolPreset = {
|
||||
id: `user-${Date.now()}`,
|
||||
const user = useAuthStore.getState().user;
|
||||
if (!user) {
|
||||
throw new Error('Usuário não autenticado');
|
||||
}
|
||||
|
||||
const input: CreateSpoolPresetInput = {
|
||||
name: data.name,
|
||||
spoolWeightG: Number(data.spoolWeightG),
|
||||
isSystem: false,
|
||||
userId: 'me',
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const preset = await createPresetUseCase.execute(user.id, input);
|
||||
addPreset(preset);
|
||||
Alert.alert('Sucesso', 'Preset criado com sucesso');
|
||||
router.back();
|
||||
} catch {
|
||||
Alert.alert('Erro', 'Não foi possível salvar o preset.');
|
||||
} catch (error) {
|
||||
Alert.alert('Erro', `Não foi possível salvar o preset: ${(error as Error).message}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@@ -5,12 +5,15 @@ import {
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import * as FileSystem from 'expo-file-system/legacy';
|
||||
import * as Sharing from 'expo-sharing';
|
||||
import { useFilamentStore } from '@store/filamentStore';
|
||||
import { usePresetStore } from '@store/presetStore';
|
||||
import { calcFilamentPercentage } from '@domain/Filament';
|
||||
import { colors, typography, spacing, radius } from '@shared/theme';
|
||||
import { formatWeight } from '@shared/utils/filament';
|
||||
import { Button } from '@presentation/components/ui/Button';
|
||||
import { httpClient } from '@adapters/remote/httpClient';
|
||||
|
||||
type LabelSize = '50x30' | '62x29' | '38x25';
|
||||
|
||||
@@ -44,6 +47,7 @@ export default function LabelScreen(): React.ReactElement {
|
||||
const preset = filament ? presets.find((p) => p.id === filament.spoolPresetId) : undefined;
|
||||
|
||||
const [selectedSize, setSelectedSize] = useState<LabelSize>('50x30');
|
||||
const [selectedFormat, setSelectedFormat] = useState<'pdf' | 'svg'>('pdf');
|
||||
const [enabledContent, setEnabledContent] = useState<Set<ContentOption>>(
|
||||
new Set(['color', 'name', 'material_brand', 'net_weight', 'print_temp', 'qrcode']),
|
||||
);
|
||||
@@ -68,9 +72,47 @@ export default function LabelScreen(): React.ReactElement {
|
||||
});
|
||||
}
|
||||
|
||||
function handleExport(): void {
|
||||
// TODO: Generate SVG and share via expo-sharing
|
||||
Alert.alert('Exportar SVG', 'Funcionalidade de exportação SVG será implementada na integração com o backend.');
|
||||
async function handleExport(): Promise<void> {
|
||||
const [widthStr, heightStr] = selectedSize.split('x');
|
||||
const width_mm = Number(widthStr);
|
||||
const height_mm = Number(heightStr);
|
||||
const isPdf = selectedFormat === 'pdf';
|
||||
const fields = Array.from(enabledContent).join(',');
|
||||
|
||||
try {
|
||||
if (isPdf) {
|
||||
const response = await httpClient.get<ArrayBuffer>(
|
||||
`/filaments/${id}/label.pdf`,
|
||||
{ params: { width_mm, height_mm, fields }, responseType: 'arraybuffer' },
|
||||
);
|
||||
const base64 = btoa(
|
||||
String.fromCharCode(...new Uint8Array(response.data)),
|
||||
);
|
||||
const fileUri = `${FileSystem.cacheDirectory}meowspool-label-${id}.pdf`;
|
||||
await FileSystem.writeAsStringAsync(fileUri, base64, {
|
||||
encoding: FileSystem.EncodingType.Base64,
|
||||
});
|
||||
if (await Sharing.isAvailableAsync()) {
|
||||
await Sharing.shareAsync(fileUri, { mimeType: 'application/pdf', UTI: 'com.adobe.pdf' });
|
||||
} else {
|
||||
Alert.alert('Compartilhamento indisponível', 'Este dispositivo não suporta compartilhamento de arquivos.');
|
||||
}
|
||||
} else {
|
||||
const response = await httpClient.get<string>(
|
||||
`/filaments/${id}/label.svg`,
|
||||
{ params: { width_mm, height_mm, fields }, responseType: 'text' },
|
||||
);
|
||||
const fileUri = `${FileSystem.cacheDirectory}meowspool-label-${id}.svg`;
|
||||
await FileSystem.writeAsStringAsync(fileUri, response.data, { encoding: 'utf8' });
|
||||
if (await Sharing.isAvailableAsync()) {
|
||||
await Sharing.shareAsync(fileUri, { mimeType: 'image/svg+xml', UTI: 'public.svg-image' });
|
||||
} else {
|
||||
Alert.alert('Compartilhamento indisponível', 'Este dispositivo não suporta compartilhamento de arquivos.');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
Alert.alert('Erro', String(err));
|
||||
}
|
||||
}
|
||||
|
||||
const sizeLabel = LABEL_SIZES.find((s) => s.id === selectedSize);
|
||||
@@ -174,8 +216,21 @@ export default function LabelScreen(): React.ReactElement {
|
||||
|
||||
{/* CTA */}
|
||||
<View style={styles.footer}>
|
||||
<View style={styles.formatRow}>
|
||||
{(['pdf', 'svg'] as const).map((fmt) => (
|
||||
<TouchableOpacity
|
||||
key={fmt}
|
||||
onPress={() => setSelectedFormat(fmt)}
|
||||
style={[styles.formatChip, selectedFormat === fmt && styles.formatChipActive]}
|
||||
>
|
||||
<Text style={[styles.formatChipText, selectedFormat === fmt && styles.formatChipTextActive]}>
|
||||
{fmt.toUpperCase()}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
<Button
|
||||
label="Exportar SVG"
|
||||
label={`Exportar ${selectedFormat.toUpperCase()}`}
|
||||
leftIcon={<Ionicons name="download-outline" size={18} color={colors.bgBase} />}
|
||||
onPress={handleExport}
|
||||
/>
|
||||
@@ -300,8 +355,27 @@ const styles = StyleSheet.create({
|
||||
contentChipTextActive: { color: colors.accent },
|
||||
footer: {
|
||||
padding: spacing[5],
|
||||
gap: spacing[3],
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
backgroundColor: colors.bgBase,
|
||||
},
|
||||
formatRow: { flexDirection: 'row', gap: spacing[2] },
|
||||
formatChip: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing[2],
|
||||
borderRadius: radius.lg,
|
||||
backgroundColor: colors.bgSurface,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
formatChipActive: { borderColor: colors.accent, backgroundColor: colors.accentMuted },
|
||||
formatChipText: {
|
||||
fontFamily: typography.fontFamily.ui,
|
||||
fontSize: typography.fontSize.sm,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.textSecondary,
|
||||
},
|
||||
formatChipTextActive: { color: colors.accent },
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View, Text, ScrollView, TouchableOpacity, TextInput, StyleSheet, Alert,
|
||||
View, Text, ScrollView, TouchableOpacity, TextInput, StyleSheet, Alert, Modal, KeyboardAvoidingView, Platform,
|
||||
} from 'react-native';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
@@ -50,7 +50,8 @@ export default function EditFilamentScreen(): React.ReactElement {
|
||||
const [selectedMaterial, setSelectedMaterial] = useState<Material>((filament?.material as Material) ?? 'PLA');
|
||||
const [selectedPresetId, setSelectedPresetId] = useState<string | null>(filament?.spoolPresetId ?? systemPresets[0]?.id ?? null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isOpeningColorPicker, setIsOpeningColorPicker] = useState(false);
|
||||
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
|
||||
const [colorInputValue, setColorInputValue] = useState('');
|
||||
|
||||
const { control, handleSubmit, watch, formState: { errors } } = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
@@ -86,31 +87,29 @@ export default function EditFilamentScreen(): React.ReactElement {
|
||||
}
|
||||
|
||||
const handleOpenColorPicker = (): void => {
|
||||
if (isOpeningColorPicker) return;
|
||||
setIsOpeningColorPicker(true);
|
||||
Alert.prompt(
|
||||
'Cor Customizada',
|
||||
'Digite um código de cor hexadecimal (ex: #FF5733)',
|
||||
[
|
||||
{ text: 'Cancelar', style: 'cancel', onPress: () => setIsOpeningColorPicker(false) },
|
||||
{
|
||||
text: 'Confirmar',
|
||||
onPress: (value) => {
|
||||
setIsOpeningColorPicker(false);
|
||||
if (!value) return;
|
||||
const normalized = normalizeHex(value.trim());
|
||||
if (isValidHex(normalized)) {
|
||||
setSelectedColor(normalized);
|
||||
setHexInput(normalized);
|
||||
} else {
|
||||
Alert.alert('Erro', 'Código hexadecimal inválido. Use o formato #RRGGBB ou #RGB.');
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
'plain-text',
|
||||
selectedColor,
|
||||
);
|
||||
setColorInputValue(selectedColor);
|
||||
setIsColorPickerOpen(true);
|
||||
};
|
||||
|
||||
const handleConfirmColor = (): void => {
|
||||
if (!colorInputValue) {
|
||||
Alert.alert('Erro', 'Digite um código hexadecimal');
|
||||
return;
|
||||
}
|
||||
const normalized = normalizeHex(colorInputValue.trim());
|
||||
if (isValidHex(normalized)) {
|
||||
setSelectedColor(normalized);
|
||||
setHexInput(normalized);
|
||||
setIsColorPickerOpen(false);
|
||||
setColorInputValue('');
|
||||
} else {
|
||||
Alert.alert('Erro', 'Código hexadecimal inválido. Use o formato #RRGGBB ou #RGB.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelColor = (): void => {
|
||||
setIsColorPickerOpen(false);
|
||||
setColorInputValue('');
|
||||
};
|
||||
|
||||
const handleDelete = async (): Promise<void> => {
|
||||
@@ -183,7 +182,34 @@ export default function EditFilamentScreen(): React.ReactElement {
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.safe}>
|
||||
<>
|
||||
<Modal visible={isColorPickerOpen} transparent animationType="fade">
|
||||
<View style={styles.modalOverlay}>
|
||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.modalContent}>
|
||||
<View style={styles.modalCard}>
|
||||
<Text style={styles.modalTitle}>Código Hexadecimal</Text>
|
||||
<TextInput
|
||||
style={styles.modalInput}
|
||||
placeholder="#FF5733"
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
value={colorInputValue}
|
||||
onChangeText={setColorInputValue}
|
||||
autoFocus
|
||||
/>
|
||||
<View style={styles.modalButtons}>
|
||||
<TouchableOpacity onPress={handleCancelColor} style={styles.modalBtnCancel}>
|
||||
<Text style={styles.modalBtnTextCancel}>Cancelar</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={handleConfirmColor} style={styles.modalBtnConfirm}>
|
||||
<Text style={styles.modalBtnTextConfirm}>Confirmar</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
<SafeAreaView style={styles.safe}>
|
||||
<ScrollView showsVerticalScrollIndicator={false} keyboardShouldPersistTaps="handled">
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
@@ -203,7 +229,7 @@ export default function EditFilamentScreen(): React.ReactElement {
|
||||
<View style={styles.colorPreviewRow}>
|
||||
<View style={[styles.colorSquare, { backgroundColor: selectedColor }]} />
|
||||
<Text style={styles.hexValue}>{hexInput}</Text>
|
||||
<TouchableOpacity><Ionicons name="pencil-outline" size={18} color={colors.textSecondary} /></TouchableOpacity>
|
||||
<TouchableOpacity onPress={handleOpenColorPicker}><Ionicons name="pencil-outline" size={18} color={colors.textSecondary} /></TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.colorPalette}>
|
||||
{PRESET_COLORS.map((c) => (
|
||||
@@ -371,6 +397,7 @@ export default function EditFilamentScreen(): React.ReactElement {
|
||||
/>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -421,4 +448,15 @@ const styles = StyleSheet.create({
|
||||
resultDot: { width: 8, height: 8, borderRadius: radius.full },
|
||||
resultPct: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.sm, fontWeight: typography.fontWeight.bold, color: colors.accent },
|
||||
footer: { padding: spacing[5], borderTopWidth: 1, borderTopColor: colors.border, backgroundColor: colors.bgBase },
|
||||
// Modal styles
|
||||
modalOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'center', alignItems: 'center' },
|
||||
modalContent: { width: '85%', maxWidth: 320 },
|
||||
modalCard: { backgroundColor: colors.bgSurface, borderRadius: radius.lg, padding: spacing[5], gap: spacing[4] },
|
||||
modalTitle: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.base, fontWeight: typography.fontWeight.semibold, color: colors.textPrimary },
|
||||
modalInput: { backgroundColor: colors.bgBase, borderWidth: 1, borderColor: colors.border, borderRadius: radius.md, paddingHorizontal: spacing[3], paddingVertical: spacing[2], fontFamily: typography.fontFamily.mono, fontSize: typography.fontSize.base, color: colors.textPrimary },
|
||||
modalButtons: { flexDirection: 'row', gap: spacing[3] },
|
||||
modalBtnCancel: { flex: 1, backgroundColor: colors.bgHover, paddingVertical: spacing[3], borderRadius: radius.md, alignItems: 'center' },
|
||||
modalBtnTextCancel: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.sm, fontWeight: typography.fontWeight.semibold, color: colors.textPrimary },
|
||||
modalBtnConfirm: { flex: 1, backgroundColor: colors.accent, paddingVertical: spacing[3], borderRadius: radius.md, alignItems: 'center' },
|
||||
modalBtnTextConfirm: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.sm, fontWeight: typography.fontWeight.semibold, color: colors.bgBase },
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View, Text, ScrollView, TouchableOpacity, TextInput, StyleSheet, Alert,
|
||||
View, Text, ScrollView, TouchableOpacity, TextInput, StyleSheet, Alert, Modal, KeyboardAvoidingView, Platform,
|
||||
} from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
@@ -49,7 +49,8 @@ export default function NewFilamentScreen(): React.ReactElement {
|
||||
const [selectedMaterial, setSelectedMaterial] = useState<Material>('PLA');
|
||||
const [selectedPresetId, setSelectedPresetId] = useState<string | null>(systemPresets[0]?.id ?? null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isOpeningColorPicker, setIsOpeningColorPicker] = useState(false);
|
||||
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
|
||||
const [colorInputValue, setColorInputValue] = useState('');
|
||||
|
||||
// Quando os presets carregam (via layout), seleciona o primeiro sistema automaticamente
|
||||
useEffect(() => {
|
||||
@@ -69,33 +70,32 @@ export default function NewFilamentScreen(): React.ReactElement {
|
||||
? calcNetWeight(Number(totalWeight), selectedPreset.spoolWeightG)
|
||||
: 0;
|
||||
const pct = Math.min(100, Math.round((netWeight / 1000) * 100));
|
||||
function handleOpenColorPicker(): void {
|
||||
if (isOpeningColorPicker) return;
|
||||
setIsOpeningColorPicker(true);
|
||||
Alert.prompt(
|
||||
'Cor Customizada',
|
||||
'Digite um código de cor hexadecimal (ex: #FF5733)',
|
||||
[
|
||||
{ text: 'Cancelar', style: 'cancel', onPress: () => setIsOpeningColorPicker(false) },
|
||||
{
|
||||
text: 'Confirmar',
|
||||
onPress: (value) => {
|
||||
setIsOpeningColorPicker(false);
|
||||
if (!value) return;
|
||||
const normalized = normalizeHex(value.trim());
|
||||
if (isValidHex(normalized)) {
|
||||
setSelectedColor(normalized);
|
||||
setHexInput(normalized);
|
||||
} else {
|
||||
Alert.alert('Erro', 'Código hexadecimal inválido. Use o formato #RRGGBB ou #RGB.');
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
'plain-text',
|
||||
selectedColor,
|
||||
);
|
||||
}
|
||||
|
||||
const handleOpenColorPicker = (): void => {
|
||||
setColorInputValue(selectedColor);
|
||||
setIsColorPickerOpen(true);
|
||||
};
|
||||
|
||||
const handleConfirmColor = (): void => {
|
||||
if (!colorInputValue) {
|
||||
Alert.alert('Erro', 'Digite um código hexadecimal');
|
||||
return;
|
||||
}
|
||||
const normalized = normalizeHex(colorInputValue.trim());
|
||||
if (isValidHex(normalized)) {
|
||||
setSelectedColor(normalized);
|
||||
setHexInput(normalized);
|
||||
setIsColorPickerOpen(false);
|
||||
setColorInputValue('');
|
||||
} else {
|
||||
Alert.alert('Erro', 'Código hexadecimal inválido. Use o formato #RRGGBB ou #RGB.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelColor = (): void => {
|
||||
setIsColorPickerOpen(false);
|
||||
setColorInputValue('');
|
||||
};
|
||||
|
||||
|
||||
async function onSubmit(data: FormData): Promise<void> {
|
||||
@@ -132,7 +132,34 @@ function handleOpenColorPicker(): void {
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.safe}> onPress={handleOpenColorPicker}
|
||||
<>
|
||||
<Modal visible={isColorPickerOpen} transparent animationType="fade">
|
||||
<View style={styles.modalOverlay}>
|
||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.modalContent}>
|
||||
<View style={styles.modalCard}>
|
||||
<Text style={styles.modalTitle}>Código Hexadecimal</Text>
|
||||
<TextInput
|
||||
style={styles.modalInput}
|
||||
placeholder="#FF5733"
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
value={colorInputValue}
|
||||
onChangeText={setColorInputValue}
|
||||
autoFocus
|
||||
/>
|
||||
<View style={styles.modalButtons}>
|
||||
<TouchableOpacity onPress={handleCancelColor} style={styles.modalBtnCancel}>
|
||||
<Text style={styles.modalBtnTextCancel}>Cancelar</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={handleConfirmColor} style={styles.modalBtnConfirm}>
|
||||
<Text style={styles.modalBtnTextConfirm}>Confirmar</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
<SafeAreaView style={styles.safe}>
|
||||
<ScrollView showsVerticalScrollIndicator={false} keyboardShouldPersistTaps="handled">
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
@@ -150,7 +177,7 @@ function handleOpenColorPicker(): void {
|
||||
<View style={styles.colorPreviewRow}>
|
||||
<View style={[styles.colorSquare, { backgroundColor: selectedColor }]} />
|
||||
<Text style={styles.hexValue}>{hexInput}</Text>
|
||||
<TouchableOpacity><Ionicons name="pencil-outline" size={18} color={colors.textSecondary} /></TouchableOpacity>
|
||||
<TouchableOpacity onPress={handleOpenColorPicker}><Ionicons name="pencil-outline" size={18} color={colors.textSecondary} /></TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.colorPalette}>
|
||||
{PRESET_COLORS.map((c) => (
|
||||
@@ -318,6 +345,7 @@ function handleOpenColorPicker(): void {
|
||||
/>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -365,4 +393,15 @@ const styles = StyleSheet.create({
|
||||
resultDot: { width: 8, height: 8, borderRadius: radius.full },
|
||||
resultPct: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.sm, fontWeight: typography.fontWeight.bold, color: colors.accent },
|
||||
footer: { padding: spacing[5], borderTopWidth: 1, borderTopColor: colors.border, backgroundColor: colors.bgBase },
|
||||
// Modal styles
|
||||
modalOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'center', alignItems: 'center' },
|
||||
modalContent: { width: '85%', maxWidth: 320 },
|
||||
modalCard: { backgroundColor: colors.bgSurface, borderRadius: radius.lg, padding: spacing[5], gap: spacing[4] },
|
||||
modalTitle: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.base, fontWeight: typography.fontWeight.semibold, color: colors.textPrimary },
|
||||
modalInput: { backgroundColor: colors.bgBase, borderWidth: 1, borderColor: colors.border, borderRadius: radius.md, paddingHorizontal: spacing[3], paddingVertical: spacing[2], fontFamily: typography.fontFamily.mono, fontSize: typography.fontSize.base, color: colors.textPrimary },
|
||||
modalButtons: { flexDirection: 'row', gap: spacing[3] },
|
||||
modalBtnCancel: { flex: 1, backgroundColor: colors.bgHover, paddingVertical: spacing[3], borderRadius: radius.md, alignItems: 'center' },
|
||||
modalBtnTextCancel: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.sm, fontWeight: typography.fontWeight.semibold, color: colors.textPrimary },
|
||||
modalBtnConfirm: { flex: 1, backgroundColor: colors.accent, paddingVertical: spacing[3], borderRadius: radius.md, alignItems: 'center' },
|
||||
modalBtnTextConfirm: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.sm, fontWeight: typography.fontWeight.semibold, color: colors.bgBase },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user