@ankhorage/zora 2.4.3 → 2.4.5

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 (82) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +155 -0
  3. package/dist/components/action-sheet/ActionSheet.d.ts +4 -0
  4. package/dist/components/action-sheet/ActionSheet.d.ts.map +1 -0
  5. package/dist/components/action-sheet/ActionSheet.js +8 -0
  6. package/dist/components/action-sheet/ActionSheet.js.map +1 -0
  7. package/dist/components/action-sheet/ActionSheetItem.d.ts +4 -0
  8. package/dist/components/action-sheet/ActionSheetItem.d.ts.map +1 -0
  9. package/dist/components/action-sheet/ActionSheetItem.js +14 -0
  10. package/dist/components/action-sheet/ActionSheetItem.js.map +1 -0
  11. package/dist/components/action-sheet/index.d.ts +4 -0
  12. package/dist/components/action-sheet/index.d.ts.map +1 -0
  13. package/dist/components/action-sheet/index.js +3 -0
  14. package/dist/components/action-sheet/index.js.map +1 -0
  15. package/dist/components/action-sheet/meta.d.ts +17 -0
  16. package/dist/components/action-sheet/meta.d.ts.map +1 -0
  17. package/dist/components/action-sheet/meta.js +17 -0
  18. package/dist/components/action-sheet/meta.js.map +1 -0
  19. package/dist/components/action-sheet/types.d.ts +13 -0
  20. package/dist/components/action-sheet/types.d.ts.map +1 -0
  21. package/dist/components/action-sheet/types.js +2 -0
  22. package/dist/components/action-sheet/types.js.map +1 -0
  23. package/dist/components/data-table/DataTable.d.ts +4 -0
  24. package/dist/components/data-table/DataTable.d.ts.map +1 -0
  25. package/dist/components/data-table/DataTable.js +207 -0
  26. package/dist/components/data-table/DataTable.js.map +1 -0
  27. package/dist/components/data-table/index.d.ts +3 -0
  28. package/dist/components/data-table/index.d.ts.map +1 -0
  29. package/dist/components/data-table/index.js +2 -0
  30. package/dist/components/data-table/index.js.map +1 -0
  31. package/dist/components/data-table/meta.d.ts +9 -0
  32. package/dist/components/data-table/meta.d.ts.map +1 -0
  33. package/dist/components/data-table/meta.js +9 -0
  34. package/dist/components/data-table/meta.js.map +1 -0
  35. package/dist/components/data-table/types.d.ts +51 -0
  36. package/dist/components/data-table/types.d.ts.map +1 -0
  37. package/dist/components/data-table/types.js +2 -0
  38. package/dist/components/data-table/types.js.map +1 -0
  39. package/dist/components/menu/DropdownMenu.d.ts +4 -0
  40. package/dist/components/menu/DropdownMenu.d.ts.map +1 -0
  41. package/dist/components/menu/DropdownMenu.js +6 -0
  42. package/dist/components/menu/DropdownMenu.js.map +1 -0
  43. package/dist/components/menu/Menu.d.ts +4 -0
  44. package/dist/components/menu/Menu.d.ts.map +1 -0
  45. package/dist/components/menu/Menu.js +43 -0
  46. package/dist/components/menu/Menu.js.map +1 -0
  47. package/dist/components/menu/index.d.ts +4 -0
  48. package/dist/components/menu/index.d.ts.map +1 -0
  49. package/dist/components/menu/index.js +3 -0
  50. package/dist/components/menu/index.js.map +1 -0
  51. package/dist/components/menu/meta.d.ts +17 -0
  52. package/dist/components/menu/meta.d.ts.map +1 -0
  53. package/dist/components/menu/meta.js +17 -0
  54. package/dist/components/menu/meta.js.map +1 -0
  55. package/dist/components/menu/types.d.ts +22 -0
  56. package/dist/components/menu/types.d.ts.map +1 -0
  57. package/dist/components/menu/types.js +2 -0
  58. package/dist/components/menu/types.js.map +1 -0
  59. package/dist/index.d.ts +6 -0
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +3 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/metadata/componentMeta.d.ts.map +1 -1
  64. package/dist/metadata/componentMeta.js +8 -0
  65. package/dist/metadata/componentMeta.js.map +1 -1
  66. package/package.json +2 -2
  67. package/src/components/action-sheet/ActionSheet.tsx +11 -0
  68. package/src/components/action-sheet/ActionSheetItem.tsx +32 -0
  69. package/src/components/action-sheet/index.ts +3 -0
  70. package/src/components/action-sheet/meta.ts +19 -0
  71. package/src/components/action-sheet/types.ts +23 -0
  72. package/src/components/data-table/DataTable.tsx +388 -0
  73. package/src/components/data-table/index.ts +11 -0
  74. package/src/components/data-table/meta.ts +10 -0
  75. package/src/components/data-table/types.ts +57 -0
  76. package/src/components/menu/DropdownMenu.tsx +8 -0
  77. package/src/components/menu/Menu.tsx +63 -0
  78. package/src/components/menu/index.ts +3 -0
  79. package/src/components/menu/meta.ts +19 -0
  80. package/src/components/menu/types.ts +31 -0
  81. package/src/index.ts +15 -0
  82. package/src/metadata/componentMeta.ts +8 -0
