@tamagui/native 2.6.4 → 2.7.1

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.
Files changed (69) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/cjs/expoUIMenuAdapter.cjs +271 -0
  3. package/dist/cjs/expoUIMenuAdapter.native.js +327 -0
  4. package/dist/cjs/expoUIMenuAdapter.native.js.map +1 -0
  5. package/dist/cjs/index.cjs +5 -2
  6. package/dist/cjs/index.native.js +5 -2
  7. package/dist/cjs/index.native.js.map +1 -1
  8. package/dist/cjs/{zeegoState.cjs → nativeMenuState.cjs} +19 -19
  9. package/dist/cjs/{zeegoState.native.js → nativeMenuState.native.js} +20 -20
  10. package/dist/cjs/nativeMenuState.native.js.map +1 -0
  11. package/dist/cjs/setup-expo-ui-menu.cjs +17 -0
  12. package/dist/cjs/setup-expo-ui-menu.native.js +20 -0
  13. package/dist/cjs/setup-expo-ui-menu.native.js.map +1 -0
  14. package/dist/cjs/setup-zeego.cjs +11 -13
  15. package/dist/cjs/setup-zeego.native.js +11 -13
  16. package/dist/cjs/setup-zeego.native.js.map +1 -1
  17. package/dist/esm/expoUIMenuAdapter.mjs +235 -0
  18. package/dist/esm/expoUIMenuAdapter.mjs.map +1 -0
  19. package/dist/esm/expoUIMenuAdapter.native.js +288 -0
  20. package/dist/esm/expoUIMenuAdapter.native.js.map +1 -0
  21. package/dist/esm/index.js +3 -2
  22. package/dist/esm/index.js.map +1 -1
  23. package/dist/esm/index.mjs +3 -2
  24. package/dist/esm/index.mjs.map +1 -1
  25. package/dist/esm/index.native.js +3 -2
  26. package/dist/esm/index.native.js.map +1 -1
  27. package/dist/esm/nativeMenuState.mjs +20 -0
  28. package/dist/esm/nativeMenuState.mjs.map +1 -0
  29. package/dist/esm/nativeMenuState.native.js +20 -0
  30. package/dist/esm/nativeMenuState.native.js.map +1 -0
  31. package/dist/esm/setup-expo-ui-menu.mjs +18 -0
  32. package/dist/esm/setup-expo-ui-menu.mjs.map +1 -0
  33. package/dist/esm/setup-expo-ui-menu.native.js +18 -0
  34. package/dist/esm/setup-expo-ui-menu.native.js.map +1 -0
  35. package/dist/esm/setup-zeego.mjs +11 -13
  36. package/dist/esm/setup-zeego.mjs.map +1 -1
  37. package/dist/esm/setup-zeego.native.js +11 -13
  38. package/dist/esm/setup-zeego.native.js.map +1 -1
  39. package/package.json +14 -2
  40. package/setup-expo-ui-menu/index.cjs +1 -0
  41. package/setup-expo-ui-menu/index.js +1 -0
  42. package/setup-expo-ui-menu/index.native.cjs +1 -0
  43. package/setup-expo-ui-menu/index.native.js +1 -0
  44. package/setup-expo-ui-menu.cjs +1 -0
  45. package/src/expoUIMenuAdapter.tsx +336 -0
  46. package/src/index.ts +5 -4
  47. package/src/nativeMenuState.ts +63 -0
  48. package/src/setup-expo-ui-menu.ts +25 -0
  49. package/src/setup-zeego.ts +12 -16
  50. package/src/types.ts +0 -6
  51. package/types/expoUIMenuAdapter.d.ts +8 -0
  52. package/types/expoUIMenuAdapter.d.ts.map +11 -0
  53. package/types/index.d.ts +5 -3
  54. package/types/index.d.ts.map +2 -2
  55. package/types/nativeMenuState.d.ts +38 -0
  56. package/types/nativeMenuState.d.ts.map +11 -0
  57. package/types/setup-expo-ui-menu.d.ts +3 -0
  58. package/types/setup-expo-ui-menu.d.ts.map +11 -0
  59. package/types/setup-zeego.d.ts.map +1 -1
  60. package/types/types.d.ts +0 -5
  61. package/types/types.d.ts.map +2 -2
  62. package/dist/cjs/zeegoState.native.js.map +0 -1
  63. package/dist/esm/zeegoState.mjs +0 -21
  64. package/dist/esm/zeegoState.mjs.map +0 -1
  65. package/dist/esm/zeegoState.native.js +0 -21
  66. package/dist/esm/zeegoState.native.js.map +0 -1
  67. package/src/zeegoState.ts +0 -28
  68. package/types/zeegoState.d.ts +0 -9
  69. package/types/zeegoState.d.ts.map +0 -11
