jfs-components 0.0.77 → 0.0.79

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 (87) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/lib/commonjs/components/Accordion/Accordion.js +55 -55
  3. package/lib/commonjs/components/ActionFooter/ActionFooter.js +48 -2
  4. package/lib/commonjs/components/Attached/Attached.js +144 -0
  5. package/lib/commonjs/components/Card/Card.js +25 -2
  6. package/lib/commonjs/components/Checkbox/Checkbox.js +21 -9
  7. package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -16
  8. package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +167 -0
  9. package/lib/commonjs/components/FormField/FormField.js +14 -1
  10. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +353 -0
  11. package/lib/commonjs/components/ListItem/ListItem.js +46 -24
  12. package/lib/commonjs/components/MessageField/MessageField.js +318 -0
  13. package/lib/commonjs/components/NavArrow/NavArrow.js +58 -17
  14. package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +328 -0
  15. package/lib/commonjs/components/Slot/Slot.js +73 -0
  16. package/lib/commonjs/components/Stepper/Step.js +47 -60
  17. package/lib/commonjs/components/Stepper/StepLabel.js +40 -10
  18. package/lib/commonjs/components/Stepper/Stepper.js +15 -17
  19. package/lib/commonjs/components/SuggestiveSearch/SuggestiveSearch.js +487 -0
  20. package/lib/commonjs/components/TextInput/TextInput.js +16 -1
  21. package/lib/commonjs/components/Title/Title.js +10 -2
  22. package/lib/commonjs/components/index.js +49 -0
  23. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  24. package/lib/commonjs/icons/registry.js +1 -1
  25. package/lib/module/components/Accordion/Accordion.js +56 -56
  26. package/lib/module/components/ActionFooter/ActionFooter.js +50 -4
  27. package/lib/module/components/Attached/Attached.js +139 -0
  28. package/lib/module/components/Card/Card.js +25 -2
  29. package/lib/module/components/Checkbox/Checkbox.js +22 -10
  30. package/lib/module/components/DropdownInput/DropdownInput.js +30 -16
  31. package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +161 -0
  32. package/lib/module/components/FormField/FormField.js +16 -3
  33. package/lib/module/components/FullscreenModal/FullscreenModal.js +348 -0
  34. package/lib/module/components/ListItem/ListItem.js +46 -24
  35. package/lib/module/components/MessageField/MessageField.js +313 -0
  36. package/lib/module/components/NavArrow/NavArrow.js +59 -18
  37. package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +322 -0
  38. package/lib/module/components/Slot/Slot.js +68 -0
  39. package/lib/module/components/Stepper/Step.js +48 -61
  40. package/lib/module/components/Stepper/StepLabel.js +40 -10
  41. package/lib/module/components/Stepper/Stepper.js +15 -17
  42. package/lib/module/components/SuggestiveSearch/SuggestiveSearch.js +481 -0
  43. package/lib/module/components/TextInput/TextInput.js +17 -2
  44. package/lib/module/components/Title/Title.js +10 -2
  45. package/lib/module/components/index.js +7 -0
  46. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  47. package/lib/module/icons/registry.js +1 -1
  48. package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -20
  49. package/lib/typescript/src/components/Attached/Attached.d.ts +61 -0
  50. package/lib/typescript/src/components/Card/Card.d.ts +9 -2
  51. package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +63 -0
  52. package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +99 -0
  53. package/lib/typescript/src/components/ListItem/ListItem.d.ts +15 -5
  54. package/lib/typescript/src/components/MessageField/MessageField.d.ts +81 -0
  55. package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +10 -5
  56. package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +64 -0
  57. package/lib/typescript/src/components/Slot/Slot.d.ts +52 -0
  58. package/lib/typescript/src/components/Stepper/Step.d.ts +4 -1
  59. package/lib/typescript/src/components/Stepper/StepLabel.d.ts +4 -1
  60. package/lib/typescript/src/components/Stepper/Stepper.d.ts +3 -1
  61. package/lib/typescript/src/components/SuggestiveSearch/SuggestiveSearch.d.ts +123 -0
  62. package/lib/typescript/src/components/index.d.ts +10 -3
  63. package/lib/typescript/src/icons/registry.d.ts +1 -1
  64. package/package.json +1 -1
  65. package/src/components/Accordion/Accordion.tsx +113 -73
  66. package/src/components/ActionFooter/ActionFooter.tsx +56 -4
  67. package/src/components/Attached/Attached.tsx +181 -0
  68. package/src/components/Card/Card.tsx +28 -1
  69. package/src/components/Checkbox/Checkbox.tsx +22 -9
  70. package/src/components/DropdownInput/DropdownInput.tsx +67 -39
  71. package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +237 -0
  72. package/src/components/FormField/FormField.tsx +19 -3
  73. package/src/components/FullscreenModal/FullscreenModal.tsx +414 -0
  74. package/src/components/ListItem/ListItem.tsx +55 -25
  75. package/src/components/MessageField/MessageField.tsx +543 -0
  76. package/src/components/NavArrow/NavArrow.tsx +81 -17
  77. package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +426 -0
  78. package/src/components/Slot/Slot.tsx +91 -0
  79. package/src/components/Stepper/Step.tsx +52 -51
  80. package/src/components/Stepper/StepLabel.tsx +46 -9
  81. package/src/components/Stepper/Stepper.tsx +20 -15
  82. package/src/components/SuggestiveSearch/SuggestiveSearch.tsx +756 -0
  83. package/src/components/TextInput/TextInput.tsx +14 -1
  84. package/src/components/Title/Title.tsx +13 -2
  85. package/src/components/index.ts +10 -3
  86. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  87. package/src/icons/registry.ts +1 -1
