@clipboard-health/ai-rules 0.2.0 → 0.2.3

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