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
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,37 @@ All notable changes to this project are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
+
## [0.0.65] - 2026-04-21
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`Drawer` state callback:** Optional `onStateChange?: (state: 'collapsed' | 'expanded') => void` runs when the drawer settles into a new snap state (after gestures), so parents can react programmatically.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## [0.0.64] - 2026-04-20
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **`Section.Bento` — expandable grid:** New built-in expand/collapse UX. Pass `collapsedCount` (default `4`) plus the full set of cells in `navSlot`, and `Section.Bento` auto-injects a toggle cell, owns the `expanded` boolean (uncontrolled), and animates extra cells in/out with a staggered fade cascade. Supports controlled mode via `expanded` + `onExpandedChange`, customizable labels (`toggleMoreLabel`, `toggleLessLabel`) and icons (`toggleMoreIcon`, `toggleLessIcon`), and a `renderToggle` escape hatch for non-`ListItem` toggles. The toggle is only injected when `navSlot.length > collapsedCount`, so existing usage at or below the threshold renders unchanged.
|
|
20
|
+
- **Drawer drop-shadow tokens:** New design tokens `drawer/shadow/primary/{offsetX,offsetY,blur,color}` and `drawer/shadow/secondary/{offsetX,color}` resolve a layered Figma drop shadow. Web stacks both shadows via `boxShadow`; iOS applies the primary shadow natively; Android uses elevation.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **`SlotGrid` sizing — first-row-anchored:** `SlotGrid` (used by `Section.Bento`) now measures only the first row's cells once on mount and applies that max width to every cell in every row. Previously every cell was measured every render, which caused width jumps when the cell count changed (expand/collapse) and could cancel `Animated.View` `entering` cascades by re-rendering them in the same React batch as the mount. Result: stable cell width across toggles, no layout shift, animations play uninterrupted. Edge-flush behavior (first cell flush-left, last cell flush-right) is preserved via `justify-content: space-between`.
|
|
25
|
+
- **`Section.Bento` height transition:** The section grows and shrinks via an explicit measured-height spring (`overflow: 'hidden'` clip + `withSpring`) instead of `LinearTransition`. Cells inside are never resized during the animation — only the container's clip rectangle interpolates.
|
|
26
|
+
- **Drawer gesture policy:** Pan now requires a 10px vertical drag to activate (`activeOffsetY([-10, 10])`) and surrenders the gesture entirely after ~16px of horizontal movement (`failOffsetX([-16, 16])`). A defense-in-depth check in `onUpdate` skips Y translation on frames where horizontal motion dominates. This lets horizontal children (carousels, sliders, horizontal `FlatList`s) inside the drawer scroll cleanly without the sheet also translating.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **Drawer iOS shadow clipping:** The sheet now renders content inside an inner clip layer so `overflow: 'hidden'` no longer trims the outer view's drop shadow on iOS.
|
|
31
|
+
|
|
32
|
+
### Accessibility
|
|
33
|
+
|
|
34
|
+
- All new `Section.Bento` animations honor the OS reduce-motion setting via Reanimated's `ReduceMotion.System`.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
7
38
|
## [0.0.63] - 2026-04-20
|
|
8
39
|
|
|
9
40
|
### Performance
|
|
@@ -51,7 +51,8 @@ function Carousel({
|
|
|
51
51
|
const gap = gapProp ?? tokenGap;
|
|
52
52
|
const containerPaddingH = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carousel/padding/horizontal', modes) || '0');
|
|
53
53
|
const containerPaddingV = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carousel/padding/vertical', modes) || '0');
|
|
54
|
-
|
|
54
|
+
// Spacing between the cards row and the pagination dots uses `carousel/gap`.
|
|
55
|
+
const paginationOffset = gap;
|
|
55
56
|
|
|
56
57
|
// ---- Refs & state ----
|
|
57
58
|
const scrollRef = (0, _react.useRef)(null);
|
|
@@ -208,7 +209,7 @@ function Carousel({
|
|
|
208
209
|
}), showPagination && totalItems > 1 && /*#__PURE__*/(0, _jsxRuntime.jsx)(Pagination, {
|
|
209
210
|
modes: modes,
|
|
210
211
|
style: {
|
|
211
|
-
marginTop:
|
|
212
|
+
marginTop: paginationOffset
|
|
212
213
|
}
|
|
213
214
|
})]
|
|
214
215
|
})
|
|
@@ -250,13 +251,15 @@ function Pagination({
|
|
|
250
251
|
} = (0, _react.useContext)(CarouselContext);
|
|
251
252
|
const modes = propModes || ctxModes || {};
|
|
252
253
|
|
|
253
|
-
// Token resolution for dots
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
const
|
|
259
|
-
const
|
|
254
|
+
// Token resolution for dots — matches Figma tokens
|
|
255
|
+
// (carousel/pagination/gap, carousel/pagination/indicator/{activecolor,inactivecolor,radius}).
|
|
256
|
+
// Dot dimensions are fixed per Figma spec: inactive 6x6, active 16x6.
|
|
257
|
+
const dotSize = 6;
|
|
258
|
+
const dotActiveWidth = 16;
|
|
259
|
+
const dotGap = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/gap', modes) || '4');
|
|
260
|
+
const dotColor = (0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/indicator/inactivecolor', modes) || 'rgba(0,0,0,0.3)';
|
|
261
|
+
const dotActiveColor = (0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/indicator/activecolor', modes) || '#170d0a';
|
|
262
|
+
const dotRadius = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/indicator/radius', modes) || '9999');
|
|
260
263
|
const containerStyle = {
|
|
261
264
|
flexDirection: 'row',
|
|
262
265
|
justifyContent: 'center',
|
|
@@ -64,7 +64,8 @@ function Drawer({
|
|
|
64
64
|
accessibilityHint,
|
|
65
65
|
contentContainerStyle,
|
|
66
66
|
showsVerticalScrollIndicator = false,
|
|
67
|
-
bottomInset = 80
|
|
67
|
+
bottomInset = 80,
|
|
68
|
+
onStateChange
|
|
68
69
|
}) {
|
|
69
70
|
const {
|
|
70
71
|
height: screenHeight
|
|
@@ -129,9 +130,24 @@ function Drawer({
|
|
|
129
130
|
|
|
130
131
|
// Update JS state for accessibility/logic if needed
|
|
131
132
|
const updateMode = (0, _react.useCallback)(newMode => {
|
|
132
|
-
setMode(
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
setMode(prev => {
|
|
134
|
+
if (prev !== newMode) {
|
|
135
|
+
onStateChange?.(newMode);
|
|
136
|
+
}
|
|
137
|
+
return newMode;
|
|
138
|
+
});
|
|
139
|
+
}, [onStateChange]);
|
|
140
|
+
|
|
141
|
+
// Gesture policy:
|
|
142
|
+
// • activeOffsetY: require a clear *vertical* drag (10px) before this
|
|
143
|
+
// pan claims the gesture. Matches typical iOS scroll activation feel.
|
|
144
|
+
// • failOffsetX: if the finger crosses ~16px horizontally *before* we
|
|
145
|
+
// activate, surrender the gesture entirely so any horizontal child
|
|
146
|
+
// (FlatList horizontal, swiper, slider, etc.) can scroll cleanly
|
|
147
|
+
// without the drawer also translating on Y.
|
|
148
|
+
// • simultaneousWithExternalGesture(scrollRef): cooperate with the
|
|
149
|
+
// drawer's own internal vertical ScrollView for nested scrolling.
|
|
150
|
+
const gesture = _reactNativeGestureHandler.Gesture.Pan().simultaneousWithExternalGesture(scrollRef).activeOffsetY([-10, 10]).failOffsetX([-16, 16]).onStart(() => {
|
|
135
151
|
context.value = {
|
|
136
152
|
y: translateY.value
|
|
137
153
|
};
|
|
@@ -140,6 +156,16 @@ function Drawer({
|
|
|
140
156
|
prevAtTop.value = scrollY.value <= 1;
|
|
141
157
|
scrollTopTranslationOffset.value = 0;
|
|
142
158
|
}).onUpdate(event => {
|
|
159
|
+
// Defense-in-depth: even after vertical activation, if the *current*
|
|
160
|
+
// motion is dominantly horizontal (e.g., the user activated with a
|
|
161
|
+
// small Y nudge and then curved into a horizontal swipe on a child
|
|
162
|
+
// carousel), don't translate the drawer this frame. failOffsetX
|
|
163
|
+
// already prevents activation in pure-horizontal swipes; this guards
|
|
164
|
+
// the diagonal-then-horizontal case.
|
|
165
|
+
if (Math.abs(event.translationX) > Math.abs(event.translationY) * 1.5) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
143
169
|
// Logic for nested scrolling:
|
|
144
170
|
// If we are at the expanded position (minTranslateY) AND content is
|
|
145
171
|
// scrolled down (scrollY > 0), let the ScrollView handle the gesture.
|
|
@@ -251,71 +277,108 @@ function Drawer({
|
|
|
251
277
|
const titleWeight = (0, _figmaVariablesResolver.getVariableByName)('drawer/title/fontWeight', modes) || '700';
|
|
252
278
|
const titleLineHeight = (0, _figmaVariablesResolver.getVariableByName)('drawer/title/lineHeight', modes) || 17;
|
|
253
279
|
const titlePaddingBottom = (0, _figmaVariablesResolver.getVariableByName)('drawer/titleWrap/padding/bottom', modes) || 8;
|
|
280
|
+
|
|
281
|
+
// Drop shadow — Figma layers two shadows (primary + secondary) sharing
|
|
282
|
+
// the same offsetY/blur but with their own offsetX and color.
|
|
283
|
+
const shadowPrimaryOffsetX = (0, _figmaVariablesResolver.getVariableByName)('drawer/shadow/primary/offsetX', modes) ?? 0;
|
|
284
|
+
const shadowPrimaryOffsetY = (0, _figmaVariablesResolver.getVariableByName)('drawer/shadow/primary/offsetY', modes) ?? 16;
|
|
285
|
+
const shadowPrimaryBlur = (0, _figmaVariablesResolver.getVariableByName)('drawer/shadow/primary/blur', modes) ?? 48;
|
|
286
|
+
const shadowPrimaryColor = (0, _figmaVariablesResolver.getVariableByName)('drawer/shadow/primary/color', modes) ?? 'rgba(12, 13, 16, 0.16)';
|
|
287
|
+
const shadowSecondaryOffsetX = (0, _figmaVariablesResolver.getVariableByName)('drawer/shadow/secondary/offsetX', modes) ?? 0;
|
|
288
|
+
const shadowSecondaryColor = (0, _figmaVariablesResolver.getVariableByName)('drawer/shadow/secondary/color', modes) ?? 'rgba(12, 13, 16, 0.12)';
|
|
289
|
+
|
|
290
|
+
// Cross-platform shadow style. Web supports stacking two shadows via
|
|
291
|
+
// boxShadow. iOS only supports a single native shadow per view, so we
|
|
292
|
+
// apply the more prominent (primary) one. Android uses elevation.
|
|
293
|
+
const shadowStyle = _reactNative.Platform.select({
|
|
294
|
+
web: {
|
|
295
|
+
boxShadow: `${shadowSecondaryOffsetX}px ${shadowPrimaryOffsetY}px ${shadowPrimaryBlur}px 0px ${shadowSecondaryColor}, ` + `${shadowPrimaryOffsetX}px ${shadowPrimaryOffsetY}px ${shadowPrimaryBlur}px 0px ${shadowPrimaryColor}`
|
|
296
|
+
},
|
|
297
|
+
ios: {
|
|
298
|
+
shadowColor: shadowPrimaryColor,
|
|
299
|
+
shadowOffset: {
|
|
300
|
+
width: shadowPrimaryOffsetX,
|
|
301
|
+
height: shadowPrimaryOffsetY
|
|
302
|
+
},
|
|
303
|
+
shadowOpacity: 1,
|
|
304
|
+
shadowRadius: shadowPrimaryBlur / 2
|
|
305
|
+
},
|
|
306
|
+
android: {
|
|
307
|
+
elevation: 16
|
|
308
|
+
},
|
|
309
|
+
default: {}
|
|
310
|
+
});
|
|
254
311
|
const defaultAccessibilityLabel = accessibilityLabel || title || 'Drawer';
|
|
255
312
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeGestureHandler.GestureHandlerRootView, {
|
|
256
313
|
style: [styles.host, style],
|
|
257
314
|
pointerEvents: "box-none",
|
|
258
315
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeGestureHandler.GestureDetector, {
|
|
259
316
|
gesture: gesture,
|
|
260
|
-
children: /*#__PURE__*/(0, _jsxRuntime.
|
|
317
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
|
|
261
318
|
style: [styles.sheet, {
|
|
262
319
|
// Constraint the height strictly to the expanded height
|
|
263
320
|
// This ensures the ScrollView has a finite frame to scroll within
|
|
264
321
|
height: expandedHeight,
|
|
265
322
|
backgroundColor,
|
|
266
323
|
borderTopLeftRadius: radius,
|
|
267
|
-
borderTopRightRadius: radius
|
|
268
|
-
|
|
269
|
-
paddingRight,
|
|
270
|
-
paddingBottom,
|
|
271
|
-
rowGap: drawerGap
|
|
272
|
-
}, sheetStyle, animatedStyle],
|
|
324
|
+
borderTopRightRadius: radius
|
|
325
|
+
}, shadowStyle, sheetStyle, animatedStyle],
|
|
273
326
|
accessible: true,
|
|
274
327
|
...(_reactNative.Platform.OS === 'web' ? {
|
|
275
328
|
accessibilityRole: 'dialog'
|
|
276
329
|
} : undefined),
|
|
277
330
|
accessibilityLabel: undefined,
|
|
278
331
|
accessibilityHint: accessibilityHint || 'Swipe up to expand, swipe down to collapse',
|
|
279
|
-
children:
|
|
280
|
-
style: [styles.
|
|
281
|
-
|
|
332
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
333
|
+
style: [styles.sheetInner, {
|
|
334
|
+
borderTopLeftRadius: radius,
|
|
335
|
+
borderTopRightRadius: radius,
|
|
336
|
+
paddingLeft,
|
|
337
|
+
paddingRight,
|
|
338
|
+
paddingBottom,
|
|
339
|
+
rowGap: drawerGap
|
|
282
340
|
}],
|
|
283
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
341
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
342
|
+
style: [styles.handleArea, !title && !header && {
|
|
343
|
+
paddingBottom: 0
|
|
344
|
+
}],
|
|
345
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
346
|
+
style: [{
|
|
347
|
+
backgroundColor: handleColor,
|
|
348
|
+
width: handleWidth,
|
|
349
|
+
height: handleHeight,
|
|
350
|
+
borderRadius: handleRadius
|
|
351
|
+
}]
|
|
352
|
+
})
|
|
353
|
+
}), header, title && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
284
354
|
style: [{
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
overScrollMode: "always",
|
|
313
|
-
onScroll: (0, _reactNativeReanimated.useAnimatedScrollHandler)(event => {
|
|
314
|
-
scrollY.value = event.contentOffset.y;
|
|
315
|
-
}),
|
|
316
|
-
scrollEventThrottle: 16,
|
|
317
|
-
children: children
|
|
318
|
-
})]
|
|
355
|
+
color: titleColor,
|
|
356
|
+
fontSize: titleSize,
|
|
357
|
+
fontWeight: titleWeight,
|
|
358
|
+
lineHeight: titleLineHeight,
|
|
359
|
+
marginBottom: titlePaddingBottom
|
|
360
|
+
}],
|
|
361
|
+
children: title
|
|
362
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(AnimatedScrollView, {
|
|
363
|
+
ref: scrollRef,
|
|
364
|
+
style: [styles.content, contentStyle],
|
|
365
|
+
contentContainerStyle: [{
|
|
366
|
+
paddingBottom: paddingBottom + bottomInset,
|
|
367
|
+
gap: drawerGap,
|
|
368
|
+
flexDirection: 'column',
|
|
369
|
+
alignItems: 'stretch'
|
|
370
|
+
}, contentContainerStyle],
|
|
371
|
+
showsVerticalScrollIndicator: showsVerticalScrollIndicator,
|
|
372
|
+
animatedProps: animatedScrollProps,
|
|
373
|
+
alwaysBounceVertical: false,
|
|
374
|
+
overScrollMode: "always",
|
|
375
|
+
onScroll: (0, _reactNativeReanimated.useAnimatedScrollHandler)(event => {
|
|
376
|
+
scrollY.value = event.contentOffset.y;
|
|
377
|
+
}),
|
|
378
|
+
scrollEventThrottle: 16,
|
|
379
|
+
children: children
|
|
380
|
+
})]
|
|
381
|
+
})
|
|
319
382
|
})
|
|
320
383
|
})
|
|
321
384
|
});
|
|
@@ -333,7 +396,10 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
333
396
|
sheet: {
|
|
334
397
|
width: '100%',
|
|
335
398
|
position: 'absolute',
|
|
336
|
-
top: 0
|
|
399
|
+
top: 0
|
|
400
|
+
},
|
|
401
|
+
sheetInner: {
|
|
402
|
+
flex: 1,
|
|
337
403
|
overflow: 'hidden'
|
|
338
404
|
},
|
|
339
405
|
handleArea: {
|
|
@@ -78,8 +78,13 @@ function resolveIconButtonTokens(modes, disabled) {
|
|
|
78
78
|
* pressed transform mirrored via React state) — removed.
|
|
79
79
|
* - Wrapped in `React.memo`.
|
|
80
80
|
*/
|
|
81
|
+
// Legacy default icon used when neither a `name` nor a `source` is supplied
|
|
82
|
+
// for the resolved slot. Kept as a constant rather than a destructuring
|
|
83
|
+
// default so source-only call sites don't accidentally render `'ic_card'`.
|
|
84
|
+
const LEGACY_DEFAULT_ICON_NAME = 'ic_card';
|
|
81
85
|
function IconButton({
|
|
82
|
-
iconName
|
|
86
|
+
iconName,
|
|
87
|
+
source,
|
|
83
88
|
modes = _reactUtils.EMPTY_MODES,
|
|
84
89
|
onPress,
|
|
85
90
|
disabled = false,
|
|
@@ -90,7 +95,9 @@ function IconButton({
|
|
|
90
95
|
webAccessibilityProps,
|
|
91
96
|
isToggle = false,
|
|
92
97
|
activeIcon,
|
|
98
|
+
activeSource,
|
|
93
99
|
inactiveIcon,
|
|
100
|
+
inactiveSource,
|
|
94
101
|
isActive = false,
|
|
95
102
|
...rest
|
|
96
103
|
}) {
|
|
@@ -113,11 +120,35 @@ function IconButton({
|
|
|
113
120
|
userHandlersRef.current.onHoverIn = rest?.onHoverIn;
|
|
114
121
|
userHandlersRef.current.onHoverOut = rest?.onHoverOut;
|
|
115
122
|
|
|
116
|
-
//
|
|
117
|
-
|
|
123
|
+
// Resolve the active (name + source) pair for the current slot. Toggle
|
|
124
|
+
// mode picks active/inactive based on `isActive`; per-state overrides
|
|
125
|
+
// fall back to the default `iconName` / `source` when omitted. We then
|
|
126
|
+
// apply the legacy default icon only as a last resort, so a source-only
|
|
127
|
+
// call site (`<IconButton source="…" />`) renders the source instead of
|
|
128
|
+
// bleeding through to `'ic_card'`.
|
|
129
|
+
let resolvedIconName;
|
|
130
|
+
let resolvedSource;
|
|
131
|
+
if (isToggle) {
|
|
132
|
+
if (isActive) {
|
|
133
|
+
resolvedIconName = activeIcon ?? iconName;
|
|
134
|
+
resolvedSource = activeSource ?? source;
|
|
135
|
+
} else {
|
|
136
|
+
resolvedIconName = inactiveIcon ?? iconName;
|
|
137
|
+
resolvedSource = inactiveSource ?? source;
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
resolvedIconName = iconName;
|
|
141
|
+
resolvedSource = source;
|
|
142
|
+
}
|
|
143
|
+
if (!resolvedIconName && resolvedSource === undefined) {
|
|
144
|
+
resolvedIconName = LEGACY_DEFAULT_ICON_NAME;
|
|
145
|
+
}
|
|
118
146
|
|
|
119
|
-
// Generate default accessibility label from icon name
|
|
120
|
-
|
|
147
|
+
// Generate default accessibility label from the resolved icon name when
|
|
148
|
+
// possible. Source-only call sites should provide an explicit
|
|
149
|
+
// `accessibilityLabel`; we fall back to a generic 'Icon button' so we
|
|
150
|
+
// never crash on `iconName.replace(...)` when only a `source` is supplied.
|
|
151
|
+
const defaultAccessibilityLabel = accessibilityLabel || (resolvedIconName ? resolvedIconName.replace(/^ic_/, '').replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) : 'Icon button');
|
|
121
152
|
const webProps = (0, _webPlatformUtils.usePressableWebSupport)({
|
|
122
153
|
restProps: rest,
|
|
123
154
|
onPress: disabled ? undefined : onPress,
|
|
@@ -170,7 +201,12 @@ function IconButton({
|
|
|
170
201
|
style: styleCallback,
|
|
171
202
|
...webProps,
|
|
172
203
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Icon.default, {
|
|
173
|
-
|
|
204
|
+
...(resolvedIconName !== undefined ? {
|
|
205
|
+
name: resolvedIconName
|
|
206
|
+
} : {}),
|
|
207
|
+
...(resolvedSource !== undefined ? {
|
|
208
|
+
source: resolvedSource
|
|
209
|
+
} : {}),
|
|
174
210
|
size: tokens.iconSize,
|
|
175
211
|
color: tokens.iconColor,
|
|
176
212
|
accessibilityElementsHidden: true,
|
|
@@ -49,6 +49,7 @@ function resolveIconCapsuleTokens(modes) {
|
|
|
49
49
|
* @component
|
|
50
50
|
* @param {Object} props - Component props
|
|
51
51
|
* @param {string} [props.iconName="ic_card"] - The name of the icon to display from the icon registry
|
|
52
|
+
* @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.
|
|
52
53
|
* @param {Object} [props.modes={}] - Mode configuration for design tokens (e.g., {"Appearance": "Primary"})
|
|
53
54
|
* @param {string} [props.accessibilityLabel] - Accessibility label for screen readers
|
|
54
55
|
* @param {string} [props.accessibilityRole] - Accessibility role (defaults to "image" for decorative icons)
|
|
@@ -62,6 +63,7 @@ function resolveIconCapsuleTokens(modes) {
|
|
|
62
63
|
*/
|
|
63
64
|
function IconCapsule({
|
|
64
65
|
iconName = 'ic_card',
|
|
66
|
+
source,
|
|
65
67
|
modes: propModes = _reactUtils.EMPTY_MODES,
|
|
66
68
|
// accessibilityLabel is accepted on the type for API back-compat but the
|
|
67
69
|
// component intentionally renders `accessibilityLabel={undefined}` (icons
|
|
@@ -91,6 +93,9 @@ function IconCapsule({
|
|
|
91
93
|
...rest,
|
|
92
94
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Icon.default, {
|
|
93
95
|
name: iconName,
|
|
96
|
+
...(source !== undefined ? {
|
|
97
|
+
source
|
|
98
|
+
} : {}),
|
|
94
99
|
size: tokens.iconSize,
|
|
95
100
|
color: tokens.iconColor,
|
|
96
101
|
accessibilityElementsHidden: true,
|
|
@@ -112,12 +112,12 @@ const Popup = /*#__PURE__*/(0, _react.forwardRef)(function Popup({
|
|
|
112
112
|
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
113
113
|
style: styles.overlay,
|
|
114
114
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, {
|
|
115
|
-
style: [_reactNative.StyleSheet.
|
|
115
|
+
style: [_reactNative.StyleSheet.absoluteFill, {
|
|
116
116
|
backgroundColor: backdropColor,
|
|
117
117
|
opacity: backdropAnim
|
|
118
118
|
}],
|
|
119
119
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
|
|
120
|
-
style: _reactNative.StyleSheet.
|
|
120
|
+
style: _reactNative.StyleSheet.absoluteFill,
|
|
121
121
|
onPress: closeOnBackdropPress ? handleClose : undefined,
|
|
122
122
|
accessibilityRole: "button",
|
|
123
123
|
accessibilityLabel: "Close popup"
|