@cdx-ui/components 0.2.2 → 0.2.3

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 (144) hide show
  1. package/README.md +4 -2
  2. package/coverage.svg +1 -1
  3. package/lib/commonjs/components/Checkbox/index.js.map +1 -1
  4. package/lib/commonjs/components/Heading/index.js.map +1 -1
  5. package/lib/commonjs/components/Icon/index.js.map +1 -1
  6. package/lib/commonjs/components/ListItem/index.js +170 -9
  7. package/lib/commonjs/components/ListItem/index.js.map +1 -1
  8. package/lib/commonjs/components/ListItem/styles.js +36 -1
  9. package/lib/commonjs/components/ListItem/styles.js.map +1 -1
  10. package/lib/commonjs/components/Menu/index.js.map +1 -1
  11. package/lib/commonjs/components/RangeCalendar/index.js.map +1 -1
  12. package/lib/commonjs/components/ReorderableList/ReorderableListShared.js +91 -0
  13. package/lib/commonjs/components/ReorderableList/ReorderableListShared.js.map +1 -0
  14. package/lib/commonjs/components/ReorderableList/index.js +135 -0
  15. package/lib/commonjs/components/ReorderableList/index.js.map +1 -0
  16. package/lib/commonjs/components/ReorderableList/index.web.js +233 -0
  17. package/lib/commonjs/components/ReorderableList/index.web.js.map +1 -0
  18. package/lib/commonjs/components/ReorderableList/labels.js +47 -0
  19. package/lib/commonjs/components/ReorderableList/labels.js.map +1 -0
  20. package/lib/commonjs/components/ReorderableList/reorderItems.js +16 -0
  21. package/lib/commonjs/components/ReorderableList/reorderItems.js.map +1 -0
  22. package/lib/commonjs/components/ReorderableList/rowContext.js +42 -0
  23. package/lib/commonjs/components/ReorderableList/rowContext.js.map +1 -0
  24. package/lib/commonjs/components/ReorderableList/types.js +6 -0
  25. package/lib/commonjs/components/ReorderableList/types.js.map +1 -0
  26. package/lib/commonjs/components/ReorderableList/useReorder.js +131 -0
  27. package/lib/commonjs/components/ReorderableList/useReorder.js.map +1 -0
  28. package/lib/commonjs/components/ReorderableList/webDnd.js +64 -0
  29. package/lib/commonjs/components/ReorderableList/webDnd.js.map +1 -0
  30. package/lib/commonjs/components/ScopedTheme/index.js.map +1 -1
  31. package/lib/commonjs/components/Skeleton/index.js.map +1 -1
  32. package/lib/commonjs/components/TablePagination/index.js.map +1 -1
  33. package/lib/commonjs/components/Text/index.js.map +1 -1
  34. package/lib/commonjs/components/Tile/styles.js +1 -1
  35. package/lib/commonjs/components/Tile/styles.js.map +1 -1
  36. package/lib/commonjs/components/index.js.map +1 -1
  37. package/lib/commonjs/reorderable-list.js +17 -0
  38. package/lib/commonjs/reorderable-list.js.map +1 -0
  39. package/lib/module/components/Checkbox/index.js.map +1 -1
  40. package/lib/module/components/Heading/index.js.map +1 -1
  41. package/lib/module/components/Icon/index.js.map +1 -1
  42. package/lib/module/components/ListItem/index.js +163 -14
  43. package/lib/module/components/ListItem/index.js.map +1 -1
  44. package/lib/module/components/ListItem/styles.js +35 -0
  45. package/lib/module/components/ListItem/styles.js.map +1 -1
  46. package/lib/module/components/Menu/index.js.map +1 -1
  47. package/lib/module/components/RangeCalendar/index.js.map +1 -1
  48. package/lib/module/components/ReorderableList/ReorderableListShared.js +86 -0
  49. package/lib/module/components/ReorderableList/ReorderableListShared.js.map +1 -0
  50. package/lib/module/components/ReorderableList/index.js +130 -0
  51. package/lib/module/components/ReorderableList/index.js.map +1 -0
  52. package/lib/module/components/ReorderableList/index.web.js +229 -0
  53. package/lib/module/components/ReorderableList/index.web.js.map +1 -0
  54. package/lib/module/components/ReorderableList/labels.js +41 -0
  55. package/lib/module/components/ReorderableList/labels.js.map +1 -0
  56. package/lib/module/components/ReorderableList/reorderItems.js +12 -0
  57. package/lib/module/components/ReorderableList/reorderItems.js.map +1 -0
  58. package/lib/module/components/ReorderableList/rowContext.js +38 -0
  59. package/lib/module/components/ReorderableList/rowContext.js.map +1 -0
  60. package/lib/module/components/ReorderableList/types.js +4 -0
  61. package/lib/module/components/ReorderableList/types.js.map +1 -0
  62. package/lib/module/components/ReorderableList/useReorder.js +123 -0
  63. package/lib/module/components/ReorderableList/useReorder.js.map +1 -0
  64. package/lib/module/components/ReorderableList/webDnd.js +59 -0
  65. package/lib/module/components/ReorderableList/webDnd.js.map +1 -0
  66. package/lib/module/components/ScopedTheme/index.js.map +1 -1
  67. package/lib/module/components/Skeleton/index.js.map +1 -1
  68. package/lib/module/components/TablePagination/index.js.map +1 -1
  69. package/lib/module/components/Text/index.js.map +1 -1
  70. package/lib/module/components/Tile/styles.js +1 -1
  71. package/lib/module/components/Tile/styles.js.map +1 -1
  72. package/lib/module/components/index.js.map +1 -1
  73. package/lib/module/reorderable-list.js +4 -0
  74. package/lib/module/reorderable-list.js.map +1 -0
  75. package/lib/typescript/components/Checkbox/index.d.ts +22 -0
  76. package/lib/typescript/components/Checkbox/index.d.ts.map +1 -1
  77. package/lib/typescript/components/Heading/index.d.ts +20 -2
  78. package/lib/typescript/components/Heading/index.d.ts.map +1 -1
  79. package/lib/typescript/components/Icon/index.d.ts +2 -0
  80. package/lib/typescript/components/Icon/index.d.ts.map +1 -1
  81. package/lib/typescript/components/ListItem/index.d.ts +23 -1
  82. package/lib/typescript/components/ListItem/index.d.ts.map +1 -1
  83. package/lib/typescript/components/ListItem/styles.d.ts +5 -0
  84. package/lib/typescript/components/ListItem/styles.d.ts.map +1 -1
  85. package/lib/typescript/components/Menu/index.d.ts +116 -4
  86. package/lib/typescript/components/Menu/index.d.ts.map +1 -1
  87. package/lib/typescript/components/RangeCalendar/index.d.ts +2 -0
  88. package/lib/typescript/components/RangeCalendar/index.d.ts.map +1 -1
  89. package/lib/typescript/components/ReorderableList/ReorderableListShared.d.ts +31 -0
  90. package/lib/typescript/components/ReorderableList/ReorderableListShared.d.ts.map +1 -0
  91. package/lib/typescript/components/ReorderableList/index.d.ts +8 -0
  92. package/lib/typescript/components/ReorderableList/index.d.ts.map +1 -0
  93. package/lib/typescript/components/ReorderableList/index.web.d.ts +8 -0
  94. package/lib/typescript/components/ReorderableList/index.web.d.ts.map +1 -0
  95. package/lib/typescript/components/ReorderableList/labels.d.ts +16 -0
  96. package/lib/typescript/components/ReorderableList/labels.d.ts.map +1 -0
  97. package/lib/typescript/components/ReorderableList/reorderItems.d.ts +3 -0
  98. package/lib/typescript/components/ReorderableList/reorderItems.d.ts.map +1 -0
  99. package/lib/typescript/components/ReorderableList/rowContext.d.ts +57 -0
  100. package/lib/typescript/components/ReorderableList/rowContext.d.ts.map +1 -0
  101. package/lib/typescript/components/ReorderableList/types.d.ts +75 -0
  102. package/lib/typescript/components/ReorderableList/types.d.ts.map +1 -0
  103. package/lib/typescript/components/ReorderableList/useReorder.d.ts +31 -0
  104. package/lib/typescript/components/ReorderableList/useReorder.d.ts.map +1 -0
  105. package/lib/typescript/components/ReorderableList/webDnd.d.ts +27 -0
  106. package/lib/typescript/components/ReorderableList/webDnd.d.ts.map +1 -0
  107. package/lib/typescript/components/ScopedTheme/index.d.ts +6 -1
  108. package/lib/typescript/components/ScopedTheme/index.d.ts.map +1 -1
  109. package/lib/typescript/components/Skeleton/index.d.ts +3 -0
  110. package/lib/typescript/components/Skeleton/index.d.ts.map +1 -1
  111. package/lib/typescript/components/TablePagination/index.d.ts +54 -0
  112. package/lib/typescript/components/TablePagination/index.d.ts.map +1 -1
  113. package/lib/typescript/components/Text/index.d.ts +12 -2
  114. package/lib/typescript/components/Text/index.d.ts.map +1 -1
  115. package/lib/typescript/components/Tile/styles.d.ts.map +1 -1
  116. package/lib/typescript/components/ToggleButton/styles.d.ts +1 -1
  117. package/lib/typescript/components/index.d.ts +1 -0
  118. package/lib/typescript/components/index.d.ts.map +1 -1
  119. package/lib/typescript/reorderable-list.d.ts +2 -0
  120. package/lib/typescript/reorderable-list.d.ts.map +1 -0
  121. package/package.json +34 -7
  122. package/src/components/Checkbox/index.tsx +22 -0
  123. package/src/components/Heading/index.tsx +10 -1
  124. package/src/components/Icon/index.tsx +2 -0
  125. package/src/components/ListItem/index.tsx +227 -11
  126. package/src/components/ListItem/styles.ts +40 -0
  127. package/src/components/Menu/index.tsx +117 -4
  128. package/src/components/RangeCalendar/index.tsx +2 -0
  129. package/src/components/ReorderableList/ReorderableListShared.tsx +113 -0
  130. package/src/components/ReorderableList/index.tsx +162 -0
  131. package/src/components/ReorderableList/index.web.tsx +254 -0
  132. package/src/components/ReorderableList/labels.ts +52 -0
  133. package/src/components/ReorderableList/reorderItems.ts +16 -0
  134. package/src/components/ReorderableList/rowContext.tsx +72 -0
  135. package/src/components/ReorderableList/types.ts +80 -0
  136. package/src/components/ReorderableList/useReorder.ts +159 -0
  137. package/src/components/ReorderableList/webDnd.ts +60 -0
  138. package/src/components/ScopedTheme/index.tsx +6 -1
  139. package/src/components/Skeleton/index.tsx +3 -0
  140. package/src/components/TablePagination/index.tsx +54 -0
  141. package/src/components/Text/index.tsx +6 -1
  142. package/src/components/Tile/styles.ts +4 -1
  143. package/src/components/index.ts +5 -0
  144. package/src/reorderable-list.ts +1 -0