@@ -0,0 +1,123 @@
1
+ import React from 'react';
2
+ import { type StyleProp, type TextInputProps as RNTextInputProps, type TextStyle, type ViewStyle } from 'react-native';
3
+ export type SuggestiveSearchOptionValue = string | number;
4
+ export type SuggestiveSearchOption = {
5
+ /** Stable, unique value used to identify the suggestion. */
6
+ value: SuggestiveSearchOptionValue;
7
+ /** Human-readable label shown in the suggestion list and the input. */
8
+ label: string;
9
+ /** Whether the suggestion is non-selectable. */
10
+ disabled?: boolean;
11
+ };
12
+ /**
13
+ * Suggestions accept either a bare string (used as both value and label) or a
14
+ * full `{ value, label }` option object for richer data.
15
+ */
16
+ export type SuggestiveSearchItem = string | SuggestiveSearchOption;
17
+ export type SuggestiveSearchProps = {
18
+ /** Label rendered above the input. */
19
+ label?: string;
20
+ /** Placeholder text shown when the query is empty. */
21
+ placeholder?: string;
22
+ /**
23
+ * Suggestions to filter against the current query. May be bare strings or
24
+ * `{ value, label }` objects.
25
+ */
26
+ items?: SuggestiveSearchItem[];
27
+ /**
28
+ * Current query text (controlled). When `undefined` the component manages
29
+ * its own query state internally.
30
+ */
31
+ inputValue?: string;
32
+ /** Initial query text for uncontrolled mode. */
33
+ defaultInputValue?: string;
34
+ /** Called whenever the query text changes (typing or selection). */
35
+ onInputChange?: (text: string) => void;
36
+ /**
37
+ * Currently selected suggestion value (controlled). When `undefined` the
38
+ * component tracks the selection internally.
39
+ */
40
+ value?: SuggestiveSearchOptionValue | null;
41
+ /** Initial selected value for uncontrolled mode. */
42
+ defaultValue?: SuggestiveSearchOptionValue | null;
43
+ /** Called when a suggestion is chosen. */
44
+ onValueChange?: (value: SuggestiveSearchOptionValue | null, option?: SuggestiveSearchOption) => void;
45
+ /**
46
+ * Custom predicate deciding whether an option matches the current query.
47
+ * Defaults to a case-insensitive substring match on the label.
48
+ */
49
+ filter?: (query: string, option: SuggestiveSearchOption) => boolean;
50
+ /**
51
+ * Minimum number of characters required before suggestions are shown.
52
+ * @default 1
53
+ */
54
+ minChars?: number;
55
+ /** Caps the number of suggestions rendered. Defaults to no limit. */
56
+ maxResults?: number;
57
+ /**
58
+ * Highlights the matched substring of each suggestion in bold.
59
+ * @default true
60
+ */
61
+ highlightMatch?: boolean;
62
+ /**
63
+ * Message shown when the query has matched no suggestions. When omitted,
64
+ * the dropdown simply stays hidden on an empty result set.
65
+ */
66
+ emptyMessage?: string;
67
+ /** Custom renderer for a suggestion row (overrides the default label). */
68
+ renderItem?: (option: SuggestiveSearchOption, meta: {
69
+ query: string;
70
+ isSelected: boolean;
71
+ }) => React.ReactNode;
72
+ /** Controlled open state of the suggestion dropdown. */
73
+ open?: boolean;
74
+ /** Initial open state for uncontrolled mode. */
75
+ defaultOpen?: boolean;
76
+ /** Called whenever the open state changes. */
77
+ onOpenChange?: (open: boolean) => void;
78
+ /**
79
+ * Maximum height of the suggestion list before it becomes scrollable.
80
+ * @default 240
81
+ */
82
+ menuMaxHeight?: number;
83
+ /**
84
+ * Vertical gap between the input and the suggestion dropdown.
85
+ * @default 6
86
+ */
87
+ menuOffset?: number;
88
+ /** Renders a required asterisk next to the label. */
89
+ isRequired?: boolean;
90
+ /** Disables interaction and dims the field. */
91
+ isDisabled?: boolean;
92
+ /** Marks the field as invalid and shows `errorMessage`. */
93
+ isInvalid?: boolean;
94
+ /** Renders the field as read-only (non-interactive, not dimmed). */
95
+ isReadOnly?: boolean;
96
+ /** Helper text displayed below the input. */
97
+ supportText?: string;
98
+ /** Replaces `supportText` when `isInvalid` is true. */
99
+ errorMessage?: string;
100
+ /** Modes for design token resolution. */
101
+ modes?: Record<string, any>;
102
+ /** Style overrides for the outermost wrapper. */
103
+ style?: StyleProp<ViewStyle>;
104
+ /** Style overrides for the input row. */
105
+ inputStyle?: StyleProp<ViewStyle>;
106
+ /** Style overrides for the input text. */
107
+ inputTextStyle?: StyleProp<TextStyle>;
108
+ /** Style overrides for the suggestion dropdown container. */
109
+ menuStyle?: StyleProp<ViewStyle>;
110
+ /** Accessibility label. Defaults to the visible label / placeholder. */
111
+ accessibilityLabel?: string;
112
+ /** Accessibility hint. */
113
+ accessibilityHint?: string;
114
+ /** Called when the input receives focus. */
115
+ onFocus?: RNTextInputProps['onFocus'];
116
+ /** Called when the input loses focus. */
117
+ onBlur?: RNTextInputProps['onBlur'];
118
+ /** Test identifier. */
119
+ testID?: string;
120
+ };
121
+ declare function SuggestiveSearch({ label, placeholder, items, inputValue, defaultInputValue, onInputChange, value, defaultValue, onValueChange, filter, minChars, maxResults, highlightMatch, emptyMessage, renderItem, open, defaultOpen, onOpenChange, menuMaxHeight, menuOffset, isRequired, isDisabled, isInvalid, isReadOnly, supportText, errorMessage, modes: propModes, style, inputStyle, inputTextStyle, menuStyle, accessibilityLabel, accessibilityHint, onFocus, onBlur, testID, }: SuggestiveSearchProps): import("react/jsx-runtime").JSX.Element;
122
+ export default SuggestiveSearch;
123
+ //# sourceMappingURL=SuggestiveSearch.d.ts.map
@@ -1,5 +1,6 @@
1
1
  export { default as AccountCard, type AccountCardProps, type AccountCardState } from './AccountCard/AccountCard';
