@clipboard-health/ai-rules 1.1.1 → 1.2.1

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