@cdx-ui/components 0.0.1-beta.171 → 0.0.1-beta.173

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 (50) hide show
  1. package/coverage.svg +1 -1
  2. package/lib/commonjs/components/Accordion/index.js +295 -0
  3. package/lib/commonjs/components/Accordion/index.js.map +1 -0
  4. package/lib/commonjs/components/Accordion/styles.js +49 -0
  5. package/lib/commonjs/components/Accordion/styles.js.map +1 -0
  6. package/lib/commonjs/components/Menu/index.js +7 -3
  7. package/lib/commonjs/components/Menu/index.js.map +1 -1
  8. package/lib/commonjs/components/Select/index.js +13 -8
  9. package/lib/commonjs/components/Select/index.js.map +1 -1
  10. package/lib/commonjs/components/index.js +12 -0
  11. package/lib/commonjs/components/index.js.map +1 -1
  12. package/lib/commonjs/figma/Accordion.figma.js +47 -0
  13. package/lib/commonjs/figma/Accordion.figma.js.map +1 -0
  14. package/lib/commonjs/providers/forge-design/provider.js +11 -6
  15. package/lib/commonjs/providers/forge-design/provider.js.map +1 -1
  16. package/lib/module/components/Accordion/index.js +291 -0
  17. package/lib/module/components/Accordion/index.js.map +1 -0
  18. package/lib/module/components/Accordion/styles.js +45 -0
  19. package/lib/module/components/Accordion/styles.js.map +1 -0
  20. package/lib/module/components/Menu/index.js +7 -3
  21. package/lib/module/components/Menu/index.js.map +1 -1
  22. package/lib/module/components/Select/index.js +13 -8
  23. package/lib/module/components/Select/index.js.map +1 -1
  24. package/lib/module/components/index.js +1 -0
  25. package/lib/module/components/index.js.map +1 -1
  26. package/lib/module/figma/Accordion.figma.js +41 -0
  27. package/lib/module/figma/Accordion.figma.js.map +1 -0
  28. package/lib/module/providers/forge-design/provider.js +11 -6
  29. package/lib/module/providers/forge-design/provider.js.map +1 -1
  30. package/lib/typescript/components/Accordion/index.d.ts +85 -0
  31. package/lib/typescript/components/Accordion/index.d.ts.map +1 -0
  32. package/lib/typescript/components/Accordion/styles.d.ts +18 -0
  33. package/lib/typescript/components/Accordion/styles.d.ts.map +1 -0
  34. package/lib/typescript/components/Menu/index.d.ts.map +1 -1
  35. package/lib/typescript/components/Select/index.d.ts +11 -0
  36. package/lib/typescript/components/Select/index.d.ts.map +1 -1
  37. package/lib/typescript/components/index.d.ts +1 -0
  38. package/lib/typescript/components/index.d.ts.map +1 -1
  39. package/lib/typescript/figma/Accordion.figma.d.ts +8 -0
  40. package/lib/typescript/figma/Accordion.figma.d.ts.map +1 -0
  41. package/lib/typescript/figma/Alert.Button.figma.d.ts +1 -1
  42. package/lib/typescript/figma/Alert.Close.figma.d.ts +1 -1
  43. package/lib/typescript/providers/forge-design/provider.d.ts.map +1 -1
  44. package/package.json +5 -5
  45. package/src/components/Accordion/index.tsx +388 -0
  46. package/src/components/Accordion/styles.ts +82 -0
  47. package/src/components/Menu/index.tsx +5 -1
  48. package/src/components/Select/index.tsx +39 -8
  49. package/src/components/index.ts +1 -0
  50. package/src/providers/forge-design/provider.tsx +15 -6
