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