diff --git a/.claude/skills/react/SKILL.md b/.claude/skills/react/SKILL.md index 507f65a01..5fe3022f0 100644 --- a/.claude/skills/react/SKILL.md +++ b/.claude/skills/react/SKILL.md @@ -43,6 +43,11 @@ a11y, Storybook), read [`ui-components`](../ui-components/SKILL.md) instead. native `.withOptimisticUpdate` (auto-rolls-back on settle/error); a local `useState` copy double-sources the truth and drifts. Set `errorToast` (or `false` when the caller shows its own toast). +- **Toasts carry a concise `title` + `description`.** A toast is a short headline plus a precise + follow-up sentence, not one crammed line — `toast({ title, description, variant })`. Shared copy + lives under the `toast` namespace as `{ title, description }` pairs (e.g. + `tToast('error.saveFailed.title')` / `tToast('error.saveFailed.description')`); feature-local toasts + follow the same shape. Omit `description` only when the title already says everything. - **Loading = `` masking the real tree, never a parallel mock.** Render the component _once_ inside [``](../../../packages/ui/src/components/feedback/skeleton-context.tsx) (from diff --git a/services/platform/app/components/ui/entity/entity-delete-dialog.tsx b/services/platform/app/components/ui/entity/entity-delete-dialog.tsx index d96bbfc23..23a7bcbb3 100644 --- a/services/platform/app/components/ui/entity/entity-delete-dialog.tsx +++ b/services/platform/app/components/ui/entity/entity-delete-dialog.tsx @@ -18,6 +18,8 @@ interface EntityDeleteTranslations { warningText?: string; /** Success toast message */ successMessage: string; + /** Success toast description (optional) */ + successDescription?: string; /** Error toast message */ errorMessage: string; } @@ -80,6 +82,7 @@ export function EntityDeleteDialog({ await deleteMutation(entity); toast({ title: translations.successMessage, + description: translations.successDescription, }); onClose(); onSuccess?.(); diff --git a/services/platform/app/components/ui/entity/use-delete-dialog.ts b/services/platform/app/components/ui/entity/use-delete-dialog.ts index 2edf8da8a..850cc4d9b 100644 --- a/services/platform/app/components/ui/entity/use-delete-dialog.ts +++ b/services/platform/app/components/ui/entity/use-delete-dialog.ts @@ -75,6 +75,7 @@ interface DeleteDialogTranslations { description: string; warningText?: string; successMessage: string; + successDescription?: string; errorMessage: string; } @@ -126,7 +127,8 @@ export function useDeleteDialogTranslations( title: tEntity(keys.title), description: tEntity(keys.description, { name: '{name}' }), warningText: keys.warningText ? tEntity(keys.warningText) : undefined, - successMessage: tToast('success.deleted'), + successMessage: tToast('success.deleted.title'), + successDescription: tToast('success.deleted.description'), errorMessage: tEntity(keys.errorMessage), }), [tEntity, tToast, keys], diff --git a/services/platform/app/features/automations/components/automation-row-actions.tsx b/services/platform/app/features/automations/components/automation-row-actions.tsx index 26fd8ecbd..a603a4f9f 100644 --- a/services/platform/app/features/automations/components/automation-row-actions.tsx +++ b/services/platform/app/features/automations/components/automation-row-actions.tsx @@ -55,14 +55,16 @@ export function AutomationRowActions({ duplicateAutomation(workflowArgs, { onSuccess: () => { toast({ - title: tToast('success.automationDuplicated'), + title: tToast('success.automationDuplicated.title'), + description: tToast('success.automationDuplicated.description'), variant: 'success', }); }, onError: (error: Error) => { console.error('Failed to duplicate automation:', error); toast({ - title: tToast('error.automationDuplicateFailed'), + title: tToast('error.automationDuplicateFailed.title'), + description: tToast('error.automationDuplicateFailed.description'), variant: 'destructive', }); }, @@ -78,7 +80,8 @@ export function AutomationRowActions({ newSlug: name, }); toast({ - title: tToast('success.automationRenamed'), + title: tToast('success.automationRenamed.title'), + description: tToast('success.automationRenamed.description'), variant: 'success', }); } catch (error: unknown) { @@ -101,7 +104,8 @@ export function AutomationRowActions({ onError: (error: Error) => { console.error('Failed to delete automation:', error); toast({ - title: tToast('error.automationDeleteFailed'), + title: tToast('error.automationDeleteFailed.title'), + description: tToast('error.automationDeleteFailed.description'), variant: 'destructive', }); }, diff --git a/services/platform/app/features/settings/account/components/account-form.tsx b/services/platform/app/features/settings/account/components/account-form.tsx index 628b297b5..6e8182986 100644 --- a/services/platform/app/features/settings/account/components/account-form.tsx +++ b/services/platform/app/features/settings/account/components/account-form.tsx @@ -116,12 +116,14 @@ function ProfileSection() { try { await updateUserName({ name }); toast({ - title: tToast('success.profileUpdated'), + title: tToast('success.profileUpdated.title'), + description: tToast('success.profileUpdated.description'), variant: 'success', }); } catch (err) { toast({ - title: tToast('error.profileUpdateFailed'), + title: tToast('error.profileUpdateFailed.title'), + description: tToast('error.profileUpdateFailed.description'), variant: 'destructive', }); throw err; @@ -318,7 +320,8 @@ function ChangePasswordDialog({ open, onOpenChange }: PasswordDialogProps) { }); } catch { toast({ - title: tToast('error.passwordChangeFailed'), + title: tToast('error.passwordChangeFailed.title'), + description: tToast('error.passwordChangeFailed.description'), variant: 'destructive', }); return; @@ -462,7 +465,8 @@ function SetPasswordDialog({ open, onOpenChange }: PasswordDialogProps) { }); toast({ - title: tToast('success.passwordSet'), + title: tToast('success.passwordSet.title'), + description: tToast('success.passwordSet.description'), variant: 'success', }); @@ -470,7 +474,8 @@ function SetPasswordDialog({ open, onOpenChange }: PasswordDialogProps) { onOpenChange(false); } catch { toast({ - title: tToast('error.passwordChangeFailed'), + title: tToast('error.passwordChangeFailed.title'), + description: tToast('error.passwordChangeFailed.description'), variant: 'destructive', }); } diff --git a/services/platform/app/features/settings/branding/components/branding-form.tsx b/services/platform/app/features/settings/branding/components/branding-form.tsx index 290ecc955..16d805c70 100644 --- a/services/platform/app/features/settings/branding/components/branding-form.tsx +++ b/services/platform/app/features/settings/branding/components/branding-form.tsx @@ -121,12 +121,14 @@ export function BrandingForm({ onSaved?.(); void refetchBranding(); toast({ - title: tToast('success.brandingUpdated'), + title: tToast('success.brandingUpdated.title'), + description: tToast('success.brandingUpdated.description'), variant: 'success', }); } catch (err) { toast({ - title: tToast('error.brandingUpdateFailed'), + title: tToast('error.brandingUpdateFailed.title'), + description: tToast('error.brandingUpdateFailed.description'), variant: 'destructive', }); throw err; @@ -207,7 +209,8 @@ export function BrandingForm({ setValue('faviconLightFilename', filename, { shouldDirty: true }); setFaviconPreviewUrl(`data:image/png;base64,${base64}`); toast({ - title: tToast('success.faviconGenerated'), + title: tToast('success.faviconGenerated.title'), + description: tToast('success.faviconGenerated.description'), variant: 'success', }); } catch (err) { diff --git a/services/platform/app/features/settings/organization/components/member-add-dialog.tsx b/services/platform/app/features/settings/organization/components/member-add-dialog.tsx index 1811c1372..3d568645c 100644 --- a/services/platform/app/features/settings/organization/components/member-add-dialog.tsx +++ b/services/platform/app/features/settings/organization/components/member-add-dialog.tsx @@ -124,8 +124,11 @@ export function AddMemberDialog({ toast({ title: result.isExistingUser - ? tToast('success.existingUserAdded') - : tToast('success.newMemberCreated'), + ? tToast('success.existingUserAdded.title') + : tToast('success.newMemberCreated.title'), + description: result.isExistingUser + ? tToast('success.existingUserAdded.description') + : tToast('success.newMemberCreated.description'), variant: 'success', }); @@ -154,7 +157,8 @@ export function AddMemberDialog({ return; } toast({ - title: tToast('error.addMemberFailed'), + title: tToast('error.addMemberFailed.title'), + description: tToast('error.addMemberFailed.description'), variant: 'destructive', }); } diff --git a/services/platform/app/features/settings/organization/components/organization-settings.tsx b/services/platform/app/features/settings/organization/components/organization-settings.tsx index 3df1166bf..dca051340 100644 --- a/services/platform/app/features/settings/organization/components/organization-settings.tsx +++ b/services/platform/app/features/settings/organization/components/organization-settings.tsx @@ -342,13 +342,15 @@ export function OrganizationSettings({ }); await queryClient.invalidateQueries({ queryKey: ['auth', 'session'] }); toast({ - title: tToast('success.organizationUpdated'), + title: tToast('success.organizationUpdated.title'), + description: tToast('success.organizationUpdated.description'), variant: 'success', }); } catch (error) { console.error(error); toast({ - title: tToast('error.organizationUpdateFailed'), + title: tToast('error.organizationUpdateFailed.title'), + description: tToast('error.organizationUpdateFailed.description'), variant: 'destructive', }); throw error; diff --git a/services/platform/app/hooks/use-convex-mutation.ts b/services/platform/app/hooks/use-convex-mutation.ts index 07338de38..96303ab4d 100644 --- a/services/platform/app/hooks/use-convex-mutation.ts +++ b/services/platform/app/hooks/use-convex-mutation.ts @@ -57,8 +57,9 @@ export function useConvexMutation>( console.error(`Mutation failed: ${getFunctionName(func)}`, error); if (errorToast !== false) { toast({ - title: errorToast?.title ?? t('error.generic'), - description: errorToast?.description?.(error), + title: errorToast?.title ?? t('error.generic.title'), + description: + errorToast?.description?.(error) ?? t('error.generic.description'), variant: 'destructive', }); } diff --git a/services/platform/app/routes/dashboard/$id/automations/$amId/configuration.tsx b/services/platform/app/routes/dashboard/$id/automations/$amId/configuration.tsx index 2eaf2bc67..fb6e5ebb0 100644 --- a/services/platform/app/routes/dashboard/$id/automations/$amId/configuration.tsx +++ b/services/platform/app/routes/dashboard/$id/automations/$amId/configuration.tsx @@ -135,10 +135,18 @@ function ConfigurationPage() { readResult && readResult.ok ? readResult.hash : undefined, }); await refetch(); - toast({ title: tToast('success.saved'), variant: 'success' }); + toast({ + title: tToast('success.saved.title'), + description: tToast('success.saved.description'), + variant: 'success', + }); } catch (error) { console.error('Failed to save configuration:', error); - toast({ title: tToast('error.saveFailed'), variant: 'destructive' }); + toast({ + title: tToast('error.saveFailed.title'), + description: tToast('error.saveFailed.description'), + variant: 'destructive', + }); throw error; } }, diff --git a/services/platform/app/routes/forced-change-password.$id.tsx b/services/platform/app/routes/forced-change-password.$id.tsx index 81cf25a31..a68ac94cd 100644 --- a/services/platform/app/routes/forced-change-password.$id.tsx +++ b/services/platform/app/routes/forced-change-password.$id.tsx @@ -129,7 +129,8 @@ function ForcedChangePasswordPage() { trigger: 'forced', }); toast({ - title: tToast('success.passwordChanged'), + title: tToast('success.passwordChanged.title'), + description: tToast('success.passwordChanged.description'), variant: 'success', }); void navigate({ @@ -140,7 +141,8 @@ function ForcedChangePasswordPage() { } catch (e) { console.error(e); toast({ - title: tToast('error.passwordChangeFailed'), + title: tToast('error.passwordChangeFailed.title'), + description: tToast('error.passwordChangeFailed.description'), variant: 'destructive', }); } diff --git a/services/platform/messages/de.json b/services/platform/messages/de.json index e59e126c8..2c77f6940 100644 --- a/services/platform/messages/de.json +++ b/services/platform/messages/de.json @@ -6864,29 +6864,92 @@ }, "toast": { "success": { - "saved": "Änderungen gespeichert", - "deleted": "Gelöscht", - "profileUpdated": "Profil aktualisiert", - "passwordChanged": "Passwort geändert", - "passwordSet": "Passwort festgelegt", - "organizationUpdated": "Organisation aktualisiert", - "newMemberCreated": "Neues Mitglied erstellt und zur Organisation hinzugefügt", - "existingUserAdded": "Bestehender Benutzer zur Organisation hinzugefügt", - "automationDuplicated": "Automatisierung dupliziert", - "automationRenamed": "Automatisierung umbenannt", - "brandingUpdated": "Branding aktualisiert", - "faviconGenerated": "Favicon aus deinem Logo erzeugt" + "saved": { + "title": "Änderungen gespeichert", + "description": "Deine Änderungen wurden gespeichert." + }, + "deleted": { + "title": "Gelöscht", + "description": "Das Element wurde gelöscht." + }, + "profileUpdated": { + "title": "Profil aktualisiert", + "description": "Deine Profiländerungen wurden gespeichert." + }, + "passwordChanged": { + "title": "Passwort geändert", + "description": "Verwende dein neues Passwort bei der nächsten Anmeldung." + }, + "passwordSet": { + "title": "Passwort festgelegt", + "description": "Du kannst dich jetzt mit deinem Passwort anmelden." + }, + "organizationUpdated": { + "title": "Organisation aktualisiert", + "description": "Deine Änderungen an der Organisation wurden gespeichert." + }, + "newMemberCreated": { + "title": "Mitglied hinzugefügt", + "description": "Das neue Mitglied wurde erstellt und zu deiner Organisation hinzugefügt." + }, + "existingUserAdded": { + "title": "Mitglied hinzugefügt", + "description": "Der Benutzer wurde zu deiner Organisation hinzugefügt." + }, + "automationDuplicated": { + "title": "Automatisierung dupliziert", + "description": "Eine Kopie der Automatisierung wurde erstellt." + }, + "automationRenamed": { + "title": "Automatisierung umbenannt", + "description": "Der neue Name wurde gespeichert." + }, + "brandingUpdated": { + "title": "Branding aktualisiert", + "description": "Deine Branding-Änderungen wurden gespeichert." + }, + "faviconGenerated": { + "title": "Favicon erzeugt", + "description": "Wir haben aus deinem Logo ein Favicon erstellt." + } }, "error": { - "saveFailed": "Änderungen konnten nicht gespeichert werden", - "profileUpdateFailed": "Profil konnte nicht aktualisiert werden", - "passwordChangeFailed": "Passwort konnte nicht geändert werden. Bitte überprüfe dein aktuelles Passwort und versuche es erneut.", - "organizationUpdateFailed": "Organisation konnte nicht aktualisiert werden", - "brandingUpdateFailed": "Branding konnte nicht aktualisiert werden", - "addMemberFailed": "Mitglied konnte nicht hinzugefügt werden", - "automationDuplicateFailed": "Automatisierung konnte nicht dupliziert werden", - "automationDeleteFailed": "Automatisierung konnte nicht gelöscht werden", - "generic": "Etwas ist schiefgelaufen. Bitte versuche es erneut." + "saveFailed": { + "title": "Änderungen konnten nicht gespeichert werden", + "description": "Etwas ist schiefgelaufen. Bitte versuche es erneut." + }, + "profileUpdateFailed": { + "title": "Profil konnte nicht aktualisiert werden", + "description": "Etwas ist schiefgelaufen. Bitte versuche es erneut." + }, + "passwordChangeFailed": { + "title": "Passwort konnte nicht geändert werden", + "description": "Bitte überprüfe dein aktuelles Passwort und versuche es erneut." + }, + "organizationUpdateFailed": { + "title": "Organisation konnte nicht aktualisiert werden", + "description": "Etwas ist schiefgelaufen. Bitte versuche es erneut." + }, + "brandingUpdateFailed": { + "title": "Branding konnte nicht aktualisiert werden", + "description": "Etwas ist schiefgelaufen. Bitte versuche es erneut." + }, + "addMemberFailed": { + "title": "Mitglied konnte nicht hinzugefügt werden", + "description": "Etwas ist schiefgelaufen. Bitte versuche es erneut." + }, + "automationDuplicateFailed": { + "title": "Automatisierung konnte nicht dupliziert werden", + "description": "Etwas ist schiefgelaufen. Bitte versuche es erneut." + }, + "automationDeleteFailed": { + "title": "Automatisierung konnte nicht gelöscht werden", + "description": "Etwas ist schiefgelaufen. Bitte versuche es erneut." + }, + "generic": { + "title": "Etwas ist schiefgelaufen", + "description": "Bitte versuche es erneut." + } } }, "todoList": { diff --git a/services/platform/messages/en.json b/services/platform/messages/en.json index fc09ec3de..0027e3b6c 100644 --- a/services/platform/messages/en.json +++ b/services/platform/messages/en.json @@ -7126,29 +7126,92 @@ }, "toast": { "success": { - "saved": "Changes saved successfully", - "deleted": "Successfully deleted", - "profileUpdated": "Profile updated successfully", - "passwordChanged": "Password changed successfully", - "passwordSet": "Password set successfully", - "organizationUpdated": "Organization updated successfully", - "newMemberCreated": "New member created and added to organization", - "existingUserAdded": "Existing user added to organization", - "automationDuplicated": "Automation duplicated successfully", - "automationRenamed": "Automation renamed successfully", - "brandingUpdated": "Branding updated successfully", - "faviconGenerated": "Favicon generated from your logo" + "saved": { + "title": "Changes saved", + "description": "Your changes have been saved." + }, + "deleted": { + "title": "Deleted", + "description": "The item has been deleted." + }, + "profileUpdated": { + "title": "Profile updated", + "description": "Your profile changes have been saved." + }, + "passwordChanged": { + "title": "Password changed", + "description": "Use your new password the next time you sign in." + }, + "passwordSet": { + "title": "Password set", + "description": "You can now sign in with your password." + }, + "organizationUpdated": { + "title": "Organization updated", + "description": "Your organization changes have been saved." + }, + "newMemberCreated": { + "title": "Member added", + "description": "The new member has been created and added to your organization." + }, + "existingUserAdded": { + "title": "Member added", + "description": "The user has been added to your organization." + }, + "automationDuplicated": { + "title": "Automation duplicated", + "description": "A copy of the automation has been created." + }, + "automationRenamed": { + "title": "Automation renamed", + "description": "The new name has been saved." + }, + "brandingUpdated": { + "title": "Branding updated", + "description": "Your branding changes have been saved." + }, + "faviconGenerated": { + "title": "Favicon generated", + "description": "We created a favicon from your logo." + } }, "error": { - "saveFailed": "Failed to save changes", - "profileUpdateFailed": "Failed to update profile", - "passwordChangeFailed": "Failed to change password. Please check your current password and try again.", - "organizationUpdateFailed": "Failed to update organization", - "brandingUpdateFailed": "Failed to update branding", - "addMemberFailed": "Failed to add member", - "automationDuplicateFailed": "Failed to duplicate automation", - "automationDeleteFailed": "Failed to delete automation", - "generic": "Something went wrong. Please try again." + "saveFailed": { + "title": "Couldn't save changes", + "description": "Something went wrong. Please try again." + }, + "profileUpdateFailed": { + "title": "Couldn't update profile", + "description": "Something went wrong. Please try again." + }, + "passwordChangeFailed": { + "title": "Couldn't change password", + "description": "Please check your current password and try again." + }, + "organizationUpdateFailed": { + "title": "Couldn't update organization", + "description": "Something went wrong. Please try again." + }, + "brandingUpdateFailed": { + "title": "Couldn't update branding", + "description": "Something went wrong. Please try again." + }, + "addMemberFailed": { + "title": "Couldn't add member", + "description": "Something went wrong. Please try again." + }, + "automationDuplicateFailed": { + "title": "Couldn't duplicate automation", + "description": "Something went wrong. Please try again." + }, + "automationDeleteFailed": { + "title": "Couldn't delete automation", + "description": "Something went wrong. Please try again." + }, + "generic": { + "title": "Something went wrong", + "description": "Please try again." + } } }, "todoList": { diff --git a/services/platform/messages/fr.json b/services/platform/messages/fr.json index e3bd63e2f..56c9272a3 100644 --- a/services/platform/messages/fr.json +++ b/services/platform/messages/fr.json @@ -6865,29 +6865,92 @@ }, "toast": { "success": { - "saved": "Modifications enregistrées avec succès", - "deleted": "Supprimé avec succès", - "profileUpdated": "Profil mis à jour avec succès", - "passwordChanged": "Mot de passe modifié avec succès", - "passwordSet": "Mot de passe défini avec succès", - "organizationUpdated": "Organisation mise à jour avec succès", - "newMemberCreated": "Nouveau membre créé et ajouté à l'organisation", - "existingUserAdded": "Utilisateur existant ajouté à l'organisation", - "automationDuplicated": "Automatisation dupliquée avec succès", - "automationRenamed": "Automatisation renommée avec succès", - "brandingUpdated": "Image de marque mise à jour avec succès", - "faviconGenerated": "Favicon généré à partir de ton logo" + "saved": { + "title": "Modifications enregistrées", + "description": "Tes modifications ont été enregistrées." + }, + "deleted": { + "title": "Supprimé", + "description": "L'élément a été supprimé." + }, + "profileUpdated": { + "title": "Profil mis à jour", + "description": "Les modifications de ton profil ont été enregistrées." + }, + "passwordChanged": { + "title": "Mot de passe modifié", + "description": "Utilise ton nouveau mot de passe lors de ta prochaine connexion." + }, + "passwordSet": { + "title": "Mot de passe défini", + "description": "Tu peux maintenant te connecter avec ton mot de passe." + }, + "organizationUpdated": { + "title": "Organisation mise à jour", + "description": "Les modifications de ton organisation ont été enregistrées." + }, + "newMemberCreated": { + "title": "Membre ajouté", + "description": "Le nouveau membre a été créé et ajouté à ton organisation." + }, + "existingUserAdded": { + "title": "Membre ajouté", + "description": "L'utilisateur a été ajouté à ton organisation." + }, + "automationDuplicated": { + "title": "Automatisation dupliquée", + "description": "Une copie de l'automatisation a été créée." + }, + "automationRenamed": { + "title": "Automatisation renommée", + "description": "Le nouveau nom a été enregistré." + }, + "brandingUpdated": { + "title": "Image de marque mise à jour", + "description": "Les modifications de ton image de marque ont été enregistrées." + }, + "faviconGenerated": { + "title": "Favicon généré", + "description": "Nous avons créé un favicon à partir de ton logo." + } }, "error": { - "saveFailed": "Échec de l'enregistrement des modifications", - "profileUpdateFailed": "Échec de la mise à jour du profil", - "passwordChangeFailed": "Échec du changement de mot de passe. Merci de vérifier ton mot de passe actuel et réessayer.", - "organizationUpdateFailed": "Échec de la mise à jour de l'organisation", - "brandingUpdateFailed": "Échec de la mise à jour de l'image de marque", - "addMemberFailed": "Échec de l'ajout du membre", - "automationDuplicateFailed": "Échec de la duplication de l'automatisation", - "automationDeleteFailed": "Échec de la suppression de l'automatisation", - "generic": "Une erreur s'est produite. Merci de réessayer." + "saveFailed": { + "title": "Impossible d'enregistrer les modifications", + "description": "Une erreur s'est produite. Merci de réessayer." + }, + "profileUpdateFailed": { + "title": "Impossible de mettre à jour le profil", + "description": "Une erreur s'est produite. Merci de réessayer." + }, + "passwordChangeFailed": { + "title": "Impossible de changer le mot de passe", + "description": "Merci de vérifier ton mot de passe actuel et de réessayer." + }, + "organizationUpdateFailed": { + "title": "Impossible de mettre à jour l'organisation", + "description": "Une erreur s'est produite. Merci de réessayer." + }, + "brandingUpdateFailed": { + "title": "Impossible de mettre à jour l'image de marque", + "description": "Une erreur s'est produite. Merci de réessayer." + }, + "addMemberFailed": { + "title": "Impossible d'ajouter le membre", + "description": "Une erreur s'est produite. Merci de réessayer." + }, + "automationDuplicateFailed": { + "title": "Impossible de dupliquer l'automatisation", + "description": "Une erreur s'est produite. Merci de réessayer." + }, + "automationDeleteFailed": { + "title": "Impossible de supprimer l'automatisation", + "description": "Une erreur s'est produite. Merci de réessayer." + }, + "generic": { + "title": "Une erreur s'est produite", + "description": "Merci de réessayer." + } } }, "todoList": {