@@ -0,0 +1,336 @@
1
+ import type { ComponentType, ReactElement, ReactNode } from 'react'
2
+ import React from 'react'
3
+
4
+ import type { NativeMenuAdapter, NativeMenuModule } from './nativeMenuState'
5
+
6
+ type ExpoMenuAction = {
7
+ id: string
8
+ title: string
9
+ image?: unknown
10
+ state?: 'on' | 'off'
11
+ attributes?: {
12
+ destructive?: boolean
13
+ disabled?: boolean
14
+ hidden?: boolean
15
+ }
16
+ subactions?: ExpoMenuAction[]
17
+ displayInline?: boolean
18
+ }
19
+
20
+ type ExpoMenuViewProps = {
21
+ actions: ExpoMenuAction[]
22
+ title?: string
23
+ shouldOpenOnLongPress?: boolean
24
+ onPressAction?: (event: { nativeEvent: { event: string } }) => void
25
+ onOpenMenu?: () => void
26
+ onCloseMenu?: () => void
27
+ children?: ReactNode
28
+ }
29
+
30
+ export type ExpoUIMenuAdapterOptions = {
31
+ MenuView: ComponentType<any>
32
+ }
33
+
34
+ type MarkerProps = {
35
+ children?: ReactNode
36
+ [key: string]: unknown
37
+ }
38
+
39
+ function marker(name: string): ComponentType<MarkerProps> {
40
+ const Marker = () => null
41
+ Marker.displayName = `ExpoUI${name}`
42
+ return Marker
43
+ }
44
+
45
+ function isMarkerType(candidate: unknown, type: ComponentType<MarkerProps>): boolean {
46
+ if (candidate === type) return true
47
+ // createMenu runs every native component through withNativeMenu, which wraps
48
+ // it in a new function component and copies the marker's displayName onto the
49
+ // wrapper. that wrapper is a different reference, so an identity-only check
50
+ // matches nothing and every item silently comes back with an empty title, no
51
+ // icon, and no submenu. fall back to the (unique, ExpoUI-prefixed) name.
52
+ const wanted = (type as { displayName?: string }).displayName
53
+ if (!wanted) return false
54
+ return (
55
+ typeof candidate === 'function' &&
56
+ (candidate as { displayName?: string }).displayName === wanted
57
+ )
58
+ }
59
+
60
+ function elementOfType(
61
+ children: ReactNode,
62
+ type: ComponentType<MarkerProps>
63
+ ): ReactElement<MarkerProps> | null {
64
+ for (const child of React.Children.toArray(children)) {
65
+ if (React.isValidElement<MarkerProps>(child) && isMarkerType(child.type, type)) {
66
+ return child
67
+ }
68
+ }
69
+ return null
70
+ }
71
+
72
+ function textFromNode(node: ReactNode): string {
73
+ if (typeof node === 'string' || typeof node === 'number') return String(node)
74
+ if (!React.isValidElement<MarkerProps>(node)) return ''
75
+ return React.Children.toArray(node.props.children).map(textFromNode).join('')
76
+ }
77
+
78
+ export function createExpoUIMenuAdapter({
79
+ MenuView,
80
+ }: ExpoUIMenuAdapterOptions): NativeMenuAdapter {
81
+ const NativeMenuView: ComponentType<ExpoMenuViewProps> = MenuView
82
+
83
+ function createModule(isContextMenu: boolean): NativeMenuModule {
84
+ const Trigger = marker('Trigger')
85
+ const Content = marker('Content')
86
+ const Item = marker('Item')
87
+ const ItemTitle = marker('ItemTitle')
88
+ const ItemSubtitle = marker('ItemSubtitle')
89
+ const ItemIcon = marker('ItemIcon')
90
+ const ItemImage = marker('ItemImage')
91
+ const ItemIndicator = marker('ItemIndicator')
92
+ const Group = marker('Group')
93
+ const Label = marker('Label')
94
+ const Separator = marker('Separator')
95
+ const Sub = marker('Sub')
96
+ const SubTrigger = marker('SubTrigger')
97
+ const SubContent = marker('SubContent')
98
+ const CheckboxItem = marker('CheckboxItem')
99
+ const Preview = marker('Preview')
100
+ const Auxiliary = marker('Auxiliary')
101
+
102
+ function Root({
103
+ children,
104
+ onOpenChange,
105
+ onOpenWillChange,
106
+ }: {
107
+ children?: ReactNode
108
+ onOpenChange?: (open: boolean) => void
109
+ onOpenWillChange?: (open: boolean) => void
110
+ }) {
111
+ const trigger = elementOfType(children, Trigger)
112
+ const content = elementOfType(children, Content)
113
+ const preview = elementOfType(children, Preview)
114
+ const auxiliary = elementOfType(children, Auxiliary)
115
+
116
+ if (preview || auxiliary) {
117
+ throw new Error(
118
+ '@expo/ui/community/menu does not support Tamagui context-menu previews or auxiliary views'
119
+ )
120
+ }
121
+ if (!trigger || !content) {
122
+ throw new Error('Tamagui native menus require one Trigger and one Content')
123
+ }
124
+
125
+ let nextId = 0
126
+ const handlers = new Map<string, () => void>()
127
+
128
+ function actionId(element: ReactElement<MarkerProps>): string {
129
+ return element.key == null
130
+ ? `tamagui-menu-${nextId++}`
131
+ : String(element.key).replace(/^\.\$/, '')
132
+ }
133
+
134
+ function itemTitle(element: ReactElement<MarkerProps>): string {
135
+ if (typeof element.props.textValue === 'string') return element.props.textValue
136
+ const title = elementOfType(element.props.children, ItemTitle)
137
+ return title ? textFromNode(title.props.children) : ''
138
+ }
139
+
140
+ function actionImage(element: ReactElement<MarkerProps>): unknown {
141
+ const image = elementOfType(element.props.children, ItemImage)
142
+ if (image?.props.source) return image.props.source
143
+ const icon = elementOfType(element.props.children, ItemIcon)
144
+ if (!icon) return undefined
145
+ const ios = icon.props.ios
146
+ if (
147
+ typeof ios === 'object' &&
148
+ ios &&
149
+ 'name' in ios &&
150
+ typeof ios.name === 'string'
151
+ ) {
152
+ return ios.name
153
+ }
154
+ return typeof icon.props.iosIconName === 'string'
155
+ ? icon.props.iosIconName
156
+ : undefined
157
+ }
158
+
159
+ function attributes(
160
+ element: ReactElement<MarkerProps>
161
+ ): ExpoMenuAction['attributes'] {
162
+ const destructive = element.props.destructive === true
163
+ const disabled = element.props.disabled === true
164
+ const hidden = element.props.hidden === true
165
+ return destructive || disabled || hidden
166
+ ? { destructive, disabled, hidden }
167
+ : undefined
168
+ }
169
+
170
+ function actionsFrom(childrenToParse: ReactNode): ExpoMenuAction[] {
171
+ const actions: ExpoMenuAction[] = []
172
+ let section: ExpoMenuAction[] | null = null
173
+
174
+ function append(action: ExpoMenuAction) {
175
+ if (section) {
176
+ section.push(action)
177
+ } else {
178
+ actions.push(action)
179
+ }
180
+ }
181
+
182
+ function flushSection() {
183
+ if (!section?.length) {
184
+ section = []
185
+ return
186
+ }
187
+ actions.push({
188
+ id: `tamagui-menu-section-${nextId++}`,
189
+ title: '',
190
+ subactions: section,
191
+ displayInline: true,
192
+ })
193
+ section = []
194
+ }
195
+
196
+ for (const child of React.Children.toArray(childrenToParse)) {
197
+ if (!React.isValidElement<MarkerProps>(child)) continue
198
+
199
+ if (isMarkerType(child.type, Separator)) {
200
+ if (!section) section = actions.splice(0)
201
+ flushSection()
202
+ continue
203
+ }
204
+
205
+ if (isMarkerType(child.type, Group)) {
206
+ const label = elementOfType(child.props.children, Label)
207
+ append({
208
+ id: actionId(child),
209
+ title: label ? textFromNode(label.props.children) : '',
210
+ subactions: actionsFrom(child.props.children),
211
+ displayInline: true,
212
+ })
213
+ continue
214
+ }
215
+
216
+ if (isMarkerType(child.type, Sub)) {
217
+ const subTrigger = elementOfType(child.props.children, SubTrigger)
218
+ const subContent = elementOfType(child.props.children, SubContent)
219
+ if (!subTrigger || !subContent) {
220
+ throw new Error(
221
+ 'Tamagui native submenus require one SubTrigger and SubContent'
222
+ )
223
+ }
224
+ append({
225
+ id: actionId(child),
226
+ title: itemTitle(subTrigger),
227
+ image: actionImage(subTrigger),
228
+ attributes: attributes(subTrigger),
229
+ subactions: actionsFrom(subContent.props.children),
230
+ })
231
+ continue
232
+ }
233
+
234
+ if (!isMarkerType(child.type, Item) && !isMarkerType(child.type, CheckboxItem))
235
+ continue
236
+
237
+ if (elementOfType(child.props.children, ItemSubtitle)) {
238
+ throw new Error(
239
+ '@expo/ui/community/menu does not support menu item subtitles'
240
+ )
241
+ }
242
+
243
+ const id = actionId(child)
244
+ if (isMarkerType(child.type, CheckboxItem)) {
245
+ const current =
246
+ child.props.value === true ||
247
+ child.props.value === 'on' ||
248
+ child.props.checked === true
249
+ const onValueChange = child.props.onValueChange
250
+ const onCheckedChange = child.props.onCheckedChange
251
+ handlers.set(id, () => {
252
+ if (typeof onValueChange === 'function') {
253
+ onValueChange(current ? 'off' : 'on', current ? 'on' : 'off')
254
+ } else if (typeof onCheckedChange === 'function') {
255
+ onCheckedChange(!current)
256
+ }
257
+ })
258
+ append({
259
+ id,
260
+ title: itemTitle(child),
261
+ image: actionImage(child),
262
+ state: current ? 'on' : 'off',
263
+ attributes: attributes(child),
264
+ })
265
+ continue
266
+ }
267
+
268
+ const onSelect = child.props.onSelect
269
+ if (typeof onSelect === 'function') {
270
+ handlers.set(id, () => onSelect())
271
+ }
272
+ append({
273
+ id,
274
+ title: itemTitle(child),
275
+ image: actionImage(child),
276
+ attributes: attributes(child),
277
+ })
278
+ }
279
+
280
+ if (section) flushSection()
281
+ return actions
282
+ }
283
+
284
+ const label = elementOfType(content.props.children, Label)
285
+ const triggerAction = trigger.props.action
286
+
287
+ return (
288
+ <NativeMenuView
289
+ actions={actionsFrom(content.props.children)}
290
+ title={label ? textFromNode(label.props.children) : undefined}
291
+ shouldOpenOnLongPress={
292
+ triggerAction === 'longPress' || (triggerAction !== 'press' && isContextMenu)
293
+ }
294
+ onPressAction={(event) => handlers.get(event.nativeEvent.event)?.()}
295
+ onOpenMenu={() => {
296
+ onOpenWillChange?.(true)
297
+ onOpenChange?.(true)
298
+ }}
299
+ onCloseMenu={() => {
300
+ onOpenWillChange?.(false)
301
+ onOpenChange?.(false)
302
+ }}
303
+ >
304
+ {trigger.props.children}
305
+ </NativeMenuView>
306
+ )
307
+ }
308
+
309
+ return {
310
+ Root,
311
+ Trigger,
312
+ Content,
313
+ Item,
314
+ ItemTitle,
315
+ ItemSubtitle,
316
+ ItemIcon,
317
+ ItemImage,
318
+ ItemIndicator,
319
+ Group,
320
+ Label,
321
+ Separator,
322
+ Sub,
323
+ SubTrigger,
324
+ SubContent,
325
+ CheckboxItem,
326
+ Preview,
327
+ Auxiliary,
328
+ }
329
+ }
330
+
331
+ return {
332
+ name: 'expo-ui',
333
+ Menu: createModule(false),
334
+ ContextMenu: createModule(true),
335
+ }
336
+ }
package/src/index.ts CHANGED
@@ -29,12 +29,12 @@ export type {
29
29
  SafeAreaFrame,
30
30
  SafeAreaMetrics,
31
31
  LinearGradientState,
32
- ZeegoState,
33
32
  BurntState,
34
33
  NativePortalProps,
35
34
  NativePortalHostProps,
36
35
  NativePortalProviderProps,
37
36
  } from './types'
