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.
Files changed (44) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/lib/commonjs/components/Carousel/Carousel.js +12 -9
  3. package/lib/commonjs/components/Drawer/Drawer.js +116 -50
  4. package/lib/commonjs/components/IconButton/IconButton.js +42 -6
  5. package/lib/commonjs/components/IconCapsule/IconCapsule.js +5 -0
  6. package/lib/commonjs/components/Popup/Popup.js +2 -2
  7. package/lib/commonjs/components/Section/Section.js +280 -58
  8. package/lib/commonjs/components/UpiHandle/UpiHandle.js +19 -7
  9. package/lib/commonjs/icons/Icon.js +72 -75
  10. package/lib/commonjs/icons/registry.js +1 -1
  11. package/lib/commonjs/utils/MediaSource.js +181 -0
  12. package/lib/commonjs/utils/index.js +9 -1
  13. package/lib/module/components/Carousel/Carousel.js +12 -9
  14. package/lib/module/components/Drawer/Drawer.js +116 -50
  15. package/lib/module/components/IconButton/IconButton.js +42 -6
  16. package/lib/module/components/IconCapsule/IconCapsule.js +5 -0
  17. package/lib/module/components/Popup/Popup.js +2 -2
  18. package/lib/module/components/Section/Section.js +280 -58
  19. package/lib/module/components/UpiHandle/UpiHandle.js +20 -8
  20. package/lib/module/icons/Icon.js +72 -75
  21. package/lib/module/icons/registry.js +1 -1
  22. package/lib/module/utils/MediaSource.js +176 -0
  23. package/lib/module/utils/index.js +2 -1
  24. package/lib/typescript/src/components/Drawer/Drawer.d.ts +6 -1
  25. package/lib/typescript/src/components/IconButton/IconButton.d.ts +25 -14
  26. package/lib/typescript/src/components/IconCapsule/IconCapsule.d.ts +12 -1
  27. package/lib/typescript/src/components/Section/Section.d.ts +42 -1
  28. package/lib/typescript/src/components/UpiHandle/UpiHandle.d.ts +17 -3
  29. package/lib/typescript/src/icons/Icon.d.ts +35 -16
  30. package/lib/typescript/src/icons/registry.d.ts +1 -1
  31. package/lib/typescript/src/utils/MediaSource.d.ts +63 -0
  32. package/lib/typescript/src/utils/index.d.ts +2 -0
  33. package/package.json +1 -1
  34. package/src/components/Carousel/Carousel.tsx +16 -17
  35. package/src/components/Drawer/Drawer.tsx +136 -60
  36. package/src/components/IconButton/IconButton.tsx +70 -11
  37. package/src/components/IconCapsule/IconCapsule.tsx +13 -0
  38. package/src/components/Popup/Popup.tsx +2 -2
  39. package/src/components/Section/Section.tsx +411 -71
  40. package/src/components/UpiHandle/UpiHandle.tsx +37 -11
  41. package/src/icons/Icon.tsx +91 -76
  42. package/src/icons/registry.ts +1 -1
  43. package/src/utils/MediaSource.tsx +220 -0
  44. package/src/utils/index.ts +2 -0
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _react = _interopRequireDefault(require("react"));
8
+ var _reactNative = require("react-native");
9
+ var _reactNativeSvg = require("react-native-svg");
10
+ var _jsxRuntime = require("react/jsx-runtime");
11
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
12
+ /**
13
+ * A unified, "do-the-right-thing" image source accepted by `MediaSource` and
14
+ * by the `source` prop on `Icon`, `IconCapsule` and `UpiHandle`.
15
+ *
16
+ * Accepts any of:
17
+ * - `string` — a URI (raster or `.svg`) **or** an inline SVG XML document
18
+ * (`'<svg …>…</svg>'`).
19
+ * - `number` — a Metro asset id from `require('./foo.png')`.
20
+ * - `{ uri, … }` — the standard RN `ImageURISource` object (works for both
21
+ * raster and `.svg` URIs).
22
+ * - `React.ComponentType` — an SVG React component (e.g. produced by
23
+ * `react-native-svg-transformer`, by `@svgr/*`,
24
+ * or hand-written). It is rendered with
25
+ * `{ width, height, color, fill }` so it can be
26
+ * tinted just like a built-in icon.
27
+ * - `React.ReactElement` — an already-rendered node, passed through verbatim.
28
+ *
29
+ * The helper sniffs the input shape (no extension hint required from the
30
+ * caller) and picks the correct renderer for the platform.
31
+ */
32
+
33
+ const SVG_XML_RE = /<svg[\s>]/i;
34
+ const SVG_URI_RE = /\.svg(\?|#|$)/i;
35
+ function isSvgXml(s) {
36
+ return /^\s*</.test(s) && SVG_XML_RE.test(s);
37
+ }
38
+ function isSvgUri(s) {
39
+ return SVG_URI_RE.test(s);
40
+ }
41
+ function isUriObject(v) {
42
+ return typeof v === 'object' && v !== null && 'uri' in v && typeof v.uri === 'string';
43
+ }
44
+
45
+ /**
46
+ * Smart renderer that picks the right primitive for the source shape. See
47
+ * {@link UnifiedSource}.
48
+ *
49
+ * Designed to be used internally by `Icon`, `IconCapsule`, and `UpiHandle`,
50
+ * but also exported for ad-hoc consumer use.
51
+ */
52
+ function MediaSource(props) {
53
+ const {
54
+ source,
55
+ size,
56
+ width,
57
+ height,
58
+ tintColor,
59
+ style,
60
+ resizeMode = 'cover',
61
+ accessibilityElementsHidden,
62
+ importantForAccessibility
63
+ } = props;
64
+ const w = width ?? size;
65
+ const h = height ?? size;
66
+
67
+ // Pre-rendered element — pass through.
68
+ if (/*#__PURE__*/_react.default.isValidElement(source)) {
69
+ return source;
70
+ }
71
+
72
+ // SVG / icon component.
73
+ if (typeof source === 'function') {
74
+ const Comp = source;
75
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(Comp, {
76
+ ...(w !== undefined ? {
77
+ width: w
78
+ } : {}),
79
+ ...(h !== undefined ? {
80
+ height: h
81
+ } : {}),
82
+ ...(tintColor !== undefined ? {
83
+ color: tintColor,
84
+ fill: tintColor
85
+ } : {})
86
+ });
87
+ }
88
+ const sizeStyle = w !== undefined || h !== undefined ? {
89
+ width: w,
90
+ height: h
91
+ } : null;
92
+ const tintStyle = tintColor ? {
93
+ tintColor
94
+ } : null;
95
+ const composedStyle = [sizeStyle, tintStyle, style];
96
+ if (typeof source === 'string') {
97
+ if (isSvgXml(source)) {
98
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.SvgXml, {
99
+ xml: source,
100
+ ...(w !== undefined ? {
101
+ width: w
102
+ } : {}),
103
+ ...(h !== undefined ? {
104
+ height: h
105
+ } : {}),
106
+ ...(tintColor !== undefined ? {
107
+ color: tintColor
108
+ } : {})
109
+ });
110
+ }
111
+ if (isSvgUri(source)) {
112
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.SvgUri, {
113
+ uri: source,
114
+ ...(w !== undefined ? {
115
+ width: w
116
+ } : {}),
117
+ ...(h !== undefined ? {
118
+ height: h
119
+ } : {}),
120
+ ...(tintColor !== undefined ? {
121
+ color: tintColor
122
+ } : {})
123
+ });
124
+ }
125
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
126
+ source: {
127
+ uri: source
128
+ },
129
+ style: composedStyle,
130
+ resizeMode: resizeMode,
131
+ ...(accessibilityElementsHidden !== undefined ? {
132
+ accessibilityElementsHidden
133
+ } : {}),
134
+ ...(importantForAccessibility !== undefined ? {
135
+ importantForAccessibility
136
+ } : {})
137
+ });
138
+ }
139
+ if (isUriObject(source)) {
140
+ if (isSvgUri(source.uri)) {
141
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.SvgUri, {
142
+ uri: source.uri,
143
+ ...(w !== undefined ? {
144
+ width: w
145
+ } : {}),
146
+ ...(h !== undefined ? {
147
+ height: h
148
+ } : {}),
149
+ ...(tintColor !== undefined ? {
150
+ color: tintColor
151
+ } : {})
152
+ });
153
+ }
154
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
155
+ source: source,
156
+ style: composedStyle,
157
+ resizeMode: resizeMode,
158
+ ...(accessibilityElementsHidden !== undefined ? {
159
+ accessibilityElementsHidden
160
+ } : {}),
161
+ ...(importantForAccessibility !== undefined ? {
162
+ importantForAccessibility
163
+ } : {})
164
+ });
165
+ }
166
+ if (typeof source === 'number') {
167
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
168
+ source: source,
169
+ style: composedStyle,
170
+ resizeMode: resizeMode,
171
+ ...(accessibilityElementsHidden !== undefined ? {
172
+ accessibilityElementsHidden
173
+ } : {}),
174
+ ...(importantForAccessibility !== undefined ? {
175
+ importantForAccessibility
176
+ } : {})
177
+ });
178
+ }
179
+ return null;
180
+ }
181
+ var _default = exports.default = /*#__PURE__*/_react.default.memo(MediaSource);
@@ -3,6 +3,12 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ Object.defineProperty(exports, "MediaSource", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _MediaSource.default;
10
+ }
11
+ });
6
12
  Object.defineProperty(exports, "cloneChildrenWithModes", {
7
13
  enumerable: true,
8
14
  get: function () {
@@ -15,4 +21,6 @@ Object.defineProperty(exports, "flattenChildren", {
15
21
  return _reactUtils.flattenChildren;
16
22
  }
17
23
  });
18
- var _reactUtils = require("./react-utils");
24
+ var _reactUtils = require("./react-utils");
25
+ var _MediaSource = _interopRequireDefault(require("./MediaSource"));
26
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
@@ -43,7 +43,8 @@ export function Carousel({
43
43
  const gap = gapProp ?? tokenGap;
44
44
  const containerPaddingH = parseFloat(getVariableByName('carousel/padding/horizontal', modes) || '0');
45
45
  const containerPaddingV = parseFloat(getVariableByName('carousel/padding/vertical', modes) || '0');
46
- const paginationGap = parseFloat(getVariableByName('carousel/pagination/gap', modes) || '12');
46
+ // Spacing between the cards row and the pagination dots uses `carousel/gap`.
47
+ const paginationOffset = gap;
47
48
 
48
49
  // ---- Refs & state ----
49
50
  const scrollRef = useRef(null);
@@ -200,7 +201,7 @@ export function Carousel({
200
201
  }), showPagination && totalItems > 1 && /*#__PURE__*/_jsx(Pagination, {
201
202
  modes: modes,
202
203
  style: {
203
- marginTop: paginationGap
204
+ marginTop: paginationOffset
204
205
  }
205
206
  })]
206
207
  })
@@ -242,13 +243,15 @@ export function Pagination({
242
243
  } = useContext(CarouselContext);
243
244
  const modes = propModes || ctxModes || {};
244
245
 
245
- // Token resolution for dots
246
- const dotSize = parseFloat(getVariableByName('carousel/pagination/dotSize', modes) || '8');
247
- const dotActiveWidth = parseFloat(getVariableByName('carousel/pagination/dotActiveWidth', modes) || '24');
248
- const dotGap = parseFloat(getVariableByName('carousel/pagination/dotGap', modes) || '8');
249
- const dotColor = getVariableByName('carousel/pagination/dotColor', modes) || 'rgba(255,255,255,0.35)';
250
- const dotActiveColor = getVariableByName('carousel/pagination/dotActiveColor', modes) || '#ffffff';
251
- const dotRadius = parseFloat(getVariableByName('carousel/pagination/dotRadius', modes) || '4');
246
+ // Token resolution for dots — matches Figma tokens
247
+ // (carousel/pagination/gap, carousel/pagination/indicator/{activecolor,inactivecolor,radius}).
248
+ // Dot dimensions are fixed per Figma spec: inactive 6x6, active 16x6.
249
+ const dotSize = 6;
250
+ const dotActiveWidth = 16;
251
+ const dotGap = parseFloat(getVariableByName('carousel/pagination/gap', modes) || '4');
252
+ const dotColor = getVariableByName('carousel/pagination/indicator/inactivecolor', modes) || 'rgba(0,0,0,0.3)';
253
+ const dotActiveColor = getVariableByName('carousel/pagination/indicator/activecolor', modes) || '#170d0a';
254
+ const dotRadius = parseFloat(getVariableByName('carousel/pagination/indicator/radius', modes) || '9999');
252
255
  const containerStyle = {
253
256
  flexDirection: 'row',
254
257
  justifyContent: 'center',
@@ -59,7 +59,8 @@ function Drawer({
59
59
  accessibilityHint,
60
60
  contentContainerStyle,
61
61
  showsVerticalScrollIndicator = false,
62
- bottomInset = 80
62
+ bottomInset = 80,
63
+ onStateChange
63
64
  }) {
64
65
  const {
65
66
  height: screenHeight
@@ -124,9 +125,24 @@ function Drawer({
124
125
 
125
126
  // Update JS state for accessibility/logic if needed
126
127
  const updateMode = useCallback(newMode => {
127
- setMode(newMode);
128
- }, []);
129
- const gesture = Gesture.Pan().simultaneousWithExternalGesture(scrollRef).activeOffsetY([-5, 5]).activeOffsetX([-5, 5]).onStart(() => {
128
+ setMode(prev => {
129
+ if (prev !== newMode) {
130
+ onStateChange?.(newMode);
131
+ }
132
+ return newMode;
133
+ });
134
+ }, [onStateChange]);
135
+
136
+ // Gesture policy:
137
+ // • activeOffsetY: require a clear *vertical* drag (10px) before this
138
+ // pan claims the gesture. Matches typical iOS scroll activation feel.
139
+ // • failOffsetX: if the finger crosses ~16px horizontally *before* we
140
+ // activate, surrender the gesture entirely so any horizontal child
141
+ // (FlatList horizontal, swiper, slider, etc.) can scroll cleanly
142
+ // without the drawer also translating on Y.
143
+ // • simultaneousWithExternalGesture(scrollRef): cooperate with the
144
+ // drawer's own internal vertical ScrollView for nested scrolling.
145
+ const gesture = Gesture.Pan().simultaneousWithExternalGesture(scrollRef).activeOffsetY([-10, 10]).failOffsetX([-16, 16]).onStart(() => {
130
146
  context.value = {
131
147
  y: translateY.value
132
148
  };
@@ -135,6 +151,16 @@ function Drawer({
135
151
  prevAtTop.value = scrollY.value <= 1;
136
152
  scrollTopTranslationOffset.value = 0;
137
153
  }).onUpdate(event => {
154
+ // Defense-in-depth: even after vertical activation, if the *current*
155
+ // motion is dominantly horizontal (e.g., the user activated with a
156
+ // small Y nudge and then curved into a horizontal swipe on a child
157
+ // carousel), don't translate the drawer this frame. failOffsetX
158
+ // already prevents activation in pure-horizontal swipes; this guards
159
+ // the diagonal-then-horizontal case.
160
+ if (Math.abs(event.translationX) > Math.abs(event.translationY) * 1.5) {
161
+ return;
162
+ }
163
+
138
164
  // Logic for nested scrolling:
139
165
  // If we are at the expanded position (minTranslateY) AND content is
140
166
  // scrolled down (scrollY > 0), let the ScrollView handle the gesture.
@@ -246,71 +272,108 @@ function Drawer({
246
272
  const titleWeight = getVariableByName('drawer/title/fontWeight', modes) || '700';
247
273
  const titleLineHeight = getVariableByName('drawer/title/lineHeight', modes) || 17;
248
274
  const titlePaddingBottom = getVariableByName('drawer/titleWrap/padding/bottom', modes) || 8;
275
+
276
+ // Drop shadow — Figma layers two shadows (primary + secondary) sharing
277
+ // the same offsetY/blur but with their own offsetX and color.
278
+ const shadowPrimaryOffsetX = getVariableByName('drawer/shadow/primary/offsetX', modes) ?? 0;
279
+ const shadowPrimaryOffsetY = getVariableByName('drawer/shadow/primary/offsetY', modes) ?? 16;
280
+ const shadowPrimaryBlur = getVariableByName('drawer/shadow/primary/blur', modes) ?? 48;
281
+ const shadowPrimaryColor = getVariableByName('drawer/shadow/primary/color', modes) ?? 'rgba(12, 13, 16, 0.16)';
282
+ const shadowSecondaryOffsetX = getVariableByName('drawer/shadow/secondary/offsetX', modes) ?? 0;
283
+ const shadowSecondaryColor = getVariableByName('drawer/shadow/secondary/color', modes) ?? 'rgba(12, 13, 16, 0.12)';
284
+
285
+ // Cross-platform shadow style. Web supports stacking two shadows via
286
+ // boxShadow. iOS only supports a single native shadow per view, so we
287
+ // apply the more prominent (primary) one. Android uses elevation.
288
+ const shadowStyle = Platform.select({
289
+ web: {
290
+ boxShadow: `${shadowSecondaryOffsetX}px ${shadowPrimaryOffsetY}px ${shadowPrimaryBlur}px 0px ${shadowSecondaryColor}, ` + `${shadowPrimaryOffsetX}px ${shadowPrimaryOffsetY}px ${shadowPrimaryBlur}px 0px ${shadowPrimaryColor}`
291
+ },
292
+ ios: {
293
+ shadowColor: shadowPrimaryColor,
294
+ shadowOffset: {
295
+ width: shadowPrimaryOffsetX,
296
+ height: shadowPrimaryOffsetY
297
+ },
298
+ shadowOpacity: 1,
299
+ shadowRadius: shadowPrimaryBlur / 2
300
+ },
301
+ android: {
302
+ elevation: 16
303
+ },
304
+ default: {}
305
+ });
249
306
  const defaultAccessibilityLabel = accessibilityLabel || title || 'Drawer';
250
307
  return /*#__PURE__*/_jsx(GestureHandlerRootView, {
251
308
  style: [styles.host, style],
252
309
  pointerEvents: "box-none",
253
310
  children: /*#__PURE__*/_jsx(GestureDetector, {
254
311
  gesture: gesture,
255
- children: /*#__PURE__*/_jsxs(Animated.View, {
312
+ children: /*#__PURE__*/_jsx(Animated.View, {
256
313
  style: [styles.sheet, {
257
314
  // Constraint the height strictly to the expanded height
258
315
  // This ensures the ScrollView has a finite frame to scroll within
259
316
  height: expandedHeight,
260
317
  backgroundColor,
261
318
  borderTopLeftRadius: radius,
262
- borderTopRightRadius: radius,
263
- paddingLeft,
264
- paddingRight,
265
- paddingBottom,
266
- rowGap: drawerGap
267
- }, sheetStyle, animatedStyle],
319
+ borderTopRightRadius: radius
320
+ }, shadowStyle, sheetStyle, animatedStyle],
268
321
  accessible: true,
269
322
  ...(Platform.OS === 'web' ? {
270
323
  accessibilityRole: 'dialog'
271
324
  } : undefined),
272
325
  accessibilityLabel: undefined,
273
326
  accessibilityHint: accessibilityHint || 'Swipe up to expand, swipe down to collapse',
274
- children: [/*#__PURE__*/_jsx(View, {
275
- style: [styles.handleArea, !title && !header && {
276
- paddingBottom: 0
327
+ children: /*#__PURE__*/_jsxs(View, {
328
+ style: [styles.sheetInner, {
329
+ borderTopLeftRadius: radius,
330
+ borderTopRightRadius: radius,
331
+ paddingLeft,
332
+ paddingRight,
333
+ paddingBottom,
334
+ rowGap: drawerGap
277
335
  }],
278
- children: /*#__PURE__*/_jsx(View, {
336
+ children: [/*#__PURE__*/_jsx(View, {
337
+ style: [styles.handleArea, !title && !header && {
338
+ paddingBottom: 0
339
+ }],
340
+ children: /*#__PURE__*/_jsx(View, {
341
+ style: [{
342
+ backgroundColor: handleColor,
343
+ width: handleWidth,
344
+ height: handleHeight,
345
+ borderRadius: handleRadius
346
+ }]
347
+ })
348
+ }), header, title && /*#__PURE__*/_jsx(Text, {
279
349
  style: [{
280
- backgroundColor: handleColor,
281
- width: handleWidth,
282
- height: handleHeight,
283
- borderRadius: handleRadius
284
- }]
285
- })
286
- }), header, title && /*#__PURE__*/_jsx(Text, {
287
- style: [{
288
- color: titleColor,
289
- fontSize: titleSize,
290
- fontWeight: titleWeight,
291
- lineHeight: titleLineHeight,
292
- marginBottom: titlePaddingBottom
293
- }],
294
- children: title
295
- }), /*#__PURE__*/_jsx(AnimatedScrollView, {
296
- ref: scrollRef,
297
- style: [styles.content, contentStyle],
298
- contentContainerStyle: [{
299
- paddingBottom: paddingBottom + bottomInset,
300
- gap: drawerGap,
301
- flexDirection: 'column',
302
- alignItems: 'stretch'
303
- }, contentContainerStyle],
304
- showsVerticalScrollIndicator: showsVerticalScrollIndicator,
305
- animatedProps: animatedScrollProps,
306
- alwaysBounceVertical: false,
307
- overScrollMode: "always",
308
- onScroll: useAnimatedScrollHandler(event => {
309
- scrollY.value = event.contentOffset.y;
310
- }),
311
- scrollEventThrottle: 16,
312
- children: children
313
- })]
350
+ color: titleColor,
351
+ fontSize: titleSize,
352
+ fontWeight: titleWeight,
353
+ lineHeight: titleLineHeight,
354
+ marginBottom: titlePaddingBottom
355
+ }],
356
+ children: title
357
+ }), /*#__PURE__*/_jsx(AnimatedScrollView, {
358
+ ref: scrollRef,
359
+ style: [styles.content, contentStyle],
360
+ contentContainerStyle: [{
361
+ paddingBottom: paddingBottom + bottomInset,
362
+ gap: drawerGap,
363
+ flexDirection: 'column',
364
+ alignItems: 'stretch'
365
+ }, contentContainerStyle],
366
+ showsVerticalScrollIndicator: showsVerticalScrollIndicator,
367
+ animatedProps: animatedScrollProps,
368
+ alwaysBounceVertical: false,
369
+ overScrollMode: "always",
370
+ onScroll: useAnimatedScrollHandler(event => {
371
+ scrollY.value = event.contentOffset.y;
372
+ }),
373
+ scrollEventThrottle: 16,
374
+ children: children
375
+ })]
376
+ })
314
377
  })
315
378
  })
316
379
  });
@@ -328,7 +391,10 @@ const styles = StyleSheet.create({
328
391
  sheet: {
329
392
  width: '100%',
330
393
  position: 'absolute',
331
- top: 0,
394
+ top: 0
395
+ },
396
+ sheetInner: {
397
+ flex: 1,
332
398
  overflow: 'hidden'
333
399
  },
334
400
  handleArea: {
@@ -72,8 +72,13 @@ function resolveIconButtonTokens(modes, disabled) {
72
72
  * pressed transform mirrored via React state) — removed.
73
73
  * - Wrapped in `React.memo`.
74
74
  */
75
+ // Legacy default icon used when neither a `name` nor a `source` is supplied
76
+ // for the resolved slot. Kept as a constant rather than a destructuring
77
+ // default so source-only call sites don't accidentally render `'ic_card'`.
78
+ const LEGACY_DEFAULT_ICON_NAME = 'ic_card';
75
79
  function IconButton({
76
- iconName = 'ic_card',
80
+ iconName,
81
+ source,
77
82
  modes = EMPTY_MODES,
78
83
  onPress,
79
84
  disabled = false,
@@ -84,7 +89,9 @@ function IconButton({
84
89
  webAccessibilityProps,
85
90
  isToggle = false,
86
91
  activeIcon,
92
+ activeSource,
87
93
  inactiveIcon,
94
+ inactiveSource,
88
95
  isActive = false,
89
96
  ...rest
90
97
  }) {
@@ -107,11 +114,35 @@ function IconButton({
107
114
  userHandlersRef.current.onHoverIn = rest?.onHoverIn;
108
115
  userHandlersRef.current.onHoverOut = rest?.onHoverOut;
109
116
 
110
- // Determine which icon to display
111
- const finalIconName = isToggle ? isActive && activeIcon ? activeIcon : !isActive && inactiveIcon ? inactiveIcon : iconName : iconName;
117
+ // Resolve the active (name + source) pair for the current slot. Toggle
118
+ // mode picks active/inactive based on `isActive`; per-state overrides
119
+ // fall back to the default `iconName` / `source` when omitted. We then
120
+ // apply the legacy default icon only as a last resort, so a source-only
121
+ // call site (`<IconButton source="…" />`) renders the source instead of
122
+ // bleeding through to `'ic_card'`.
123
+ let resolvedIconName;
124
+ let resolvedSource;
125
+ if (isToggle) {
126
+ if (isActive) {
127
+ resolvedIconName = activeIcon ?? iconName;
128
+ resolvedSource = activeSource ?? source;
129
+ } else {
130
+ resolvedIconName = inactiveIcon ?? iconName;
131
+ resolvedSource = inactiveSource ?? source;
132
+ }
133
+ } else {
134
+ resolvedIconName = iconName;
135
+ resolvedSource = source;
136
+ }
137
+ if (!resolvedIconName && resolvedSource === undefined) {
138
+ resolvedIconName = LEGACY_DEFAULT_ICON_NAME;
139
+ }
112
140
 
113
- // Generate default accessibility label from icon name if not provided
114
- const defaultAccessibilityLabel = accessibilityLabel || iconName.replace(/^ic_/, '').replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
141
+ // Generate default accessibility label from the resolved icon name when
142
+ // possible. Source-only call sites should provide an explicit
143
+ // `accessibilityLabel`; we fall back to a generic 'Icon button' so we
144
+ // never crash on `iconName.replace(...)` when only a `source` is supplied.
145
+ const defaultAccessibilityLabel = accessibilityLabel || (resolvedIconName ? resolvedIconName.replace(/^ic_/, '').replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) : 'Icon button');
115
146
  const webProps = usePressableWebSupport({
116
147
  restProps: rest,
117
148
  onPress: disabled ? undefined : onPress,
@@ -164,7 +195,12 @@ function IconButton({
164
195
  style: styleCallback,
165
196
  ...webProps,
166
197
  children: /*#__PURE__*/_jsx(Icon, {
167
- name: finalIconName,
198
+ ...(resolvedIconName !== undefined ? {
199
+ name: resolvedIconName
200
+ } : {}),
201
+ ...(resolvedSource !== undefined ? {
202
+ source: resolvedSource
203
+ } : {}),
168
204
  size: tokens.iconSize,
169
205
  color: tokens.iconColor,
170
206
  accessibilityElementsHidden: true,
@@ -43,6 +43,7 @@ function resolveIconCapsuleTokens(modes) {
43
43
  * @component
44
44
  * @param {Object} props - Component props
45
45
  * @param {string} [props.iconName="ic_card"] - The name of the icon to display from the icon registry
46
+ * @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.
46
47
  * @param {Object} [props.modes={}] - Mode configuration for design tokens (e.g., {"Appearance": "Primary"})
47
48
  * @param {string} [props.accessibilityLabel] - Accessibility label for screen readers
48
49
  * @param {string} [props.accessibilityRole] - Accessibility role (defaults to "image" for decorative icons)
@@ -56,6 +57,7 @@ function resolveIconCapsuleTokens(modes) {
56
57
  */
57
58
  function IconCapsule({
58
59
  iconName = 'ic_card',
60
+ source,
59
61
  modes: propModes = EMPTY_MODES,
60
62
  // accessibilityLabel is accepted on the type for API back-compat but the
61
63
  // component intentionally renders `accessibilityLabel={undefined}` (icons
@@ -85,6 +87,9 @@ function IconCapsule({
85
87
  ...rest,
86
88
  children: /*#__PURE__*/_jsx(Icon, {
87
89
  name: iconName,
90
+ ...(source !== undefined ? {
91
+ source
92
+ } : {}),
88
93
  size: tokens.iconSize,
89
94
  color: tokens.iconColor,
90
95
  accessibilityElementsHidden: true,
@@ -106,12 +106,12 @@ const Popup = /*#__PURE__*/forwardRef(function Popup({
106
106
  children: /*#__PURE__*/_jsxs(View, {
107
107
  style: styles.overlay,
108
108
  children: [/*#__PURE__*/_jsx(Animated.View, {
109
- style: [StyleSheet.absoluteFillObject, {
109
+ style: [StyleSheet.absoluteFill, {
110
110
  backgroundColor: backdropColor,
111
111
  opacity: backdropAnim
112
112
  }],
113
113
  children: /*#__PURE__*/_jsx(Pressable, {
114
- style: StyleSheet.absoluteFillObject,
114
+ style: StyleSheet.absoluteFill,
115
115
  onPress: closeOnBackdropPress ? handleClose : undefined,
116
116
  accessibilityRole: "button",
117
117
  accessibilityLabel: "Close popup"