Three rules cleaned in parallel passes — 187 fewer warnings, 0 TS
errors, 0 behaviour change beyond one incidental auth bugfix
flagged below.
storybook/no-redundant-story-name (23 → 0) — 14 stories files
Storybook v7+ infers the story name from the variable name, so
`name: 'Default'` next to `export const Default: Story = …` is
pure noise. Removed only when the name was redundant ;
preserved when the label was a French translation
('Par défaut', 'Chargement', 'Avec erreur', etc.) since those
are intentional.
react-refresh/only-export-components (25 → 0) — 21 files
Each warning marks a file that exports a React component AND a
hook / context / constant / barrel re-export. Suppressed
per-line with the suppression-with-justification pattern :
// eslint-disable-next-line react-refresh/only-export-components -- <kind>; refactor would split a tightly-coupled API
The justification matters — every comment names the specific
thing being co-located (hook / context / CVA constant / lazy
registry / route config / test util / backward-compat barrel).
Splitting these would create 21 new files for a HMR-only DX
win that's already a non-issue in practice.
@typescript-eslint/no-non-null-assertion (139 → 0) — 43 files
Distribution of fixes :
~85 cases : refactored to explicit guard
`if (!x) throw new Error('invariant: …')`
or hoisted into local with narrowing.
~36 cases : helper extraction (one tooltip test had 16
`wrapper!` patterns reduced to a single
`getWrapper()` helper).
~18 cases : suppressed with specific reason :
static literal arrays where index is provably
in bounds, mock fixtures with structural
guarantees, filter-then-map patterns where the
filter excludes the null branch.
One incidental find : services/api/auth.ts threw on missing
tokens but didn't guard `user` ; added the missing check while
refactoring the `user!` to a guard.
baseline post-commit : 921 warnings, 0 errors, 0 TS errors.
The remaining buckets are no-restricted-syntax (757, design-system
guardrail), no-explicit-any (115), exhaustive-deps (49).
CI --max-warnings will be lowered to 921 in the follow-up commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
136 lines
3.8 KiB
TypeScript
136 lines
3.8 KiB
TypeScript
import {
|
||
createContext,
|
||
useContext,
|
||
useState,
|
||
useCallback,
|
||
ReactNode,
|
||
} from 'react';
|
||
import toast from '@/utils/toast';
|
||
import { logger } from '@/utils/logger';
|
||
import { Toast, ToastComponent } from './Toast';
|
||
import { cn } from '@/lib/utils';
|
||
|
||
interface ToastContextValue {
|
||
toasts: Toast[];
|
||
addToast: (toast: Omit<Toast, 'id'>) => void;
|
||
removeToast: (id: string) => void;
|
||
}
|
||
|
||
const ToastContext = createContext<ToastContextValue | undefined>(undefined);
|
||
|
||
// Export hooks for usage
|
||
// eslint-disable-next-line react-refresh/only-export-components -- co-located hook; tightly coupled to the provider's context
|
||
export function useToastContext() {
|
||
const context = useContext(ToastContext);
|
||
if (!context) {
|
||
throw new Error('useToastContext must be used within ToastProvider');
|
||
}
|
||
return context;
|
||
}
|
||
|
||
/**
|
||
* @deprecated S1.2: Use `useToast` from `@/hooks/useToast` or `toast` from `@/utils/toast` instead.
|
||
* Legacy compatibility hook — delegates to react-hot-toast. Works without ToastProvider.
|
||
*/
|
||
// eslint-disable-next-line react-refresh/only-export-components -- co-located deprecated hook; kept here until consumers migrate to @/hooks/useToast
|
||
export function useToast() {
|
||
const addToast = (messageOrToast: string | Omit<Toast, 'id'>, type?: 'success' | 'error' | 'warning' | 'info') => {
|
||
if (typeof messageOrToast === 'string') {
|
||
const t = type || 'info';
|
||
if (t === 'success') toast.success(messageOrToast);
|
||
else if (t === 'error') toast.error(messageOrToast);
|
||
else if (t === 'warning') toast(messageOrToast, { icon: '⚠️' });
|
||
else toast(messageOrToast, { icon: 'ℹ️' });
|
||
} else {
|
||
const msg = messageOrToast.message;
|
||
const t = messageOrToast.type || 'info';
|
||
if (t === 'success') toast.success(msg);
|
||
else if (t === 'error') toast.error(msg);
|
||
else if (t === 'warning') toast(msg, { icon: '⚠️' });
|
||
else toast(msg, { icon: 'ℹ️' });
|
||
}
|
||
};
|
||
|
||
return {
|
||
addToast,
|
||
toasts: [] as Toast[],
|
||
removeToast: (_id: string) => {},
|
||
};
|
||
}
|
||
|
||
export interface ToastProviderProps {
|
||
children: ReactNode;
|
||
position?:
|
||
| 'top-right'
|
||
| 'top-left'
|
||
| 'bottom-right'
|
||
| 'bottom-left'
|
||
| 'top-center'
|
||
| 'bottom-center';
|
||
className?: string;
|
||
}
|
||
|
||
const POSITION_CLASSES = {
|
||
'top-right': 'top-4 right-4',
|
||
'top-left': 'top-4 left-4',
|
||
'bottom-right': 'bottom-4 right-4',
|
||
'bottom-left': 'bottom-4 left-4',
|
||
'top-center': 'top-4 left-1/2 -translate-x-1/2',
|
||
'bottom-center': 'bottom-4 left-1/2 -translate-x-1/2',
|
||
};
|
||
|
||
/**
|
||
* Provider pour gérer la queue des toasts.
|
||
*/
|
||
export function ToastProvider({
|
||
children,
|
||
position = 'top-right',
|
||
className,
|
||
}: ToastProviderProps) {
|
||
const [toasts, setToasts] = useState<Toast[]>([]);
|
||
|
||
const addToast = useCallback((toast: Omit<Toast, 'id'>) => {
|
||
if (import.meta.env.VITE_STORYBOOK === 'true') {
|
||
logger.debug('[Storybook Toast]', { type: toast.type ?? 'info', message: toast.message });
|
||
return;
|
||
}
|
||
const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||
const newToast: Toast = {
|
||
...toast,
|
||
id,
|
||
};
|
||
|
||
setToasts((prev) => [...prev, newToast]);
|
||
}, []);
|
||
|
||
const removeToast = useCallback((id: string) => {
|
||
setToasts((prev) => prev.filter((toast) => toast.id !== id));
|
||
}, []);
|
||
|
||
const value: ToastContextValue = {
|
||
toasts,
|
||
addToast,
|
||
removeToast,
|
||
};
|
||
|
||
return (
|
||
<ToastContext.Provider value={value}>
|
||
{children}
|
||
<div
|
||
className={cn(
|
||
'fixed z-50 flex flex-col gap-2',
|
||
POSITION_CLASSES[position],
|
||
className,
|
||
)}
|
||
>
|
||
{toasts.map((toast) => (
|
||
<ToastComponent
|
||
key={toast.id}
|
||
toast={toast}
|
||
onDismiss={removeToast}
|
||
/>
|
||
))}
|
||
</div>
|
||
</ToastContext.Provider>
|
||
);
|
||
}
|