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