jfs-components 0.1.19 → 0.1.23

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 (40) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +31 -8
  3. package/lib/commonjs/components/ContentSheet/ContentSheet.js +131 -0
  4. package/lib/commonjs/components/HeroSection/HeroSection.js +165 -0
  5. package/lib/commonjs/components/Image/Image.js +33 -7
  6. package/lib/commonjs/components/Link/Link.js +115 -0
  7. package/lib/commonjs/components/ListItem/ListItem.js +16 -1
  8. package/lib/commonjs/components/index.js +21 -0
  9. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  10. package/lib/commonjs/design-tokens/figma-modes.generated.js +53 -43
  11. package/lib/commonjs/icons/registry.js +1 -1
  12. package/lib/module/components/CheckboxItem/CheckboxItem.js +31 -8
  13. package/lib/module/components/ContentSheet/ContentSheet.js +126 -0
  14. package/lib/module/components/HeroSection/HeroSection.js +159 -0
  15. package/lib/module/components/Image/Image.js +34 -7
  16. package/lib/module/components/Link/Link.js +110 -0
  17. package/lib/module/components/ListItem/ListItem.js +16 -1
  18. package/lib/module/components/index.js +3 -0
  19. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  20. package/lib/module/design-tokens/figma-modes.generated.js +53 -43
  21. package/lib/module/icons/registry.js +1 -1
  22. package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +26 -3
  23. package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +71 -0
  24. package/lib/typescript/src/components/HeroSection/HeroSection.d.ts +80 -0
  25. package/lib/typescript/src/components/Image/Image.d.ts +27 -2
  26. package/lib/typescript/src/components/Link/Link.d.ts +73 -0
  27. package/lib/typescript/src/components/index.d.ts +3 -0
  28. package/lib/typescript/src/design-tokens/figma-modes.generated.d.ts +8 -0
  29. package/lib/typescript/src/icons/registry.d.ts +1 -1
  30. package/package.json +1 -1
  31. package/src/components/CheckboxItem/CheckboxItem.tsx +59 -14
  32. package/src/components/ContentSheet/ContentSheet.tsx +217 -0
  33. package/src/components/HeroSection/HeroSection.tsx +231 -0
  34. package/src/components/Image/Image.tsx +55 -3
  35. package/src/components/Link/Link.tsx +159 -0
  36. package/src/components/ListItem/ListItem.tsx +15 -0
  37. package/src/components/index.ts +3 -0
  38. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  39. package/src/design-tokens/figma-modes.generated.ts +53 -43
  40. package/src/icons/registry.ts +1 -1
@@ -24,13 +24,18 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
24
24
  * ```tsx
25
25
  * const [checked, setChecked] = useState(false)
26
26
  *
27
+ * // Recommended: pass the label via the children slot.
27
28
  * <CheckboxItem
28
- * label="Fixed deposit • 0245"
29
29
  * checked={checked}
30
30
  * onValueChange={setChecked}
31
31
  * control="leading"
32
32
  * modes={{ 'Color Mode': 'Light' }}
33
- * />
33
+ * >
34
+ * Fixed deposit • 0245
35
+ * </CheckboxItem>
36
+ *
37
+ * // Still supported (deprecated): the `label` prop.
38
+ * <CheckboxItem label="Fixed deposit • 0245" />
34
39
  * ```
35
40
  */
