@clipboard-health/ai-rules 0.2.0 → 0.2.3

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