@codyswann/lisa 1.55.2 → 1.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,456 @@
1
+ # Extraction Strategies
2
+
3
+ This reference provides detailed patterns for extracting code to reduce cognitive complexity.
4
+
5
+ ## Strategy 1: Helper Functions in View Files
6
+
7
+ Helper functions are the simplest extraction strategy. Use them for rendering logic that doesn't need its own component lifecycle.
8
+
9
+ ### Pattern: Render Helper with Props Object
10
+
11
+ Always use a props object (not positional arguments) for type safety and clarity:
12
+
13
+ ```tsx
14
+ /**
15
+ * Renders a section header with title and count.
16
+ * @param props - Helper function properties
17
+ * @param props.title - Section title text
18
+ * @param props.count - Item count to display
19
+ * @param props.colors - Theme colors object
20
+ */
21
+ function renderSectionHeader(props: {
22
+ readonly title: string;
23
+ readonly count: number;
24
+ readonly colors: KanbanColors;
25
+ }) {
26
+ const { title, count, colors } = props;
27
+ return (
28
+ <HStack className="items-center justify-between py-2">
29
+ <Text
30
+ className="text-xs font-semibold uppercase tracking-wider"
31
+ style={{ color: colors.textMuted }}
32
+ >
33
+ {title}
34
+ </Text>
35
+ <Text className="text-xs" style={{ color: colors.textSecondary }}>
36
+ ({count})
37
+ </Text>
38
+ </HStack>
39
+ );
40
+ }
41
+ ```
42
+
43
+ ### Pattern: Conditional Render Helper
44
+
45
+ For sections that render conditionally based on data:
46
+
47
+ ```tsx
48
+ /**
49
+ * Renders the empty state when no items exist.
50
+ * @param props - Helper function properties
51
+ * @param props.message - Empty state message
52
+ * @param props.colors - Theme colors object
53
+ */
54
+ function renderEmptyState(props: {
55
+ readonly message: string;
56
+ readonly colors: KanbanColors;
57
+ }) {
58
+ const { message, colors } = props;
59
+ return (
60
+ <Box
61
+ className="items-center justify-center p-8"
62
+ style={{ backgroundColor: colors.cardBackground }}
63
+ >
64
+ <Text style={{ color: colors.textMuted }}>{message}</Text>
65
+ </Box>
66
+ );
67
+ }
68
+
69
+ // Usage in View
70
+ const ListView = ({ items, isEmpty, colors }: Props) => (
71
+ <Box>
72
+ {isEmpty
73
+ ? renderEmptyState({ message: "No items found", colors })
74
+ : items.map(item => <Item key={item.id} item={item} />)}
75
+ </Box>
76
+ );
77
+ ```
78
+
79
+ ### Pattern: List Item Render Helper
80
+
81
+ For repeated item rendering with complex styling:
82
+
83
+ ```tsx
84
+ /**
85
+ * Renders a selectable chip/pill item.
86
+ * @param props - Helper function properties
87
+ * @param props.label - Display text for the chip
88
+ * @param props.isSelected - Whether the chip is currently selected
89
+ * @param props.colors - Theme colors for styling
90
+ * @param props.onPress - Callback when chip is pressed
91
+ */
92
+ function renderChip(props: {
93
+ readonly label: string;
94
+ readonly isSelected: boolean;
95
+ readonly colors: KanbanColors;
96
+ readonly onPress: () => void;
97
+ }) {
98
+ const { label, isSelected, colors, onPress } = props;
99
+ return (
100
+ <Pressable
101
+ onPress={onPress}
102
+ style={{
103
+ paddingHorizontal: 10,
104
+ paddingVertical: 5,
105
+ borderRadius: 6,
106
+ backgroundColor: isSelected ? colors.primary : colors.cardBackground,
107
+ borderWidth: 1,
108
+ borderColor: isSelected ? colors.primary : colors.border,
109
+ }}
110
+ >
111
+ <Text
112
+ className="text-xs font-medium"
113
+ style={{ color: isSelected ? "#FFFFFF" : colors.textSecondary }}
114
+ >
115
+ {label}
116
+ </Text>
117
+ </Pressable>
118
+ );
119
+ }
120
+ ```
121
+
122
+ ### When NOT to Use Helper Functions
123
+
124
+ Do not use helper functions when:
125
+
126
+ 1. **The helper would need hooks** - Extract as full component instead
127
+ 2. **The helper is used in multiple files** - Extract as shared component
128
+ 3. **The helper has complex event handling** - Container should handle this
129
+ 4. **The helper exceeds 30 lines** - Consider full component extraction
130
+
131
+ ## Strategy 2: Pre-compute in Container
132
+
133
+ Move complexity from View to Container by pre-computing values.
134
+
135
+ ### Pattern: Selection State Pre-computation
136
+
137
+ **Before (complexity in View):**
138
+
139
+ ```tsx
140
+ // View has repeated .includes() checks
141
+ <Pressable
142
+ style={{
143
+ backgroundColor: filters.positions.includes(position)
144
+ ? colors.primary
145
+ : colors.cardBackground,
146
+ }}
147
+ />
148
+ ```
149
+
150
+ **After (pre-computed in Container):**
151
+
152
+ ```tsx
153
+ // Container
154
+ const positionItems = useMemo(
155
+ () =>
156
+ availablePositions.map(position => ({
157
+ value: position,
158
+ label: position,
159
+ isSelected: filters.positions.includes(position),
160
+ })),
161
+ [availablePositions, filters.positions]
162
+ );
163
+
164
+ // View - simple prop access
165
+ {
166
+ positionItems.map(({ value, label, isSelected }) => (
167
+ <Chip
168
+ key={value}
169
+ label={label}
170
+ isSelected={isSelected}
171
+ onPress={() => onPositionToggle(value)}
172
+ />
173
+ ));
174
+ }
175
+ ```
176
+
177
+ ### Pattern: View State Enumeration
178
+
179
+ **Before (nested ternaries):**
180
+
181
+ ```tsx
182
+ {
183
+ isLoading ? (
184
+ <Spinner />
185
+ ) : error ? (
186
+ <ErrorView error={error} />
187
+ ) : data.length === 0 ? (
188
+ <EmptyState />
189
+ ) : (
190
+ <DataList data={data} />
191
+ );
192
+ }
193
+ ```
194
+
195
+ **After (enumerated state):**
196
+
197
+ ```tsx
198
+ // Container
199
+ type ViewState = "loading" | "error" | "empty" | "ready";
200
+
201
+ const viewState = useMemo((): ViewState => {
202
+ if (isLoading) return "loading";
203
+ if (error) return "error";
204
+ if (data.length === 0) return "empty";
205
+ return "ready";
206
+ }, [isLoading, error, data.length]);
207
+
208
+ // View - switch or map
209
+ const ComponentView = ({ viewState, data, error }: Props) => (
210
+ <Box>
211
+ {viewState === "loading" && <Spinner />}
212
+ {viewState === "error" && <ErrorView error={error} />}
213
+ {viewState === "empty" && <EmptyState />}
214
+ {viewState === "ready" && <DataList data={data} />}
215
+ </Box>
216
+ );
217
+ ```
218
+
219
+ ### Pattern: Style Pre-computation
220
+
221
+ **Before (inline style objects create complexity and perf issues):**
222
+
223
+ ```tsx
224
+ <Box
225
+ style={{
226
+ backgroundColor: isDark ? colors.dark.bg : colors.light.bg,
227
+ padding: isCompact ? 8 : 16,
228
+ borderRadius: isRounded ? 12 : 0,
229
+ }}
230
+ />
231
+ ```
232
+
233
+ **After (computed style in Container):**
234
+
235
+ ```tsx
236
+ // Container
237
+ const boxStyle = useMemo(
238
+ () => ({
239
+ backgroundColor: isDark ? colors.dark.bg : colors.light.bg,
240
+ padding: isCompact ? 8 : 16,
241
+ borderRadius: isRounded ? 12 : 0,
242
+ }),
243
+ [isDark, isCompact, isRounded, colors]
244
+ );
245
+
246
+ // View
247
+ <Box style={boxStyle} />;
248
+ ```
249
+
250
+ ## Strategy 3: Full Component Extraction
251
+
252
+ When helper functions aren't sufficient, extract a full Container/View component.
253
+
254
+ ### Decision Checklist
255
+
256
+ Extract as full component when 3+ of these apply:
257
+
258
+ - [ ] Pattern repeats in 2+ files
259
+ - [ ] Section needs its own state
260
+ - [ ] Section has 4+ props
261
+ - [ ] Section has callbacks that could be simplified
262
+ - [ ] Section represents a meaningful domain concept
263
+ - [ ] Section could be tested independently
264
+
265
+ ### Extraction Steps
266
+
267
+ 1. **Identify the repeated/complex pattern**
268
+ 2. **Define the component's props interface**
269
+ 3. **Create directory structure:**
270
+ ```text
271
+ ComponentName/
272
+ ├── ComponentNameContainer.tsx
273
+ ├── ComponentNameView.tsx
274
+ └── index.tsx
275
+ ```
276
+ 4. **Write Container with logic/state**
277
+ 5. **Write View with pure rendering**
278
+ 6. **Update parent to use new component**
279
+ 7. **Write tests for new component**
280
+
281
+ ### Example: Extracting FilterChipList
282
+
283
+ **Before (in FilterModalView.tsx):**
284
+
285
+ ```tsx
286
+ {
287
+ /* Positions - repeated pattern */
288
+ }
289
+ {
290
+ availablePositions.length > 0 && (
291
+ <VStack className="gap-3">
292
+ <Text style={{ color: colors.textMuted }}>Positions</Text>
293
+ <HStack className="flex-wrap gap-2">
294
+ {availablePositions.map(position => (
295
+ <Pressable
296
+ key={position}
297
+ onPress={() => onPositionToggle(position)}
298
+ style={{
299
+ backgroundColor: filters.positions.includes(position)
300
+ ? colors.primary
301
+ : colors.cardBackground,
302
+ // ... more styles
303
+ }}
304
+ >
305
+ <Text>{position}</Text>
306
+ </Pressable>
307
+ ))}
308
+ </HStack>
309
+ </VStack>
310
+ );
311
+ }
312
+
313
+ {
314
+ /* Tags - same pattern with slight variation */
315
+ }
316
+ {
317
+ availableTags.length > 0 && (
318
+ <VStack className="gap-3">
319
+ <Text style={{ color: colors.textMuted }}>Tags</Text>
320
+ <HStack className="flex-wrap gap-2">
321
+ {availableTags.map(tag => (
322
+ <Pressable
323
+ key={tag.id}
324
+ onPress={() => onTagToggle(tag.id)}
325
+ // ... same pattern
326
+ >
327
+ <Text>{tag.name}</Text>
328
+ </Pressable>
329
+ ))}
330
+ </HStack>
331
+ </VStack>
332
+ );
333
+ }
334
+ ```
335
+
336
+ **After (FilterChipList component):**
337
+
338
+ ```tsx
339
+ // FilterChipListContainer.tsx
340
+ interface FilterChipListProps {
341
+ readonly title: string;
342
+ readonly items: readonly { id: string; label: string; color?: string }[];
343
+ readonly selectedIds: readonly string[];
344
+ readonly colors: KanbanColors;
345
+ readonly onToggle: (id: string) => void;
346
+ }
347
+
348
+ const FilterChipListContainer = ({
349
+ title,
350
+ items,
351
+ selectedIds,
352
+ colors,
353
+ onToggle,
354
+ }: FilterChipListProps) => {
355
+ const chipItems = useMemo(
356
+ () =>
357
+ items.map(item => ({
358
+ ...item,
359
+ isSelected: selectedIds.includes(item.id),
360
+ })),
361
+ [items, selectedIds]
362
+ );
363
+
364
+ const handleToggle = useCallback(
365
+ (id: string) => () => onToggle(id),
366
+ [onToggle]
367
+ );
368
+
369
+ return (
370
+ <FilterChipListView
371
+ title={title}
372
+ items={chipItems}
373
+ colors={colors}
374
+ onToggle={handleToggle}
375
+ />
376
+ );
377
+ };
378
+ ```
379
+
380
+ ```tsx
381
+ // FilterChipListView.tsx
382
+ interface FilterChipListViewProps {
383
+ readonly title: string;
384
+ readonly items: readonly {
385
+ id: string;
386
+ label: string;
387
+ color?: string;
388
+ isSelected: boolean;
389
+ }[];
390
+ readonly colors: KanbanColors;
391
+ readonly onToggle: (id: string) => () => void;
392
+ }
393
+
394
+ const FilterChipListView = ({
395
+ title,
396
+ items,
397
+ colors,
398
+ onToggle,
399
+ }: FilterChipListViewProps) => (
400
+ <VStack className="gap-3">
401
+ <Text
402
+ className="text-xs font-semibold uppercase tracking-wider"
403
+ style={{ color: colors.textMuted }}
404
+ >
405
+ {title}
406
+ </Text>
407
+ <HStack className="flex-wrap gap-2">
408
+ {items.map(({ id, label, color, isSelected }) => (
409
+ <Pressable
410
+ key={id}
411
+ onPress={onToggle(id)}
412
+ style={{
413
+ paddingHorizontal: 10,
414
+ paddingVertical: 5,
415
+ borderRadius: 6,
416
+ backgroundColor: isSelected
417
+ ? (color ?? colors.primary)
418
+ : colors.cardBackground,
419
+ borderWidth: 1,
420
+ borderColor: isSelected ? (color ?? colors.primary) : colors.border,
421
+ }}
422
+ >
423
+ <Text
424
+ className="text-xs font-medium"
425
+ style={{ color: isSelected ? "#FFFFFF" : colors.textSecondary }}
426
+ >
427
+ {label}
428
+ </Text>
429
+ </Pressable>
430
+ ))}
431
+ </HStack>
432
+ </VStack>
433
+ );
434
+
435
+ FilterChipListView.displayName = "FilterChipListView";
436
+ export default memo(FilterChipListView);
437
+ ```
438
+
439
+ **Usage in parent:**
440
+
441
+ ```tsx
442
+ <FilterChipList
443
+ title="Positions"
444
+ items={positionItems}
445
+ selectedIds={filters.positions}
446
+ colors={colors}
447
+ onToggle={onPositionToggle}
448
+ />
449
+ <FilterChipList
450
+ title="Tags"
451
+ items={tagItems}
452
+ selectedIds={filters.tags}
453
+ colors={colors}
454
+ onToggle={onTagToggle}
455
+ />
456
+ ```