36
41
  const CheckboxItem = /*#__PURE__*/forwardRef(function CheckboxItem({
@@ -38,7 +43,8 @@ const CheckboxItem = /*#__PURE__*/forwardRef(function CheckboxItem({
38
43
  defaultChecked = false,
39
44
  onValueChange,
40
45
  disabled = false,
41
- label = 'Fixed deposit • 0245',
46
+ children,
47
+ label,
42
48
  control = 'leading',
43
49
  endSlot,
44
50
  endSlotWidth = 80,
@@ -47,6 +53,16 @@ const CheckboxItem = /*#__PURE__*/forwardRef(function CheckboxItem({
47
53
  labelStyle,
48
54
  accessibilityLabel
49
55
  }, ref) {
56
+ // Label slot resolution — the `children` slot is the primary API; the legacy
57
+ // `label` prop is a backward-compatible fallback. Precedence:
58
+ // 1. `children`, when provided (non-null / non-false).
59
+ // 2. otherwise the legacy `label` prop, whenever it was passed at all —
60
+ // including an explicit `null`/`false`, which (as before) hides the
61
+ // label entirely.
62
+ // 3. otherwise the Figma placeholder, so the default story still renders
63
+ // something meaningful.
64
+ const hasChildren = children != null && children !== false;
65
+ const slotContent = hasChildren ? children : label !== undefined ? label : 'Fixed deposit • 0245';
50
66
  const isTrailing = control === 'trailing';
51
67
  const isControlled = controlledChecked !== undefined;
52
68
  const [internalChecked, setInternalChecked] = useState(defaultChecked);
@@ -85,7 +101,7 @@ const CheckboxItem = /*#__PURE__*/forwardRef(function CheckboxItem({
85
101
  lineHeight: labelLineHeight,
86
102
  fontWeight: labelFontWeight
87
103
  };
88
- const a11yLabel = accessibilityLabel ?? (typeof label === 'string' ? label : undefined);
104
+ const a11yLabel = accessibilityLabel ?? (typeof slotContent === 'string' ? slotContent : undefined);
89
105
  const checkboxNode = /*#__PURE__*/_jsx(Checkbox, {
90
106
  checked: isChecked,
91
107
  disabled: disabled,
@@ -95,17 +111,24 @@ const CheckboxItem = /*#__PURE__*/forwardRef(function CheckboxItem({
95
111
  accessibilityLabel: a11yLabel
96
112
  } : {})
97
113
  });
98
- const labelNode = label != null && label !== false ? typeof label === 'string' || typeof label === 'number' ? /*#__PURE__*/_jsx(Text, {
114
+
115
+ // A plain string/number renders with the token-driven label style. Any other
116
+ // node is treated as custom slot content: it fills the label region and
117
+ // receives the parent `modes` (so nested design-system components theme
118
+ // correctly). An explicit null/false hides the label entirely (legacy
119
+ // behaviour of the `label` prop).
120
+ const isTextSlot = typeof slotContent === 'string' || typeof slotContent === 'number';
121
+ const labelNode = slotContent == null || slotContent === false ? null : isTextSlot ? /*#__PURE__*/_jsx(Text, {
99
122
  style: [resolvedLabelStyle, labelStyle],
100
123
  selectable: false,
101
- children: label
124
+ children: slotContent
102
125
  }) : /*#__PURE__*/_jsx(View, {
103
126
  style: {
104
127
  flex: 1,
105
128
  minWidth: 0
106
129
  },
107
- children: label
108
- }) : null;
130
+ children: cloneChildrenWithModes(slotContent, modes)
131
+ });
109
132
  const endSlotNode = endSlot ? /*#__PURE__*/_jsx(View, {
110
133
  style: {
111
134
  width: endSlotWidth,
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+
3
+ import React, { useContext, useMemo } from 'react';
4
+ import { Platform, View } from 'react-native';
5
+ import Animated, { useAnimatedKeyboard, useAnimatedStyle } from 'react-native-reanimated';
6
+ import { SafeAreaInsetsContext } from 'react-native-safe-area-context';
7
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
8
+ import { EMPTY_MODES, cloneChildrenWithModes, flattenChildren } from '../../utils/react-utils';
9
+ import { jsx as _jsx } from "react/jsx-runtime";
10
+ const IS_WEB = Platform.OS === 'web';
11
+ function resolveContentSheetStyle(modes) {
12
+ const backgroundColor = getVariableByName('contentSheet/bg', modes);
13
+ const gap = getVariableByName('contentSheet/gap', modes);
14
+ const paddingHorizontal = getVariableByName('contentSheet/padding/horizontal', modes);
15
+ const paddingTop = getVariableByName('contentSheet/padding/top', modes);
16
+ const paddingBottom = getVariableByName('contentSheet/padding/bottom', modes);
17
+ const radiusTop = getVariableByName('contentSheet/padding/radius/top', modes);
18
+ const radiusBottom = getVariableByName('contentSheet/padding/radius/bottom', modes);
19
+ return {
20
+ container: {
21
+ backgroundColor,
22
+ gap,
23
+ paddingHorizontal,
24
+ paddingTop,
25
+ // paddingBottom is applied separately so the safe-area inset can be added.
26
+ borderTopLeftRadius: radiusTop,
27
+ borderTopRightRadius: radiusTop,
28
+ borderBottomLeftRadius: radiusBottom,
29
+ borderBottomRightRadius: radiusBottom,
30
+ flexDirection: 'column',
31
+ alignItems: 'stretch',
32
+ overflow: 'hidden'
33
+ },
34
+ paddingBottom
35
+ };
36
+ }
37
+ const pinnedStyle = {
38
+ position: 'absolute',
39
+ left: 0,
40
+ right: 0,
41
+ bottom: 0
42
+ };
43
+
44
+ /**
45
+ * ContentSheet — a bottom-anchored surface that is essentially one big slot
46
+ * with padding and rounded top corners, mirroring the Figma "Content Sheet".
47
+ *
48
+ * Behaviour highlights:
49
+ * - **Auto height (free & automatic).** The sheet never sets an explicit
50
+ * height. React Native's layout engine (Yoga) sizes it to its children on
51
+ * the native thread, so when the slot content grows or shrinks the sheet
52
+ * follows with zero JS measurement and zero extra re-renders — the most
53
+ * performant option possible.
54
+ * - **Keyboard avoidance** on iOS and Android via `avoidKeyboard` (UI-thread
55
+ * `useAnimatedKeyboard`, no re-renders).
56
+ * - **Bottom pinned** by default; opt out with `pinToBottom={false}`.
57
+ * - **Token-driven** styling via `getVariableByName` + `modes`, with `modes`
58
+ * cascaded to all slot children.
59
+ */
60
+ function ContentSheet({
61
+ children,
62
+ modes = EMPTY_MODES,
63
+ avoidKeyboard = true,
64
+ keyboardSpacing = 0,
65
+ safeAreaBottom = true,
66
+ pinToBottom = true,
67
+ style,
68
+ ...rest
69
+ }) {
70
+ // Read the safe-area inset directly from context (rather than
71
+ // `useSafeAreaInsets`, which throws when no provider is mounted). This keeps
72
+ // the sheet usable without a `SafeAreaProvider` — it simply falls back to 0.
73
+ const safeAreaInsets = useContext(SafeAreaInsetsContext);
74
+ const bottomInset = safeAreaInsets?.bottom ?? 0;
75
+ const resolved = useMemo(() => resolveContentSheetStyle(modes), [modes]);
76
+ const processedChildren = useMemo(() => cloneChildrenWithModes(flattenChildren(children), modes), [children, modes]);
77
+ const paddingBottom = resolved.paddingBottom + (safeAreaBottom ? bottomInset : 0);
78
+ const containerStyle = useMemo(() => [pinToBottom ? pinnedStyle : null, resolved.container, {
79
+ paddingBottom
80
+ }, style], [pinToBottom, resolved.container, paddingBottom, style]);
81
+
82
+ // Switching between the keyboard-aware and plain implementation is keyed off
83
+ // `avoidKeyboard` (and platform). Because they are distinct component types,
84
+ // React remounts on toggle, so the conditional `useAnimatedKeyboard` hook
85
+ // inside `KeyboardAwareSheet` always runs in a stable hook order.
86
+ if (avoidKeyboard && !IS_WEB) {
87
+ return /*#__PURE__*/_jsx(KeyboardAwareSheet, {
88
+ style: containerStyle,
89
+ spacing: keyboardSpacing,
90
+ ...rest,
91
+ children: processedChildren
92
+ });
93
+ }
94
+ return /*#__PURE__*/_jsx(View, {
95
+ style: containerStyle,
96
+ ...rest,
97
+ children: processedChildren
98
+ });
99
+ }
100
+ /**
101
+ * Native-only wrapper that lifts the sheet by the live keyboard height. The
102
+ * translation is computed on the UI thread from `useAnimatedKeyboard`, so the
103
+ * sheet tracks the keyboard frame-for-frame without any JS work.
104
+ */
105
+ function KeyboardAwareSheet({
106
+ style,
107
+ spacing,
108
+ children,
109
+ ...rest
110
+ }) {
111
+ const keyboard = useAnimatedKeyboard();
112
+ const keyboardStyle = useAnimatedStyle(() => {
113
+ const height = keyboard.height.value;
114
+ return {
115
+ transform: [{
116
+ translateY: height > 0 ? -(height + spacing) : 0
117
+ }]
118
+ };
119
+ });
120
+ return /*#__PURE__*/_jsx(Animated.View, {
121
+ style: [style, keyboardStyle],
122
+ ...rest,
123
+ children: children
124
+ });
125
+ }
126
+ export default ContentSheet;
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+
3
+ import React, { useMemo } from 'react';
4
+ import { View } from 'react-native';
5
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
+ import { useTokens } from '../../design-tokens/JFSThemeProvider';
7
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
8
+ import Title from '../Title/Title';
9
+ import InputSearch from '../InputSearch/InputSearch';
10
+ import IconButton from '../IconButton/IconButton';
11
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
+ // The filter button in the design is a tonal "secondary" variant (light-purple
13
+ // fill, purple icon). These modes reproduce `iconButton/background = #dbcfff`
14
+ // and `iconButton/icon/color = #5d00b5`. Applied as defaults so the button
15
+ // matches the Figma design out of the box; consumer-provided `modes` still win.
16
+ const FILTER_BUTTON_MODES = {
17
+ AppearanceBrand: 'Secondary',
18
+ Emphasis: 'Medium'
19
+ };
20
+
21
+ // Inside a Hero Section the Title is flush with the section padding — the
22
+ // `title/padding/*` tokens default to 16 but resolve to 0 in the `Page Hero`
23
+ // context (matching the Figma design). Cascaded so the title lines up with the
24
+ // search row instead of getting a double 16px inset.
25
+ const TITLE_MODES = {
26
+ context7: 'Page Hero'
27
+ };
28
+ /**
29
+ * HeroSection is a page-level header block: a title (with optional subtitle),
30
+ * an optional search row (a flexible search input plus a trailing filter
31
+ * button), and a free-form content slot below.
32
+ *
33
+ * All spacing/background values resolve from the `HeroSection / Output` design
34
+ * token collection via `getVariableByName`. `modes` cascade to every slot and
35
+ * child component so downstream tokens stay in sync.
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * <HeroSection
40
+ * title="Transactions"
41
+ * subtitle="Last 30 days"
42
+ * searchValue={query}
43
+ * onSearchChange={setQuery}
44
+ * onFilterPress={openFilters}
45
+ * >
46
+ * <TransactionList data={items} />
47
+ * </HeroSection>
48
+ * ```
49
+ */
50
+ function HeroSection({
51
+ title = 'Page Title',
52
+ subtitle = 'Subtitle',
53
+ titleTextAlign = 'Left',
54
+ showTitle = true,
55
+ titleSlot,
56
+ showSearch = true,
57
+ searchValue,
58
+ onSearchChange,
59
+ searchPlaceholder = 'Search',
60
+ searchSlot,
61
+ showFilter = true,
62
+ filterIcon = 'ic_filter',
63
+ onFilterPress,
64
+ filterAccessibilityLabel = 'Filter',
65
+ filterSlot,
66
+ children,
67
+ modes: propModes = EMPTY_MODES,
68
+ style,
69
+ testID
70
+ }) {
71
+ const {
72
+ modes: globalModes
73
+ } = useTokens();
74
+ const modes = useMemo(() => ({
75
+ ...globalModes,
76
+ ...propModes
77
+ }), [globalModes, propModes]);
78
+ const gap = Number(getVariableByName('heroSection/gap', modes));
79
+ const paddingHorizontal = Number(getVariableByName('heroSection/padding/horizontal', modes));
80
+ const paddingVertical = Number(getVariableByName('heroSection/padding/vertical', modes));
81
+ const containerStyle = {
82
+ backgroundColor: '#ffffff',
83
+ flexDirection: 'column',
84
+ alignItems: 'flex-start',
85
+ gap,
86
+ paddingHorizontal,
87
+ paddingVertical,
88
+ width: '100%'
89
+ };
90
+ const titleContent = useMemo(() => {
91
+ if (titleSlot !== undefined && titleSlot !== null) {
92
+ return cloneChildrenWithModes(titleSlot, modes);
93
+ }
94
+ if (!showTitle) return null;
95
+ return /*#__PURE__*/_jsx(Title, {
96
+ title: title,
97
+ subtitle: subtitle,
98
+ textAlign: titleTextAlign,
99
+ modes: {
100
+ ...TITLE_MODES,
101
+ ...modes
102
+ }
103
+ });
104
+ }, [titleSlot, showTitle, title, subtitle, titleTextAlign, modes]);
105
+ const searchInput = useMemo(() => {
106
+ if (searchSlot !== undefined && searchSlot !== null) {
107
+ return cloneChildrenWithModes(searchSlot, modes);
108
+ }
109
+ return /*#__PURE__*/_jsx(InputSearch, {
110
+ supportText: false,
111
+ placeholder: searchPlaceholder,
112
+ value: searchValue,
113
+ onChangeText: onSearchChange,
114
+ containerStyle: {
115
+ flex: 1
116
+ },
117
+ modes: modes
118
+ });
119
+ }, [searchSlot, searchPlaceholder, searchValue, onSearchChange, modes]);
120
+ const filterControl = useMemo(() => {
121
+ if (filterSlot !== undefined && filterSlot !== null) {
122
+ return cloneChildrenWithModes(filterSlot, modes);
123
+ }
124
+ if (!showFilter) return null;
125
+ return /*#__PURE__*/_jsx(IconButton, {
126
+ iconName: filterIcon,
127
+ onPress: onFilterPress,
128
+ accessibilityLabel: filterAccessibilityLabel,
129
+ modes: {
130
+ ...FILTER_BUTTON_MODES,
131
+ ...modes
132
+ }
133
+ });
134
+ }, [filterSlot, showFilter, filterIcon, onFilterPress, filterAccessibilityLabel, modes]);
135
+ const searchRow = searchSlot !== undefined && searchSlot !== null ? true : showSearch;
136
+ const bodyContent = useMemo(() => {
137
+ if (children === undefined || children === null) return null;
138
+ return cloneChildrenWithModes(children, modes);
139
+ }, [children, modes]);
140
+ return /*#__PURE__*/_jsxs(View, {
141
+ style: [containerStyle, style],
142
+ testID: testID,
143
+ children: [titleContent, searchRow ? /*#__PURE__*/_jsxs(View, {
144
+ style: {
145
+ flexDirection: 'row',
146
+ alignItems: 'center',
147
+ gap,
148
+ width: '100%'
149
+ },
150
+ children: [searchInput, filterControl]
151
+ }) : null, bodyContent != null ? /*#__PURE__*/_jsx(View, {
152
+ style: {
153
+ width: '100%'
154
+ },
155
+ children: bodyContent
156
+ }) : null]
157
+ });
158
+ }
159
+ export default HeroSection;
@@ -2,14 +2,36 @@
2
2
 
3
3
  import React, { useMemo } from 'react';
4
4
  import { Image as RNImage, View } from 'react-native';
5
+
6
+ /**
7
+ * iOS URL cache control (maps to RN's `source.cache`, no-op on Android/web):
8
+ * - `'default'` — use the native platform's default caching.
9
+ * - `'reload'` — ignore any cache, always fetch from the network.
10
+ * - `'force-cache'` — use the cached response regardless of age; fetch
11
+ * only if absent.
12
+ * - `'only-if-cached'` — use the cache only; never hit the network.
13
+ */
14
+
5
15
  import Skeleton from '../../skeleton/Skeleton';
6
16
  import { useSkeleton } from '../../skeleton/SkeletonGroup';
7
17
  import { jsx as _jsx } from "react/jsx-runtime";
8
- function normalizeSource(imageSource) {
18
+ function normalizeSource(imageSource, cache) {
9
19
  if (imageSource == null) return undefined;
10
- if (typeof imageSource === 'string') return {
11
- uri: imageSource
12
- };
20
+ if (typeof imageSource === 'string') {
21
+ return cache ? {
22
+ uri: imageSource,
23
+ cache
24
+ } : {
25
+ uri: imageSource
26
+ };
27
+ }
28
+ // Only remote sources (single object with a `uri`) can carry a cache policy.
29
+ if (cache && !Array.isArray(imageSource) && typeof imageSource === 'object' && 'uri' in imageSource) {
30
+ return {
31
+ ...imageSource,
32
+ cache
33
+ };
34
+ }
13
35
  return imageSource;
14
36
  }
15
37
 
@@ -45,9 +67,12 @@ function Image({
45
67
  accessibilityLabel,
46
68
  accessibilityElementsHidden,
47
69
  importantForAccessibility,
48
- loading
70
+ loading,
71
+ cache,
72
+ onLoad,
73
+ onError
49
74
  }) {
50
- const source = useMemo(() => normalizeSource(imageSource), [imageSource]);
75
+ const source = useMemo(() => normalizeSource(imageSource, cache), [imageSource, cache]);
51
76
 
52
77
  // Explicit { width, height } means a "fill an exact box" layout — typically a
53
78
  // full-bleed hero/background where the asset is high-res and sharpness
@@ -104,7 +129,9 @@ function Image({
104
129
  resizeMethod: effectiveResizeMethod,
105
130
  accessibilityLabel: accessibilityLabel,
106
131
  accessibilityElementsHidden: accessibilityElementsHidden,
107
- importantForAccessibility: importantForAccessibility
132
+ importantForAccessibility: importantForAccessibility,
133
+ onLoad: onLoad,
134
+ onError: onError
108
135
  });
109
136
  }
110
137
  export default /*#__PURE__*/React.memo(Image);
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+
3
+ import React, { useCallback } from 'react';
4
+ import { Text as RNText } from 'react-native';
5
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
+ import { EMPTY_MODES, resolveTextLayout } from '../../utils/react-utils';
7
+ import { jsx as _jsx } from "react/jsx-runtime";
8
+ const TEXT_ALIGN_MAP = {
9
+ Left: 'left',
10
+ Center: 'center'
11
+ };
12
+ const DISABLED_OPACITY = 0.4;
13
+
14
+ /**
15
+ * Link — an underlined, pressable text primitive.
16
+ *
17
+ * It renders a single React Native `<Text>` (not a `Pressable`), so it flows
18
+ * inline and can be nested inside {@link TextSegment} exactly like a `Text`
19
+ * run — the React Native equivalent of an `<a>` inside a `<p>`. Font family,
20
+ * size, weight, line-height and letter-spacing come from the dedicated `link/*`
21
+ * tokens, while the colour resolves from `text/foreground` (so a link sits on
22
+ * the same colour as the surrounding copy). The label is always underlined.
23
+ *
24
+ * @example Standalone
25
+ * ```tsx
26
+ * <Link text="Forgot PIN?" onPress={() => navigation.navigate('ResetPin')} />
27
+ * ```
28
+ *
29
+ * @example Inline inside TextSegment
30
+ * ```tsx
31
+ * <TextSegment>
32
+ * <Text>By continuing you agree to our </Text>
33
+ * <Link onPress={openTerms}>Terms</Link>
34
+ * <Text>.</Text>
35
+ * </TextSegment>
36
+ * ```
37
+ */
38
+ function Link({
39
+ text,
40
+ children,
41
+ onPress,
42
+ disabled = false,
43
+ autolayout = 'Fill',
44
+ textAlign = 'Left',
45
+ modes = EMPTY_MODES,
46
+ style,
47
+ numberOfLines,
48
+ disableTruncation,
49
+ singleLine,
50
+ accessibilityLabel,
51
+ accessibilityHint
52
+ }) {
53
+ // Bindings mirror the Figma `Link` node exactly: colour from `text/foreground`
54
+ // (so the link matches the surrounding copy), everything else from the
55
+ // dedicated `link/*` tokens.
56
+ const foreground = getVariableByName('text/foreground', modes) ?? '#000000';
57
+ const fontFamily = getVariableByName('link/fontFamily', modes) ?? 'JioType Var';
58
+ const fontSize = getVariableByName('link/fontSize', modes) ?? 12;
59
+ const fontWeight = getVariableByName('link/fontWeight', modes) ?? 400;
60
+ const lineHeight = getVariableByName('link/lineHeight', modes) ?? 16;
61
+ const letterSpacing = getVariableByName('link/letterSpacing', modes) ?? -0.5;
62
+ const linkStyle = {
63
+ color: foreground,
64
+ fontFamily: fontFamily,
65
+ fontSize: fontSize,
66
+ fontWeight: String(fontWeight),
67
+ lineHeight: lineHeight,
68
+ letterSpacing: letterSpacing,
69
+ textAlign: TEXT_ALIGN_MAP[textAlign],
70
+ textDecorationLine: 'underline',
71
+ alignSelf: autolayout === 'Fill' ? 'stretch' : 'flex-start',
72
+ ...(disabled ? {
73
+ opacity: DISABLED_OPACITY
74
+ } : null)
75
+ };
76
+ const content = children !== undefined && children !== null && children !== false ? children : text !== undefined ? text : 'Link';
77
+ const handlePress = useCallback(event => {
78
+ if (disabled) return;
79
+ onPress?.(event);
80
+ }, [disabled, onPress]);
81
+ const {
82
+ style: layoutStyle,
83
+ ...truncation
84
+ } = resolveTextLayout({
85
+ disableTruncation,
86
+ singleLine,
87
+ numberOfLines
88
+ });
89
+ const resolvedLabel = accessibilityLabel ?? (typeof content === 'string' ? content : undefined);
90
+ return /*#__PURE__*/_jsx(RNText, {
91
+ accessibilityRole: "link",
92
+ accessibilityState: {
93
+ disabled
94
+ },
95
+ ...(resolvedLabel !== undefined ? {
96
+ accessibilityLabel: resolvedLabel
97
+ } : null),
98
+ ...(accessibilityHint !== undefined ? {
99
+ accessibilityHint
100
+ } : null),
101
+ ...(onPress !== undefined ? {
102
+ onPress: handlePress
103
+ } : null),
104
+ disabled: disabled,
105
+ style: [linkStyle, style, layoutStyle],
106
+ ...truncation,
107
+ children: content
108
+ });
109
+ }
110
+ export default /*#__PURE__*/React.memo(Link);
@@ -57,6 +57,17 @@ function resolveListItemTokens(modes) {
57
57
  const paddingLeft = getVariableByName('listItem/padding/left', resolvedModes) ?? 0;
58
58
  const paddingRight = getVariableByName('listItem/padding/right', resolvedModes) ?? 0;
59
59
  const textWrapGap = getVariableByName('listItem/text wrap', resolvedModes) ?? 0;
60
+
61
+ // Container surface — driven by the `List Item Style` collection
62
+ // (Default | Boxed | Minimal) plus `ListItem State`, `Selectable`,
63
+ // `Page type` and `Color Mode`. In the default style these all resolve to a
64
+ // transparent background / no border / 0 radius, so existing usages are
65
+ // visually unchanged; the "Boxed" style paints a filled, rounded, bordered
66
+ // surface.
67
+ const backgroundColor = getVariableByName('listItem/background/color', resolvedModes);
68
+ const borderColor = getVariableByName('listItem/border/color', resolvedModes);
69
+ const borderWidth = getVariableByName('listItem/borderWidth', resolvedModes) ?? 0;
70
+ const borderRadius = getVariableByName('listItem/radius', resolvedModes) ?? 0;
60
71
  const titleColor = getVariableByName('listItem/title/color', textModes);
61
72
  const titleFontSize = getVariableByName('listItem/title/fontSize', textModes);
62
73
  const titleLineHeight = getVariableByName('listItem/title/lineHeight', textModes);
@@ -74,7 +85,11 @@ function resolveListItemTokens(modes) {
74
85
  paddingTop: paddingTop,
75
86
  paddingBottom: paddingBottom,
76
87
  paddingLeft: paddingLeft,
77
- paddingRight: paddingRight
88
+ paddingRight: paddingRight,
89
+ backgroundColor: backgroundColor,
90
+ borderColor: borderColor,
91
+ borderWidth,
92
+ borderRadius
78
93
  },
79
94
  horizontalLayoutStyle: {
80
95
  flexDirection: 'row',
@@ -37,6 +37,7 @@ export { default as FullscreenModal } from './FullscreenModal/FullscreenModal';
37
37
  export { default as Form } from './Form/Form';
38
38
  export { useFormContext } from './Form/Form';
39
39
  export { default as FormField } from './FormField/FormField';
40
+ export { default as ContentSheet } from './ContentSheet/ContentSheet';
40
41
  export { default as CircularProgressBar } from './CircularProgressBar/CircularProgressBar';
41
42
  export { default as CircularProgressBarDoted } from './CircularProgressBarDoted/CircularProgressBarDoted';
42
43
  export { default as CircularRating } from './CircularRating/CircularRating';
@@ -46,6 +47,7 @@ export { default as ComparisonBar } from './ComparisonBar/ComparisonBar';
46
47
  export { default as AllocationComparisonChart } from './AllocationComparisonChart/AllocationComparisonChart';
47
48
  export { default as MonthlyStatusGrid, CalendarGlyph } from './MonthlyStatusGrid/MonthlyStatusGrid';
48
49
  export { default as Gauge } from './Gauge/Gauge';
50
+ export { default as HeroSection } from './HeroSection/HeroSection';
49
51
  export { default as HoldingsCard } from './HoldingsCard/HoldingsCard';
50
52
  export { default as HStack } from './HStack/HStack';
51
53
  export { default as Icon } from './Icon/Icon';
@@ -53,6 +55,7 @@ export { default as IconButton } from './IconButton/IconButton';
53
55
  export { default as IconCapsule } from './IconCapsule/IconCapsule';
54
56
  export { default as Image } from './Image/Image';
55
57
  export { default as LazyList } from './LazyList/LazyList';
58
+ export { default as Link } from './Link/Link';
56
59
  export { default as LinearMeter } from './LinearMeter/LinearMeter';
57
60
  export { default as LinearProgress } from './LinearProgress/LinearProgress';
58
61
  export { default as ListGroup } from './ListGroup/ListGroup';