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