jfs-components 0.0.63 → 0.0.65
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.
- package/CHANGELOG.md +31 -0
- package/lib/commonjs/components/Carousel/Carousel.js +12 -9
- package/lib/commonjs/components/Drawer/Drawer.js +116 -50
- package/lib/commonjs/components/IconButton/IconButton.js +42 -6
- package/lib/commonjs/components/IconCapsule/IconCapsule.js +5 -0
- package/lib/commonjs/components/Popup/Popup.js +2 -2
- package/lib/commonjs/components/Section/Section.js +280 -58
- package/lib/commonjs/components/UpiHandle/UpiHandle.js +19 -7
- package/lib/commonjs/icons/Icon.js +72 -75
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/commonjs/utils/MediaSource.js +181 -0
- package/lib/commonjs/utils/index.js +9 -1
- package/lib/module/components/Carousel/Carousel.js +12 -9
- package/lib/module/components/Drawer/Drawer.js +116 -50
- package/lib/module/components/IconButton/IconButton.js +42 -6
- package/lib/module/components/IconCapsule/IconCapsule.js +5 -0
- package/lib/module/components/Popup/Popup.js +2 -2
- package/lib/module/components/Section/Section.js +280 -58
- package/lib/module/components/UpiHandle/UpiHandle.js +20 -8
- package/lib/module/icons/Icon.js +72 -75
- package/lib/module/icons/registry.js +1 -1
- package/lib/module/utils/MediaSource.js +176 -0
- package/lib/module/utils/index.js +2 -1
- package/lib/typescript/src/components/Drawer/Drawer.d.ts +6 -1
- package/lib/typescript/src/components/IconButton/IconButton.d.ts +25 -14
- package/lib/typescript/src/components/IconCapsule/IconCapsule.d.ts +12 -1
- package/lib/typescript/src/components/Section/Section.d.ts +42 -1
- package/lib/typescript/src/components/UpiHandle/UpiHandle.d.ts +17 -3
- package/lib/typescript/src/icons/Icon.d.ts +35 -16
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/lib/typescript/src/utils/MediaSource.d.ts +63 -0
- package/lib/typescript/src/utils/index.d.ts +2 -0
- package/package.json +1 -1
- package/src/components/Carousel/Carousel.tsx +16 -17
- package/src/components/Drawer/Drawer.tsx +136 -60
- package/src/components/IconButton/IconButton.tsx +70 -11
- package/src/components/IconCapsule/IconCapsule.tsx +13 -0
- package/src/components/Popup/Popup.tsx +2 -2
- package/src/components/Section/Section.tsx +411 -71
- package/src/components/UpiHandle/UpiHandle.tsx +37 -11
- package/src/icons/Icon.tsx +91 -76
- package/src/icons/registry.ts +1 -1
- package/src/utils/MediaSource.tsx +220 -0
- package/src/utils/index.ts +2 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useState, useRef } from 'react'
|
|
2
|
-
import { Platform, StyleSheet, Text, useWindowDimensions, View } from 'react-native'
|
|
2
|
+
import { Platform, StyleSheet, Text, useWindowDimensions, View, ViewStyle } from 'react-native'
|
|
3
3
|
import {
|
|
4
4
|
Gesture,
|
|
5
5
|
GestureDetector,
|
|
@@ -79,6 +79,11 @@ type DrawerProps = {
|
|
|
79
79
|
* as a tab bar. Defaults to 80.
|
|
80
80
|
*/
|
|
81
81
|
bottomInset?: number
|
|
82
|
+
/**
|
|
83
|
+
* Called whenever the drawer settles into a new state (collapsed or
|
|
84
|
+
* expanded), so parent components can react programmatically.
|
|
85
|
+
*/
|
|
86
|
+
onStateChange?: (state: 'collapsed' | 'expanded') => void
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
/**
|
|
@@ -102,6 +107,7 @@ function Drawer({
|
|
|
102
107
|
contentContainerStyle,
|
|
103
108
|
showsVerticalScrollIndicator = false,
|
|
104
109
|
bottomInset = 80,
|
|
110
|
+
onStateChange,
|
|
105
111
|
}: DrawerProps) {
|
|
106
112
|
const { height: screenHeight } = useWindowDimensions()
|
|
107
113
|
|
|
@@ -164,13 +170,27 @@ function Drawer({
|
|
|
164
170
|
|
|
165
171
|
// Update JS state for accessibility/logic if needed
|
|
166
172
|
const updateMode = useCallback((newMode: 'collapsed' | 'expanded') => {
|
|
167
|
-
setMode(
|
|
168
|
-
|
|
169
|
-
|
|
173
|
+
setMode((prev) => {
|
|
174
|
+
if (prev !== newMode) {
|
|
175
|
+
onStateChange?.(newMode)
|
|
176
|
+
}
|
|
177
|
+
return newMode
|
|
178
|
+
})
|
|
179
|
+
}, [onStateChange])
|
|
180
|
+
|
|
181
|
+
// Gesture policy:
|
|
182
|
+
// • activeOffsetY: require a clear *vertical* drag (10px) before this
|
|
183
|
+
// pan claims the gesture. Matches typical iOS scroll activation feel.
|
|
184
|
+
// • failOffsetX: if the finger crosses ~16px horizontally *before* we
|
|
185
|
+
// activate, surrender the gesture entirely so any horizontal child
|
|
186
|
+
// (FlatList horizontal, swiper, slider, etc.) can scroll cleanly
|
|
187
|
+
// without the drawer also translating on Y.
|
|
188
|
+
// • simultaneousWithExternalGesture(scrollRef): cooperate with the
|
|
189
|
+
// drawer's own internal vertical ScrollView for nested scrolling.
|
|
170
190
|
const gesture = Gesture.Pan()
|
|
171
191
|
.simultaneousWithExternalGesture(scrollRef)
|
|
172
|
-
.activeOffsetY([-
|
|
173
|
-
.
|
|
192
|
+
.activeOffsetY([-10, 10])
|
|
193
|
+
.failOffsetX([-16, 16])
|
|
174
194
|
.onStart(() => {
|
|
175
195
|
context.value = { y: translateY.value }
|
|
176
196
|
isDrawerActive.value = true
|
|
@@ -179,6 +199,16 @@ function Drawer({
|
|
|
179
199
|
scrollTopTranslationOffset.value = 0
|
|
180
200
|
})
|
|
181
201
|
.onUpdate((event) => {
|
|
202
|
+
// Defense-in-depth: even after vertical activation, if the *current*
|
|
203
|
+
// motion is dominantly horizontal (e.g., the user activated with a
|
|
204
|
+
// small Y nudge and then curved into a horizontal swipe on a child
|
|
205
|
+
// carousel), don't translate the drawer this frame. failOffsetX
|
|
206
|
+
// already prevents activation in pure-horizontal swipes; this guards
|
|
207
|
+
// the diagonal-then-horizontal case.
|
|
208
|
+
if (Math.abs(event.translationX) > Math.abs(event.translationY) * 1.5) {
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
182
212
|
// Logic for nested scrolling:
|
|
183
213
|
// If we are at the expanded position (minTranslateY) AND content is
|
|
184
214
|
// scrolled down (scrollY > 0), let the ScrollView handle the gesture.
|
|
@@ -299,6 +329,36 @@ function Drawer({
|
|
|
299
329
|
const titleLineHeight = getVariableByName('drawer/title/lineHeight', modes) || 17
|
|
300
330
|
const titlePaddingBottom = getVariableByName('drawer/titleWrap/padding/bottom', modes) || 8
|
|
301
331
|
|
|
332
|
+
// Drop shadow — Figma layers two shadows (primary + secondary) sharing
|
|
333
|
+
// the same offsetY/blur but with their own offsetX and color.
|
|
334
|
+
const shadowPrimaryOffsetX = (getVariableByName('drawer/shadow/primary/offsetX', modes) ?? 0) as number
|
|
335
|
+
const shadowPrimaryOffsetY = (getVariableByName('drawer/shadow/primary/offsetY', modes) ?? 16) as number
|
|
336
|
+
const shadowPrimaryBlur = (getVariableByName('drawer/shadow/primary/blur', modes) ?? 48) as number
|
|
337
|
+
const shadowPrimaryColor = (getVariableByName('drawer/shadow/primary/color', modes) ?? 'rgba(12, 13, 16, 0.16)') as string
|
|
338
|
+
const shadowSecondaryOffsetX = (getVariableByName('drawer/shadow/secondary/offsetX', modes) ?? 0) as number
|
|
339
|
+
const shadowSecondaryColor = (getVariableByName('drawer/shadow/secondary/color', modes) ?? 'rgba(12, 13, 16, 0.12)') as string
|
|
340
|
+
|
|
341
|
+
// Cross-platform shadow style. Web supports stacking two shadows via
|
|
342
|
+
// boxShadow. iOS only supports a single native shadow per view, so we
|
|
343
|
+
// apply the more prominent (primary) one. Android uses elevation.
|
|
344
|
+
const shadowStyle: ViewStyle = Platform.select({
|
|
345
|
+
web: {
|
|
346
|
+
boxShadow:
|
|
347
|
+
`${shadowSecondaryOffsetX}px ${shadowPrimaryOffsetY}px ${shadowPrimaryBlur}px 0px ${shadowSecondaryColor}, ` +
|
|
348
|
+
`${shadowPrimaryOffsetX}px ${shadowPrimaryOffsetY}px ${shadowPrimaryBlur}px 0px ${shadowPrimaryColor}`,
|
|
349
|
+
} as ViewStyle,
|
|
350
|
+
ios: {
|
|
351
|
+
shadowColor: shadowPrimaryColor,
|
|
352
|
+
shadowOffset: { width: shadowPrimaryOffsetX, height: shadowPrimaryOffsetY },
|
|
353
|
+
shadowOpacity: 1,
|
|
354
|
+
shadowRadius: shadowPrimaryBlur / 2,
|
|
355
|
+
},
|
|
356
|
+
android: {
|
|
357
|
+
elevation: 16,
|
|
358
|
+
},
|
|
359
|
+
default: {},
|
|
360
|
+
}) as ViewStyle
|
|
361
|
+
|
|
302
362
|
const defaultAccessibilityLabel = accessibilityLabel || title || 'Drawer'
|
|
303
363
|
|
|
304
364
|
return (
|
|
@@ -314,11 +374,8 @@ function Drawer({
|
|
|
314
374
|
backgroundColor,
|
|
315
375
|
borderTopLeftRadius: radius,
|
|
316
376
|
borderTopRightRadius: radius,
|
|
317
|
-
paddingLeft,
|
|
318
|
-
paddingRight,
|
|
319
|
-
paddingBottom,
|
|
320
|
-
rowGap: drawerGap,
|
|
321
377
|
},
|
|
378
|
+
shadowStyle,
|
|
322
379
|
sheetStyle,
|
|
323
380
|
animatedStyle,
|
|
324
381
|
]}
|
|
@@ -327,57 +384,73 @@ function Drawer({
|
|
|
327
384
|
accessibilityLabel={undefined}
|
|
328
385
|
accessibilityHint={accessibilityHint || 'Swipe up to expand, swipe down to collapse'}
|
|
329
386
|
>
|
|
330
|
-
{/*
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
{/* Custom Header Slot */}
|
|
345
|
-
{header}
|
|
346
|
-
|
|
347
|
-
{/* Title (Legacy/Simple Mode) */}
|
|
348
|
-
{title && (
|
|
349
|
-
<Text
|
|
350
|
-
style={[
|
|
351
|
-
{
|
|
352
|
-
color: titleColor,
|
|
353
|
-
fontSize: titleSize,
|
|
354
|
-
fontWeight: titleWeight as any,
|
|
355
|
-
lineHeight: titleLineHeight,
|
|
356
|
-
marginBottom: titlePaddingBottom,
|
|
357
|
-
}
|
|
358
|
-
]}
|
|
359
|
-
>
|
|
360
|
-
{title}
|
|
361
|
-
</Text>
|
|
362
|
-
)}
|
|
363
|
-
|
|
364
|
-
{/* Scrollable Content */}
|
|
365
|
-
<AnimatedScrollView
|
|
366
|
-
ref={scrollRef}
|
|
367
|
-
style={[styles.content, contentStyle]}
|
|
368
|
-
contentContainerStyle={[{ paddingBottom: paddingBottom + bottomInset, gap: drawerGap, flexDirection: 'column', alignItems: 'stretch' }, contentContainerStyle]}
|
|
369
|
-
showsVerticalScrollIndicator={showsVerticalScrollIndicator}
|
|
370
|
-
animatedProps={animatedScrollProps}
|
|
371
|
-
alwaysBounceVertical={false}
|
|
372
|
-
overScrollMode="always"
|
|
373
|
-
onScroll={useAnimatedScrollHandler((event) => {
|
|
374
|
-
scrollY.value = event.contentOffset.y
|
|
375
|
-
})}
|
|
376
|
-
scrollEventThrottle={16}
|
|
387
|
+
{/* Inner clip layer — keeps overflow:'hidden' off the shadow-carrying
|
|
388
|
+
outer view so iOS doesn't clip the drop shadow. */}
|
|
389
|
+
<View
|
|
390
|
+
style={[
|
|
391
|
+
styles.sheetInner,
|
|
392
|
+
{
|
|
393
|
+
borderTopLeftRadius: radius,
|
|
394
|
+
borderTopRightRadius: radius,
|
|
395
|
+
paddingLeft,
|
|
396
|
+
paddingRight,
|
|
397
|
+
paddingBottom,
|
|
398
|
+
rowGap: drawerGap,
|
|
399
|
+
},
|
|
400
|
+
]}
|
|
377
401
|
>
|
|
378
|
-
{/*
|
|
379
|
-
{
|
|
380
|
-
|
|
402
|
+
{/* Handle Area */}
|
|
403
|
+
<View style={[styles.handleArea, (!title && !header) && { paddingBottom: 0 }]}>
|
|
404
|
+
<View
|
|
405
|
+
style={[
|
|
406
|
+
{
|
|
407
|
+
backgroundColor: handleColor,
|
|
408
|
+
width: handleWidth,
|
|
409
|
+
height: handleHeight,
|
|
410
|
+
borderRadius: handleRadius
|
|
411
|
+
},
|
|
412
|
+
]}
|
|
413
|
+
/>
|
|
414
|
+
</View>
|
|
415
|
+
|
|
416
|
+
{/* Custom Header Slot */}
|
|
417
|
+
{header}
|
|
418
|
+
|
|
419
|
+
{/* Title (Legacy/Simple Mode) */}
|
|
420
|
+
{title && (
|
|
421
|
+
<Text
|
|
422
|
+
style={[
|
|
423
|
+
{
|
|
424
|
+
color: titleColor,
|
|
425
|
+
fontSize: titleSize,
|
|
426
|
+
fontWeight: titleWeight as any,
|
|
427
|
+
lineHeight: titleLineHeight,
|
|
428
|
+
marginBottom: titlePaddingBottom,
|
|
429
|
+
}
|
|
430
|
+
]}
|
|
431
|
+
>
|
|
432
|
+
{title}
|
|
433
|
+
</Text>
|
|
434
|
+
)}
|
|
435
|
+
|
|
436
|
+
{/* Scrollable Content */}
|
|
437
|
+
<AnimatedScrollView
|
|
438
|
+
ref={scrollRef}
|
|
439
|
+
style={[styles.content, contentStyle]}
|
|
440
|
+
contentContainerStyle={[{ paddingBottom: paddingBottom + bottomInset, gap: drawerGap, flexDirection: 'column', alignItems: 'stretch' }, contentContainerStyle]}
|
|
441
|
+
showsVerticalScrollIndicator={showsVerticalScrollIndicator}
|
|
442
|
+
animatedProps={animatedScrollProps}
|
|
443
|
+
alwaysBounceVertical={false}
|
|
444
|
+
overScrollMode="always"
|
|
445
|
+
onScroll={useAnimatedScrollHandler((event) => {
|
|
446
|
+
scrollY.value = event.contentOffset.y
|
|
447
|
+
})}
|
|
448
|
+
scrollEventThrottle={16}
|
|
449
|
+
>
|
|
450
|
+
{/* Prevent touch propagation for text selection if needed */}
|
|
451
|
+
{children}
|
|
452
|
+
</AnimatedScrollView>
|
|
453
|
+
</View>
|
|
381
454
|
</Animated.View>
|
|
382
455
|
</GestureDetector>
|
|
383
456
|
</GestureHandlerRootView>
|
|
@@ -399,6 +472,9 @@ const styles = StyleSheet.create({
|
|
|
399
472
|
width: '100%',
|
|
400
473
|
position: 'absolute',
|
|
401
474
|
top: 0,
|
|
475
|
+
},
|
|
476
|
+
sheetInner: {
|
|
477
|
+
flex: 1,
|
|
402
478
|
overflow: 'hidden',
|
|
403
479
|
},
|
|
404
480
|
handleArea: {
|
|
@@ -11,9 +11,19 @@ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
|
11
11
|
import Icon from '../../icons/Icon'
|
|
12
12
|
import { usePressableWebSupport, type SafePressableProps, type WebAccessibilityProps } from '../../utils/web-platform-utils'
|
|
13
13
|
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
14
|
+
import type { UnifiedSource } from '../../utils/MediaSource'
|
|
14
15
|
|
|
15
16
|
type IconButtonProps = SafePressableProps & {
|
|
17
|
+
/** Built-in icon name from the registry (default state). */
|
|
16
18
|
iconName?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Unified fallback source for the default state, used when `iconName` is
|
|
21
|
+
* missing or not in the registry. Accepts a remote URI, an inline SVG XML
|
|
22
|
+
* string, a `require()` asset, an SVG React component, or a React element.
|
|
23
|
+
* The result is tinted with the button's mode-resolved icon color so it
|
|
24
|
+
* follows design tokens just like a built-in icon. See {@link UnifiedSource}.
|
|
25
|
+
*/
|
|
26
|
+
source?: UnifiedSource;
|
|
17
27
|
modes?: Record<string, any>;
|
|
18
28
|
onPress?: () => void;
|
|
19
29
|
disabled?: boolean;
|
|
@@ -29,10 +39,24 @@ type IconButtonProps = SafePressableProps & {
|
|
|
29
39
|
* Icon to display when isToggle is true and isActive is true
|
|
30
40
|
*/
|
|
31
41
|
activeIcon?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Unified fallback source for the active state. Used when `activeIcon` is
|
|
44
|
+
* missing or not in the registry (and only when `isToggle` is true and
|
|
45
|
+
* `isActive` is true). Falls back to the default `source` if not provided.
|
|
46
|
+
* See {@link UnifiedSource}.
|
|
47
|
+
*/
|
|
48
|
+
activeSource?: UnifiedSource;
|
|
32
49
|
/**
|
|
33
50
|
* Icon to display when isToggle is true and isActive is false
|
|
34
51
|
*/
|
|
35
52
|
inactiveIcon?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Unified fallback source for the inactive state. Used when `inactiveIcon`
|
|
55
|
+
* is missing or not in the registry (and only when `isToggle` is true and
|
|
56
|
+
* `isActive` is false). Falls back to the default `source` if not provided.
|
|
57
|
+
* See {@link UnifiedSource}.
|
|
58
|
+
*/
|
|
59
|
+
inactiveSource?: UnifiedSource;
|
|
36
60
|
/**
|
|
37
61
|
* Whether the toggle button is in active state (only used when isToggle is true)
|
|
38
62
|
*/
|
|
@@ -109,8 +133,14 @@ function resolveIconButtonTokens(modes: Record<string, any>, disabled: boolean):
|
|
|
109
133
|
* pressed transform mirrored via React state) — removed.
|
|
110
134
|
* - Wrapped in `React.memo`.
|
|
111
135
|
*/
|
|
136
|
+
// Legacy default icon used when neither a `name` nor a `source` is supplied
|
|
137
|
+
// for the resolved slot. Kept as a constant rather than a destructuring
|
|
138
|
+
// default so source-only call sites don't accidentally render `'ic_card'`.
|
|
139
|
+
const LEGACY_DEFAULT_ICON_NAME = 'ic_card'
|
|
140
|
+
|
|
112
141
|
function IconButton({
|
|
113
|
-
iconName
|
|
142
|
+
iconName,
|
|
143
|
+
source,
|
|
114
144
|
modes = EMPTY_MODES,
|
|
115
145
|
onPress,
|
|
116
146
|
disabled = false,
|
|
@@ -121,7 +151,9 @@ function IconButton({
|
|
|
121
151
|
webAccessibilityProps,
|
|
122
152
|
isToggle = false,
|
|
123
153
|
activeIcon,
|
|
154
|
+
activeSource,
|
|
124
155
|
inactiveIcon,
|
|
156
|
+
inactiveSource,
|
|
125
157
|
isActive = false,
|
|
126
158
|
...rest
|
|
127
159
|
}: IconButtonProps) {
|
|
@@ -160,16 +192,42 @@ function IconButton({
|
|
|
160
192
|
userHandlersRef.current.onHoverIn = (rest as any)?.onHoverIn
|
|
161
193
|
userHandlersRef.current.onHoverOut = (rest as any)?.onHoverOut
|
|
162
194
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
195
|
+
// Resolve the active (name + source) pair for the current slot. Toggle
|
|
196
|
+
// mode picks active/inactive based on `isActive`; per-state overrides
|
|
197
|
+
// fall back to the default `iconName` / `source` when omitted. We then
|
|
198
|
+
// apply the legacy default icon only as a last resort, so a source-only
|
|
199
|
+
// call site (`<IconButton source="…" />`) renders the source instead of
|
|
200
|
+
// bleeding through to `'ic_card'`.
|
|
201
|
+
let resolvedIconName: string | undefined
|
|
202
|
+
let resolvedSource: UnifiedSource | undefined
|
|
203
|
+
if (isToggle) {
|
|
204
|
+
if (isActive) {
|
|
205
|
+
resolvedIconName = activeIcon ?? iconName
|
|
206
|
+
resolvedSource = activeSource ?? source
|
|
207
|
+
} else {
|
|
208
|
+
resolvedIconName = inactiveIcon ?? iconName
|
|
209
|
+
resolvedSource = inactiveSource ?? source
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
resolvedIconName = iconName
|
|
213
|
+
resolvedSource = source
|
|
214
|
+
}
|
|
215
|
+
if (!resolvedIconName && resolvedSource === undefined) {
|
|
216
|
+
resolvedIconName = LEGACY_DEFAULT_ICON_NAME
|
|
217
|
+
}
|
|
170
218
|
|
|
171
|
-
// Generate default accessibility label from icon name
|
|
172
|
-
|
|
219
|
+
// Generate default accessibility label from the resolved icon name when
|
|
220
|
+
// possible. Source-only call sites should provide an explicit
|
|
221
|
+
// `accessibilityLabel`; we fall back to a generic 'Icon button' so we
|
|
222
|
+
// never crash on `iconName.replace(...)` when only a `source` is supplied.
|
|
223
|
+
const defaultAccessibilityLabel =
|
|
224
|
+
accessibilityLabel ||
|
|
225
|
+
(resolvedIconName
|
|
226
|
+
? resolvedIconName
|
|
227
|
+
.replace(/^ic_/, '')
|
|
228
|
+
.replace(/_/g, ' ')
|
|
229
|
+
.replace(/\b\w/g, (l) => l.toUpperCase())
|
|
230
|
+
: 'Icon button')
|
|
173
231
|
|
|
174
232
|
const webProps = usePressableWebSupport({
|
|
175
233
|
restProps: rest,
|
|
@@ -235,7 +293,8 @@ function IconButton({
|
|
|
235
293
|
{...webProps}
|
|
236
294
|
>
|
|
237
295
|
<Icon
|
|
238
|
-
name
|
|
296
|
+
{...(resolvedIconName !== undefined ? { name: resolvedIconName } : {})}
|
|
297
|
+
{...(resolvedSource !== undefined ? { source: resolvedSource } : {})}
|
|
239
298
|
size={tokens.iconSize}
|
|
240
299
|
color={tokens.iconColor}
|
|
241
300
|
accessibilityElementsHidden={true}
|
|
@@ -4,9 +4,19 @@ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
|
4
4
|
import { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
5
5
|
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
6
6
|
import Icon from '../../icons/Icon'
|
|
7
|
+
import type { UnifiedSource } from '../../utils/MediaSource'
|
|
7
8
|
|
|
8
9
|
type IconCapsuleProps = {
|
|
9
10
|
iconName?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Unified fallback source rendered when `iconName` is missing or not in the
|
|
13
|
+
* registry. Accepts a remote URI, an inline SVG XML string, a `require()`
|
|
14
|
+
* asset, an SVG React component, or an already-rendered element. The
|
|
15
|
+
* resulting media is tinted with the capsule's mode-resolved icon color so
|
|
16
|
+
* it follows design tokens just like a built-in icon. See
|
|
17
|
+
* {@link UnifiedSource}.
|
|
18
|
+
*/
|
|
19
|
+
source?: UnifiedSource;
|
|
10
20
|
modes?: Record<string, any>;
|
|
11
21
|
accessibilityLabel?: string;
|
|
12
22
|
accessibilityRole?: string;
|
|
@@ -55,6 +65,7 @@ function resolveIconCapsuleTokens(modes: Record<string, any>): IconCapsuleTokens
|
|
|
55
65
|
* @component
|
|
56
66
|
* @param {Object} props - Component props
|
|
57
67
|
* @param {string} [props.iconName="ic_card"] - The name of the icon to display from the icon registry
|
|
68
|
+
* @param {UnifiedSource} [props.source] - Fallback source (remote URI, inline SVG XML, `require()` asset, SVG React component, or React element). Used when `iconName` is missing or unknown. Tinted with the mode-resolved icon color so it follows design tokens just like a built-in icon.
|
|
58
69
|
* @param {Object} [props.modes={}] - Mode configuration for design tokens (e.g., {"Appearance": "Primary"})
|
|
59
70
|
* @param {string} [props.accessibilityLabel] - Accessibility label for screen readers
|
|
60
71
|
* @param {string} [props.accessibilityRole] - Accessibility role (defaults to "image" for decorative icons)
|
|
@@ -68,6 +79,7 @@ function resolveIconCapsuleTokens(modes: Record<string, any>): IconCapsuleTokens
|
|
|
68
79
|
*/
|
|
69
80
|
function IconCapsule({
|
|
70
81
|
iconName = 'ic_card',
|
|
82
|
+
source,
|
|
71
83
|
modes: propModes = EMPTY_MODES,
|
|
72
84
|
// accessibilityLabel is accepted on the type for API back-compat but the
|
|
73
85
|
// component intentionally renders `accessibilityLabel={undefined}` (icons
|
|
@@ -105,6 +117,7 @@ function IconCapsule({
|
|
|
105
117
|
>
|
|
106
118
|
<Icon
|
|
107
119
|
name={iconName}
|
|
120
|
+
{...(source !== undefined ? { source } : {})}
|
|
108
121
|
size={tokens.iconSize}
|
|
109
122
|
color={tokens.iconColor}
|
|
110
123
|
accessibilityElementsHidden={true}
|
|
@@ -162,12 +162,12 @@ const Popup = forwardRef<PopupRef, PopupProps>(function Popup(
|
|
|
162
162
|
<View style={styles.overlay}>
|
|
163
163
|
<Animated.View
|
|
164
164
|
style={[
|
|
165
|
-
StyleSheet.
|
|
165
|
+
StyleSheet.absoluteFill,
|
|
166
166
|
{ backgroundColor: backdropColor, opacity: backdropAnim },
|
|
167
167
|
]}
|
|
168
168
|
>
|
|
169
169
|
<Pressable
|
|
170
|
-
style={StyleSheet.
|
|
170
|
+
style={StyleSheet.absoluteFill}
|
|
171
171
|
onPress={closeOnBackdropPress ? handleClose : undefined}
|
|
172
172
|
accessibilityRole="button"
|
|
173
173
|
accessibilityLabel="Close popup"
|