@ankhorage/zora 14.0.1 → 15.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.
Files changed (218) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/dist/features/app-bar/adapters/inbound/AppBar.d.ts.map +1 -1
  4. package/dist/features/app-bar/adapters/inbound/AppBar.js +28 -27
  5. package/dist/features/app-bar/adapters/inbound/AppBar.js.map +1 -1
  6. package/dist/features/app-bar/public.d.ts +1 -1
  7. package/dist/features/app-bar/public.d.ts.map +1 -1
  8. package/dist/features/app-bar/public.js.map +1 -1
  9. package/dist/features/bottom-sheet/public.d.ts +2 -2
  10. package/dist/features/bottom-sheet/public.d.ts.map +1 -1
  11. package/dist/features/bottom-sheet/public.js +1 -1
  12. package/dist/features/bottom-sheet/public.js.map +1 -1
  13. package/dist/features/breadcrumbs/adapters/inbound/Breadcrumbs.d.ts +5 -0
  14. package/dist/features/breadcrumbs/adapters/inbound/Breadcrumbs.d.ts.map +1 -0
  15. package/dist/features/breadcrumbs/adapters/inbound/Breadcrumbs.js +81 -0
  16. package/dist/features/breadcrumbs/adapters/inbound/Breadcrumbs.js.map +1 -0
  17. package/dist/features/breadcrumbs/breadcrumbsMeta.d.ts +119 -0
  18. package/dist/features/breadcrumbs/breadcrumbsMeta.d.ts.map +1 -0
  19. package/dist/features/breadcrumbs/breadcrumbsMeta.js +94 -0
  20. package/dist/features/breadcrumbs/breadcrumbsMeta.js.map +1 -0
  21. package/dist/features/breadcrumbs/public.d.ts +3 -0
  22. package/dist/features/breadcrumbs/public.d.ts.map +1 -0
  23. package/dist/features/breadcrumbs/public.js +2 -0
  24. package/dist/features/breadcrumbs/public.js.map +1 -0
  25. package/dist/features/button/utils/resolveButtonThemeRecipe.d.ts +1 -1
  26. package/dist/features/data-table/adapters/inbound/DataTable.d.ts.map +1 -1
  27. package/dist/features/data-table/adapters/inbound/DataTable.js +4 -2
  28. package/dist/features/data-table/adapters/inbound/DataTable.js.map +1 -1
  29. package/dist/features/form/formFieldMeta.d.ts +1 -1
  30. package/dist/features/form/formFieldMeta.js +1 -1
  31. package/dist/features/form/formFieldMeta.js.map +1 -1
  32. package/dist/features/form/radio/utils/resolveRadioGroupThemeRecipe.d.ts +1 -1
  33. package/dist/features/form/select/adapters/inbound/Select.d.ts +2 -0
  34. package/dist/features/form/select/adapters/inbound/Select.d.ts.map +1 -0
  35. package/dist/features/form/select/adapters/inbound/Select.js +2 -0
  36. package/dist/features/form/select/adapters/inbound/Select.js.map +1 -0
  37. package/dist/features/form/select/adapters/inbound/Select.native.d.ts +5 -0
  38. package/dist/features/form/select/adapters/inbound/Select.native.d.ts.map +1 -0
  39. package/dist/features/form/select/adapters/inbound/Select.native.js +42 -0
  40. package/dist/features/form/select/adapters/inbound/Select.native.js.map +1 -0
  41. package/dist/features/form/select/adapters/inbound/Select.web.d.ts +5 -0
  42. package/dist/features/form/select/adapters/inbound/Select.web.d.ts.map +1 -0
  43. package/dist/features/form/select/adapters/inbound/Select.web.js +31 -0
  44. package/dist/features/form/select/adapters/inbound/Select.web.js.map +1 -0
  45. package/dist/features/form/select/composition/SelectField.d.ts +10 -0
  46. package/dist/features/form/select/composition/SelectField.d.ts.map +1 -0
  47. package/dist/features/form/select/composition/SelectField.js +8 -0
  48. package/dist/features/form/select/composition/SelectField.js.map +1 -0
  49. package/dist/features/form/select/composition/SelectOptionRow.d.ts +12 -0
  50. package/dist/features/form/select/composition/SelectOptionRow.d.ts.map +1 -0
  51. package/dist/features/form/select/composition/SelectOptionRow.js +9 -0
  52. package/dist/features/form/select/composition/SelectOptionRow.js.map +1 -0
  53. package/dist/features/form/select/composition/SelectTrigger.d.ts +10 -0
  54. package/dist/features/form/select/composition/SelectTrigger.d.ts.map +1 -0
  55. package/dist/features/form/select/composition/SelectTrigger.js +9 -0
  56. package/dist/features/form/select/composition/SelectTrigger.js.map +1 -0
  57. package/dist/features/form/select/composition/useSelectController.d.ts +8 -0
  58. package/dist/features/form/select/composition/useSelectController.d.ts.map +1 -0
  59. package/dist/features/form/select/composition/useSelectController.js +15 -0
  60. package/dist/features/form/select/composition/useSelectController.js.map +1 -0
  61. package/dist/features/form/select/public.d.ts +3 -0
  62. package/dist/features/form/select/public.d.ts.map +1 -0
  63. package/dist/features/form/select/public.js +2 -0
  64. package/dist/features/form/select/public.js.map +1 -0
  65. package/dist/features/form/select/selectMeta.d.ts +195 -0
  66. package/dist/features/form/select/selectMeta.d.ts.map +1 -0
  67. package/dist/features/form/select/selectMeta.js +152 -0
  68. package/dist/features/form/select/selectMeta.js.map +1 -0
  69. package/dist/features/popover-menu/adapters/inbound/PopoverMenu.d.ts +5 -0
  70. package/dist/features/popover-menu/adapters/inbound/PopoverMenu.d.ts.map +1 -0
  71. package/dist/features/popover-menu/adapters/inbound/PopoverMenu.js +43 -0
  72. package/dist/features/popover-menu/adapters/inbound/PopoverMenu.js.map +1 -0
  73. package/dist/features/popover-menu/popoverMenuMeta.d.ts +9 -0
  74. package/dist/features/popover-menu/popoverMenuMeta.d.ts.map +1 -0
  75. package/dist/features/popover-menu/popoverMenuMeta.js +9 -0
  76. package/dist/features/popover-menu/popoverMenuMeta.js.map +1 -0
  77. package/dist/features/popover-menu/public.d.ts +3 -0
  78. package/dist/features/popover-menu/public.d.ts.map +1 -0
  79. package/dist/features/popover-menu/public.js +2 -0
  80. package/dist/features/popover-menu/public.js.map +1 -0
  81. package/dist/features/typography/utils/resolveHeadingThemeRecipe.d.ts +1 -1
  82. package/dist/features/typography/utils/resolveTextThemeRecipe.d.ts +1 -1
  83. package/dist/index.d.ts +4 -7
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +3 -3
  86. package/dist/index.js.map +1 -1
  87. package/dist/metadata/bindableComponentMeta.d.ts +1 -2
  88. package/dist/metadata/bindableComponentMeta.d.ts.map +1 -1
  89. package/dist/metadata/bindableComponentMeta.js +1 -2
  90. package/dist/metadata/bindableComponentMeta.js.map +1 -1
  91. package/dist/metadata/componentMeta.d.ts.map +1 -1
  92. package/dist/metadata/componentMeta.js +4 -5
  93. package/dist/metadata/componentMeta.js.map +1 -1
  94. package/dist/patterns/theme-composer/ThemeComposer.js +1 -1
  95. package/dist/patterns/theme-composer/ThemeComposer.js.map +1 -1
  96. package/dist/registry.d.ts.map +1 -1
  97. package/dist/registry.js +4 -5
  98. package/dist/registry.js.map +1 -1
  99. package/dist/types/app-bar.d.ts +6 -3
  100. package/dist/types/app-bar.d.ts.map +1 -1
  101. package/dist/types/app-bar.js.map +1 -1
  102. package/dist/{components/breadcrumbs/types.d.ts → types/breadcrumbs.d.ts} +8 -6
  103. package/dist/types/breadcrumbs.d.ts.map +1 -0
  104. package/dist/types/breadcrumbs.js +2 -0
  105. package/dist/types/breadcrumbs.js.map +1 -0
  106. package/dist/types/data-table.d.ts +2 -2
  107. package/dist/types/data-table.d.ts.map +1 -1
  108. package/dist/types/data-table.js.map +1 -1
  109. package/dist/types/popover-menu.d.ts +24 -0
  110. package/dist/types/popover-menu.d.ts.map +1 -0
  111. package/dist/types/popover-menu.js +2 -0
  112. package/dist/types/popover-menu.js.map +1 -0
  113. package/dist/types/select.d.ts +22 -0
  114. package/dist/types/select.d.ts.map +1 -0
  115. package/dist/types/select.js +2 -0
  116. package/dist/types/select.js.map +1 -0
  117. package/package.json +2 -4
  118. package/src/features/app-bar/adapters/inbound/AppBar.tsx +50 -42
  119. package/src/features/app-bar/public.ts +1 -1
  120. package/src/features/bottom-sheet/public.test.ts +10 -1
  121. package/src/features/bottom-sheet/public.ts +9 -1
  122. package/src/features/breadcrumbs/adapters/inbound/Breadcrumbs.tsx +179 -0
  123. package/src/features/breadcrumbs/breadcrumbsMeta.ts +95 -0
  124. package/src/features/breadcrumbs/public.test.ts +22 -0
  125. package/src/features/breadcrumbs/public.ts +6 -0
  126. package/src/features/data-table/adapters/inbound/DataTable.tsx +9 -5
  127. package/src/features/form/formFieldMeta.ts +1 -1
  128. package/src/features/form/select/adapters/inbound/Select.native.tsx +78 -0
  129. package/src/features/form/select/adapters/inbound/Select.tsx +1 -0
  130. package/src/features/form/select/adapters/inbound/Select.web.tsx +63 -0
  131. package/src/features/form/select/composition/SelectField.tsx +27 -0
  132. package/src/features/form/select/composition/SelectOptionRow.tsx +36 -0
  133. package/src/features/form/select/composition/SelectTrigger.tsx +32 -0
  134. package/src/features/form/select/composition/useSelectController.ts +21 -0
  135. package/src/features/form/select/public.test.ts +25 -0
  136. package/src/features/form/select/public.ts +2 -0
  137. package/src/features/form/select/selectMeta.ts +154 -0
  138. package/src/features/popover-menu/adapters/inbound/PopoverMenu.tsx +60 -0
  139. package/src/{components/breadcrumbs/meta.ts → features/popover-menu/popoverMenuMeta.ts} +3 -3
  140. package/src/features/popover-menu/public.test.ts +11 -0
  141. package/src/features/popover-menu/public.ts +6 -0
  142. package/src/index.ts +14 -7
  143. package/src/interactionPolicy.test.ts +19 -21
  144. package/src/metadata/bindableComponentMeta.test.ts +8 -0
  145. package/src/metadata/bindableComponentMeta.ts +1 -2
  146. package/src/metadata/componentMeta.test.ts +3 -0
  147. package/src/metadata/componentMeta.ts +4 -5
  148. package/src/nativePeerContract.test.ts +5 -19
  149. package/src/patterns/theme-composer/ThemeComposer.tsx +1 -1
  150. package/src/platformAcceptance.test.ts +1 -1
  151. package/src/registry.ts +4 -5
  152. package/src/theme/themeScopeStructure.test.ts +8 -3
  153. package/src/types/app-bar.ts +6 -3
  154. package/src/{components/breadcrumbs/types.ts → types/breadcrumbs.ts} +8 -5
  155. package/src/types/data-table.ts +2 -2
  156. package/src/types/popover-menu.ts +31 -0
  157. package/src/{components/select/types.ts → types/select.ts} +11 -3
  158. package/dist/components/breadcrumbs/Breadcrumbs.d.ts +0 -7
  159. package/dist/components/breadcrumbs/Breadcrumbs.d.ts.map +0 -1
  160. package/dist/components/breadcrumbs/Breadcrumbs.js +0 -80
  161. package/dist/components/breadcrumbs/Breadcrumbs.js.map +0 -1
  162. package/dist/components/breadcrumbs/index.d.ts +0 -3
  163. package/dist/components/breadcrumbs/index.d.ts.map +0 -1
  164. package/dist/components/breadcrumbs/index.js +0 -2
  165. package/dist/components/breadcrumbs/index.js.map +0 -1
  166. package/dist/components/breadcrumbs/meta.d.ts +0 -9
  167. package/dist/components/breadcrumbs/meta.d.ts.map +0 -1
  168. package/dist/components/breadcrumbs/meta.js +0 -9
  169. package/dist/components/breadcrumbs/meta.js.map +0 -1
  170. package/dist/components/breadcrumbs/types.d.ts.map +0 -1
  171. package/dist/components/breadcrumbs/types.js +0 -2
  172. package/dist/components/breadcrumbs/types.js.map +0 -1
  173. package/dist/components/menu/DropdownMenu.d.ts +0 -7
  174. package/dist/components/menu/DropdownMenu.d.ts.map +0 -1
  175. package/dist/components/menu/DropdownMenu.js +0 -9
  176. package/dist/components/menu/DropdownMenu.js.map +0 -1
  177. package/dist/components/menu/Menu.d.ts +0 -7
  178. package/dist/components/menu/Menu.d.ts.map +0 -1
  179. package/dist/components/menu/Menu.js +0 -46
  180. package/dist/components/menu/Menu.js.map +0 -1
  181. package/dist/components/menu/index.d.ts +0 -4
  182. package/dist/components/menu/index.d.ts.map +0 -1
  183. package/dist/components/menu/index.js +0 -3
  184. package/dist/components/menu/index.js.map +0 -1
  185. package/dist/components/menu/meta.d.ts +0 -17
  186. package/dist/components/menu/meta.d.ts.map +0 -1
  187. package/dist/components/menu/meta.js +0 -17
  188. package/dist/components/menu/meta.js.map +0 -1
  189. package/dist/components/menu/types.d.ts +0 -22
  190. package/dist/components/menu/types.d.ts.map +0 -1
  191. package/dist/components/menu/types.js +0 -2
  192. package/dist/components/menu/types.js.map +0 -1
  193. package/dist/components/select/Select.d.ts +0 -7
  194. package/dist/components/select/Select.d.ts.map +0 -1
  195. package/dist/components/select/Select.js +0 -32
  196. package/dist/components/select/Select.js.map +0 -1
  197. package/dist/components/select/index.d.ts +0 -3
  198. package/dist/components/select/index.d.ts.map +0 -1
  199. package/dist/components/select/index.js +0 -3
  200. package/dist/components/select/index.js.map +0 -1
  201. package/dist/components/select/meta.d.ts +0 -9
  202. package/dist/components/select/meta.d.ts.map +0 -1
  203. package/dist/components/select/meta.js +0 -9
  204. package/dist/components/select/meta.js.map +0 -1
  205. package/dist/components/select/types.d.ts +0 -14
  206. package/dist/components/select/types.d.ts.map +0 -1
  207. package/dist/components/select/types.js +0 -2
  208. package/dist/components/select/types.js.map +0 -1
  209. package/src/components/breadcrumbs/Breadcrumbs.tsx +0 -147
  210. package/src/components/breadcrumbs/index.ts +0 -2
  211. package/src/components/menu/DropdownMenu.tsx +0 -11
  212. package/src/components/menu/Menu.tsx +0 -59
  213. package/src/components/menu/index.ts +0 -3
  214. package/src/components/menu/meta.ts +0 -19
  215. package/src/components/menu/types.ts +0 -31
  216. package/src/components/select/Select.tsx +0 -67
  217. package/src/components/select/index.ts +0 -2
  218. package/src/components/select/meta.ts +0 -10