37
+ export type { NativeMenuAdapter, NativeMenuModule } from './nativeMenuState'
38
38
 
39
39
  // portal
40
40
  export { getPortal } from './portalState'
@@ -76,9 +76,10 @@ export {
76
76
  } from './keyboardControllerState'
77
77
  export type { KeyboardControllerState } from './keyboardControllerState'
78
78
 
79
- // zeego (native menus)
80
- export { getZeego } from './zeegoState'
81
- export type { ZeegoAccessor } from './zeegoState'
79
+ // native menus
80
+ export { getNativeMenuAdapter, registerNativeMenuAdapter } from './nativeMenuState'
81
+ export { createExpoUIMenuAdapter } from './expoUIMenuAdapter'
82
+ export type { ExpoUIMenuAdapterOptions } from './expoUIMenuAdapter'
82
83
  export { NativeMenuContext } from './nativeMenuContext'
83
84
 
84
85
  // burnt (native toasts)
@@ -0,0 +1,63 @@
1
+ import type { ComponentType } from 'react'
2
+
3
+ import { createGlobalState } from './globalState'
4
+
5
+ export type NativeMenuModule = {
6
+ Root: ComponentType<any>
7
+ Trigger: ComponentType<any>
8
+ Content: ComponentType<any>
9
+ Item: ComponentType<any>
10
+ ItemTitle: ComponentType<any>
11
+ ItemSubtitle: ComponentType<any>
12
+ ItemIcon: ComponentType<any>
13
+ ItemImage: ComponentType<any>
14
+ ItemIndicator: ComponentType<any>
15
+ Group: ComponentType<any>
16
+ Label: ComponentType<any>
17
+ Separator: ComponentType<any>
18
+ Sub: ComponentType<any>
19
+ SubTrigger: ComponentType<any>
20
+ SubContent: ComponentType<any>
21
+ CheckboxItem: ComponentType<any>
22
+ Preview?: ComponentType<any>
23
+ Auxiliary?: ComponentType<any>
24
+ }
25
+
26
+ /**
27
+ * A native implementation for Tamagui Menu and ContextMenu compound components.
28
+ * Register one adapter before the first native menu renders.
29
+ */
30
+ export type NativeMenuAdapter = {
31
+ name: string
32
+ Menu: NativeMenuModule
33
+ ContextMenu: NativeMenuModule
34
+ }
35
+
36
+ type NativeMenuState = {
37
+ enabled: boolean
38
+ adapter: NativeMenuAdapter | null
39
+ }
40
+
41
+ const state = createGlobalState<NativeMenuState>('native-menu-adapter', {
42
+ enabled: false,
43
+ adapter: null,
44
+ })
45
+
46
+ /**
47
+ * Registers the process-wide native menu implementation. Re-registering the same
48
+ * named adapter supports fast refresh; registering a different adapter throws.
49
+ */
50
+ export function registerNativeMenuAdapter(adapter: NativeMenuAdapter): void {
51
+ const current = state.get().adapter
52
+ if (current && current.name !== adapter.name) {
53
+ throw new Error(
54
+ `A native menu adapter named "${current.name}" is already registered. ` +
55
+ `Register exactly one adapter before rendering Tamagui menus.`
56
+ )
57
+ }
58
+ state.set({ enabled: true, adapter })
59
+ }
60
+
61
+ export function getNativeMenuAdapter(): NativeMenuAdapter | null {
62
+ return state.get().adapter
63
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Setup Expo UI for Tamagui native Menu and ContextMenu components.
3
+ *
4
+ * Import this module once at the top of the app entry point after installing
5
+ * `@expo/ui`.
6
+ */
7
+
8
+ import { createExpoUIMenuAdapter } from './expoUIMenuAdapter'
9
+ import { registerNativeMenuAdapter } from './nativeMenuState'
10
+
11
+ function setup(): void {
12
+ let MenuView
13
+ try {
14
+ MenuView = require('@expo/ui/community/menu').MenuView
15
+ } catch (error) {
16
+ if (process.env.NODE_ENV === 'development') {
17
+ console.warn('Error setting up Expo UI native menus', error)
18
+ }
19
+ return
20
+ }
21
+
22
+ registerNativeMenuAdapter(createExpoUIMenuAdapter({ MenuView }))
23
+ }
24
+
25
+ setup()
@@ -12,30 +12,26 @@
12
12
  * Menu and ContextMenu native mode.
13
13
  */
14
14
 
15
- import { getZeego } from './zeegoState'
15
+ import { registerNativeMenuAdapter } from './nativeMenuState'
16
16
 
17
17
  function setup(): void {
18
- const g = globalThis as any
19
- if (g.__tamagui_native_zeego_setup) return
20
- g.__tamagui_native_zeego_setup = true
21
-
18
+ let Menu
19
+ let ContextMenu
22
20
  try {
23
- const DropdownMenu = require('zeego/dropdown-menu')
24
- const ContextMenu = require('zeego/context-menu')
25
-
26
- if (DropdownMenu && ContextMenu) {
27
- getZeego().set({
28
- enabled: true,
29
- DropdownMenu,
30
- ContextMenu,
31
- })
32
- }
21
+ Menu = require('zeego/dropdown-menu')
22
+ ContextMenu = require('zeego/context-menu')
33
23
  } catch (err) {
34
24
  if (process.env.NODE_ENV === 'development') {
35
25
  console.warn(`Error setting up Zeego`, err)
36
26
  }
37
- // zeego not installed
27
+ return
38
28
  }
29
+
30
+ registerNativeMenuAdapter({
31
+ name: 'zeego',
32
+ Menu,
33
+ ContextMenu,
34
+ })
39
35
  }
40
36
 
41
37
  // run setup immediately on import
package/src/types.ts CHANGED
@@ -68,12 +68,6 @@ export interface LinearGradientState {
68
68
  Component: any
69
69
  }
70
70
 
71
- export interface ZeegoState {
72
- enabled: boolean
73
- DropdownMenu: any
74
- ContextMenu: any
75
- }
76
-
77
71
  export interface BurntState {
78
72
  enabled: boolean
79
73
  toast: ((options: any) => void) | null
@@ -0,0 +1,8 @@
1
+ import type { ComponentType } from "react";
2
+ import type { NativeMenuAdapter } from "./nativeMenuState";
3
+ export type ExpoUIMenuAdapterOptions = {
4
+ MenuView: ComponentType<any>;
5
+ };
6
+ export declare function createExpoUIMenuAdapter({ MenuView }: ExpoUIMenuAdapterOptions): NativeMenuAdapter;
7
+
8
+ //# sourceMappingURL=expoUIMenuAdapter.d.ts.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "mappings": "AAAA,cAAc,qBAA8C;AAG5D,cAAc,yBAA2C;AA0BzD,YAAY,2BAA2B;CACrC,UAAU;;AA+CZ,OAAO,iBAAS,wBAAwB,EACtC,YACC,2BAA2B",
3
+ "names": [],
4
+ "sources": [
5
+ "src/expoUIMenuAdapter.tsx"
6
+ ],
7
+ "version": 3,
8
+ "sourcesContent": [
9
+ "import type { ComponentType, ReactElement, ReactNode } from 'react'\nimport React from 'react'\n\nimport type { NativeMenuAdapter, NativeMenuModule } from './nativeMenuState'\n\ntype ExpoMenuAction = {\n id: string\n title: string\n image?: unknown\n state?: 'on' | 'off'\n attributes?: {\n destructive?: boolean\n disabled?: boolean\n hidden?: boolean\n }\n subactions?: ExpoMenuAction[]\n displayInline?: boolean\n}\n\ntype ExpoMenuViewProps = {\n actions: ExpoMenuAction[]\n title?: string\n shouldOpenOnLongPress?: boolean\n onPressAction?: (event: { nativeEvent: { event: string } }) => void\n onOpenMenu?: () => void\n onCloseMenu?: () => void\n children?: ReactNode\n}\n\nexport type ExpoUIMenuAdapterOptions = {\n MenuView: ComponentType<any>\n}\n\ntype MarkerProps = {\n children?: ReactNode\n [key: string]: unknown\n}\n\nfunction marker(name: string): ComponentType<MarkerProps> {\n const Marker = () => null\n Marker.displayName = `ExpoUI${name}`\n return Marker\n}\n\nfunction isMarkerType(candidate: unknown, type: ComponentType<MarkerProps>): boolean {\n if (candidate === type) return true\n // createMenu runs every native component through withNativeMenu, which wraps\n // it in a new function component and copies the marker's displayName onto the\n // wrapper. that wrapper is a different reference, so an identity-only check\n // matches nothing and every item silently comes back with an empty title, no\n // icon, and no submenu. fall back to the (unique, ExpoUI-prefixed) name.\n const wanted = (type as { displayName?: string }).displayName\n if (!wanted) return false\n return (\n typeof candidate === 'function' &&\n (candidate as { displayName?: string }).displayName === wanted\n )\n}\n\nfunction elementOfType(\n children: ReactNode,\n type: ComponentType<MarkerProps>\n): ReactElement<MarkerProps> | null {\n for (const child of React.Children.toArray(children)) {\n if (React.isValidElement<MarkerProps>(child) && isMarkerType(child.type, type)) {\n return child\n }\n }\n return null\n}\n\nfunction textFromNode(node: ReactNode): string {\n if (typeof node === 'string' || typeof node === 'number') return String(node)\n if (!React.isValidElement<MarkerProps>(node)) return ''\n return React.Children.toArray(node.props.children).map(textFromNode).join('')\n}\n\nexport function createExpoUIMenuAdapter({\n MenuView,\n}: ExpoUIMenuAdapterOptions): NativeMenuAdapter {\n const NativeMenuView: ComponentType<ExpoMenuViewProps> = MenuView\n\n function createModule(isContextMenu: boolean): NativeMenuModule {\n const Trigger = marker('Trigger')\n const Content = marker('Content')\n const Item = marker('Item')\n const ItemTitle = marker('ItemTitle')\n const ItemSubtitle = marker('ItemSubtitle')\n const ItemIcon = marker('ItemIcon')\n const ItemImage = marker('ItemImage')\n const ItemIndicator = marker('ItemIndicator')\n const Group = marker('Group')\n const Label = marker('Label')\n const Separator = marker('Separator')\n const Sub = marker('Sub')\n const SubTrigger = marker('SubTrigger')\n const SubContent = marker('SubContent')\n const CheckboxItem = marker('CheckboxItem')\n const Preview = marker('Preview')\n const Auxiliary = marker('Auxiliary')\n\n function Root({\n children,\n onOpenChange,\n onOpenWillChange,\n }: {\n children?: ReactNode\n onOpenChange?: (open: boolean) => void\n onOpenWillChange?: (open: boolean) => void\n }) {\n const trigger = elementOfType(children, Trigger)\n const content = elementOfType(children, Content)\n const preview = elementOfType(children, Preview)\n const auxiliary = elementOfType(children, Auxiliary)\n\n if (preview || auxiliary) {\n throw new Error(\n '@expo/ui/community/menu does not support Tamagui context-menu previews or auxiliary views'\n )\n }\n if (!trigger || !content) {\n throw new Error('Tamagui native menus require one Trigger and one Content')\n }\n\n let nextId = 0\n const handlers = new Map<string, () => void>()\n\n function actionId(element: ReactElement<MarkerProps>): string {\n return element.key == null\n ? `tamagui-menu-${nextId++}`\n : String(element.key).replace(/^\\.\\$/, '')\n }\n\n function itemTitle(element: ReactElement<MarkerProps>): string {\n if (typeof element.props.textValue === 'string') return element.props.textValue\n const title = elementOfType(element.props.children, ItemTitle)\n return title ? textFromNode(title.props.children) : ''\n }\n\n function actionImage(element: ReactElement<MarkerProps>): unknown {\n const image = elementOfType(element.props.children, ItemImage)\n if (image?.props.source) return image.props.source\n const icon = elementOfType(element.props.children, ItemIcon)\n if (!icon) return undefined\n const ios = icon.props.ios\n if (\n typeof ios === 'object' &&\n ios &&\n 'name' in ios &&\n typeof ios.name === 'string'\n ) {\n return ios.name\n }\n return typeof icon.props.iosIconName === 'string'\n ? icon.props.iosIconName\n : undefined\n }\n\n function attributes(\n element: ReactElement<MarkerProps>\n ): ExpoMenuAction['attributes'] {\n const destructive = element.props.destructive === true\n const disabled = element.props.disabled === true\n const hidden = element.props.hidden === true\n return destructive || disabled || hidden\n ? { destructive, disabled, hidden }\n : undefined\n }\n\n function actionsFrom(childrenToParse: ReactNode): ExpoMenuAction[] {\n const actions: ExpoMenuAction[] = []\n let section: ExpoMenuAction[] | null = null\n\n function append(action: ExpoMenuAction) {\n if (section) {\n section.push(action)\n } else {\n actions.push(action)\n }\n }\n\n function flushSection() {\n if (!section?.length) {\n section = []\n return\n }\n actions.push({\n id: `tamagui-menu-section-${nextId++}`,\n title: '',\n subactions: section,\n displayInline: true,\n })\n section = []\n }\n\n for (const child of React.Children.toArray(childrenToParse)) {\n if (!React.isValidElement<MarkerProps>(child)) continue\n\n if (isMarkerType(child.type, Separator)) {\n if (!section) section = actions.splice(0)\n flushSection()\n continue\n }\n\n if (isMarkerType(child.type, Group)) {\n const label = elementOfType(child.props.children, Label)\n append({\n id: actionId(child),\n title: label ? textFromNode(label.props.children) : '',\n subactions: actionsFrom(child.props.children),\n displayInline: true,\n })\n continue\n }\n\n if (isMarkerType(child.type, Sub)) {\n const subTrigger = elementOfType(child.props.children, SubTrigger)\n const subContent = elementOfType(child.props.children, SubContent)\n if (!subTrigger || !subContent) {\n throw new Error(\n 'Tamagui native submenus require one SubTrigger and SubContent'\n )\n }\n append({\n id: actionId(child),\n title: itemTitle(subTrigger),\n image: actionImage(subTrigger),\n attributes: attributes(subTrigger),\n subactions: actionsFrom(subContent.props.children),\n })\n continue\n }\n\n if (!isMarkerType(child.type, Item) && !isMarkerType(child.type, CheckboxItem))\n continue\n\n if (elementOfType(child.props.children, ItemSubtitle)) {\n throw new Error(\n '@expo/ui/community/menu does not support menu item subtitles'\n )\n }\n\n const id = actionId(child)\n if (isMarkerType(child.type, CheckboxItem)) {\n const current =\n child.props.value === true ||\n child.props.value === 'on' ||\n child.props.checked === true\n const onValueChange = child.props.onValueChange\n const onCheckedChange = child.props.onCheckedChange\n handlers.set(id, () => {\n if (typeof onValueChange === 'function') {\n onValueChange(current ? 'off' : 'on', current ? 'on' : 'off')\n } else if (typeof onCheckedChange === 'function') {\n onCheckedChange(!current)\n }\n })\n append({\n id,\n title: itemTitle(child),\n image: actionImage(child),\n state: current ? 'on' : 'off',\n attributes: attributes(child),\n })\n continue\n }\n\n const onSelect = child.props.onSelect\n if (typeof onSelect === 'function') {\n handlers.set(id, () => onSelect())\n }\n append({\n id,\n title: itemTitle(child),\n image: actionImage(child),\n attributes: attributes(child),\n })\n }\n\n if (section) flushSection()\n return actions\n }\n\n const label = elementOfType(content.props.children, Label)\n const triggerAction = trigger.props.action\n\n return (\n <NativeMenuView\n actions={actionsFrom(content.props.children)}\n title={label ? textFromNode(label.props.children) : undefined}\n shouldOpenOnLongPress={\n triggerAction === 'longPress' || (triggerAction !== 'press' && isContextMenu)\n }\n onPressAction={(event) => handlers.get(event.nativeEvent.event)?.()}\n onOpenMenu={() => {\n onOpenWillChange?.(true)\n onOpenChange?.(true)\n }}\n onCloseMenu={() => {\n onOpenWillChange?.(false)\n onOpenChange?.(false)\n }}\n >\n {trigger.props.children}\n </NativeMenuView>\n )\n }\n\n return {\n Root,\n Trigger,\n Content,\n Item,\n ItemTitle,\n ItemSubtitle,\n ItemIcon,\n ItemImage,\n ItemIndicator,\n Group,\n Label,\n Separator,\n Sub,\n SubTrigger,\n SubContent,\n CheckboxItem,\n Preview,\n Auxiliary,\n }\n }\n\n return {\n name: 'expo-ui',\n Menu: createModule(false),\n ContextMenu: createModule(true),\n }\n}\n"
10
+ ]
11
+ }
package/types/index.d.ts CHANGED
@@ -18,7 +18,8 @@
18
18
  * // LinearGradient will use expo-linear-gradient when installed
19
19
  * ```
20
20
  */
21
- export type { NativePortalState, GestureState, WorkletsState, SafeAreaState, SafeAreaInsets, SafeAreaFrame, SafeAreaMetrics, LinearGradientState, ZeegoState, BurntState, NativePortalProps, NativePortalHostProps, NativePortalProviderProps } from "./types";
21
+ export type { NativePortalState, GestureState, WorkletsState, SafeAreaState, SafeAreaInsets, SafeAreaFrame, SafeAreaMetrics, LinearGradientState, BurntState, NativePortalProps, NativePortalHostProps, NativePortalProviderProps } from "./types";
22
+ export type { NativeMenuAdapter, NativeMenuModule } from "./nativeMenuState";
22
23
  export { getPortal } from "./portalState";
23
24
  export type { PortalAccessor } from "./portalState";
24
25
  export { getGestureHandler } from "./gestureState";
@@ -33,8 +34,9 @@ export { getLinearGradient } from "./linearGradientState";
33
34
  export type { LinearGradientAccessor } from "./linearGradientState";
34
35
  export { isKeyboardControllerEnabled, getKeyboardControllerState, setKeyboardControllerState } from "./keyboardControllerState";
35
36
  export type { KeyboardControllerState } from "./keyboardControllerState";
36
- export { getZeego } from "./zeegoState";
37
- export type { ZeegoAccessor } from "./zeegoState";
37
+ export { getNativeMenuAdapter, registerNativeMenuAdapter } from "./nativeMenuState";
38
+ export { createExpoUIMenuAdapter } from "./expoUIMenuAdapter";
39
+ export type { ExpoUIMenuAdapterOptions } from "./expoUIMenuAdapter";
38
40
  export { NativeMenuContext } from "./nativeMenuContext";
39
41
  export { getBurnt } from "./burntState";
40
42
  export type { BurntAccessor } from "./burntState";
@@ -1,11 +1,11 @@
1
1
  {
2
- "mappings": ";;;;;;;;;;;;;;;;;;;;AAsBA,cACE,mBACA,cACA,eACA,eACA,gBACA,eACA,iBACA,qBACA,YACA,YACA,mBACA,uBACA,iCACK;AAGP,SAAS,iBAAiB;AAC1B,cAAc,sBAAsB;AAGpC,SAAS,yBAAyB;AAClC,SACE,+BAA+B,sCAC/B,6BAA6B,oCAC7B,iCAAiC,8CAC5B;AACP,cACE,6BACA,wBACA,0BACK;AAGP,cAAc,4BAA4B;AAG1C,SAAS,mBAAmB;AAC5B,cAAc,wBAAwB;AAGtC,SAAS,mBAAmB;AAC5B,cAAc,wBAAwB;AAGtC,SAAS,yBAAyB;AAClC,cAAc,8BAA8B;AAG5C,SACE,6BACA,4BACA,kCACK;AACP,cAAc,+BAA+B;AAG7C,SAAS,gBAAgB;AACzB,cAAc,qBAAqB;AACnC,SAAS,yBAAyB;AAGlC,SAAS,gBAAgB;AACzB,cAAc,qBAAqB;AAGnC,SAAS,cAAc,kBAAkB,4BAA4B",
2
+ "mappings": ";;;;;;;;;;;;;;;;;;;;AAsBA,cACE,mBACA,cACA,eACA,eACA,gBACA,eACA,iBACA,qBACA,YACA,mBACA,uBACA,iCACK;AACP,cAAc,mBAAmB,wBAAwB;AAGzD,SAAS,iBAAiB;AAC1B,cAAc,sBAAsB;AAGpC,SAAS,yBAAyB;AAClC,SACE,+BAA+B,sCAC/B,6BAA6B,oCAC7B,iCAAiC,8CAC5B;AACP,cACE,6BACA,wBACA,0BACK;AAGP,cAAc,4BAA4B;AAG1C,SAAS,mBAAmB;AAC5B,cAAc,wBAAwB;AAGtC,SAAS,mBAAmB;AAC5B,cAAc,wBAAwB;AAGtC,SAAS,yBAAyB;AAClC,cAAc,8BAA8B;AAG5C,SACE,6BACA,4BACA,kCACK;AACP,cAAc,+BAA+B;AAG7C,SAAS,sBAAsB,iCAAiC;AAChE,SAAS,+BAA+B;AACxC,cAAc,gCAAgC;AAC9C,SAAS,yBAAyB;AAGlC,SAAS,gBAAgB;AACzB,cAAc,qBAAqB;AAGnC,SAAS,cAAc,kBAAkB,4BAA4B",
3
3
  "names": [],
4
4
  "sources": [
5
5
  "src/index.ts"
6
6
  ],
7
7
  "version": 3,
8
8
  "sourcesContent": [
9
- "/**\n * @tamagui/native\n *\n * Native setup modules for Tamagui. Import these at the top of your app entry point.\n *\n * @example\n * ```tsx\n * // In your app entry (index.js or App.tsx)\n * import '@tamagui/native/setup-teleport'\n * import '@tamagui/native/setup-gesture-handler'\n * import '@tamagui/native/setup-worklets'\n * import '@tamagui/native/setup-safe-area'\n * import '@tamagui/native/expo-linear-gradient'\n * import '@tamagui/native/setup-keyboard-controller'\n *\n * // Then use Tamagui components normally\n * // Sheet will automatically use native gestures when available\n * // LinearGradient will use expo-linear-gradient when installed\n * ```\n */\n\n// types\nexport type {\n NativePortalState,\n GestureState,\n WorkletsState,\n SafeAreaState,\n SafeAreaInsets,\n SafeAreaFrame,\n SafeAreaMetrics,\n LinearGradientState,\n ZeegoState,\n BurntState,\n NativePortalProps,\n NativePortalHostProps,\n NativePortalProviderProps,\n} from './types'\n\n// portal\nexport { getPortal } from './portalState'\nexport type { PortalAccessor } from './portalState'\n\n// gesture handler\nexport { getGestureHandler } from './gestureState'\nexport {\n claimExternalPressOwnership as unstable_claimExternalPressOwnership,\n hasExternalPressOwnership as unstable_hasExternalPressOwnership,\n releaseExternalPressOwnership as unstable_releaseExternalPressOwnership,\n} from './gestureState'\nexport type {\n ExternalPressOwnershipToken,\n GestureHandlerAccessor,\n PressGestureConfig,\n} from './gestureState'\n// NOTE: setupGestureHandler is exported from setup-gesture-handler.ts entry point,\n// not here, to avoid bundler pulling in RNGH require during tree-shaking\nexport type { GestureHandlerConfig } from './setup-gesture-handler'\n\n// worklets\nexport { getWorklets } from './workletsState'\nexport type { WorkletsAccessor } from './workletsState'\n\n// safe area\nexport { getSafeArea } from './safeAreaState'\nexport type { SafeAreaAccessor } from './safeAreaState'\n\n// linear gradient\nexport { getLinearGradient } from './linearGradientState'\nexport type { LinearGradientAccessor } from './linearGradientState'\n\n// keyboard controller state exports (safe - no side effects)\nexport {\n isKeyboardControllerEnabled,\n getKeyboardControllerState,\n setKeyboardControllerState,\n} from './keyboardControllerState'\nexport type { KeyboardControllerState } from './keyboardControllerState'\n\n// zeego (native menus)\nexport { getZeego } from './zeegoState'\nexport type { ZeegoAccessor } from './zeegoState'\nexport { NativeMenuContext } from './nativeMenuContext'\n\n// burnt (native toasts)\nexport { getBurnt } from './burntState'\nexport type { BurntAccessor } from './burntState'\n\n// components\nexport { NativePortal, NativePortalHost, NativePortalProvider } from './components'\n"
9
+ "/**\n * @tamagui/native\n *\n * Native setup modules for Tamagui. Import these at the top of your app entry point.\n *\n * @example\n * ```tsx\n * // In your app entry (index.js or App.tsx)\n * import '@tamagui/native/setup-teleport'\n * import '@tamagui/native/setup-gesture-handler'\n * import '@tamagui/native/setup-worklets'\n * import '@tamagui/native/setup-safe-area'\n * import '@tamagui/native/expo-linear-gradient'\n * import '@tamagui/native/setup-keyboard-controller'\n *\n * // Then use Tamagui components normally\n * // Sheet will automatically use native gestures when available\n * // LinearGradient will use expo-linear-gradient when installed\n * ```\n */\n\n// types\nexport type {\n NativePortalState,\n GestureState,\n WorkletsState,\n SafeAreaState,\n SafeAreaInsets,\n SafeAreaFrame,\n SafeAreaMetrics,\n LinearGradientState,\n BurntState,\n NativePortalProps,\n NativePortalHostProps,\n NativePortalProviderProps,\n} from './types'\nexport type { NativeMenuAdapter, NativeMenuModule } from './nativeMenuState'\n\n// portal\nexport { getPortal } from './portalState'\nexport type { PortalAccessor } from './portalState'\n\n// gesture handler\nexport { getGestureHandler } from './gestureState'\nexport {\n claimExternalPressOwnership as unstable_claimExternalPressOwnership,\n hasExternalPressOwnership as unstable_hasExternalPressOwnership,\n releaseExternalPressOwnership as unstable_releaseExternalPressOwnership,\n} from './gestureState'\nexport type {\n ExternalPressOwnershipToken,\n GestureHandlerAccessor,\n PressGestureConfig,\n} from './gestureState'\n// NOTE: setupGestureHandler is exported from setup-gesture-handler.ts entry point,\n// not here, to avoid bundler pulling in RNGH require during tree-shaking\nexport type { GestureHandlerConfig } from './setup-gesture-handler'\n\n// worklets\nexport { getWorklets } from './workletsState'\nexport type { WorkletsAccessor } from './workletsState'\n\n// safe area\nexport { getSafeArea } from './safeAreaState'\nexport type { SafeAreaAccessor } from './safeAreaState'\n\n// linear gradient\nexport { getLinearGradient } from './linearGradientState'\nexport type { LinearGradientAccessor } from './linearGradientState'\n\n// keyboard controller state exports (safe - no side effects)\nexport {\n isKeyboardControllerEnabled,\n getKeyboardControllerState,\n setKeyboardControllerState,\n} from './keyboardControllerState'\nexport type { KeyboardControllerState } from './keyboardControllerState'\n\n// native menus\nexport { getNativeMenuAdapter, registerNativeMenuAdapter } from './nativeMenuState'\nexport { createExpoUIMenuAdapter } from './expoUIMenuAdapter'\nexport type { ExpoUIMenuAdapterOptions } from './expoUIMenuAdapter'\nexport { NativeMenuContext } from './nativeMenuContext'\n\n// burnt (native toasts)\nexport { getBurnt } from './burntState'\nexport type { BurntAccessor } from './burntState'\n\n// components\nexport { NativePortal, NativePortalHost, NativePortalProvider } from './components'\n"
10
10
  ]
