ginskill-init 2.7.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/.wrangler/cache/pages.json +4 -0
- package/.wrangler/cache/wrangler-account.json +6 -0
- package/DEVELOPMENT.md +510 -0
- package/README.md +104 -0
- package/agents/developer.md +56 -0
- package/agents/frontend-design.md +69 -0
- package/agents/mobile-reviewer.md +36 -0
- package/agents/review-code.md +49 -0
- package/agents/security-scanner.md +50 -0
- package/agents/tester.md +72 -0
- package/bin/cli.js +461 -0
- package/landing/ai-build-ai.png +0 -0
- package/landing/index.html +1495 -0
- package/landing/logo.png +0 -0
- package/package.json +37 -0
- package/skills/active-life-dev/SKILL.md +157 -0
- package/skills/active-life-dev/docs/auth.md +187 -0
- package/skills/active-life-dev/docs/customers.md +216 -0
- package/skills/active-life-dev/docs/integrations.md +209 -0
- package/skills/active-life-dev/docs/inventory.md +192 -0
- package/skills/active-life-dev/docs/modules.md +181 -0
- package/skills/active-life-dev/docs/orders.md +180 -0
- package/skills/active-life-dev/docs/patterns.md +319 -0
- package/skills/active-life-dev/docs/products.md +216 -0
- package/skills/active-life-dev/docs/schema.md +502 -0
- package/skills/active-life-dev/docs/setup.md +169 -0
- package/skills/active-life-dev/docs/vouchers.md +144 -0
- package/skills/ai-asset-generator/SKILL.md +247 -0
- package/skills/ai-asset-generator/docs/gen-image.md +274 -0
- package/skills/ai-asset-generator/docs/genvideo.md +341 -0
- package/skills/ai-asset-generator/docs/remove-background.md +19 -0
- package/skills/ai-asset-generator/lib/bg-remove.mjs +34 -0
- package/skills/ai-asset-generator/lib/env.mjs +48 -0
- package/skills/ai-asset-generator/lib/kie-client.mjs +100 -0
- package/skills/ai-build-ai/SKILL.md +127 -0
- package/skills/ai-build-ai/docs/agent-teams.md +293 -0
- package/skills/ai-build-ai/docs/checkpointing.md +161 -0
- package/skills/ai-build-ai/docs/create-agent.md +399 -0
- package/skills/ai-build-ai/docs/create-mcp.md +395 -0
- package/skills/ai-build-ai/docs/create-skill.md +299 -0
- package/skills/ai-build-ai/docs/headless-mode.md +614 -0
- package/skills/ai-build-ai/docs/hooks.md +578 -0
- package/skills/ai-build-ai/docs/memory-claude-md.md +375 -0
- package/skills/ai-build-ai/docs/output-styles.md +208 -0
- package/skills/ai-build-ai/docs/overview.md +162 -0
- package/skills/ai-build-ai/docs/permissions.md +391 -0
- package/skills/ai-build-ai/docs/plugins.md +396 -0
- package/skills/ai-build-ai/docs/sandbox.md +262 -0
- package/skills/ai-build-ai/docs/team-lead-workflow.md +648 -0
- package/skills/ant-design/SKILL.md +323 -0
- package/skills/ant-design/docs/components.md +160 -0
- package/skills/ant-design/docs/data-entry.md +406 -0
- package/skills/ant-design/docs/display.md +594 -0
- package/skills/ant-design/docs/feedback.md +451 -0
- package/skills/ant-design/docs/key-components.md +414 -0
- package/skills/ant-design/docs/navigation.md +310 -0
- package/skills/ant-design/docs/pro-components.md +543 -0
- package/skills/ant-design/docs/setup.md +213 -0
- package/skills/ant-design/docs/theme.md +265 -0
- package/skills/flutter-performance/SKILL.md +803 -0
- package/skills/flutter-performance/references/flutter-patterns.md +595 -0
- package/skills/icon-generator/SKILL.md +270 -0
- package/skills/mobile-app-review/SKILL.md +321 -0
- package/skills/mobile-app-review/references/apple-review.md +132 -0
- package/skills/mobile-app-review/references/google-play-review.md +203 -0
- package/skills/mongodb/SKILL.md +667 -0
- package/skills/mongodb/references/mongoose-patterns.md +368 -0
- package/skills/nestjs-architecture/SKILL.md +1086 -0
- package/skills/nestjs-architecture/references/advanced-patterns.md +590 -0
- package/skills/performance/SKILL.md +509 -0
- package/skills/react-fsd-architecture/SKILL.md +693 -0
- package/skills/react-fsd-architecture/references/fsd-patterns.md +747 -0
- package/skills/react-native-expo/SKILL.md +128 -0
- package/skills/react-native-expo/references/data-layer.md +252 -0
- package/skills/react-native-expo/references/design-system.md +252 -0
- package/skills/react-native-expo/references/navigation.md +199 -0
- package/skills/react-native-expo/references/performance.md +229 -0
- package/skills/react-native-expo/references/platform-services.md +179 -0
- package/skills/react-native-expo/references/state-management.md +209 -0
- package/skills/react-native-expo/references/ui-patterns.md +301 -0
- package/skills/react-query/SKILL.md +685 -0
- package/skills/react-query/references/query-patterns.md +365 -0
- package/skills/review-code/SKILL.md +374 -0
- package/skills/review-code/references/clean-code-principles.md +395 -0
- package/skills/review-code/references/frontend-patterns.md +136 -0
- package/skills/review-code/references/nestjs-patterns.md +184 -0
- package/skills/security-scanner/SKILL.md +366 -0
- package/skills/security-scanner/references/nestjs-security.md +260 -0
- package/skills/security-scanner/references/nextjs-security.md +201 -0
- package/skills/security-scanner/references/react-native-security.md +199 -0
- package/skills/traefik/SKILL.md +105 -0
- package/skills/traefik/docs/advanced-routing.md +186 -0
- package/skills/traefik/docs/auth-providers.md +137 -0
- package/skills/traefik/docs/cicd-devops.md +396 -0
- package/skills/traefik/docs/core-config.md +171 -0
- package/skills/traefik/docs/distributed-config.md +96 -0
- package/skills/traefik/docs/docker-compose.md +182 -0
- package/skills/traefik/docs/ha-performance.md +177 -0
- package/skills/traefik/docs/kubernetes.md +278 -0
- package/skills/traefik/docs/middleware.md +205 -0
- package/skills/traefik/docs/monitoring.md +357 -0
- package/skills/traefik/docs/security.md +391 -0
- package/skills/traefik/docs/tls-acme.md +155 -0
- package/skills/ui-ux-pro-max/SKILL.md +377 -0
- package/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# State Management — Zustand v5 + MMKV
|
|
2
|
+
|
|
3
|
+
## Architecture Overview
|
|
4
|
+
|
|
5
|
+
- **Server state**: React Query (never duplicate in Zustand)
|
|
6
|
+
- **Client state**: Zustand with MMKV or AsyncStorage persistence
|
|
7
|
+
- **Secure storage**: react-native-keychain (JWT tokens only)
|
|
8
|
+
- **Fast sync storage**: MMKV (non-sensitive persisted state)
|
|
9
|
+
|
|
10
|
+
## Zustand Store Patterns
|
|
11
|
+
|
|
12
|
+
### Simple Store
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import { create } from "zustand"
|
|
16
|
+
import { persist, createJSONStorage } from "zustand/middleware"
|
|
17
|
+
import { MMKV } from "react-native-mmkv"
|
|
18
|
+
|
|
19
|
+
const storage = new MMKV()
|
|
20
|
+
const mmkvStorage = {
|
|
21
|
+
getItem: (name: string) => storage.getString(name) ?? null,
|
|
22
|
+
setItem: (name: string, value: string) => storage.set(name, value),
|
|
23
|
+
removeItem: (name: string) => storage.delete(name),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface UserStore {
|
|
27
|
+
user: UserRes | null
|
|
28
|
+
setUser: (user: UserRes | null) => void
|
|
29
|
+
updateUser: (partial: Partial<UserRes>) => void
|
|
30
|
+
clearUser: () => void
|
|
31
|
+
isPremium: () => boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const useUserStore = create<UserStore>()(
|
|
35
|
+
persist(
|
|
36
|
+
(set, get) => ({
|
|
37
|
+
user: null,
|
|
38
|
+
setUser: (user) => set({ user }),
|
|
39
|
+
updateUser: (partial) => set({ user: { ...get().user!, ...partial } }),
|
|
40
|
+
clearUser: () => set({ user: null }),
|
|
41
|
+
isPremium: () => get().user?.subscription === "premium",
|
|
42
|
+
}),
|
|
43
|
+
{
|
|
44
|
+
name: "user-store",
|
|
45
|
+
storage: createJSONStorage(() => mmkvStorage),
|
|
46
|
+
partialize: (state) => ({ user: state.user }),
|
|
47
|
+
},
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Composite Store with Slice Factories
|
|
53
|
+
|
|
54
|
+
The app uses a composite store pattern:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { StateCreator } from "zustand"
|
|
58
|
+
|
|
59
|
+
// Slice factory
|
|
60
|
+
interface StySlice {
|
|
61
|
+
styBalance: StyBalance | null
|
|
62
|
+
isCheckInSheetVisible: boolean
|
|
63
|
+
setStyBalance: (balance: StyBalance | null) => void
|
|
64
|
+
syncStyBalance: () => Promise<void>
|
|
65
|
+
hasSty: () => boolean
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const createStySlice: StateCreator<AppStore, [], [], StySlice> = (set, get) => ({
|
|
69
|
+
styBalance: null,
|
|
70
|
+
isCheckInSheetVisible: false,
|
|
71
|
+
setStyBalance: (balance) => set({ styBalance: balance }),
|
|
72
|
+
syncStyBalance: async () => {
|
|
73
|
+
try {
|
|
74
|
+
const balance = await getStyBalance()
|
|
75
|
+
set({ styBalance: balance })
|
|
76
|
+
} catch (error) {
|
|
77
|
+
appLog.error("[Sty] Failed to sync balance:", error)
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
hasSty: () => (get().styBalance?.total ?? 0) > 0,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// Composite store (app-store.ts)
|
|
84
|
+
type AppStore = AppState & ChatSlice & StySlice & AdaptySlice
|
|
85
|
+
|
|
86
|
+
export const useAppStore = create<AppStore>()(
|
|
87
|
+
persist(
|
|
88
|
+
(set, get) => ({
|
|
89
|
+
...createChatSlice(set, get),
|
|
90
|
+
...createStySlice(set, get),
|
|
91
|
+
...createAdaptySlice(set, get),
|
|
92
|
+
isAuthenticated: false,
|
|
93
|
+
isAppInitialized: false,
|
|
94
|
+
isHydrated: false,
|
|
95
|
+
setAuthenticated: (v) => set({ isAuthenticated: v }),
|
|
96
|
+
logout: () => { clearTokens(); set({ isAuthenticated: false }) },
|
|
97
|
+
initializeApp: () => set({ isAppInitialized: true }),
|
|
98
|
+
}),
|
|
99
|
+
{
|
|
100
|
+
name: "app-store",
|
|
101
|
+
storage: createJSONStorage(() => AsyncStorage),
|
|
102
|
+
partialize: (state) => ({ isAuthenticated: state.isAuthenticated }),
|
|
103
|
+
onRehydrateStorage: () => () => {
|
|
104
|
+
useAppStore.setState({ isHydrated: true })
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Using Stores in Components
|
|
112
|
+
|
|
113
|
+
**ALWAYS use `useShallow`** when selecting multiple fields:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { useShallow } from "zustand/react/shallow"
|
|
117
|
+
|
|
118
|
+
// CORRECT — useShallow prevents unnecessary re-renders
|
|
119
|
+
const { user, setUser } = useUserStore(
|
|
120
|
+
useShallow((state) => ({ user: state.user, setUser: state.setUser })),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
// CORRECT — single primitive selector (no useShallow needed)
|
|
124
|
+
const styBalance = useUserStore((state) => state.styBalance)
|
|
125
|
+
|
|
126
|
+
// WRONG — creates new object reference every render
|
|
127
|
+
const { user, setUser } = useUserStore((state) => ({
|
|
128
|
+
user: state.user,
|
|
129
|
+
setUser: state.setUser,
|
|
130
|
+
}))
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Feature Dialog Stores
|
|
134
|
+
|
|
135
|
+
Features use dedicated stores for dialog state:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
interface ItemDialogsStore {
|
|
139
|
+
activeDialog: ItemDialogTypeEnum | null
|
|
140
|
+
selectedItemId: string | null
|
|
141
|
+
openDialog: (type: ItemDialogTypeEnum, itemId: string) => void
|
|
142
|
+
closeDialog: () => void
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const useItemDialogsStore = create<ItemDialogsStore>((set) => ({
|
|
146
|
+
activeDialog: null,
|
|
147
|
+
selectedItemId: null,
|
|
148
|
+
openDialog: (type, itemId) => set({ activeDialog: type, selectedItemId: itemId }),
|
|
149
|
+
closeDialog: () => set({ activeDialog: null, selectedItemId: null }),
|
|
150
|
+
}))
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Hydration Check
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// Wait for Zustand hydration before using persisted state
|
|
157
|
+
const isHydrated = useAppStore((s) => s.isHydrated)
|
|
158
|
+
|
|
159
|
+
// Or use persist API
|
|
160
|
+
const isHydrated = useAppStore.persist.hasHydrated()
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
const unsub = useAppStore.persist.onFinishHydration(() => { /* ready */ })
|
|
163
|
+
return unsub
|
|
164
|
+
}, [])
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Existing Stores
|
|
168
|
+
|
|
169
|
+
| Store | File | Purpose |
|
|
170
|
+
|-------|------|---------|
|
|
171
|
+
| `useAppStore` | `shared/stores/app-store.ts` | Composite: auth + chat + sty + adapty slices |
|
|
172
|
+
| `useUserStore` | `shared/stores/user-store.ts` | User profile, subscription, access level |
|
|
173
|
+
| `useFeedbackStore` | `shared/stores/feedback-store.ts` | Feedback dialog state |
|
|
174
|
+
| `useTryOnStore` | `shared/stores/try-on-store.ts` | Try-on feature state |
|
|
175
|
+
| `useNewItemsStore` | `shared/stores/new-items-store.ts` | New items tracking |
|
|
176
|
+
| `useFullBodyImageStore` | `shared/stores/full-body-image-store.ts` | Full body photo state |
|
|
177
|
+
| `useVersionCheckStore` | `shared/stores/version-check-store.ts` | App version check |
|
|
178
|
+
| `useStylistNameStore` | `shared/stores/stylist-name-store.ts` | AI stylist name |
|
|
179
|
+
| `useDuplicateCheckStore` | `shared/stores/duplicate-check-store.ts` | Duplicate detection |
|
|
180
|
+
| `useUnreadStylistStore` | `shared/stores/unread-stylist-store.ts` | Unread chat badge |
|
|
181
|
+
|
|
182
|
+
## MMKV Direct Usage
|
|
183
|
+
|
|
184
|
+
For simple key-value outside Zustand:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import { MMKV } from "react-native-mmkv"
|
|
188
|
+
const storage = new MMKV()
|
|
189
|
+
|
|
190
|
+
storage.set("user.language", "en") // String
|
|
191
|
+
storage.set("onboarding.complete", true) // Boolean
|
|
192
|
+
storage.set("app.launchCount", 5) // Number
|
|
193
|
+
const lang = storage.getString("user.language")
|
|
194
|
+
storage.delete("key")
|
|
195
|
+
storage.contains("key")
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Best Practices
|
|
199
|
+
|
|
200
|
+
- **Server state in React Query**, not Zustand — never cache API responses in stores
|
|
201
|
+
- **Use `partialize`** to exclude runtime-only state from persistence
|
|
202
|
+
- **Use `useShallow`** when selecting multiple fields
|
|
203
|
+
- **Single selector for primitives** — no `useShallow` needed
|
|
204
|
+
- **Keep stores small** — split by domain
|
|
205
|
+
- **MMKV for speed** — synchronous, 30-100x faster than AsyncStorage
|
|
206
|
+
- **Keychain for secrets** — JWT tokens only in `react-native-keychain`
|
|
207
|
+
- **Batch state updates** in single `set()` call
|
|
208
|
+
- **Use `getState()`** for reading in async functions (no hook needed)
|
|
209
|
+
- **Atomic updates**: Read + write in single `set()` or use `get()` inside setter
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# UI Patterns — Forms, Bottom Sheets, Components
|
|
2
|
+
|
|
3
|
+
## Forms — react-hook-form + zod
|
|
4
|
+
|
|
5
|
+
### Schema + Hook
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { z } from "zod"
|
|
9
|
+
import { useForm, Controller } from "react-hook-form"
|
|
10
|
+
import { zodResolver } from "@hookform/resolvers/zod"
|
|
11
|
+
|
|
12
|
+
const createItemSchema = z.object({
|
|
13
|
+
name: z.string().min(1, "Name is required").max(100),
|
|
14
|
+
category: z.string().min(1, "Category is required"),
|
|
15
|
+
brand: z.string().optional(),
|
|
16
|
+
notes: z.string().max(500).optional(),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
type CreateItemForm = z.infer<typeof createItemSchema>
|
|
20
|
+
|
|
21
|
+
const useItemForm = (defaults?: Partial<CreateItemForm>) =>
|
|
22
|
+
useForm<CreateItemForm>({
|
|
23
|
+
resolver: zodResolver(createItemSchema),
|
|
24
|
+
defaultValues: { name: "", category: "", brand: "", ...defaults },
|
|
25
|
+
})
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Form Component
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
const ItemForm = () => {
|
|
32
|
+
const { control, handleSubmit, formState: { errors } } = useItemForm()
|
|
33
|
+
const { mutate: createItem, isPending } = useCreateItem()
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<View>
|
|
37
|
+
<Controller
|
|
38
|
+
control={control}
|
|
39
|
+
name="name"
|
|
40
|
+
render={({ field: { onChange, onBlur, value } }) => (
|
|
41
|
+
<TextInput
|
|
42
|
+
placeholder="Item name"
|
|
43
|
+
onChangeText={onChange} // onChangeText, NOT onChange
|
|
44
|
+
onBlur={onBlur}
|
|
45
|
+
value={value}
|
|
46
|
+
/>
|
|
47
|
+
)}
|
|
48
|
+
/>
|
|
49
|
+
{errors.name && (
|
|
50
|
+
<Typography variant="b3" color={Theme.color.error}>
|
|
51
|
+
{errors.name.message}
|
|
52
|
+
</Typography>
|
|
53
|
+
)}
|
|
54
|
+
<Button title="Save" onPress={handleSubmit((d) => createItem(d))} loading={isPending} />
|
|
55
|
+
</View>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### React Native Specifics
|
|
61
|
+
|
|
62
|
+
- Use `onChangeText` not `onChange` for TextInput
|
|
63
|
+
- Use `Controller` wrapper (RN doesn't support `ref` registration)
|
|
64
|
+
- Stick with **zod v3** syntax — v4 has RN compatibility issues
|
|
65
|
+
- Memoize `onSubmit` with `useCallback` for list forms
|
|
66
|
+
|
|
67
|
+
## Bottom Sheet — @gorhom/bottom-sheet v5
|
|
68
|
+
|
|
69
|
+
### Basic Usage
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import BottomSheet, { BottomSheetView, BottomSheetBackdrop } from "@gorhom/bottom-sheet"
|
|
73
|
+
|
|
74
|
+
const MySheet = () => {
|
|
75
|
+
const ref = useRef<BottomSheet>(null)
|
|
76
|
+
const snapPoints = useMemo(() => ["25%", "50%", "90%"], [])
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<BottomSheet
|
|
80
|
+
ref={ref}
|
|
81
|
+
snapPoints={snapPoints}
|
|
82
|
+
enableDynamicSizing={false} // Disable when using fixed snap points
|
|
83
|
+
enablePanDownToClose
|
|
84
|
+
index={-1} // Start closed
|
|
85
|
+
backdropComponent={BottomSheetBackdrop}
|
|
86
|
+
>
|
|
87
|
+
<BottomSheetView>{/* Content */}</BottomSheetView>
|
|
88
|
+
</BottomSheet>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### With Scrollable Content
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import { BottomSheetScrollView, BottomSheetFlashList } from "@gorhom/bottom-sheet"
|
|
97
|
+
|
|
98
|
+
// ScrollView
|
|
99
|
+
<BottomSheet snapPoints={["50%", "90%"]}>
|
|
100
|
+
<BottomSheetScrollView>{/* content */}</BottomSheetScrollView>
|
|
101
|
+
</BottomSheet>
|
|
102
|
+
|
|
103
|
+
// FlashList
|
|
104
|
+
<BottomSheet snapPoints={["50%", "90%"]}>
|
|
105
|
+
<BottomSheetFlashList data={items} renderItem={renderItem} estimatedItemSize={60} />
|
|
106
|
+
</BottomSheet>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Dynamic Sizing (v5 default)
|
|
110
|
+
|
|
111
|
+
v5 enables dynamic sizing by default. Disable for fixed snap points or limit height:
|
|
112
|
+
```typescript
|
|
113
|
+
<BottomSheet enableDynamicSizing={false} snapPoints={["50%"]} />
|
|
114
|
+
<BottomSheet enableDynamicSizing maxDynamicContentSize={500} />
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### ⚠️ CRITICAL: Dynamic Sizing + Scrollable Content
|
|
118
|
+
|
|
119
|
+
**NEVER** use `enableDynamicSizing={true}` with `BottomSheetScrollView` or `BottomSheetFlashList`.
|
|
120
|
+
Dynamic sizing measures children's intrinsic height via `BottomSheetView`, but scrollable content
|
|
121
|
+
has no fixed intrinsic height → sheet appears truncated (header-only).
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// ❌ WRONG — sheet will appear truncated, only showing header
|
|
125
|
+
<BottomSheetWrapper ref={ref} name="my-sheet">
|
|
126
|
+
<BottomSheetScrollView>{/* list content */}</BottomSheetScrollView>
|
|
127
|
+
</BottomSheetWrapper>
|
|
128
|
+
|
|
129
|
+
// ✅ CORRECT — disable dynamic sizing, use explicit snap points
|
|
130
|
+
const snapPoints = useMemo(() => ["50%"], [])
|
|
131
|
+
|
|
132
|
+
<BottomSheetWrapper ref={ref} name="my-sheet" enableDynamicSizing={false} snapPoints={snapPoints}>
|
|
133
|
+
<BottomSheetScrollView>{/* list content */}</BottomSheetScrollView>
|
|
134
|
+
</BottomSheetWrapper>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The app's `BottomSheetWrapper` defaults to `enableDynamicSizing={true}`. Always override when content is scrollable.
|
|
138
|
+
|
|
139
|
+
### BottomSheetWrapper (App Component)
|
|
140
|
+
|
|
141
|
+
Located at `@/shared/components/bottom-sheet`. Wraps `@gorhom/bottom-sheet` BottomSheetModal with:
|
|
142
|
+
- Auto safe-area bottom padding
|
|
143
|
+
- Header component (title + close button + drag handle)
|
|
144
|
+
- Backdrop with 0.5 opacity
|
|
145
|
+
- Patched `present()` that always cleans up first (no stuck states)
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import { BottomSheetModal } from "@gorhom/bottom-sheet"
|
|
149
|
+
import { BottomSheetWrapper } from "@/shared/components/bottom-sheet"
|
|
150
|
+
|
|
151
|
+
const ref = useRef<BottomSheetModal>(null)
|
|
152
|
+
const snapPoints = useMemo(() => ["50%"], [])
|
|
153
|
+
|
|
154
|
+
<BottomSheetWrapper
|
|
155
|
+
ref={ref}
|
|
156
|
+
name="my-sheet"
|
|
157
|
+
enableDynamicSizing={false}
|
|
158
|
+
snapPoints={snapPoints}
|
|
159
|
+
header={{
|
|
160
|
+
title: "Sheet Title",
|
|
161
|
+
showCloseButton: true,
|
|
162
|
+
closeButtonText: "Cancel",
|
|
163
|
+
}}
|
|
164
|
+
onDismiss={() => setSheetOpen(false)}
|
|
165
|
+
>
|
|
166
|
+
{/* Content */}
|
|
167
|
+
</BottomSheetWrapper>
|
|
168
|
+
|
|
169
|
+
// Open: ref.current?.present()
|
|
170
|
+
// Close: ref.current?.dismiss()
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Shared Components
|
|
174
|
+
|
|
175
|
+
### Typography
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { Typography } from "@/shared/components"
|
|
179
|
+
|
|
180
|
+
<Typography variant="h1">Title</Typography>
|
|
181
|
+
<Typography variant="b2" color={Theme.color.textSecondary}>Body</Typography>
|
|
182
|
+
<Typography variant="b3" numberOfLines={2}>Truncated</Typography>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Button
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
import { Button } from "@/shared/components"
|
|
189
|
+
|
|
190
|
+
// Variants: primary (brand+shadow), secondary (neutral), outline, ghost, danger
|
|
191
|
+
// Sizes: small (36px), medium (44px), large (52px)
|
|
192
|
+
<Button title="Save" variant="primary" onPress={handlePress} loading={isPending} />
|
|
193
|
+
<Button title="Cancel" variant="secondary" onPress={handleCancel} />
|
|
194
|
+
<Button title="Delete" variant="danger" onPress={handleDelete} />
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Image (expo-image)
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import { Image } from "expo-image"
|
|
201
|
+
|
|
202
|
+
<Image
|
|
203
|
+
source={{ uri: imageUrl }}
|
|
204
|
+
style={{ width: 120, height: 120, borderRadius: Spacing.sm }}
|
|
205
|
+
contentFit="cover"
|
|
206
|
+
transition={200}
|
|
207
|
+
cachePolicy="memory-disk"
|
|
208
|
+
recyclingKey={item._id} // Important in lists
|
|
209
|
+
/>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Loading Skeletons
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import { Shimmer } from "@/shared/components"
|
|
216
|
+
|
|
217
|
+
{isLoading ? (
|
|
218
|
+
<Shimmer width={200} height={20} borderRadius={4} />
|
|
219
|
+
) : (
|
|
220
|
+
<Typography variant="b1">{data.name}</Typography>
|
|
221
|
+
)}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Platform-specific: `shimmer.ios.tsx` / `shimmer.android.tsx`
|
|
225
|
+
|
|
226
|
+
### Toast Messages
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
import { showSuccessToast, showErrorToast } from "@/shared/components/toast"
|
|
230
|
+
|
|
231
|
+
showSuccessToast("Success", "Item saved successfully")
|
|
232
|
+
showErrorToast("Error", "Failed to save item")
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Dialogs
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
// Pattern: Zustand store controls dialog visibility
|
|
239
|
+
const { isOpen, open, close } = useDialogStore(
|
|
240
|
+
useShallow((s) => ({ isOpen: s.isOpen, open: s.open, close: s.close }))
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
<Dialog visible={isOpen} onDismiss={close}>
|
|
244
|
+
<Dialog.Title>Confirm</Dialog.Title>
|
|
245
|
+
<Dialog.Content><Typography variant="b1">Are you sure?</Typography></Dialog.Content>
|
|
246
|
+
<Dialog.Actions>
|
|
247
|
+
<Button title="Cancel" variant="secondary" onPress={close} />
|
|
248
|
+
<Button title="Delete" variant="danger" onPress={handleDelete} />
|
|
249
|
+
</Dialog.Actions>
|
|
250
|
+
</Dialog>
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Glass Morphism Components
|
|
254
|
+
|
|
255
|
+
Available in `@/shared/components/`: `GlassCard`, `GlassButton`, `GlassInput`
|
|
256
|
+
|
|
257
|
+
## Keyboard Handling
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
import { KeyboardAwareScrollView } from "react-native-keyboard-controller"
|
|
261
|
+
|
|
262
|
+
<KeyboardAwareScrollView>{/* Form content */}</KeyboardAwareScrollView>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Image Picking & Compression
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
import * as ImagePicker from "expo-image-picker"
|
|
269
|
+
import { compressImage } from "@/shared/utils/compress-image"
|
|
270
|
+
|
|
271
|
+
const result = await ImagePicker.launchImageLibraryAsync({
|
|
272
|
+
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
|
273
|
+
quality: 0.8,
|
|
274
|
+
allowsEditing: true,
|
|
275
|
+
aspect: [1, 1],
|
|
276
|
+
})
|
|
277
|
+
if (!result.canceled) {
|
|
278
|
+
const compressed = await compressImage(result.assets[0].uri)
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Calendar
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
import { Calendar } from "@marceloterreiro/flash-calendar"
|
|
286
|
+
|
|
287
|
+
<Calendar
|
|
288
|
+
calendarActiveDateRanges={activeDateRanges}
|
|
289
|
+
onCalendarDayPress={handleDayPress}
|
|
290
|
+
/>
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Popover
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
import Popover from "react-native-popover-view"
|
|
297
|
+
|
|
298
|
+
<Popover from={<TouchableOpacity><Icon name="more" /></TouchableOpacity>}>
|
|
299
|
+
{/* Menu content */}
|
|
300
|
+
</Popover>
|
|
301
|
+
```
|