@@ -0,0 +1,32 @@
1
+ import { ActionSheetItem as SurfaceActionSheetItem } from '@ankhorage/surface';
2
+ import React from 'react';
3
+
4
+ import { resolveIconSize } from '../../internal/recipes';
5
+ import { useZoraTheme } from '../../theme/useZoraTheme';
6
+ import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
7
+ import { Icon } from '../icon';
8
+ import type { ActionSheetItemProps } from './types';
9
+
10
+ function ActionSheetItemInner({
11
+ themeId: _themeId,
12
+ mode: _mode,
13
+ icon,
14
+ leading,
15
+ ...props
16
+ }: ActionSheetItemProps) {
17
+ const { theme } = useZoraTheme();
18
+ const resolvedLeading =
19
+ leading ??
20
+ (icon ? (
21
+ <Icon
22
+ color={theme.semantics.content.muted}
23
+ name={icon.name}
24
+ provider={icon.provider}
25
+ size={resolveIconSize('s')}
26
+ />
27
+ ) : undefined);
28
+
29
+ return <SurfaceActionSheetItem {...props} leading={resolvedLeading} />;
30
+ }
31
+
32
+ export const ActionSheetItem = withZoraThemeScope(ActionSheetItemInner);
@@ -0,0 +1,3 @@
1
+ export { ActionSheet } from './ActionSheet';
2
+ export { ActionSheetItem } from './ActionSheetItem';
3
+ export type { ActionSheetItemProps, ActionSheetProps } from './types';
@@ -0,0 +1,19 @@
1
+ import type { ZoraComponentMeta } from '../../metadata';
2
+
3
+ export const actionSheetMeta = {
4
+ name: 'ActionSheet',
5
+ category: 'component',
6
+ directManifestNode: false,
7
+ allowedChildren: [],
8
+ note: 'Surface-backed contextual action sheet; not represented as a manifest node in v1.',
9
+ props: {},
10
+ } as const satisfies ZoraComponentMeta;
11
+
12
+ export const actionSheetItemMeta = {
13
+ name: 'ActionSheetItem',
14
+ category: 'component',
15
+ directManifestNode: false,
16
+ allowedChildren: [],
17
+ note: 'Surface-backed contextual action sheet row; not represented as a manifest node in v1.',
18
+ props: {},
19
+ } as const satisfies ZoraComponentMeta;
@@ -0,0 +1,23 @@
1
+ import type {
2
+ ActionSheetItemProps as SurfaceActionSheetItemProps,
3
+ ActionSheetProps as SurfaceActionSheetProps,
4
+ ButtonIconSpec,
5
+ } from '@ankhorage/surface';
6
+ import type React from 'react';
7
+
8
+ import type { ZoraColor } from '../../internal/recipes';
9
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
10
+
11
+ export interface ActionSheetProps
12
+ extends ZoraBaseProps, Omit<SurfaceActionSheetProps, 'children' | 'mode' | 'themeId'> {
13
+ children?: React.ReactNode;
14
+ }
15
+
16
+ export interface ActionSheetItemProps
17
+ extends
18
+ ZoraBaseProps,
19
+ Omit<SurfaceActionSheetItemProps, 'color' | 'leading' | 'mode' | 'themeId'> {
20
+ color?: ZoraColor;
21
+ icon?: ButtonIconSpec;
22
+ leading?: React.ReactNode;
23
+ }
@@ -0,0 +1,388 @@
1
+ import React from 'react';
2
+ import { ScrollView, type ViewStyle } from 'react-native';
3
+
4
+ import { Box, Show, Stack } from '../../foundation';
5
+ import { EmptyState } from '../../patterns/empty-state';
6
+ import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
7
+ import { Button } from '../button';
8
+ import { Card } from '../card';
9
+ import { IconButton } from '../icon-button';
10
+ import { DropdownMenu, type MenuAction } from '../menu';
11
+ import { SkeletonList } from '../skeleton';
12
+ import { Text, type TextAlign } from '../text';
13
+ import type {
14
+ DataTableCellContext,
15
+ DataTableColumn,
16
+ DataTableColumnAlign,
17
+ DataTableDensity,
18
+ DataTableProps,
19
+ DataTableRowAction,
20
+ DataTableSortDirection,
21
+ } from './types';
22
+
23
+ function resolveTextAlign(align: DataTableColumnAlign | undefined): TextAlign {
24
+ switch (align) {
25
+ case 'center':
26
+ return 'center';
27
+ case 'end':
28
+ return 'right';
29
+ case 'start':
30
+ default:
31
+ return 'left';
32
+ }
33
+ }
34
+
35
+ function resolveCellJustify(align: DataTableColumnAlign | undefined): ViewStyle['alignItems'] {
36
+ switch (align) {
37
+ case 'center':
38
+ return 'center';
39
+ case 'end':
40
+ return 'flex-end';
41
+ case 'start':
42
+ default:
43
+ return 'flex-start';
44
+ }
45
+ }
46
+
47
+ function resolveCellStyle<TRow extends object>(column: DataTableColumn<TRow>): ViewStyle {
48
+ return {
49
+ alignItems: resolveCellJustify(column.align),
50
+ flexGrow: column.width === undefined ? 1 : 0,
51
+ flexShrink: 0,
52
+ minWidth: column.minWidth ?? 140,
53
+ width: column.width,
54
+ };
55
+ }
56
+
57
+ function resolveRowPadding(density: DataTableDensity) {
58
+ return density === 'compact'
59
+ ? { px: 'm' as const, py: 's' as const }
60
+ : { px: 'm' as const, py: 'm' as const };
61
+ }
62
+
63
+ function resolveAccessorValue<TRow extends object>(row: TRow, column: DataTableColumn<TRow>) {
64
+ if (column.accessor === undefined) {
65
+ return undefined;
66
+ }
67
+
68
+ return row[column.accessor];
69
+ }
70
+
71
+ function renderDefaultCell(value: unknown): React.ReactNode {
72
+ if (value === null || value === undefined) {
73
+ return '—';
74
+ }
75
+
76
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
77
+ return String(value);
78
+ }
79
+
80
+ if (typeof value === 'bigint') {
81
+ return value.toString();
82
+ }
83
+
84
+ if (value instanceof Date) {
85
+ return value.toLocaleDateString();
86
+ }
87
+
88
+ return '—';
89
+ }
90
+
91
+ function createCellContext<TRow extends object>(
92
+ column: DataTableColumn<TRow>,
93
+ row: TRow,
94
+ rowIndex: number,
95
+ ): DataTableCellContext<TRow> {
96
+ return {
97
+ column,
98
+ row,
99
+ rowIndex,
100
+ value: resolveAccessorValue(row, column),
101
+ };
102
+ }
103
+
104
+ function renderCell<TRow extends object>(
105
+ column: DataTableColumn<TRow>,
106
+ row: TRow,
107
+ rowIndex: number,
108
+ ) {
109
+ const context = createCellContext(column, row, rowIndex);
110
+ return column.renderCell ? column.renderCell(context) : renderDefaultCell(context.value);
111
+ }
112
+
113
+ function renderTableCell<TRow extends object>(
114
+ column: DataTableColumn<TRow>,
115
+ row: TRow,
116
+ rowIndex: number,
117
+ ) {
118
+ if (column.renderCell) {
119
+ return column.renderCell(createCellContext(column, row, rowIndex));
120
+ }
121
+
122
+ return (
123
+ <Text align={resolveTextAlign(column.align)} variant="bodySmall">
124
+ {renderDefaultCell(resolveAccessorValue(row, column))}
125
+ </Text>
126
+ );
127
+ }
128
+
129
+ function resolveNextSortDirection(
130
+ current: DataTableSortDirection | undefined,
131
+ ): DataTableSortDirection {
132
+ return current === 'asc' ? 'desc' : 'asc';
133
+ }
134
+
135
+ function mapRowActions<TRow extends object>(
136
+ row: TRow,
137
+ actions: readonly DataTableRowAction<TRow>[],
138
+ ): readonly MenuAction[] {
139
+ return actions.map((action) => ({
140
+ description: action.description,
141
+ disabled: action.disabled,
142
+ icon: action.icon,
143
+ id: action.id,
144
+ intent: action.intent,
145
+ onPress: action.onPress ? () => action.onPress?.(row) : undefined,
146
+ title: action.title,
147
+ }));
148
+ }
149
+
150
+ function renderRowActions<TRow extends object>({
151
+ row,
152
+ rowIndex,
153
+ rowActions,
154
+ testID,
155
+ }: {
156
+ row: TRow;
157
+ rowIndex: number;
158
+ rowActions: DataTableProps<TRow>['rowActions'];
159
+ testID?: string;
160
+ }) {
161
+ const actions = rowActions?.(row, rowIndex) ?? [];
162
+
163
+ if (actions.length === 0) {
164
+ return null;
165
+ }
166
+
167
+ return (
168
+ <DropdownMenu
169
+ actions={mapRowActions(row, actions)}
170
+ trigger={
171
+ <IconButton
172
+ icon={{ name: 'ellipsis-horizontal' }}
173
+ label="Row actions"
174
+ size="s"
175
+ variant="ghost"
176
+ />
177
+ }
178
+ testID={testID ? `${testID}-row-actions-${rowIndex}` : undefined}
179
+ />
180
+ );
181
+ }
182
+
183
+ function DataTableHeader<TRow extends object>({
184
+ columns,
185
+ sort,
186
+ onSortChange,
187
+ density,
188
+ }: Pick<DataTableProps<TRow>, 'columns' | 'density' | 'onSortChange' | 'sort'>) {
189
+ const padding = resolveRowPadding(density ?? 'comfortable');
190
+
191
+ return (
192
+ <Box bg="subtle" borderColor="border" borderWidth={1} radius="m">
193
+ <Stack direction="row" align="center">
194
+ {columns.map((column) => {
195
+ const currentDirection = sort?.columnId === column.id ? sort.direction : undefined;
196
+ const sortable = Boolean(column.sortable && onSortChange);
197
+ const directionLabel =
198
+ currentDirection === undefined ? '' : currentDirection === 'asc' ? ' ↑' : ' ↓';
199
+
200
+ return (
201
+ <Box key={column.id} px={padding.px} py={padding.py} style={resolveCellStyle(column)}>
202
+ {sortable ? (
203
+ <Button
204
+ color="primary"
205
+ onPress={() =>
206
+ onSortChange?.({
207
+ columnId: column.id,
208
+ direction: resolveNextSortDirection(currentDirection),
209
+ })
210
+ }
211
+ size="s"
212
+ variant="ghost"
213
+ >
214
+ {column.header}
215
+ {directionLabel}
216
+ </Button>
217
+ ) : (
218
+ <Text
219
+ align={resolveTextAlign(column.align)}
220
+ emphasis="muted"
221
+ variant="caption"
222
+ weight="semiBold"
223
+ >
224
+ {column.header}
225
+ </Text>
226
+ )}
227
+ </Box>
228
+ );
229
+ })}
230
+ <Box px={padding.px} py={padding.py} style={{ minWidth: 56, width: 56 }}>
231
+ <Text align="right" emphasis="muted" variant="caption" weight="semiBold">
232
+ Actions
233
+ </Text>
234
+ </Box>
235
+ </Stack>
236
+ </Box>
237
+ );
238
+ }
239
+
240
+ function DataTableDesktop<TRow extends object>({
241
+ columns,
242
+ rows,
243
+ rowActions,
244
+ rowId,
245
+ sort,
246
+ onSortChange,
247
+ density,
248
+ testID,
249
+ }: DataTableProps<TRow>) {
250
+ const padding = resolveRowPadding(density ?? 'comfortable');
251
+
252
+ return (
253
+ <ScrollView horizontal showsHorizontalScrollIndicator={false}>
254
+ <Box minWidth={720} style={{ width: '100%' }}>
255
+ <Stack gap="xs">
256
+ <DataTableHeader
257
+ columns={columns}
258
+ density={density}
259
+ onSortChange={onSortChange}
260
+ sort={sort}
261
+ />
262
+ <Stack gap="xs">
263
+ {rows.map((row, rowIndex) => (
264
+ <Box
265
+ bg="surface"
266
+ borderColor="border"
267
+ borderWidth={1}
268
+ key={rowId(row, rowIndex)}
269
+ radius="m"
270
+ testID={testID ? `${testID}-row-${rowIndex}` : undefined}
271
+ >
272
+ <Stack direction="row" align="center">
273
+ {columns.map((column) => (
274
+ <Box
275
+ key={column.id}
276
+ px={padding.px}
277
+ py={padding.py}
278
+ style={resolveCellStyle(column)}
279
+ >
280
+ {renderTableCell(column, row, rowIndex)}
281
+ </Box>
282
+ ))}
283
+ <Box
284
+ px={padding.px}
285
+ py={padding.py}
286
+ style={{ alignItems: 'flex-end', minWidth: 56, width: 56 }}
287
+ >
288
+ {renderRowActions({ row, rowActions, rowIndex, testID })}
289
+ </Box>
290
+ </Stack>
291
+ </Box>
292
+ ))}
293
+ </Stack>
294
+ </Stack>
295
+ </Box>
296
+ </ScrollView>
297
+ );
298
+ }
299
+
300
+ function DataTableMobile<TRow extends object>({
301
+ columns,
302
+ rows,
303
+ rowActions,
304
+ rowId,
305
+ testID,
306
+ }: DataTableProps<TRow>) {
307
+ const [primaryColumn, ...detailColumns] = columns;
308
+
309
+ return (
310
+ <Stack gap="s">
311
+ {rows.map((row, rowIndex) => {
312
+ const title = primaryColumn
313
+ ? renderCell(primaryColumn, row, rowIndex)
314
+ : `Row ${rowIndex + 1}`;
315
+ const actions = renderRowActions({ row, rowActions, rowIndex, testID });
316
+
317
+ return (
318
+ <Card
319
+ actions={actions}
320
+ compact
321
+ key={rowId(row, rowIndex)}
322
+ testID={testID ? `${testID}-card-${rowIndex}` : undefined}
323
+ title={title}
324
+ >
325
+ <Stack gap="s">
326
+ {detailColumns.map((column) => (
327
+ <Stack gap="xxs" key={column.id}>
328
+ <Text emphasis="muted" variant="caption" weight="semiBold">
329
+ {column.header}
330
+ </Text>
331
+ {column.renderCell ? (
332
+ renderCell(column, row, rowIndex)
333
+ ) : (
334
+ <Text variant="bodySmall">
335
+ {renderDefaultCell(resolveAccessorValue(row, column))}
336
+ </Text>
337
+ )}
338
+ </Stack>
339
+ ))}
340
+ </Stack>
341
+ </Card>
342
+ );
343
+ })}
344
+ </Stack>
345
+ );
346
+ }
347
+
348
+ function DataTableInner<TRow extends object>({
349
+ themeId: _themeId,
350
+ mode: _mode,
351
+ rows,
352
+ loading = false,
353
+ loadingRows = 5,
354
+ emptyTitle = 'No data',
355
+ emptyDescription = 'There are no rows to display.',
356
+ testID,
357
+ density = 'comfortable',
358
+ ...props
359
+ }: DataTableProps<TRow>) {
360
+ if (loading) {
361
+ return <SkeletonList rows={loadingRows} variant="card" testID={testID} />;
362
+ }
363
+
364
+ if (rows.length === 0) {
365
+ return <EmptyState title={emptyTitle} description={emptyDescription} />;
366
+ }
367
+
368
+ const tableProps: DataTableProps<TRow> = {
369
+ ...props,
370
+ density,
371
+ emptyDescription,
372
+ emptyTitle,
373
+ loading,
374
+ loadingRows,
375
+ rows,
376
+ testID,
377
+ };
378
+
379
+ return (
380
+ <Box testID={testID}>
381
+ <Show when={{ base: false, md: true }} fallback={<DataTableMobile {...tableProps} />}>
382
+ <DataTableDesktop {...tableProps} />
383
+ </Show>
384
+ </Box>
385
+ );
386
+ }
387
+
388
+ export const DataTable = withZoraThemeScope(DataTableInner);
@@ -0,0 +1,11 @@
1
+ export { DataTable } from './DataTable';
2
+ export type {
3
+ DataTableCellContext,
4
+ DataTableColumn,
5
+ DataTableColumnAlign,
6
+ DataTableDensity,
7
+ DataTableProps,
8
+ DataTableRowAction,
9
+ DataTableSortDirection,
10
+ DataTableSortState,
11
+ } from './types';
@@ -0,0 +1,10 @@
1
+ import type { ZoraComponentMeta } from '../../metadata';
2
+
3
+ export const dataTableMeta = {
4
+ name: 'DataTable',
5
+ category: 'component',
6
+ directManifestNode: false,
7
+ allowedChildren: [],
8
+ note: 'Code-facing typed data table component; not represented as a manifest node in v1.',
9
+ props: {},
10
+ } as const satisfies ZoraComponentMeta;
@@ -0,0 +1,57 @@
1
+ import type { ButtonIconSpec } from '@ankhorage/surface';
2
+ import type React from 'react';
3
+
4
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
5
+ import type { MenuActionIntent } from '../menu';
6
+
7
+ export type DataTableColumnAlign = 'start' | 'center' | 'end';
8
+ export type DataTableDensity = 'comfortable' | 'compact';
9
+ export type DataTableSortDirection = 'asc' | 'desc';
10
+
11
+ export interface DataTableSortState {
12
+ columnId: string;
13
+ direction: DataTableSortDirection;
14
+ }
15
+
16
+ export interface DataTableCellContext<TRow extends object> {
17
+ row: TRow;
18
+ rowIndex: number;
19
+ column: DataTableColumn<TRow>;
20
+ value: unknown;
21
+ }
22
+
23
+ export interface DataTableColumn<TRow extends object> {
24
+ id: string;
25
+ header: React.ReactNode;
26
+ accessor?: keyof TRow;
27
+ align?: DataTableColumnAlign;
28
+ width?: number;
29
+ minWidth?: number;
30
+ sortable?: boolean;
31
+ renderCell?: (context: DataTableCellContext<TRow>) => React.ReactNode;
32
+ }
33
+
34
+ export interface DataTableRowAction<TRow extends object> {
35
+ id: string;
36
+ title: React.ReactNode;
37
+ description?: React.ReactNode;
38
+ icon?: ButtonIconSpec;
39
+ intent?: MenuActionIntent;
40
+ disabled?: boolean;
41
+ onPress?: (row: TRow) => void;
42
+ }
43
+
44
+ export interface DataTableProps<TRow extends object> extends ZoraBaseProps {
45
+ columns: readonly DataTableColumn<TRow>[];
46
+ rows: readonly TRow[];
47
+ rowId: (row: TRow, index: number) => string;
48
+ rowActions?: (row: TRow, index: number) => readonly DataTableRowAction<TRow>[];
49
+ sort?: DataTableSortState;
50
+ onSortChange?: (sort: DataTableSortState) => void;
51
+ loading?: boolean;
52
+ loadingRows?: number;
53
+ emptyTitle?: React.ReactNode;
54
+ emptyDescription?: React.ReactNode;
55
+ density?: DataTableDensity;
56
+ testID?: string;
57
+ }
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+
3
+ import { Menu } from './Menu';
4
+ import type { DropdownMenuProps } from './types';
5
+
6
+ export function DropdownMenu(props: DropdownMenuProps) {
7
+ return <Menu {...props} />;
8
+ }
@@ -0,0 +1,63 @@
1
+ import { Menu as SurfaceMenu, type MenuAction as SurfaceMenuAction } from '@ankhorage/surface';
2
+ import React, { useMemo } from 'react';
3
+
4
+ import { Box } from '../../foundation';
5
+ import { resolveIconSize } from '../../internal/recipes';
6
+ import { useZoraTheme } from '../../theme/useZoraTheme';
7
+ import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
8
+ import { Icon } from '../icon';
9
+ import type { MenuAction, MenuProps } from './types';
10
+
11
+ function renderActionLeading(action: MenuAction, iconColor: string) {
12
+ if (action.leading) {
13
+ return action.leading;
14
+ }
15
+
16
+ if (!action.icon) {
17
+ return undefined;
18
+ }
19
+
20
+ return (
21
+ <Icon
22
+ color={iconColor}
23
+ name={action.icon.name}
24
+ provider={action.icon.provider}
25
+ size={resolveIconSize('s')}
26
+ />
27
+ );
28
+ }
29
+
30
+ function renderActionTrailing(action: MenuAction) {
31
+ if (!action.trailing) {
32
+ return undefined;
33
+ }
34
+
35
+ return <Box>{action.trailing}</Box>;
36
+ }
37
+
38
+ function toSurfaceAction(action: MenuAction, iconColor: string): SurfaceMenuAction {
39
+ return {
40
+ activate: action.onPress,
41
+ description: action.description,
42
+ disabled: action.disabled,
43
+ id: action.id,
44
+ intent: action.intent,
45
+ leading: renderActionLeading(action, iconColor),
46
+ selected: action.selected,
47
+ title: action.title,
48
+ trailing: renderActionTrailing(action),
49
+ };
50
+ }
51
+
52
+ function MenuInner({ themeId: _themeId, mode: _mode, actions, onDismiss, ...props }: MenuProps) {
53
+ const { theme } = useZoraTheme();
54
+ const iconColor = theme.semantics.content.muted;
55
+ const surfaceActions = useMemo(
56
+ () => actions.map((action) => toSurfaceAction(action, iconColor)),
57
+ [actions, iconColor],
58
+ );
59
+
60
+ return <SurfaceMenu {...props} actions={surfaceActions} dismiss={onDismiss} />;
61
+ }
62
+
63
+ export const Menu = withZoraThemeScope(MenuInner);
@@ -0,0 +1,3 @@
1
+ export { DropdownMenu } from './DropdownMenu';
2
+ export { Menu } from './Menu';
3
+ export type { DropdownMenuProps, MenuAction, MenuActionIntent, MenuProps } from './types';
@@ -0,0 +1,19 @@
1
+ import type { ZoraComponentMeta } from '../../metadata';
2
+
3
+ export const menuMeta = {
4
+ name: 'Menu',
5
+ category: 'component',
6
+ directManifestNode: false,
7
+ allowedChildren: [],
8
+ note: 'Surface-backed action menu; not represented as a manifest node in v1.',
9
+ props: {},
10
+ } as const satisfies ZoraComponentMeta;
11
+
12
+ export const dropdownMenuMeta = {
13
+ name: 'DropdownMenu',
14
+ category: 'component',
15
+ directManifestNode: false,
16
+ allowedChildren: [],
17
+ note: 'Product-facing alias for Menu trigger/action composition; not represented as a manifest node in v1.',
18
+ props: {},
19
+ } as const satisfies ZoraComponentMeta;
@@ -0,0 +1,31 @@
1
+ import type {
2
+ ButtonIconSpec,
3
+ MenuActionIntent as SurfaceMenuActionIntent,
4
+ MenuProps as SurfaceMenuProps,
5
+ } from '@ankhorage/surface';
6
+ import type React from 'react';
7
+
8
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
9
+
10
+ export type MenuActionIntent = SurfaceMenuActionIntent;
11
+
12
+ export interface MenuAction {
13
+ id: string;
14
+ title: React.ReactNode;
15
+ description?: React.ReactNode;
16
+ icon?: ButtonIconSpec;
17
+ leading?: React.ReactNode;
18
+ trailing?: React.ReactNode;
19
+ intent?: MenuActionIntent;
20
+ disabled?: boolean;
21
+ selected?: boolean;
22
+ onPress?: () => void;
23
+ }
24
+
25
+ export interface MenuProps
26
+ extends ZoraBaseProps, Omit<SurfaceMenuProps, 'actions' | 'dismiss' | 'mode' | 'themeId'> {
27
+ actions: readonly MenuAction[];
28
+ onDismiss?: () => void;
29
+ }
30
+
31
+ export type DropdownMenuProps = MenuProps;