@@ -0,0 +1,179 @@
1
+ import React from 'react';
2
+
3
+ import { Inline } from '../../../../foundation';
4
+ import { resolveIconSize } from '../../../../internal/recipes';
5
+ import { useZoraTheme } from '../../../../theme/useZoraTheme';
6
+ import { withZoraThemeScope } from '../../../../theme/withZoraThemeScope';
7
+ import type {
8
+ BreadcrumbItem,
9
+ BreadcrumbPressEvent,
10
+ BreadcrumbsProps,
11
+ } from '../../../../types/breadcrumbs';
12
+ import { Button } from '../../../button/public';
13
+ import { Icon } from '../../../icon/public';
14
+ import { Text } from '../../../typography/public';
15
+
16
+ /*** Renders a breadcrumb trail whose final item represents the current location. */
17
+ export const Breadcrumbs = withZoraThemeScope(BreadcrumbsInner);
18
+
19
+ type BreadcrumbRenderItem =
20
+ | {
21
+ item: BreadcrumbItem;
22
+ type: 'item';
23
+ }
24
+ | {
25
+ type: 'ellipsis';
26
+ };
27
+
28
+ /*** Connects serializable breadcrumb items to the component-level item press event. */
29
+ function BreadcrumbsInner({
30
+ themeId: _themeId,
31
+ mode: _mode,
32
+ interactionPolicy,
33
+ items,
34
+ separator = '/',
35
+ maxItems,
36
+ compact = false,
37
+ disabled = false,
38
+ onItemPress,
39
+ testID,
40
+ }: BreadcrumbsProps) {
41
+ const normalizedMaxItems = normalizeMaxItems(maxItems);
42
+ const renderItems = createRenderItems(items, normalizedMaxItems);
43
+ const currentItemId = items.at(-1)?.id;
44
+
45
+ if (items.length === 0) return null;
46
+
47
+ return (
48
+ <Inline align="center" gap={compact ? 'xs' : 's'} testID={testID} wrap="wrap">
49
+ {renderItems.map((renderItem, index) => {
50
+ const showSeparator = index !== renderItems.length - 1;
51
+
52
+ if (renderItem.type === 'ellipsis') {
53
+ return (
54
+ <React.Fragment key="ellipsis">
55
+ <Text emphasis="muted" variant={compact ? 'caption' : 'bodySmall'}>
56
+ …
57
+ </Text>
58
+ {showSeparator ? renderSeparator(separator) : null}
59
+ </React.Fragment>
60
+ );
61
+ }
62
+
63
+ return (
64
+ <BreadcrumbItemView
65
+ compact={compact}
66
+ current={renderItem.item.id === currentItemId}
67
+ disabled={disabled}
68
+ interactionPolicy={interactionPolicy}
69
+ item={renderItem.item}
70
+ key={renderItem.item.id}
71
+ onItemPress={onItemPress}
72
+ separator={showSeparator ? separator : undefined}
73
+ testID={testID}
74
+ />
75
+ );
76
+ })}
77
+ </Inline>
78
+ );
79
+ }
80
+
81
+ /*** Renders one breadcrumb item and its optional following separator. */
82
+ function BreadcrumbItemView({
83
+ compact,
84
+ current,
85
+ disabled,
86
+ interactionPolicy,
87
+ item,
88
+ onItemPress,
89
+ separator,
90
+ testID,
91
+ }: {
92
+ compact: boolean;
93
+ current: boolean;
94
+ disabled: boolean;
95
+ interactionPolicy: BreadcrumbsProps['interactionPolicy'];
96
+ item: BreadcrumbItem;
97
+ onItemPress: ((event: BreadcrumbPressEvent) => void) | undefined;
98
+ separator: string | undefined;
99
+ testID: string | undefined;
100
+ }) {
101
+ const interactive = !current && !disabled && !item.disabled && onItemPress !== undefined;
102
+
103
+ return (
104
+ <>
105
+ {interactive ? (
106
+ <Button
107
+ color="neutral"
108
+ interactionPolicy={interactionPolicy}
109
+ leadingIcon={item.icon}
110
+ onPress={() => onItemPress({ id: item.id })}
111
+ size={compact ? 's' : 'm'}
112
+ testID={testID ? `${testID}-item-${item.id}` : undefined}
113
+ variant="ghost"
114
+ >
115
+ {item.label}
116
+ </Button>
117
+ ) : (
118
+ <BreadcrumbLabel compact={compact} item={item} />
119
+ )}
120
+ {separator === undefined ? null : renderSeparator(separator)}
121
+ </>
122
+ );
123
+ }
124
+
125
+ /*** Renders one non-interactive breadcrumb label. */
126
+ function BreadcrumbLabel({ item, compact }: { item: BreadcrumbItem; compact: boolean }) {
127
+ const { theme } = useZoraTheme();
128
+
129
+ return (
130
+ <Inline align="center" gap="xs" wrap="nowrap">
131
+ {item.icon ? (
132
+ <Icon
133
+ {...item.icon}
134
+ color={theme.semantics.content.muted}
135
+ size={resolveIconSize(compact ? 's' : 'm')}
136
+ />
137
+ ) : null}
138
+ <Text emphasis="muted" variant={compact ? 'caption' : 'bodySmall'}>
139
+ {item.label}
140
+ </Text>
141
+ </Inline>
142
+ );
143
+ }
144
+
145
+ /*** Renders the configured separator with muted breadcrumb styling. */
146
+ function renderSeparator(separator: string): React.ReactNode {
147
+ return (
148
+ <Text emphasis="muted" variant="bodySmall">
149
+ {separator}
150
+ </Text>
151
+ );
152
+ }
153
+
154
+ /*** Normalizes an optional maximum breadcrumb item count. */
155
+ function normalizeMaxItems(value: number | undefined): number | undefined {
156
+ if (value === undefined || !Number.isFinite(value)) return undefined;
157
+ return Math.max(0, Math.trunc(value));
158
+ }
159
+
160
+ /*** Collapses middle breadcrumb items into one ellipsis when required. */
161
+ function createRenderItems(
162
+ items: readonly BreadcrumbItem[],
163
+ maxItems: number | undefined,
164
+ ): readonly BreadcrumbRenderItem[] {
165
+ if (maxItems === undefined || maxItems === 0 || items.length <= maxItems || maxItems < 3) {
166
+ return items.map((item) => ({ item, type: 'item' }));
167
+ }
168
+
169
+ const firstItem = items.at(0);
170
+ if (firstItem === undefined) return [];
171
+
172
+ const trailingCount = maxItems - 2;
173
+ const trailingItems = items.slice(items.length - trailingCount);
174
+ return [
175
+ { item: firstItem, type: 'item' },
176
+ { type: 'ellipsis' },
177
+ ...trailingItems.map((item) => ({ item, type: 'item' }) satisfies BreadcrumbRenderItem),
178
+ ];
179
+ }
@@ -0,0 +1,95 @@
1
+ import type { ZoraComponentMeta } from '../../metadata';
2
+
3
+ export const breadcrumbsMeta = {
4
+ name: 'Breadcrumbs',
5
+ category: 'component',
6
+ description: 'Hierarchical route context where the final item represents the current location.',
7
+ directManifestNode: true,
8
+ allowedChildren: [],
9
+ blueprint: {
10
+ label: 'Breadcrumbs',
11
+ defaultProps: {
12
+ items: [
13
+ { id: 'settings', label: 'Settings' },
14
+ { id: 'general', label: 'General' },
15
+ ],
16
+ separator: '/',
17
+ compact: false,
18
+ disabled: false,
19
+ },
20
+ },
21
+ bindings: {
22
+ events: {
23
+ itemPress: {
24
+ label: 'Item press',
25
+ description: 'Runs when a non-current breadcrumb item is pressed.',
26
+ payload: {
27
+ eventType: 'breadcrumbs.itemPress',
28
+ fields: [{ path: 'id', type: 'string', label: 'Item id' }],
29
+ },
30
+ },
31
+ },
32
+ },
33
+ events: {
34
+ itemPress: {
35
+ label: 'Item press',
36
+ eventType: 'breadcrumbs.itemPress',
37
+ description: 'Emitted when a non-current breadcrumb item is pressed.',
38
+ payloadFields: [{ path: 'id', type: 'string', label: 'Item id' }],
39
+ },
40
+ },
41
+ props: {
42
+ items: {
43
+ type: 'array',
44
+ category: 'Content',
45
+ label: 'Items',
46
+ itemSchema: [
47
+ {
48
+ key: 'id',
49
+ schema: { type: 'string', category: 'Content', label: 'Id' },
50
+ },
51
+ {
52
+ key: 'label',
53
+ schema: { type: 'string', category: 'Content', label: 'Label' },
54
+ },
55
+ {
56
+ key: 'disabled',
57
+ schema: {
58
+ type: 'boolean',
59
+ category: 'State',
60
+ label: 'Disabled',
61
+ default: false,
62
+ },
63
+ },
64
+ ],
65
+ authoring: { authority: 'instance' },
66
+ },
67
+ separator: {
68
+ type: 'string',
69
+ category: 'Content',
70
+ label: 'Separator',
71
+ default: '/',
72
+ authoring: { authority: 'instance' },
73
+ },
74
+ maxItems: {
75
+ type: 'number',
76
+ category: 'Layout',
77
+ label: 'Maximum items',
78
+ authoring: { authority: 'instance' },
79
+ },
80
+ compact: {
81
+ type: 'boolean',
82
+ category: 'Layout',
83
+ label: 'Compact',
84
+ default: false,
85
+ authoring: { authority: 'instance' },
86
+ },
87
+ disabled: {
88
+ type: 'boolean',
89
+ category: 'State',
90
+ label: 'Disabled',
91
+ default: false,
92
+ authoring: { authority: 'instance' },
93
+ },
94
+ },
95
+ } as const satisfies ZoraComponentMeta;
@@ -0,0 +1,22 @@
1
+ import { expect, test } from 'bun:test';
2
+
3
+ import { breadcrumbsMeta } from './breadcrumbsMeta';
4
+
5
+ test('Breadcrumbs is a direct manifest node with serializable item press payload', () => {
6
+ expect(breadcrumbsMeta.directManifestNode).toBe(true);
7
+ expect(breadcrumbsMeta.events.itemPress.eventType).toBe('breadcrumbs.itemPress');
8
+ expect(breadcrumbsMeta.events.itemPress.payloadFields).toEqual([
9
+ { path: 'id', type: 'string', label: 'Item id' },
10
+ ]);
11
+ });
12
+
13
+ test('Breadcrumbs keeps current-location derivation and callbacks outside item data', async () => {
14
+ const [source, types] = await Promise.all([
15
+ Bun.file('src/features/breadcrumbs/adapters/inbound/Breadcrumbs.tsx').text(),
16
+ Bun.file('src/types/breadcrumbs.ts').text(),
17
+ ]);
18
+
19
+ expect(source).toContain('const currentItemId = items.at(-1)?.id;');
20
+ expect(source).toContain('onItemPress({ id: item.id })');
21
+ expect(types).not.toContain('onPress?:');
22
+ });
@@ -0,0 +1,6 @@
1
+ export type {
2
+ BreadcrumbItem,
3
+ BreadcrumbPressEvent,
4
+ BreadcrumbsProps,
5
+ } from '../../types/breadcrumbs';
6
+ export { Breadcrumbs } from './adapters/inbound/Breadcrumbs';
@@ -2,7 +2,6 @@ import type { InteractionPolicy } from '@ankhorage/surface';
2
2
  import React from 'react';
