@kodrunhq/opencode-autopilot 1.9.0 → 1.11.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.
- package/assets/commands/oc-review-agents.md +103 -0
- package/assets/skills/coding-standards/SKILL.md +313 -0
- package/assets/skills/csharp-patterns/SKILL.md +327 -0
- package/assets/skills/frontend-design/SKILL.md +433 -0
- package/assets/skills/java-patterns/SKILL.md +258 -0
- package/assets/templates/cli-tool.md +49 -0
- package/assets/templates/fullstack.md +71 -0
- package/assets/templates/library.md +49 -0
- package/assets/templates/web-api.md +60 -0
- package/package.json +1 -1
- package/src/agents/debugger.ts +329 -0
- package/src/agents/index.ts +13 -4
- package/src/agents/metaprompter.ts +1 -1
- package/src/agents/planner.ts +563 -0
- package/src/agents/researcher.ts +1 -1
- package/src/agents/reviewer.ts +270 -0
- package/src/installer.ts +11 -3
- package/src/registry/model-groups.ts +4 -1
- package/src/review/stack-gate.ts +2 -0
- package/src/skills/adaptive-injector.ts +47 -2
- package/src/tools/stocktake.ts +64 -9
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: frontend-design
|
|
3
|
+
description: State-of-the-art frontend UX/UI design patterns covering component architecture, responsive design, accessibility, and design system integration
|
|
4
|
+
stacks:
|
|
5
|
+
- react
|
|
6
|
+
- vue
|
|
7
|
+
- svelte
|
|
8
|
+
- angular
|
|
9
|
+
requires: []
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Frontend Design Patterns
|
|
13
|
+
|
|
14
|
+
Practical frontend design patterns for building polished, accessible, and performant user interfaces. Covers component architecture, responsive design, accessibility, state management, animation, visual design principles, and design system integration. Apply these when building, reviewing, or refactoring frontend code.
|
|
15
|
+
|
|
16
|
+
## 1. Component Architecture
|
|
17
|
+
|
|
18
|
+
**DO:** Structure components using atomic design principles and clear composition patterns.
|
|
19
|
+
|
|
20
|
+
- Organize components as atoms, molecules, and organisms:
|
|
21
|
+
```
|
|
22
|
+
atoms/ Button, Input, Label, Icon, Badge
|
|
23
|
+
molecules/ SearchBar (Input + Button), FormField (Label + Input + Error)
|
|
24
|
+
organisms/ Header (Logo + Nav + SearchBar), OrderForm (FormFields + Submit)
|
|
25
|
+
```
|
|
26
|
+
- Use compound components for related elements that share state:
|
|
27
|
+
```jsx
|
|
28
|
+
// DO: Compound component -- parent manages shared state
|
|
29
|
+
<Select value={selected} onChange={setSelected}>
|
|
30
|
+
<Select.Trigger>{selected}</Select.Trigger>
|
|
31
|
+
<Select.Options>
|
|
32
|
+
<Select.Option value="a">Option A</Select.Option>
|
|
33
|
+
<Select.Option value="b">Option B</Select.Option>
|
|
34
|
+
</Select.Options>
|
|
35
|
+
</Select>
|
|
36
|
+
```
|
|
37
|
+
- Separate container (data fetching, state) from presentational (rendering) components:
|
|
38
|
+
```jsx
|
|
39
|
+
// Container: handles data
|
|
40
|
+
function OrderListContainer() {
|
|
41
|
+
const { data, isLoading } = useOrders();
|
|
42
|
+
return <OrderList orders={data} loading={isLoading} />;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Presentational: pure rendering
|
|
46
|
+
function OrderList({ orders, loading }) {
|
|
47
|
+
if (loading) return <Skeleton count={3} />;
|
|
48
|
+
return orders.map(o => <OrderCard key={o.id} order={o} />);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
- Use composition (children, slots) instead of prop drilling:
|
|
52
|
+
```jsx
|
|
53
|
+
// DO: Composition via children
|
|
54
|
+
<Card>
|
|
55
|
+
<Card.Header>Title</Card.Header>
|
|
56
|
+
<Card.Body>{content}</Card.Body>
|
|
57
|
+
<Card.Footer><Button>Save</Button></Card.Footer>
|
|
58
|
+
</Card>
|
|
59
|
+
|
|
60
|
+
// DON'T: Prop drilling through many levels
|
|
61
|
+
<Card title="Title" content={content} buttonText="Save" onButtonClick={...} />
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**DON'T:**
|
|
65
|
+
|
|
66
|
+
- Pass data through more than 2 intermediate components (prop drilling) -- use context, composition, or state management
|
|
67
|
+
- Create components with more than 10 props -- split into smaller components or use composition
|
|
68
|
+
- Mix data fetching with rendering in the same component
|
|
69
|
+
- Use `index` as `key` for lists that can be reordered, filtered, or mutated
|
|
70
|
+
|
|
71
|
+
## 2. Responsive Design
|
|
72
|
+
|
|
73
|
+
**DO:** Design mobile-first and use modern CSS features for fluid layouts.
|
|
74
|
+
|
|
75
|
+
- Use `min-width` breakpoints (mobile-first):
|
|
76
|
+
```css
|
|
77
|
+
/* Base: mobile */
|
|
78
|
+
.grid { display: flex; flex-direction: column; }
|
|
79
|
+
|
|
80
|
+
/* Tablet and up */
|
|
81
|
+
@media (min-width: 768px) {
|
|
82
|
+
.grid { flex-direction: row; flex-wrap: wrap; }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Desktop and up */
|
|
86
|
+
@media (min-width: 1024px) {
|
|
87
|
+
.grid { max-width: 1200px; margin: 0 auto; }
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
- Use `clamp()` for fluid typography:
|
|
91
|
+
```css
|
|
92
|
+
/* Fluid font size: 1rem at 320px, 1.5rem at 1200px */
|
|
93
|
+
h1 { font-size: clamp(1rem, 0.5rem + 2.5vw, 1.5rem); }
|
|
94
|
+
```
|
|
95
|
+
- Use container queries for component-level responsiveness:
|
|
96
|
+
```css
|
|
97
|
+
.card-container { container-type: inline-size; }
|
|
98
|
+
|
|
99
|
+
@container (min-width: 400px) {
|
|
100
|
+
.card { display: flex; flex-direction: row; }
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
- Use `aspect-ratio` for media containers:
|
|
104
|
+
```css
|
|
105
|
+
.video-wrapper { aspect-ratio: 16 / 9; width: 100%; }
|
|
106
|
+
.avatar { aspect-ratio: 1; border-radius: 50%; }
|
|
107
|
+
```
|
|
108
|
+
- Use logical properties for internationalization:
|
|
109
|
+
```css
|
|
110
|
+
/* DO: Works for LTR and RTL */
|
|
111
|
+
.sidebar { margin-inline-start: 1rem; padding-block: 0.5rem; }
|
|
112
|
+
|
|
113
|
+
/* DON'T: Only works for LTR */
|
|
114
|
+
.sidebar { margin-left: 1rem; padding-top: 0.5rem; padding-bottom: 0.5rem; }
|
|
115
|
+
```
|
|
116
|
+
- Use responsive images:
|
|
117
|
+
```html
|
|
118
|
+
<picture>
|
|
119
|
+
<source srcset="hero-wide.webp" media="(min-width: 1024px)" />
|
|
120
|
+
<source srcset="hero-medium.webp" media="(min-width: 640px)" />
|
|
121
|
+
<img src="hero-small.webp" alt="Hero image" loading="lazy" />
|
|
122
|
+
</picture>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**DON'T:**
|
|
126
|
+
|
|
127
|
+
- Use `max-width` breakpoints (desktop-first) -- mobile-first produces smaller CSS and better progressive enhancement
|
|
128
|
+
- Use fixed pixel widths for layouts -- use relative units (`rem`, `%`, `fr`, `vw`)
|
|
129
|
+
- Hide content with `display: none` on mobile instead of designing a mobile-appropriate layout
|
|
130
|
+
- Use `@media` for component-level responsiveness when container queries are available
|
|
131
|
+
|
|
132
|
+
## 3. Accessibility (a11y)
|
|
133
|
+
|
|
134
|
+
**DO:** Build accessible interfaces from the start, not as an afterthought.
|
|
135
|
+
|
|
136
|
+
- Use semantic HTML elements:
|
|
137
|
+
```html
|
|
138
|
+
<!-- DO: Semantic -->
|
|
139
|
+
<nav aria-label="Main navigation">
|
|
140
|
+
<ul>
|
|
141
|
+
<li><a href="/home">Home</a></li>
|
|
142
|
+
<li><a href="/about">About</a></li>
|
|
143
|
+
</ul>
|
|
144
|
+
</nav>
|
|
145
|
+
|
|
146
|
+
<!-- DON'T: Div soup -->
|
|
147
|
+
<div class="nav">
|
|
148
|
+
<div class="nav-item" onclick="goto('/home')">Home</div>
|
|
149
|
+
<div class="nav-item" onclick="goto('/about')">About</div>
|
|
150
|
+
</div>
|
|
151
|
+
```
|
|
152
|
+
- Use ARIA only when native HTML semantics are insufficient:
|
|
153
|
+
```html
|
|
154
|
+
<!-- DO: ARIA for custom widgets -->
|
|
155
|
+
<div role="tablist">
|
|
156
|
+
<button role="tab" aria-selected="true" aria-controls="panel-1">Tab 1</button>
|
|
157
|
+
<div role="tabpanel" id="panel-1">Content 1</div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<!-- DON'T: ARIA on native elements that already have semantics -->
|
|
161
|
+
<button role="button">Submit</button> <!-- Redundant -->
|
|
162
|
+
```
|
|
163
|
+
- Manage focus for keyboard navigation:
|
|
164
|
+
```jsx
|
|
165
|
+
// Skip link for keyboard users
|
|
166
|
+
<a href="#main-content" className="skip-link">Skip to main content</a>
|
|
167
|
+
|
|
168
|
+
// Focus management in modals
|
|
169
|
+
function Modal({ isOpen, onClose }) {
|
|
170
|
+
const closeRef = useRef(null);
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (isOpen) closeRef.current?.focus();
|
|
173
|
+
}, [isOpen]);
|
|
174
|
+
// Trap focus inside modal while open
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
- Meet WCAG AA color contrast minimums:
|
|
178
|
+
```css
|
|
179
|
+
/* AA minimums: 4.5:1 for normal text, 3:1 for large text */
|
|
180
|
+
.text { color: #333; background: #fff; } /* 12.6:1 -- PASS */
|
|
181
|
+
.text { color: #999; background: #fff; } /* 2.8:1 -- FAIL */
|
|
182
|
+
```
|
|
183
|
+
- Use `focus-visible` for keyboard-only focus indicators:
|
|
184
|
+
```css
|
|
185
|
+
button:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
|
|
186
|
+
button:focus:not(:focus-visible) { outline: none; }
|
|
187
|
+
```
|
|
188
|
+
- Use live regions for dynamic content updates:
|
|
189
|
+
```html
|
|
190
|
+
<div aria-live="polite" aria-atomic="true">
|
|
191
|
+
{statusMessage} <!-- Screen readers announce changes -->
|
|
192
|
+
</div>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**DON'T:**
|
|
196
|
+
|
|
197
|
+
- Use `div` or `span` for interactive elements -- use `button`, `a`, `input`, `select`
|
|
198
|
+
- Remove focus outlines without providing an alternative visual indicator
|
|
199
|
+
- Use color alone to convey information (add icons, text, or patterns)
|
|
200
|
+
- Use `tabindex` values greater than 0 -- it disrupts natural tab order
|
|
201
|
+
- Auto-play video or audio without user consent
|
|
202
|
+
|
|
203
|
+
## 4. State Management
|
|
204
|
+
|
|
205
|
+
**DO:** Start with the simplest state solution and scale up only when needed.
|
|
206
|
+
|
|
207
|
+
- Use local state first -- most state belongs to a single component:
|
|
208
|
+
```jsx
|
|
209
|
+
function Counter() {
|
|
210
|
+
const [count, setCount] = useState(0);
|
|
211
|
+
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
- Separate server state from client state:
|
|
215
|
+
```jsx
|
|
216
|
+
// Server state: fetched, cached, synced with backend (use React Query/SWR)
|
|
217
|
+
const { data: orders } = useQuery({ queryKey: ['orders'], queryFn: fetchOrders });
|
|
218
|
+
|
|
219
|
+
// Client state: UI-only (use useState/useReducer)
|
|
220
|
+
const [isFilterOpen, setFilterOpen] = useState(false);
|
|
221
|
+
```
|
|
222
|
+
- Use URL as state for shareable, bookmarkable views:
|
|
223
|
+
```jsx
|
|
224
|
+
// Search params as state
|
|
225
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
226
|
+
const page = Number(searchParams.get("page") ?? "1");
|
|
227
|
+
const filter = searchParams.get("status") ?? "all";
|
|
228
|
+
```
|
|
229
|
+
- Use optimistic updates for perceived performance:
|
|
230
|
+
```jsx
|
|
231
|
+
const mutation = useMutation({
|
|
232
|
+
mutationFn: updateOrder,
|
|
233
|
+
onMutate: async (newOrder) => {
|
|
234
|
+
await queryClient.cancelQueries({ queryKey: ['orders'] });
|
|
235
|
+
const previous = queryClient.getQueryData(['orders']);
|
|
236
|
+
queryClient.setQueryData(['orders'], old => optimisticUpdate(old, newOrder));
|
|
237
|
+
return { previous };
|
|
238
|
+
},
|
|
239
|
+
onError: (err, newOrder, context) => {
|
|
240
|
+
queryClient.setQueryData(['orders'], context.previous); // Rollback
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**DON'T:**
|
|
246
|
+
|
|
247
|
+
- Put everything in global state -- only share state that multiple components need simultaneously
|
|
248
|
+
- Use global state for server data -- use a data fetching library with caching (React Query, SWR, RTK Query)
|
|
249
|
+
- Store derived values in state -- compute them during render:
|
|
250
|
+
```jsx
|
|
251
|
+
// DON'T: Derived state
|
|
252
|
+
const [total, setTotal] = useState(0);
|
|
253
|
+
useEffect(() => setTotal(items.reduce(...)), [items]);
|
|
254
|
+
|
|
255
|
+
// DO: Computed value
|
|
256
|
+
const total = items.reduce((sum, item) => sum + item.price, 0);
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## 5. Animation and Interaction
|
|
260
|
+
|
|
261
|
+
**DO:** Use animations purposefully to provide feedback and guide attention.
|
|
262
|
+
|
|
263
|
+
- Use CSS transitions for simple state changes:
|
|
264
|
+
```css
|
|
265
|
+
.button {
|
|
266
|
+
transition: background-color 150ms ease, transform 100ms ease;
|
|
267
|
+
}
|
|
268
|
+
.button:hover { background-color: var(--color-hover); }
|
|
269
|
+
.button:active { transform: scale(0.97); }
|
|
270
|
+
```
|
|
271
|
+
- Respect reduced motion preferences:
|
|
272
|
+
```css
|
|
273
|
+
@media (prefers-reduced-motion: reduce) {
|
|
274
|
+
*, *::before, *::after {
|
|
275
|
+
animation-duration: 0.01ms !important;
|
|
276
|
+
transition-duration: 0.01ms !important;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
- Use `will-change` sparingly for GPU acceleration hints:
|
|
281
|
+
```css
|
|
282
|
+
/* Only on elements that WILL animate, not everything */
|
|
283
|
+
.sliding-panel { will-change: transform; }
|
|
284
|
+
.sliding-panel.idle { will-change: auto; } /* Remove when done */
|
|
285
|
+
```
|
|
286
|
+
- Use Intersection Observer for scroll-triggered animations:
|
|
287
|
+
```jsx
|
|
288
|
+
function FadeIn({ children }) {
|
|
289
|
+
const ref = useRef(null);
|
|
290
|
+
const isVisible = useIntersectionObserver(ref, { threshold: 0.1 });
|
|
291
|
+
return (
|
|
292
|
+
<div ref={ref} className={isVisible ? "fade-in visible" : "fade-in"}>
|
|
293
|
+
{children}
|
|
294
|
+
</div>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
- Prefer skeleton screens over spinners:
|
|
299
|
+
```jsx
|
|
300
|
+
// DO: Skeleton preserves layout, reduces perceived wait time
|
|
301
|
+
function OrderSkeleton() {
|
|
302
|
+
return <div className="skeleton-card"><div className="skeleton-line" />...</div>;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// DON'T: Spinner gives no information about what's loading
|
|
306
|
+
function Loading() { return <Spinner />; }
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**DON'T:**
|
|
310
|
+
|
|
311
|
+
- Animate `width`, `height`, `top`, `left` -- animate `transform` and `opacity` (GPU-composited, no layout recalculation)
|
|
312
|
+
- Use animations that last longer than 300ms for UI transitions (feels sluggish)
|
|
313
|
+
- Add `will-change` to everything -- it consumes GPU memory. Apply only to animating elements
|
|
314
|
+
- Ignore `prefers-reduced-motion` -- some users experience motion sickness
|
|
315
|
+
|
|
316
|
+
## 6. Visual Design Principles
|
|
317
|
+
|
|
318
|
+
**DO:** Apply systematic design decisions for consistent, professional interfaces.
|
|
319
|
+
|
|
320
|
+
- Use a typographic scale (e.g., major third 1.25):
|
|
321
|
+
```css
|
|
322
|
+
:root {
|
|
323
|
+
--font-xs: 0.64rem; /* 10.24px */
|
|
324
|
+
--font-sm: 0.8rem; /* 12.8px */
|
|
325
|
+
--font-base: 1rem; /* 16px */
|
|
326
|
+
--font-lg: 1.25rem; /* 20px */
|
|
327
|
+
--font-xl: 1.563rem; /* 25px */
|
|
328
|
+
--font-2xl: 1.953rem; /* 31.25px */
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
- Use a 4px/8px spacing grid:
|
|
332
|
+
```css
|
|
333
|
+
:root {
|
|
334
|
+
--space-1: 0.25rem; /* 4px */
|
|
335
|
+
--space-2: 0.5rem; /* 8px */
|
|
336
|
+
--space-3: 0.75rem; /* 12px */
|
|
337
|
+
--space-4: 1rem; /* 16px */
|
|
338
|
+
--space-6: 1.5rem; /* 24px */
|
|
339
|
+
--space-8: 2rem; /* 32px */
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
- Use HSL-based color systems with semantic tokens:
|
|
343
|
+
```css
|
|
344
|
+
:root {
|
|
345
|
+
/* Primitives */
|
|
346
|
+
--blue-500: hsl(220 90% 56%);
|
|
347
|
+
--red-500: hsl(0 84% 60%);
|
|
348
|
+
|
|
349
|
+
/* Semantic tokens */
|
|
350
|
+
--color-primary: var(--blue-500);
|
|
351
|
+
--color-danger: var(--red-500);
|
|
352
|
+
--color-text: hsl(220 20% 15%);
|
|
353
|
+
--color-text-muted: hsl(220 15% 50%);
|
|
354
|
+
--color-surface: hsl(0 0% 100%);
|
|
355
|
+
--color-border: hsl(220 15% 88%);
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
- Use a consistent elevation/shadow system:
|
|
359
|
+
```css
|
|
360
|
+
:root {
|
|
361
|
+
--shadow-sm: 0 1px 2px hsl(0 0% 0% / 0.05);
|
|
362
|
+
--shadow-md: 0 4px 6px hsl(0 0% 0% / 0.07);
|
|
363
|
+
--shadow-lg: 0 10px 15px hsl(0 0% 0% / 0.1);
|
|
364
|
+
--shadow-xl: 0 20px 25px hsl(0 0% 0% / 0.15);
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
- Establish visual hierarchy through size, weight, and color -- not decoration:
|
|
368
|
+
```css
|
|
369
|
+
.heading { font-size: var(--font-xl); font-weight: 700; color: var(--color-text); }
|
|
370
|
+
.subheading { font-size: var(--font-lg); font-weight: 500; color: var(--color-text); }
|
|
371
|
+
.body { font-size: var(--font-base); font-weight: 400; color: var(--color-text); }
|
|
372
|
+
.caption { font-size: var(--font-sm); font-weight: 400; color: var(--color-text-muted); }
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**DON'T:**
|
|
376
|
+
|
|
377
|
+
- Use arbitrary pixel values for spacing -- stick to the grid
|
|
378
|
+
- Use more than 3 font sizes on a single screen (headings + body + caption)
|
|
379
|
+
- Mix color definition methods (hex, rgb, hsl) -- pick one system
|
|
380
|
+
- Use shadows for decoration -- shadows indicate elevation (interactive, floating, overlay)
|
|
381
|
+
|
|
382
|
+
## 7. Design System Integration
|
|
383
|
+
|
|
384
|
+
**DO:** Build a token-based system that scales across themes and components.
|
|
385
|
+
|
|
386
|
+
- Use CSS custom properties for theming:
|
|
387
|
+
```css
|
|
388
|
+
:root {
|
|
389
|
+
--color-bg: hsl(0 0% 100%);
|
|
390
|
+
--color-text: hsl(220 20% 15%);
|
|
391
|
+
--radius-md: 0.375rem;
|
|
392
|
+
--font-body: system-ui, -apple-system, sans-serif;
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
- Support dark mode via custom properties and media query:
|
|
396
|
+
```css
|
|
397
|
+
@media (prefers-color-scheme: dark) {
|
|
398
|
+
:root {
|
|
399
|
+
--color-bg: hsl(220 20% 10%);
|
|
400
|
+
--color-text: hsl(220 15% 85%);
|
|
401
|
+
--color-surface: hsl(220 20% 14%);
|
|
402
|
+
--color-border: hsl(220 15% 25%);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/* Manual toggle via data attribute */
|
|
407
|
+
[data-theme="dark"] {
|
|
408
|
+
--color-bg: hsl(220 20% 10%);
|
|
409
|
+
--color-text: hsl(220 15% 85%);
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
- Use component variants via data attributes or props:
|
|
413
|
+
```css
|
|
414
|
+
/* Data attribute variants */
|
|
415
|
+
.button { padding: var(--space-2) var(--space-4); border-radius: var(--radius-md); }
|
|
416
|
+
.button[data-variant="primary"] { background: var(--color-primary); color: white; }
|
|
417
|
+
.button[data-variant="secondary"] { background: transparent; border: 1px solid var(--color-border); }
|
|
418
|
+
.button[data-size="sm"] { padding: var(--space-1) var(--space-2); font-size: var(--font-sm); }
|
|
419
|
+
```
|
|
420
|
+
- Use consistent naming across tokens and components:
|
|
421
|
+
```
|
|
422
|
+
Tokens: --color-primary, --space-4, --radius-md, --shadow-lg
|
|
423
|
+
Components: Button, Card, Input (PascalCase)
|
|
424
|
+
Variants: data-variant="primary", data-size="sm" (kebab-case values)
|
|
425
|
+
```
|
|
426
|
+
- Document component APIs -- props, variants, and usage examples should be clear from the component definition
|
|
427
|
+
|
|
428
|
+
**DON'T:**
|
|
429
|
+
|
|
430
|
+
- Hardcode colors or spacing values in components -- always reference tokens
|
|
431
|
+
- Create one-off styles that don't fit the system -- either extend the system or use an existing token
|
|
432
|
+
- Switch themes by overriding individual properties in JS -- toggle a class or data attribute on `<html>`
|
|
433
|
+
- Mix multiple theming approaches (CSS-in-JS, CSS Modules, global CSS) in the same project without clear boundaries
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: java-patterns
|
|
3
|
+
description: Idiomatic Java patterns including records, Spring Boot conventions, JPA/Hibernate, and common pitfalls
|
|
4
|
+
stacks:
|
|
5
|
+
- java
|
|
6
|
+
requires:
|
|
7
|
+
- coding-standards
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Java Patterns
|
|
11
|
+
|
|
12
|
+
Idiomatic Java patterns for modern (17+) projects. Covers language features, Spring Boot conventions, JPA/Hibernate best practices, testing idioms, and common pitfalls. Apply these when writing, reviewing, or refactoring Java code.
|
|
13
|
+
|
|
14
|
+
## 1. Modern Java Idioms
|
|
15
|
+
|
|
16
|
+
**DO:** Use modern Java features to write concise, safe, and expressive code.
|
|
17
|
+
|
|
18
|
+
- Use records for immutable data carriers:
|
|
19
|
+
```java
|
|
20
|
+
// Record: immutable, equals/hashCode/toString auto-generated
|
|
21
|
+
public record UserDto(String name, String email, Instant createdAt) {}
|
|
22
|
+
|
|
23
|
+
// Use in APIs, DTOs, value objects -- not JPA entities
|
|
24
|
+
var user = new UserDto("Alice", "alice@example.com", Instant.now());
|
|
25
|
+
```
|
|
26
|
+
- Use sealed classes for restricted type hierarchies:
|
|
27
|
+
```java
|
|
28
|
+
public sealed interface Shape permits Circle, Rectangle, Triangle {
|
|
29
|
+
double area();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public record Circle(double radius) implements Shape {
|
|
33
|
+
public double area() { return Math.PI * radius * radius; }
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
- Use Optional for nullable returns -- never for fields or parameters:
|
|
37
|
+
```java
|
|
38
|
+
// DO: Chain operations safely
|
|
39
|
+
Optional<User> user = repository.findById(id);
|
|
40
|
+
String name = user.map(User::name).orElse("Anonymous");
|
|
41
|
+
|
|
42
|
+
// DO: Handle absence explicitly
|
|
43
|
+
return repository.findById(id)
|
|
44
|
+
.orElseThrow(() -> new UserNotFoundException(id));
|
|
45
|
+
```
|
|
46
|
+
- Use pattern matching with `instanceof`:
|
|
47
|
+
```java
|
|
48
|
+
// DO: Pattern matching eliminates manual cast
|
|
49
|
+
if (shape instanceof Circle c) {
|
|
50
|
+
return Math.PI * c.radius() * c.radius();
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
- Use text blocks for multi-line strings:
|
|
54
|
+
```java
|
|
55
|
+
String query = """
|
|
56
|
+
SELECT u.name, u.email
|
|
57
|
+
FROM users u
|
|
58
|
+
WHERE u.active = true
|
|
59
|
+
ORDER BY u.name
|
|
60
|
+
""";
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**DON'T:**
|
|
64
|
+
|
|
65
|
+
- Use `Optional.get()` without checking `isPresent()` -- use `orElse()`, `orElseThrow()`, or `map()`
|
|
66
|
+
- Use Optional as a method parameter or field type -- it's for return values only
|
|
67
|
+
- Return `null` from public methods when Optional is appropriate
|
|
68
|
+
- Use raw types: `List` instead of `List<String>`
|
|
69
|
+
- Use `instanceof` followed by a manual cast when pattern matching is available
|
|
70
|
+
|
|
71
|
+
## 2. Spring Boot Patterns
|
|
72
|
+
|
|
73
|
+
**DO:** Follow Spring Boot conventions for maintainable, testable applications.
|
|
74
|
+
|
|
75
|
+
- Use constructor-based dependency injection -- never field injection:
|
|
76
|
+
```java
|
|
77
|
+
// DO: Constructor injection (immutable, testable)
|
|
78
|
+
@Service
|
|
79
|
+
public class OrderService {
|
|
80
|
+
private final OrderRepository orderRepo;
|
|
81
|
+
private final PaymentGateway paymentGateway;
|
|
82
|
+
|
|
83
|
+
public OrderService(OrderRepository orderRepo, PaymentGateway paymentGateway) {
|
|
84
|
+
this.orderRepo = orderRepo;
|
|
85
|
+
this.paymentGateway = paymentGateway;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
- Layer architecture: Controller -> Service -> Repository:
|
|
90
|
+
```java
|
|
91
|
+
@RestController // Thin: HTTP concerns only
|
|
92
|
+
@Service // Business logic and orchestration
|
|
93
|
+
@Repository // Data access only
|
|
94
|
+
```
|
|
95
|
+
- Place `@Transactional` on the service layer, not on repositories:
|
|
96
|
+
```java
|
|
97
|
+
@Service
|
|
98
|
+
public class TransferService {
|
|
99
|
+
@Transactional
|
|
100
|
+
public void transfer(AccountId from, AccountId to, Money amount) {
|
|
101
|
+
accountRepo.debit(from, amount);
|
|
102
|
+
accountRepo.credit(to, amount);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
- Use `@ConfigurationProperties` for type-safe configuration:
|
|
107
|
+
```java
|
|
108
|
+
@ConfigurationProperties(prefix = "app.email")
|
|
109
|
+
public record EmailConfig(String host, int port, boolean useTls) {}
|
|
110
|
+
```
|
|
111
|
+
- Handle exceptions with `@ControllerAdvice`:
|
|
112
|
+
```java
|
|
113
|
+
@ControllerAdvice
|
|
114
|
+
public class GlobalExceptionHandler {
|
|
115
|
+
@ExceptionHandler(UserNotFoundException.class)
|
|
116
|
+
public ResponseEntity<ErrorResponse> handleNotFound(UserNotFoundException ex) {
|
|
117
|
+
return ResponseEntity.status(404)
|
|
118
|
+
.body(new ErrorResponse(ex.getMessage()));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**DON'T:**
|
|
124
|
+
|
|
125
|
+
- Use `@Autowired` on fields -- constructor injection is immutable and explicit
|
|
126
|
+
- Put business logic in controllers -- controllers parse HTTP, services execute logic
|
|
127
|
+
- Use `@Transactional` on repository methods -- Spring Data already wraps queries
|
|
128
|
+
- Catch exceptions in controllers individually -- use `@ControllerAdvice` for centralized handling
|
|
129
|
+
- Use Spring profiles for feature flags -- profiles are for environments (dev, staging, prod)
|
|
130
|
+
|
|
131
|
+
## 3. JPA/Hibernate Conventions
|
|
132
|
+
|
|
133
|
+
**DO:** Design entities carefully and be explicit about fetching behavior.
|
|
134
|
+
|
|
135
|
+
- Mark IDs as immutable and use `@Version` for optimistic locking:
|
|
136
|
+
```java
|
|
137
|
+
@Entity
|
|
138
|
+
public class Order {
|
|
139
|
+
@Id @GeneratedValue(strategy = GenerationType.UUID)
|
|
140
|
+
private UUID id;
|
|
141
|
+
|
|
142
|
+
@Version
|
|
143
|
+
private Long version; // Optimistic locking -- prevents lost updates
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
- Default to lazy fetching, use explicit join fetch for read paths:
|
|
147
|
+
```java
|
|
148
|
+
// DO: Lazy by default
|
|
149
|
+
@ManyToOne(fetch = FetchType.LAZY)
|
|
150
|
+
private Customer customer;
|
|
151
|
+
|
|
152
|
+
// DO: Explicit fetch when needed
|
|
153
|
+
@EntityGraph(attributePaths = {"customer", "items"})
|
|
154
|
+
Optional<Order> findWithDetailsById(UUID id);
|
|
155
|
+
```
|
|
156
|
+
- Prevent N+1 queries with `@EntityGraph` or `JOIN FETCH`:
|
|
157
|
+
```java
|
|
158
|
+
// Repository method with join fetch
|
|
159
|
+
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.status = :status")
|
|
160
|
+
List<Order> findByStatusWithItems(@Param("status") OrderStatus status);
|
|
161
|
+
```
|
|
162
|
+
- Use DTOs for API responses, never expose entities directly:
|
|
163
|
+
```java
|
|
164
|
+
// DO: Project to DTO
|
|
165
|
+
public record OrderSummary(UUID id, String customerName, BigDecimal total) {
|
|
166
|
+
public static OrderSummary from(Order order) {
|
|
167
|
+
return new OrderSummary(order.getId(), order.getCustomer().getName(), order.getTotal());
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**DON'T:**
|
|
173
|
+
|
|
174
|
+
- Use `FetchType.EAGER` on `@ManyToOne` or `@OneToMany` -- it causes unexpected extra queries
|
|
175
|
+
- Return JPA entities from REST endpoints -- entities carry lazy proxies and hibernate state
|
|
176
|
+
- Use `CascadeType.ALL` without thinking -- cascade delete can wipe related data unexpectedly
|
|
177
|
+
- Call `entity.getCollection().size()` to check emptiness -- use a count query instead
|
|
178
|
+
- Mutate entity state outside a `@Transactional` context -- changes won't persist or may throw
|
|
179
|
+
|
|
180
|
+
## 4. Testing
|
|
181
|
+
|
|
182
|
+
**DO:** Write focused tests that verify behavior, using the right test slice.
|
|
183
|
+
|
|
184
|
+
- Use JUnit 5 `@Nested` for grouping related test scenarios:
|
|
185
|
+
```java
|
|
186
|
+
class OrderServiceTest {
|
|
187
|
+
@Nested
|
|
188
|
+
class WhenOrderIsValid {
|
|
189
|
+
@Test
|
|
190
|
+
void shouldCalculateTotal() { ... }
|
|
191
|
+
|
|
192
|
+
@Test
|
|
193
|
+
void shouldApplyDiscount() { ... }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@Nested
|
|
197
|
+
class WhenOrderIsEmpty {
|
|
198
|
+
@Test
|
|
199
|
+
void shouldThrowValidationException() { ... }
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
- Use Mockito for isolating dependencies:
|
|
204
|
+
```java
|
|
205
|
+
@ExtendWith(MockitoExtension.class)
|
|
206
|
+
class OrderServiceTest {
|
|
207
|
+
@Mock OrderRepository orderRepo;
|
|
208
|
+
@Mock PaymentGateway paymentGateway;
|
|
209
|
+
@InjectMocks OrderService service;
|
|
210
|
+
|
|
211
|
+
@Test
|
|
212
|
+
void shouldProcessPayment() {
|
|
213
|
+
when(paymentGateway.charge(any())).thenReturn(PaymentResult.success());
|
|
214
|
+
service.placeOrder(order);
|
|
215
|
+
verify(paymentGateway).charge(any());
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
- Use test slices instead of `@SpringBootTest` for faster tests:
|
|
220
|
+
```java
|
|
221
|
+
@WebMvcTest(OrderController.class) // Controller tests only
|
|
222
|
+
@DataJpaTest // Repository tests only
|
|
223
|
+
@JsonTest // JSON serialization tests only
|
|
224
|
+
```
|
|
225
|
+
- Use AssertJ for fluent, readable assertions:
|
|
226
|
+
```java
|
|
227
|
+
assertThat(orders)
|
|
228
|
+
.hasSize(3)
|
|
229
|
+
.extracting(Order::status)
|
|
230
|
+
.containsExactly(PENDING, SHIPPED, DELIVERED);
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**DON'T:**
|
|
234
|
+
|
|
235
|
+
- Use `@SpringBootTest` for every test -- it loads the full application context (slow)
|
|
236
|
+
- Mock everything -- test real behavior where possible, mock only external boundaries
|
|
237
|
+
- Write tests that depend on database state from other tests -- each test should set up its own state
|
|
238
|
+
- Use `assertEquals(expected, actual)` when AssertJ provides a more readable alternative
|
|
239
|
+
|
|
240
|
+
## 5. Common Pitfalls
|
|
241
|
+
|
|
242
|
+
**Pitfall: Mutable Date/Time**
|
|
243
|
+
Use `java.time.*` exclusively. Never use `java.util.Date` or `java.util.Calendar` -- they are mutable and error-prone. Use `Instant` for timestamps, `LocalDate` for dates without time, `ZonedDateTime` for timezone-aware dates.
|
|
244
|
+
|
|
245
|
+
**Pitfall: Checked Exceptions Overuse**
|
|
246
|
+
Reserve checked exceptions for truly recoverable conditions that the caller MUST handle (e.g., `IOException` when writing to disk). Use unchecked exceptions (`RuntimeException` subclasses) for programming errors and business rule violations.
|
|
247
|
+
|
|
248
|
+
**Pitfall: Null Returns**
|
|
249
|
+
Return `Optional<T>` from methods that may not produce a value. For collections, return empty collections instead of `null`. Use `@Nullable` annotations from `jakarta.annotation` when Optional is not appropriate (e.g., record fields).
|
|
250
|
+
|
|
251
|
+
**Pitfall: `==` vs `.equals()` for Objects**
|
|
252
|
+
`==` compares references, `.equals()` compares values. Always use `.equals()` for `String`, `Integer`, `BigDecimal`, and all objects. Exception: enum values can use `==` because enums are singletons.
|
|
253
|
+
|
|
254
|
+
**Pitfall: Raw Types**
|
|
255
|
+
Always specify generic type parameters. `List` instead of `List<String>` bypasses compile-time type safety and can cause `ClassCastException` at runtime.
|
|
256
|
+
|
|
257
|
+
**Pitfall: Synchronized Blocks in Spring Beans**
|
|
258
|
+
Spring beans are singletons by default. Using `synchronized` on a bean method serializes all requests through that method. Use `@Async`, message queues, or database-level locking instead of in-process synchronization for concurrent request handling.
|