@clipboard-health/ai-rules 0.2.0 → 0.2.4

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