@@ -0,0 +1,388 @@
1
+ import { forwardRef, useMemo, type ReactNode } from 'react';
2
+ import { Pressable, Text, View, type TextProps, type ViewProps } from 'react-native';
3
+ import {
4
+ AccordionItemRoot as AccordionItemRootPrimitive,
5
+ AccordionPanelRoot as AccordionPanelRootPrimitive,
6
+ AccordionRoot as AccordionRootPrimitive,
7
+ AccordionTriggerRoot as AccordionTriggerRootPrimitive,
8
+ Slot,
9
+ dataAttributes,
10
+ mergeDataAttributes,
11
+ useAccordionItemContext,
12
+ type IAccordionItemProps,
13
+ type IAccordionProps,
14
+ type IAccordionTriggerProps,
15
+ } from '@cdx-ui/primitives';
16
+ import { KeyboardArrowDown } from '@cdx-ui/icons';
17
+ import { cn, ParentContext, useParentContext, useStyleContext } from '@cdx-ui/utils';
18
+ import { Divider } from '../Divider';
19
+ import { Icon } from '../Icon';
20
+ import {
21
+ accordionBodyVariants,
22
+ accordionDescriptionVariants,
23
+ accordionHeadingVariants,
24
+ accordionIndicatorVariants,
25
+ accordionItemVariants,
26
+ accordionLabelVariants,
27
+ accordionLeadingSlotVariants,
28
+ accordionMetaVariants,
29
+ accordionRootVariants,
30
+ accordionTitleVariants,
31
+ accordionTriggerVariants,
32
+ type AccordionItemVariantProps,
33
+ } from './styles';
34
+
35
+ const ITEM_SCOPE = 'ACCORDION_ITEM';
36
+
37
+ type AccordionItemStyleContext = Pick<AccordionItemVariantProps, 'disableGutters'> & {
38
+ showSeparator?: boolean;
39
+ };
40
+
41
+ const useAccordionItemStyleContext = (): AccordionItemStyleContext => {
42
+ const ctx = useStyleContext(ITEM_SCOPE) as AccordionItemStyleContext | undefined;
43
+ return {
44
+ disableGutters: ctx?.disableGutters ?? false,
45
+ showSeparator: ctx?.showSeparator ?? true,
46
+ };
47
+ };
48
+
49
+ // =============================================================================
50
+ // ROOT
51
+ // =============================================================================
52
+
53
+ export interface AccordionProps extends IAccordionProps {
54
+ className?: string;
55
+ children?: ReactNode;
56
+ }
57
+
58
+ const AccordionRoot = forwardRef<View, AccordionProps>(
59
+ ({ className, style, children, ...props }, ref) => (
60
+ <AccordionRootPrimitive
61
+ ref={ref}
62
+ className={cn(accordionRootVariants(), className)}
63
+ style={style}
64
+ {...props}
65
+ >
66
+ {children}
67
+ </AccordionRootPrimitive>
68
+ ),
69
+ );
70
+
71
+ AccordionRoot.displayName = 'Accordion';
72
+
73
+ // =============================================================================
74
+ // ITEM
75
+ // =============================================================================
76
+
77
+ export interface AccordionItemProps extends IAccordionItemProps {
78
+ className?: string;
79
+ children?: ReactNode;
80
+ /** Removes horizontal padding on trigger and body. */
81
+ disableGutters?: boolean;
82
+ /** Renders a Divider below the item when true. @default true */
83
+ showSeparator?: boolean;
84
+ }
85
+
86
+ const AccordionItem = forwardRef<View, AccordionItemProps>(
87
+ ({ className, style, children, disableGutters = false, showSeparator = true, ...props }, ref) => {
88
+ const parent = useParentContext();
89
+ const ctx = useMemo(
90
+ () => ({ ...parent, [ITEM_SCOPE]: { disableGutters, showSeparator } }),
91
+ [parent, disableGutters, showSeparator],
92
+ );
93
+
94
+ return (
95
+ <ParentContext.Provider value={ctx}>
96
+ <AccordionItemRootPrimitive
97
+ ref={ref}
98
+ className={cn(accordionItemVariants(), className)}
99
+ style={style}
100
+ {...props}
101
+ >
102
+ {children}
103
+ {showSeparator ? <Divider /> : null}
104
+ </AccordionItemRootPrimitive>
105
+ </ParentContext.Provider>
106
+ );
107
+ },
108
+ );
109
+
110
+ AccordionItem.displayName = 'Accordion.Item';
111
+
112
+ // =============================================================================
113
+ // TRIGGER
114
+ // =============================================================================
115
+
116
+ export interface AccordionTriggerProps extends IAccordionTriggerProps {
117
+ /**
118
+ * Substitute the rendered trigger element with the single child. The child must
119
+ * be another Forge primitive (IconButton, Chip, Button) — anything whose
120
+ * underlying Pressable accepts Pressable-shaped handlers. When set, the
121
+ * auto-injected expand indicator is skipped and `accordionTriggerVariants` is not
122
+ * applied so the substituted component's own appearance wins.
123
+ */
124
+ asChild?: boolean;
125
+ className?: string;
126
+ children?: ReactNode;
127
+ }
128
+
129
+ const AccordionTrigger = forwardRef<React.ComponentRef<typeof Pressable>, AccordionTriggerProps>(
130
+ ({ className, style, children, asChild, accessibilityLabel, ...props }, ref) => {
131
+ const { disableGutters } = useAccordionItemStyleContext();
132
+
133
+ const computedClassName = asChild
134
+ ? className
135
+ : cn(accordionTriggerVariants({ disableGutters }), className);
136
+
137
+ return (
138
+ <View className={cn(accordionHeadingVariants())}>
139
+ <AccordionTriggerRootPrimitive
140
+ ref={ref}
141
+ asChild={asChild}
142
+ accessibilityLabel={accessibilityLabel}
143
+ className={computedClassName}
144
+ style={style}
145
+ {...props}
146
+ >
147
+ {asChild ? (
148
+ children
149
+ ) : (
150
+ <>
151
+ {children}
152
+ <AccordionIndicator />
153
+ </>
154
+ )}
155
+ </AccordionTriggerRootPrimitive>
156
+ </View>
157
+ );
158
+ },
159
+ );
160
+
161
+ AccordionTrigger.displayName = 'Accordion.Trigger';
162
+
163
+ // =============================================================================
164
+ // INDICATOR
165
+ // =============================================================================
166
+
167
+ export interface AccordionIndicatorProps extends ViewProps {
168
+ className?: string;
169
+ }
170
+
171
+ const AccordionIndicator = forwardRef<View, AccordionIndicatorProps>(
172
+ ({ className, style, ...props }, ref) => {
173
+ const { isExpanded } = useAccordionItemContext();
174
+
175
+ return (
176
+ <View
177
+ ref={ref}
178
+ className={cn(accordionIndicatorVariants(), className)}
179
+ style={style}
180
+ {...props}
181
+ {...dataAttributes({
182
+ slot: 'accordion-indicator',
183
+ state: isExpanded ? 'open' : 'closed',
184
+ })}
185
+ >
186
+ <Icon as={KeyboardArrowDown} size={24} />
187
+ </View>
188
+ );
189
+ },
190
+ );
191
+
192
+ AccordionIndicator.displayName = 'Accordion.Indicator';
193
+
194
+ // =============================================================================
195
+ // SLOTS
196
+ // =============================================================================
197
+
198
+ export interface AccordionLeadingSlotProps extends ViewProps {
199
+ asChild?: boolean;
200
+ className?: string;
201
+ 'aria-hidden'?: boolean;
202
+ }
203
+
204
+ const AccordionLeadingSlot = forwardRef<View, AccordionLeadingSlotProps>(
205
+ ({ asChild, className, style, 'aria-hidden': ariaHidden, ...props }, ref) => {
206
+ const Comp = asChild ? Slot : View;
207
+ return (
208
+ <Comp
209
+ ref={ref}
210
+ accessibilityElementsHidden={ariaHidden !== false}
211
+ aria-hidden={ariaHidden ?? true}
212
+ className={cn(accordionLeadingSlotVariants(), className)}
213
+ style={style}
214
+ {...props}
215
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'accordion-leading' }))}
216
+ />
217
+ );
218
+ },
219
+ );
220
+
221
+ AccordionLeadingSlot.displayName = 'Accordion.LeadingSlot';
222
+
223
+ export interface AccordionLabelProps extends ViewProps {
224
+ asChild?: boolean;
225
+ className?: string;
226
+ }
227
+
228
+ const AccordionLabel = forwardRef<View, AccordionLabelProps>(
229
+ ({ asChild, className, style, ...props }, ref) => {
230
+ const Comp = asChild ? Slot : View;
231
+ return (
232
+ <Comp
233
+ ref={ref}
234
+ className={cn(accordionLabelVariants(), className)}
235
+ style={style}
236
+ {...props}
237
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'accordion-label' }))}
238
+ />
239
+ );
240
+ },
241
+ );
242
+
243
+ AccordionLabel.displayName = 'Accordion.Label';
244
+
245
+ export interface AccordionTitleProps extends TextProps {
246
+ asChild?: boolean;
247
+ className?: string;
248
+ }
249
+
250
+ const AccordionTitle = forwardRef<Text, AccordionTitleProps>(
251
+ ({ asChild, className, style, ...props }, ref) => {
252
+ const Comp = asChild ? Slot : Text;
253
+ return (
254
+ <Comp
255
+ ref={ref}
256
+ className={cn(accordionTitleVariants(), className)}
257
+ style={style}
258
+ {...props}
259
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'accordion-title' }))}
260
+ />
261
+ );
262
+ },
263
+ );
264
+
265
+ AccordionTitle.displayName = 'Accordion.Title';
266
+
267
+ export interface AccordionDescriptionProps extends TextProps {
268
+ asChild?: boolean;
269
+ className?: string;
270
+ }
271
+
272
+ const AccordionDescription = forwardRef<Text, AccordionDescriptionProps>(
273
+ ({ asChild, className, style, ...props }, ref) => {
274
+ const Comp = asChild ? Slot : Text;
275
+ return (
276
+ <Comp
277
+ ref={ref}
278
+ className={cn(accordionDescriptionVariants(), className)}
279
+ style={style}
280
+ {...props}
281
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'accordion-description' }))}
282
+ />
283
+ );
284
+ },
285
+ );
286
+
287
+ AccordionDescription.displayName = 'Accordion.Description';
288
+
289
+ export interface AccordionMetaProps extends TextProps {
290
+ asChild?: boolean;
291
+ className?: string;
292
+ }
293
+
294
+ const AccordionMeta = forwardRef<Text, AccordionMetaProps>(
295
+ ({ asChild, className, style, ...props }, ref) => {
296
+ const Comp = asChild ? Slot : Text;
297
+ return (
298
+ <Comp
299
+ ref={ref}
300
+ className={cn(accordionMetaVariants(), className)}
301
+ style={style}
302
+ {...props}
303
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'accordion-meta' }))}
304
+ />
305
+ );
306
+ },
307
+ );
308
+
309
+ AccordionMeta.displayName = 'Accordion.Meta';
310
+
311
+ // =============================================================================
312
+ // PANEL / BODY
313
+ // =============================================================================
314
+
315
+ export interface AccordionPanelProps extends ViewProps {
316
+ className?: string;
317
+ children?: ReactNode;
318
+ }
319
+
320
+ const AccordionPanel = forwardRef<View, AccordionPanelProps>(
321
+ ({ className, style, children, ...props }, ref) => (
322
+ <AccordionPanelRootPrimitive
323
+ ref={ref}
324
+ className={cn('w-full', className)}
325
+ style={style}
326
+ {...props}
327
+ >
328
+ {children}
329
+ </AccordionPanelRootPrimitive>
330
+ ),
331
+ );
332
+
333
+ AccordionPanel.displayName = 'Accordion.Panel';
334
+
335
+ export interface AccordionBodyProps extends ViewProps {
336
+ className?: string;
337
+ children?: ReactNode;
338
+ }
339
+
340
+ const AccordionBody = forwardRef<View, AccordionBodyProps>(
341
+ ({ className, style, children, ...props }, ref) => {
342
+ const { disableGutters } = useAccordionItemStyleContext();
343
+
344
+ return (
345
+ <View
346
+ ref={ref}
347
+ className={cn(accordionBodyVariants({ disableGutters }), className)}
348
+ style={style}
349
+ {...props}
350
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'accordion-body' }))}
351
+ >
352
+ {children}
353
+ </View>
354
+ );
355
+ },
356
+ );
357
+
358
+ AccordionBody.displayName = 'Accordion.Body';
359
+
360
+ // =============================================================================
361
+ // COMPOUND EXPORT
362
+ // =============================================================================
363
+
364
+ type AccordionCompoundComponent = typeof AccordionRoot & {
365
+ Item: typeof AccordionItem;
366
+ Trigger: typeof AccordionTrigger;
367
+ LeadingSlot: typeof AccordionLeadingSlot;
368
+ Label: typeof AccordionLabel;
369
+ Title: typeof AccordionTitle;
370
+ Description: typeof AccordionDescription;
371
+ Meta: typeof AccordionMeta;
372
+ Indicator: typeof AccordionIndicator;
373
+ Panel: typeof AccordionPanel;
374
+ Body: typeof AccordionBody;
375
+ };
376
+
377
+ export const Accordion = Object.assign(AccordionRoot, {
378
+ Item: AccordionItem,
379
+ Trigger: AccordionTrigger,
380
+ LeadingSlot: AccordionLeadingSlot,
381
+ Label: AccordionLabel,
382
+ Title: AccordionTitle,
383
+ Description: AccordionDescription,
384
+ Meta: AccordionMeta,
385
+ Indicator: AccordionIndicator,
386
+ Panel: AccordionPanel,
387
+ Body: AccordionBody,
388
+ }) as AccordionCompoundComponent;
@@ -0,0 +1,82 @@
1
+ import { Platform } from 'react-native';
2
+ import { cva, type VariantProps } from '@cdx-ui/utils';
3
+ import { DISABLED_CURSOR, TRANSITION_COLORS } from '../../styles/constants';
4
+
5
+ export const accordionRootVariants = cva(['flex w-full flex-col']);
6
+
7
+ export const accordionItemVariants = cva([
8
+ 'flex w-full flex-col overflow-clip',
9
+ 'data-[disabled=true]:opacity-[var(--opacity-disabled)]',
10
+ ]);
11
+
12
+ export const accordionHeadingVariants = cva([
13
+ 'relative w-full bg-[var(--color-transparent,rgba(255,255,255,0))]',
14
+ Platform.select({
15
+ web: [
16
+ 'before:pointer-events-none before:absolute before:inset-0 before:z-0 before:bg-content-primary before:opacity-0 before:content-[""]',
17
+ 'before:transition-opacity before:duration-150 before:ease-out',
18
+ // Overlay tracks trigger interaction data-* (Figma Accordion.Heading > [background-color]).
19
+ 'has-[[data-hover=true]]:before:opacity-[var(--opacity-hover)]',
20
+ 'has-[[data-active=true][data-hover=true]]:before:opacity-[var(--opacity-hover-overlay)]',
21
+ 'has-[[data-focus-visible=true]]:before:opacity-[var(--opacity-focus)]',
22
+ ].join(' '),
23
+ default: '',
24
+ }),
25
+ ]);
26
+
27
+ export const accordionTriggerVariants = cva(
28
+ [
29
+ 'relative z-10 flex w-full flex-row items-center gap-3 min-h-[52px] bg-transparent outline-none',
30
+ 'web:outline-none web:focus:outline-none web:focus-visible:outline-none',
31
+ DISABLED_CURSOR,
32
+ Platform.select({
33
+ web: '',
34
+ default: 'data-[active=true]:bg-surface-primary-hover',
35
+ }),
36
+ ],
37
+ {
38
+ variants: {
39
+ disableGutters: {
40
+ true: 'px-0 py-4',
41
+ false: 'p-4',
42
+ },
43
+ },
44
+ defaultVariants: {
45
+ disableGutters: false,
46
+ },
47
+ },
48
+ );
49
+
50
+ export const accordionLeadingSlotVariants = cva([
51
+ 'relative z-10 flex shrink-0 items-center justify-center',
52
+ ]);
53
+
54
+ export const accordionLabelVariants = cva([
55
+ 'relative z-10 flex min-w-0 flex-1 flex-col justify-center gap-0.5',
56
+ ]);
57
+
58
+ export const accordionTitleVariants = cva(['body-md font-medium text-content-primary']);
59
+
60
+ export const accordionDescriptionVariants = cva(['body-sm text-content-secondary']);
61
+
62
+ export const accordionMetaVariants = cva(['body-xs text-content-secondary']);
63
+
64
+ export const accordionIndicatorVariants = cva([
65
+ 'relative z-10 flex shrink-0 items-center justify-center text-content-primary',
66
+ 'data-[state=open]:rotate-180',
67
+ TRANSITION_COLORS,
68
+ ]);
69
+
70
+ export const accordionBodyVariants = cva(['flex w-full flex-col gap-2 pb-4'], {
71
+ variants: {
72
+ disableGutters: {
73
+ true: 'px-0',
74
+ false: 'px-4',
75
+ },
76
+ },
77
+ defaultVariants: {
78
+ disableGutters: false,
79
+ },
80
+ });
81
+
82
+ export type AccordionItemVariantProps = VariantProps<typeof accordionTriggerVariants>;
@@ -184,6 +184,10 @@ const MenuContent = forwardRef<View, MenuContentProps>(
184
184
  };
