@0xsown/vibe-code-fe 1.0.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/bin/index.js +181 -0
- package/package.json +32 -0
- package/skills/claude-md-improver/SKILL.md +179 -0
- package/skills/claude-md-improver/references/quality-criteria.md +109 -0
- package/skills/claude-md-improver/references/templates.md +253 -0
- package/skills/claude-md-improver/references/update-guidelines.md +150 -0
- package/skills/find-skills/SKILL.md +133 -0
- package/skills/frontend-design/LICENSE.txt +177 -0
- package/skills/frontend-design/SKILL.md +42 -0
- package/skills/next-best-practices/SKILL.md +153 -0
- package/skills/next-best-practices/async-patterns.md +87 -0
- package/skills/next-best-practices/bundling.md +180 -0
- package/skills/next-best-practices/data-patterns.md +297 -0
- package/skills/next-best-practices/debug-tricks.md +105 -0
- package/skills/next-best-practices/directives.md +73 -0
- package/skills/next-best-practices/error-handling.md +227 -0
- package/skills/next-best-practices/file-conventions.md +140 -0
- package/skills/next-best-practices/font.md +245 -0
- package/skills/next-best-practices/functions.md +108 -0
- package/skills/next-best-practices/hydration-error.md +91 -0
- package/skills/next-best-practices/image.md +173 -0
- package/skills/next-best-practices/metadata.md +301 -0
- package/skills/next-best-practices/parallel-routes.md +287 -0
- package/skills/next-best-practices/route-handlers.md +146 -0
- package/skills/next-best-practices/rsc-boundaries.md +159 -0
- package/skills/next-best-practices/runtime-selection.md +39 -0
- package/skills/next-best-practices/scripts.md +141 -0
- package/skills/next-best-practices/self-hosting.md +371 -0
- package/skills/next-best-practices/suspense-boundaries.md +67 -0
- package/skills/next-cache-components/SKILL.md +411 -0
- package/skills/shadcn-ui/README.md +248 -0
- package/skills/shadcn-ui/SKILL.md +326 -0
- package/skills/shadcn-ui/examples/auth-layout.tsx +177 -0
- package/skills/shadcn-ui/examples/data-table.tsx +313 -0
- package/skills/shadcn-ui/examples/form-pattern.tsx +177 -0
- package/skills/shadcn-ui/resources/component-catalog.md +481 -0
- package/skills/shadcn-ui/resources/customization-guide.md +516 -0
- package/skills/shadcn-ui/resources/migration-guide.md +463 -0
- package/skills/shadcn-ui/resources/setup-guide.md +412 -0
- package/skills/shadcn-ui/scripts/verify-setup.sh +134 -0
- package/skills/supabase-postgres-best-practices/AGENTS.md +68 -0
- package/skills/supabase-postgres-best-practices/CLAUDE.md +68 -0
- package/skills/supabase-postgres-best-practices/README.md +116 -0
- package/skills/supabase-postgres-best-practices/SKILL.md +64 -0
- package/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +55 -0
- package/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +49 -0
- package/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +46 -0
- package/skills/supabase-postgres-best-practices/references/conn-limits.md +44 -0
- package/skills/supabase-postgres-best-practices/references/conn-pooling.md +41 -0
- package/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +46 -0
- package/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +54 -0
- package/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +53 -0
- package/skills/supabase-postgres-best-practices/references/data-pagination.md +50 -0
- package/skills/supabase-postgres-best-practices/references/data-upsert.md +50 -0
- package/skills/supabase-postgres-best-practices/references/lock-advisory.md +56 -0
- package/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +68 -0
- package/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +50 -0
- package/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +54 -0
- package/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +45 -0
- package/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +55 -0
- package/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +55 -0
- package/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +44 -0
- package/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +40 -0
- package/skills/supabase-postgres-best-practices/references/query-index-types.md +48 -0
- package/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +43 -0
- package/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +45 -0
- package/skills/supabase-postgres-best-practices/references/schema-constraints.md +80 -0
- package/skills/supabase-postgres-best-practices/references/schema-data-types.md +46 -0
- package/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +59 -0
- package/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +55 -0
- package/skills/supabase-postgres-best-practices/references/schema-partitioning.md +55 -0
- package/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +61 -0
- package/skills/supabase-postgres-best-practices/references/security-privileges.md +54 -0
- package/skills/supabase-postgres-best-practices/references/security-rls-basics.md +50 -0
- package/skills/supabase-postgres-best-practices/references/security-rls-performance.md +57 -0
- package/skills/tailwind-design-system/SKILL.md +874 -0
- package/skills/vercel-composition-patterns/AGENTS.md +946 -0
- package/skills/vercel-composition-patterns/README.md +60 -0
- package/skills/vercel-composition-patterns/SKILL.md +89 -0
- package/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md +100 -0
- package/skills/vercel-composition-patterns/rules/architecture-compound-components.md +112 -0
- package/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md +87 -0
- package/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md +100 -0
- package/skills/vercel-composition-patterns/rules/react19-no-forwardref.md +42 -0
- package/skills/vercel-composition-patterns/rules/state-context-interface.md +191 -0
- package/skills/vercel-composition-patterns/rules/state-decouple-implementation.md +113 -0
- package/skills/vercel-composition-patterns/rules/state-lift-state.md +125 -0
- package/skills/vercel-react-best-practices/AGENTS.md +2934 -0
- package/skills/vercel-react-best-practices/README.md +123 -0
- package/skills/vercel-react-best-practices/SKILL.md +136 -0
- package/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
- package/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
- package/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
- package/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
- package/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
- package/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
- package/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
- package/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/skills/vercel-react-native-skills/AGENTS.md +2897 -0
- package/skills/vercel-react-native-skills/README.md +165 -0
- package/skills/vercel-react-native-skills/SKILL.md +121 -0
- package/skills/vercel-react-native-skills/rules/animation-derived-value.md +53 -0
- package/skills/vercel-react-native-skills/rules/animation-gesture-detector-press.md +95 -0
- package/skills/vercel-react-native-skills/rules/animation-gpu-properties.md +65 -0
- package/skills/vercel-react-native-skills/rules/design-system-compound-components.md +66 -0
- package/skills/vercel-react-native-skills/rules/fonts-config-plugin.md +71 -0
- package/skills/vercel-react-native-skills/rules/imports-design-system-folder.md +68 -0
- package/skills/vercel-react-native-skills/rules/js-hoist-intl.md +61 -0
- package/skills/vercel-react-native-skills/rules/list-performance-callbacks.md +44 -0
- package/skills/vercel-react-native-skills/rules/list-performance-function-references.md +132 -0
- package/skills/vercel-react-native-skills/rules/list-performance-images.md +53 -0
- package/skills/vercel-react-native-skills/rules/list-performance-inline-objects.md +97 -0
- package/skills/vercel-react-native-skills/rules/list-performance-item-expensive.md +94 -0
- package/skills/vercel-react-native-skills/rules/list-performance-item-memo.md +82 -0
- package/skills/vercel-react-native-skills/rules/list-performance-item-types.md +104 -0
- package/skills/vercel-react-native-skills/rules/list-performance-virtualize.md +67 -0
- package/skills/vercel-react-native-skills/rules/monorepo-native-deps-in-app.md +46 -0
- package/skills/vercel-react-native-skills/rules/monorepo-single-dependency-versions.md +63 -0
- package/skills/vercel-react-native-skills/rules/navigation-native-navigators.md +188 -0
- package/skills/vercel-react-native-skills/rules/react-compiler-destructure-functions.md +50 -0
- package/skills/vercel-react-native-skills/rules/react-compiler-reanimated-shared-values.md +48 -0
- package/skills/vercel-react-native-skills/rules/react-state-dispatcher.md +91 -0
- package/skills/vercel-react-native-skills/rules/react-state-fallback.md +56 -0
- package/skills/vercel-react-native-skills/rules/react-state-minimize.md +65 -0
- package/skills/vercel-react-native-skills/rules/rendering-no-falsy-and.md +74 -0
- package/skills/vercel-react-native-skills/rules/rendering-text-in-text-component.md +36 -0
- package/skills/vercel-react-native-skills/rules/scroll-position-no-state.md +82 -0
- package/skills/vercel-react-native-skills/rules/state-ground-truth.md +80 -0
- package/skills/vercel-react-native-skills/rules/ui-expo-image.md +66 -0
- package/skills/vercel-react-native-skills/rules/ui-image-gallery.md +104 -0
- package/skills/vercel-react-native-skills/rules/ui-measure-views.md +78 -0
- package/skills/vercel-react-native-skills/rules/ui-menus.md +174 -0
- package/skills/vercel-react-native-skills/rules/ui-native-modals.md +77 -0
- package/skills/vercel-react-native-skills/rules/ui-pressable.md +61 -0
- package/skills/vercel-react-native-skills/rules/ui-safe-area-scroll.md +65 -0
- package/skills/vercel-react-native-skills/rules/ui-scrollview-content-inset.md +45 -0
- package/skills/vercel-react-native-skills/rules/ui-styling.md +87 -0
- package/skills/web-design-guidelines/SKILL.md +39 -0
- package/templates/AGENTS.md +31 -0
- package/templates/CLAUDE.md +31 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Never Use && with Potentially Falsy Values
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: prevents production crash
|
|
5
|
+
tags: rendering, conditional, jsx, crash
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Never Use && with Potentially Falsy Values
|
|
9
|
+
|
|
10
|
+
Never use `{value && <Component />}` when `value` could be an empty string or
|
|
11
|
+
`0`. These are falsy but JSX-renderable—React Native will try to render them as
|
|
12
|
+
text outside a `<Text>` component, causing a hard crash in production.
|
|
13
|
+
|
|
14
|
+
**Incorrect (crashes if count is 0 or name is ""):**
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
function Profile({ name, count }: { name: string; count: number }) {
|
|
18
|
+
return (
|
|
19
|
+
<View>
|
|
20
|
+
{name && <Text>{name}</Text>}
|
|
21
|
+
{count && <Text>{count} items</Text>}
|
|
22
|
+
</View>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
// If name="" or count=0, renders the falsy value → crash
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Correct (ternary with null):**
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
function Profile({ name, count }: { name: string; count: number }) {
|
|
32
|
+
return (
|
|
33
|
+
<View>
|
|
34
|
+
{name ? <Text>{name}</Text> : null}
|
|
35
|
+
{count ? <Text>{count} items</Text> : null}
|
|
36
|
+
</View>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Correct (explicit boolean coercion):**
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
function Profile({ name, count }: { name: string; count: number }) {
|
|
45
|
+
return (
|
|
46
|
+
<View>
|
|
47
|
+
{!!name && <Text>{name}</Text>}
|
|
48
|
+
{!!count && <Text>{count} items</Text>}
|
|
49
|
+
</View>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Best (early return):**
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
function Profile({ name, count }: { name: string; count: number }) {
|
|
58
|
+
if (!name) return null
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<View>
|
|
62
|
+
<Text>{name}</Text>
|
|
63
|
+
{count > 0 ? <Text>{count} items</Text> : null}
|
|
64
|
+
</View>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Early returns are clearest. When using conditionals inline, prefer ternary or
|
|
70
|
+
explicit boolean checks.
|
|
71
|
+
|
|
72
|
+
**Lint rule:** Enable `react/jsx-no-leaked-render` from
|
|
73
|
+
[eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-leaked-render.md)
|
|
74
|
+
to catch this automatically.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Wrap Strings in Text Components
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: prevents runtime crash
|
|
5
|
+
tags: rendering, text, core
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Wrap Strings in Text Components
|
|
9
|
+
|
|
10
|
+
Strings must be rendered inside `<Text>`. React Native crashes if a string is a
|
|
11
|
+
direct child of `<View>`.
|
|
12
|
+
|
|
13
|
+
**Incorrect (crashes):**
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { View } from 'react-native'
|
|
17
|
+
|
|
18
|
+
function Greeting({ name }: { name: string }) {
|
|
19
|
+
return <View>Hello, {name}!</View>
|
|
20
|
+
}
|
|
21
|
+
// Error: Text strings must be rendered within a <Text> component.
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Correct:**
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { View, Text } from 'react-native'
|
|
28
|
+
|
|
29
|
+
function Greeting({ name }: { name: string }) {
|
|
30
|
+
return (
|
|
31
|
+
<View>
|
|
32
|
+
<Text>Hello, {name}!</Text>
|
|
33
|
+
</View>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
```
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Never Track Scroll Position in useState
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: prevents render thrashing during scroll
|
|
5
|
+
tags: scroll, performance, reanimated, useRef
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Never Track Scroll Position in useState
|
|
9
|
+
|
|
10
|
+
Never store scroll position in `useState`. Scroll events fire rapidly—state
|
|
11
|
+
updates cause render thrashing and dropped frames. Use a Reanimated shared value
|
|
12
|
+
for animations or a ref for non-reactive tracking.
|
|
13
|
+
|
|
14
|
+
**Incorrect (useState causes jank):**
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import { useState } from 'react'
|
|
18
|
+
import {
|
|
19
|
+
ScrollView,
|
|
20
|
+
NativeSyntheticEvent,
|
|
21
|
+
NativeScrollEvent,
|
|
22
|
+
} from 'react-native'
|
|
23
|
+
|
|
24
|
+
function Feed() {
|
|
25
|
+
const [scrollY, setScrollY] = useState(0)
|
|
26
|
+
|
|
27
|
+
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
28
|
+
setScrollY(e.nativeEvent.contentOffset.y) // re-renders on every frame
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return <ScrollView onScroll={onScroll} scrollEventThrottle={16} />
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Correct (Reanimated for animations):**
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import Animated, {
|
|
39
|
+
useSharedValue,
|
|
40
|
+
useAnimatedScrollHandler,
|
|
41
|
+
} from 'react-native-reanimated'
|
|
42
|
+
|
|
43
|
+
function Feed() {
|
|
44
|
+
const scrollY = useSharedValue(0)
|
|
45
|
+
|
|
46
|
+
const onScroll = useAnimatedScrollHandler({
|
|
47
|
+
onScroll: (e) => {
|
|
48
|
+
scrollY.value = e.contentOffset.y // runs on UI thread, no re-render
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Animated.ScrollView
|
|
54
|
+
onScroll={onScroll}
|
|
55
|
+
// higher number has better performance, but it fires less often.
|
|
56
|
+
// unset this if you need higher precision over performance.
|
|
57
|
+
scrollEventThrottle={16}
|
|
58
|
+
/>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Correct (ref for non-reactive tracking):**
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { useRef } from 'react'
|
|
67
|
+
import {
|
|
68
|
+
ScrollView,
|
|
69
|
+
NativeSyntheticEvent,
|
|
70
|
+
NativeScrollEvent,
|
|
71
|
+
} from 'react-native'
|
|
72
|
+
|
|
73
|
+
function Feed() {
|
|
74
|
+
const scrollY = useRef(0)
|
|
75
|
+
|
|
76
|
+
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
77
|
+
scrollY.current = e.nativeEvent.contentOffset.y // no re-render
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return <ScrollView onScroll={onScroll} scrollEventThrottle={16} />
|
|
81
|
+
}
|
|
82
|
+
```
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: State Must Represent Ground Truth
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: cleaner logic, easier debugging, single source of truth
|
|
5
|
+
tags: state, derived-state, reanimated, hooks
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## State Must Represent Ground Truth
|
|
9
|
+
|
|
10
|
+
State variables—both React `useState` and Reanimated shared values—should
|
|
11
|
+
represent the actual state of something (e.g., `pressed`, `progress`, `isOpen`),
|
|
12
|
+
not derived visual values (e.g., `scale`, `opacity`, `translateY`). Derive
|
|
13
|
+
visual values from state using computation or interpolation.
|
|
14
|
+
|
|
15
|
+
**Incorrect (storing the visual output):**
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
const scale = useSharedValue(1)
|
|
19
|
+
|
|
20
|
+
const tap = Gesture.Tap()
|
|
21
|
+
.onBegin(() => {
|
|
22
|
+
scale.set(withTiming(0.95))
|
|
23
|
+
})
|
|
24
|
+
.onFinalize(() => {
|
|
25
|
+
scale.set(withTiming(1))
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
29
|
+
transform: [{ scale: scale.get() }],
|
|
30
|
+
}))
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Correct (storing the state, deriving the visual):**
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
const pressed = useSharedValue(0) // 0 = not pressed, 1 = pressed
|
|
37
|
+
|
|
38
|
+
const tap = Gesture.Tap()
|
|
39
|
+
.onBegin(() => {
|
|
40
|
+
pressed.set(withTiming(1))
|
|
41
|
+
})
|
|
42
|
+
.onFinalize(() => {
|
|
43
|
+
pressed.set(withTiming(0))
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
47
|
+
transform: [{ scale: interpolate(pressed.get(), [0, 1], [1, 0.95]) }],
|
|
48
|
+
}))
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Why this matters:**
|
|
52
|
+
|
|
53
|
+
State variables should represent real "state", not necessarily a desired end
|
|
54
|
+
result.
|
|
55
|
+
|
|
56
|
+
1. **Single source of truth** — The state (`pressed`) describes what's
|
|
57
|
+
happening; visuals are derived
|
|
58
|
+
2. **Easier to extend** — Adding opacity, rotation, or other effects just
|
|
59
|
+
requires more interpolations from the same state
|
|
60
|
+
3. **Debugging** — Inspecting `pressed = 1` is clearer than `scale = 0.95`
|
|
61
|
+
4. **Reusable logic** — The same `pressed` value can drive multiple visual
|
|
62
|
+
properties
|
|
63
|
+
|
|
64
|
+
**Same principle for React state:**
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// Incorrect: storing derived values
|
|
68
|
+
const [isExpanded, setIsExpanded] = useState(false)
|
|
69
|
+
const [height, setHeight] = useState(0)
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
setHeight(isExpanded ? 200 : 0)
|
|
73
|
+
}, [isExpanded])
|
|
74
|
+
|
|
75
|
+
// Correct: derive from state
|
|
76
|
+
const [isExpanded, setIsExpanded] = useState(false)
|
|
77
|
+
const height = isExpanded ? 200 : 0
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
State is the minimal truth. Everything else is derived.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use expo-image for Optimized Images
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: memory efficiency, caching, blurhash placeholders, progressive loading
|
|
5
|
+
tags: images, performance, expo-image, ui
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use expo-image for Optimized Images
|
|
9
|
+
|
|
10
|
+
Use `expo-image` instead of React Native's `Image`. It provides memory-efficient caching, blurhash placeholders, progressive loading, and better performance for lists.
|
|
11
|
+
|
|
12
|
+
**Incorrect (React Native Image):**
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { Image } from 'react-native'
|
|
16
|
+
|
|
17
|
+
function Avatar({ url }: { url: string }) {
|
|
18
|
+
return <Image source={{ uri: url }} style={styles.avatar} />
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Correct (expo-image):**
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { Image } from 'expo-image'
|
|
26
|
+
|
|
27
|
+
function Avatar({ url }: { url: string }) {
|
|
28
|
+
return <Image source={{ uri: url }} style={styles.avatar} />
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**With blurhash placeholder:**
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
<Image
|
|
36
|
+
source={{ uri: url }}
|
|
37
|
+
placeholder={{ blurhash: 'LGF5]+Yk^6#M@-5c,1J5@[or[Q6.' }}
|
|
38
|
+
contentFit="cover"
|
|
39
|
+
transition={200}
|
|
40
|
+
style={styles.image}
|
|
41
|
+
/>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**With priority and caching:**
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
<Image
|
|
48
|
+
source={{ uri: url }}
|
|
49
|
+
priority="high"
|
|
50
|
+
cachePolicy="memory-disk"
|
|
51
|
+
style={styles.hero}
|
|
52
|
+
/>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Key props:**
|
|
56
|
+
|
|
57
|
+
- `placeholder` — Blurhash or thumbnail while loading
|
|
58
|
+
- `contentFit` — `cover`, `contain`, `fill`, `scale-down`
|
|
59
|
+
- `transition` — Fade-in duration (ms)
|
|
60
|
+
- `priority` — `low`, `normal`, `high`
|
|
61
|
+
- `cachePolicy` — `memory`, `disk`, `memory-disk`, `none`
|
|
62
|
+
- `recyclingKey` — Unique key for list recycling
|
|
63
|
+
|
|
64
|
+
For cross-platform (web + native), use `SolitoImage` from `solito/image` which uses `expo-image` under the hood.
|
|
65
|
+
|
|
66
|
+
Reference: [expo-image](https://docs.expo.dev/versions/latest/sdk/image/)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use Galeria for Image Galleries and Lightbox
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription:
|
|
5
|
+
native shared element transitions, pinch-to-zoom, pan-to-close
|
|
6
|
+
tags: images, gallery, lightbox, expo-image, ui
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Use Galeria for Image Galleries and Lightbox
|
|
10
|
+
|
|
11
|
+
For image galleries with lightbox (tap to fullscreen), use `@nandorojo/galeria`.
|
|
12
|
+
It provides native shared element transitions with pinch-to-zoom, double-tap
|
|
13
|
+
zoom, and pan-to-close. Works with any image component including `expo-image`.
|
|
14
|
+
|
|
15
|
+
**Incorrect (custom modal implementation):**
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
function ImageGallery({ urls }: { urls: string[] }) {
|
|
19
|
+
const [selected, setSelected] = useState<string | null>(null)
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
{urls.map((url) => (
|
|
24
|
+
<Pressable key={url} onPress={() => setSelected(url)}>
|
|
25
|
+
<Image source={{ uri: url }} style={styles.thumbnail} />
|
|
26
|
+
</Pressable>
|
|
27
|
+
))}
|
|
28
|
+
<Modal visible={!!selected} onRequestClose={() => setSelected(null)}>
|
|
29
|
+
<Image source={{ uri: selected! }} style={styles.fullscreen} />
|
|
30
|
+
</Modal>
|
|
31
|
+
</>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Correct (Galeria with expo-image):**
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { Galeria } from '@nandorojo/galeria'
|
|
40
|
+
import { Image } from 'expo-image'
|
|
41
|
+
|
|
42
|
+
function ImageGallery({ urls }: { urls: string[] }) {
|
|
43
|
+
return (
|
|
44
|
+
<Galeria urls={urls}>
|
|
45
|
+
{urls.map((url, index) => (
|
|
46
|
+
<Galeria.Image index={index} key={url}>
|
|
47
|
+
<Image source={{ uri: url }} style={styles.thumbnail} />
|
|
48
|
+
</Galeria.Image>
|
|
49
|
+
))}
|
|
50
|
+
</Galeria>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Single image:**
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { Galeria } from '@nandorojo/galeria'
|
|
59
|
+
import { Image } from 'expo-image'
|
|
60
|
+
|
|
61
|
+
function Avatar({ url }: { url: string }) {
|
|
62
|
+
return (
|
|
63
|
+
<Galeria urls={[url]}>
|
|
64
|
+
<Galeria.Image>
|
|
65
|
+
<Image source={{ uri: url }} style={styles.avatar} />
|
|
66
|
+
</Galeria.Image>
|
|
67
|
+
</Galeria>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**With low-res thumbnails and high-res fullscreen:**
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
<Galeria urls={highResUrls}>
|
|
76
|
+
{lowResUrls.map((url, index) => (
|
|
77
|
+
<Galeria.Image index={index} key={url}>
|
|
78
|
+
<Image source={{ uri: url }} style={styles.thumbnail} />
|
|
79
|
+
</Galeria.Image>
|
|
80
|
+
))}
|
|
81
|
+
</Galeria>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**With FlashList:**
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
<Galeria urls={urls}>
|
|
88
|
+
<FlashList
|
|
89
|
+
data={urls}
|
|
90
|
+
renderItem={({ item, index }) => (
|
|
91
|
+
<Galeria.Image index={index}>
|
|
92
|
+
<Image source={{ uri: item }} style={styles.thumbnail} />
|
|
93
|
+
</Galeria.Image>
|
|
94
|
+
)}
|
|
95
|
+
numColumns={3}
|
|
96
|
+
estimatedItemSize={100}
|
|
97
|
+
/>
|
|
98
|
+
</Galeria>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Works with `expo-image`, `SolitoImage`, `react-native` Image, or any image
|
|
102
|
+
component.
|
|
103
|
+
|
|
104
|
+
Reference: [Galeria](https://github.com/nandorojo/galeria)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Measuring View Dimensions
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: synchronous measurement, avoid unnecessary re-renders
|
|
5
|
+
tags: layout, measurement, onLayout, useLayoutEffect
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Measuring View Dimensions
|
|
9
|
+
|
|
10
|
+
Use both `useLayoutEffect` (synchronous) and `onLayout` (for updates). The sync
|
|
11
|
+
measurement gives you the initial size immediately; `onLayout` keeps it current
|
|
12
|
+
when the view changes. For non-primitive states, use a dispatch updater to
|
|
13
|
+
compare values and avoid unnecessary re-renders.
|
|
14
|
+
|
|
15
|
+
**Height only:**
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { useLayoutEffect, useRef, useState } from 'react'
|
|
19
|
+
import { View, LayoutChangeEvent } from 'react-native'
|
|
20
|
+
|
|
21
|
+
function MeasuredBox({ children }: { children: React.ReactNode }) {
|
|
22
|
+
const ref = useRef<View>(null)
|
|
23
|
+
const [height, setHeight] = useState<number | undefined>(undefined)
|
|
24
|
+
|
|
25
|
+
useLayoutEffect(() => {
|
|
26
|
+
// Sync measurement on mount (RN 0.82+)
|
|
27
|
+
const rect = ref.current?.getBoundingClientRect()
|
|
28
|
+
if (rect) setHeight(rect.height)
|
|
29
|
+
// Pre-0.82: ref.current?.measure((x, y, w, h) => setHeight(h))
|
|
30
|
+
}, [])
|
|
31
|
+
|
|
32
|
+
const onLayout = (e: LayoutChangeEvent) => {
|
|
33
|
+
setHeight(e.nativeEvent.layout.height)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<View ref={ref} onLayout={onLayout}>
|
|
38
|
+
{children}
|
|
39
|
+
</View>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Both dimensions:**
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { useLayoutEffect, useRef, useState } from 'react'
|
|
48
|
+
import { View, LayoutChangeEvent } from 'react-native'
|
|
49
|
+
|
|
50
|
+
type Size = { width: number; height: number }
|
|
51
|
+
|
|
52
|
+
function MeasuredBox({ children }: { children: React.ReactNode }) {
|
|
53
|
+
const ref = useRef<View>(null)
|
|
54
|
+
const [size, setSize] = useState<Size | undefined>(undefined)
|
|
55
|
+
|
|
56
|
+
useLayoutEffect(() => {
|
|
57
|
+
const rect = ref.current?.getBoundingClientRect()
|
|
58
|
+
if (rect) setSize({ width: rect.width, height: rect.height })
|
|
59
|
+
}, [])
|
|
60
|
+
|
|
61
|
+
const onLayout = (e: LayoutChangeEvent) => {
|
|
62
|
+
const { width, height } = e.nativeEvent.layout
|
|
63
|
+
setSize((prev) => {
|
|
64
|
+
// for non-primitive states, compare values before firing a re-render
|
|
65
|
+
if (prev?.width === width && prev?.height === height) return prev
|
|
66
|
+
return { width, height }
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<View ref={ref} onLayout={onLayout}>
|
|
72
|
+
{children}
|
|
73
|
+
</View>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Use functional setState to compare—don't read state directly in the callback.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use Native Menus for Dropdowns and Context Menus
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: native accessibility, platform-consistent UX
|
|
5
|
+
tags: user-interface, menus, context-menus, zeego, accessibility
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use Native Menus for Dropdowns and Context Menus
|
|
9
|
+
|
|
10
|
+
Use native platform menus instead of custom JS implementations. Native menus
|
|
11
|
+
provide built-in accessibility, consistent platform UX, and better performance.
|
|
12
|
+
Use [zeego](https://zeego.dev) for cross-platform native menus.
|
|
13
|
+
|
|
14
|
+
**Incorrect (custom JS menu):**
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import { useState } from 'react'
|
|
18
|
+
import { View, Pressable, Text } from 'react-native'
|
|
19
|
+
|
|
20
|
+
function MyMenu() {
|
|
21
|
+
const [open, setOpen] = useState(false)
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<View>
|
|
25
|
+
<Pressable onPress={() => setOpen(!open)}>
|
|
26
|
+
<Text>Open Menu</Text>
|
|
27
|
+
</Pressable>
|
|
28
|
+
{open && (
|
|
29
|
+
<View style={{ position: 'absolute', top: 40 }}>
|
|
30
|
+
<Pressable onPress={() => console.log('edit')}>
|
|
31
|
+
<Text>Edit</Text>
|
|
32
|
+
</Pressable>
|
|
33
|
+
<Pressable onPress={() => console.log('delete')}>
|
|
34
|
+
<Text>Delete</Text>
|
|
35
|
+
</Pressable>
|
|
36
|
+
</View>
|
|
37
|
+
)}
|
|
38
|
+
</View>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Correct (native menu with zeego):**
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import * as DropdownMenu from 'zeego/dropdown-menu'
|
|
47
|
+
|
|
48
|
+
function MyMenu() {
|
|
49
|
+
return (
|
|
50
|
+
<DropdownMenu.Root>
|
|
51
|
+
<DropdownMenu.Trigger>
|
|
52
|
+
<Pressable>
|
|
53
|
+
<Text>Open Menu</Text>
|
|
54
|
+
</Pressable>
|
|
55
|
+
</DropdownMenu.Trigger>
|
|
56
|
+
|
|
57
|
+
<DropdownMenu.Content>
|
|
58
|
+
<DropdownMenu.Item key='edit' onSelect={() => console.log('edit')}>
|
|
59
|
+
<DropdownMenu.ItemTitle>Edit</DropdownMenu.ItemTitle>
|
|
60
|
+
</DropdownMenu.Item>
|
|
61
|
+
|
|
62
|
+
<DropdownMenu.Item
|
|
63
|
+
key='delete'
|
|
64
|
+
destructive
|
|
65
|
+
onSelect={() => console.log('delete')}
|
|
66
|
+
>
|
|
67
|
+
<DropdownMenu.ItemTitle>Delete</DropdownMenu.ItemTitle>
|
|
68
|
+
</DropdownMenu.Item>
|
|
69
|
+
</DropdownMenu.Content>
|
|
70
|
+
</DropdownMenu.Root>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Context menu (long-press):**
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import * as ContextMenu from 'zeego/context-menu'
|
|
79
|
+
|
|
80
|
+
function MyContextMenu() {
|
|
81
|
+
return (
|
|
82
|
+
<ContextMenu.Root>
|
|
83
|
+
<ContextMenu.Trigger>
|
|
84
|
+
<View style={{ padding: 20 }}>
|
|
85
|
+
<Text>Long press me</Text>
|
|
86
|
+
</View>
|
|
87
|
+
</ContextMenu.Trigger>
|
|
88
|
+
|
|
89
|
+
<ContextMenu.Content>
|
|
90
|
+
<ContextMenu.Item key='copy' onSelect={() => console.log('copy')}>
|
|
91
|
+
<ContextMenu.ItemTitle>Copy</ContextMenu.ItemTitle>
|
|
92
|
+
</ContextMenu.Item>
|
|
93
|
+
|
|
94
|
+
<ContextMenu.Item key='paste' onSelect={() => console.log('paste')}>
|
|
95
|
+
<ContextMenu.ItemTitle>Paste</ContextMenu.ItemTitle>
|
|
96
|
+
</ContextMenu.Item>
|
|
97
|
+
</ContextMenu.Content>
|
|
98
|
+
</ContextMenu.Root>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Checkbox items:**
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
import * as DropdownMenu from 'zeego/dropdown-menu'
|
|
107
|
+
|
|
108
|
+
function SettingsMenu() {
|
|
109
|
+
const [notifications, setNotifications] = useState(true)
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<DropdownMenu.Root>
|
|
113
|
+
<DropdownMenu.Trigger>
|
|
114
|
+
<Pressable>
|
|
115
|
+
<Text>Settings</Text>
|
|
116
|
+
</Pressable>
|
|
117
|
+
</DropdownMenu.Trigger>
|
|
118
|
+
|
|
119
|
+
<DropdownMenu.Content>
|
|
120
|
+
<DropdownMenu.CheckboxItem
|
|
121
|
+
key='notifications'
|
|
122
|
+
value={notifications}
|
|
123
|
+
onValueChange={() => setNotifications((prev) => !prev)}
|
|
124
|
+
>
|
|
125
|
+
<DropdownMenu.ItemIndicator />
|
|
126
|
+
<DropdownMenu.ItemTitle>Notifications</DropdownMenu.ItemTitle>
|
|
127
|
+
</DropdownMenu.CheckboxItem>
|
|
128
|
+
</DropdownMenu.Content>
|
|
129
|
+
</DropdownMenu.Root>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Submenus:**
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
import * as DropdownMenu from 'zeego/dropdown-menu'
|
|
138
|
+
|
|
139
|
+
function MenuWithSubmenu() {
|
|
140
|
+
return (
|
|
141
|
+
<DropdownMenu.Root>
|
|
142
|
+
<DropdownMenu.Trigger>
|
|
143
|
+
<Pressable>
|
|
144
|
+
<Text>Options</Text>
|
|
145
|
+
</Pressable>
|
|
146
|
+
</DropdownMenu.Trigger>
|
|
147
|
+
|
|
148
|
+
<DropdownMenu.Content>
|
|
149
|
+
<DropdownMenu.Item key='home' onSelect={() => console.log('home')}>
|
|
150
|
+
<DropdownMenu.ItemTitle>Home</DropdownMenu.ItemTitle>
|
|
151
|
+
</DropdownMenu.Item>
|
|
152
|
+
|
|
153
|
+
<DropdownMenu.Sub>
|
|
154
|
+
<DropdownMenu.SubTrigger key='more'>
|
|
155
|
+
<DropdownMenu.ItemTitle>More Options</DropdownMenu.ItemTitle>
|
|
156
|
+
</DropdownMenu.SubTrigger>
|
|
157
|
+
|
|
158
|
+
<DropdownMenu.SubContent>
|
|
159
|
+
<DropdownMenu.Item key='settings'>
|
|
160
|
+
<DropdownMenu.ItemTitle>Settings</DropdownMenu.ItemTitle>
|
|
161
|
+
</DropdownMenu.Item>
|
|
162
|
+
|
|
163
|
+
<DropdownMenu.Item key='help'>
|
|
164
|
+
<DropdownMenu.ItemTitle>Help</DropdownMenu.ItemTitle>
|
|
165
|
+
</DropdownMenu.Item>
|
|
166
|
+
</DropdownMenu.SubContent>
|
|
167
|
+
</DropdownMenu.Sub>
|
|
168
|
+
</DropdownMenu.Content>
|
|
169
|
+
</DropdownMenu.Root>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Reference: [Zeego Documentation](https://zeego.dev/components/dropdown-menu)
|