2
2
  export { default as ActionFooter, type ActionFooterProps } from './ActionFooter/ActionFooter';
3
+ export { default as Attached, type AttachedProps, type AttachedPosition } from './Attached/Attached';
3
4
  export { default as AppBar } from './AppBar/AppBar';
4
5
  export { default as Avatar, type AvatarProps } from './Avatar/Avatar';
5
6
  export { default as AvatarGroup } from './AvatarGroup/AvatarGroup';
@@ -24,9 +25,11 @@ export { default as Divider, type DividerProps, type DividerDirection } from './
24
25
  export { default as Drawer } from './Drawer/Drawer';
25
26
  export { default as Dropdown, DropdownItem, type DropdownProps, type DropdownItemProps } from './Dropdown/Dropdown';
26
27
  export { default as DropdownInput, type DropdownInputProps, type DropdownInputOption, type DropdownInputOptionValue } from './DropdownInput/DropdownInput';
28
+ export { default as SuggestiveSearch, type SuggestiveSearchProps, type SuggestiveSearchOption, type SuggestiveSearchOptionValue, type SuggestiveSearchItem } from './SuggestiveSearch/SuggestiveSearch';
27
29
  export { default as CardCTA, type CardCTAProps, type CardCTAType } from './CardCTA/CardCTA';
