@ranimontagna/agent-toolkit 0.1.5 → 0.1.6
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/README.md +42 -8
- package/package.json +1 -1
- package/skills/frontend/react/react-patterns/LICENSE +21 -0
- package/skills/frontend/react/react-patterns/NOTICE.md +11 -0
- package/skills/frontend/react/react-patterns/SKILL.md +341 -0
- package/skills/frontend/react/react-performance/LICENSE +21 -0
- package/skills/frontend/react/react-performance/NOTICE.md +11 -0
- package/skills/frontend/react/react-performance/SKILL.md +574 -0
- package/skills/frontend/react/react-testing/LICENSE +21 -0
- package/skills/frontend/react/react-testing/NOTICE.md +11 -0
- package/skills/frontend/react/react-testing/SKILL.md +423 -0
- package/skills/frontend/react-native/react-native-expert/LICENSE +21 -0
- package/skills/frontend/react-native/react-native-expert/NOTICE.md +11 -0
- package/skills/frontend/react-native/react-native-expert/SKILL.md +187 -0
- package/skills/frontend/react-native/react-native-expert/references/expo-router.md +187 -0
- package/skills/frontend/react-native/react-native-expert/references/list-optimization.md +204 -0
- package/skills/frontend/react-native/react-native-expert/references/platform-handling.md +188 -0
- package/skills/frontend/react-native/react-native-expert/references/project-structure.md +171 -0
- package/skills/frontend/react-native/react-native-expert/references/storage-hooks.md +173 -0
- package/skills/frontend/react-native/react-native-unistyles-v3/LICENSE +21 -0
- package/skills/frontend/react-native/react-native-unistyles-v3/NOTICE.md +11 -0
- package/skills/frontend/react-native/react-native-unistyles-v3/SKILL.md +159 -0
- package/skills/frontend/react-native/react-native-unistyles-v3/references/api-reference.md +495 -0
- package/skills/frontend/react-native/react-native-unistyles-v3/references/common-issues.md +389 -0
- package/skills/frontend/react-native/react-native-unistyles-v3/references/setup-guide.md +217 -0
- package/skills/frontend/react-native/react-native-unistyles-v3/references/styling-patterns.md +705 -0
- package/skills/frontend/react-native/react-native-unistyles-v3/references/third-party-integration.md +318 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
# Styling Patterns
|
|
2
|
+
|
|
3
|
+
Comprehensive patterns and code examples for react-native-unistyles v3.
|
|
4
|
+
|
|
5
|
+
## Basic Stylesheet
|
|
6
|
+
|
|
7
|
+
### Static styles (no theme)
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { StyleSheet } from 'react-native-unistyles'
|
|
11
|
+
|
|
12
|
+
const styles = StyleSheet.create({
|
|
13
|
+
container: {
|
|
14
|
+
flex: 1,
|
|
15
|
+
padding: 16,
|
|
16
|
+
justifyContent: 'center',
|
|
17
|
+
},
|
|
18
|
+
title: {
|
|
19
|
+
fontSize: 24,
|
|
20
|
+
fontWeight: 'bold',
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Theme-aware styles (zero re-renders)
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
const styles = StyleSheet.create(theme => ({
|
|
29
|
+
container: {
|
|
30
|
+
flex: 1,
|
|
31
|
+
backgroundColor: theme.colors.background,
|
|
32
|
+
padding: theme.spacing.md,
|
|
33
|
+
},
|
|
34
|
+
title: {
|
|
35
|
+
color: theme.colors.text,
|
|
36
|
+
fontSize: theme.fontSize.lg,
|
|
37
|
+
},
|
|
38
|
+
}))
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Theme + runtime styles (zero re-renders)
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
const styles = StyleSheet.create((theme, rt) => ({
|
|
45
|
+
container: {
|
|
46
|
+
flex: 1,
|
|
47
|
+
backgroundColor: theme.colors.background,
|
|
48
|
+
paddingTop: rt.insets.top,
|
|
49
|
+
paddingBottom: rt.insets.bottom,
|
|
50
|
+
},
|
|
51
|
+
hero: {
|
|
52
|
+
width: rt.screen.width,
|
|
53
|
+
height: rt.screen.height * 0.4,
|
|
54
|
+
},
|
|
55
|
+
}))
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Dynamic Functions
|
|
61
|
+
|
|
62
|
+
Pass arguments to styles at the call site. Arguments must be serializable.
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
const styles = StyleSheet.create(theme => ({
|
|
66
|
+
// Single argument
|
|
67
|
+
avatar: (size: number) => ({
|
|
68
|
+
width: size,
|
|
69
|
+
height: size,
|
|
70
|
+
borderRadius: size / 2,
|
|
71
|
+
}),
|
|
72
|
+
|
|
73
|
+
// Multiple arguments
|
|
74
|
+
badge: (color: string, isActive: boolean) => ({
|
|
75
|
+
backgroundColor: isActive ? color : theme.colors.disabled,
|
|
76
|
+
opacity: isActive ? 1 : 0.5,
|
|
77
|
+
}),
|
|
78
|
+
|
|
79
|
+
// Conditional with theme
|
|
80
|
+
card: (elevation: number) => ({
|
|
81
|
+
backgroundColor: theme.colors.surface,
|
|
82
|
+
borderRadius: theme.radius.md,
|
|
83
|
+
shadowOpacity: elevation * 0.1,
|
|
84
|
+
shadowRadius: elevation * 2,
|
|
85
|
+
}),
|
|
86
|
+
}))
|
|
87
|
+
|
|
88
|
+
// Usage in JSX:
|
|
89
|
+
<Image style={styles.avatar(48)} />
|
|
90
|
+
<View style={styles.badge('#ff0000', true)} />
|
|
91
|
+
<View style={styles.card(3)} />
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Breakpoint-Based Responsive Styles
|
|
97
|
+
|
|
98
|
+
Register breakpoints in `StyleSheet.configure`, then use breakpoint names as keys in style values:
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
StyleSheet.configure({
|
|
102
|
+
breakpoints: { xs: 0, sm: 576, md: 768, lg: 992, xl: 1200 }
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const styles = StyleSheet.create(theme => ({
|
|
106
|
+
container: {
|
|
107
|
+
padding: {
|
|
108
|
+
xs: 8,
|
|
109
|
+
sm: 16,
|
|
110
|
+
md: 24,
|
|
111
|
+
lg: 32,
|
|
112
|
+
},
|
|
113
|
+
flexDirection: {
|
|
114
|
+
xs: 'column',
|
|
115
|
+
md: 'row',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
sidebar: {
|
|
119
|
+
display: {
|
|
120
|
+
xs: 'none',
|
|
121
|
+
md: 'flex',
|
|
122
|
+
},
|
|
123
|
+
width: {
|
|
124
|
+
md: 250,
|
|
125
|
+
lg: 300,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
}))
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Nested breakpoints (transform, shadowOffset)
|
|
132
|
+
|
|
133
|
+
For properties expecting objects (like `transform` or `shadowOffset`), use breakpoints at the nested level:
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
const styles = StyleSheet.create({
|
|
137
|
+
box: {
|
|
138
|
+
transform: [
|
|
139
|
+
{ translateX: { xs: 0, md: 50 } },
|
|
140
|
+
{ scale: { xs: 0.8, lg: 1 } },
|
|
141
|
+
],
|
|
142
|
+
shadowOffset: {
|
|
143
|
+
width: { xs: 1, md: 2 },
|
|
144
|
+
height: { xs: 1, md: 4 },
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Built-in breakpoints: landscape / portrait
|
|
151
|
+
|
|
152
|
+
Unistyles has built-in `landscape` and `portrait` breakpoints:
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
const styles = StyleSheet.create({
|
|
156
|
+
grid: {
|
|
157
|
+
flexDirection: {
|
|
158
|
+
portrait: 'column',
|
|
159
|
+
landscape: 'row',
|
|
160
|
+
},
|
|
161
|
+
gap: {
|
|
162
|
+
portrait: 8,
|
|
163
|
+
landscape: 16,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Variants
|
|
172
|
+
|
|
173
|
+
Variants allow conditional style groups selected at the component level.
|
|
174
|
+
|
|
175
|
+
### Basic variants
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
const styles = StyleSheet.create(theme => ({
|
|
179
|
+
button: {
|
|
180
|
+
borderRadius: theme.radius.md,
|
|
181
|
+
variants: {
|
|
182
|
+
size: {
|
|
183
|
+
small: { paddingVertical: 4, paddingHorizontal: 8 },
|
|
184
|
+
medium: { paddingVertical: 8, paddingHorizontal: 16 },
|
|
185
|
+
large: { paddingVertical: 12, paddingHorizontal: 24 },
|
|
186
|
+
},
|
|
187
|
+
variant: {
|
|
188
|
+
filled: { backgroundColor: theme.colors.primary },
|
|
189
|
+
outlined: { borderWidth: 1, borderColor: theme.colors.primary },
|
|
190
|
+
ghost: { backgroundColor: 'transparent' },
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
}))
|
|
195
|
+
|
|
196
|
+
const Button = ({ size = 'medium', variant = 'filled', children }) => {
|
|
197
|
+
styles.useVariants({ size, variant })
|
|
198
|
+
return <TouchableOpacity style={styles.button}><Text>{children}</Text></TouchableOpacity>
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Boolean variants
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
const styles = StyleSheet.create(theme => ({
|
|
206
|
+
card: {
|
|
207
|
+
padding: theme.spacing.md,
|
|
208
|
+
variants: {
|
|
209
|
+
elevated: {
|
|
210
|
+
true: { shadowOpacity: 0.2, shadowRadius: 4, elevation: 3 },
|
|
211
|
+
false: { shadowOpacity: 0 },
|
|
212
|
+
},
|
|
213
|
+
disabled: {
|
|
214
|
+
true: { opacity: 0.5 },
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
}))
|
|
219
|
+
|
|
220
|
+
styles.useVariants({ elevated: true, disabled: false })
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Default variant values
|
|
224
|
+
|
|
225
|
+
Use the `default` key for a fallback when no variant is selected:
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
const styles = StyleSheet.create(theme => ({
|
|
229
|
+
text: {
|
|
230
|
+
variants: {
|
|
231
|
+
weight: {
|
|
232
|
+
default: { fontWeight: 'normal' },
|
|
233
|
+
bold: { fontWeight: 'bold' },
|
|
234
|
+
light: { fontWeight: '300' },
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
}))
|
|
239
|
+
|
|
240
|
+
// No variant selected → uses 'default'
|
|
241
|
+
styles.useVariants({})
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Compound variants
|
|
245
|
+
|
|
246
|
+
Apply styles only when multiple variant values match simultaneously:
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
const styles = StyleSheet.create(theme => ({
|
|
250
|
+
button: {
|
|
251
|
+
variants: {
|
|
252
|
+
size: {
|
|
253
|
+
small: { padding: 4 },
|
|
254
|
+
large: { padding: 16 },
|
|
255
|
+
},
|
|
256
|
+
color: {
|
|
257
|
+
primary: { backgroundColor: theme.colors.primary },
|
|
258
|
+
danger: { backgroundColor: theme.colors.danger },
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
compoundVariants: [
|
|
262
|
+
{
|
|
263
|
+
size: 'large',
|
|
264
|
+
color: 'danger',
|
|
265
|
+
styles: { borderWidth: 3, borderColor: 'red' },
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
}))
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### TypeScript inference with UnistylesVariants
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
import type { UnistylesVariants } from 'react-native-unistyles'
|
|
276
|
+
|
|
277
|
+
const styles = StyleSheet.create(theme => ({
|
|
278
|
+
chip: {
|
|
279
|
+
variants: {
|
|
280
|
+
size: { sm: { height: 24 }, md: { height: 32 }, lg: { height: 40 } },
|
|
281
|
+
color: { primary: { backgroundColor: 'blue' }, secondary: { backgroundColor: 'gray' } },
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
}))
|
|
285
|
+
|
|
286
|
+
type ChipVariants = UnistylesVariants<typeof styles>
|
|
287
|
+
// { size?: 'sm' | 'md' | 'lg'; color?: 'primary' | 'secondary' }
|
|
288
|
+
|
|
289
|
+
type ChipProps = { label: string } & ChipVariants
|
|
290
|
+
|
|
291
|
+
const Chip = ({ label, ...variants }: ChipProps) => {
|
|
292
|
+
styles.useVariants(variants)
|
|
293
|
+
return <View style={styles.chip}><Text>{label}</Text></View>
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Media Queries
|
|
300
|
+
|
|
301
|
+
Use `mq` for fine-grained width/height-based responsive values:
|
|
302
|
+
|
|
303
|
+
```tsx
|
|
304
|
+
import { mq, StyleSheet } from 'react-native-unistyles'
|
|
305
|
+
|
|
306
|
+
const styles = StyleSheet.create({
|
|
307
|
+
container: {
|
|
308
|
+
padding: {
|
|
309
|
+
[mq.only.width(null, 576)]: 8, // width <= 576
|
|
310
|
+
[mq.only.width(576, 768)]: 16, // 576 < width <= 768
|
|
311
|
+
[mq.only.width(768)]: 24, // width > 768
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
sidebar: {
|
|
315
|
+
display: {
|
|
316
|
+
[mq.only.width(null, 768)]: 'none',
|
|
317
|
+
[mq.only.width(768)]: 'flex',
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
})
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Mixing breakpoint names and pixel values
|
|
324
|
+
|
|
325
|
+
```tsx
|
|
326
|
+
mq.only.width('sm', 'lg') // between sm and lg breakpoints
|
|
327
|
+
mq.only.width(320, 768) // between 320px and 768px
|
|
328
|
+
mq.only.height(400) // height > 400px
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Combined width + height
|
|
332
|
+
|
|
333
|
+
```tsx
|
|
334
|
+
const styles = StyleSheet.create({
|
|
335
|
+
panel: {
|
|
336
|
+
flexDirection: {
|
|
337
|
+
[mq.width(null, 768).and.height(null, 500)]: 'column', // small screen
|
|
338
|
+
[mq.width(768).and.height(500)]: 'row', // large screen
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
})
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Style Merging
|
|
347
|
+
|
|
348
|
+
**NEVER spread styles.** Always use array syntax.
|
|
349
|
+
|
|
350
|
+
```tsx
|
|
351
|
+
// Combine multiple styles
|
|
352
|
+
<View style={[styles.container, styles.centered]} />
|
|
353
|
+
|
|
354
|
+
// Conditional styles
|
|
355
|
+
<View style={[styles.button, isActive && styles.active]} />
|
|
356
|
+
|
|
357
|
+
// Inline overrides
|
|
358
|
+
<View style={[styles.card, { marginTop: 20 }]} />
|
|
359
|
+
|
|
360
|
+
// Dynamic function + static
|
|
361
|
+
<View style={[styles.box(100), styles.shadow]} />
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Theme Management
|
|
367
|
+
|
|
368
|
+
### Setting up themes
|
|
369
|
+
|
|
370
|
+
```tsx
|
|
371
|
+
const lightTheme = {
|
|
372
|
+
colors: {
|
|
373
|
+
background: '#ffffff',
|
|
374
|
+
text: '#000000',
|
|
375
|
+
primary: '#007bff',
|
|
376
|
+
},
|
|
377
|
+
spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 },
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const darkTheme = {
|
|
381
|
+
colors: {
|
|
382
|
+
background: '#1a1a1a',
|
|
383
|
+
text: '#ffffff',
|
|
384
|
+
primary: '#4dabf7',
|
|
385
|
+
},
|
|
386
|
+
spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 },
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
StyleSheet.configure({
|
|
390
|
+
themes: { light: lightTheme, dark: darkTheme },
|
|
391
|
+
settings: { initialTheme: 'light' },
|
|
392
|
+
})
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Adaptive themes (auto light/dark)
|
|
396
|
+
|
|
397
|
+
```tsx
|
|
398
|
+
StyleSheet.configure({
|
|
399
|
+
themes: { light: lightTheme, dark: darkTheme },
|
|
400
|
+
settings: { adaptiveThemes: true }, // auto-switches based on OS setting
|
|
401
|
+
})
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Requires themes named exactly `light` and `dark`.
|
|
405
|
+
|
|
406
|
+
### Switching themes programmatically
|
|
407
|
+
|
|
408
|
+
```tsx
|
|
409
|
+
import { UnistylesRuntime } from 'react-native-unistyles'
|
|
410
|
+
|
|
411
|
+
// Switch to specific theme
|
|
412
|
+
UnistylesRuntime.setTheme('dark')
|
|
413
|
+
|
|
414
|
+
// Toggle
|
|
415
|
+
const toggle = () => {
|
|
416
|
+
UnistylesRuntime.setTheme(
|
|
417
|
+
UnistylesRuntime.themeName === 'light' ? 'dark' : 'light'
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Updating theme values at runtime
|
|
423
|
+
|
|
424
|
+
```tsx
|
|
425
|
+
UnistylesRuntime.updateTheme('light', current => ({
|
|
426
|
+
...current,
|
|
427
|
+
colors: { ...current.colors, primary: '#ff6600' }
|
|
428
|
+
}))
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### ScopedTheme for subtree overrides
|
|
432
|
+
|
|
433
|
+
```tsx
|
|
434
|
+
import { ScopedTheme } from 'react-native-unistyles'
|
|
435
|
+
|
|
436
|
+
// Force dark theme for a section
|
|
437
|
+
<ScopedTheme name="dark">
|
|
438
|
+
<DarkCard />
|
|
439
|
+
<DarkFooter />
|
|
440
|
+
</ScopedTheme>
|
|
441
|
+
|
|
442
|
+
// Invert: light→dark, dark→light
|
|
443
|
+
<ScopedTheme invertedAdaptive>
|
|
444
|
+
<InvertedSection />
|
|
445
|
+
</ScopedTheme>
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
## Responsive Components (Display / Hide)
|
|
451
|
+
|
|
452
|
+
```tsx
|
|
453
|
+
import { Display, Hide, mq } from 'react-native-unistyles'
|
|
454
|
+
|
|
455
|
+
const Layout = () => (
|
|
456
|
+
<View style={styles.row}>
|
|
457
|
+
{/* Only show on tablet+ */}
|
|
458
|
+
<Display mq={mq.only.width(768)}>
|
|
459
|
+
<Sidebar />
|
|
460
|
+
</Display>
|
|
461
|
+
|
|
462
|
+
<MainContent />
|
|
463
|
+
|
|
464
|
+
{/* Hide mobile nav on tablet+ */}
|
|
465
|
+
<Hide mq={mq.only.width(768)}>
|
|
466
|
+
<BottomNav />
|
|
467
|
+
</Hide>
|
|
468
|
+
</View>
|
|
469
|
+
)
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## Web-Specific Features
|
|
475
|
+
|
|
476
|
+
### _web property
|
|
477
|
+
|
|
478
|
+
Add web-only styles using the `_web` key:
|
|
479
|
+
|
|
480
|
+
```tsx
|
|
481
|
+
const styles = StyleSheet.create(theme => ({
|
|
482
|
+
button: {
|
|
483
|
+
padding: 16,
|
|
484
|
+
_web: {
|
|
485
|
+
cursor: 'pointer',
|
|
486
|
+
transition: 'background-color 0.2s ease',
|
|
487
|
+
outline: 'none',
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
}))
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Pseudo-classes
|
|
494
|
+
|
|
495
|
+
```tsx
|
|
496
|
+
const styles = StyleSheet.create(theme => ({
|
|
497
|
+
button: {
|
|
498
|
+
backgroundColor: theme.colors.primary,
|
|
499
|
+
_web: {
|
|
500
|
+
_hover: {
|
|
501
|
+
backgroundColor: theme.colors.primaryHover,
|
|
502
|
+
},
|
|
503
|
+
_active: {
|
|
504
|
+
backgroundColor: theme.colors.primaryActive,
|
|
505
|
+
transform: [{ scale: 0.98 }],
|
|
506
|
+
},
|
|
507
|
+
_focus: {
|
|
508
|
+
outlineWidth: 2,
|
|
509
|
+
outlineColor: theme.colors.focus,
|
|
510
|
+
},
|
|
511
|
+
_disabled: {
|
|
512
|
+
opacity: 0.5,
|
|
513
|
+
cursor: 'not-allowed',
|
|
514
|
+
},
|
|
515
|
+
_focusVisible: {
|
|
516
|
+
outlineStyle: 'dashed',
|
|
517
|
+
},
|
|
518
|
+
_focusWithin: {
|
|
519
|
+
borderColor: theme.colors.focus,
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
}))
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Supported pseudo-classes: `_hover`, `_active`, `_focus`, `_disabled`, `_focusVisible`, `_focusWithin`.
|
|
527
|
+
|
|
528
|
+
### Custom CSS class names
|
|
529
|
+
|
|
530
|
+
```tsx
|
|
531
|
+
const styles = StyleSheet.create({
|
|
532
|
+
container: {
|
|
533
|
+
_web: {
|
|
534
|
+
_classNames: ['my-custom-class', 'another-class'],
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
})
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### getWebProps for custom web components
|
|
541
|
+
|
|
542
|
+
```tsx
|
|
543
|
+
import { getWebProps } from 'react-native-unistyles/web-only'
|
|
544
|
+
|
|
545
|
+
const CustomWebComponent = () => {
|
|
546
|
+
const { className, style } = getWebProps(styles.container)
|
|
547
|
+
return <div className={className} style={style}>Content</div>
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## Reanimated Integration
|
|
554
|
+
|
|
555
|
+
### useAnimatedTheme — access theme in worklets
|
|
556
|
+
|
|
557
|
+
```tsx
|
|
558
|
+
import { useAnimatedTheme } from 'react-native-unistyles/reanimated'
|
|
559
|
+
import Animated, { useAnimatedStyle } from 'react-native-reanimated'
|
|
560
|
+
|
|
561
|
+
const MyComponent = () => {
|
|
562
|
+
const animatedTheme = useAnimatedTheme()
|
|
563
|
+
|
|
564
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
565
|
+
backgroundColor: animatedTheme.value.colors.background,
|
|
566
|
+
}))
|
|
567
|
+
|
|
568
|
+
// IMPORTANT: use array syntax to combine Unistyles + Reanimated styles
|
|
569
|
+
return <Animated.View style={[styles.container, animatedStyle]} />
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### useAnimatedVariantColor — animate color transitions
|
|
574
|
+
|
|
575
|
+
```tsx
|
|
576
|
+
import { useAnimatedVariantColor } from 'react-native-unistyles/reanimated'
|
|
577
|
+
|
|
578
|
+
const styles = StyleSheet.create(theme => ({
|
|
579
|
+
button: {
|
|
580
|
+
variants: {
|
|
581
|
+
state: {
|
|
582
|
+
active: { backgroundColor: theme.colors.primary },
|
|
583
|
+
inactive: { backgroundColor: theme.colors.disabled },
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
}))
|
|
588
|
+
|
|
589
|
+
const AnimatedButton = ({ isActive }) => {
|
|
590
|
+
styles.useVariants({ state: isActive ? 'active' : 'inactive' })
|
|
591
|
+
const animatedColor = useAnimatedVariantColor(styles.button, 'backgroundColor')
|
|
592
|
+
|
|
593
|
+
return <Animated.View style={[styles.button, animatedColor]} />
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### Combining Reanimated + Unistyles styles
|
|
598
|
+
|
|
599
|
+
Always use **array syntax**:
|
|
600
|
+
|
|
601
|
+
```tsx
|
|
602
|
+
<Animated.View style={[styles.container, animatedStyle]} />
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
Never spread:
|
|
606
|
+
```tsx
|
|
607
|
+
// WRONG: <Animated.View style={{ ...styles.container, ...animatedStyle }} />
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
## SSR with Next.js
|
|
613
|
+
|
|
614
|
+
### App Router
|
|
615
|
+
|
|
616
|
+
```tsx
|
|
617
|
+
// app/layout.tsx
|
|
618
|
+
import { useServerUnistyles } from 'react-native-unistyles'
|
|
619
|
+
|
|
620
|
+
export default function RootLayout({ children }) {
|
|
621
|
+
const styles = useServerUnistyles()
|
|
622
|
+
|
|
623
|
+
return (
|
|
624
|
+
<html>
|
|
625
|
+
<head>{styles}</head>
|
|
626
|
+
<body>{children}</body>
|
|
627
|
+
</html>
|
|
628
|
+
)
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// app/page.tsx (client component)
|
|
632
|
+
'use client'
|
|
633
|
+
import { hydrateServerUnistyles } from 'react-native-unistyles'
|
|
634
|
+
|
|
635
|
+
hydrateServerUnistyles()
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### Pages Router
|
|
639
|
+
|
|
640
|
+
```tsx
|
|
641
|
+
// pages/_document.tsx
|
|
642
|
+
import { getServerUnistyles, resetServerUnistyles } from 'react-native-unistyles'
|
|
643
|
+
|
|
644
|
+
export default function Document() {
|
|
645
|
+
const styles = getServerUnistyles()
|
|
646
|
+
|
|
647
|
+
return (
|
|
648
|
+
<Html>
|
|
649
|
+
<Head>{styles}</Head>
|
|
650
|
+
<body><Main /><NextScript /></body>
|
|
651
|
+
</Html>
|
|
652
|
+
)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// pages/_app.tsx
|
|
656
|
+
import { hydrateServerUnistyles } from 'react-native-unistyles'
|
|
657
|
+
|
|
658
|
+
hydrateServerUnistyles()
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
---
|
|
662
|
+
|
|
663
|
+
## Runtime Values in Styles
|
|
664
|
+
|
|
665
|
+
The mini runtime (`rt`) provides these values for use in `StyleSheet.create((theme, rt) => ...)`:
|
|
666
|
+
|
|
667
|
+
```tsx
|
|
668
|
+
const styles = StyleSheet.create((theme, rt) => ({
|
|
669
|
+
container: {
|
|
670
|
+
paddingTop: rt.insets.top,
|
|
671
|
+
paddingBottom: rt.insets.bottom,
|
|
672
|
+
paddingLeft: rt.insets.left,
|
|
673
|
+
paddingRight: rt.insets.right,
|
|
674
|
+
},
|
|
675
|
+
content: {
|
|
676
|
+
width: rt.screen.width - 32,
|
|
677
|
+
maxHeight: rt.screen.height * 0.8,
|
|
678
|
+
},
|
|
679
|
+
statusBarSpacer: {
|
|
680
|
+
height: rt.statusBar.height,
|
|
681
|
+
},
|
|
682
|
+
navBarSpacer: {
|
|
683
|
+
height: rt.navigationBar.height,
|
|
684
|
+
},
|
|
685
|
+
responsive: {
|
|
686
|
+
fontSize: rt.fontScale * 16,
|
|
687
|
+
padding: rt.pixelRatio > 2 ? 16 : 12,
|
|
688
|
+
},
|
|
689
|
+
rtlAware: {
|
|
690
|
+
textAlign: rt.rtl ? 'right' : 'left',
|
|
691
|
+
},
|
|
692
|
+
}))
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
### IME / Keyboard inset
|
|
696
|
+
|
|
697
|
+
`rt.insets.ime` provides the keyboard height, replacing `react-native-reanimated`'s `useAnimatedKeyboard`:
|
|
698
|
+
|
|
699
|
+
```tsx
|
|
700
|
+
const styles = StyleSheet.create((theme, rt) => ({
|
|
701
|
+
input: {
|
|
702
|
+
marginBottom: rt.insets.ime, // automatically adjusts when keyboard opens/closes
|
|
703
|
+
},
|
|
704
|
+
}))
|
|
705
|
+
```
|