11
11
  }
@@ -0,0 +1,38 @@
1
+ import type { ComponentType } from "react";
2
+ export type NativeMenuModule = {
3
+ Root: ComponentType<any>;
4
+ Trigger: ComponentType<any>;
5
+ Content: ComponentType<any>;
6
+ Item: ComponentType<any>;
7
+ ItemTitle: ComponentType<any>;
8
+ ItemSubtitle: ComponentType<any>;
9
+ ItemIcon: ComponentType<any>;
10
+ ItemImage: ComponentType<any>;
11
+ ItemIndicator: ComponentType<any>;
12
+ Group: ComponentType<any>;
13
+ Label: ComponentType<any>;
14
+ Separator: ComponentType<any>;
15
+ Sub: ComponentType<any>;
16
+ SubTrigger: ComponentType<any>;
17
+ SubContent: ComponentType<any>;
18
+ CheckboxItem: ComponentType<any>;
19
+ Preview?: ComponentType<any>;
20
+ Auxiliary?: ComponentType<any>;
21
+ };
22
+ /**
23
+ * A native implementation for Tamagui Menu and ContextMenu compound components.
24
+ * Register one adapter before the first native menu renders.
25
+ */
26
+ export type NativeMenuAdapter = {
27
+ name: string;
28
+ Menu: NativeMenuModule;
29
+ ContextMenu: NativeMenuModule;
30
+ };
31
+ /**
32
+ * Registers the process-wide native menu implementation. Re-registering the same
33
+ * named adapter supports fast refresh; registering a different adapter throws.
34
+ */
35
+ export declare function registerNativeMenuAdapter(adapter: NativeMenuAdapter): void;
36
+ export declare function getNativeMenuAdapter(): NativeMenuAdapter | null;
37
+
38
+ //# sourceMappingURL=nativeMenuState.d.ts.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "mappings": "AAAA,cAAc,qBAAqB;AAInC,YAAY,mBAAmB;CAC7B,MAAM;CACN,SAAS;CACT,SAAS;CACT,MAAM;CACN,WAAW;CACX,cAAc;CACd,UAAU;CACV,WAAW;CACX,eAAe;CACf,OAAO;CACP,OAAO;CACP,WAAW;CACX,KAAK;CACL,YAAY;CACZ,YAAY;CACZ,cAAc;CACd,UAAU;CACV,YAAY;;;;;;AAOd,YAAY,oBAAoB;CAC9B;CACA,MAAM;CACN,aAAa;;;;;;AAiBf,OAAO,iBAAS,0BAA0B,SAAS;AAWnD,OAAO,iBAAS,wBAAwB",
3
+ "names": [],
4
+ "sources": [
5
+ "src/nativeMenuState.ts"
6
+ ],
7
+ "version": 3,
8
+ "sourcesContent": [
9
+ "import type { ComponentType } from 'react'\n\nimport { createGlobalState } from './globalState'\n\nexport type NativeMenuModule = {\n Root: ComponentType<any>\n Trigger: ComponentType<any>\n Content: ComponentType<any>\n Item: ComponentType<any>\n ItemTitle: ComponentType<any>\n ItemSubtitle: ComponentType<any>\n ItemIcon: ComponentType<any>\n ItemImage: ComponentType<any>\n ItemIndicator: ComponentType<any>\n Group: ComponentType<any>\n Label: ComponentType<any>\n Separator: ComponentType<any>\n Sub: ComponentType<any>\n SubTrigger: ComponentType<any>\n SubContent: ComponentType<any>\n CheckboxItem: ComponentType<any>\n Preview?: ComponentType<any>\n Auxiliary?: ComponentType<any>\n}\n\n/**\n * A native implementation for Tamagui Menu and ContextMenu compound components.\n * Register one adapter before the first native menu renders.\n */\nexport type NativeMenuAdapter = {\n name: string\n Menu: NativeMenuModule\n ContextMenu: NativeMenuModule\n}\n\ntype NativeMenuState = {\n enabled: boolean\n adapter: NativeMenuAdapter | null\n}\n\nconst state = createGlobalState<NativeMenuState>('native-menu-adapter', {\n enabled: false,\n adapter: null,\n})\n\n/**\n * Registers the process-wide native menu implementation. Re-registering the same\n * named adapter supports fast refresh; registering a different adapter throws.\n */\nexport function registerNativeMenuAdapter(adapter: NativeMenuAdapter): void {\n const current = state.get().adapter\n if (current && current.name !== adapter.name) {\n throw new Error(\n `A native menu adapter named \"${current.name}\" is already registered. ` +\n `Register exactly one adapter before rendering Tamagui menus.`\n )\n }\n state.set({ enabled: true, adapter })\n}\n\nexport function getNativeMenuAdapter(): NativeMenuAdapter | null {\n return state.get().adapter\n}\n"
10
+ ]
11
+ }
@@ -0,0 +1,3 @@
1
+ export {};
2
+
3
+ //# sourceMappingURL=setup-expo-ui-menu.d.ts.map