@@ -1,5 +1,16 @@
1
- import { forwardRef, useMemo, type ReactNode } from 'react';
2
- import { Text, View, type TextProps, type ViewProps } from 'react-native';
1
+ import {
2
+ Children,
3
+ createContext,
4
+ forwardRef,
5
+ isValidElement,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ type ReactElement,
10
+ type ReactNode,
11
+ } from 'react';
12
+ import { Platform, Text, View, type TextProps, type ViewProps } from 'react-native';
13
+ import { DragIndicator } from '@cdx-ui/icons';
3
14
  import {
4
15
  ListItemRoot as ListItemRootPrimitive,
5
16
  Slot,
@@ -8,12 +19,23 @@ import {
8
19
  type IListItemProps,
9
20
  type ListItemCrossAlign,
10
21
  } from '@cdx-ui/primitives';
11
- import { cn, ParentContext, useParentContext, useStyleContext } from '@cdx-ui/utils';
22
+ import {
23
+ cn,
24
+ composeEventHandlers,
25
+ mergeRefs,
26
+ ParentContext,
27
+ useParentContext,
28
+ useStyleContext,
29
+ } from '@cdx-ui/utils';
30
+ import { Icon } from '../Icon';
31
+ import { useReorderableRow } from '../ReorderableList/rowContext';
12
32
  import {
13
33
  listItemContentVariants,
14
34
  listItemDescriptionVariants,
35
+ listItemDragHandleVariants,
15
36
  listItemLeadingSlotVariants,
16
37
  listItemMetaVariants,
38
+ listItemPressableRegionVariants,
17
39
  listItemRootVariants,
18
40
  listItemSectionHeaderLabelVariants,
19
41
  listItemSectionHeaderVariants,
@@ -34,6 +56,40 @@ const useListItemStyleContext = (): Pick<ListItemVariantProps, 'crossAlign'> =>
34
56
  // ROOT
35
57
  // =============================================================================
36
58
 
59
+ /**
60
+ * `true` inside a pressable row's interactive element. `ListItem.DragHandle` reads it to warn when it
61
+ * is nested there instead of being a direct child of the row.
62
+ */
63
+ const ListItemPressableContext = createContext(false);
64
+
65
+ interface DragHandlePartition {
66
+ handle: ReactElement;
67
+ position: 'start' | 'end';
68
+ rest: ReactNode[];
69
+ misplaced: boolean;
70
+ extra: boolean;
71
+ }
72
+
73
+ /** Finds a direct-child `ListItem.DragHandle` (matched by identity) of a pressable row. */
74
+ function partitionDragHandle(children: ReactNode): DragHandlePartition | undefined {
75
+ const items = Children.toArray(children);
76
+ const handleIndexes: number[] = [];
77
+ items.forEach((child, index) => {
78
+ if (isValidElement(child) && child.type === ListItemDragHandle) handleIndexes.push(index);
79
+ });
80
+ if (handleIndexes.length === 0) return undefined;
81
+ const [first] = handleIndexes;
82
+ return {
83
+ handle: items[first] as ReactElement,
84
+ position: first === 0 ? 'start' : 'end',
85
+ rest: items.filter((_, index) => !handleIndexes.includes(index)),
86
+ misplaced: first !== 0 && first !== items.length - 1,
87
+ extra: handleIndexes.length > 1,
88
+ };
89
+ }
90
+
91
+ let hasWarnedDragHandlePlacement = false;
92
+
37
93
  export interface ListItemProps extends Omit<IListItemProps, 'crossAlign'>, ListItemVariantProps {
38
94
  /**
39
95
  * Vertical density. `compact` reduces padding and min-height; typography stays the same.
@@ -60,6 +116,13 @@ export interface ListItemProps extends Omit<IListItemProps, 'crossAlign'>, ListI
60
116
  className?: string;
61
117
  }
62
118
 
119
+ /**
120
+ * A pressable row (`onPress`, or `asChild`) with a `ListItem.DragHandle` as its first or last
121
+ * direct child renders the handle beside the pressable element instead of inside it. The handle
122
+ * is matched by identity, so a `memo()` or wrapper component around it is not recognized. Under
123
+ * `asChild`, pass the handle and the single interactive element as siblings:
124
+ * `<ListItem asChild><ListItem.DragHandle /><Link>…</Link></ListItem>`.
125
+ */
63
126
  const ListItemRoot = forwardRef<View, ListItemProps>(
64
127
  (
65
128
  {
@@ -68,6 +131,7 @@ const ListItemRoot = forwardRef<View, ListItemProps>(
68
131
  crossAlign: crossAlignProp = 'center',
69
132
  className,
70
133
  style,
134
+ children,
71
135
  ...props
72
136
  },
73
137
  ref,
@@ -75,22 +139,60 @@ const ListItemRoot = forwardRef<View, ListItemProps>(
75
139
  const crossAlign: ListItemCrossAlign = crossAlignProp ?? 'center';
76
140
  const parent = useParentContext();
77
141
  const ctx = useMemo(() => ({ ...parent, [SCOPE]: { crossAlign } }), [parent, crossAlign]);
142
+ const isDragging = useReorderableRow()?.isDragging ?? false;
143
+ const isPressable = props.onPress != null || !!props.asChild;
144
+ const partition = isPressable ? partitionDragHandle(children) : undefined;
145
+ const misplacedHandle = !!partition && (partition.misplaced || partition.extra);
146
+
147
+ useEffect(() => {
148
+ if (__DEV__ && misplacedHandle && !hasWarnedDragHandlePlacement) {
149
+ hasWarnedDragHandlePlacement = true;
150
+ console.warn(
151
+ '[ListItem] Place one ListItem.DragHandle as the first or last child of a pressable ListItem. A handle elsewhere is moved to the end, and extra handles are dropped.',
152
+ );
153
+ }
154
+ }, [misplacedHandle]);
78
155
 
79
156
  const computedClassName = cn(
80
157
  listItemRootVariants({ size, showSeparator, crossAlign }),
81
158
  className,
82
159
  );
83
160
 
161
+ const splitProps = partition && {
162
+ accessory: (
163
+ <ListItemPressableContext.Provider value={false}>
164
+ {partition.handle}
165
+ </ListItemPressableContext.Provider>
166
+ ),
167
+ accessoryPosition: partition.position,
168
+ pressableClassName: listItemPressableRegionVariants({
169
+ size: size ?? 'default',
170
+ handlePosition: partition.position,
171
+ }),
172
+ };
173
+ const regionChildren =
174
+ partition && props.asChild && partition.rest.length === 1
175
+ ? partition.rest[0]
176
+ : partition?.rest;
177
+
84
178
  return (
85
179
  <ParentContext.Provider value={ctx}>
86
- <ListItemRootPrimitive
87
- ref={ref}
88
- className={computedClassName}
89
- crossAlign={crossAlign}
90
- style={style}
91
- {...props}
92
- {...mergeDataAttributes(props, dataAttributes({ slot: 'list-item' }))}
93
- />
180
+ <ListItemPressableContext.Provider value={isPressable}>
181
+ <ListItemRootPrimitive
182
+ ref={ref}
183
+ className={computedClassName}
184
+ crossAlign={crossAlign}
185
+ style={style}
186
+ {...props}
187
+ {...splitProps}
188
+ {...mergeDataAttributes(
189
+ props,
190
+ dataAttributes({ slot: 'list-item', dragging: isDragging }),
191
+ )}
192
+ >
193
+ {partition ? regionChildren : children}
194
+ </ListItemRootPrimitive>
195
+ </ListItemPressableContext.Provider>
94
196
  </ParentContext.Provider>
95
197
  );
96
198
  },
@@ -263,6 +365,116 @@ const ListItemTrailingSlot = forwardRef<View, ListItemTrailingSlotProps>(
263
365
 
264
366
  ListItemTrailingSlot.displayName = 'ListItem.TrailingSlot';
265
367
 
368
+ function DefaultDragHandleGrip() {
369
+ return <Icon as={DragIndicator} className="size-5 text-content-secondary" />;
370
+ }
371
+
372
+ /**
373
+ * Screen reader hint for the default handle. Web omits it: react-native-web drops
374
+ * `accessibilityHint`, and dnd-kit describes the keyboard controls through `aria-describedby`.
375
+ */
376
+ const DEFAULT_DRAG_HANDLE_HINT = Platform.select<string | undefined>({
377
+ ios: 'Drag to reorder, or swipe up or down to choose a move action.',
378
+ android: 'Drag to reorder, or open the actions menu to move this item.',
379
+ default: undefined,
380
+ });
381
+
382
+ const dragHandleOuterStyle = { flexShrink: 0 as const };
383
+
384
+ let hasWarnedDragHandleOutsideList = false;
385
+ let hasWarnedNestedDragHandle = false;
386
+
387
+ export interface ListItemDragHandleProps extends ViewProps {
388
+ /**
389
+ * Merges the handle onto the consumer's single child (for example an `IconButton`) so that
390
+ * element is the one focusable handle.
391
+ */
392
+ asChild?: boolean;
393
+ className?: string;
394
+ }
395
+
396
+ /**
397
+ * Drag activator for a row rendered by `ReorderableList`. Renders `null` outside a list. On a
398
+ * pressable `ListItem`, place it as the first or last direct child so it sits beside the row's
399
+ * pressable element rather than inside it.
400
+ */
401
+ const ListItemDragHandle = forwardRef<View, ListItemDragHandleProps>(
402
+ (
403
+ {
404
+ asChild,
405
+ className,
406
+ style,
407
+ children,
408
+ accessibilityLabel = 'Reorder',
409
+ accessibilityHint,
410
+ accessibilityActions,
411
+ onAccessibilityAction,
412
+ ...props
413
+ },
414
+ ref,
415
+ ) => {
416
+ const row = useReorderableRow();
417
+ const insidePressable = useContext(ListItemPressableContext);
418
+ const isOutsideList = row === null;
419
+
420
+ useEffect(() => {
421
+ if (!__DEV__) return;
422
+ if (isOutsideList && !hasWarnedDragHandleOutsideList) {
423
+ hasWarnedDragHandleOutsideList = true;
424
+ console.warn(
425
+ '[ListItem.DragHandle] Rendered outside a ReorderableList row, so it renders nothing. Use it inside ReorderableList renderItem.',
426
+ );
427
+ }
428
+ if (insidePressable && !hasWarnedNestedDragHandle) {
429
+ hasWarnedNestedDragHandle = true;
430
+ console.warn(
431
+ "[ListItem.DragHandle] Nested inside a pressable ListItem, so it is part of the row's pressable element. Make it the first or last direct child of ListItem so it renders beside it.",
432
+ );
433
+ }
434
+ }, [isOutsideList, insidePressable]);
435
+
436
+ const handleRef = row?.handle.ref;
437
+ const hostRef = useMemo(() => mergeRefs(ref, handleRef), [ref, handleRef]);
438
+
439
+ if (!row) return null;
440
+
441
+ const { handle } = row;
442
+ const handleProps = {
443
+ ...handle.props,
444
+ accessible: true,
445
+ accessibilityRole: 'button' as const,
446
+ accessibilityLabel,
447
+ accessibilityHint:
448
+ accessibilityHint ??
449
+ (row.accessibilityActions.length > 0 ? DEFAULT_DRAG_HANDLE_HINT : undefined),
450
+ accessibilityActions: [...row.accessibilityActions, ...(accessibilityActions ?? [])],
451
+ onAccessibilityAction: composeEventHandlers(onAccessibilityAction, row.onAccessibilityAction),
452
+ className: cn(listItemDragHandleVariants(), className),
453
+ ...props,
454
+ ...mergeDataAttributes(props, dataAttributes({ slot: 'list-item-drag-handle' })),
455
+ style: [handle.props.style, style],
456
+ };
457
+
458
+ if (asChild) {
459
+ return handle.wrap(
460
+ <Slot ref={hostRef} {...handleProps}>
461
+ {children}
462
+ </Slot>,
463
+ dragHandleOuterStyle,
464
+ );
465
+ }
466
+
467
+ return handle.wrap(
468
+ <View ref={hostRef} {...handleProps}>
469
+ {children ?? <DefaultDragHandleGrip />}
470
+ </View>,
471
+ dragHandleOuterStyle,
472
+ );
473
+ },
474
+ );
475
+
476
+ ListItemDragHandle.displayName = 'ListItem.DragHandle';
477
+
266
478
  const wrapSectionHeaderLabel = (children: ReactNode): ReactNode => {
267
479
  if (typeof children === 'string' || typeof children === 'number') {
268
480
  return <Text className={cn(listItemSectionHeaderLabelVariants())}>{children}</Text>;
@@ -367,6 +579,7 @@ ListItemSectionHeader.displayName = 'ListItem.SectionHeader';
367
579
 
368
580
  type ListItemCompound = typeof ListItemRoot & {
369
581
  LeadingSlot: typeof ListItemLeadingSlot;
582
+ DragHandle: typeof ListItemDragHandle;
370
583
  Content: typeof ListItemContent;
371
584
  Title: typeof ListItemTitle;
372
585
  Description: typeof ListItemDescription;
@@ -377,6 +590,7 @@ type ListItemCompound = typeof ListItemRoot & {
377
590
 
378
591
  export const ListItem = Object.assign(ListItemRoot, {
379
592
  LeadingSlot: ListItemLeadingSlot,
593
+ DragHandle: ListItemDragHandle,
380
594
  Content: ListItemContent,
381
595
  Title: ListItemTitle,
382
596
  Description: ListItemDescription,
@@ -389,8 +603,10 @@ export type { ListItemCrossAlign, ListItemSectionHeaderVariantProps, ListItemVar
389
603
  export {
390
604
  listItemContentVariants,
391
605
  listItemDescriptionVariants,
606
+ listItemDragHandleVariants,
392
607
  listItemLeadingSlotVariants,
393
608
  listItemMetaVariants,
609
+ listItemPressableRegionVariants,
394
610
  listItemRootVariants,
395
611
  listItemSectionHeaderLabelVariants,
396
612
  listItemSectionHeaderVariants,
@@ -31,6 +31,10 @@ export const listItemRootVariants = cva(
31
31
  ].join(' '),
32
32
  default: 'data-[active=true]:bg-surface-primary-hover',
33
33
  }),
34
+ 'data-[dragging=true]:bg-surface-secondary',
35
+ // Native: changing the dragged row's zIndex mid-drag cancels the handle's pan on Fabric iOS;
36
+ // the drag engine stacks the active row itself.
37
+ Platform.select({ web: 'data-[dragging=true]:z-20', default: '' }),
34
38
  ],
35
39
  {
36
40
  variants: {
@@ -58,6 +62,37 @@ export const listItemRootVariants = cva(
58
62
  },
59
63
  );
60
64
 
65
+ // ── Pressable region (pressable row with a direct-child DragHandle) ─────────
66
+ //
67
+ // The root becomes a non-interactive container and keeps every visual above: the primitive
68
+ // mirrors the region's hover/active/focus state onto it, so the overlay, focus ring, and native
69
+ // tint still span the whole row. The region itself is transparent; negative margins extend it
70
+ // over the container's padding (and the gap beside the handle) so the hit area matches a row
71
+ // without a handle. A consumer `className` that changes the row's padding is not mirrored here.
72
+
73
+ export const listItemPressableRegionVariants = cva(
74
+ [
75
+ 'relative flex-1 min-w-0 flex-row self-stretch gap-3 bg-transparent',
76
+ Platform.select({ web: 'outline-none', default: '' }),
77
+ ],
78
+ {
79
+ variants: {
80
+ size: {
81
+ default: '-my-3 py-3',
82
+ compact: '-my-2 py-2',
83
+ },
84
+ handlePosition: {
85
+ start: '-ms-3 ps-3 -me-5 pe-5',
86
+ end: '-ms-5 ps-5 -me-3 pe-3',
87
+ },
88
+ },
89
+ defaultVariants: {
90
+ size: 'default',
91
+ handlePosition: 'start',
92
+ },
93
+ },
94
+ );
95
+
61
96
  // ── Leading / trailing slots ─────────────────────────────────────────────────
62
97
 
63
98
  export const listItemLeadingSlotVariants = cva([
@@ -79,6 +114,11 @@ export const listItemTrailingSlotVariants = cva(
79
114
  },
80
115
  );
81
116
 
117
+ export const listItemDragHandleVariants = cva([
118
+ 'relative z-10 flex shrink-0 self-stretch items-center justify-center',
119
+ 'web:cursor-grab web:active:cursor-grabbing',
120
+ ]);
121
+
82
122
  // ── Content stack (title / description / meta) ───────────────────────────────
83
123
 
84
124
  export const listItemContentVariants = cva('relative z-10 flex min-w-0 flex-1 flex-col gap-1', {
@@ -98,7 +98,10 @@ const useMenuItemStyleValue = (
98
98
  // =============================================================================
99
99
 
100
100
  export interface MenuProps extends ViewProps, IMenuOverlayRootProps {
101
+ /** Additional Uniwind/Tailwind classes, merged after the root's own styles. */
101
102
  className?: string;
103
+ /** The `Menu.Trigger` and `Menu.Popup`. */
104
+ children?: ReactNode;
102
105
  }
103
106
 
104
107
  const MenuOverlayRoot = forwardRef<View, MenuProps>(
@@ -122,7 +125,9 @@ MenuOverlayRoot.displayName = 'Menu';
122
125
  // =============================================================================
123
126
 
124
127
  export interface MenuTriggerProps extends IMenuTriggerProps {
128
+ /** Additional Uniwind/Tailwind classes, merged after the trigger's own styles. */
125
129
  className?: string;
130
+ /** Trigger content (or the element to render when `asChild` is set). */
126
131
  children?: ReactNode;
127
132
  }
128
133
 
@@ -147,7 +152,9 @@ MenuTrigger.displayName = 'Menu.Trigger';
147
152
  // =============================================================================
148
153
 
149
154
  export interface MenuPopupProps extends IMenuPopupProps {
155
+ /** Additional Uniwind/Tailwind classes, merged after the popup's own styles. */
150
156
  className?: string;
157
+ /** Popup content, typically a `Menu.Content`. */
151
158
  children?: ReactNode;
152
159
  }
153
160
 
@@ -178,8 +185,16 @@ MenuPopup.displayName = 'Menu.Popup';
178
185
  // =============================================================================
179
186
 
180
187
  export interface MenuContentProps
181
- extends ViewProps, Omit<IMenuProps, 'scrollable'>, MenuRootVariantProps {
188
+ extends ViewProps, Omit<IMenuProps, 'scrollable'>, Omit<MenuRootVariantProps, 'scrollable'> {
189
+ /** Additional Uniwind/Tailwind classes, merged after the menu surface's own styles. */
182
190
  className?: string;
191
+ /**
192
+ * Caps the surface height and scrolls its items. Content inside a `Menu.Popup` always scrolls.
193
+ * @default false
194
+ */
195
+ scrollable?: MenuRootVariantProps['scrollable'];
196
+ /** The menu's items, groups and separators. */
197
+ children?: ReactNode;
183
198
  /** Visual-only surface without menu semantics (e.g. inside Select listbox). */
184
199
  asSurface?: boolean;
185
200
  }
@@ -241,14 +256,28 @@ MenuContent.displayName = 'Menu.Content';
241
256
  // ITEM
242
257
  // =============================================================================
243
258
 
244
- export interface MenuItemProps extends Omit<MenuItemVariantProps, 'color'>, IMenuItemProps {
259
+ export interface MenuItemProps
260
+ extends Omit<MenuItemVariantProps, 'color' | 'inset' | 'layout'>, IMenuItemProps {
261
+ /** Render the consumer's child element in place of the default `Pressable` host. */
245
262
  asChild?: boolean;
246
263
  /**
247
264
  * Color theme applied to the item content and interaction states.
248
265
  * @default 'neutral'
249
266
  */
250
267
  color?: 'neutral' | 'danger';
268
+ /**
269
+ * Indents the item content so its label lines up with items that have a leading icon.
270
+ * @default false
271
+ */
272
+ inset?: MenuItemVariantProps['inset'];
273
+ /**
274
+ * Set automatically: `filter` when the item contains a `Menu.ItemControl` or `Menu.ItemAction`.
275
+ * @internal
276
+ */
277
+ layout?: MenuItemVariantProps['layout'];
278
+ /** Additional Uniwind/Tailwind classes, merged after the item's own styles. */
251
279
  className?: string;
280
+ /** Item content, typically a `Menu.ItemLabel` with optional icon, shortcut and indicator slots. */
252
281
  children?: ReactNode;
253
282
  }
254
283
 
@@ -279,10 +308,27 @@ const MenuItem = forwardRef<View, MenuItemProps>(
279
308
  MenuItem.displayName = 'Menu.Item';
280
309
 
281
310
  export interface MenuCheckboxItemProps
282
- extends Omit<MenuItemVariantProps, 'color'>, IMenuCheckboxItemProps {
311
+ extends Omit<MenuItemVariantProps, 'color' | 'inset' | 'layout'>, IMenuCheckboxItemProps {
312
+ /** Render the consumer's child element in place of the default `Pressable` host. */
283
313
  asChild?: boolean;
314
+ /**
315
+ * Color theme applied to the item content and interaction states.
316
+ * @default 'neutral'
317
+ */
284
318
  color?: 'neutral' | 'danger';
319
+ /**
320
+ * Indents the item content so its label lines up with items that have a leading icon.
321
+ * @default false
322
+ */
323
+ inset?: MenuItemVariantProps['inset'];
324
+ /**
325
+ * Set automatically: `filter` when the item contains a `Menu.ItemControl` or `Menu.ItemAction`.
326
+ * @internal
327
+ */
328
+ layout?: MenuItemVariantProps['layout'];
329
+ /** Additional Uniwind/Tailwind classes, merged after the item's own styles. */
285
330
  className?: string;
331
+ /** Item content, typically a `Menu.ItemLabel` with optional icon, shortcut and indicator slots. */
286
332
  children?: ReactNode;
287
333
  }
288
334
 
@@ -311,8 +357,11 @@ const MenuCheckboxItem = forwardRef<View, MenuCheckboxItemProps>(
311
357
  MenuCheckboxItem.displayName = 'Menu.CheckboxItem';
312
358
 
313
359
  export interface MenuRadioGroupProps extends IMenuRadioGroupProps {
360
+ /** Render the consumer's child element in place of the default `View` host. */
314
361
  asChild?: boolean;
362
+ /** Additional Uniwind/Tailwind classes, merged after the group's own styles. */
315
363
  className?: string;
364
+ /** The `Menu.RadioItem` options in the group. */
316
365
  children?: ReactNode;
317
366
  }
318
367
 
@@ -334,10 +383,27 @@ const MenuRadioGroup = forwardRef<View, MenuRadioGroupProps>(
334
383
  MenuRadioGroup.displayName = 'Menu.RadioGroup';
335
384
 
336
385
  export interface MenuRadioItemProps
337
- extends Omit<MenuItemVariantProps, 'color'>, IMenuRadioItemProps {
386
+ extends Omit<MenuItemVariantProps, 'color' | 'inset' | 'layout'>, IMenuRadioItemProps {
387
+ /** Render the consumer's child element in place of the default `Pressable` host. */
338
388
  asChild?: boolean;
389
+ /**
390
+ * Color theme applied to the item content and interaction states.
391
+ * @default 'neutral'
392
+ */
339
393
  color?: 'neutral' | 'danger';
394
+ /**
395
+ * Indents the item content so its label lines up with items that have a leading icon.
396
+ * @default false
397
+ */
398
+ inset?: MenuItemVariantProps['inset'];
399
+ /**
400
+ * Set automatically: `filter` when the item contains a `Menu.ItemControl` or `Menu.ItemAction`.
401
+ * @internal
402
+ */
403
+ layout?: MenuItemVariantProps['layout'];
404
+ /** Additional Uniwind/Tailwind classes, merged after the item's own styles. */
340
405
  className?: string;
406
+ /** Item content, typically a `Menu.ItemLabel` with optional icon, shortcut and indicator slots. */
341
407
  children?: ReactNode;
342
408
  }
343
409
 
@@ -383,8 +449,12 @@ MenuRadioItem.displayName = 'Menu.RadioItem';
383
449
  // =============================================================================
384
450
 
385
451
  export interface MenuItemLabelProps extends TextProps {
452
+ /** Render the consumer's child element in place of the default `Text` host. */
386
453
  asChild?: boolean;
454
+ /** Additional Uniwind/Tailwind classes, merged after the label's own styles. */
387
455
  className?: string;
456
+ /** Label text. */
457
+ children?: ReactNode;
388
458
  /** Checked state when used outside a selectable Menu item (e.g. `Select.ItemLabel`). */
389
459
  isChecked?: boolean;
390
460
  }
@@ -423,10 +493,18 @@ const MenuItemLabel = forwardRef<Text, MenuItemLabelProps>(
423
493
  MenuItemLabel.displayName = 'Menu.ItemLabel';
424
494
 
425
495
  export interface MenuItemIconProps extends Omit<IconProps, 'as' | 'children' | 'color'> {
496
+ /** Render the consumer's child element in place of the icon. */
426
497
  asChild?: boolean;
498
+ /** The `@cdx-ui/icons` component to render. */
427
499
  as?: IconProps['as'];
500
+ /**
501
+ * Side of the label the icon sits on; sets the spacing between them.
502
+ * @default 'leading'
503
+ */
428
504
  position?: IconSlotPosition;
505
+ /** Additional Uniwind/Tailwind classes, merged after the icon's own styles. */
429
506
  className?: string;
507
+ /** The element to render when `asChild` is set. */
430
508
  children?: ReactNode;
431
509
  }
432
510
 
@@ -482,8 +560,12 @@ const MenuItemIcon = ({
482
560
  MenuItemIcon.displayName = 'Menu.ItemIcon';
483
561
 
484
562
  export interface MenuItemShortcutProps extends TextProps {
563
+ /** Render the consumer's child element in place of the default `Text` host. */
485
564
  asChild?: boolean;
565
+ /** Additional Uniwind/Tailwind classes, merged after the shortcut's own styles. */
486
566
  className?: string;
567
+ /** Shortcut hint text, such as `⌘C`. */
568
+ children?: ReactNode;
487
569
  }
488
570
 
489
571
  const MenuItemShortcut = forwardRef<Text, MenuItemShortcutProps>(
@@ -509,11 +591,23 @@ const MenuItemShortcut = forwardRef<Text, MenuItemShortcutProps>(
509
591
  MenuItemShortcut.displayName = 'Menu.ItemShortcut';
510
592
 
511
593
  export interface MenuItemIndicatorProps extends Omit<IconProps, 'as' | 'children' | 'color'> {
594
+ /** Render the consumer's child element in place of the icon. */
512
595
  asChild?: boolean;
596
+ /**
597
+ * The `@cdx-ui/icons` component to render.
598
+ * @default Check
599
+ */
513
600
  as?: IconProps['as'];
601
+ /** Shows the indicator. Defaults to the checked state of the enclosing item. */
514
602
  isChecked?: boolean;
603
+ /**
604
+ * Side of the label the indicator sits on; sets the spacing between them.
605
+ * @default 'trailing'
606
+ */
515
607
  position?: IconSlotPosition;
608
+ /** Additional Uniwind/Tailwind classes, merged after the indicator's own styles. */
516
609
  className?: string;
610
+ /** The element to render when `asChild` is set. */
517
611
  children?: ReactNode;
518
612
  }
519
613
 
@@ -575,8 +669,12 @@ MenuItemIndicator.displayName = 'Menu.ItemIndicator';
575
669
  // =============================================================================
576
670
 
577
671
  export interface MenuSectionLabelProps extends TextProps {
672
+ /** Render the consumer's child element in place of the default `Text` host. */
578
673
  asChild?: boolean;
674
+ /** Additional Uniwind/Tailwind classes, merged after the section label's own styles. */
579
675
  className?: string;
676
+ /** Section heading text. */
677
+ children?: ReactNode;
580
678
  }
581
679
 
582
680
  const MenuSectionLabel = forwardRef<Text, MenuSectionLabelProps>(
@@ -601,7 +699,10 @@ const MenuSectionLabel = forwardRef<Text, MenuSectionLabelProps>(
601
699
  MenuSectionLabel.displayName = 'Menu.SectionLabel';
602
700
 
603
701
  export interface MenuSeparatorProps extends ViewProps {
702
+ /** Additional Uniwind/Tailwind classes, merged after the separator's own styles. */
604
703
  className?: string;
704
+ /** Not rendered; the separator draws its own line. */
705
+ children?: ReactNode;
605
706
  }
606
707
 
607
708
  const MenuSeparator = forwardRef<View, MenuSeparatorProps>(
@@ -630,8 +731,11 @@ MenuSeparator.displayName = 'Menu.Separator';
630
731
  // =============================================================================
631
732
 
632
733
  export interface MenuGroupHeaderProps extends ViewProps {
734
+ /** Render the consumer's child element in place of the default `View` host. */
633
735
  asChild?: boolean;
736
+ /** Additional Uniwind/Tailwind classes, merged after the group header's own styles. */
634
737
  className?: string;
738
+ /** Header content, typically a `Menu.ItemLabel` and an optional `Menu.ItemAction`. */
635
739
  children?: ReactNode;
636
740
  }
637
741
 
@@ -666,7 +770,9 @@ const MenuGroupHeader = forwardRef<View, MenuGroupHeaderProps>(
666
770
  MenuGroupHeader.displayName = 'Menu.GroupHeader';
667
771
 
668
772
  export interface MenuItemActionProps extends IMenuItemActionProps {
773
+ /** Additional Uniwind/Tailwind classes, merged after the action's own styles. */
669
774
  className?: string;
775
+ /** The trailing interactive control, such as a `Button`. */
670
776
  children?: ReactNode;
671
777
  }
672
778
 
@@ -688,7 +794,10 @@ const MenuItemAction = forwardRef<View, MenuItemActionProps>(
688
794
  MenuItemAction.displayName = 'Menu.ItemAction';
689
795
 
690
796
  export interface MenuItemControlProps extends ViewProps {
797
+ /** Additional Uniwind/Tailwind classes, merged after the control's own styles. */
691
798
  className?: string;
799
+ /** Not rendered; the control draws a checkbox or radio that mirrors the enclosing item. */
800
+ children?: ReactNode;
692
801
  }
693
802
 
694
803
  const MenuItemControl = ({ className, style, ...props }: MenuItemControlProps) => {
@@ -740,8 +849,12 @@ const MenuItemControl = ({ className, style, ...props }: MenuItemControlProps) =
740
849
  MenuItemControl.displayName = 'Menu.ItemControl';
741
850
 
742
851
  export interface MenuItemCountProps extends TextProps {
852
+ /** Render the consumer's child element in place of the default `Text` host. */
743
853
  asChild?: boolean;
854
+ /** Additional Uniwind/Tailwind classes, merged after the count's own styles. */
744
855
  className?: string;
856
+ /** The count to display. */
857
+ children?: ReactNode;
745
858
  }
746
859
 
747
860
  const MenuItemCount = forwardRef<Text, MenuItemCountProps>(
@@ -22,7 +22,9 @@ import { calendarRootVariants } from '../Calendar/styles';
22
22
 
23
23
  export interface RangeCalendarProps
24
24
  extends Omit<RangeCalendarRootProps, 'children'>, Omit<ViewProps, 'children'> {
25
+ /** Additional Uniwind/Tailwind classes, merged after the calendar's own styles. */
25
26
  className?: string;
27
+ /** Custom calendar body. Omit to render the default header and month grid. */
26
28
  children?: ReactNode;
27
29
  }
28
30