3
3
  import { ScrollView, type ViewStyle } from 'react-native';
4
4
 
5
- import { DropdownMenu, type MenuAction } from '../../../../components/menu';
6
5
  import { Show } from '../../../../foundation';
7
6
  import { withZoraThemeScope } from '../../../../theme/withZoraThemeScope';
8
7
  import type {
@@ -19,6 +18,7 @@ import { IconButton } from '../../../button/public';
19
18
  import { Card } from '../../../card/public';
20
19
  import { EmptyState } from '../../../empty-state/public';
21
20
  import { Box, Stack } from '../../../layout/public';
21
+ import { PopoverMenu, type PopoverMenuAction } from '../../../popover-menu/public';
22
22
  import { SkeletonList } from '../../../skeleton/public';
23
23
  import { Text, type TextAlign } from '../../../typography/public';
24
24
  import { resolveDataTableRowKey } from '../../utils/resolveDataTableRowKey';
@@ -139,10 +139,11 @@ function resolveNextSortDirection(
139
139
  return current === 'asc' ? 'desc' : 'asc';
140
140
  }
141
141
 
142
+ /*** Maps row-scoped DataTable actions into the shared PopoverMenu action contract. */
142
143
  function mapRowActions<TRow extends object>(
143
144
  row: TRow,
144
145
  actions: readonly DataTableRowAction<TRow>[],
145
- ): readonly MenuAction[] {
146
+ ): readonly PopoverMenuAction[] {
146
147
  return actions.map((action) => ({
147
148
  description: action.description,
148
149
  disabled: action.disabled,
@@ -154,6 +155,7 @@ function mapRowActions<TRow extends object>(
154
155
  }));
155
156
  }
156
157
 
158
+ /*** Renders the row action trigger and anchored PopoverMenu when actions exist. */
157
159
  function renderRowActions<TRow extends object>({
158
160
  row,
159
161
  rowIndex,
@@ -174,18 +176,20 @@ function renderRowActions<TRow extends object>({
174
176
  }
175
177
 
176
178
  return (
177
- <DropdownMenu
179
+ <PopoverMenu
178
180
  actions={mapRowActions(row, actions)}
179
181
  interactionPolicy={interactionPolicy}
180
182
  testID={testID ? `${testID}-row-actions-${rowIndex}` : undefined}
181
- trigger={
183
+ trigger={({ toggle }) => (
182
184
  <IconButton
183
185
  icon={{ name: 'ellipsis-horizontal' }}
186
+ interactionPolicy={interactionPolicy}
184
187
  label="Row actions"
188
+ onPress={toggle}
185
189
  size="s"
186
190
  variant="ghost"
187
191
  />
188
- }
192
+ )}
189
193
  />
190
194
  );
191
195
  }
@@ -3,7 +3,7 @@ export const formFieldMeta = {
3
3
  name: 'FormField',
4
4
  category: 'component',
5
5
  directManifestNode: true,
6
- allowedChildren: ['Checkbox', 'CheckboxGroup', 'RadioGroup', 'TextInput'],
6
+ allowedChildren: ['Checkbox', 'CheckboxGroup', 'RadioGroup', 'Select', 'TextInput'],
7
7
  blueprint: { label: 'Form field', defaultProps: { label: 'Label' } },
8
8
  bindings: {
9
9
  props: {
@@ -0,0 +1,78 @@
1
+ import React from 'react';
2
+ import { type ListRenderItemInfo, StyleSheet } from 'react-native';
3
+
4
+ import { withZoraThemeScope } from '../../../../../theme/withZoraThemeScope';
5
+ import type { SelectOption, SelectProps } from '../../../../../types/select';
6
+ import { BottomSheetFlatList, useBottomSheet } from '../../../../bottom-sheet/public';
7
+ import { SelectField } from '../../composition/SelectField';
8
+ import { SelectOptionRow } from '../../composition/SelectOptionRow';
9
+ import { SelectTrigger } from '../../composition/SelectTrigger';
10
+ import { useSelectController } from '../../composition/useSelectController';
11
+
12
+ const SELECT_SHEET_MAX_HEIGHT = 480;
13
+
14
+ /*** Renders Select with BottomSheetFlatList presentation on native platforms. */
15
+ export const Select = withZoraThemeScope(SelectInner);
16
+
17
+ /*** Connects shared Select state and controls to the native BottomSheet host. */
18
+ function SelectInner<TValue extends string = string>({
19
+ themeId: _themeId,
20
+ mode: _mode,
21
+ ...props
22
+ }: SelectProps<TValue>) {
23
+ const { dismiss, present } = useBottomSheet();
24
+ const { select, selectedOption, value } = useSelectController(props);
25
+ const passive = props.interactionPolicy === 'passive';
26
+ const { disabled, interactionPolicy, options, readOnly, testID } = props;
27
+ const listData = React.useMemo(() => [...options], [options]);
28
+
29
+ const handleSelect = React.useCallback(
30
+ (nextValue: TValue) => {
31
+ select(nextValue);
32
+ dismiss();
33
+ },
34
+ [dismiss, select],
35
+ );
36
+
37
+ const renderOption = React.useCallback(
38
+ ({ item }: ListRenderItemInfo<SelectOption<TValue>>) => (
39
+ <SelectOptionRow
40
+ interactionPolicy={interactionPolicy}
41
+ onSelect={handleSelect}
42
+ option={item}
43
+ selected={value === item.value}
44
+ testID={testID}
45
+ />
46
+ ),
47
+ [handleSelect, interactionPolicy, testID, value],
48
+ );
49
+
50
+ const openSelect = React.useCallback(() => {
51
+ if (disabled || readOnly || passive) return;
52
+
53
+ present({
54
+ contentMode: 'direct',
55
+ maxDynamicContentSize: SELECT_SHEET_MAX_HEIGHT,
56
+ content: (
57
+ <BottomSheetFlatList
58
+ contentContainerStyle={styles.listContent}
59
+ data={listData}
60
+ keyExtractor={(item) => item.value}
61
+ renderItem={renderOption}
62
+ />
63
+ ),
64
+ });
65
+ }, [disabled, listData, passive, present, readOnly, renderOption]);
66
+
67
+ return (
68
+ <SelectField props={props}>
69
+ <SelectTrigger displayLabel={selectedOption?.label} onPress={openSelect} props={props} />
70
+ </SelectField>
71
+ );
72
+ }
73
+
74
+ const styles = StyleSheet.create({
75
+ listContent: {
76
+ padding: 16,
77
+ },
78
+ });
@@ -0,0 +1 @@
1
+ export { Select } from './Select.web';
@@ -0,0 +1,63 @@
1
+ import { Popover } from '@ankhorage/surface';
2
+ import React from 'react';
3
+
4
+ import { withZoraThemeScope } from '../../../../../theme/withZoraThemeScope';
5
+ import type { SelectProps } from '../../../../../types/select';
6
+ import { Stack } from '../../../../layout/public';
7
+ import { Surface } from '../../../../surface/public';
8
+ import { SelectField } from '../../composition/SelectField';
9
+ import { SelectOptionRow } from '../../composition/SelectOptionRow';
10
+ import { SelectTrigger } from '../../composition/SelectTrigger';
11
+ import { useSelectController } from '../../composition/useSelectController';
12
+
13
+ /*** Renders Select with anchored Popover presentation on web. */
14
+ export const Select = withZoraThemeScope(SelectInner);
15
+
16
+ /*** Connects shared Select state and controls to the web Popover host. */
17
+ function SelectInner<TValue extends string = string>({
18
+ themeId: _themeId,
19
+ mode: _mode,
20
+ ...props
21
+ }: SelectProps<TValue>) {
22
+ const [open, setOpen] = React.useState(false);
23
+ const { select, selectedOption, value } = useSelectController(props);
24
+ const { interactionPolicy, options, testID } = props;
25
+
26
+ const selectAndClose = React.useCallback(
27
+ (nextValue: TValue) => {
28
+ select(nextValue);
29
+ setOpen(false);
30
+ },
31
+ [select],
32
+ );
33
+
34
+ return (
35
+ <SelectField props={props}>
36
+ <Popover
37
+ anchor={({ toggle }) => (
38
+ <SelectTrigger displayLabel={selectedOption?.label} onPress={toggle} props={props} />
39
+ )}
40
+ interactionPolicy={props.interactionPolicy}
41
+ onOpenChange={setOpen}
42
+ open={open}
43
+ placement="bottom-start"
44
+ testID={testID ? `${testID}-popover` : undefined}
45
+ >
46
+ <Surface variant="raised">
47
+ <Stack gap="xs" p="xs">
48
+ {options.map((option) => (
49
+ <SelectOptionRow
50
+ interactionPolicy={interactionPolicy}
51
+ key={option.value}
52
+ onSelect={selectAndClose}
53
+ option={option}
54
+ selected={value === option.value}
55
+ testID={testID}
56
+ />
57
+ ))}
58
+ </Stack>
59
+ </Surface>
60
+ </Popover>
61
+ </SelectField>
62
+ );
63
+ }
@@ -0,0 +1,27 @@
1
+ import { Field } from '@ankhorage/surface';
2
+ import type React from 'react';
3
+
4
+ import type { SelectProps } from '../../../../types/select';
5
+
6
+ /*** Wraps Select presentation in the canonical Surface form-field semantics. */
7
+ export function SelectField<TValue extends string>({ children, props }: SelectFieldProps<TValue>) {
8
+ return (
9
+ <Field
10
+ disabled={props.disabled}
11
+ errorText={props.errorText}
12
+ helperText={props.helperText}
13
+ invalid={props.invalid}
14
+ label={props.label}
15
+ readOnly={props.readOnly}
16
+ required={props.required}
17
+ testID={props.testID ? `${props.testID}-field` : undefined}
18
+ >
19
+ {children}
20
+ </Field>
21
+ );
22
+ }
23
+
24
+ interface SelectFieldProps<TValue extends string> {
25
+ children: React.ReactNode;
26
+ props: SelectProps<TValue>;
27
+ }
@@ -0,0 +1,36 @@
1
+ import type { SelectOption, SelectProps } from '../../../../types/select';
2
+ import { Button } from '../../../button/public';
3
+
4
+ const SELECTED_ICON = { name: 'checkmark' } as const;
5
+
6
+ /*** Renders one Select option with shared web/native selection semantics. */
7
+ export function SelectOptionRow<TValue extends string>({
8
+ interactionPolicy,
9
+ onSelect,
10
+ option,
11
+ selected,
12
+ testID,
13
+ }: SelectOptionRowProps<TValue>) {
14
+ return (
15
+ <Button
16
+ color={selected ? 'primary' : 'neutral'}
17
+ disabled={option.disabled}
18
+ fullWidth
19
+ interactionPolicy={interactionPolicy}
20
+ onPress={() => onSelect(option.value)}
21
+ testID={testID ? `${testID}-option-${option.value}` : undefined}
22
+ trailingIcon={selected ? SELECTED_ICON : undefined}
23
+ variant={selected ? 'soft' : 'ghost'}
24
+ >
25
+ {option.label}
26
+ </Button>
27
+ );
28
+ }
29
+
30
+ interface SelectOptionRowProps<TValue extends string> {
31
+ interactionPolicy: SelectProps<TValue>['interactionPolicy'];
32
+ onSelect: (value: TValue) => void;
33
+ option: SelectOption<TValue>;
34
+ selected: boolean;
35
+ testID: string | undefined;
36
+ }
@@ -0,0 +1,32 @@
1
+ import type { SelectProps } from '../../../../types/select';
2
+ import { Button } from '../../../button/public';
3
+
4
+ const SELECT_CHEVRON_ICON = { name: 'chevron-down' } as const;
5
+
6
+ /*** Renders the shared Select trigger with the resolved option label or placeholder. */
7
+ export function SelectTrigger<TValue extends string>({
8
+ displayLabel,
9
+ onPress,
10
+ props,
11
+ }: SelectTriggerProps<TValue>) {
12
+ return (
13
+ <Button
14
+ color={props.invalid ? 'danger' : 'neutral'}
15
+ disabled={props.disabled}
16
+ fullWidth
17
+ interactionPolicy={props.interactionPolicy}
18
+ onPress={props.readOnly ? undefined : onPress}
19
+ testID={props.testID ? `${props.testID}-trigger` : undefined}
20
+ trailingIcon={SELECT_CHEVRON_ICON}
21
+ variant="outline"
22
+ >
23
+ {displayLabel ?? props.placeholder ?? 'Select'}
24
+ </Button>
25
+ );
26
+ }
27
+
28
+ interface SelectTriggerProps<TValue extends string> {
29
+ displayLabel: string | undefined;
30
+ onPress: () => void;
31
+ props: SelectProps<TValue>;
32
+ }
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+
3
+ import type { SelectProps } from '../../../../types/select';
4
+
5
+ /*** Resolves controlled or uncontrolled Select value state and the selected option. */
6
+ export function useSelectController<TValue extends string>(props: SelectProps<TValue>) {
7
+ const { defaultValue, onValueChange, options, value: controlledValue } = props;
8
+ const [localValue, setLocalValue] = React.useState<TValue | undefined>(defaultValue);
9
+ const value = controlledValue ?? localValue;
10
+ const selectedOption = options.find((option) => option.value === value);
11
+
12
+ const select = React.useCallback(
13
+ (nextValue: TValue) => {
14
+ if (controlledValue === undefined) setLocalValue(nextValue);
15
+ onValueChange?.(nextValue);
16
+ },
17
+ [controlledValue, onValueChange],
18
+ );
19
+
20
+ return { select, selectedOption, value } as const;
21
+ }
@@ -0,0 +1,25 @@
1
+ import { expect, test } from 'bun:test';
2
+
3
+ import { selectMeta } from './selectMeta';
4
+
5
+ test('Select is a direct manifest node with valueChange payload', () => {
6
+ expect(selectMeta.directManifestNode).toBe(true);
7
+ expect(selectMeta.events.valueChange.eventType).toBe('select.valueChange');
8
+ expect(selectMeta.events.valueChange.payloadFields).toEqual([
9
+ { path: 'value', type: 'string', label: 'Value' },
10
+ ]);
11
+ });
12
+
13
+ test('Select uses web Popover and native BottomSheetFlatList presentation', async () => {
14
+ const [neutral, web, native] = await Promise.all([
15
+ Bun.file('src/features/form/select/adapters/inbound/Select.tsx').text(),
16
+ Bun.file('src/features/form/select/adapters/inbound/Select.web.tsx').text(),
17
+ Bun.file('src/features/form/select/adapters/inbound/Select.native.tsx').text(),
18
+ ]);
19
+
20
+ expect(neutral).toContain("export { Select } from './Select.web';");
21
+ expect(web).toContain("import { Popover } from '@ankhorage/surface';");
22
+ expect(native).toContain('<BottomSheetFlatList');
23
+ expect(native).toContain("contentMode: 'direct'");
24
+ expect(`${neutral}\n${web}\n${native}`).not.toContain('@react-native-picker/picker');
25
+ });
@@ -0,0 +1,2 @@
1
+ export type { SelectOption, SelectProps } from '../../../types/select';
2
+ export { Select } from './adapters/inbound/Select';