185
185
 
186
186
  const computedClassName = cn(menuRootVariants({ scrollable: shouldScroll }), className);
187
+ // Embedded surfaces inherit the parent collection's slot identity.
188
+ const contentDataAttributes = asSurface
189
+ ? mergeDataAttributes(dataAttributes({ slot: 'menu-content' }), props)
190
+ : mergeDataAttributes(props, dataAttributes({ slot: 'menu-content' }));
187
191
 
188
192
  const content = shouldScroll ? (
189
193
  <ScrollView
@@ -207,7 +211,7 @@ const MenuContent = forwardRef<View, MenuContentProps>(
207
211
  className={computedClassName}
208
212
  style={[style, constrainedStyle]}
209
213
  {...props}
210
- {...mergeDataAttributes(props, dataAttributes({ slot: 'menu-content' }))}
214
+ {...contentDataAttributes}
211
215
  >
212
216
  {content}
213
217
  </MenuRootPrimitive>
@@ -89,18 +89,38 @@ export interface SelectTriggerProps extends ISelectTriggerProps {
89
89
  * trigger instead of relying on `data-slot`.
90
90
  */
91
91
  asChild?: boolean;
92
+ /**
93
+ * Replaces the default open/closed chevron. Pass `null` to suppress the
94
+ * indicator. When `asChild` is set, only an explicitly provided indicator is
95
+ * appended to the substituted child.
96
+ */
97
+ indicator?: ReactNode;
92
98
  className?: string;
93
99
  children?: ReactNode;
94
100
  }
95
101
 
96
102
  const SelectTrigger = forwardRef<View, SelectTriggerProps>(
97
- ({ asChild, className, children, style, ...props }, ref) => {
103
+ ({ asChild, indicator, className, children, style, ...props }, ref) => {
98
104
  const { open } = useSelectContext();
99
105
  const { variant, size } = useSelectStyleContext();
100
106
 
101
107
  const computedClassName = asChild
102
108
  ? className
103
109
  : cn(selectTriggerVariants({ variant, size }), className);
110
+ const triggerIndicator =
111
+ indicator === undefined ? (
112
+ asChild ? undefined : (
113
+ <SelectIcon as={open ? KeyboardArrowUp : KeyboardArrowDown} />
114
+ )
115
+ ) : (
116
+ indicator
117
+ );
118
+ const triggerChildren =
119
+ asChild &&
120
+ triggerIndicator !== undefined &&
121
+ isValidElement<{ children?: ReactNode }>(children)
122
+ ? cloneElement(children, undefined, children.props.children, triggerIndicator)
123
+ : children;
104
124
 
105
125
  return (
106
126
  <SelectTriggerPrimitive
@@ -112,11 +132,11 @@ const SelectTrigger = forwardRef<View, SelectTriggerProps>(
112
132
  {...mergeDataAttributes(props, dataAttributes({ slot: 'select-trigger' }))}
113
133
  >
114
134
  {asChild ? (
115
- children
135
+ triggerChildren
116
136
  ) : (
117
137
  <>
118
138
  {children}
119
- <SelectIcon as={open ? KeyboardArrowUp : KeyboardArrowDown} />
139
+ {triggerIndicator}
120
140
  </>
121
141
  )}
122
142
  </SelectTriggerPrimitive>
@@ -243,6 +263,7 @@ const SelectContent = forwardRef<View, SelectContentProps>(
243
263
  return (
244
264
  <SelectContentPrimitive
245
265
  ref={ref}
266
+ asChild
246
267
  style={style}
247
268
  {...props}
248
269
  {...mergeDataAttributes(props, dataAttributes({ slot: 'select-content' }))}
@@ -263,29 +284,39 @@ SelectContent.displayName = 'Select.Content';
263
284
 
264
285
  export interface SelectItemProps extends ISelectItemProps {
265
286
  asChild?: boolean;
287
+ /**
288
+ * Replaces the default selected-item checkmark. The replacement is rendered
289
+ * only while the item is selected. Pass `null` to suppress the indicator.
290
+ */
291
+ indicator?: ReactNode;
266
292
  className?: string;
267
293
  children?: ReactNode;
268
294
  }
269
295
 
270
296
  const SelectItem = forwardRef<View, SelectItemProps>(
271
- ({ asChild, className, children, style, value: itemValue, ...props }, ref) => {
297
+ ({ asChild, indicator, className, children, style, value: itemValue, ...props }, ref) => {
272
298
  const { size } = useSelectStyleContext();
273
299
  const { value: selectedValue } = useSelectContext();
274
300
  const menuSize = toMenuItemSize(size);
275
301
  const checked = selectedValue === itemValue;
276
302
 
277
303
  const computedClassName = cn(menuItemVariants({ size: menuSize }), className);
278
- const selectedIcon = <Menu.ItemIndicator isChecked={checked} />;
304
+ const selectedIndicator =
305
+ indicator === undefined ? (
306
+ <Menu.ItemIndicator isChecked={checked} />
307
+ ) : checked ? (
308
+ indicator
309
+ ) : null;
279
310
  const itemChildren = asChild ? (
280
- selectedIcon && isValidElement<{ children?: ReactNode }>(children) ? (
281
- cloneElement(children, undefined, children.props.children, selectedIcon)
311
+ isValidElement<{ children?: ReactNode }>(children) ? (
312
+ cloneElement(children, undefined, children.props.children, selectedIndicator)
282
313
  ) : (
283
314
  children
284
315
  )
285
316
  ) : (
286
317
  <>
287
318
  {children}
288
- {selectedIcon}
319
+ {selectedIndicator}
289
320
  </>
290
321
  );
291
322
 
@@ -31,6 +31,7 @@ export * from './Input';
31
31
  export * from './OtpInput';
32
32
  export * from './Link';
33
33
  export * from './ListItem';
34
+ export * from './Accordion';
34
35
  export * from './Menu';
35
36
  export * from './ProgressBar';
36
37
  export * from './ProgressCircle';
@@ -13,6 +13,15 @@ import { TextComponentProvider } from '../text-component/provider';
13
13
  import { ToastProvider } from '../toast/provider';
14
14
  import type { ForgeDesignProviderProps } from './types';
15
15
 
16
+ function areInsetsEqual(current: EdgeInsets, next: EdgeInsets): boolean {
17
+ return (
18
+ current.top === next.top &&
19
+ current.right === next.right &&
20
+ current.bottom === next.bottom &&
21
+ current.left === next.left
22
+ );
23
+ }
24
+
16
25
  /**
17
26
  * ForgeDesignProvider Component
18
27
  *
@@ -41,17 +50,17 @@ const ForgeDesignProvider: React.FC<ForgeDesignProviderProps> = ({ children, con
41
50
  left: 0,
42
51
  });
43
52
 
53
+ const handleSafeAreaChange = React.useCallback(({ insets }: { insets: EdgeInsets }) => {
54
+ Uniwind.updateInsets(insets);
55
+ setOverlayInsets((current) => (areInsetsEqual(current, insets) ? current : insets));
56
+ }, []);
57
+
44
58
  // Determine if toast should be enabled and get props
45
59
  const isToastEnabled = toast !== false && toast !== 'disabled';
46
60
  const toastProps = typeof toast === 'object' ? toast : {};
47
61
 
48
62
  return (
49
- <SafeAreaListener
50
- onChange={({ insets }) => {
51
- Uniwind.updateInsets(insets);
52
- setOverlayInsets(insets);
53
- }}
54
- >
63
+ <SafeAreaListener onChange={handleSafeAreaChange}>
55
64
  <PortalProvider>
56
65
  <OverlayStackProvider>
57
66
  <OverlayInsetsProvider value={overlayInsets}>