diff --git a/apps/web/src/components/settings/security/two-factor-setup/useTwoFactorSetup.ts b/apps/web/src/components/settings/security/two-factor-setup/useTwoFactorSetup.ts index afc93951a..e0f831615 100644 --- a/apps/web/src/components/settings/security/two-factor-setup/useTwoFactorSetup.ts +++ b/apps/web/src/components/settings/security/two-factor-setup/useTwoFactorSetup.ts @@ -54,7 +54,7 @@ export function useTwoFactorSetup(onBack: () => void, _onComplete: () => void) { const copyCodes = useCallback(() => { if (!setupData?.recovery_codes) return; navigator.clipboard.writeText(setupData.recovery_codes.join('\n')); - toast('Backup codes copied to clipboard'); + toast.success('Backup codes copied to clipboard'); }, [setupData]); const downloadCodes = useCallback(() => { @@ -67,7 +67,7 @@ export function useTwoFactorSetup(onBack: () => void, _onComplete: () => void) { element.download = 'veza-backup-codes.txt'; document.body.appendChild(element); element.click(); - toast('Backup codes downloaded'); + toast.success('Backup codes downloaded'); }, [setupData]); const goToStep2Totp = useCallback(() => { @@ -76,7 +76,7 @@ export function useTwoFactorSetup(onBack: () => void, _onComplete: () => void) { }, []); const handleSmsUnavailable = useCallback(() => { - toast('SMS method not yet available in this region', { icon: 'ℹ️' }); + toast.success('SMS method not yet available in this region'); }, []); return { diff --git a/apps/web/src/components/ui/radio-group.tsx b/apps/web/src/components/ui/radio-group.tsx index 0f4a319f7..26022652c 100644 --- a/apps/web/src/components/ui/radio-group.tsx +++ b/apps/web/src/components/ui/radio-group.tsx @@ -2,222 +2,132 @@ import * as React from 'react'; import { Circle } from 'lucide-react'; import { cn } from '@/lib/utils'; -/** - * RadioGroupProps - Propriétés du composant RadioGroup - * - * @interface RadioGroupProps - * @extends Omit, 'onChange'> - */ -export interface RadioGroupProps - extends Omit, 'onChange'> { - /** - * Valeur sélectionnée du groupe de boutons radio - * - * @example - * ```tsx - * - * - * - * - * ``` - */ +// Context to pass RadioGroup state to deeply nested RadioGroupItems +interface RadioGroupContextValue { value?: string; - - /** - * Fonction appelée lorsque la valeur sélectionnée change - * - * @param {string} value - Nouvelle valeur sélectionnée - * - * @example - * ```tsx - * console.log('Selected:', value)}> - * ... - * - * ``` - */ onValueChange?: (value: string) => void; - - /** - * Si `true`, désactive tous les boutons radio du groupe - * - * @default false - */ disabled?: boolean; + name: string; } -/** - * RadioGroup - Composant de groupe de boutons radio avec design system Kodo - * - * Composant pour gérer un groupe de boutons radio mutuellement exclusifs. - * Utilise le design system Kodo avec des styles cohérents et support pour l'accessibilité. - * - * @example - * ```tsx - * // Groupe de boutons radio simple - * - * - * - * - * - * ``` - * - * @example - * ```tsx - * // Avec labels - * - * - * - * - * ``` - * - * @component - * @param {RadioGroupProps} props - Propriétés du composant - * @returns {JSX.Element} Élément div avec role="radiogroup" contenant les boutons radio - */ +const RadioGroupContext = React.createContext(null); + +function useRadioGroupContext() { + return React.useContext(RadioGroupContext); +} + +export interface RadioGroupProps + extends Omit, 'onChange'> { + value?: string; + onValueChange?: (value: string) => void; + disabled?: boolean; + name?: string; +} const RadioGroup = React.forwardRef( - ({ className, value, onValueChange, disabled, children, ...props }, ref) => { - // Collect RadioGroupItem values for arrow key navigation - const itemValues: string[] = []; - React.Children.forEach(children, (child) => { - if (React.isValidElement(child) && child.type === RadioGroupItem && !child.props.disabled) { - itemValues.push(child.props.value); - } - }); + ({ className, value, onValueChange, disabled, name, children, ...props }, ref) => { + const generatedId = React.useId(); + const groupName = name || `radio-group-${generatedId}`; const handleKeyDown = (e: React.KeyboardEvent) => { - if (itemValues.length === 0) return; - const currentIndex = value ? itemValues.indexOf(value) : -1; + const radios = (e.currentTarget as HTMLElement).querySelectorAll( + 'input[type="radio"]:not(:disabled)', + ); + if (radios.length === 0) return; + + const values = Array.from(radios).map((r) => r.value); + const currentIndex = value ? values.indexOf(value) : -1; let nextIndex: number | undefined; switch (e.key) { case 'ArrowDown': case 'ArrowRight': - nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % itemValues.length; + nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % values.length; break; case 'ArrowUp': case 'ArrowLeft': - nextIndex = currentIndex === -1 ? itemValues.length - 1 : (currentIndex - 1 + itemValues.length) % itemValues.length; + nextIndex = + currentIndex === -1 + ? values.length - 1 + : (currentIndex - 1 + values.length) % values.length; break; default: return; } e.preventDefault(); - const nextValue = itemValues[nextIndex]; + const nextValue = values[nextIndex]; if (nextValue !== undefined) onValueChange?.(nextValue); - // Focus the newly selected radio - const radios = (e.currentTarget as HTMLElement).querySelectorAll('input[type="radio"]'); radios[nextIndex]?.focus(); }; + const contextValue = React.useMemo( + () => ({ value, onValueChange, disabled, name: groupName }), + [value, onValueChange, disabled, groupName], + ); + return ( -
- {React.Children.map(children, (child) => { - if (React.isValidElement(child) && child.type === RadioGroupItem) { - const isChecked = child.props.value === value; - const isFirstItem = child.props.value === itemValues[0]; - return React.cloneElement(child, { - checked: isChecked, - onCheckedChange: () => onValueChange?.(child.props.value), - disabled: disabled || child.props.disabled, - // Only the checked item (or first if none checked) gets tabIndex 0 - tabIndex: isChecked ? 0 : (value === undefined && isFirstItem ? 0 : -1), - } as any); - } - return child; - })} -
+ +
+ {children} +
+
); }, ); RadioGroup.displayName = 'RadioGroup'; -/** - * RadioGroupItemProps - Propriétés du composant RadioGroupItem - * - * @interface RadioGroupItemProps - * @extends Omit, 'type'> - */ export interface RadioGroupItemProps - extends Omit, 'type'> { - /** - * Valeur unique du bouton radio (doit être unique dans le groupe) - * - * @example - * ```tsx - * - * ``` - */ + extends Omit, 'type' | 'onChange'> { value: string; - - /** - * État checked du bouton radio (géré automatiquement par RadioGroup) - * @internal - */ checked?: boolean; - - /** - * Fonction appelée lors du clic (gérée automatiquement par RadioGroup) - * @internal - */ onCheckedChange?: () => void; - - /** - * Tab index for keyboard navigation (managed by RadioGroup) - * @internal - */ - tabIndex?: number; } -/** - * RadioGroupItem - Bouton radio individuel - * - * Bouton radio à utiliser à l'intérieur d'un RadioGroup. - * L'état checked est géré automatiquement par le RadioGroup parent. - * - * @component - */ - const RadioGroupItem = React.forwardRef( - ({ className, value, checked, onCheckedChange, disabled, tabIndex, ...props }, ref) => { + ({ className, value, checked: checkedProp, onCheckedChange, disabled: disabledProp, tabIndex, ...props }, ref) => { + const ctx = useRadioGroupContext(); + + // Use context values if available, otherwise fall back to props + const isChecked = ctx ? ctx.value === value : !!checkedProp; + const isDisabled = ctx ? ctx.disabled || disabledProp : disabledProp; + const handleChange = ctx + ? () => ctx.onValueChange?.(value) + : onCheckedChange; + const radioName = ctx?.name; + return ( diff --git a/apps/web/src/features/settings/components/account-settings/AccountSettingsDeleteCard.tsx b/apps/web/src/features/settings/components/account-settings/AccountSettingsDeleteCard.tsx index c3f9cf517..c457ecd7f 100644 --- a/apps/web/src/features/settings/components/account-settings/AccountSettingsDeleteCard.tsx +++ b/apps/web/src/features/settings/components/account-settings/AccountSettingsDeleteCard.tsx @@ -72,10 +72,21 @@ export function AccountSettingsDeleteCard({ onClose={() => setIsDeleteDialogOpen(false)} title="Are you absolutely sure?" variant="alert" - onConfirm={onDeleteAccount} - confirmLabel={isDeletingAccount ? 'Deleting...' : 'Delete Account'} - cancelLabel="Cancel" size="lg" + footer={ +
+ + +
+ } >

diff --git a/apps/web/src/features/settings/schemas/settingsSchema.ts b/apps/web/src/features/settings/schemas/settingsSchema.ts index 2838adda0..03ae247c6 100644 --- a/apps/web/src/features/settings/schemas/settingsSchema.ts +++ b/apps/web/src/features/settings/schemas/settingsSchema.ts @@ -58,6 +58,12 @@ export const settingsSchema = z.object({ }), }), }), + playback: z.object({ + quality: z.enum(['low', 'medium', 'high', 'lossless']), + volume: z.number().min(0).max(1), + crossfade: z.number().min(0).max(12), + autoplay: z.boolean(), + }).optional(), }); export type SettingsFormData = z.infer;