jfs-components 0.0.86 → 0.0.95

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.
@@ -0,0 +1,167 @@
1
+ import React, { useMemo } from 'react'
2
+ import {
3
+ View,
4
+ type AccessibilityProps,
5
+ type StyleProp,
6
+ type ViewStyle,
7
+ } from 'react-native'
8
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
9
+ import { useTokens } from '../../design-tokens/JFSThemeProvider'
10
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
11
+ import BaseIcon from '../../icons/Icon'
12
+ import type { UnifiedSource } from '../../utils/MediaSource'
13
+
14
+ type IconProps = AccessibilityProps & {
15
+ /**
16
+ * Built-in icon name from the registry, in the `ic_something` format
17
+ * (e.g. `'ic_card'`, `'ic_scan_qr_code'`). When omitted and no `source` or
18
+ * `children` slot is supplied, defaults to `'ic_card'` to match the Figma
19
+ * design's default glyph.
20
+ */
21
+ iconName?: string
22
+ /**
23
+ * Unified fallback source rendered when `iconName` is missing or not in the
24
+ * registry. Accepts a remote URI, an inline SVG XML string, a `require()`
25
+ * asset, an SVG React component, or an already-rendered element. The media
26
+ * is tinted with the mode-resolved icon color so it follows design tokens
27
+ * just like a built-in icon. See {@link UnifiedSource}.
28
+ */
29
+ source?: UnifiedSource
30
+ /**
31
+ * Icon slot. Render any node here (another `Icon`, a custom SVG component,
32
+ * etc.) and it takes precedence over `iconName`/`source`. `modes` cascade
33
+ * into the slotted children automatically.
34
+ */
35
+ children?: React.ReactNode
36
+ /**
37
+ * Override the mode-resolved icon color. When omitted the value comes from
38
+ * the `icon/color` design token.
39
+ */
40
+ color?: string
41
+ /**
42
+ * Override the mode-resolved icon size (in px). When omitted the value comes
43
+ * from the `icon/size` design token.
44
+ */
45
+ size?: number
46
+ modes?: Record<string, any>
47
+ style?: StyleProp<ViewStyle>
48
+ }
49
+
50
+ interface IconTokens {
51
+ containerStyle: ViewStyle
52
+ iconColor: string
53
+ iconSize: number
54
+ }
55
+
56
+ function resolveIconTokens(modes: Record<string, any>): IconTokens {
57
+ const iconColor = (getVariableByName('icon/color', modes) || '#ad8444') as string
58
+ const iconSize = (getVariableByName('icon/size', modes) || 18) as number
59
+ const paddingLeft = (getVariableByName('icon/padding/left', modes) || 0) as number
60
+ const paddingTop = (getVariableByName('icon/padding/top', modes) || 0) as number
61
+ const paddingRight = (getVariableByName('icon/padding/right', modes) || 0) as number
62
+ const paddingBottom = (getVariableByName('icon/padding/bottom', modes) || 0) as number
63
+
64
+ return {
65
+ containerStyle: {
66
+ flexDirection: 'column',
67
+ alignItems: 'center',
68
+ justifyContent: 'center',
69
+ overflow: 'hidden',
70
+ paddingLeft,
71
+ paddingTop,
72
+ paddingRight,
73
+ paddingBottom,
74
+ },
75
+ iconColor,
76
+ iconSize,
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Icon component — a design-token-driven wrapper around a single glyph.
82
+ *
83
+ * It mirrors the Figma "Icon" component: a padded, centered container whose
84
+ * color and size are resolved from the `icon/*` design tokens via `modes`.
85
+ * The glyph itself can be supplied three ways, in order of precedence:
86
+ *
87
+ * 1. `children` — a real slot for any node (custom SVG component, nested
88
+ * `Icon`, etc.). `modes` cascade into the slot automatically.
89
+ * 2. `iconName` — a registry icon in the `ic_something` format.
90
+ * 3. `source` — a {@link UnifiedSource} fallback (remote URI, inline SVG XML,
91
+ * `require()` asset, SVG component, or React element), tinted with the
92
+ * mode-resolved icon color.
93
+ *
94
+ * `color` and `size` props let consumers override the token values per
95
+ * instance without touching `modes`.
96
+ *
97
+ * @example
98
+ * ```tsx
99
+ * // Built-in registry icon (default path).
100
+ * <Icon iconName="ic_card" modes={{ 'Color Mode': 'Light' }} />
101
+ *
102
+ * // Per-instance overrides.
103
+ * <Icon iconName="ic_ccv" color="#5c00b5" size={24} />
104
+ *
105
+ * // Fallback to an external source when the name isn't in the registry.
106
+ * <Icon source="https://cdn.example.com/glyph.svg" />
107
+ *
108
+ * // Slot: render any node as the icon.
109
+ * <Icon><BrandLogo /></Icon>
110
+ * ```
111
+ */
112
+ function Icon({
113
+ iconName,
114
+ source,
115
+ children,
116
+ color,
117
+ size,
118
+ modes: propModes = EMPTY_MODES,
119
+ style: styleProp,
120
+ ...rest
121
+ }: IconProps) {
122
+ const { modes: globalModes } = useTokens()
123
+
124
+ const modes = useMemo(
125
+ () => (globalModes === EMPTY_MODES && propModes === EMPTY_MODES
126
+ ? EMPTY_MODES
127
+ : { ...globalModes, ...propModes }),
128
+ [globalModes, propModes]
129
+ )
130
+
131
+ const tokens = useMemo(() => resolveIconTokens(modes), [modes])
132
+
133
+ const composedStyle = useMemo<StyleProp<ViewStyle>>(
134
+ () => (styleProp ? [tokens.containerStyle, styleProp] : tokens.containerStyle),
135
+ [tokens.containerStyle, styleProp]
136
+ )
137
+
138
+ const hasSlot = React.Children.count(children) > 0
139
+
140
+ // Only fall back to the default glyph when nothing at all is provided so an
141
+ // explicit `source` (without an `iconName`) isn't shadowed by `ic_card`.
142
+ const resolvedName =
143
+ iconName ?? (source === undefined ? 'ic_card' : undefined)
144
+
145
+ const iconColor = color ?? tokens.iconColor
146
+ const iconSize = size ?? tokens.iconSize
147
+
148
+ return (
149
+ <View style={composedStyle} {...rest}>
150
+ {hasSlot ? (
151
+ cloneChildrenWithModes(children, modes)
152
+ ) : (
153
+ <BaseIcon
154
+ name={resolvedName}
155
+ {...(source !== undefined ? { source } : {})}
156
+ size={iconSize}
157
+ color={iconColor}
158
+ accessibilityElementsHidden={true}
159
+ importantForAccessibility="no"
160
+ />
161
+ )}
162
+ </View>
163
+ )
164
+ }
165
+
166
+ export default React.memo(Icon)
167
+ export type { IconProps }
@@ -43,6 +43,7 @@ export { default as MonthlyStatusGrid, CalendarGlyph, type MonthlyStatusGridProp
43
43
  export { default as Gauge, type GaugeProps } from './Gauge/Gauge';
44
44
  export { default as HoldingsCard, type HoldingsCardProps } from './HoldingsCard/HoldingsCard';
45
45
  export { default as HStack, type HStackProps } from './HStack/HStack';
46
+ export { default as Icon, type IconProps } from './Icon/Icon';
46
47
  export { default as IconButton } from './IconButton/IconButton';
47
48
  export { default as IconCapsule } from './IconCapsule/IconCapsule';
48
49
  export { default as Image, type ImageProps } from './Image/Image';