@clipboard-health/ai-rules 1.1.1 → 1.2.0

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