omgkit 2.1.1 → 2.3.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/package.json +1 -1
- package/plugin/skills/databases/mongodb/SKILL.md +81 -28
- package/plugin/skills/databases/prisma/SKILL.md +87 -32
- package/plugin/skills/databases/redis/SKILL.md +80 -27
- package/plugin/skills/devops/aws/SKILL.md +80 -26
- package/plugin/skills/devops/github-actions/SKILL.md +84 -32
- package/plugin/skills/devops/kubernetes/SKILL.md +94 -32
- package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
- package/plugin/skills/frameworks/django/SKILL.md +158 -24
- package/plugin/skills/frameworks/express/SKILL.md +153 -33
- package/plugin/skills/frameworks/fastapi/SKILL.md +153 -34
- package/plugin/skills/frameworks/laravel/SKILL.md +146 -33
- package/plugin/skills/frameworks/nestjs/SKILL.md +137 -25
- package/plugin/skills/frameworks/rails/SKILL.md +594 -28
- package/plugin/skills/frameworks/react/SKILL.md +94 -962
- package/plugin/skills/frameworks/spring/SKILL.md +528 -35
- package/plugin/skills/frameworks/vue/SKILL.md +147 -25
- package/plugin/skills/frontend/accessibility/SKILL.md +145 -36
- package/plugin/skills/frontend/frontend-design/SKILL.md +114 -29
- package/plugin/skills/frontend/responsive/SKILL.md +131 -28
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +133 -43
- package/plugin/skills/frontend/tailwindcss/SKILL.md +105 -37
- package/plugin/skills/frontend/threejs/SKILL.md +110 -35
- package/plugin/skills/languages/javascript/SKILL.md +195 -34
- package/plugin/skills/methodology/brainstorming/SKILL.md +98 -30
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +83 -37
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +92 -31
- package/plugin/skills/methodology/executing-plans/SKILL.md +117 -28
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +111 -32
- package/plugin/skills/methodology/problem-solving/SKILL.md +65 -311
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +76 -27
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +93 -22
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +75 -40
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +75 -224
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +81 -35
- package/plugin/skills/methodology/test-driven-development/SKILL.md +120 -26
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +88 -35
- package/plugin/skills/methodology/token-optimization/SKILL.md +73 -34
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +128 -28
- package/plugin/skills/methodology/writing-plans/SKILL.md +105 -20
- package/plugin/skills/omega/omega-architecture/SKILL.md +178 -40
- package/plugin/skills/omega/omega-coding/SKILL.md +247 -41
- package/plugin/skills/omega/omega-sprint/SKILL.md +208 -46
- package/plugin/skills/omega/omega-testing/SKILL.md +253 -42
- package/plugin/skills/omega/omega-thinking/SKILL.md +263 -51
- package/plugin/skills/security/better-auth/SKILL.md +83 -34
- package/plugin/skills/security/oauth/SKILL.md +118 -35
- package/plugin/skills/security/owasp/SKILL.md +112 -35
- package/plugin/skills/testing/playwright/SKILL.md +141 -38
- package/plugin/skills/testing/pytest/SKILL.md +137 -38
- package/plugin/skills/testing/vitest/SKILL.md +124 -39
- package/plugin/skills/tools/document-processing/SKILL.md +111 -838
- package/plugin/skills/tools/image-processing/SKILL.md +126 -659
- package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
- package/plugin/skills/tools/media-processing/SKILL.md +118 -735
- package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
|
@@ -1,62 +1,184 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: vue
|
|
3
|
-
description: Vue
|
|
2
|
+
name: building-vue-apps
|
|
3
|
+
description: Builds production Vue 3 applications with Composition API, TypeScript, Pinia, and Vue Router. Use when creating Vue.js SPAs, component libraries, or reactive web interfaces.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Vue.js
|
|
6
|
+
# Vue.js
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Quick Start
|
|
9
9
|
|
|
10
|
-
### Component
|
|
11
10
|
```vue
|
|
12
11
|
<script setup lang="ts">
|
|
13
12
|
import { ref, computed } from 'vue';
|
|
14
13
|
|
|
15
14
|
const count = ref(0);
|
|
16
15
|
const doubled = computed(() => count.value * 2);
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<button @click="count++">
|
|
20
|
+
Count: {{ count }} (doubled: {{ doubled }})
|
|
21
|
+
</button>
|
|
22
|
+
</template>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Features
|
|
17
26
|
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
| Feature | Description | Guide |
|
|
28
|
+
|---------|-------------|-------|
|
|
29
|
+
| Composition API | Reactive state, composables, lifecycle | [COMPOSITION.md](COMPOSITION.md) |
|
|
30
|
+
| TypeScript | Props, emits, refs typing | [TYPESCRIPT.md](TYPESCRIPT.md) |
|
|
31
|
+
| Pinia | State management, stores, plugins | [PINIA.md](PINIA.md) |
|
|
32
|
+
| Vue Router | Navigation, guards, lazy loading | [ROUTER.md](ROUTER.md) |
|
|
33
|
+
| Testing | Vitest, Vue Test Utils patterns | [TESTING.md](TESTING.md) |
|
|
34
|
+
| Performance | Lazy loading, memoization, virtual lists | [PERFORMANCE.md](PERFORMANCE.md) |
|
|
35
|
+
|
|
36
|
+
## Common Patterns
|
|
37
|
+
|
|
38
|
+
### Component with Props and Emits
|
|
39
|
+
|
|
40
|
+
```vue
|
|
41
|
+
<script setup lang="ts">
|
|
42
|
+
interface Props {
|
|
43
|
+
user: { id: string; name: string; email: string };
|
|
44
|
+
showActions?: boolean;
|
|
20
45
|
}
|
|
46
|
+
|
|
47
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
48
|
+
showActions: true,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const emit = defineEmits<{
|
|
52
|
+
(e: 'edit', user: Props['user']): void;
|
|
53
|
+
(e: 'delete', id: string): void;
|
|
54
|
+
}>();
|
|
21
55
|
</script>
|
|
22
56
|
|
|
23
57
|
<template>
|
|
24
|
-
<
|
|
58
|
+
<div class="user-card">
|
|
59
|
+
<h3>{{ user.name }}</h3>
|
|
60
|
+
<p>{{ user.email }}</p>
|
|
61
|
+
<div v-if="showActions">
|
|
62
|
+
<button @click="emit('edit', user)">Edit</button>
|
|
63
|
+
<button @click="emit('delete', user.id)">Delete</button>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
25
66
|
</template>
|
|
26
67
|
```
|
|
27
68
|
|
|
28
|
-
### Composable
|
|
69
|
+
### Composable for Reusable Logic
|
|
70
|
+
|
|
29
71
|
```typescript
|
|
30
|
-
// composables/
|
|
31
|
-
|
|
32
|
-
const user = ref<User | null>(null);
|
|
33
|
-
const loading = ref(true);
|
|
72
|
+
// composables/useFetch.ts
|
|
73
|
+
import { ref, watch, type Ref } from 'vue';
|
|
34
74
|
|
|
35
|
-
|
|
75
|
+
export function useFetch<T>(url: Ref<string>) {
|
|
76
|
+
const data = ref<T | null>(null);
|
|
77
|
+
const loading = ref(false);
|
|
78
|
+
const error = ref<Error | null>(null);
|
|
79
|
+
|
|
80
|
+
async function fetchData() {
|
|
36
81
|
loading.value = true;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
82
|
+
error.value = null;
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch(url.value);
|
|
85
|
+
data.value = await response.json();
|
|
86
|
+
} catch (e) {
|
|
87
|
+
error.value = e as Error;
|
|
88
|
+
} finally {
|
|
89
|
+
loading.value = false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
watch(url, fetchData, { immediate: true });
|
|
40
94
|
|
|
41
|
-
return {
|
|
95
|
+
return { data, loading, error, refetch: fetchData };
|
|
42
96
|
}
|
|
43
97
|
```
|
|
44
98
|
|
|
45
99
|
### Pinia Store
|
|
100
|
+
|
|
46
101
|
```typescript
|
|
102
|
+
// stores/user.ts
|
|
103
|
+
import { defineStore } from 'pinia';
|
|
104
|
+
import { ref, computed } from 'vue';
|
|
105
|
+
|
|
47
106
|
export const useUserStore = defineStore('user', () => {
|
|
48
107
|
const user = ref<User | null>(null);
|
|
108
|
+
const token = ref<string | null>(null);
|
|
109
|
+
|
|
110
|
+
const isAuthenticated = computed(() => !!user.value);
|
|
49
111
|
|
|
50
|
-
async function login(email: string
|
|
51
|
-
|
|
112
|
+
async function login(credentials: { email: string; password: string }) {
|
|
113
|
+
const res = await authService.login(credentials);
|
|
114
|
+
user.value = res.user;
|
|
115
|
+
token.value = res.token;
|
|
52
116
|
}
|
|
53
117
|
|
|
54
|
-
|
|
118
|
+
function logout() {
|
|
119
|
+
user.value = null;
|
|
120
|
+
token.value = null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { user, token, isAuthenticated, login, logout };
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Workflows
|
|
128
|
+
|
|
129
|
+
### Component Development
|
|
130
|
+
|
|
131
|
+
1. Define props interface with TypeScript
|
|
132
|
+
2. Define emits with typed events
|
|
133
|
+
3. Use `<script setup>` syntax
|
|
134
|
+
4. Create composables for reusable logic
|
|
135
|
+
5. Write tests with Vitest + Vue Test Utils
|
|
136
|
+
|
|
137
|
+
### Router Setup
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
const routes = [
|
|
141
|
+
{ path: '/', component: () => import('./views/Home.vue') },
|
|
142
|
+
{
|
|
143
|
+
path: '/dashboard',
|
|
144
|
+
component: () => import('./views/Dashboard.vue'),
|
|
145
|
+
meta: { requiresAuth: true },
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
router.beforeEach((to, from, next) => {
|
|
150
|
+
const userStore = useUserStore();
|
|
151
|
+
if (to.meta.requiresAuth && !userStore.isAuthenticated) {
|
|
152
|
+
next({ path: '/login', query: { redirect: to.fullPath } });
|
|
153
|
+
} else {
|
|
154
|
+
next();
|
|
155
|
+
}
|
|
55
156
|
});
|
|
56
157
|
```
|
|
57
158
|
|
|
58
159
|
## Best Practices
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
160
|
+
|
|
161
|
+
| Do | Avoid |
|
|
162
|
+
|----|-------|
|
|
163
|
+
| Use Composition API with `<script setup>` | Options API in new code |
|
|
164
|
+
| Type props/emits with interfaces | `any` types |
|
|
165
|
+
| Create composables for shared logic | Duplicating reactive logic |
|
|
166
|
+
| Use Pinia for global state | Overusing provide/inject |
|
|
167
|
+
| Lazy load routes and components | Bundling everything upfront |
|
|
168
|
+
|
|
169
|
+
## Project Structure
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
src/
|
|
173
|
+
├── App.vue
|
|
174
|
+
├── main.ts
|
|
175
|
+
├── components/ # Reusable components
|
|
176
|
+
├── composables/ # Composition functions
|
|
177
|
+
├── views/ # Page components
|
|
178
|
+
├── stores/ # Pinia stores
|
|
179
|
+
├── router/ # Route definitions
|
|
180
|
+
├── services/ # API services
|
|
181
|
+
└── types/ # TypeScript types
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
For detailed examples and patterns, see reference files above.
|
|
@@ -1,52 +1,161 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: accessibility
|
|
3
|
-
description:
|
|
2
|
+
name: implementing-accessibility
|
|
3
|
+
description: Claude implements WCAG 2.1 AA compliant web interfaces with proper ARIA, keyboard navigation, and screen reader support. Use when building accessible components or auditing existing UIs.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Accessibility
|
|
6
|
+
# Implementing Accessibility
|
|
7
7
|
|
|
8
|
-
##
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
## Quick Start
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
// Accessible dialog with focus trap and ARIA
|
|
12
|
+
export function Dialog({ isOpen, onClose, title, children }: DialogProps) {
|
|
13
|
+
const dialogRef = useRef<HTMLDivElement>(null);
|
|
14
|
+
const titleId = useId();
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (isOpen) {
|
|
18
|
+
dialogRef.current?.focus();
|
|
19
|
+
document.body.style.overflow = 'hidden';
|
|
20
|
+
}
|
|
21
|
+
return () => { document.body.style.overflow = ''; };
|
|
22
|
+
}, [isOpen]);
|
|
23
|
+
|
|
24
|
+
if (!isOpen) return null;
|
|
25
|
+
return createPortal(
|
|
26
|
+
<>
|
|
27
|
+
<div className="dialog-backdrop" onClick={onClose} />
|
|
28
|
+
<div ref={dialogRef} role="dialog" aria-modal="true" aria-labelledby={titleId} tabIndex={-1}>
|
|
29
|
+
<h2 id={titleId}>{title}</h2>
|
|
30
|
+
{children}
|
|
31
|
+
<button onClick={onClose} aria-label="Close dialog">×</button>
|
|
32
|
+
</div>
|
|
33
|
+
</>,
|
|
34
|
+
document.body
|
|
35
|
+
);
|
|
36
|
+
}
|
|
16
37
|
```
|
|
17
38
|
|
|
18
|
-
##
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
| Feature | Description | Guide |
|
|
42
|
+
|---------|-------------|-------|
|
|
43
|
+
| Semantic HTML | Proper landmarks, headings, and native elements | `ref/semantic-structure.md` |
|
|
44
|
+
| ARIA Attributes | Roles, states, and properties for custom widgets | `ref/aria-patterns.md` |
|
|
45
|
+
| Keyboard Navigation | Tab order, roving tabindex, keyboard shortcuts | `ref/keyboard-nav.md` |
|
|
46
|
+
| Focus Management | Focus trapping, restoration, visible indicators | `ref/focus-management.md` |
|
|
47
|
+
| Live Regions | Dynamic content announcements for screen readers | `ref/live-regions.md` |
|
|
48
|
+
| Testing | jest-axe, Cypress accessibility, manual testing | `ref/a11y-testing.md` |
|
|
49
|
+
|
|
50
|
+
## Common Patterns
|
|
51
|
+
|
|
52
|
+
### Accessible Form Field
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
export function FormField({ id, label, error, hint, required, ...props }: FormFieldProps) {
|
|
56
|
+
const hintId = hint ? `${id}-hint` : undefined;
|
|
57
|
+
const errorId = error ? `${id}-error` : undefined;
|
|
58
|
+
const describedBy = [hintId, errorId].filter(Boolean).join(' ') || undefined;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className={`form-field ${error ? 'has-error' : ''}`}>
|
|
62
|
+
<label htmlFor={id}>
|
|
63
|
+
{label}
|
|
64
|
+
{required && <span aria-hidden="true">*</span>}
|
|
65
|
+
{required && <span className="sr-only">(required)</span>}
|
|
66
|
+
</label>
|
|
67
|
+
{hint && <p id={hintId} className="form-hint">{hint}</p>}
|
|
68
|
+
<input
|
|
69
|
+
id={id}
|
|
70
|
+
aria-required={required}
|
|
71
|
+
aria-invalid={!!error}
|
|
72
|
+
aria-describedby={describedBy}
|
|
73
|
+
{...props}
|
|
74
|
+
/>
|
|
75
|
+
{error && <p id={errorId} role="alert">{error}</p>}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
23
79
|
```
|
|
24
80
|
|
|
25
|
-
|
|
81
|
+
### Roving Tabindex for Tab Component
|
|
82
|
+
|
|
26
83
|
```tsx
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
84
|
+
export function Tabs({ tabs }: { tabs: Tab[] }) {
|
|
85
|
+
const [activeTab, setActiveTab] = useState(0);
|
|
86
|
+
const tabRefs = useRef<HTMLButtonElement[]>([]);
|
|
87
|
+
|
|
88
|
+
const handleKeyDown = (e: KeyboardEvent, index: number) => {
|
|
89
|
+
let newIndex = index;
|
|
90
|
+
if (e.key === 'ArrowRight') newIndex = (index + 1) % tabs.length;
|
|
91
|
+
if (e.key === 'ArrowLeft') newIndex = (index - 1 + tabs.length) % tabs.length;
|
|
92
|
+
if (e.key === 'Home') newIndex = 0;
|
|
93
|
+
if (e.key === 'End') newIndex = tabs.length - 1;
|
|
94
|
+
if (newIndex !== index) {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
setActiveTab(newIndex);
|
|
97
|
+
tabRefs.current[newIndex]?.focus();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div>
|
|
103
|
+
<div role="tablist">
|
|
104
|
+
{tabs.map((tab, i) => (
|
|
105
|
+
<button
|
|
106
|
+
key={tab.id}
|
|
107
|
+
ref={el => tabRefs.current[i] = el!}
|
|
108
|
+
role="tab"
|
|
109
|
+
aria-selected={activeTab === i}
|
|
110
|
+
aria-controls={`panel-${tab.id}`}
|
|
111
|
+
tabIndex={activeTab === i ? 0 : -1}
|
|
112
|
+
onClick={() => setActiveTab(i)}
|
|
113
|
+
onKeyDown={e => handleKeyDown(e, i)}
|
|
114
|
+
>{tab.label}</button>
|
|
115
|
+
))}
|
|
116
|
+
</div>
|
|
117
|
+
{tabs.map((tab, i) => (
|
|
118
|
+
<div key={tab.id} role="tabpanel" id={`panel-${tab.id}`} hidden={activeTab !== i} tabIndex={0}>
|
|
119
|
+
{tab.content}
|
|
120
|
+
</div>
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
34
124
|
}
|
|
35
125
|
```
|
|
36
126
|
|
|
37
|
-
|
|
127
|
+
### Live Region Announcer
|
|
128
|
+
|
|
38
129
|
```tsx
|
|
39
|
-
|
|
130
|
+
export function useAnnounce() {
|
|
131
|
+
const [message, setMessage] = useState('');
|
|
40
132
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
133
|
+
const announce = useCallback((text: string) => {
|
|
134
|
+
setMessage('');
|
|
135
|
+
requestAnimationFrame(() => setMessage(text));
|
|
136
|
+
setTimeout(() => setMessage(''), 1000);
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
const Announcer = () => (
|
|
140
|
+
<div role="status" aria-live="polite" aria-atomic="true" className="sr-only">
|
|
141
|
+
{message}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return { announce, Announcer };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Usage: announce('3 results found');
|
|
44
149
|
```
|
|
45
150
|
|
|
46
|
-
##
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
151
|
+
## Best Practices
|
|
152
|
+
|
|
153
|
+
| Do | Avoid |
|
|
154
|
+
|----|-------|
|
|
155
|
+
| Use semantic HTML elements (`<button>`, `<nav>`, `<main>`) | Removing focus outlines without replacement |
|
|
156
|
+
| Provide text alternatives for images | Relying on color alone to convey information |
|
|
157
|
+
| Ensure 4.5:1 color contrast for text | Using placeholder as the only label |
|
|
158
|
+
| Associate labels with form controls | Trapping keyboard focus unintentionally |
|
|
159
|
+
| Provide skip links for keyboard users | Auto-playing media with sound |
|
|
160
|
+
| Test with actual screen readers (NVDA, VoiceOver) | Using ARIA when native HTML suffices |
|
|
161
|
+
| Announce dynamic content changes with live regions | Very small touch targets (min 44x44px) |
|
|
@@ -1,47 +1,132 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: frontend-
|
|
3
|
-
description:
|
|
2
|
+
name: designing-frontend-patterns
|
|
3
|
+
description: Claude designs scalable React component architectures using compound components, custom hooks, and state machines. Use when building reusable UI systems or complex component APIs.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Frontend
|
|
6
|
+
# Designing Frontend Patterns
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Quick Start
|
|
9
9
|
|
|
10
|
-
### Compound Components
|
|
11
10
|
```tsx
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
<SelectContent>
|
|
15
|
-
<SelectItem value="1">Option 1</SelectItem>
|
|
16
|
-
</SelectContent>
|
|
17
|
-
</Select>
|
|
18
|
-
```
|
|
11
|
+
// Compound component pattern with context
|
|
12
|
+
const SelectContext = createContext<SelectContextValue | null>(null);
|
|
19
13
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
</
|
|
14
|
+
export function Select({ children, value, onValueChange }: SelectProps) {
|
|
15
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
16
|
+
return (
|
|
17
|
+
<SelectContext.Provider value={{ isOpen, setIsOpen, value, onValueChange }}>
|
|
18
|
+
<div className="relative">{children}</div>
|
|
19
|
+
</SelectContext.Provider>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
Select.Trigger = SelectTrigger;
|
|
24
|
+
Select.Content = SelectContent;
|
|
25
|
+
Select.Item = SelectItem;
|
|
25
26
|
```
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
| Feature | Description | Guide |
|
|
31
|
+
|---------|-------------|-------|
|
|
32
|
+
| Compound Components | Shared state via context for flexible component APIs | `ref/compound-components.md` |
|
|
33
|
+
| Custom Hooks | Encapsulate reusable logic (useDebounce, useLocalStorage) | `ref/custom-hooks.md` |
|
|
34
|
+
| Render Props | Maximum flexibility for data fetching and rendering | `ref/render-props.md` |
|
|
35
|
+
| State Machines | Predictable state transitions for complex flows | `ref/state-machines.md` |
|
|
36
|
+
| HOCs | Cross-cutting concerns (auth, error boundaries) | `ref/higher-order-components.md` |
|
|
37
|
+
| Optimistic UI | Instant feedback with rollback on failure | `ref/optimistic-updates.md` |
|
|
38
|
+
|
|
39
|
+
## Common Patterns
|
|
40
|
+
|
|
41
|
+
### Custom Hook with Cleanup
|
|
42
|
+
|
|
28
43
|
```tsx
|
|
29
|
-
function
|
|
30
|
-
const [
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
export function useDebounce<T>(value: T, delay: number): T {
|
|
45
|
+
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const timer = setTimeout(() => setDebouncedValue(value), delay);
|
|
49
|
+
return () => clearTimeout(timer);
|
|
50
|
+
}, [value, delay]);
|
|
51
|
+
|
|
52
|
+
return debouncedValue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
56
|
+
const [stored, setStored] = useState<T>(() => {
|
|
57
|
+
try {
|
|
58
|
+
const item = window.localStorage.getItem(key);
|
|
59
|
+
return item ? JSON.parse(item) : initialValue;
|
|
60
|
+
} catch { return initialValue; }
|
|
33
61
|
});
|
|
34
62
|
|
|
35
63
|
useEffect(() => {
|
|
36
|
-
localStorage.setItem(key, JSON.stringify(
|
|
37
|
-
}, [key,
|
|
64
|
+
window.localStorage.setItem(key, JSON.stringify(stored));
|
|
65
|
+
}, [key, stored]);
|
|
66
|
+
|
|
67
|
+
return [stored, setStored] as const;
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### State Machine Pattern
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
type FormState = 'idle' | 'validating' | 'submitting' | 'success' | 'error';
|
|
75
|
+
type FormEvent =
|
|
76
|
+
| { type: 'SUBMIT'; data: FormData }
|
|
77
|
+
| { type: 'SUCCESS'; response: any }
|
|
78
|
+
| { type: 'ERROR'; error: string };
|
|
38
79
|
|
|
39
|
-
|
|
80
|
+
function useFormMachine() {
|
|
81
|
+
const [state, setState] = useState<FormState>('idle');
|
|
82
|
+
const [context, setContext] = useState({ data: null, error: null });
|
|
83
|
+
|
|
84
|
+
const send = useCallback((event: FormEvent) => {
|
|
85
|
+
switch (state) {
|
|
86
|
+
case 'idle':
|
|
87
|
+
if (event.type === 'SUBMIT') { setState('validating'); }
|
|
88
|
+
break;
|
|
89
|
+
case 'submitting':
|
|
90
|
+
if (event.type === 'SUCCESS') { setState('success'); }
|
|
91
|
+
if (event.type === 'ERROR') { setState('error'); setContext(c => ({ ...c, error: event.error })); }
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}, [state]);
|
|
95
|
+
|
|
96
|
+
return { state, context, send };
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Optimistic Update Hook
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
export function useOptimistic<T>(initialData: T, reducer: (state: T, action: any) => T) {
|
|
104
|
+
const [state, setState] = useState({ data: initialData, pending: false, error: null });
|
|
105
|
+
const previousRef = useRef(initialData);
|
|
106
|
+
|
|
107
|
+
const optimisticUpdate = useCallback(async (action: any, asyncOp: () => Promise<T>) => {
|
|
108
|
+
previousRef.current = state.data;
|
|
109
|
+
setState({ data: reducer(state.data, action), pending: true, error: null });
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const result = await asyncOp();
|
|
113
|
+
setState({ data: result, pending: false, error: null });
|
|
114
|
+
} catch (error) {
|
|
115
|
+
setState({ data: previousRef.current, pending: false, error: error as Error });
|
|
116
|
+
}
|
|
117
|
+
}, [state.data, reducer]);
|
|
118
|
+
|
|
119
|
+
return { ...state, optimisticUpdate };
|
|
40
120
|
}
|
|
41
121
|
```
|
|
42
122
|
|
|
43
123
|
## Best Practices
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
124
|
+
|
|
125
|
+
| Do | Avoid |
|
|
126
|
+
|----|-------|
|
|
127
|
+
| Use compound components for complex UI with shared state | Overusing HOCs (prefer hooks) |
|
|
128
|
+
| Create custom hooks to encapsulate reusable logic | Mutating state directly |
|
|
129
|
+
| Implement state machines for complex state transitions | Deeply nested component hierarchies |
|
|
130
|
+
| Use TypeScript for type-safe component APIs | Passing too many props (use context/composition) |
|
|
131
|
+
| Use forwardRef for component library primitives | Creating components with side effects in render |
|
|
132
|
+
| Keep components focused with single responsibility | Prop drilling for deeply nested data |
|