@clipboard-health/ai-rules 0.2.0 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4188 @@
1
+ <!-- Source: .ruler/common/codeStyleAndStructure.md -->
2
+
3
+ # Code style and structure
4
+
5
+ - Write concise, technical TypeScript code with accurate examples.
6
+ - Use functional and declarative programming patterns.
7
+ - Prefer iteration and modularization over code duplication.
8
+ - Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
9
+ - Structure files: constants, types, exported functions, non-exported functions.
10
+ - Avoid magic strings and numbers; define constants.
11
+ - Use camelCase for files and directories (e.g., modules/shiftOffers.ts).
12
+ - When declaring functions, use the `function` keyword, not `const`.
13
+ - Prefer data immutability.
14
+
15
+ <!-- Source: .ruler/common/errorHandlingAndValidation.md -->
16
+
17
+ # Error handling and validation
18
+
19
+ - Sanitize user input.
20
+ - Handle errors and edge cases at the beginning of functions.
21
+ - Use early returns for error conditions to avoid deeply nested if statements.
22
+ - Place the happy path last in the function for improved readability.
23
+ - Avoid unnecessary else statements; use the if-return pattern instead.
24
+ - Use guard clauses to handle preconditions and invalid states early.
25
+ - Implement proper error logging and user-friendly error messages.
26
+ - Favor `@clipboard-health/util-ts`'s `Either` type for expected errors instead of `try`/`catch`.
27
+
28
+ <!-- Source: .ruler/common/keyConventions.md -->
29
+
30
+ # Key conventions
31
+
32
+ - You are familiar with the latest features and best practices.
33
+ - You carefully provide accurate, factual, thoughtful answers and are a genius at reasoning.
34
+ - You always write correct, up-to-date, bug-free, fully functional, working, secure, easy-to-read, and efficient code.
35
+ - If there might not be a correct answer or do not know the answer, say so instead of guessing.
36
+
37
+ <!-- Source: .ruler/common/testing.md -->
38
+
39
+ # Testing
40
+
41
+ - Follow the Arrange-Act-Assert convention for tests with newlines between each section.
42
+ - Name test variables using the `mockX`, `input`, `expected`, `actual` convention.
43
+ - Aim for high test coverage, writing both positive and negative test cases.
44
+ - Prefer `it.each` for multiple test cases.
45
+ - Avoid conditional logic in tests.
46
+
47
+ <!-- Source: .ruler/common/typeScript.md -->
48
+
49
+ # TypeScript usage
50
+
51
+ - Use strict-mode TypeScript for all code; prefer interfaces over types.
52
+ - Avoid enums; use const maps instead.
53
+ - Strive for precise types. Look for type definitions in the codebase and create your own if none exist.
54
+ - Avoid using type assertions like `as` or `!` unless absolutely necessary.
55
+ - Use the `unknown` type instead of `any` when the type is truly unknown.
56
+ - Use an object to pass multiple function params and to return results.
57
+ - Leverage union types, intersection types, and conditional types for complex type definitions.
58
+ - Use mapped types and utility types (e.g., `Partial<T>`, `Pick<T>`, `Omit<T>`) to transform existing types.
59
+ - Implement generic types to create reusable, flexible type definitions.
60
+ - Utilize the `keyof` operator and index access types for dynamic property access.
61
+ - Implement discriminated unions for type-safe handling of different object shapes where appropriate.
62
+ - Use the `infer` keyword in conditional types for type inference.
63
+ - Leverage `readonly` properties for function parameter immutability.
64
+ - Prefer narrow types whenever possible with `as const` assertions, `typeof`, `instanceof`, `satisfies`, and custom type guards.
65
+ - Implement exhaustiveness checking using `never`.
66
+
67
+ <!-- Source: .ruler/frontend/custom-hooks.md -->
68
+
69
+ # Custom Hook Standards
70
+
71
+ ## Hook Structure
72
+
73
+ ```typescript
74
+ interface UseFeatureOptions {
75
+ enabled?: boolean;
76
+ refetchInterval?: number;
77
+ }
78
+
79
+ export function useFeature(params: Params, options: UseFeatureOptions = {}) {
80
+ const { enabled = true } = options;
81
+
82
+ // Multiple queries/hooks
83
+ const query1 = useQuery(...);
84
+ const query2 = useQuery(...);
85
+
86
+ // Derived state
87
+ const computed = useMemo(() => {
88
+ // Combine data
89
+ return transformData(query1.data, query2.data);
90
+ }, [query1.data, query2.data]);
91
+
92
+ // Callbacks
93
+ const handleAction = useCallback(async () => {
94
+ // Perform action
95
+ }, [dependencies]);
96
+
97
+ // Return structured object
98
+ return {
99
+ // Data
100
+ data: computed,
101
+
102
+ // Loading states
103
+ isLoading: query1.isLoading || query2.isLoading,
104
+ isError: query1.isError || query2.isError,
105
+
106
+ // Actions
107
+ refetch: async () => {
108
+ await Promise.all([query1.refetch(), query2.refetch()]);
109
+ },
110
+ handleAction,
111
+ };
112
+ }
113
+ ```
114
+
115
+ ## Naming Rules
116
+
117
+ - **Always** prefix with `use`
118
+ - Boolean hooks: `useIsFeatureEnabled`, `useShouldShowModal`, `useHasPermission`
119
+ - Data hooks: `useGetData`, `useFeatureData`
120
+ - Action hooks: `useBookShift`, `useSubmitForm`
121
+
122
+ ## Return Values
123
+
124
+ - Return objects (not arrays) for complex hooks
125
+ - Name properties clearly: `isLoading` not `loading`, `refetch` not `refresh`
126
+ - Group related properties together
127
+
128
+ ```typescript
129
+ // Good - clear property names
130
+ return {
131
+ data,
132
+ isLoading,
133
+ isError,
134
+ refetch,
135
+ };
136
+
137
+ // Avoid - array destructuring for complex returns
138
+ return [data, isLoading, refetch]; // Only for very simple hooks
139
+ ```
140
+
141
+ ## State Management with Constate
142
+
143
+ For local/shared state management, use **`constate`** to create context with minimal boilerplate:
144
+
145
+ ```typescript
146
+ import constate from "constate";
147
+ import { useState, useCallback } from "react";
148
+
149
+ // Define your hook with state logic
150
+ function useShiftFilters() {
151
+ const [filters, setFilters] = useState<Filters>({});
152
+ const [isLoading, setIsLoading] = useState(false);
153
+
154
+ const applyFilters = useCallback((newFilters: Filters) => {
155
+ setFilters(newFilters);
156
+ }, []);
157
+
158
+ const clearFilters = useCallback(() => {
159
+ setFilters({});
160
+ }, []);
161
+
162
+ return {
163
+ filters,
164
+ isLoading,
165
+ applyFilters,
166
+ clearFilters,
167
+ };
168
+ }
169
+
170
+ // Create provider and hook with constate
171
+ export const [ShiftFiltersProvider, useShiftFiltersContext] = constate(useShiftFilters);
172
+ ```
173
+
174
+ **Usage:**
175
+
176
+ ```typescript
177
+ // Wrap components that need access
178
+ function ShiftsPage() {
179
+ return (
180
+ <ShiftFiltersProvider>
181
+ <ShiftList />
182
+ <FilterPanel />
183
+ </ShiftFiltersProvider>
184
+ );
185
+ }
186
+
187
+ // Use in child components
188
+ function ShiftList() {
189
+ const { filters, applyFilters } = useShiftFiltersContext();
190
+
191
+ // Use shared state
192
+ return <div>{/* ... */}</div>;
193
+ }
194
+ ```
195
+
196
+ **Benefits:**
197
+
198
+ - Minimal boilerplate compared to raw Context API
199
+ - TypeScript support out of the box
200
+ - Avoids prop drilling
201
+ - Clean separation of state logic
202
+
203
+ **When to use Constate:**
204
+
205
+ - Sharing state between multiple sibling components
206
+ - Complex feature-level state (filters, UI state, form state)
207
+ - Alternative to prop drilling
208
+
209
+ **When NOT to use Constate:**
210
+
211
+ - Server state (use React Query instead)
212
+ - Global app state (consider if it's truly needed)
213
+ - Simple parent-child communication (use props)
214
+
215
+ ## Boolean Hooks
216
+
217
+ ```typescript
218
+ // Pattern: useIs*, useHas*, useShould*
219
+ export function useIsFeatureEnabled(): boolean {
220
+ const flags = useFeatureFlags();
221
+ return flags.includes("new-feature");
222
+ }
223
+
224
+ export function useHasPermission(permission: string): boolean {
225
+ const user = useUser();
226
+ return user.permissions.includes(permission);
227
+ }
228
+
229
+ export function useShouldShowModal(): boolean {
230
+ const hasSeenModal = usePreference("hasSeenModal");
231
+ return !hasSeenModal;
232
+ }
233
+ ```
234
+
235
+ ## Data Transformation Hooks
236
+
237
+ ```typescript
238
+ export function useTransformedData() {
239
+ const { data: rawData, isLoading, isError } = useGetRawData();
240
+
241
+ const transformedData = useMemo(() => {
242
+ if (!rawData) return undefined;
243
+
244
+ return rawData.map((item) => ({
245
+ ...item,
246
+ displayName: formatName(item),
247
+ }));
248
+ }, [rawData]);
249
+
250
+ return {
251
+ data: transformedData,
252
+ isLoading,
253
+ isError,
254
+ };
255
+ }
256
+ ```
257
+
258
+ ## Side Effect Hooks
259
+
260
+ ```typescript
261
+ // Hooks that perform side effects (logging, tracking, etc.)
262
+ export function useTrackPageView(pageName: string) {
263
+ useEffect(() => {
264
+ logEvent("PAGE_VIEWED", { pageName });
265
+ }, [pageName]);
266
+ }
267
+
268
+ // Usage
269
+ export function MyPage() {
270
+ useTrackPageView("MyPage");
271
+ // ... rest of component
272
+ }
273
+ ```
274
+
275
+ ## Composite Hooks
276
+
277
+ ```typescript
278
+ // Combines multiple hooks and data sources
279
+ export function useWorkerBookingsData() {
280
+ const worker = useDefinedWorker();
281
+ const isFeatureEnabled = useIsFeatureEnabled("new-bookings");
282
+
283
+ const {
284
+ data: shifts,
285
+ isLoading: isLoadingShifts,
286
+ refetch: refetchShifts,
287
+ } = useGetShifts(worker.id);
288
+
289
+ const {
290
+ data: invites,
291
+ isLoading: isLoadingInvites,
292
+ refetch: refetchInvites,
293
+ } = useGetInvites(worker.id, { enabled: isFeatureEnabled });
294
+
295
+ // Combine data
296
+ const bookings = useMemo(() => {
297
+ return [...(shifts ?? []), ...(invites ?? [])].sort(sortByDate);
298
+ }, [shifts, invites]);
299
+
300
+ // Combined loading state
301
+ const isLoading = isLoadingShifts || isLoadingInvites;
302
+
303
+ // Combined refetch
304
+ async function refreshAllData() {
305
+ await Promise.all([refetchShifts(), refetchInvites()]);
306
+ }
307
+
308
+ return {
309
+ // Data
310
+ bookings,
311
+
312
+ // States
313
+ isLoading,
314
+ isFeatureEnabled,
315
+
316
+ // Actions
317
+ refreshAllData,
318
+ };
319
+ }
320
+ ```
321
+
322
+ ## Co-location
323
+
324
+ - Place hooks in `hooks/` folder within feature directory
325
+ - Place API hooks in `api/` folder
326
+ - Keep generic/shared hooks in `lib/` or `utils/`
327
+
328
+ ```text
329
+ FeatureName/
330
+ ├── api/
331
+ │ ├── useGetFeature.ts # API data fetching
332
+ │ └── useUpdateFeature.ts
333
+ ├── hooks/
334
+ │ ├── useFeatureLogic.ts # Business logic hooks
335
+ │ ├── useFeatureState.ts # Constate state hooks
336
+ │ └── useFeatureFilters.ts
337
+ └── components/
338
+ └── FeatureComponent.tsx
339
+ ```
340
+
341
+ ## Options Pattern
342
+
343
+ ```typescript
344
+ // Always use options object for flexibility
345
+ interface UseFeatureOptions {
346
+ enabled?: boolean;
347
+ refetchInterval?: number;
348
+ onSuccess?: (data: Data) => void;
349
+ }
350
+
351
+ export function useFeature(params: Params, options: UseFeatureOptions = {}) {
352
+ const { enabled = true, refetchInterval, onSuccess } = options;
353
+
354
+ // Use options in queries
355
+ return useQuery({
356
+ enabled,
357
+ refetchInterval,
358
+ onSuccess,
359
+ // ...
360
+ });
361
+ }
362
+ ```
363
+
364
+ ## Hook Dependencies
365
+
366
+ ```typescript
367
+ // Be explicit about dependencies
368
+ export function useFormattedData(rawData: Data[]) {
369
+ return useMemo(() => {
370
+ return rawData.map(format);
371
+ }, [rawData]); // Clear dependency
372
+ }
373
+
374
+ // Avoid creating functions in dependency arrays
375
+ export function useHandler() {
376
+ const [value, setValue] = useState();
377
+
378
+ // Good - stable reference
379
+ const handleChange = useCallback((newValue: string) => {
380
+ setValue(newValue);
381
+ }, []); // No dependencies on value
382
+
383
+ return handleChange;
384
+ }
385
+ ```
386
+
387
+ ## Best Practices
388
+
389
+ - **Prefix with `use`** - Always
390
+ - **Return objects, not arrays** - For hooks with multiple values
391
+ - **Use constate for shared state** - Avoid prop drilling
392
+ - **API hooks in `api/`** - Logic hooks in `hooks/`
393
+ - **Clear property names** - `isLoading`, not `loading`
394
+ - **Options pattern** - For flexible configuration
395
+ - **Explicit dependencies** - In useMemo/useCallback
396
+
397
+ <!-- Source: .ruler/frontend/data-fetching.md -->
398
+
399
+ # Data Fetching Standards
400
+
401
+ ## Technology Stack
402
+
403
+ - **React Query** (@tanstack/react-query) for all API calls
404
+ - **Axios** for HTTP requests
405
+ - **Zod** for response validation
406
+
407
+ ## Core Principles
408
+
409
+ 1. **Use URL and query parameters in query keys** - Makes cache invalidation predictable
410
+ 2. **Always use `useGetQuery` hook** - Provides consistent structure, logging, and validation
411
+ 3. **Define Zod schemas** for all API requests and responses
412
+ 4. **Log errors with centralized constants** - From `APP_V2_APP_EVENTS`, never inline strings
413
+ 5. **Rely on React Query state** - Use `isLoading`, `isError`, `isSuccess` - don't reinvent state management
414
+ 6. **Use `enabled` for conditional fetching** - With `isDefined()` helper
415
+ 7. **Use `invalidateQueries` for disabled queries** - Not `refetch()` which ignores enabled state
416
+
417
+ ## Hook Patterns
418
+
419
+ ### Simple Query
420
+
421
+ ```typescript
422
+ export function useGetUser(userId: string) {
423
+ return useGetQuery({
424
+ url: `/api/users/${userId}`,
425
+ responseSchema: userSchema,
426
+ enabled: isDefined(userId),
427
+ staleTime: minutesToMilliseconds(5),
428
+ meta: {
429
+ logErrorMessage: APP_V2_APP_EVENTS.GET_USER_FAILURE,
430
+ },
431
+ });
432
+ }
433
+ ```
434
+
435
+ ### Infinite/Paginated Query
436
+
437
+ ```typescript
438
+ export function usePaginatedShifts(params: Params) {
439
+ return useInfiniteQuery({
440
+ queryKey: ["shifts", params],
441
+ queryFn: async ({ pageParam }) => {
442
+ const response = await get({
443
+ url: "/api/shifts",
444
+ queryParams: { cursor: pageParam, ...params },
445
+ responseSchema: shiftsResponseSchema,
446
+ });
447
+ return response.data;
448
+ },
449
+ getNextPageParam: (lastPage) => lastPage.links.nextCursor,
450
+ });
451
+ }
452
+ ```
453
+
454
+ ### Composite Data Fetching
455
+
456
+ ```typescript
457
+ // Hook that combines multiple queries
458
+ export function useWorkerBookingsData() {
459
+ const { data: shifts, isLoading: isLoadingShifts, refetch: refetchShifts } = useGetShifts();
460
+ const { data: invites, isLoading: isLoadingInvites, refetch: refetchInvites } = useGetInvites();
461
+
462
+ // Combine data
463
+ const bookings = useMemo(() => {
464
+ return [...(shifts ?? []), ...(invites ?? [])];
465
+ }, [shifts, invites]);
466
+
467
+ // Combine loading states
468
+ const isLoading = isLoadingShifts || isLoadingInvites;
469
+
470
+ // Combine refetch functions
471
+ async function refreshAllData() {
472
+ await Promise.all([refetchShifts(), refetchInvites()]);
473
+ }
474
+
475
+ return {
476
+ bookings,
477
+ isLoading,
478
+ refreshAllData,
479
+ };
480
+ }
481
+ ```
482
+
483
+ ## Error Handling
484
+
485
+ Always use centralized error constants and handle expected errors gracefully:
486
+
487
+ ```typescript
488
+ useGetQuery({
489
+ url: "/api/resource",
490
+ responseSchema: schema,
491
+ meta: {
492
+ logErrorMessage: APP_V2_APP_EVENTS.GET_RESOURCE_FAILURE, // ✅ Centralized
493
+ userErrorMessage: "Failed to load data", // Shows alert to user
494
+ },
495
+ useErrorBoundary: (error) => {
496
+ // Don't show error boundary for 404s (expected errors)
497
+ return !(axios.isAxiosError(error) && error.response?.status === 404);
498
+ },
499
+ retry: (failureCount, error) => {
500
+ // Don't retry 404s or 401s
501
+ if (axios.isAxiosError(error)) {
502
+ const status = error.response?.status;
503
+ return ![404, 401].includes(status ?? 0);
504
+ }
505
+ return failureCount < 3;
506
+ },
507
+ });
508
+ ```
509
+
510
+ ## State Management - Don't Reinvent the Wheel
511
+
512
+ ❌ **Don't** create your own loading/error state:
513
+
514
+ ```typescript
515
+ const [data, setData] = useState();
516
+ const [isLoading, setIsLoading] = useState(false);
517
+ const [error, setError] = useState();
518
+
519
+ useEffect(() => {
520
+ async function fetchData() {
521
+ try {
522
+ setIsLoading(true);
523
+ const result = await api.get("/data");
524
+ setData(result);
525
+ } catch (err) {
526
+ setError(err);
527
+ } finally {
528
+ setIsLoading(false);
529
+ }
530
+ }
531
+ fetchData();
532
+ }, []);
533
+ ```
534
+
535
+ ✅ **Do** use React Query states:
536
+
537
+ ```typescript
538
+ const { data, isLoading, isError, isSuccess } = useGetQuery({...});
539
+
540
+ if (isLoading) return <Loading />;
541
+ if (isError) return <Error />;
542
+ if (isSuccess) return <div>{data.property}</div>;
543
+ ```
544
+
545
+ ## Mutations
546
+
547
+ ```typescript
548
+ export function useCreateDocument() {
549
+ const queryClient = useQueryClient();
550
+
551
+ return useMutation({
552
+ mutationFn: async (data: CreateDocumentRequest) => {
553
+ return await post({
554
+ url: "/api/documents",
555
+ data,
556
+ responseSchema: documentSchema,
557
+ });
558
+ },
559
+ onSuccess: () => {
560
+ // Invalidate queries to refetch
561
+ queryClient.invalidateQueries(["documents"]);
562
+ },
563
+ onError: (error) => {
564
+ logEvent(APP_V2_APP_EVENTS.CREATE_DOCUMENT_FAILURE, { error });
565
+ },
566
+ });
567
+ }
568
+ ```
569
+
570
+ ## Query Keys
571
+
572
+ Always include URL and parameters in query keys:
573
+
574
+ ```typescript
575
+ // ❌ Don't use static strings
576
+ useQuery({ queryKey: "users", ... });
577
+
578
+ // ✅ Do include URL and params
579
+ useQuery({ queryKey: [`/api/users?${status}`, { status }], ... });
580
+
581
+ // Consistent query key structure
582
+ export const queryKeys = {
583
+ users: ["users"] as const,
584
+ user: (id: string) => ["users", id] as const,
585
+ userShifts: (id: string) => ["users", id, "shifts"] as const,
586
+ };
587
+
588
+ // Usage
589
+ useQuery({
590
+ queryKey: queryKeys.user(userId),
591
+ // ...
592
+ });
593
+ ```
594
+
595
+ ## Refetch Intervals
596
+
597
+ ```typescript
598
+ useGetQuery({
599
+ url: "/api/resource",
600
+ responseSchema: schema,
601
+ refetchInterval: (data) => {
602
+ // Dynamic refetch based on data state
603
+ if (!data?.isComplete) {
604
+ return 1000; // Poll every second until complete
605
+ }
606
+ return 0; // Stop refetching
607
+ },
608
+ });
609
+ ```
610
+
611
+ ## Conditional Fetching
612
+
613
+ Use `enabled` option with `isDefined()` helper:
614
+
615
+ ```typescript
616
+ import { isDefined } from "@/lib/utils";
617
+
618
+ const { data } = useGetQuery({
619
+ url: "/api/resource",
620
+ responseSchema: schema,
621
+ // Only fetch when conditions are met
622
+ enabled: isDefined(userId) && isFeatureEnabled,
623
+ });
624
+ ```
625
+
626
+ ## Refetch vs InvalidateQueries
627
+
628
+ **Important:** For disabled queries, use `invalidateQueries` instead of `refetch`:
629
+
630
+ ❌ **Don't** use `refetch()` on disabled queries:
631
+
632
+ ```typescript
633
+ const { refetch, data } = useGetQuery({
634
+ enabled: isDefined(shift.agentId),
635
+ ...
636
+ });
637
+
638
+ // Will fetch even if agentId is undefined!
639
+ refetch();
640
+ ```
641
+
642
+ ✅ **Do** use `invalidateQueries`:
643
+
644
+ ```typescript
645
+ const queryClient = useQueryClient();
646
+
647
+ const { data } = useGetQuery({
648
+ enabled: isDefined(shift.agentId),
649
+ ...
650
+ });
651
+
652
+ // Respects the enabled state
653
+ queryClient.invalidateQueries({ queryKey: [myQueryKey] });
654
+ ```
655
+
656
+ ## Query Cancellation
657
+
658
+ ```typescript
659
+ export function usePaginatedData() {
660
+ const queryClient = useQueryClient();
661
+
662
+ return useInfiniteQuery({
663
+ queryKey: ["data"],
664
+ queryFn: async ({ pageParam }) => {
665
+ // Cancel previous in-flight requests
666
+ await queryClient.cancelQueries({ queryKey: ["data"] });
667
+
668
+ const response = await get({
669
+ url: "/api/data",
670
+ queryParams: { cursor: pageParam },
671
+ });
672
+ return response.data;
673
+ },
674
+ // ...
675
+ });
676
+ }
677
+ ```
678
+
679
+ ## Naming Conventions
680
+
681
+ - **useGet\*** - Simple queries: `useGetUser`, `useGetShift`
682
+ - **usePaginated\*** - Infinite queries: `usePaginatedPlacements`
683
+ - **useFetch\*** - Complex fetching logic: `useFetchPaginatedInterviews`
684
+ - **Mutations**: `useCreateDocument`, `useUpdateShift`, `useDeletePlacement`
685
+
686
+ ## Response Transformation
687
+
688
+ ```typescript
689
+ export function useGetUser(userId: string) {
690
+ return useGetQuery({
691
+ url: `/api/users/${userId}`,
692
+ responseSchema: userResponseSchema,
693
+ select: (data) => {
694
+ // Transform response data
695
+ return {
696
+ ...data,
697
+ fullName: `${data.firstName} ${data.lastName}`,
698
+ };
699
+ },
700
+ });
701
+ }
702
+ ```
703
+
704
+ ## Hook Location
705
+
706
+ - **API hooks** → Place in `api/` folder within feature directory
707
+ - **One endpoint = one hook** principle
708
+ - Export types inferred from Zod: `export type User = z.infer<typeof userSchema>`
709
+
710
+ Example:
711
+
712
+ ```text
713
+ Feature/
714
+ ├── api/
715
+ │ ├── useGetResource.ts
716
+ │ ├── useCreateResource.ts
717
+ │ └── schemas.ts (optional)
718
+ ```
719
+
720
+ <!-- Source: .ruler/frontend/error-handling.md -->
721
+
722
+ # Error Handling Standards
723
+
724
+ ## React Query Error Handling
725
+
726
+ ### Basic Error Configuration
727
+
728
+ ```typescript
729
+ useGetQuery({
730
+ url: "/api/resource",
731
+ responseSchema: schema,
732
+ meta: {
733
+ logErrorMessage: APP_V2_APP_EVENTS.GET_RESOURCE_FAILURE,
734
+ },
735
+ useErrorBoundary: (error) => {
736
+ // Show error boundary for 500s, not for 404s
737
+ return !(axios.isAxiosError(error) && error.response?.status === 404);
738
+ },
739
+ retry: (failureCount, error) => {
740
+ // Don't retry 404s or 401s
741
+ if (axios.isAxiosError(error)) {
742
+ const status = error.response?.status;
743
+ return ![404, 401].includes(status ?? 0);
744
+ }
745
+ return failureCount < 3;
746
+ },
747
+ });
748
+ ```
749
+
750
+ ### Error Boundary Strategy
751
+
752
+ - **Show error boundary** for unexpected errors (500s, network failures)
753
+ - **Don't show error boundary** for expected errors (404s, validation errors)
754
+ - Use `useErrorBoundary` to control this behavior
755
+
756
+ ```typescript
757
+ useErrorBoundary: (error) => {
758
+ // Only show error boundary for server errors
759
+ if (axios.isAxiosError(error)) {
760
+ const status = error.response?.status;
761
+ return status !== undefined && status >= 500;
762
+ }
763
+ return true; // Show for non-Axios errors
764
+ };
765
+ ```
766
+
767
+ ### Retry Configuration
768
+
769
+ ```typescript
770
+ // Don't retry client errors
771
+ retry: (failureCount, error) => {
772
+ if (axios.isAxiosError(error)) {
773
+ const status = error.response?.status;
774
+ // Don't retry 4xx errors
775
+ if (status !== undefined && status >= 400 && status < 500) {
776
+ return false;
777
+ }
778
+ }
779
+ // Retry server errors up to 3 times
780
+ return failureCount < 3;
781
+ };
782
+ ```
783
+
784
+ ### Exponential Backoff
785
+
786
+ ```typescript
787
+ import { type QueryClient } from "@tanstack/react-query";
788
+
789
+ const queryClient = new QueryClient({
790
+ defaultOptions: {
791
+ queries: {
792
+ retry: 3,
793
+ retryDelay: (attemptIndex) => {
794
+ // Exponential backoff: 1s, 2s, 4s
795
+ return Math.min(1000 * 2 ** attemptIndex, 30000);
796
+ },
797
+ },
798
+ },
799
+ });
800
+ ```
801
+
802
+ ## Component Error States
803
+
804
+ ### Pattern: Loading → Error → Success
805
+
806
+ ```typescript
807
+ export function DataComponent() {
808
+ const { data, isLoading, isError, error, refetch } = useGetData();
809
+
810
+ if (isLoading) {
811
+ return <LoadingState />;
812
+ }
813
+
814
+ if (isError) {
815
+ return <ErrorState message="Failed to load data" onRetry={refetch} error={error} />;
816
+ }
817
+
818
+ // Happy path
819
+ return <DataDisplay data={data} />;
820
+ }
821
+ ```
822
+
823
+ ### Inline Error Messages
824
+
825
+ ```typescript
826
+ export function FormComponent() {
827
+ const mutation = useCreateResource();
828
+
829
+ return (
830
+ <form onSubmit={handleSubmit}>
831
+ {mutation.isError && <Alert severity="error">Failed to save. Please try again.</Alert>}
832
+
833
+ <Button type="submit" loading={mutation.isLoading} disabled={mutation.isLoading}>
834
+ Save
835
+ </Button>
836
+ </form>
837
+ );
838
+ }
839
+ ```
840
+
841
+ ### Graceful Degradation
842
+
843
+ ```typescript
844
+ export function OptionalDataComponent() {
845
+ const { data, isError } = useGetOptionalData();
846
+
847
+ // Don't block UI for optional data
848
+ if (isError) {
849
+ logError("Failed to load optional data");
850
+ return null; // or show simplified version
851
+ }
852
+
853
+ if (!data) {
854
+ return null;
855
+ }
856
+
857
+ return <EnhancedView data={data} />;
858
+ }
859
+ ```
860
+
861
+ ## Mutation Error Handling
862
+
863
+ ### onError Callback
864
+
865
+ ```typescript
866
+ export function useCreateDocument() {
867
+ const queryClient = useQueryClient();
868
+
869
+ return useMutation({
870
+ mutationFn: createDocumentApi,
871
+ onSuccess: (data) => {
872
+ queryClient.invalidateQueries(["documents"]);
873
+ showSuccessToast("Document created");
874
+ },
875
+ onError: (error) => {
876
+ logEvent(APP_V2_APP_EVENTS.CREATE_DOCUMENT_FAILURE, {
877
+ error: error.message,
878
+ });
879
+ showErrorToast("Failed to create document");
880
+ },
881
+ });
882
+ }
883
+ ```
884
+
885
+ ### Handling Specific Errors
886
+
887
+ ```typescript
888
+ export function useUpdateProfile() {
889
+ return useMutation({
890
+ mutationFn: updateProfileApi,
891
+ onError: (error: AxiosError) => {
892
+ if (error.response?.status === 409) {
893
+ showErrorToast("Email already exists");
894
+ } else if (error.response?.status === 422) {
895
+ showErrorToast("Invalid data provided");
896
+ } else {
897
+ showErrorToast("Failed to update profile");
898
+ }
899
+ },
900
+ });
901
+ }
902
+ ```
903
+
904
+ ## Logging and Monitoring
905
+
906
+ ### Event Logging
907
+
908
+ ```typescript
909
+ import { logEvent } from '@/lib/analytics';
910
+ import { APP_EVENTS } from '@/constants/events';
911
+
912
+ // In query configuration
913
+ meta: {
914
+ logErrorMessage: APP_V2_APP_EVENTS.GET_SHIFTS_FAILURE,
915
+ }
916
+
917
+ // In error handlers
918
+ onError: (error) => {
919
+ logEvent(APP_V2_APP_EVENTS.BOOKING_FAILED, {
920
+ shiftId,
921
+ error: error.message,
922
+ userId: worker.id,
923
+ });
924
+ }
925
+ ```
926
+
927
+ ### Error Context
928
+
929
+ Always include relevant context when logging errors:
930
+
931
+ ```typescript
932
+ logEvent(APP_V2_APP_EVENTS.API_ERROR, {
933
+ endpoint: "/api/shifts",
934
+ method: "GET",
935
+ statusCode: error.response?.status,
936
+ errorMessage: error.message,
937
+ userId: worker.id,
938
+ timestamp: new Date().toISOString(),
939
+ });
940
+ ```
941
+
942
+ ## Validation Errors
943
+
944
+ ### Zod Validation
945
+
946
+ ```typescript
947
+ import { z } from "zod";
948
+
949
+ const formSchema = z.object({
950
+ email: z.string().email("Invalid email address"),
951
+ age: z.number().min(18, "Must be 18 or older"),
952
+ });
953
+
954
+ try {
955
+ const validated = formSchema.parse(formData);
956
+ // Use validated data
957
+ } catch (error) {
958
+ if (error instanceof z.ZodError) {
959
+ // Handle validation errors
960
+ error.errors.forEach((err) => {
961
+ showFieldError(err.path.join("."), err.message);
962
+ });
963
+ }
964
+ }
965
+ ```
966
+
967
+ ### API Validation Errors
968
+
969
+ ```typescript
970
+ interface ApiValidationError {
971
+ field: string;
972
+ message: string;
973
+ }
974
+
975
+ function handleApiValidationError(error: AxiosError) {
976
+ const validationErrors = error.response?.data?.errors as ApiValidationError[];
977
+
978
+ if (validationErrors) {
979
+ validationErrors.forEach(({ field, message }) => {
980
+ setFieldError(field, message);
981
+ });
982
+ }
983
+ }
984
+ ```
985
+
986
+ ## Network Errors
987
+
988
+ ### Offline Detection
989
+
990
+ ```typescript
991
+ export function useNetworkStatus() {
992
+ const [isOnline, setIsOnline] = useState(navigator.onLine);
993
+
994
+ useEffect(() => {
995
+ function handleOnline() {
996
+ setIsOnline(true);
997
+ }
998
+
999
+ function handleOffline() {
1000
+ setIsOnline(false);
1001
+ }
1002
+
1003
+ window.addEventListener("online", handleOnline);
1004
+ window.addEventListener("offline", handleOffline);
1005
+
1006
+ return () => {
1007
+ window.removeEventListener("online", handleOnline);
1008
+ window.removeEventListener("offline", handleOffline);
1009
+ };
1010
+ }, []);
1011
+
1012
+ return isOnline;
1013
+ }
1014
+ ```
1015
+
1016
+ ### Offline UI
1017
+
1018
+ ```typescript
1019
+ export function AppContainer() {
1020
+ const isOnline = useNetworkStatus();
1021
+
1022
+ return (
1023
+ <>
1024
+ {!isOnline && (
1025
+ <Banner severity="warning">You are offline. Some features may not be available.</Banner>
1026
+ )}
1027
+ <App />
1028
+ </>
1029
+ );
1030
+ }
1031
+ ```
1032
+
1033
+ ## Timeout Handling
1034
+
1035
+ ### Request Timeouts
1036
+
1037
+ ```typescript
1038
+ import axios from "axios";
1039
+
1040
+ const api = axios.create({
1041
+ timeout: 30000, // 30 seconds
1042
+ });
1043
+
1044
+ api.interceptors.response.use(
1045
+ (response) => response,
1046
+ (error) => {
1047
+ if (error.code === "ECONNABORTED") {
1048
+ showErrorToast("Request timed out. Please try again.");
1049
+ }
1050
+ return Promise.reject(error);
1051
+ },
1052
+ );
1053
+ ```
1054
+
1055
+ ## Error Boundaries
1056
+
1057
+ ### React Error Boundary
1058
+
1059
+ ```typescript
1060
+ import { Component, type ReactNode } from "react";
1061
+
1062
+ interface Props {
1063
+ children: ReactNode;
1064
+ fallback?: ReactNode;
1065
+ }
1066
+
1067
+ interface State {
1068
+ hasError: boolean;
1069
+ error?: Error;
1070
+ }
1071
+
1072
+ export class ErrorBoundary extends Component<Props, State> {
1073
+ constructor(props: Props) {
1074
+ super(props);
1075
+ this.state = { hasError: false };
1076
+ }
1077
+
1078
+ static getDerivedStateFromError(error: Error): State {
1079
+ return { hasError: true, error };
1080
+ }
1081
+
1082
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
1083
+ logEvent("ERROR_BOUNDARY_TRIGGERED", {
1084
+ error: error.message,
1085
+ componentStack: errorInfo.componentStack,
1086
+ });
1087
+ }
1088
+
1089
+ render() {
1090
+ if (this.state.hasError) {
1091
+ return this.props.fallback || <ErrorFallback />;
1092
+ }
1093
+
1094
+ return this.props.children;
1095
+ }
1096
+ }
1097
+ ```
1098
+
1099
+ ## Best Practices
1100
+
1101
+ ### 1. Always Handle Errors
1102
+
1103
+ ```typescript
1104
+ // ❌ Don't ignore errors
1105
+ const { data } = useGetData();
1106
+
1107
+ // ✅ Handle error states
1108
+ const { data, isError, error } = useGetData();
1109
+ if (isError) {
1110
+ return <ErrorState error={error} />;
1111
+ }
1112
+ ```
1113
+
1114
+ ### 2. Provide User-Friendly Messages
1115
+
1116
+ ```typescript
1117
+ // ❌ Show technical errors to users
1118
+ <Alert>{error.message}</Alert>
1119
+
1120
+ // ✅ Show helpful messages
1121
+ <Alert>
1122
+ We couldn't load your shifts. Please check your connection and try again.
1123
+ </Alert>
1124
+ ```
1125
+
1126
+ ### 3. Log Errors for Debugging
1127
+
1128
+ ```typescript
1129
+ // Always log errors for monitoring
1130
+ onError: (error) => {
1131
+ logEvent(APP_V2_APP_EVENTS.ERROR, {
1132
+ context: "shift-booking",
1133
+ error: error.message,
1134
+ });
1135
+ showErrorToast("Booking failed");
1136
+ };
1137
+ ```
1138
+
1139
+ ### 4. Provide Recovery Actions
1140
+
1141
+ ```typescript
1142
+ // ✅ Give users a way to recover
1143
+ <ErrorState message="Failed to load data" onRetry={refetch} onDismiss={() => navigate("/home")} />
1144
+ ```
1145
+
1146
+ ### 5. Different Strategies for Different Errors
1147
+
1148
+ ```typescript
1149
+ // Critical errors: Show error boundary
1150
+ useErrorBoundary: (error) => isCriticalError(error);
1151
+
1152
+ // Expected errors: Show inline message
1153
+ if (isError && error.response?.status === 404) {
1154
+ return <NotFoundMessage />;
1155
+ }
1156
+
1157
+ // Transient errors: Auto-retry with backoff
1158
+ retry: (failureCount) => failureCount < 3;
1159
+ ```
1160
+
1161
+ <!-- Source: .ruler/frontend/file-organization.md -->
1162
+
1163
+ # File Organization Standards
1164
+
1165
+ ## Feature-Based Structure
1166
+
1167
+ ```text
1168
+ FeatureName/
1169
+ ├── api/ # Data fetching hooks
1170
+ │ ├── useGetFeature.ts
1171
+ │ ├── useUpdateFeature.ts
1172
+ │ └── useDeleteFeature.ts
1173
+ ├── components/ # Feature-specific components
1174
+ │ ├── FeatureCard.tsx
1175
+ │ ├── FeatureList.tsx
1176
+ │ └── FeatureHeader.tsx
1177
+ ├── hooks/ # Feature-specific hooks (non-API)
1178
+ │ ├── useFeatureLogic.ts
1179
+ │ └── useFeatureState.ts
1180
+ ├── utils/ # Feature utilities
1181
+ │ ├── formatFeature.ts
1182
+ │ ├── formatFeature.test.ts
1183
+ │ └── validateFeature.ts
1184
+ ├── __tests__/ # Integration tests (optional)
1185
+ │ └── FeatureFlow.test.tsx
1186
+ ├── Page.tsx # Main page component
1187
+ ├── Router.tsx # Feature routes
1188
+ ├── paths.ts # Route paths
1189
+ ├── types.ts # Shared types
1190
+ ├── constants.ts # Constants
1191
+ └── README.md # Feature documentation (optional)
1192
+ ```
1193
+
1194
+ ## File Naming Conventions
1195
+
1196
+ ### React Components
1197
+
1198
+ - **PascalCase** for all React components
1199
+ - Examples: `Button.tsx`, `UserProfile.tsx`, `ShiftCard.tsx`
1200
+
1201
+ ### Avoid Path Stuttering
1202
+
1203
+ Don't repeat directory names in file names - the full path provides enough context:
1204
+
1205
+ ❌ **Bad** - Path stuttering:
1206
+
1207
+ ```text
1208
+ Shift/
1209
+ ShiftInvites/
1210
+ ShiftInviteCard.tsx // ❌ "Shift" repeated 3 times in path
1211
+ ShiftInviteList.tsx // ❌ Import: Shift/ShiftInvites/ShiftInviteCard
1212
+ ```
1213
+
1214
+ ✅ **Good** - Clean, concise:
1215
+
1216
+ ```text
1217
+ Shift/
1218
+ Invites/
1219
+ Card.tsx // ✅ Path: Shift/Invites/Card
1220
+ List.tsx // ✅ Import: Shift/Invites/List
1221
+ ```
1222
+
1223
+ **Reasoning:**
1224
+
1225
+ - The full path already provides context (`Shift/Invites/Card` is clear)
1226
+ - Repeating names makes imports verbose: `import { ShiftInviteCard } from 'Shift/ShiftInvites/ShiftInviteCard'`
1227
+ - Shorter names are easier to work with in the editor
1228
+
1229
+ ### Utilities and Hooks
1230
+
1231
+ - **camelCase** for utilities, hooks, and non-component files
1232
+ - Examples: `formatDate.ts`, `useAuth.ts`, `calculateTotal.ts`
1233
+
1234
+ ### Multi-word Non-Components
1235
+
1236
+ - **camelCase** for multi-word utility and configuration files
1237
+ - Examples: `userProfileUtils.ts`, `apiHelpers.ts`
1238
+
1239
+ ### Test Files
1240
+
1241
+ - Co-locate with source: `Button.test.tsx` next to `Button.tsx`
1242
+ - Test folder: `__tests__/` for integration tests
1243
+ - Pattern: `*.test.ts` or `*.test.tsx`
1244
+
1245
+ ### Constants and Types
1246
+
1247
+ - `types.ts` - Shared types for the feature
1248
+ - `constants.ts` - Feature constants
1249
+ - `paths.ts` - Route path constants
1250
+
1251
+ ## Import Organization
1252
+
1253
+ ### Import Order
1254
+
1255
+ ```typescript
1256
+ // 1. External dependencies (React, third-party)
1257
+ import { useState, useEffect } from "react";
1258
+ import { useQuery } from "@tanstack/react-query";
1259
+ import { parseISO, format } from "date-fns";
1260
+
1261
+ // 2. Internal packages (@clipboard-health, @src)
1262
+ import { Button } from "@clipboard-health/ui-components";
1263
+ import { CbhIcon } from "@clipboard-health/ui-components";
1264
+ import { formatDate } from "@src/appV2/lib/dates";
1265
+ import { useDefinedWorker } from "@src/appV2/Worker/useDefinedWorker";
1266
+
1267
+ // 3. Relative imports (same feature)
1268
+ import { useFeatureData } from "./hooks/useFeatureData";
1269
+ import { FeatureCard } from "./components/FeatureCard";
1270
+ import { FEATURE_PATHS } from "./paths";
1271
+ import { type FeatureData } from "./types";
1272
+ ```
1273
+
1274
+ ### Import Grouping Rules
1275
+
1276
+ - Add blank lines between groups
1277
+ - Sort alphabetically within each group (optional but recommended)
1278
+ - Group type imports with their module: `import { type User } from './types'`
1279
+
1280
+ ## Path Management
1281
+
1282
+ ### Defining Paths
1283
+
1284
+ ```typescript
1285
+ // paths.ts
1286
+ import { APP_PATHS } from "@/constants/paths";
1287
+
1288
+ export const FEATURE_BASE_PATH = "feature";
1289
+ export const FEATURE_FULL_PATH = `${APP_PATHS.APP_V2_HOME}/${FEATURE_BASE_PATH}`;
1290
+ export const FEATURE_PATHS = {
1291
+ ROOT: FEATURE_FULL_PATH,
1292
+ DETAILS: `${FEATURE_FULL_PATH}/:id`,
1293
+ EDIT: `${FEATURE_FULL_PATH}/:id/edit`,
1294
+ CREATE: `${FEATURE_FULL_PATH}/create`,
1295
+ } as const;
1296
+ ```
1297
+
1298
+ ### Using Paths
1299
+
1300
+ ```typescript
1301
+ import { useHistory } from "react-router-dom";
1302
+ import { FEATURE_PATHS } from "./paths";
1303
+
1304
+ // Navigation
1305
+ history.push(FEATURE_PATHS.DETAILS.replace(":id", featureId));
1306
+
1307
+ // Route definition
1308
+ <Route path={FEATURE_PATHS.DETAILS} component={FeatureDetailsPage} />;
1309
+ ```
1310
+
1311
+ ## Types and Interfaces
1312
+
1313
+ ### Separate vs Co-located Types
1314
+
1315
+ ```typescript
1316
+ // Option 1: Separate types.ts for shared types
1317
+ // types.ts
1318
+ export interface Feature {
1319
+ id: string;
1320
+ name: string;
1321
+ }
1322
+
1323
+ export type FeatureStatus = "active" | "inactive";
1324
+
1325
+ // Option 2: Co-located with component for component-specific types
1326
+ // FeatureCard.tsx
1327
+ interface FeatureCardProps {
1328
+ feature: Feature;
1329
+ onSelect: (id: string) => void;
1330
+ }
1331
+
1332
+ export function FeatureCard(props: FeatureCardProps) {
1333
+ // ...
1334
+ }
1335
+ ```
1336
+
1337
+ ## Constants
1338
+
1339
+ ### Defining Constants
1340
+
1341
+ ```typescript
1342
+ // constants.ts
1343
+ export const FEATURE_STATUS = {
1344
+ ACTIVE: "active",
1345
+ INACTIVE: "inactive",
1346
+ PENDING: "pending",
1347
+ } as const;
1348
+
1349
+ export type FeatureStatus = (typeof FEATURE_STATUS)[keyof typeof FEATURE_STATUS];
1350
+
1351
+ export const MAX_ITEMS = 100;
1352
+ export const DEFAULT_PAGE_SIZE = 20;
1353
+
1354
+ export const FEATURE_EVENTS = {
1355
+ VIEWED: "FEATURE_VIEWED",
1356
+ CREATED: "FEATURE_CREATED",
1357
+ UPDATED: "FEATURE_UPDATED",
1358
+ } as const;
1359
+ ```
1360
+
1361
+ ## Folder Depth Guidelines
1362
+
1363
+ - **Maximum 3 levels deep** for feature folders
1364
+ - For deeper nesting, consider splitting into separate features
1365
+ - Use meaningful folder names that describe the content
1366
+
1367
+ ```text
1368
+ Good:
1369
+ ├── Shift/
1370
+ │ ├── Calendar/
1371
+ │ │ └── ShiftCalendarCore.tsx
1372
+ │ └── Card/
1373
+ │ └── ShiftCard.tsx
1374
+
1375
+ Avoid:
1376
+ ├── Shift/
1377
+ │ ├── Components/
1378
+ │ │ ├── Display/
1379
+ │ │ │ ├── Calendar/
1380
+ │ │ │ │ └── Core/
1381
+ │ │ │ │ └── ShiftCalendarCore.tsx # Too deep!
1382
+ ```
1383
+
1384
+ ## Module Exports
1385
+
1386
+ ### Index Files
1387
+
1388
+ Avoid using `index.ts` files in the redesign folder. Prefer explicit imports.
1389
+
1390
+ ```typescript
1391
+ // Avoid
1392
+ // index.ts
1393
+ export * from "./Button";
1394
+ export * from "./Card";
1395
+
1396
+ // Prefer explicit imports
1397
+ import { Button } from "@/components/Button";
1398
+ import { Card } from "@/components/Card";
1399
+ ```
1400
+
1401
+ ### Named Exports
1402
+
1403
+ Always use named exports (not default exports) in redesign code.
1404
+
1405
+ ```typescript
1406
+ // Good
1407
+ export function Button(props: ButtonProps) {}
1408
+
1409
+ // Avoid
1410
+ export default function Button(props: ButtonProps) {}
1411
+ ```
1412
+
1413
+ ## API Folder Structure
1414
+
1415
+ ```text
1416
+ api/
1417
+ ├── useGetFeatures.ts # GET requests
1418
+ ├── useCreateFeature.ts # POST requests
1419
+ ├── useUpdateFeature.ts # PUT/PATCH requests
1420
+ ├── useDeleteFeature.ts # DELETE requests
1421
+ └── schemas.ts # Zod schemas (optional)
1422
+ ```
1423
+
1424
+ ## Utils Folder Guidelines
1425
+
1426
+ - Keep utilities pure functions when possible
1427
+ - Co-locate tests with utilities
1428
+ - Export individual functions (not as objects)
1429
+
1430
+ ```typescript
1431
+ // Good
1432
+ // utils/formatFeatureName.ts
1433
+ export function formatFeatureName(name: string): string {
1434
+ return name.trim().toUpperCase();
1435
+ }
1436
+
1437
+ // utils/formatFeatureName.test.ts
1438
+ import { formatFeatureName } from "./formatFeatureName";
1439
+
1440
+ describe("formatFeatureName", () => {
1441
+ it("should format name correctly", () => {
1442
+ expect(formatFeatureName(" test ")).toBe("TEST");
1443
+ });
1444
+ });
1445
+ ```
1446
+
1447
+ <!-- Source: .ruler/frontend/imports.md -->
1448
+
1449
+ # Import Standards
1450
+
1451
+ ## Component Wrapper Pattern
1452
+
1453
+ Many projects wrap third-party UI library components to add app-specific functionality. This can be enforced via ESLint rules.
1454
+
1455
+ ## Restricted MUI Imports
1456
+
1457
+ ### ❌ Component Restrictions (Example)
1458
+
1459
+ Some projects restrict direct imports of certain components from `@mui/material`:
1460
+
1461
+ ```typescript
1462
+ // ❌ Forbidden
1463
+ import {
1464
+ Avatar,
1465
+ Accordion,
1466
+ AccordionDetails,
1467
+ AccordionSummary,
1468
+ Badge,
1469
+ Button,
1470
+ Card,
1471
+ Chip,
1472
+ Dialog,
1473
+ DialogTitle,
1474
+ Divider,
1475
+ Drawer,
1476
+ FilledInput,
1477
+ Icon,
1478
+ IconButton,
1479
+ InputBase,
1480
+ List,
1481
+ ListItem,
1482
+ ListItemButton,
1483
+ ListItemIcon,
1484
+ ListItemText,
1485
+ Modal,
1486
+ OutlinedInput,
1487
+ Rating,
1488
+ Slider,
1489
+ TextField,
1490
+ Typography,
1491
+ Tab,
1492
+ Tabs,
1493
+ SvgIcon,
1494
+ } from "@mui/material";
1495
+ ```
1496
+
1497
+ ### ✅ Use Internal Wrappers
1498
+
1499
+ ```typescript
1500
+ // ✅ Correct - Use project wrappers
1501
+ import { Button } from "@clipboard-health/ui-components/Button";
1502
+ import { IconButton } from "@clipboard-health/ui-components/IconButton";
1503
+ import { LoadingButton } from "@clipboard-health/ui-components/LoadingButton";
1504
+ ```
1505
+
1506
+ ### Rationale
1507
+
1508
+ 1. Wrappers provide app-specific functionality and consistent behavior
1509
+ 2. Use `sx` prop for custom styles with theme access
1510
+ 3. Prefer app-specific dialog components over generic Modal components
1511
+
1512
+ ## Icon Restrictions
1513
+
1514
+ ### ❌ Third-Party Icons (Example)
1515
+
1516
+ ```typescript
1517
+ // ❌ Avoid direct imports from icon libraries
1518
+ import SearchIcon from "@mui/icons-material/Search";
1519
+ import CloseIcon from "@mui/icons-material/Close";
1520
+ import AddIcon from "@mui/icons-material/Add";
1521
+ ```
1522
+
1523
+ ### ✅ Use Project Icon Component
1524
+
1525
+ ```typescript
1526
+ // ✅ Correct - Use project's icon system
1527
+ import { Icon } from '@clipboard-health/ui-components';
1528
+
1529
+ <Icon type="search" size="large" />
1530
+ <Icon type="close" size="medium" />
1531
+ <Icon type="plus" size="small" />
1532
+ ```
1533
+
1534
+ Many projects maintain their own icon system for consistency and customization.
1535
+
1536
+ ## Allowed MUI Imports
1537
+
1538
+ ### ✅ Safe to Import from MUI
1539
+
1540
+ These components can be imported directly:
1541
+
1542
+ ```typescript
1543
+ import {
1544
+ Box,
1545
+ Stack,
1546
+ Container,
1547
+ Grid,
1548
+ Paper,
1549
+ Skeleton,
1550
+ CircularProgress,
1551
+ LinearProgress,
1552
+ BottomNavigation,
1553
+ BottomNavigationAction,
1554
+ ThemeProvider,
1555
+ useTheme,
1556
+ useMediaQuery,
1557
+ } from "@mui/material";
1558
+ ```
1559
+
1560
+ Layout and utility components are generally safe.
1561
+
1562
+ ## Path Aliases
1563
+
1564
+ ### Use @ Prefix for Absolute Imports
1565
+
1566
+ ```typescript
1567
+ // ✅ Use path aliases configured in tsconfig
1568
+ import { formatDate } from "@/lib/dates";
1569
+ import { useUser } from "@/features/user/hooks/useUser";
1570
+ import { Button } from "@clipboard-health/ui-components/Button";
1571
+ import { theme } from "@/theme";
1572
+
1573
+ // ❌ Avoid relative paths to distant folders
1574
+ import { formatDate } from "../../../lib/dates";
1575
+ ```
1576
+
1577
+ ### Relative Imports for Same Feature
1578
+
1579
+ ```typescript
1580
+ // ✅ Relative imports within the same feature
1581
+ import { FeatureCard } from "./components/FeatureCard";
1582
+ import { useFeatureData } from "./hooks/useFeatureData";
1583
+ import { FEATURE_PATHS } from "./paths";
1584
+ import { type FeatureData } from "./types";
1585
+ ```
1586
+
1587
+ ## Import Grouping
1588
+
1589
+ ### Standard Order
1590
+
1591
+ ```typescript
1592
+ // 1. External dependencies (React, third-party libraries)
1593
+ import { useState, useEffect, useMemo } from "react";
1594
+ import { useQuery } from "@tanstack/react-query";
1595
+ import { parseISO, format } from "date-fns";
1596
+ import { z } from "zod";
1597
+
1598
+ // 2. Internal absolute imports (via path aliases)
1599
+ import { Button, Icon } from "@clipboard-health/ui-components";
1600
+ import { theme } from "@/theme";
1601
+ import { formatDate } from "@/lib/dates";
1602
+ import { useUser } from "@/features/user/hooks/useUser";
1603
+ import { APP_PATHS } from "@/constants/paths";
1604
+
1605
+ // 3. Relative imports (same feature/module)
1606
+ import { useFeatureData } from "./hooks/useFeatureData";
1607
+ import { FeatureCard } from "./components/FeatureCard";
1608
+ import { FEATURE_PATHS } from "./paths";
1609
+ import { type FeatureData, type FeatureOptions } from "./types";
1610
+ ```
1611
+
1612
+ ### Blank Lines Between Groups
1613
+
1614
+ - Add blank line between each group
1615
+ - Helps with readability and organization
1616
+ - ESLint/Prettier can auto-format this
1617
+
1618
+ ## Type Imports
1619
+
1620
+ ### Inline Type Imports
1621
+
1622
+ ```typescript
1623
+ // ✅ Preferred: Inline type imports
1624
+ import { type User, type UserOptions } from "./types";
1625
+ import { formatUser } from "./utils";
1626
+ ```
1627
+
1628
+ ### Separate Type Imports
1629
+
1630
+ ```typescript
1631
+ // ✅ Also acceptable
1632
+ import type { User, UserOptions } from "./types";
1633
+ import { formatUser } from "./utils";
1634
+ ```
1635
+
1636
+ ## Barrel Exports (index.ts)
1637
+
1638
+ ### Consider Avoiding Barrel Exports
1639
+
1640
+ ```typescript
1641
+ // ❌ Barrel exports can slow build times
1642
+ // index.ts
1643
+ export * from "./Button";
1644
+ export * from "./Card";
1645
+ ```
1646
+
1647
+ ### ✅ Use Explicit Imports
1648
+
1649
+ ```typescript
1650
+ // ✅ Import directly from files for better tree-shaking
1651
+ import { Button } from "@clipboard-health/ui-components/Button";
1652
+ import { Card } from "@clipboard-health/ui-components/Card";
1653
+ ```
1654
+
1655
+ Note: Barrel exports can cause issues with circular dependencies and slow down builds, especially in large projects.
1656
+
1657
+ ## Dynamic Imports
1658
+
1659
+ ### Code Splitting
1660
+
1661
+ ```typescript
1662
+ // For large components or routes
1663
+ const HeavyComponent = lazy(() => import("./HeavyComponent"));
1664
+
1665
+ // In component
1666
+ <Suspense fallback={<Loading />}>
1667
+ <HeavyComponent />
1668
+ </Suspense>;
1669
+ ```
1670
+
1671
+ ## Common Import Patterns
1672
+
1673
+ ### API Utilities
1674
+
1675
+ ```typescript
1676
+ import { useQuery } from "@/lib/api/hooks";
1677
+ import { api } from "@/lib/api/client";
1678
+ ```
1679
+
1680
+ ### React Query
1681
+
1682
+ ```typescript
1683
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
1684
+ import { type UseQueryOptions, type UseQueryResult } from "@tanstack/react-query";
1685
+ ```
1686
+
1687
+ ### Routing
1688
+
1689
+ ```typescript
1690
+ import { useHistory, useLocation, useParams } from "react-router-dom";
1691
+ import { type LocationState } from "history";
1692
+ ```
1693
+
1694
+ ### Date Utilities
1695
+
1696
+ ```typescript
1697
+ import { parseISO, format, addDays, isBefore } from "date-fns";
1698
+ ```
1699
+
1700
+ ### Validation
1701
+
1702
+ ```typescript
1703
+ import { z } from "zod";
1704
+ import { zodResolver } from "@hookform/resolvers/zod";
1705
+ ```
1706
+
1707
+ ## ESLint Configuration
1708
+
1709
+ Import restrictions can be enforced with ESLint's `no-restricted-imports` rule:
1710
+
1711
+ ```javascript
1712
+ module.exports = {
1713
+ rules: {
1714
+ "no-restricted-imports": [
1715
+ "error",
1716
+ {
1717
+ paths: [
1718
+ {
1719
+ name: "@mui/material",
1720
+ importNames: ["Button", "TextField", "Dialog"],
1721
+ message: "Use wrapper components from @/components",
1722
+ },
1723
+ ],
1724
+ patterns: [
1725
+ {
1726
+ group: ["@mui/icons-material/*"],
1727
+ message: "Use project icon component instead",
1728
+ },
1729
+ ],
1730
+ },
1731
+ ],
1732
+ },
1733
+ };
1734
+ ```
1735
+
1736
+ ## Checking for Violations
1737
+
1738
+ ```bash
1739
+ # Lint your code
1740
+ npm run lint
1741
+
1742
+ # Auto-fix import issues
1743
+ npm run lint:fix
1744
+ ```
1745
+
1746
+ ## Migration Guide
1747
+
1748
+ ### When You See Import Errors
1749
+
1750
+ 1. **Wrapper Component Error**
1751
+ - Check if wrapper exists in `@/components/`
1752
+ - If yes, import from there
1753
+ - If no, discuss with team about creating wrapper
1754
+
1755
+ 2. **Icon Error**
1756
+ - Find equivalent in project icon system
1757
+ - Use project's icon component
1758
+ - Check available icon names in documentation
1759
+
1760
+ 3. **Deprecated Pattern Error**
1761
+ - Follow the suggested replacement pattern
1762
+ - Move to modern alternatives (e.g., `sx` prop instead of `styled`)
1763
+
1764
+ ## Summary
1765
+
1766
+ ✅ **DO**:
1767
+
1768
+ - Use project wrapper components instead of third-party components directly
1769
+ - Use project icon system for consistency
1770
+ - Use path aliases (e.g., `@/`) for absolute imports
1771
+ - Group imports by external, internal, relative
1772
+ - Consider avoiding barrel exports for better build performance
1773
+
1774
+ ❌ **DON'T**:
1775
+
1776
+ - Import third-party UI components directly if wrappers exist
1777
+ - Import icon libraries directly if project has custom icon system
1778
+ - Use deprecated styling patterns (check project guidelines)
1779
+ - Use relative imports for distant files
1780
+ - Create excessive barrel exports that hurt tree-shaking
1781
+
1782
+ <!-- Source: .ruler/frontend/performance.md -->
1783
+
1784
+ # Performance Standards
1785
+
1786
+ ## React Query Optimization
1787
+
1788
+ ### Stale Time Configuration
1789
+
1790
+ ```typescript
1791
+ // Set appropriate staleTime to avoid unnecessary refetches
1792
+ useGetQuery({
1793
+ url: "/api/resource",
1794
+ responseSchema: schema,
1795
+ staleTime: minutesToMilliseconds(5), // Don't refetch for 5 minutes
1796
+ cacheTime: minutesToMilliseconds(30), // Keep in cache for 30 minutes
1797
+ });
1798
+ ```
1799
+
1800
+ ### Conditional Fetching
1801
+
1802
+ ```typescript
1803
+ // Only fetch when conditions are met
1804
+ const { data } = useGetQuery({
1805
+ url: `/api/users/${userId}`,
1806
+ responseSchema: userSchema,
1807
+ enabled: isDefined(userId) && isFeatureEnabled, // Don't fetch until ready
1808
+ });
1809
+ ```
1810
+
1811
+ ### Query Cancellation
1812
+
1813
+ ```typescript
1814
+ export function usePaginatedData(params: Params) {
1815
+ const queryClient = useQueryClient();
1816
+
1817
+ return useInfiniteQuery({
1818
+ queryKey: ["data", params],
1819
+ queryFn: async ({ pageParam }) => {
1820
+ // Cancel previous in-flight requests
1821
+ await queryClient.cancelQueries({ queryKey: ["data"] });
1822
+
1823
+ const response = await get({
1824
+ url: "/api/data",
1825
+ queryParams: { cursor: pageParam, ...params },
1826
+ });
1827
+ return response.data;
1828
+ },
1829
+ getNextPageParam: (lastPage) => lastPage.nextCursor,
1830
+ });
1831
+ }
1832
+ ```
1833
+
1834
+ ### Prefetching
1835
+
1836
+ ```typescript
1837
+ export function usePreloadNextPage(nextUserId: string) {
1838
+ const queryClient = useQueryClient();
1839
+
1840
+ useEffect(() => {
1841
+ if (nextUserId) {
1842
+ // Prefetch next page in background
1843
+ queryClient.prefetchQuery({
1844
+ queryKey: ["user", nextUserId],
1845
+ queryFn: () => fetchUser(nextUserId),
1846
+ staleTime: minutesToMilliseconds(5),
1847
+ });
1848
+ }
1849
+ }, [nextUserId, queryClient]);
1850
+ }
1851
+ ```
1852
+
1853
+ ## React Optimization
1854
+
1855
+ ### useMemo for Expensive Computations
1856
+
1857
+ ```typescript
1858
+ export function DataList({ items }: { items: Item[] }) {
1859
+ // Memoize expensive sorting/filtering
1860
+ const sortedItems = useMemo(() => {
1861
+ return items.filter((item) => item.isActive).sort((a, b) => a.priority - b.priority);
1862
+ }, [items]);
1863
+
1864
+ return <List items={sortedItems} />;
1865
+ }
1866
+ ```
1867
+
1868
+ ### useCallback for Event Handlers
1869
+
1870
+ ```typescript
1871
+ export function ParentComponent() {
1872
+ const [selected, setSelected] = useState<string>();
1873
+
1874
+ // Memoize callback to prevent child re-renders
1875
+ const handleSelect = useCallback((id: string) => {
1876
+ setSelected(id);
1877
+ logEvent("ITEM_SELECTED", { id });
1878
+ }, []); // Stable reference
1879
+
1880
+ return <ChildComponent onSelect={handleSelect} />;
1881
+ }
1882
+ ```
1883
+
1884
+ ### React.memo for Pure Components
1885
+
1886
+ ```typescript
1887
+ import { memo } from "react";
1888
+
1889
+ interface ItemCardProps {
1890
+ item: Item;
1891
+ onSelect: (id: string) => void;
1892
+ }
1893
+
1894
+ // Memoize component to prevent unnecessary re-renders
1895
+ export const ItemCard = memo(function ItemCard({ item, onSelect }: ItemCardProps) {
1896
+ return (
1897
+ <Card onClick={() => onSelect(item.id)}>
1898
+ <h3>{item.name}</h3>
1899
+ </Card>
1900
+ );
1901
+ });
1902
+ ```
1903
+
1904
+ ### Avoid Inline Object/Array Creation
1905
+
1906
+ ```typescript
1907
+ // ❌ Creates new object on every render
1908
+ <Component style={{ padding: 8 }} />
1909
+ <Component items={[1, 2, 3]} />
1910
+
1911
+ // ✅ Define outside or use useMemo
1912
+ const style = { padding: 8 };
1913
+ const items = [1, 2, 3];
1914
+ <Component style={style} items={items} />
1915
+
1916
+ // ✅ Or use useMemo for dynamic values
1917
+ const style = useMemo(() => ({ padding: spacing }), [spacing]);
1918
+ ```
1919
+
1920
+ ## List Rendering
1921
+
1922
+ ### Key Prop for Lists
1923
+
1924
+ ```typescript
1925
+ // ✅ Use stable, unique keys
1926
+ items.map((item) => <ItemCard key={item.id} item={item} />);
1927
+
1928
+ // ❌ Don't use index as key (unless list never changes)
1929
+ items.map((item, index) => <ItemCard key={index} item={item} />);
1930
+ ```
1931
+
1932
+ ### Virtualization for Long Lists
1933
+
1934
+ ```typescript
1935
+ import { FixedSizeList } from "react-window";
1936
+
1937
+ export function VirtualizedList({ items }: { items: Item[] }) {
1938
+ return (
1939
+ <FixedSizeList height={600} itemCount={items.length} itemSize={80} width="100%">
1940
+ {({ index, style }) => (
1941
+ <div style={style}>
1942
+ <ItemCard item={items[index]} />
1943
+ </div>
1944
+ )}
1945
+ </FixedSizeList>
1946
+ );
1947
+ }
1948
+ ```
1949
+
1950
+ ### Pagination Instead of Infinite Data
1951
+
1952
+ ```typescript
1953
+ // For large datasets, use pagination
1954
+ export function PaginatedList() {
1955
+ const [page, setPage] = useState(1);
1956
+ const pageSize = 20;
1957
+
1958
+ const { data, isLoading } = useGetPaginatedData({
1959
+ page,
1960
+ pageSize,
1961
+ });
1962
+
1963
+ return (
1964
+ <>
1965
+ <List items={data?.items} />
1966
+ <Pagination page={page} total={data?.total} pageSize={pageSize} onChange={setPage} />
1967
+ </>
1968
+ );
1969
+ }
1970
+ ```
1971
+
1972
+ ## Data Fetching Patterns
1973
+
1974
+ ### Parallel Queries
1975
+
1976
+ ```typescript
1977
+ export function useParallelData() {
1978
+ // Fetch in parallel
1979
+ const users = useGetUsers();
1980
+ const shifts = useGetShifts();
1981
+ const workplaces = useGetWorkplaces();
1982
+
1983
+ // All queries run simultaneously
1984
+ const isLoading = users.isLoading || shifts.isLoading || workplaces.isLoading;
1985
+
1986
+ return {
1987
+ users: users.data,
1988
+ shifts: shifts.data,
1989
+ workplaces: workplaces.data,
1990
+ isLoading,
1991
+ };
1992
+ }
1993
+ ```
1994
+
1995
+ ### Dependent Queries
1996
+
1997
+ ```typescript
1998
+ export function useDependentData(userId?: string) {
1999
+ // First query
2000
+ const { data: user } = useGetUser(userId, {
2001
+ enabled: isDefined(userId),
2002
+ });
2003
+
2004
+ // Second query depends on first
2005
+ const { data: shifts } = useGetUserShifts(user?.id, {
2006
+ enabled: isDefined(user?.id), // Only fetch when user is loaded
2007
+ });
2008
+
2009
+ return { user, shifts };
2010
+ }
2011
+ ```
2012
+
2013
+ ### Debouncing Search Queries
2014
+
2015
+ ```typescript
2016
+ import { useDebouncedValue } from "@/lib/hooks";
2017
+
2018
+ export function SearchComponent() {
2019
+ const [searchTerm, setSearchTerm] = useState("");
2020
+ const debouncedSearch = useDebouncedValue(searchTerm, 300);
2021
+
2022
+ const { data } = useSearchQuery(debouncedSearch, {
2023
+ enabled: debouncedSearch.length > 2,
2024
+ });
2025
+
2026
+ return (
2027
+ <>
2028
+ <input value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} />
2029
+ <Results data={data} />
2030
+ </>
2031
+ );
2032
+ }
2033
+ ```
2034
+
2035
+ ## Code Splitting
2036
+
2037
+ ### Route-Based Splitting
2038
+
2039
+ ```typescript
2040
+ import { lazy, Suspense } from "react";
2041
+
2042
+ // Lazy load route components
2043
+ const ShiftDetailsPage = lazy(() => import("./Shift/DetailsPage"));
2044
+ const ProfilePage = lazy(() => import("./Profile/Page"));
2045
+
2046
+ export function Router() {
2047
+ return (
2048
+ <Suspense fallback={<LoadingScreen />}>
2049
+ <Routes>
2050
+ <Route path="/shifts/:id" element={<ShiftDetailsPage />} />
2051
+ <Route path="/profile" element={<ProfilePage />} />
2052
+ </Routes>
2053
+ </Suspense>
2054
+ );
2055
+ }
2056
+ ```
2057
+
2058
+ ### Component-Based Splitting
2059
+
2060
+ ```typescript
2061
+ // Split large components that aren't immediately needed
2062
+ const HeavyChart = lazy(() => import("./HeavyChart"));
2063
+
2064
+ export function Dashboard() {
2065
+ const [showChart, setShowChart] = useState(false);
2066
+
2067
+ return (
2068
+ <div>
2069
+ <Summary />
2070
+ {showChart && (
2071
+ <Suspense fallback={<ChartSkeleton />}>
2072
+ <HeavyChart />
2073
+ </Suspense>
2074
+ )}
2075
+ </div>
2076
+ );
2077
+ }
2078
+ ```
2079
+
2080
+ ## Image Optimization
2081
+
2082
+ ### Lazy Loading Images
2083
+
2084
+ ```typescript
2085
+ <img
2086
+ src={imageUrl}
2087
+ alt={alt}
2088
+ loading="lazy" // Native lazy loading
2089
+ />
2090
+ ```
2091
+
2092
+ ### Responsive Images
2093
+
2094
+ ```typescript
2095
+ <img
2096
+ srcSet={`
2097
+ ${image_small} 480w,
2098
+ ${image_medium} 800w,
2099
+ ${image_large} 1200w
2100
+ `}
2101
+ sizes="(max-width: 480px) 480px, (max-width: 800px) 800px, 1200px"
2102
+ src={image_medium}
2103
+ alt={alt}
2104
+ />
2105
+ ```
2106
+
2107
+ ## State Management
2108
+
2109
+ ### Avoid Prop Drilling
2110
+
2111
+ ```typescript
2112
+ // ❌ Prop drilling
2113
+ <Parent>
2114
+ <Child data={data}>
2115
+ <GrandChild data={data}>
2116
+ <GreatGrandChild data={data} />
2117
+ </GrandChild>
2118
+ </Child>
2119
+ </Parent>;
2120
+
2121
+ // ✅ Use context for deeply nested data
2122
+ const DataContext = createContext<Data | undefined>(undefined);
2123
+
2124
+ <DataContext.Provider value={data}>
2125
+ <Parent>
2126
+ <Child>
2127
+ <GrandChild>
2128
+ <GreatGrandChild />
2129
+ </GrandChild>
2130
+ </Child>
2131
+ </Parent>
2132
+ </DataContext.Provider>;
2133
+ ```
2134
+
2135
+ ### Local State Over Global State
2136
+
2137
+ ```typescript
2138
+ // ✅ Keep state as local as possible
2139
+ export function FormComponent() {
2140
+ const [formData, setFormData] = useState({}); // Local state
2141
+ // Only lift state up when needed by multiple components
2142
+ }
2143
+ ```
2144
+
2145
+ ## Bundle Size Optimization
2146
+
2147
+ ### Tree Shaking
2148
+
2149
+ ```typescript
2150
+ // ✅ Import only what you need
2151
+ import { format } from "date-fns";
2152
+
2153
+ // ❌ Imports entire library
2154
+ import * as dateFns from "date-fns";
2155
+ ```
2156
+
2157
+ ### Avoid Large Dependencies
2158
+
2159
+ ```typescript
2160
+ // Check bundle size before adding dependencies
2161
+ // Use lighter alternatives when possible
2162
+
2163
+ // ❌ Heavy library for simple task
2164
+ import moment from "moment";
2165
+
2166
+ // ✅ Lighter alternative
2167
+ import { format } from "date-fns";
2168
+ ```
2169
+
2170
+ ## Monitoring Performance
2171
+
2172
+ ### React DevTools Profiler
2173
+
2174
+ ```typescript
2175
+ // Wrap components to profile
2176
+ <Profiler id="ShiftList" onRender={onRenderCallback}>
2177
+ <ShiftList />
2178
+ </Profiler>
2179
+ ```
2180
+
2181
+ ### Web Vitals
2182
+
2183
+ ```typescript
2184
+ import { getCLS, getFID, getFCP, getLCP, getTTFB } from "web-vitals";
2185
+
2186
+ // Track Core Web Vitals
2187
+ getCLS(console.log);
2188
+ getFID(console.log);
2189
+ getFCP(console.log);
2190
+ getLCP(console.log);
2191
+ getTTFB(console.log);
2192
+ ```
2193
+
2194
+ ## Best Practices Summary
2195
+
2196
+ ### ✅ DO
2197
+
2198
+ - Set appropriate `staleTime` and `cacheTime` for queries
2199
+ - Use `useMemo` for expensive computations
2200
+ - Use `useCallback` for callbacks passed to children
2201
+ - Use React.memo for pure components
2202
+ - Virtualize long lists
2203
+ - Lazy load routes and heavy components
2204
+ - Use pagination for large datasets
2205
+ - Debounce search inputs
2206
+ - Cancel queries when component unmounts
2207
+ - Prefetch data when predictable
2208
+
2209
+ ### ❌ DON'T
2210
+
2211
+ - Fetch data unnecessarily
2212
+ - Create objects/arrays inline in props
2213
+ - Use index as key for dynamic lists
2214
+ - Render all items in very long lists
2215
+ - Import entire libraries when only need parts
2216
+ - Keep all state global
2217
+ - Ignore performance warnings in console
2218
+ - Skip memoization for expensive operations
2219
+
2220
+ <!-- Source: .ruler/frontend/react-patterns.md -->
2221
+
2222
+ # React Component Patterns
2223
+
2224
+ ## Core Principles
2225
+
2226
+ - **Storybook is the single source of truth** for UI components, not Figma
2227
+ - **Named exports only** (prefer named exports over default exports)
2228
+ - **Explicit types** for all props and return values
2229
+ - **Handle all states**: loading, error, empty, success
2230
+ - **Composition over configuration** - prefer children over complex props
2231
+
2232
+ ## Component Structure
2233
+
2234
+ Follow this consistent structure for all components:
2235
+
2236
+ ```typescript
2237
+ // 1. Imports (grouped with blank lines between groups)
2238
+ import { useState, useMemo, useCallback } from "react";
2239
+ import { Box, Typography } from "@mui/material";
2240
+
2241
+ import { useGetUser } from "@/api/hooks/useGetUser";
2242
+ import { formatDate } from "@/utils/date";
2243
+
2244
+ import { ChildComponent } from "./ChildComponent";
2245
+
2246
+ // 2. Types (interfaces for props, types for unions)
2247
+ interface UserCardProps {
2248
+ userId: string;
2249
+ onAction: (action: string) => void;
2250
+ isHighlighted?: boolean;
2251
+ }
2252
+
2253
+ // 3. Component definition
2254
+ export function UserCard({ userId, onAction, isHighlighted = false }: UserCardProps) {
2255
+ // A. React Query hooks first
2256
+ const { data: user, isLoading, isError } = useGetUser(userId);
2257
+
2258
+ // B. Local state
2259
+ const [isExpanded, setIsExpanded] = useState(false);
2260
+
2261
+ // C. Derived values (with useMemo for expensive computations)
2262
+ const displayName = useMemo(
2263
+ () => (user ? `${user.firstName} ${user.lastName}` : "Unknown"),
2264
+ [user]
2265
+ );
2266
+
2267
+ // D. Event handlers (with useCallback when passed to children)
2268
+ const handleToggle = useCallback(() => {
2269
+ setIsExpanded((prev) => !prev);
2270
+ onAction("toggle");
2271
+ }, [onAction]);
2272
+
2273
+ // E. Early returns for loading/error states
2274
+ if (isLoading) return <LoadingState />;
2275
+ if (isError) return <ErrorState message="Failed to load user" />;
2276
+ if (!user) return <EmptyState message="User not found" />;
2277
+
2278
+ // F. Render
2279
+ return (
2280
+ <Box sx={{ padding: 2, backgroundColor: isHighlighted ? "primary.light" : "background.paper" }}>
2281
+ <Typography variant="h6">{displayName}</Typography>
2282
+ <ChildComponent onClick={handleToggle} />
2283
+ </Box>
2284
+ );
2285
+ }
2286
+ ```
2287
+
2288
+ ## Why This Structure?
2289
+
2290
+ 1. **Imports grouped** - Easy to see dependencies
2291
+ 2. **Types first** - Documents component API
2292
+ 3. **Hooks at top** - React rules of hooks
2293
+ 4. **Derived values next** - Shows data flow
2294
+ 5. **Handlers together** - Easy to find event logic
2295
+ 6. **Early returns** - Fail fast, reduce nesting
2296
+ 7. **Render last** - Main component logic
2297
+
2298
+ ## Component Naming
2299
+
2300
+ - **PascalCase** for components: `UserProfile`, `DataCard`
2301
+ - **camelCase** for hooks and utilities: `useUserData`, `formatDate`
2302
+ - Prefer named exports over default exports for better refactoring support
2303
+
2304
+ ## Props
2305
+
2306
+ - Always define explicit prop types
2307
+ - Use destructuring with types
2308
+ - Avoid prop spreading unless wrapping a component
2309
+
2310
+ ```typescript
2311
+ // Good
2312
+ interface ButtonProps {
2313
+ onClick: () => void;
2314
+ label: string;
2315
+ disabled?: boolean;
2316
+ }
2317
+
2318
+ export function Button({ onClick, label, disabled = false }: ButtonProps) {
2319
+ return (
2320
+ <button onClick={onClick} disabled={disabled}>
2321
+ {label}
2322
+ </button>
2323
+ );
2324
+ }
2325
+
2326
+ // Acceptable for wrappers
2327
+ export function CustomButton(props: ButtonProps) {
2328
+ return <ButtonBase {...props} LinkComponent={InternalLink} />;
2329
+ }
2330
+ ```
2331
+
2332
+ ## Wrappers
2333
+
2334
+ - Wrap third-party components to add app-specific functionality
2335
+ - Example: Wrapping a third-party Button with custom link handling
2336
+
2337
+ ```typescript
2338
+ import {
2339
+ Button as ButtonBase,
2340
+ type ButtonProps as ButtonPropsBase,
2341
+ } from "@some-ui-library/components";
2342
+ import { type LocationState } from "history";
2343
+
2344
+ import { InternalLink } from "./InternalLink";
2345
+
2346
+ interface ButtonProps extends Omit<ButtonPropsBase, "LinkComponent"> {
2347
+ locationState?: LocationState;
2348
+ }
2349
+
2350
+ export function Button(props: ButtonProps) {
2351
+ return <ButtonBase {...props} LinkComponent={InternalLink} />;
2352
+ }
2353
+ ```
2354
+
2355
+ ## State Management
2356
+
2357
+ - Use `useState` for local component state
2358
+ - Use `useMemo` for expensive computations
2359
+ - Use `useCallback` for functions passed to children
2360
+ - Lift state up when needed by multiple components
2361
+
2362
+ ## Conditional Rendering
2363
+
2364
+ ```typescript
2365
+ // Early returns for loading/error states
2366
+ if (isLoading) return <LoadingState />;
2367
+ if (isError) return <ErrorState />;
2368
+
2369
+ // Ternary for simple conditions
2370
+ return isActive ? <ActiveView /> : <InactiveView />;
2371
+
2372
+ // && for conditional rendering
2373
+ return <div>{hasData && <DataDisplay data={data} />}</div>;
2374
+ ```
2375
+
2376
+ ## Component Composition
2377
+
2378
+ Prefer composition over complex props. Build small, focused components that compose together.
2379
+
2380
+ ```typescript
2381
+ // ✅ Good - Composition pattern
2382
+ interface CardProps {
2383
+ children: ReactNode;
2384
+ }
2385
+
2386
+ export function Card({ children }: CardProps) {
2387
+ return <Box sx={{ padding: 3, borderRadius: 2, boxShadow: 1 }}>{children}</Box>;
2388
+ }
2389
+
2390
+ export function CardHeader({ children }: { children: ReactNode }) {
2391
+ return (
2392
+ <Box sx={{ marginBottom: 2 }}>
2393
+ <Typography variant="h6">{children}</Typography>
2394
+ </Box>
2395
+ );
2396
+ }
2397
+
2398
+ export function CardContent({ children }: { children: ReactNode }) {
2399
+ return <Box>{children}</Box>;
2400
+ }
2401
+
2402
+ // Usage - Flexible and composable
2403
+ <Card>
2404
+ <CardHeader>User Profile</CardHeader>
2405
+ <CardContent>
2406
+ <UserDetails user={user} />
2407
+ <UserActions onEdit={handleEdit} />
2408
+ </CardContent>
2409
+ </Card>;
2410
+
2411
+ // ❌ Bad - Complex props that limit flexibility
2412
+ interface ComplexCardProps {
2413
+ title: string;
2414
+ subtitle?: string;
2415
+ actions?: ReactNode;
2416
+ content: ReactNode;
2417
+ variant?: "default" | "highlighted" | "error";
2418
+ showBorder?: boolean;
2419
+ elevation?: number;
2420
+ }
2421
+
2422
+ export function ComplexCard(props: ComplexCardProps) {
2423
+ // Too many props, hard to extend
2424
+ // ...
2425
+ }
2426
+ ```
2427
+
2428
+ ## Children Patterns
2429
+
2430
+ ### Render Props Pattern
2431
+
2432
+ Use when child components need access to parent state:
2433
+
2434
+ ```typescript
2435
+ interface DataProviderProps {
2436
+ children: (data: Data, isLoading: boolean) => ReactNode;
2437
+ }
2438
+
2439
+ export function DataProvider({ children }: DataProviderProps) {
2440
+ const { data, isLoading } = useGetData();
2441
+
2442
+ return <>{children(data, isLoading)}</>;
2443
+ }
2444
+
2445
+ // Usage
2446
+ <DataProvider>
2447
+ {(data, isLoading) => (isLoading ? <Loading /> : <DataDisplay data={data} />)}
2448
+ </DataProvider>;
2449
+ ```
2450
+
2451
+ ### Compound Components Pattern
2452
+
2453
+ For components that work together:
2454
+
2455
+ ```typescript
2456
+ interface TabsContextValue {
2457
+ activeTab: string;
2458
+ setActiveTab: (tab: string) => void;
2459
+ }
2460
+
2461
+ const TabsContext = createContext<TabsContextValue | undefined>(undefined);
2462
+
2463
+ export function Tabs({ children, defaultTab }: { children: ReactNode; defaultTab: string }) {
2464
+ const [activeTab, setActiveTab] = useState(defaultTab);
2465
+
2466
+ return (
2467
+ <TabsContext.Provider value={{ activeTab, setActiveTab }}>
2468
+ <Box>{children}</Box>
2469
+ </TabsContext.Provider>
2470
+ );
2471
+ }
2472
+
2473
+ export function TabList({ children }: { children: ReactNode }) {
2474
+ return <Box sx={{ display: "flex", gap: 1 }}>{children}</Box>;
2475
+ }
2476
+
2477
+ export function Tab({ value, children }: { value: string; children: ReactNode }) {
2478
+ const context = useContext(TabsContext);
2479
+ if (!context) throw new Error("Tab must be used within Tabs");
2480
+
2481
+ const { activeTab, setActiveTab } = context;
2482
+ const isActive = activeTab === value;
2483
+
2484
+ return (
2485
+ <Button onClick={() => setActiveTab(value)} variant={isActive ? "contained" : "text"}>
2486
+ {children}
2487
+ </Button>
2488
+ );
2489
+ }
2490
+
2491
+ export function TabPanel({ value, children }: { value: string; children: ReactNode }) {
2492
+ const context = useContext(TabsContext);
2493
+ if (!context) throw new Error("TabPanel must be used within Tabs");
2494
+
2495
+ if (context.activeTab !== value) return null;
2496
+ return <Box sx={{ padding: 2 }}>{children}</Box>;
2497
+ }
2498
+
2499
+ // Usage - Intuitive API
2500
+ <Tabs defaultTab="profile">
2501
+ <TabList>
2502
+ <Tab value="profile">Profile</Tab>
2503
+ <Tab value="settings">Settings</Tab>
2504
+ </TabList>
2505
+ <TabPanel value="profile">
2506
+ <ProfileContent />
2507
+ </TabPanel>
2508
+ <TabPanel value="settings">
2509
+ <SettingsContent />
2510
+ </TabPanel>
2511
+ </Tabs>;
2512
+ ```
2513
+
2514
+ ## Error Boundaries
2515
+
2516
+ Use error boundaries for graceful error handling:
2517
+
2518
+ ```typescript
2519
+ interface ErrorBoundaryProps {
2520
+ children: ReactNode;
2521
+ fallback?: ReactNode;
2522
+ }
2523
+
2524
+ interface ErrorBoundaryState {
2525
+ hasError: boolean;
2526
+ error?: Error;
2527
+ }
2528
+
2529
+ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
2530
+ constructor(props: ErrorBoundaryProps) {
2531
+ super(props);
2532
+ this.state = { hasError: false };
2533
+ }
2534
+
2535
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
2536
+ return { hasError: true, error };
2537
+ }
2538
+
2539
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
2540
+ console.error("Error caught by boundary:", error, errorInfo);
2541
+ // Log to error tracking service
2542
+ }
2543
+
2544
+ render() {
2545
+ if (this.state.hasError) {
2546
+ return this.props.fallback || <ErrorFallback error={this.state.error} />;
2547
+ }
2548
+
2549
+ return this.props.children;
2550
+ }
2551
+ }
2552
+
2553
+ // Usage
2554
+ <ErrorBoundary fallback={<ErrorPage />}>
2555
+ <App />
2556
+ </ErrorBoundary>;
2557
+ ```
2558
+
2559
+ ## Handling Lists
2560
+
2561
+ ### Key Props
2562
+
2563
+ Always use stable, unique keys (never index):
2564
+
2565
+ ```typescript
2566
+ // ✅ Good - Unique, stable ID
2567
+ {
2568
+ users.map((user) => <UserCard key={user.id} user={user} />);
2569
+ }
2570
+
2571
+ // ❌ Bad - Index as key (causes bugs when list changes)
2572
+ {
2573
+ users.map((user, index) => <UserCard key={index} user={user} />);
2574
+ }
2575
+
2576
+ // ✅ Acceptable - Composite key when no ID available
2577
+ {
2578
+ items.map((item) => <ItemCard key={`${item.type}-${item.name}`} item={item} />);
2579
+ }
2580
+ ```
2581
+
2582
+ ### Empty States
2583
+
2584
+ Always handle empty lists:
2585
+
2586
+ ```typescript
2587
+ export function UserList({ users }: { users: User[] }) {
2588
+ if (users.length === 0) {
2589
+ return (
2590
+ <EmptyState
2591
+ icon={<PersonIcon />}
2592
+ title="No users found"
2593
+ description="Try adjusting your search criteria"
2594
+ />
2595
+ );
2596
+ }
2597
+
2598
+ return (
2599
+ <Box>
2600
+ {users.map((user) => (
2601
+ <UserCard key={user.id} user={user} />
2602
+ ))}
2603
+ </Box>
2604
+ );
2605
+ }
2606
+ ```
2607
+
2608
+ ## Conditional Rendering Best Practices
2609
+
2610
+ ```typescript
2611
+ export function ItemCard({ item }: { item: Item }) {
2612
+ // ✅ Good - Early returns for invalid states
2613
+ if (!item) return null;
2614
+ if (item.isDeleted) return null;
2615
+
2616
+ // ✅ Good - Ternary for simple either/or
2617
+ const statusColor = item.isUrgent ? "error" : "default";
2618
+
2619
+ return (
2620
+ <Box>
2621
+ <Typography color={statusColor}>{item.title}</Typography>
2622
+
2623
+ {/* ✅ Good - && for optional elements */}
2624
+ {item.isFeatured && <FeaturedBadge />}
2625
+
2626
+ {/* ✅ Good - Ternary for alternate content */}
2627
+ {item.isAvailable ? <AvailableStatus /> : <UnavailableStatus />}
2628
+
2629
+ {/* ❌ Bad - Nested ternaries (hard to read) */}
2630
+ {item.status === "urgent" ? (
2631
+ <UrgentBadge />
2632
+ ) : item.status === "normal" ? (
2633
+ <NormalBadge />
2634
+ ) : (
2635
+ <DefaultBadge />
2636
+ )}
2637
+
2638
+ {/* ✅ Good - Extract to variable or switch */}
2639
+ <StatusBadge status={item.status} />
2640
+ </Box>
2641
+ );
2642
+ }
2643
+ ```
2644
+
2645
+ ## Performance Optimization
2646
+
2647
+ ### When to Use `useMemo`
2648
+
2649
+ ```typescript
2650
+ // ✅ Do - Expensive computation
2651
+ const sortedUsers = useMemo(() => [...users].sort((a, b) => a.name.localeCompare(b.name)), [users]);
2652
+
2653
+ // ❌ Don't - Simple operations (premature optimization)
2654
+ const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);
2655
+ ```
2656
+
2657
+ ### When to Use `useCallback`
2658
+
2659
+ ```typescript
2660
+ // ✅ Do - Function passed to memoized child
2661
+ const MemoizedChild = React.memo(ChildComponent);
2662
+
2663
+ function Parent() {
2664
+ const handleClick = useCallback(() => {
2665
+ console.log("clicked");
2666
+ }, []);
2667
+
2668
+ return <MemoizedChild onClick={handleClick} />;
2669
+ }
2670
+
2671
+ // ❌ Don't - Function not passed to children
2672
+ const handleSubmit = useCallback(() => {
2673
+ // No child components use this
2674
+ }, []);
2675
+ ```
2676
+
2677
+ <!-- Source: .ruler/frontend/react.md -->
2678
+
2679
+ # React
2680
+
2681
+ - Destructure props in function body rather than in function signature
2682
+ - Prefer inline JSX rather than extracting variables and functions as variables outside of JSX
2683
+ - Use useModalState for any showing/hiding functionality like dialogs
2684
+ - Utilize custom hooks to encapsulate and reuse stateful logic
2685
+ - When performing data-fetching in a custom hook, always use Zod to define any request and response schemas
2686
+ - Use react-hook-form for all form UIs and use zod resolver for form schema validation
2687
+ - Use date-fns for any Date based operations like formatting
2688
+
2689
+ <!-- Source: .ruler/frontend/styling.md -->
2690
+
2691
+ # Styling Standards
2692
+
2693
+ ## Technology Stack
2694
+
2695
+ - **Material UI (MUI)** with custom theme
2696
+ - **sx prop** for custom styles (NOT `styled()`)
2697
+ - Custom component wrappers for consistent UI
2698
+ - **Storybook** as single source of truth for UI components
2699
+
2700
+ ## Core Principles
2701
+
2702
+ 1. **Always use `sx` prop** - Never CSS/SCSS/SASS files
2703
+ 2. **Use theme tokens** - Never hardcode colors, spacing, or sizes
2704
+ 3. **Leverage meaningful tokens** - Use semantic names like `theme.palette.text.primary`, not `common.white`
2705
+ 4. **Type-safe theme access** - Use `sx={(theme) => ({...})}`, not string paths like `"text.secondary"`
2706
+ 5. **Follow spacing system** - Use indices 1-12 (4px-64px)
2707
+ 6. **Storybook is source of truth** - Check Storybook before Figma
2708
+
2709
+ ## Restricted Patterns
2710
+
2711
+ ### ❌ DO NOT USE
2712
+
2713
+ - `styled()` from MUI (deprecated in our codebase)
2714
+ - `makeStyles()` from MUI (deprecated)
2715
+ - CSS/SCSS/SASS files
2716
+ - Direct MUI icons from `@mui/icons-material`
2717
+ - Direct MUI components without wrappers (except layout primitives; see list below)
2718
+ - Inline styles via `style` prop (use `sx` instead)
2719
+ - String paths for theme tokens (not type-safe)
2720
+
2721
+ ### Rationale
2722
+
2723
+ 1. Component wrappers provide consistent behavior and app-specific functionality
2724
+ 2. `sx` prop provides type-safe access to theme tokens
2725
+ 3. Project-specific dialog components ensure consistent UX patterns
2726
+
2727
+ ## Storybook as Source of Truth
2728
+
2729
+ **Important:** Storybook reflects what's actually implemented, not Figma designs.
2730
+
2731
+ ### When Storybook Differs from Figma
2732
+
2733
+ 1. **Check Storybook first** - It shows real, implemented components
2734
+ 2. **Use closest existing variant** - Don't create one-off font sizes/colors
2735
+ 3. **Confirm changes are intentional** - Consult with team before updating components
2736
+ 4. **Create follow-up ticket** - If component needs updating but you're short on time
2737
+ 5. **Make changes system-wide** - Component updates should benefit entire app
2738
+
2739
+ ### Process
2740
+
2741
+ - **Minor differences** (font sizes, colors) → Stick to Storybook
2742
+ - **Component looks different** → Confirm with team, update component intentionally
2743
+ - **Missing component** → Check with team - it may exist with a different name
2744
+
2745
+ ## Use Internal Components
2746
+
2747
+ ### ✅ ALWAYS USE Project Wrappers
2748
+
2749
+ Instead of importing directly from `@mui/material`, use project wrappers:
2750
+
2751
+ ```typescript
2752
+ // ❌ Don't
2753
+ import { Button, IconButton } from "@mui/material";
2754
+
2755
+ // ✅ Do
2756
+ import { Button } from "@/components/Button";
2757
+ import { IconButton } from "@clipboard-health/ui-components";
2758
+ ```
2759
+
2760
+ ### Component Wrapper List (Example)
2761
+
2762
+ Common components that may have wrappers:
2763
+
2764
+ - `Button`, `LoadingButton`, `IconButton`
2765
+ - `Avatar`, `Accordion`, `Badge`
2766
+ - `Card`, `Chip`, `Dialog`
2767
+ - `Drawer`, `List`, `ListItem`
2768
+ - `Rating`, `Slider`, `Switch`
2769
+ - `TextField`, `Typography`, `Tab`, `Tabs`
2770
+
2771
+ Check your project's ESLint configuration for the full list.
2772
+
2773
+ ## Icons
2774
+
2775
+ ### Use Project Icon Component
2776
+
2777
+ ```typescript
2778
+ // ❌ Don't use third-party icons directly
2779
+ import SearchIcon from '@mui/icons-material/Search';
2780
+
2781
+ // ✅ Use project's icon system
2782
+ import { CbhIcon } from '@clipboard-health/ui-components';
2783
+
2784
+ <CbhIcon type="search" size="large" />
2785
+ <CbhIcon type="search-colored" size="medium" />
2786
+ ```
2787
+
2788
+ ### Icon Variants
2789
+
2790
+ - Many icon systems have variants for different states (e.g., `-colored` for active states)
2791
+ - Check your project's icon documentation for available variants
2792
+
2793
+ ## Styling with sx Prop
2794
+
2795
+ ### Basic Usage - Type-Safe Theme Access
2796
+
2797
+ ❌ **Never** hardcode values or use string paths:
2798
+
2799
+ ```typescript
2800
+ <Box
2801
+ sx={{
2802
+ backgroundColor: "red", // ❌ Raw color
2803
+ color: "#ADFF11", // ❌ Hex code
2804
+ padding: "16px", // ❌ Raw size
2805
+ color: "text.secondary", // ❌ String path (no TypeScript support)
2806
+ }}
2807
+ />
2808
+ ```
2809
+
2810
+ ✅ **Always** use theme with type safety:
2811
+
2812
+ ```typescript
2813
+ <Box
2814
+ sx={(theme) => ({
2815
+ backgroundColor: theme.palette.background.primary, // ✅ Semantic token
2816
+ color: theme.palette.text.secondary, // ✅ Type-safe
2817
+ padding: theme.spacing(4), // or just: padding: 4 // ✅ Spacing system
2818
+ })}
2819
+ />
2820
+ ```
2821
+
2822
+ ### Use Meaningful Tokens
2823
+
2824
+ ❌ **Avoid** non-descriptive tokens:
2825
+
2826
+ ```typescript
2827
+ theme.palette.common.white; // ❌ No context about usage
2828
+ theme.palette.green300; // ❌ Which green? When to use?
2829
+ ```
2830
+
2831
+ ✅ **Use** semantic tokens:
2832
+
2833
+ ```typescript
2834
+ theme.palette.background.tertiary; // ✅ Clear purpose
2835
+ theme.palette.instantPay.background; // ✅ Intent is obvious
2836
+ theme.palette.text.primary; // ✅ Meaningful
2837
+ ```
2838
+
2839
+ ## Spacing System
2840
+
2841
+ We use a strict index-based spacing system:
2842
+
2843
+ | Index | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
2844
+ | ----- | --- | --- | --- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |
2845
+ | Size | 4px | 6px | 8px | 12px | 16px | 20px | 24px | 32px | 40px | 48px | 56px | 64px |
2846
+
2847
+ **Usage:**
2848
+
2849
+ ```typescript
2850
+ <Box sx={{ padding: 5 }} /> // → 16px
2851
+ <Box sx={{ marginX: 4 }} /> // → 12px left and right
2852
+ <Box sx={{ gap: 3 }} /> // → 8px
2853
+ ```
2854
+
2855
+ **Use `rem` for fonts and heights:**
2856
+
2857
+ ```typescript
2858
+ <Box
2859
+ sx={(theme) => ({
2860
+ height: "3rem", // ✅ Scales with user zoom
2861
+ fontSize: theme.typography.body1.fontSize, // ✅ From theme
2862
+ padding: 5, // ✅ px prevents overflow when zoomed
2863
+ })}
2864
+ />
2865
+ ```
2866
+
2867
+ **Reasoning:** Users who adjust device-wide zoom need `rem` for fonts/heights to scale properly, but `px` spacing prevents layout overflow.
2868
+
2869
+ ## Theme Integration
2870
+
2871
+ ### Responsive Styles
2872
+
2873
+ ```typescript
2874
+ <Box
2875
+ sx={{
2876
+ width: {
2877
+ xs: "100%", // Mobile
2878
+ sm: "75%", // Tablet
2879
+ md: "50%", // Desktop
2880
+ },
2881
+ padding: {
2882
+ xs: 1,
2883
+ md: 3,
2884
+ },
2885
+ }}
2886
+ />
2887
+ ```
2888
+
2889
+ ### Pseudo-classes and Hover States
2890
+
2891
+ ```typescript
2892
+ <Box
2893
+ sx={(theme) => ({
2894
+ "&:hover": {
2895
+ backgroundColor: theme.palette.primary.dark,
2896
+ cursor: "pointer",
2897
+ },
2898
+ "&:disabled": {
2899
+ opacity: 0.5,
2900
+ },
2901
+ "&.active": {
2902
+ borderColor: theme.palette.primary.main,
2903
+ },
2904
+ })}
2905
+ />
2906
+ ```
2907
+
2908
+ ### Nested Selectors
2909
+
2910
+ ```typescript
2911
+ <Box
2912
+ sx={(theme) => ({
2913
+ "& .child-element": {
2914
+ color: theme.palette.text.secondary,
2915
+ },
2916
+ "& > div": {
2917
+ marginBottom: 1,
2918
+ },
2919
+ // Target nested MUI components
2920
+ "& .MuiTypography-root": {
2921
+ color: theme.palette.intent?.disabled.text,
2922
+ },
2923
+ })}
2924
+ />
2925
+ ```
2926
+
2927
+ ## Shorthand Properties
2928
+
2929
+ MUI provides shorthand properties - use full names, not abbreviations:
2930
+
2931
+ ✅ **Use full names:**
2932
+
2933
+ ```typescript
2934
+ <Box
2935
+ sx={{
2936
+ padding: 2, // ✅ Clear
2937
+ paddingX: 4, // ✅ Readable
2938
+ marginY: 2, // ✅ Explicit
2939
+ }}
2940
+ />
2941
+ ```
2942
+
2943
+ ❌ **Avoid abbreviations** (per naming conventions best practice):
2944
+
2945
+ ```typescript
2946
+ <Box
2947
+ sx={{
2948
+ p: 2, // ❌ Too terse
2949
+ px: 4, // ❌ Not clear
2950
+ my: 2, // ❌ What does this mean?
2951
+ }}
2952
+ />
2953
+ ```
2954
+
2955
+ [Full list of shorthand properties](https://mui.com/system/properties/)
2956
+
2957
+ ## Merging sx Props
2958
+
2959
+ For generic components accepting an `sx` prop, merge default styles with custom styles:
2960
+
2961
+ ```typescript
2962
+ // Option 1: Array syntax (MUI native)
2963
+ <Box
2964
+ sx={[
2965
+ (theme) => ({
2966
+ backgroundColor: theme.palette.background.tertiary,
2967
+ padding: 2,
2968
+ }),
2969
+ sx, // User's custom sx prop
2970
+ ]}
2971
+ {...restProps}
2972
+ />
2973
+
2974
+ // Option 2: Use a utility function if your project provides one
2975
+ import { mergeSxProps } from "@clipboard-health/ui-components";
2976
+
2977
+ <Box
2978
+ sx={mergeSxProps(defaultStyles, sx)}
2979
+ {...restProps}
2980
+ />;
2981
+ ```
2982
+
2983
+ ## Theme Access
2984
+
2985
+ ### Using useTheme Hook
2986
+
2987
+ ```typescript
2988
+ import { useTheme } from "@mui/material";
2989
+
2990
+ export function Component() {
2991
+ const theme = useTheme();
2992
+
2993
+ return (
2994
+ <Box sx={{ color: theme.palette.primary.main }}>
2995
+ {/* Your components */}
2996
+ </Box>
2997
+ );
2998
+ }
2999
+ ```
3000
+
3001
+ ### Theme Properties
3002
+
3003
+ ```typescript
3004
+ const theme = useTheme();
3005
+
3006
+ // Colors
3007
+ theme.palette.primary.main;
3008
+ theme.palette.secondary.main;
3009
+ theme.palette.error.main;
3010
+ theme.palette.text.primary;
3011
+ theme.palette.background.default;
3012
+
3013
+ // Spacing
3014
+ theme.spacing(1); // Project-defined (see spacing system above)
3015
+ theme.spacing(2); // Project-defined (see spacing system above)
3016
+
3017
+ // Typography
3018
+ theme.typography.h1;
3019
+ theme.typography.body1;
3020
+
3021
+ // Breakpoints
3022
+ theme.breakpoints.up("md");
3023
+ theme.breakpoints.down("sm");
3024
+ ```
3025
+
3026
+ ## Modal Patterns
3027
+
3028
+ ### ❌ Avoid Generic Modal
3029
+
3030
+ ```typescript
3031
+ // Avoid generic Modal when project has specific dialog components
3032
+ import { Modal } from "@mui/material";
3033
+ ```
3034
+
3035
+ ### ✅ Use Project-Specific Dialog Components
3036
+
3037
+ ```typescript
3038
+ // For mobile-friendly modals
3039
+ import { BottomSheet } from "@/components/BottomSheet";
3040
+
3041
+ // For full-screen views
3042
+ import { FullScreenDialog } from "@/components/FullScreenDialog";
3043
+
3044
+ // For standard dialogs
3045
+ import { Dialog } from "@/components/Dialog";
3046
+ ```
3047
+
3048
+ Check your project's component library for available dialog/modal components.
3049
+
3050
+ ## Layout Components
3051
+
3052
+ ### Use MUI Layout Components
3053
+
3054
+ These are safe to import directly from MUI:
3055
+
3056
+ ```typescript
3057
+ import { Box, Stack, Container, Grid } from '@mui/material';
3058
+
3059
+ // Stack for vertical/horizontal layouts
3060
+ <Stack spacing={2} direction="row">
3061
+ <Item />
3062
+ <Item />
3063
+ </Stack>
3064
+
3065
+ // Box for flexible containers
3066
+ <Box sx={{ display: 'flex', gap: 2 }}>
3067
+ <Child />
3068
+ </Box>
3069
+
3070
+ // Grid for responsive layouts
3071
+ <Grid container spacing={2}>
3072
+ <Grid item xs={12} md={6}>
3073
+ <Content />
3074
+ </Grid>
3075
+ </Grid>
3076
+ ```
3077
+
3078
+ ## MUI Theme Augmentation
3079
+
3080
+ ### Type Safety
3081
+
3082
+ Projects often augment MUI's theme with custom properties:
3083
+
3084
+ ```typescript
3085
+ // Example: Custom theme augmentation
3086
+ declare module "@mui/material/styles" {
3087
+ interface Theme {
3088
+ customSpacing: {
3089
+ large: string;
3090
+ xlarge: string;
3091
+ };
3092
+ }
3093
+ interface ThemeOptions {
3094
+ customSpacing?: {
3095
+ large?: string;
3096
+ xlarge?: string;
3097
+ };
3098
+ }
3099
+ }
3100
+ ```
3101
+
3102
+ Check your project's type definitions for available theme augmentations.
3103
+
3104
+ ## Common Patterns
3105
+
3106
+ ### Card with Custom Styling
3107
+
3108
+ ```typescript
3109
+ import { Card, CardContent } from "@mui/material";
3110
+
3111
+ <Card
3112
+ sx={{
3113
+ borderRadius: 2,
3114
+ boxShadow: 2,
3115
+ "&:hover": {
3116
+ boxShadow: 4,
3117
+ },
3118
+ }}
3119
+ >
3120
+ <CardContent>Content here</CardContent>
3121
+ </Card>;
3122
+ ```
3123
+
3124
+ ### Buttons with Theme Colors
3125
+
3126
+ ```typescript
3127
+ import { Button } from "@/components/Button";
3128
+
3129
+ <Button
3130
+ variant="contained"
3131
+ color="primary"
3132
+ sx={{
3133
+ textTransform: "none", // Override uppercase
3134
+ fontWeight: "bold",
3135
+ }}
3136
+ >
3137
+ Submit
3138
+ </Button>;
3139
+ ```
3140
+
3141
+ ### Conditional Styles
3142
+
3143
+ ```typescript
3144
+ <Box
3145
+ sx={(theme) => ({
3146
+ backgroundColor: isActive
3147
+ ? theme.palette.primary.main
3148
+ : theme.palette.grey[200],
3149
+ padding: 2,
3150
+ })}
3151
+ >
3152
+ {content}
3153
+ </Box>
3154
+ ```
3155
+
3156
+ ## Best Practices
3157
+
3158
+ - **Check Storybook first** - It's the single source of truth
3159
+ - **Use theme tokens** - Never hardcode colors/spacing
3160
+ - **Type-safe access** - Function form: `sx={(theme) => ({...})}`
3161
+ - **Meaningful tokens** - Semantic names over raw colors
3162
+ - **Spacing system** - Indices 1-12 (or `theme.spacing(n)`)
3163
+ - **Use shorthand props** - `paddingX`, `marginY` (full names, not `px`, `my`)
3164
+ - **Leverage pseudo-classes** - For hover, focus, disabled states
3165
+ - **Prefer `sx` over direct props** - `sx` takes priority and is more flexible
3166
+
3167
+ <!-- Source: .ruler/frontend/testing.md -->
3168
+
3169
+ # Testing Standards
3170
+
3171
+ ## The Testing Trophy Philosophy
3172
+
3173
+ Our testing strategy follows the **Testing Trophy** model:
3174
+
3175
+ ```text
3176
+ /\_
3177
+ /E2E\ ← End-to-End (smallest layer)
3178
+ /-----\
3179
+ / Integ \ ← Integration (largest layer - FOCUS HERE!)
3180
+ /---------\
3181
+ / Unit \ ← Unit Tests (helpers/utilities only)
3182
+ /-------------\
3183
+ / Static \ ← TypeScript + ESLint (foundation)
3184
+ ```
3185
+
3186
+ **Investment Priority:**
3187
+
3188
+ 1. **Static** (TypeScript/ESLint) - Free confidence, catches typos and type errors
3189
+ 2. **Integration** - Most valuable, test how components work together as users experience them
3190
+ 3. **Unit** - For pure helpers/utilities, NOT UI components
3191
+ 4. **E2E** - Critical user flows only, slow and expensive
3192
+
3193
+ **Key Principle:** Test as close to how users interact with your app as possible. Users don't shallow-render components or call functions in isolation - they interact with features.
3194
+
3195
+ ## Technology Stack
3196
+
3197
+ - **Vitest** for test runner
3198
+ - **@testing-library/react** for component testing
3199
+ - **@testing-library/user-event** for user interactions
3200
+ - **Mock Service Worker (MSW)** for API mocking
3201
+
3202
+ ## Test File Structure
3203
+
3204
+ ```typescript
3205
+ import { render, screen, waitFor } from "@testing-library/react";
3206
+ import { renderHook } from "@testing-library/react";
3207
+ import userEvent from "@testing-library/user-event";
3208
+ import { describe, expect, it, vi, beforeEach } from "vitest";
3209
+
3210
+ describe("ComponentName", () => {
3211
+ beforeEach(() => {
3212
+ vi.clearAllMocks();
3213
+ });
3214
+
3215
+ it("should render correctly", () => {
3216
+ render(<Component />);
3217
+ expect(screen.getByText("Expected")).toBeInTheDocument();
3218
+ });
3219
+
3220
+ it("should handle user interaction", async () => {
3221
+ const user = userEvent.setup();
3222
+ render(<Component />);
3223
+
3224
+ await user.click(screen.getByRole("button", { name: "Submit" }));
3225
+
3226
+ await waitFor(() => {
3227
+ expect(screen.getByText("Success")).toBeInTheDocument();
3228
+ });
3229
+ });
3230
+ });
3231
+ ```
3232
+
3233
+ ## Test Naming Conventions
3234
+
3235
+ ### Describe Blocks
3236
+
3237
+ - Use the component/function name: `describe('ComponentName', ...)`
3238
+ - Nest describe blocks for complex scenarios
3239
+
3240
+ ```typescript
3241
+ describe("ShiftCard", () => {
3242
+ describe("when shift is urgent", () => {
3243
+ it("should display urgent badge", () => {
3244
+ // ...
3245
+ });
3246
+ });
3247
+
3248
+ describe("when shift is booked", () => {
3249
+ it("should show booked status", () => {
3250
+ // ...
3251
+ });
3252
+ });
3253
+ });
3254
+ ```
3255
+
3256
+ ### Test Names
3257
+
3258
+ - Pattern: `'should [expected behavior] when [condition]'`
3259
+ - Be descriptive and specific
3260
+
3261
+ ```typescript
3262
+ // Good
3263
+ it("should show loading spinner when data is fetching", () => {});
3264
+ it("should display error message when API call fails", () => {});
3265
+ it("should enable submit button when form is valid", () => {});
3266
+
3267
+ // Avoid
3268
+ it("works", () => {});
3269
+ it("loading state", () => {});
3270
+ ```
3271
+
3272
+ ## Parameterized Tests
3273
+
3274
+ ### Using it.each
3275
+
3276
+ ```typescript
3277
+ it.each([
3278
+ { input: { isUrgent: true }, expected: "URGENT" },
3279
+ { input: { isUrgent: false }, expected: "REGULAR" },
3280
+ ])("should return $expected when isUrgent is $input.isUrgent", ({ input, expected }) => {
3281
+ expect(getShiftType(input)).toBe(expected);
3282
+ });
3283
+ ```
3284
+
3285
+ ### Table-Driven Tests
3286
+
3287
+ ```typescript
3288
+ describe("calculateShiftPay", () => {
3289
+ it.each([
3290
+ { hours: 8, rate: 30, expected: 240 },
3291
+ { hours: 10, rate: 25, expected: 250 },
3292
+ { hours: 12, rate: 35, expected: 420 },
3293
+ ])("should calculate $expected for $hours hours at $rate/hr", ({ hours, rate, expected }) => {
3294
+ expect(calculateShiftPay(hours, rate)).toBe(expected);
3295
+ });
3296
+ });
3297
+ ```
3298
+
3299
+ ## Component Testing (Integration Tests)
3300
+
3301
+ Integration tests form the largest part of the Testing Trophy. Test features, not isolated components.
3302
+
3303
+ ### Rendering Components
3304
+
3305
+ ```typescript
3306
+ import { render, screen } from "@testing-library/react";
3307
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3308
+
3309
+ function renderWithProviders(component: ReactElement) {
3310
+ const queryClient = new QueryClient({
3311
+ defaultOptions: {
3312
+ queries: { retry: false },
3313
+ },
3314
+ });
3315
+
3316
+ return render(<QueryClientProvider client={queryClient}>{component}</QueryClientProvider>);
3317
+ }
3318
+
3319
+ it("should render user name", () => {
3320
+ renderWithProviders(<UserProfile userId="123" />);
3321
+ expect(screen.getByText("John Doe")).toBeInTheDocument();
3322
+ });
3323
+ ```
3324
+
3325
+ ### Querying Elements - Priority Order
3326
+
3327
+ Follow [Testing Library's query priority](https://testing-library.com/docs/queries/about#priority):
3328
+
3329
+ 1. **`getByRole`** - Best for accessibility (buttons, links, inputs)
3330
+ 2. **`getByLabelText`** - For form fields with labels
3331
+ 3. **`getByPlaceholderText`** - For inputs without labels
3332
+ 4. **`getByText`** - For non-interactive content
3333
+ 5. **`getByDisplayValue`** - For current input values
3334
+ 6. **`getByAltText`** - For images
3335
+ 7. **`getByTitle`** - Less common
3336
+ 8. **`getByTestId`** - ⚠️ **LAST RESORT** - Use only when no other option exists
3337
+
3338
+ ```typescript
3339
+ // ✅ Prefer accessible queries
3340
+ screen.getByRole("button", { name: /submit/i });
3341
+ screen.getByLabelText("Email address");
3342
+ screen.getByText("Welcome back");
3343
+
3344
+ // ❌ Avoid CSS selectors and implementation details
3345
+ screen.getByClassName("user-card"); // Users don't see classes
3346
+ wrapper.find("UserCard").prop("user"); // Testing implementation
3347
+ screen.getByTestId("custom-element"); // Last resort only
3348
+ ```
3349
+
3350
+ ### User Interactions
3351
+
3352
+ ```typescript
3353
+ import userEvent from "@testing-library/user-event";
3354
+
3355
+ it("should handle form submission", async () => {
3356
+ const user = userEvent.setup();
3357
+ const onSubmit = vi.fn();
3358
+
3359
+ render(<Form onSubmit={onSubmit} />);
3360
+
3361
+ // Type in input
3362
+ await user.type(screen.getByLabelText("Name"), "John Doe");
3363
+
3364
+ // Click button
3365
+ await user.click(screen.getByRole("button", { name: "Submit" }));
3366
+
3367
+ // Assert
3368
+ expect(onSubmit).toHaveBeenCalledWith({ name: "John Doe" });
3369
+ });
3370
+ ```
3371
+
3372
+ ## Hook Testing (Unit Tests)
3373
+
3374
+ Only write unit tests for hooks that contain business logic, not for UI components.
3375
+
3376
+ ### Using renderHook
3377
+
3378
+ ```typescript
3379
+ import { renderHook, waitFor } from "@testing-library/react";
3380
+
3381
+ describe("useCustomHook", () => {
3382
+ it("should return loading state initially", () => {
3383
+ const { result } = renderHook(() => useCustomHook());
3384
+
3385
+ expect(result.current.isLoading).toBe(true);
3386
+ });
3387
+
3388
+ it("should return data after loading", async () => {
3389
+ const { result } = renderHook(() => useCustomHook());
3390
+
3391
+ await waitFor(() => {
3392
+ expect(result.current.isLoading).toBe(false);
3393
+ });
3394
+
3395
+ expect(result.current.data).toEqual(expectedData);
3396
+ });
3397
+ });
3398
+ ```
3399
+
3400
+ ### Testing Hook Updates
3401
+
3402
+ ```typescript
3403
+ it("should update when dependencies change", async () => {
3404
+ const { result, rerender } = renderHook(({ userId }) => useGetUser(userId), {
3405
+ initialProps: { userId: "1" },
3406
+ });
3407
+
3408
+ await waitFor(() => {
3409
+ expect(result.current.data?.id).toBe("1");
3410
+ });
3411
+
3412
+ // Update props
3413
+ rerender({ userId: "2" });
3414
+
3415
+ await waitFor(() => {
3416
+ expect(result.current.data?.id).toBe("2");
3417
+ });
3418
+ });
3419
+ ```
3420
+
3421
+ ## MSW (Mock Service Worker)
3422
+
3423
+ ### Factory Functions Pattern - IMPORTANT
3424
+
3425
+ **Always export factory functions, not static handlers**. This allows tests to customize mock responses.
3426
+
3427
+ ❌ **Don't** export static handlers (ties tests to single response):
3428
+
3429
+ ```typescript
3430
+ // Bad - can only return this one mock
3431
+ export const facilityNotesSuccessScenario = rest.get(
3432
+ `${TEST_API_URL}/facilityNotes`,
3433
+ async (_, res, ctx) => res(ctx.status(200), ctx.json(mockFacilityNotes)),
3434
+ );
3435
+ ```
3436
+
3437
+ ✅ **Do** export factory functions (flexible per test):
3438
+
3439
+ ```typescript
3440
+ // Good - each test can provide custom data
3441
+ export const createFacilityNotesTestHandler = (facilityNotes: FacilityNote[]) => {
3442
+ return rest.get<string, Record<string, string>, FacilityNotesResponse>(
3443
+ `${TEST_API_URL}/facilityNotes/:facilityId`,
3444
+ async (_req, res, ctx) => {
3445
+ return res(ctx.status(200), ctx.json(facilityNotes));
3446
+ },
3447
+ );
3448
+ };
3449
+
3450
+ // Export default success scenario for convenience
3451
+ export const facilityNotesTestHandlers = [createFacilityNotesTestHandler(mockFacilityNotes)];
3452
+ ```
3453
+
3454
+ **Usage in tests:**
3455
+
3456
+ ```typescript
3457
+ // In test setup
3458
+ mockApiServer.use(
3459
+ createFacilityNotesTestHandler(myCustomFacilityNotes),
3460
+ createExtraTimePaySettingsTestHandler({ payload: customSettings }),
3461
+ );
3462
+ ```
3463
+
3464
+ **Rationale:** When endpoints need different responses for different test scenarios, factory functions avoid duplication and inline mocks that become hard to maintain.
3465
+
3466
+ ## Mocking
3467
+
3468
+ ### Mocking Modules
3469
+
3470
+ ```typescript
3471
+ import { vi } from "vitest";
3472
+ import * as useUserModule from "@/features/user/hooks/useUser";
3473
+
3474
+ // Mock entire module
3475
+ vi.mock("@/features/user/hooks/useUser");
3476
+
3477
+ // Spy on specific function
3478
+ const useDefinedWorkerSpy = vi.spyOn(useDefinedWorkerModule, "useDefinedWorker");
3479
+ useDefinedWorkerSpy.mockReturnValue(getMockWorker({ id: "123" }));
3480
+ ```
3481
+
3482
+ ### Mocking Functions
3483
+
3484
+ ```typescript
3485
+ it("should call callback on success", async () => {
3486
+ const onSuccess = vi.fn();
3487
+
3488
+ render(<Component onSuccess={onSuccess} />);
3489
+
3490
+ await user.click(screen.getByRole("button"));
3491
+
3492
+ await waitFor(() => {
3493
+ expect(onSuccess).toHaveBeenCalledWith(expectedData);
3494
+ });
3495
+ });
3496
+ ```
3497
+
3498
+ ### Mocking API Calls
3499
+
3500
+ ```typescript
3501
+ import { vi } from "vitest";
3502
+
3503
+ vi.mock("@/lib/api", () => ({
3504
+ get: vi.fn().mockResolvedValue({
3505
+ data: { id: "1", name: "Test" },
3506
+ }),
3507
+ }));
3508
+ ```
3509
+
3510
+ ## Async Testing
3511
+
3512
+ ### Using waitFor
3513
+
3514
+ ```typescript
3515
+ it("should display data after loading", async () => {
3516
+ render(<AsyncComponent />);
3517
+
3518
+ expect(screen.getByText("Loading...")).toBeInTheDocument();
3519
+
3520
+ await waitFor(() => {
3521
+ expect(screen.getByText("Data loaded")).toBeInTheDocument();
3522
+ });
3523
+ });
3524
+ ```
3525
+
3526
+ ### Using findBy Queries
3527
+
3528
+ ```typescript
3529
+ it("should display user name", async () => {
3530
+ render(<UserProfile />);
3531
+
3532
+ // findBy automatically waits
3533
+ const name = await screen.findByText("John Doe");
3534
+ expect(name).toBeInTheDocument();
3535
+ });
3536
+ ```
3537
+
3538
+ ## Test Organization
3539
+
3540
+ ### Co-location
3541
+
3542
+ - Place test files next to source files
3543
+ - Use same name with `.test.ts` or `.test.tsx` extension
3544
+
3545
+ ```text
3546
+ Feature/
3547
+ ├── Component.tsx
3548
+ ├── Component.test.tsx
3549
+ ├── utils.ts
3550
+ └── utils.test.ts
3551
+ ```
3552
+
3553
+ ### Test Helpers
3554
+
3555
+ - Create test utilities in `testUtils.ts` or `test-utils.ts`
3556
+ - Reusable mocks in `mocks/` folder
3557
+
3558
+ ```typescript
3559
+ // testUtils.ts
3560
+ export function getMockShift(overrides = {}): Shift {
3561
+ return {
3562
+ id: "1",
3563
+ title: "Test Shift",
3564
+ ...overrides,
3565
+ };
3566
+ }
3567
+ ```
3568
+
3569
+ ## What to Test
3570
+
3571
+ ### ✅ Do Test
3572
+
3573
+ - **Integration tests for features** - Multiple components working together
3574
+ - **Unit tests for helpers/utilities** - Pure business logic
3575
+ - **All states** - Loading, success, error
3576
+ - **User interactions** - Clicks, typing, form submissions
3577
+ - **Conditional rendering** - Different states/permissions
3578
+
3579
+ ### ❌ Don't Test
3580
+
3581
+ - **UI components in isolation** - Users never shallow-render
3582
+ - **Implementation details** - Internal state, function calls
3583
+ - **Third-party libraries** - Trust they're tested
3584
+ - **Styles/CSS** - Visual regression tests are separate
3585
+
3586
+ ## Coverage Guidelines
3587
+
3588
+ - Aim for high coverage on business logic and utilities
3589
+ - Don't obsess over 100% coverage on UI components
3590
+ - **Focus on testing behavior**, not implementation
3591
+ - If you can't query it the way a user would, you're testing wrong
3592
+
3593
+ ## Common Patterns
3594
+
3595
+ ### Testing Loading States
3596
+
3597
+ ```typescript
3598
+ it("should show loading state", () => {
3599
+ render(<Component />);
3600
+ expect(screen.getByRole("progressbar")).toBeInTheDocument();
3601
+ });
3602
+ ```
3603
+
3604
+ ### Testing Error States
3605
+
3606
+ ```typescript
3607
+ it("should display error message on failure", async () => {
3608
+ // Mock API error
3609
+ vi.mocked(get).mockRejectedValue(new Error("API Error"));
3610
+
3611
+ render(<Component />);
3612
+
3613
+ expect(await screen.findByText("Error loading data")).toBeInTheDocument();
3614
+ });
3615
+ ```
3616
+
3617
+ ### Testing Conditional Rendering
3618
+
3619
+ ```typescript
3620
+ it("should show premium badge when user is premium", () => {
3621
+ render(<UserCard user={{ ...mockUser, isPremium: true }} />);
3622
+ expect(screen.getByText("Premium")).toBeInTheDocument();
3623
+ });
3624
+
3625
+ it("should not show premium badge when user is not premium", () => {
3626
+ render(<UserCard user={{ ...mockUser, isPremium: false }} />);
3627
+ expect(screen.queryByText("Premium")).not.toBeInTheDocument();
3628
+ });
3629
+ ```
3630
+
3631
+ <!-- Source: .ruler/frontend/typeScript.md -->
3632
+
3633
+ # TypeScript Standards
3634
+
3635
+ ## Core Principles
3636
+
3637
+ - **Strict mode enabled** - no `any` unless absolutely necessary (document why)
3638
+ - **Prefer type inference** - let TypeScript infer when possible
3639
+ - **Explicit return types** for exported functions
3640
+ - **Zod for runtime validation** - single source of truth for types and validation
3641
+ - **No type assertions** unless unavoidable (prefer type guards)
3642
+
3643
+ ## Type vs Interface
3644
+
3645
+ Use the right tool for the job:
3646
+
3647
+ ### Use `interface` for:
3648
+
3649
+ - **Component props**
3650
+ - **Object shapes**
3651
+ - **Class definitions**
3652
+ - **Anything that might be extended**
3653
+
3654
+ ```typescript
3655
+ // ✅ Good - Interface for props
3656
+ interface UserCardProps {
3657
+ user: User;
3658
+ onSelect: (id: string) => void;
3659
+ isHighlighted?: boolean;
3660
+ }
3661
+
3662
+ // ✅ Good - Interface can be extended
3663
+ interface BaseEntity {
3664
+ id: string;
3665
+ createdAt: string;
3666
+ }
3667
+
3668
+ interface User extends BaseEntity {
3669
+ name: string;
3670
+ email: string;
3671
+ }
3672
+ ```
3673
+
3674
+ ### Use `type` for:
3675
+
3676
+ - **Unions**
3677
+ - **Intersections**
3678
+ - **Tuples**
3679
+ - **Derived/conditional types**
3680
+ - **Type aliases**
3681
+
3682
+ ```typescript
3683
+ // ✅ Good - Type for unions
3684
+ type Status = "pending" | "active" | "completed" | "failed";
3685
+
3686
+ // ✅ Good - Type for intersections
3687
+ type UserWithPermissions = User & { permissions: string[] };
3688
+
3689
+ // ✅ Good - Type for tuples
3690
+ type Coordinate = [number, number];
3691
+
3692
+ // ✅ Good - Derived types
3693
+ type UserKeys = keyof User;
3694
+ type PartialUser = Partial<User>;
3695
+ ```
3696
+
3697
+ ## Naming Conventions
3698
+
3699
+ ### Types and Interfaces
3700
+
3701
+ - **Suffix with purpose**: `Props`, `Response`, `Request`, `Options`, `Params`, `State`
3702
+ - **No `I` or `T` prefix** (we're not in C# or Java)
3703
+ - **PascalCase** for type names
3704
+
3705
+ ```typescript
3706
+ // ✅ Good
3707
+ interface ButtonProps { ... }
3708
+ type ApiResponse = { ... };
3709
+ type UserOptions = { ... };
3710
+
3711
+ // ❌ Bad
3712
+ interface IButton { ... } // No I prefix
3713
+ type TResponse = { ... }; // No T prefix
3714
+ interface buttonProps { ... } // Wrong case
3715
+ ```
3716
+
3717
+ ### Boolean Properties
3718
+
3719
+ Always prefix with `is`, `has`, `should`, `can`, `will`:
3720
+
3721
+ ```typescript
3722
+ // ✅ Good
3723
+ interface User {
3724
+ isActive: boolean;
3725
+ hasPermission: boolean;
3726
+ shouldNotify: boolean;
3727
+ canEdit: boolean;
3728
+ willExpire: boolean;
3729
+ }
3730
+
3731
+ // ❌ Bad
3732
+ interface User {
3733
+ active: boolean; // Unclear
3734
+ permission: boolean; // Unclear
3735
+ notify: boolean; // Unclear
3736
+ }
3737
+ ```
3738
+
3739
+ ## Zod Integration
3740
+
3741
+ ```typescript
3742
+ // Define schema first
3743
+ const userSchema = z.object({
3744
+ id: z.string(),
3745
+ name: z.string(),
3746
+ });
3747
+
3748
+ // Infer type from schema
3749
+ export type User = z.infer<typeof userSchema>;
3750
+
3751
+ // Use for API validation
3752
+ const response = await get({
3753
+ url: "/api/users",
3754
+ responseSchema: userSchema,
3755
+ });
3756
+ ```
3757
+
3758
+ ## Type Guards
3759
+
3760
+ ```typescript
3761
+ export function isEventKey(key: string): key is EventKey {
3762
+ return VALID_EVENT_KEYS.includes(key as EventKey);
3763
+ }
3764
+ ```
3765
+
3766
+ ## Constants
3767
+
3768
+ ```typescript
3769
+ // Use const assertions for readonly values
3770
+ export const STAGES = {
3771
+ DRAFT: "draft",
3772
+ PUBLISHED: "published",
3773
+ } as const;
3774
+
3775
+ export type Stage = (typeof STAGES)[keyof typeof STAGES];
3776
+ ```
3777
+
3778
+ ## Function Types
3779
+
3780
+ ```typescript
3781
+ // Explicit return types for exported functions
3782
+ export function calculateTotal(items: Item[]): number {
3783
+ return items.reduce((sum, item) => sum + item.price, 0);
3784
+ }
3785
+
3786
+ // Prefer interfaces for function props
3787
+ interface HandleSubmitProps {
3788
+ userId: string;
3789
+ data: FormData;
3790
+ }
3791
+
3792
+ export function handleSubmit(props: HandleSubmitProps): Promise<void> {
3793
+ // ...
3794
+ }
3795
+ ```
3796
+
3797
+ ## Utility Types
3798
+
3799
+ TypeScript provides powerful built-in utility types. Use them:
3800
+
3801
+ ```typescript
3802
+ // Partial - Make all properties optional
3803
+ type PartialUser = Partial<User>;
3804
+
3805
+ // Required - Make all properties required
3806
+ type RequiredUser = Required<User>;
3807
+
3808
+ // Readonly - Make all properties readonly
3809
+ type ReadonlyUser = Readonly<User>;
3810
+
3811
+ // Pick - Select specific properties
3812
+ type UserIdOnly = Pick<User, "id" | "name">;
3813
+
3814
+ // Omit - Remove specific properties
3815
+ type UserWithoutPassword = Omit<User, "password">;
3816
+
3817
+ // Record - Create object type with specific key/value types
3818
+ type UserMap = Record<string, User>;
3819
+
3820
+ // NonNullable - Remove null and undefined
3821
+ type DefinedString = NonNullable<string | null | undefined>; // string
3822
+
3823
+ // ReturnType - Extract return type from function
3824
+ function getUser() { return { id: '1', name: 'John' }; }
3825
+ type GetUserResult = ReturnType<typeof getUser>;
3826
+
3827
+ // Parameters - Extract parameter types from function
3828
+ function updateUser(id: string, data: UserData) { ... }
3829
+ type UpdateUserParams = Parameters<typeof updateUser>; // [string, UserData]
3830
+ ```
3831
+
3832
+ ## Avoiding `any`
3833
+
3834
+ The `any` type defeats the purpose of TypeScript. Use alternatives:
3835
+
3836
+ ```typescript
3837
+ // ❌ Bad - Loses all type safety
3838
+ function process(data: any) {
3839
+ return data.value; // No error if value doesn't exist!
3840
+ }
3841
+
3842
+ // ✅ Good - Use unknown for truly unknown types
3843
+ function process(data: unknown) {
3844
+ if (typeof data === "object" && data !== null && "value" in data) {
3845
+ return (data as { value: string }).value;
3846
+ }
3847
+ throw new Error("Invalid data");
3848
+ }
3849
+
3850
+ // ✅ Better - Use generics
3851
+ function process<T extends { value: string }>(data: T) {
3852
+ return data.value; // Type safe!
3853
+ }
3854
+
3855
+ // ✅ Best - Use Zod for runtime validation
3856
+ const dataSchema = z.object({ value: z.string() });
3857
+ function process(data: unknown) {
3858
+ const parsed = dataSchema.parse(data); // Throws if invalid
3859
+ return parsed.value; // Type safe!
3860
+ }
3861
+ ```
3862
+
3863
+ ## Generics
3864
+
3865
+ Use generics for reusable, type-safe code:
3866
+
3867
+ ```typescript
3868
+ // ✅ Good - Generic function
3869
+ function getFirst<T>(array: T[]): T | undefined {
3870
+ return array[0];
3871
+ }
3872
+
3873
+ const firstNumber = getFirst([1, 2, 3]); // number | undefined
3874
+ const firstName = getFirst(["a", "b"]); // string | undefined
3875
+
3876
+ // ✅ Good - Generic component
3877
+ interface ListProps<T> {
3878
+ items: T[];
3879
+ renderItem: (item: T) => ReactNode;
3880
+ keyExtractor: (item: T) => string;
3881
+ }
3882
+
3883
+ export function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
3884
+ return (
3885
+ <Box>
3886
+ {items.map((item) => (
3887
+ <Box key={keyExtractor(item)}>{renderItem(item)}</Box>
3888
+ ))}
3889
+ </Box>
3890
+ );
3891
+ }
3892
+
3893
+ // Usage - Type inferred!
3894
+ <List
3895
+ items={users}
3896
+ renderItem={(user) => <UserCard user={user} />} // user is User
3897
+ keyExtractor={(user) => user.id}
3898
+ />;
3899
+
3900
+ // ✅ Good - Constrained generics
3901
+ interface HasId {
3902
+ id: string;
3903
+ }
3904
+
3905
+ function findById<T extends HasId>(items: T[], id: string): T | undefined {
3906
+ return items.find((item) => item.id === id);
3907
+ }
3908
+ ```
3909
+
3910
+ ## Runtime Type Checking
3911
+
3912
+ Use type guards for runtime type checking:
3913
+
3914
+ ```typescript
3915
+ // ✅ Good - Type predicate
3916
+ export function isUser(value: unknown): value is User {
3917
+ return (
3918
+ typeof value === "object" &&
3919
+ value !== null &&
3920
+ "id" in value &&
3921
+ "name" in value &&
3922
+ typeof value.id === "string" &&
3923
+ typeof value.name === "string"
3924
+ );
3925
+ }
3926
+
3927
+ // Usage
3928
+ function processData(data: unknown) {
3929
+ if (isUser(data)) {
3930
+ console.log(data.name); // TypeScript knows data is User
3931
+ }
3932
+ }
3933
+
3934
+ // ✅ Better - Use Zod (runtime validation + type guard)
3935
+ const userSchema = z.object({
3936
+ id: z.string(),
3937
+ name: z.string(),
3938
+ });
3939
+
3940
+ export function isUser(value: unknown): value is User {
3941
+ return userSchema.safeParse(value).success;
3942
+ }
3943
+
3944
+ // ✅ Good - Discriminated unions
3945
+ type ApiResponse =
3946
+ | { status: "success"; data: User }
3947
+ | { status: "error"; error: string }
3948
+ | { status: "loading" };
3949
+
3950
+ function handleResponse(response: ApiResponse) {
3951
+ switch (response.status) {
3952
+ case "success":
3953
+ console.log(response.data); // TypeScript knows data exists
3954
+ break;
3955
+ case "error":
3956
+ console.log(response.error); // TypeScript knows error exists
3957
+ break;
3958
+ case "loading":
3959
+ // TypeScript knows no data or error exists
3960
+ break;
3961
+ }
3962
+ }
3963
+ ```
3964
+
3965
+ ## Const Assertions
3966
+
3967
+ Use `as const` for readonly literal types:
3968
+
3969
+ ```typescript
3970
+ // ✅ Good - Const assertion for object
3971
+ export const STATUSES = {
3972
+ PENDING: "pending",
3973
+ ACTIVE: "active",
3974
+ COMPLETED: "completed",
3975
+ } as const;
3976
+
3977
+ // Type: 'pending' | 'active' | 'completed'
3978
+ export type Status = (typeof STATUSES)[keyof typeof STATUSES];
3979
+
3980
+ // ✅ Good - Const assertion for array
3981
+ export const COLORS = ["red", "blue", "green"] as const;
3982
+ export type Color = (typeof COLORS)[number]; // 'red' | 'blue' | 'green'
3983
+
3984
+ // ❌ Bad - Without const assertion
3985
+ export const STATUSES = {
3986
+ PENDING: "pending", // Type: string (too loose!)
3987
+ ACTIVE: "active",
3988
+ };
3989
+ ```
3990
+
3991
+ ## Discriminated Unions
3992
+
3993
+ Use for state machines and API responses:
3994
+
3995
+ ```typescript
3996
+ // ✅ Good - Discriminated union for query state
3997
+ type QueryState<T> =
3998
+ | { status: "idle" }
3999
+ | { status: "loading" }
4000
+ | { status: "success"; data: T }
4001
+ | { status: "error"; error: Error };
4002
+
4003
+ function useQueryState<T>(): QueryState<T> {
4004
+ // ...
4005
+ }
4006
+
4007
+ // Usage - Type narrowing works automatically
4008
+ const state = useQueryState<User>();
4009
+
4010
+ if (state.status === "success") {
4011
+ console.log(state.data); // TypeScript knows data exists
4012
+ }
4013
+
4014
+ // ✅ Good - Discriminated union for actions
4015
+ type Action =
4016
+ | { type: "SET_USER"; payload: User }
4017
+ | { type: "CLEAR_USER" }
4018
+ | { type: "UPDATE_USER"; payload: Partial<User> };
4019
+
4020
+ function reducer(state: State, action: Action) {
4021
+ switch (action.type) {
4022
+ case "SET_USER":
4023
+ return { ...state, user: action.payload }; // payload is User
4024
+ case "CLEAR_USER":
4025
+ return { ...state, user: null }; // no payload
4026
+ case "UPDATE_USER":
4027
+ return { ...state, user: { ...state.user, ...action.payload } };
4028
+ }
4029
+ }
4030
+ ```
4031
+
4032
+ ## Function Overloads
4033
+
4034
+ Use for functions with different parameter/return combinations:
4035
+
4036
+ ```typescript
4037
+ // ✅ Good - Function overloads
4038
+ function formatValue(value: string): string;
4039
+ function formatValue(value: number): string;
4040
+ function formatValue(value: Date): string;
4041
+ function formatValue(value: string | number | Date): string {
4042
+ if (typeof value === "string") return value;
4043
+ if (typeof value === "number") return value.toString();
4044
+ return value.toISOString();
4045
+ }
4046
+
4047
+ // TypeScript knows the return type based on input
4048
+ const str1 = formatValue("hello"); // string
4049
+ const str2 = formatValue(123); // string
4050
+ const str3 = formatValue(new Date()); // string
4051
+ ```
4052
+
4053
+ ## Template Literal Types
4054
+
4055
+ Use for type-safe string patterns:
4056
+
4057
+ ```typescript
4058
+ // ✅ Good - Event handler props with enforced naming
4059
+ type EventHandlers<T extends string> = {
4060
+ [K in T as `on${Capitalize<K>}`]: () => void;
4061
+ };
4062
+
4063
+ type Props = EventHandlers<"click" | "hover" | "submit">;
4064
+ // Results in: { onClick: () => void; onHover: () => void; onSubmit: () => void; }
4065
+
4066
+ // ✅ Good - Validate event handler keys
4067
+ type ValidateEventKey<K extends string> = K extends `on${Capitalize<string>}` ? K : never;
4068
+
4069
+ interface ComponentProps {
4070
+ onClick: () => void; // ✅ Valid
4071
+ onHover: () => void; // ✅ Valid
4072
+ // click: () => void; // ❌ Does not satisfy ValidateEventKey
4073
+ }
4074
+
4075
+ // ✅ Good - Route paths
4076
+ type Route = `/users/${string}` | `/posts/${string}` | "/";
4077
+
4078
+ const validRoute: Route = "/users/123"; // ✅
4079
+ const invalidRoute: Route = "users/123"; // ❌ Error
4080
+
4081
+ // ✅ Good - CSS properties
4082
+ type CSSProperty = `${"margin" | "padding"}${"Top" | "Bottom" | "Left" | "Right"}`;
4083
+ // 'marginTop' | 'marginBottom' | 'marginLeft' | 'marginRight' | 'paddingTop' | ...
4084
+ ```
4085
+
4086
+ ## Mapped Types
4087
+
4088
+ Create new types by transforming existing ones:
4089
+
4090
+ ```typescript
4091
+ // ✅ Good - Make all properties optional
4092
+ type Optional<T> = {
4093
+ [K in keyof T]?: T[K];
4094
+ };
4095
+
4096
+ // ✅ Good - Make all properties readonly
4097
+ type Immutable<T> = {
4098
+ readonly [K in keyof T]: T[K];
4099
+ };
4100
+
4101
+ // ✅ Good - Add suffix to all keys
4102
+ type Prefixed<T, P extends string> = {
4103
+ [K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
4104
+ };
4105
+
4106
+ type User = { name: string; age: number };
4107
+ type PrefixedUser = Prefixed<User, "user">;
4108
+ // { userName: string; userAge: number }
4109
+ ```
4110
+
4111
+ ## Type Narrowing
4112
+
4113
+ Let TypeScript narrow types automatically:
4114
+
4115
+ ```typescript
4116
+ // ✅ Good - Typeof narrowing
4117
+ function format(value: string | number) {
4118
+ if (typeof value === "string") {
4119
+ return value.toUpperCase(); // TypeScript knows value is string
4120
+ }
4121
+ return value.toFixed(2); // TypeScript knows value is number
4122
+ }
4123
+
4124
+ // ✅ Good - Truthiness narrowing
4125
+ function getLength(value: string | null) {
4126
+ if (value) {
4127
+ return value.length; // TypeScript knows value is string
4128
+ }
4129
+ return 0;
4130
+ }
4131
+
4132
+ // ✅ Good - In narrowing
4133
+ interface Fish {
4134
+ swim: () => void;
4135
+ }
4136
+ interface Bird {
4137
+ fly: () => void;
4138
+ }
4139
+
4140
+ function move(animal: Fish | Bird) {
4141
+ if ("swim" in animal) {
4142
+ animal.swim(); // TypeScript knows animal is Fish
4143
+ } else {
4144
+ animal.fly(); // TypeScript knows animal is Bird
4145
+ }
4146
+ }
4147
+
4148
+ // ✅ Good - Instanceof narrowing
4149
+ function handleError(error: unknown) {
4150
+ if (error instanceof Error) {
4151
+ console.log(error.message); // TypeScript knows error is Error
4152
+ }
4153
+ }
4154
+ ```
4155
+
4156
+ ## Common Patterns
4157
+
4158
+ ### Optional Chaining & Nullish Coalescing
4159
+
4160
+ ```typescript
4161
+ // ✅ Good - Optional chaining
4162
+ const userName = user?.profile?.name;
4163
+
4164
+ // ✅ Good - Nullish coalescing (only null/undefined, not '')
4165
+ const displayName = userName ?? "Anonymous";
4166
+
4167
+ // ❌ Bad - Logical OR (treats '' and 0 as falsy)
4168
+ const displayName = userName || "Anonymous";
4169
+ ```
4170
+
4171
+ ### Non-null Assertion (Use Sparingly)
4172
+
4173
+ ```typescript
4174
+ // ⚠️ Use sparingly - Only when you're 100% sure
4175
+ const user = getUser()!; // Tells TS: trust me, it's not null
4176
+
4177
+ // ✅ Better - Handle null case
4178
+ const user = getUser();
4179
+ if (!user) throw new Error("User not found");
4180
+ // Now TypeScript knows user exists
4181
+ ```
4182
+
4183
+ <!-- Source: .ruler/frontend/uiAndStyling.md -->
4184
+
4185
+ # UI and Styling
4186
+
4187
+ - Use Material UI for components and styling and a mobile-first approach.
4188
+ - Favor TanStack Query over "useEffect".