28
30
  export { default as DebitCard, type DebitCardProps } from './DebitCard/DebitCard';
29
31
  export { default as FilterBar } from './FilterBar/FilterBar';
32
+ export { default as FullscreenModal, type FullscreenModalProps } from './FullscreenModal/FullscreenModal';
30
33
  export { default as Form, type FormProps } from './Form/Form';
31
34
  export { useFormContext } from './Form/Form';
32
35
  export { default as FormField, type FormFieldProps, type FormFieldType } from './FormField/FormField';
@@ -51,6 +54,7 @@ export { default as LottiePlayer, type LottiePlayerProps, type LottieAnimationSo
51
54
  export { default as ListItem } from './ListItem/ListItem';
52
55
  export { default as MediaCard, type MediaCardProps } from './MediaCard/MediaCard';
53
56
  export { default as MerchantProfile, type MerchantProfileProps } from './MerchantProfile/MerchantProfile';
57
+ export { default as MessageField, type MessageFieldProps, type MessageFieldState } from './MessageField/MessageField';
54
58
  export { default as MetricLegendItem, type MetricLegendItemProps } from './MetricLegendItem/MetricLegendItem';
55
59
  export { default as MoneyValue } from './MoneyValue/MoneyValue';
56
60
  export { default as NoteInput, type NoteInputProps } from './NoteInput/NoteInput';
@@ -60,9 +64,10 @@ export { default as Numpad, type NumpadProps, type NumpadKeyValue } from './Nump
60
64
  export { default as Title, type TitleProps } from './Title/Title';
61
65
  export { default as Screen, type ScreenProps } from './Screen/Screen';
62
66
  export { default as Section } from './Section/Section';
63
- export { default as Stepper } from './Stepper/Stepper';
64
- export { Step } from './Stepper/Step';
65
- export { StepLabel } from './Stepper/StepLabel';
67
+ export { default as Slot, type SlotProps, type SlotLayoutDirection } from './Slot/Slot';
68
+ export { default as Stepper, type StepperProps } from './Stepper/Stepper';
69
+ export { Step, type StepProps, type StepStatus } from './Stepper/Step';
70
+ export { StepLabel, type StepLabelProps } from './Stepper/StepLabel';
66
71
  export { default as TextInput } from './TextInput/TextInput';
67
72
  export { default as StatusHero, type StatusHeroProps } from './StatusHero/StatusHero';
68
73
  export { default as ThreadHero, type ThreadHeroProps } from './ThreadHero/ThreadHero';
@@ -74,6 +79,7 @@ export { default as UpiHandle } from './UpiHandle/UpiHandle';
74
79
  export { default as VStack, type VStackProps } from './VStack/VStack';
75
80
  export { default as ChipGroup, type ChipGroupProps } from './ChipGroup/ChipGroup';
76
81
  export { default as EmptyState, type EmptyStateProps } from './EmptyState/EmptyState';
82
+ export { default as ExpandableCheckbox, type ExpandableCheckboxProps } from './ExpandableCheckbox/ExpandableCheckbox';
77
83
  export { default as Accordion, type AccordionProps } from './Accordion/Accordion';
78
84
  export { default as AccordionCheckbox, type AccordionCheckboxProps } from './AccordionCheckbox/AccordionCheckbox';
79
85
  export { default as ActionTile, type ActionTileProps } from './ActionTile/ActionTile';
@@ -106,6 +112,7 @@ export { default as AmountInput, type AmountInputProps } from './AmountInput/Amo
106
112
  export { default as PageHero, type PageHeroProps } from './PageHero/PageHero';
107
113
  export { default as Popup, type PopupProps, type PopupRef } from './Popup/Popup';
108
114
  export { default as PortfolioHero, type PortfolioHeroProps } from './PortfolioHero/PortfolioHero';
115
+ export { default as PlanComparisonCard, type PlanComparisonCardProps, type PlanComparisonColumn, type PlanComparisonRow, type PlanComparisonCellValue } from './PlanComparisonCard/PlanComparisonCard';
109
116
  export { default as PoweredByLabel, type PoweredByLabelProps } from './PoweredByLabel/PoweredByLabel';
110
117
  export { default as ProductLabel, type ProductLabelProps } from './ProductLabel/ProductLabel';
111
118
  export { default as ProductOverview, type ProductOverviewProps, type ProductOverviewStat } from './ProductOverview/ProductOverview';
@@ -4,7 +4,7 @@
4
4
  * Auto-generated from SVG files in src/icons/
5
5
  * DO NOT EDIT MANUALLY - Run "npm run icons:generate" to regenerate
6
6
  *
7
- * Generated: 2026-05-25T10:18:38.037Z
7
+ * Generated: 2026-05-29T17:01:15.629Z
8
8
  */
9
9
  export declare const iconRegistry: Record<string, {
10
10
  path: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jfs-components",
3
- "version": "0.0.77",
3
+ "version": "0.0.79",
4
4
  "description": "React Native Jio Finance Components Library",
5
5
  "author": "sunshuaiqi@gmail.com",
6
6
  "license": "MIT",
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react'
1
+ import React, { useMemo, useState } from 'react'
2
2
  import {
3
3
  View,
4
4
  Text,
@@ -21,9 +21,49 @@ if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental
21
21
  UIManager.setLayoutAnimationEnabledExperimental(true)
22
22
  }
23
23
 
24
+ type AccordionStateMode = 'Idle' | 'Hover' | 'Open' | 'Open Hover' | 'Disabled'
25
+
26
+ function resolveAccordionStateMode(
27
+ disabled: boolean,
28
+ isExpanded: boolean,
29
+ isHovered: boolean,
30
+ contained: boolean,
31
+ ): AccordionStateMode {
32
+ if (disabled) return 'Disabled'
33
+
34
+ if (contained) {
35
+ return isExpanded ? 'Open Hover' : 'Hover'
36
+ }
37
+
38
+ if (isExpanded) {
39
+ return isHovered ? 'Open Hover' : 'Open'
40
+ }
41
+
42
+ return isHovered ? 'Hover' : 'Idle'
43
+ }
44
+
45
+ function toFontWeight(value: unknown, fallback: TextStyle['fontWeight']): TextStyle['fontWeight'] {
46
+ if (typeof value === 'number') return String(value) as TextStyle['fontWeight']
47
+ if (typeof value === 'string') {
48
+ const normalized = value.trim().toLowerCase()
49
+ if (normalized === 'bold') return '700'
50
+ if (normalized === 'medium') return '500'
51
+ if (normalized === 'regular' || normalized === 'normal') return '400'
52
+ if (/^\d+$/.test(normalized)) return normalized as TextStyle['fontWeight']
53
+ return value as TextStyle['fontWeight']
54
+ }
55
+ return fallback
56
+ }
57
+
24
58
  export type AccordionProps = {
25
59
  /** The accordion header title */
26
60
  title?: string;
61
+ /**
62
+ * When `true`, the header always uses the filled background treatment
63
+ * (Figma Hover / Open Hover visuals). Defaults to `false` (transparent at
64
+ * rest, filled only while hovered or pressed).
65
+ */
66
+ contained?: boolean;
27
67
  /** Initial expanded state. Defaults to false (collapsed) */
28
68
  defaultExpanded?: boolean;
29
69
  /** Controlled expanded state. When provided, the component becomes controlled */
@@ -51,31 +91,20 @@ export type AccordionProps = {
51
91
  /**
52
92
  * Accordion component that mirrors the Figma "Accordion" component.
53
93
  *
54
- * This component supports:
55
- * - **Expandable/collapsible content** with smooth animation
56
- * - **States**: Idle, Hover, Open, Disabled
57
- * - **Slot** for custom content
58
- * - **Design-token driven styling** via `getVariableByName` and `modes`
94
+ * Supports two visual treatments via the `contained` prop:
95
+ * - **`contained={false}`** (default) transparent header at rest; filled
96
+ * background on hover / press.
97
+ * - **`contained={true}`** header always uses the filled background.
59
98
  *
60
- * Wherever the Figma layer name contains "Slot", this component exposes a
61
- * dedicated React "slot" prop:
62
- * - Slot "content" `children`
99
+ * Interaction states (Idle, Hover, Open, Disabled) are resolved automatically
100
+ * from `expanded`, `disabled`, hover, and `contained` — consumers should not
101
+ * pass `'Accordion States'` in `modes`.
63
102
  *
64
103
  * @component
65
- * @param {Object} props
66
- * @param {string} [props.title='Accordion title'] - The accordion header title
67
- * @param {boolean} [props.defaultExpanded=false] - Initial expanded state
68
- * @param {boolean} [props.expanded] - Controlled expanded state
69
- * @param {Function} [props.onExpandedChange] - Callback fired when expanded state changes
70
- * @param {boolean} [props.disabled=false] - Whether the accordion is disabled
71
- * @param {React.ReactNode} [props.children] - Content to display when expanded
72
- * @param {Object} [props.modes={}] - Modes object passed to `getVariableByName` for all design tokens
73
- * @param {Object} [props.style] - Optional container style overrides
74
- * @param {string} [props.accessibilityLabel] - Accessibility label for the accordion. If not provided, uses title
75
- * @param {string} [props.accessibilityHint] - Additional accessibility hint for screen readers
76
104
  */
77
105
  function Accordion({
78
106
  title = 'Accordion title',
107
+ contained = false,
79
108
  defaultExpanded = false,
80
109
  expanded: controlledExpanded,
81
110
  onExpandedChange,
@@ -89,23 +118,31 @@ function Accordion({
89
118
  webAccessibilityProps,
90
119
  ...rest
91
120
  }: AccordionProps) {
92
- // Internal state for uncontrolled mode
93
121
  const [internalExpanded, setInternalExpanded] = useState(defaultExpanded)
94
-
95
- // Determine if controlled or uncontrolled
122
+ const [isHovered, setIsHovered] = useState(false)
123
+
96
124
  const isControlled = controlledExpanded !== undefined
97
125
  const isExpanded = isControlled ? controlledExpanded : internalExpanded
98
-
99
- // Hover state for web
100
- const [isHovered, setIsHovered] = useState(false)
101
-
102
- // Handle toggle
126
+
127
+ const resolvedModes = useMemo(() => {
128
+ const accordionState = resolveAccordionStateMode(
129
+ disabled,
130
+ isExpanded,
131
+ isHovered,
132
+ contained,
133
+ )
134
+
135
+ return {
136
+ ...modes,
137
+ 'Accordion States': accordionState,
138
+ }
139
+ }, [contained, disabled, isExpanded, isHovered, modes])
140
+
103
141
  const handleToggle = () => {
104
142
  if (disabled) return
105
-
106
- // Animate the layout change
143
+
107
144
  LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
108
-
145
+
109
146
  if (isControlled) {
110
147
  onExpandedChange?.(!isExpanded)
111
148
  } else {
@@ -113,38 +150,45 @@ function Accordion({
113
150
  onExpandedChange?.(!isExpanded)
114
151
  }
115
152
  }
116
-
117
- // Resolve design tokens
118
- const titleColor = disabled
119
- ? '#999999'
120
- : getVariableByName('accordion/title/color', modes) || '#0d0d0d'
121
- const titleFontSize = getVariableByName('accordion/title/fontSize', modes) || 18
122
- const titleLineHeight = getVariableByName('accordion/title/lineHeight', modes) || 20
123
- const titleFontFamily = getVariableByName('accordion/title/fontFamily', modes) || 'System'
124
-
125
- const iconColor = getVariableByName('accordion/icon/color', modes) || '#141414'
126
- const iconSize = getVariableByName('accordion/icon/size', modes) || 24
127
-
128
- const headerGap = getVariableByName('accordion/header/gap', modes) || 12
129
- const headerPaddingVertical = getVariableByName('accordion/header/padding/vertical', modes) || 24
130
- const headerBackground = isHovered && !disabled
131
- ? '#f2f2f2'
132
- : getVariableByName('accordion/header/background', modes) || 'transparent'
133
-
134
- const contentGap = getVariableByName('accordion/content/gap', modes) || 12
135
- const contentPaddingTop = getVariableByName('accordion/content/padding/top', modes) || 8
136
- const contentPaddingBottom = isExpanded
137
- ? (getVariableByName('accordion/content/padding/bottom', modes) ?? 24)
138
- : 8
139
-
140
- const borderColor = getVariableByName('accordion/border/color', modes) || '#e6e6e6'
141
-
142
- // Styles
153
+
154
+ const titleColor =
155
+ (getVariableByName('accordion/title/color', resolvedModes) as string | null) ?? '#0d0d0d'
156
+ const titleFontSize =
157
+ (getVariableByName('accordion/title/fontSize', resolvedModes) as number | null) ?? 14
158
+ const titleLineHeight =
159
+ (getVariableByName('accordion/title/lineHeight', resolvedModes) as number | null) ?? 20
160
+ const titleFontFamily =
161
+ (getVariableByName('accordion/title/fontFamily', resolvedModes) as string | null) ?? 'System'
162
+ const titleFontWeight = toFontWeight(
163
+ getVariableByName('accordion/title/fontWeight', resolvedModes),
164
+ '700',
165
+ )
166
+
167
+ const iconColor =
168
+ (getVariableByName('accordion/icon/color', resolvedModes) as string | null) ?? '#141414'
169
+ const iconSize = (getVariableByName('accordion/icon/size', resolvedModes) as number | null) ?? 24
170
+
171
+ const headerGap = (getVariableByName('accordion/header/gap', resolvedModes) as number | null) ?? 12
172
+ const headerPaddingVertical =
173
+ (getVariableByName('accordion/header/padding/vertical', resolvedModes) as number | null) ?? 8
174
+ const headerBackground =
175
+ (getVariableByName('accordion/header/background', resolvedModes) as string | null) ??
176
+ 'transparent'
177
+
178
+ const contentGap = (getVariableByName('accordion/content/gap', resolvedModes) as number | null) ?? 12
179
+ const contentPaddingTop =
180
+ (getVariableByName('accordion/content/padding/top', resolvedModes) as number | null) ?? 8
181
+ const contentPaddingBottom =
182
+ (getVariableByName('accordion/content/padding/bottom', resolvedModes) as number | null) ?? 8
183
+
184
+ const borderColor =
185
+ (getVariableByName('accordion/border/color', resolvedModes) as string | null) ?? '#e6e6e6'
186
+
143
187
  const containerStyle: ViewStyle = {
144
188
  borderBottomWidth: 1,
145
189
  borderBottomColor: borderColor,
146
190
  }
147
-
191
+
148
192
  const headerStyle: ViewStyle = {
149
193
  flexDirection: 'row',
150
194
  alignItems: 'center',
@@ -154,16 +198,16 @@ function Accordion({
154
198
  backgroundColor: headerBackground,
155
199
  overflow: 'hidden',
156
200
  }
157
-
201
+
158
202
  const titleStyle: TextStyle = {
159
203
  flex: 1,
160
204
  color: titleColor,
161
205
  fontSize: titleFontSize,
162
206
  lineHeight: titleLineHeight,
163
207
  fontFamily: titleFontFamily,
164
- fontWeight: '700',
208
+ fontWeight: titleFontWeight,
165
209
  }
166
-
210
+
167
211
  const contentStyle: ViewStyle = {
168
212
  backgroundColor: 'transparent',
169
213
  gap: contentGap,
@@ -172,11 +216,9 @@ function Accordion({
172
216
  paddingHorizontal: 0,
173
217
  overflow: 'hidden',
174
218
  }
175
-
176
- // Generate default accessibility label
219
+
177
220
  const defaultAccessibilityLabel = accessibilityLabel || title
178
-
179
- // Web platform support
221
+
180
222
  const webProps = usePressableWebSupport({
181
223
  restProps: {},
182
224
  onPress: handleToggle,
@@ -184,12 +226,11 @@ function Accordion({
184
226
  accessibilityLabel: defaultAccessibilityLabel,
185
227
  webAccessibilityProps,
186
228
  })
187
-
188
- // Process children to pass modes
229
+
189
230
  const processedChildren = children
190
- ? cloneChildrenWithModes(React.Children.toArray(children), modes)
231
+ ? cloneChildrenWithModes(React.Children.toArray(children), resolvedModes)
191
232
  : null
192
-
233
+
193
234
  return (
194
235
  <View style={[containerStyle, style]} {...rest}>
195
236
  <Pressable
@@ -217,12 +258,12 @@ function Accordion({
217
258
  <Icon
218
259
  name={isExpanded ? 'ic_minus' : 'ic_add'}
219
260
  size={iconSize}
220
- color={disabled ? '#999999' : iconColor}
261
+ color={iconColor}
221
262
  accessibilityElementsHidden={true}
222
263
  importantForAccessibility="no"
223
264
  />
224
265
  </Pressable>
225
-
266
+
226
267
  {isExpanded && processedChildren && (
227
268
  <View style={contentStyle}>
228
269
  {processedChildren}
@@ -233,4 +274,3 @@ function Accordion({
233
274
  }
234
275
 
235
276
  export default Accordion
236
-
@@ -1,7 +1,10 @@
1
- import React, { useMemo } from 'react'
1
+ import React, { useEffect, useMemo, useRef } from 'react'
2
2
  import {
3
+ Animated,
4
+ Keyboard,
3
5
  View,
4
6
  Platform,
7
+ type KeyboardEvent,
5
8
  type ViewStyle,
6
9
  type StyleProp,
7
10
  } from 'react-native'
@@ -133,6 +136,47 @@ function ActionFooter({
133
136
  style,
134
137
  accessibilityLabel,
135
138
  }: ActionFooterProps) {
139
+ // -------------------------------------------------------------------------
140
+ // Keep the footer locked in place behind the software keyboard (Android).
141
+ // -------------------------------------------------------------------------
142
+ //
143
+ // The Android activity is configured with `windowSoftInputMode="adjustResize"`,
144
+ // which shrinks the app window by the keyboard height when the keyboard
145
+ // opens. A bottom-anchored footer therefore gets lifted UP by the keyboard
146
+ // height — exactly the jump the design does not want.
147
+ //
148
+ // To counteract that, we translate the footer back DOWN by the same keyboard
149
+ // height so it visually stays exactly where it was (now sitting behind the
150
+ // keyboard). iOS does not resize the window for the keyboard, so the footer
151
+ // already stays put there; we only run this on Android to avoid pushing the
152
+ // footer off-screen on platforms that don't lift it in the first place.
153
+ const keyboardOffset = useRef(new Animated.Value(0)).current
154
+ useEffect(() => {
155
+ if (Platform.OS !== 'android') return undefined
156
+
157
+ const animateTo = (toValue: number, duration?: number) => {
158
+ Animated.timing(keyboardOffset, {
159
+ toValue,
160
+ // Match the OS keyboard animation so the resize and our counter-shift
161
+ // cancel out smoothly with no visible footer movement.
162
+ duration: typeof duration === 'number' && duration > 0 ? duration : 150,
163
+ useNativeDriver: true,
164
+ }).start()
165
+ }
166
+
167
+ const showSub = Keyboard.addListener('keyboardDidShow', (e: KeyboardEvent) => {
168
+ animateTo(e?.endCoordinates?.height ?? 0, e?.duration)
169
+ })
170
+ const hideSub = Keyboard.addListener('keyboardDidHide', (e: KeyboardEvent) => {
171
+ animateTo(0, e?.duration)
172
+ })
173
+
174
+ return () => {
175
+ showSub.remove()
176
+ hideSub.remove()
177
+ }
178
+ }, [keyboardOffset])
179
+
136
180
  // All token reads collapsed into a single useMemo keyed on `modes`. With
137
181
  // the shared `EMPTY_MODES` default this resolves once for the common path
138
182
  // and never re-allocates the container/slot style objects between renders.
@@ -192,13 +236,21 @@ function ActionFooter({
192
236
  }, [children, modes])
193
237
 
194
238
  return (
195
- <View
196
- style={[containerStyle, WEB_SHADOW, style]}
239
+ <Animated.View
240
+ style={[
241
+ containerStyle,
242
+ WEB_SHADOW,
243
+ style,
244
+ // Counter-translate by the keyboard height on Android so `adjustResize`
245
+ // can't lift the footer above the keyboard (no-op on iOS/web where the
246
+ // value stays at 0).
247
+ { transform: [{ translateY: keyboardOffset }] },
248
+ ]}
197
249
  accessibilityRole="toolbar"
198
250
  accessibilityLabel={accessibilityLabel}
199
251
  >
200
252
  <View style={slotStyle}>{enhancedChildren}</View>
201
- </View>
253
+ </Animated.View>
202
254
  )
203
255
  }
204
256