jfs-components 0.0.74 → 0.0.77
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 +92 -0
- package/lib/commonjs/components/ActionFooter/ActionFooter.js +147 -82
- package/lib/commonjs/components/Avatar/Avatar.js +20 -0
- package/lib/commonjs/components/Badge/Badge.js +23 -0
- package/lib/commonjs/components/Button/Button.js +37 -0
- package/lib/commonjs/components/IconButton/IconButton.js +20 -0
- package/lib/commonjs/components/Image/Image.js +26 -1
- package/lib/commonjs/components/LottiePlayer/LottiePlayer.js +116 -0
- package/lib/commonjs/components/LottiePlayer/LottiePlayer.web.js +82 -0
- package/lib/commonjs/components/LottiePlayer/loadNativeLottieView.js +74 -0
- package/lib/commonjs/components/LottiePlayer/loadWebLottieView.js +50 -0
- package/lib/commonjs/components/PageHero/PageHero.js +41 -5
- package/lib/commonjs/components/RechargeCard/RechargeCard.js +32 -17
- package/lib/commonjs/components/Text/Text.js +31 -1
- package/lib/commonjs/components/index.js +7 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/Icon.js +16 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/commonjs/index.js +12 -0
- package/lib/commonjs/skeleton/Skeleton.js +234 -0
- package/lib/commonjs/skeleton/SkeletonGroup.js +140 -0
- package/lib/commonjs/skeleton/index.js +58 -0
- package/lib/commonjs/skeleton/shimmer-tokens.js +189 -0
- package/lib/commonjs/skeleton/useReducedMotion.js +64 -0
- package/lib/module/components/ActionFooter/ActionFooter.js +146 -82
- package/lib/module/components/Avatar/Avatar.js +19 -0
- package/lib/module/components/Badge/Badge.js +23 -0
- package/lib/module/components/Button/Button.js +37 -0
- package/lib/module/components/IconButton/IconButton.js +20 -0
- package/lib/module/components/Image/Image.js +25 -1
- package/lib/module/components/LottiePlayer/LottiePlayer.js +111 -0
- package/lib/module/components/LottiePlayer/LottiePlayer.web.js +77 -0
- package/lib/module/components/LottiePlayer/loadNativeLottieView.js +69 -0
- package/lib/module/components/LottiePlayer/loadWebLottieView.js +45 -0
- package/lib/module/components/PageHero/PageHero.js +41 -5
- package/lib/module/components/RechargeCard/RechargeCard.js +33 -17
- package/lib/module/components/Text/Text.js +31 -1
- package/lib/module/components/index.js +1 -0
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/Icon.js +16 -0
- package/lib/module/icons/registry.js +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/skeleton/Skeleton.js +229 -0
- package/lib/module/skeleton/SkeletonGroup.js +133 -0
- package/lib/module/skeleton/index.js +6 -0
- package/lib/module/skeleton/shimmer-tokens.js +181 -0
- package/lib/module/skeleton/useReducedMotion.js +61 -0
- package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +26 -21
- package/lib/typescript/src/components/Avatar/Avatar.d.ts +7 -1
- package/lib/typescript/src/components/Badge/Badge.d.ts +7 -1
- package/lib/typescript/src/components/Button/Button.d.ts +8 -1
- package/lib/typescript/src/components/IconButton/IconButton.d.ts +7 -1
- package/lib/typescript/src/components/Image/Image.d.ts +8 -1
- package/lib/typescript/src/components/LottiePlayer/LottiePlayer.d.ts +85 -0
- package/lib/typescript/src/components/LottiePlayer/LottiePlayer.web.d.ts +28 -0
- package/lib/typescript/src/components/LottiePlayer/loadNativeLottieView.d.ts +11 -0
- package/lib/typescript/src/components/LottiePlayer/loadWebLottieView.d.ts +11 -0
- package/lib/typescript/src/components/PageHero/PageHero.d.ts +31 -5
- package/lib/typescript/src/components/Text/Text.d.ts +20 -1
- package/lib/typescript/src/components/index.d.ts +1 -0
- package/lib/typescript/src/icons/Icon.d.ts +7 -1
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/skeleton/Skeleton.d.ts +60 -0
- package/lib/typescript/src/skeleton/SkeletonGroup.d.ts +78 -0
- package/lib/typescript/src/skeleton/index.d.ts +5 -0
- package/lib/typescript/src/skeleton/shimmer-tokens.d.ts +160 -0
- package/lib/typescript/src/skeleton/useReducedMotion.d.ts +15 -0
- package/package.json +11 -1
- package/src/components/ActionFooter/ActionFooter.tsx +152 -86
- package/src/components/Avatar/Avatar.tsx +26 -0
- package/src/components/Badge/Badge.tsx +27 -0
- package/src/components/Button/Button.tsx +40 -0
- package/src/components/IconButton/IconButton.tsx +27 -0
- package/src/components/Image/Image.tsx +25 -0
- package/src/components/LottiePlayer/LottiePlayer.tsx +145 -0
- package/src/components/LottiePlayer/LottiePlayer.web.tsx +94 -0
- package/src/components/LottiePlayer/loadNativeLottieView.tsx +87 -0
- package/src/components/LottiePlayer/loadWebLottieView.tsx +64 -0
- package/src/components/PageHero/PageHero.tsx +61 -4
- package/src/components/RechargeCard/RechargeCard.tsx +32 -24
- package/src/components/Text/Text.tsx +54 -0
- package/src/components/index.ts +1 -0
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/icons/Icon.tsx +17 -0
- package/src/icons/registry.ts +1 -1
- package/src/index.ts +1 -0
- package/src/skeleton/Skeleton.tsx +298 -0
- package/src/skeleton/SkeletonGroup.tsx +193 -0
- package/src/skeleton/index.ts +10 -0
- package/src/skeleton/shimmer-tokens.ts +221 -0
- package/src/skeleton/useReducedMotion.ts +72 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.useReducedMotion = useReducedMotion;
|
|
7
|
+
var _react = require("react");
|
|
8
|
+
var _reactNative = require("react-native");
|
|
9
|
+
/**
|
|
10
|
+
* Cross-platform "prefers reduced motion" hook.
|
|
11
|
+
*
|
|
12
|
+
* - Native: reads `AccessibilityInfo.isReduceMotionEnabled()` and subscribes
|
|
13
|
+
* to `reduceMotionChanged` events so the value stays live as the user
|
|
14
|
+
* toggles the OS setting.
|
|
15
|
+
* - Web: uses `window.matchMedia('(prefers-reduced-motion: reduce)')`,
|
|
16
|
+
* subscribing to its `change` event.
|
|
17
|
+
* - Anywhere either API is missing: returns `false` (no reduction).
|
|
18
|
+
*
|
|
19
|
+
* The hook never throws — every native API access is defensively guarded so
|
|
20
|
+
* the skeleton system stays safe in tests, SSR, and constrained sandboxes.
|
|
21
|
+
*/
|
|
22
|
+
function useReducedMotion() {
|
|
23
|
+
const [reduced, setReduced] = (0, _react.useState)(false);
|
|
24
|
+
(0, _react.useEffect)(() => {
|
|
25
|
+
let cancelled = false;
|
|
26
|
+
if (_reactNative.Platform.OS === 'web') {
|
|
27
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const mql = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
31
|
+
const update = matches => {
|
|
32
|
+
if (!cancelled) setReduced(matches);
|
|
33
|
+
};
|
|
34
|
+
update(mql.matches);
|
|
35
|
+
const listener = e => update(e.matches);
|
|
36
|
+
if (typeof mql.addEventListener === 'function') {
|
|
37
|
+
mql.addEventListener('change', listener);
|
|
38
|
+
return () => {
|
|
39
|
+
cancelled = true;
|
|
40
|
+
mql.removeEventListener('change', listener);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const legacyMql = mql;
|
|
44
|
+
legacyMql.addListener?.(listener);
|
|
45
|
+
return () => {
|
|
46
|
+
cancelled = true;
|
|
47
|
+
legacyMql.removeListener?.(listener);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (typeof _reactNative.AccessibilityInfo?.isReduceMotionEnabled === 'function') {
|
|
51
|
+
_reactNative.AccessibilityInfo.isReduceMotionEnabled().then(value => {
|
|
52
|
+
if (!cancelled) setReduced(!!value);
|
|
53
|
+
}).catch(() => {});
|
|
54
|
+
}
|
|
55
|
+
const sub = typeof _reactNative.AccessibilityInfo?.addEventListener === 'function' ? _reactNative.AccessibilityInfo.addEventListener('reduceMotionChanged', value => {
|
|
56
|
+
if (!cancelled) setReduced(!!value);
|
|
57
|
+
}) : null;
|
|
58
|
+
return () => {
|
|
59
|
+
cancelled = true;
|
|
60
|
+
sub?.remove?.();
|
|
61
|
+
};
|
|
62
|
+
}, []);
|
|
63
|
+
return reduced;
|
|
64
|
+
}
|
|
@@ -1,35 +1,101 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React from 'react';
|
|
3
|
+
import React, { useMemo } from 'react';
|
|
4
4
|
import { View, Platform } from 'react-native';
|
|
5
5
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
6
|
import { EMPTY_MODES, cloneChildrenWithModes, flattenChildren } from '../../utils/react-utils';
|
|
7
7
|
import IconButton from '../IconButton/IconButton';
|
|
8
8
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
|
+
const IS_WEB = Platform.OS === 'web';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Yoga-safe stretch
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
//
|
|
15
|
+
// React Native (Yoga) interprets the `flex: 1` shorthand as
|
|
16
|
+
// { flexGrow: 1, flexShrink: 1, flexBasis: 0 }
|
|
17
|
+
// which is the *equal-share* variant. That is the correct math for what we
|
|
18
|
+
// want here (equal-width action buttons), BUT Yoga has a well-known foot-gun
|
|
19
|
+
// when this child sits inside a parent whose main-axis size hasn't been
|
|
20
|
+
// resolved yet on the first layout pass: the child collapses to 0 and the
|
|
21
|
+
// inner text gets clipped to "" before the parent ever measures.
|
|
22
|
+
//
|
|
23
|
+
// The defensive incantation used elsewhere in this codebase (see
|
|
24
|
+
// `CardCTA.leftWrap` and the `MediaCard.Header` fix in CHANGELOG.md) is to
|
|
25
|
+
// keep the equal-share math but explicitly clamp `minWidth` to 0 so Yoga
|
|
26
|
+
// always allows the child to participate in the shrink algorithm, even when
|
|
27
|
+
// the parent itself is in an undetermined state. Combined with explicit
|
|
28
|
+
// `flexGrow`/`flexShrink`/`flexBasis` (NOT the `flex` shorthand) this
|
|
29
|
+
// renders correctly on iOS, Android, and Web — and crucially never produces
|
|
30
|
+
// the "buttons render as empty pills" failure mode the previous version had
|
|
31
|
+
// on iOS dev clients.
|
|
32
|
+
const STRETCH_STYLE = {
|
|
33
|
+
flexGrow: 1,
|
|
34
|
+
flexShrink: 1,
|
|
35
|
+
flexBasis: 0,
|
|
36
|
+
minWidth: 0
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Platform-specific drop shadow. Web boxShadow can't go through
|
|
40
|
+
// Platform.select (RN's typed surface doesn't include it) so we keep it as a
|
|
41
|
+
// separate constant and append it below.
|
|
42
|
+
const NATIVE_SHADOW = Platform.select({
|
|
43
|
+
ios: {
|
|
44
|
+
shadowColor: '#0c0d10',
|
|
45
|
+
shadowOffset: {
|
|
46
|
+
width: 0,
|
|
47
|
+
height: -12
|
|
48
|
+
},
|
|
49
|
+
shadowOpacity: 0.16,
|
|
50
|
+
shadowRadius: 24
|
|
51
|
+
},
|
|
52
|
+
android: {
|
|
53
|
+
elevation: 16
|
|
54
|
+
},
|
|
55
|
+
default: {}
|
|
56
|
+
});
|
|
57
|
+
const WEB_SHADOW = IS_WEB ? {
|
|
58
|
+
boxShadow: '0px -12px 24px 0px rgba(12, 13, 16, 0.12), 0px -16px 48px 0px rgba(12, 13, 16, 0.16)'
|
|
59
|
+
} : null;
|
|
60
|
+
|
|
61
|
+
// The runtime token a slot child must equal (by reference) to be treated as
|
|
62
|
+
// an IconButton. `IconButton` is exported wrapped in `React.memo`, so the
|
|
63
|
+
// element.type identity comparison works for both `<IconButton />` from the
|
|
64
|
+
// same module and any `React.memo`-wrapped re-export. The fallback check
|
|
65
|
+
// (`type.type === IconButton`) catches one extra layer of `forwardRef` /
|
|
66
|
+
// `memo` wrapping which can happen when consumers re-export the component.
|
|
67
|
+
function isIconButtonElement(element) {
|
|
68
|
+
const t = element.type;
|
|
69
|
+
if (t === IconButton) return true;
|
|
70
|
+
if (t && typeof t === 'object' && t.type === IconButton) return true;
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
9
74
|
/**
|
|
10
|
-
* ActionFooter
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
75
|
+
* ActionFooter — a sticky bottom container for primary screen actions.
|
|
76
|
+
*
|
|
77
|
+
* Layout contract:
|
|
78
|
+
* - The outer container stretches horizontally (`alignSelf: 'stretch'`) so
|
|
79
|
+
* it fills the parent regardless of whether the parent is a flex column,
|
|
80
|
+
* a ScrollView contentContainer, or a plain View.
|
|
81
|
+
* - The inner slot is a single row sized by its tallest child. It does NOT
|
|
82
|
+
* use `flex: 1` — that previously caused the row to collapse to zero on
|
|
83
|
+
* the first Yoga pass on native, taking the button labels with it.
|
|
84
|
+
* - `IconButton` children keep their intrinsic square size.
|
|
85
|
+
* - Every other child is auto-stretched with the Yoga-safe stretch style
|
|
86
|
+
* above so two `<Button>` siblings render at equal width on iOS, Android,
|
|
87
|
+
* and Web.
|
|
88
|
+
*
|
|
89
|
+
* The `modes` prop is automatically pushed down to every slot child via
|
|
90
|
+
* {@link cloneChildrenWithModes}; explicit child-level modes win over the
|
|
91
|
+
* parent's modes.
|
|
92
|
+
*
|
|
25
93
|
* @example
|
|
26
94
|
* ```tsx
|
|
27
|
-
* // Basic usage - modes are automatically passed to all children.
|
|
28
|
-
* // Non-IconButton children (e.g., Button) are auto-stretched to fill.
|
|
29
95
|
* <ActionFooter modes={modes}>
|
|
30
96
|
* <IconButton iconName="ic_split" />
|
|
31
|
-
* <Button label="Request" />
|
|
32
|
-
* <Button label="Pay" />
|
|
97
|
+
* <Button label="Request" modes={{ AppearanceBrand: 'Secondary' }} />
|
|
98
|
+
* <Button label="Pay" modes={{ AppearanceBrand: 'Primary' }} />
|
|
33
99
|
* </ActionFooter>
|
|
34
100
|
* ```
|
|
35
101
|
*/
|
|
@@ -37,76 +103,74 @@ function ActionFooter({
|
|
|
37
103
|
children,
|
|
38
104
|
modes = EMPTY_MODES,
|
|
39
105
|
style,
|
|
40
|
-
accessibilityLabel
|
|
106
|
+
accessibilityLabel
|
|
41
107
|
}) {
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
const containerStyle = {
|
|
68
|
-
backgroundColor,
|
|
69
|
-
paddingLeft: paddingHorizontal,
|
|
70
|
-
paddingRight: paddingHorizontal,
|
|
71
|
-
paddingTop,
|
|
72
|
-
paddingBottom,
|
|
73
|
-
...shadowStyle
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
// Slot container style for horizontal layout of action items
|
|
77
|
-
const slotStyle = {
|
|
78
|
-
flexDirection: 'row',
|
|
79
|
-
alignItems: 'flex-start',
|
|
80
|
-
gap,
|
|
81
|
-
flex: 1
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
// Web-specific box-shadow
|
|
85
|
-
const webShadow = Platform.OS === 'web' ? {
|
|
86
|
-
boxShadow: '0px -12px 24px 0px rgba(12, 13, 16, 0.12), 0px -16px 48px 0px rgba(12, 13, 16, 0.16)'
|
|
87
|
-
} : {};
|
|
88
|
-
const flatChildren = flattenChildren(children);
|
|
89
|
-
const processedChildren = cloneChildrenWithModes(flatChildren, modes);
|
|
90
|
-
const enhancedChildren = processedChildren.map((child, index) => {
|
|
91
|
-
if (! /*#__PURE__*/React.isValidElement(child)) return child;
|
|
92
|
-
const element = child;
|
|
93
|
-
const isIconButton = element.type === IconButton;
|
|
94
|
-
const stretchStyle = isIconButton ? undefined : {
|
|
95
|
-
flex: 1
|
|
108
|
+
// All token reads collapsed into a single useMemo keyed on `modes`. With
|
|
109
|
+
// the shared `EMPTY_MODES` default this resolves once for the common path
|
|
110
|
+
// and never re-allocates the container/slot style objects between renders.
|
|
111
|
+
const {
|
|
112
|
+
containerStyle,
|
|
113
|
+
slotStyle
|
|
114
|
+
} = useMemo(() => {
|
|
115
|
+
const backgroundColor = getVariableByName('actionFooter/background', modes) ?? '#ffffff';
|
|
116
|
+
const gap = getVariableByName('actionFooter/gap', modes) ?? 8;
|
|
117
|
+
const paddingHorizontal = getVariableByName('actionFooter/padding/horizontal', modes) ?? 16;
|
|
118
|
+
const paddingTop = getVariableByName('actionFooter/padding/top', modes) ?? 10;
|
|
119
|
+
const paddingBottom = getVariableByName('actionFooter/padding/bottom', modes) ?? 41;
|
|
120
|
+
const container = {
|
|
121
|
+
// `alignSelf: 'stretch'` is the cross-platform way to ask "fill the
|
|
122
|
+
// parent's cross axis" — in the common case (column parent) this gives
|
|
123
|
+
// us full-width without the caller needing to pass `width: '100%'`.
|
|
124
|
+
alignSelf: 'stretch',
|
|
125
|
+
backgroundColor,
|
|
126
|
+
paddingLeft: paddingHorizontal,
|
|
127
|
+
paddingRight: paddingHorizontal,
|
|
128
|
+
paddingTop,
|
|
129
|
+
paddingBottom,
|
|
130
|
+
...NATIVE_SHADOW
|
|
96
131
|
};
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
132
|
+
const slot = {
|
|
133
|
+
flexDirection: 'row',
|
|
134
|
+
// Vertically center the IconButton against the slightly taller Buttons
|
|
135
|
+
// so the row reads as a single optical baseline.
|
|
136
|
+
alignItems: 'center',
|
|
137
|
+
gap
|
|
138
|
+
};
|
|
139
|
+
return {
|
|
140
|
+
containerStyle: container,
|
|
141
|
+
slotStyle: slot
|
|
142
|
+
};
|
|
143
|
+
}, [modes]);
|
|
144
|
+
|
|
145
|
+
// Process children once per (children, modes) tuple:
|
|
146
|
+
// 1. Flatten Fragments so each action is its own keyed sibling.
|
|
147
|
+
// 2. Push `modes` down so callers don't have to thread it manually.
|
|
148
|
+
// 3. Auto-stretch every non-IconButton with the Yoga-safe stretch style.
|
|
149
|
+
//
|
|
150
|
+
// The result identity is stable across re-renders when the inputs don't
|
|
151
|
+
// change, which keeps the `React.memo`-wrapped Button/IconButton children
|
|
152
|
+
// from re-rendering for no reason.
|
|
153
|
+
const enhancedChildren = useMemo(() => {
|
|
154
|
+
const flat = flattenChildren(children);
|
|
155
|
+
const withModes = cloneChildrenWithModes(flat, modes);
|
|
156
|
+
return withModes.map((child, index) => {
|
|
157
|
+
if (! /*#__PURE__*/React.isValidElement(child)) return child;
|
|
158
|
+
const element = child;
|
|
159
|
+
if (isIconButtonElement(element)) return element;
|
|
160
|
+
return /*#__PURE__*/React.cloneElement(element, {
|
|
161
|
+
key: element.key ?? `action-footer-item-${index}`,
|
|
162
|
+
style: [STRETCH_STYLE, element.props.style]
|
|
163
|
+
});
|
|
100
164
|
});
|
|
101
|
-
});
|
|
165
|
+
}, [children, modes]);
|
|
102
166
|
return /*#__PURE__*/_jsx(View, {
|
|
103
|
-
style: [containerStyle,
|
|
167
|
+
style: [containerStyle, WEB_SHADOW, style],
|
|
104
168
|
accessibilityRole: "toolbar",
|
|
105
|
-
accessibilityLabel:
|
|
169
|
+
accessibilityLabel: accessibilityLabel,
|
|
106
170
|
children: /*#__PURE__*/_jsx(View, {
|
|
107
171
|
style: slotStyle,
|
|
108
172
|
children: enhancedChildren
|
|
109
173
|
})
|
|
110
174
|
});
|
|
111
175
|
}
|
|
112
|
-
export default ActionFooter;
|
|
176
|
+
export default /*#__PURE__*/React.memo(ActionFooter);
|
|
@@ -4,6 +4,8 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|
|
4
4
|
import { Pressable, View, Image, Text, Platform } from 'react-native';
|
|
5
5
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
6
|
import { EMPTY_MODES } from '../../utils/react-utils';
|
|
7
|
+
import Skeleton from '../../skeleton/Skeleton';
|
|
8
|
+
import { useSkeleton } from '../../skeleton/SkeletonGroup';
|
|
7
9
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
8
10
|
const avatarImage = require('./31595e70c4181263f9971590224b12934b280c9b.png');
|
|
9
11
|
|
|
@@ -123,11 +125,19 @@ function Avatar({
|
|
|
123
125
|
// component intentionally renders `accessibilityLabel={undefined}` on the
|
|
124
126
|
// wrapper (the inner Text/Image carry the label instead).
|
|
125
127
|
accessibilityLabel: _accessibilityLabel,
|
|
128
|
+
loading,
|
|
126
129
|
...rest
|
|
127
130
|
}) {
|
|
128
131
|
const isMonogram = style === 'Monogram';
|
|
129
132
|
const tokens = useMemo(() => resolveAvatarTokens(modes, isMonogram), [modes, isMonogram]);
|
|
130
133
|
|
|
134
|
+
// Skeleton context — read unconditionally; the actual short-circuit
|
|
135
|
+
// happens AFTER all remaining hooks below.
|
|
136
|
+
const {
|
|
137
|
+
active: groupActive
|
|
138
|
+
} = useSkeleton();
|
|
139
|
+
const isLoading = loading ?? groupActive;
|
|
140
|
+
|
|
131
141
|
// Focus is a sustained visible state — keep mirroring on web; gate the
|
|
132
142
|
// setter so it never fires on native (where focus events don't fire on
|
|
133
143
|
// these elements anyway).
|
|
@@ -158,6 +168,15 @@ function Avatar({
|
|
|
158
168
|
pressed
|
|
159
169
|
}) => [tokens.containerStyle, pressed ? pressedOverlayStyle : null, isFocused ? focusOverlayStyle : null], [tokens.containerStyle, isFocused]);
|
|
160
170
|
const staticContainerStyle = useMemo(() => [tokens.containerStyle, isFocused ? focusOverlayStyle : null], [tokens.containerStyle, isFocused]);
|
|
171
|
+
if (isLoading) {
|
|
172
|
+
const size = tokens.containerStyle.width;
|
|
173
|
+
return /*#__PURE__*/_jsx(Skeleton, {
|
|
174
|
+
kind: "other",
|
|
175
|
+
width: size,
|
|
176
|
+
height: size,
|
|
177
|
+
modes: modes
|
|
178
|
+
});
|
|
179
|
+
}
|
|
161
180
|
|
|
162
181
|
// The inner content varies; everything else (wrapper, handlers, style) is shared.
|
|
163
182
|
const innerContent = isMonogram ? /*#__PURE__*/_jsx(View, {
|
|
@@ -4,6 +4,8 @@ import React from 'react';
|
|
|
4
4
|
import { View, Text, Pressable } from 'react-native';
|
|
5
5
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
6
|
import { EMPTY_MODES } from '../../utils/react-utils';
|
|
7
|
+
import Skeleton from '../../skeleton/Skeleton';
|
|
8
|
+
import { useSkeleton } from '../../skeleton/SkeletonGroup';
|
|
7
9
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
8
10
|
function Badge({
|
|
9
11
|
label = 'Label',
|
|
@@ -12,6 +14,7 @@ function Badge({
|
|
|
12
14
|
accessibilityLabel,
|
|
13
15
|
style,
|
|
14
16
|
labelStyle,
|
|
17
|
+
loading,
|
|
15
18
|
...rest
|
|
16
19
|
}) {
|
|
17
20
|
// Resolve token values (fall back to sensible defaults)
|
|
@@ -24,6 +27,26 @@ function Badge({
|
|
|
24
27
|
const paddingVertical = Number(getVariableByName('badge/padding/vertical', modes)) || 4;
|
|
25
28
|
const borderRadius = Number(getVariableByName('badge/radius', modes)) || 4;
|
|
26
29
|
const lineHeight = Number(getVariableByName('badge/label/lineHeight', modes)) || Math.round(fontSize * 1.2);
|
|
30
|
+
|
|
31
|
+
// Skeleton short-circuit. Size derived from the same tokens the loaded
|
|
32
|
+
// badge would use so the placeholder occupies the same box.
|
|
33
|
+
const {
|
|
34
|
+
active: groupActive
|
|
35
|
+
} = useSkeleton();
|
|
36
|
+
const isLoading = loading ?? groupActive;
|
|
37
|
+
if (isLoading) {
|
|
38
|
+
const charWidth = fontSize * 0.55;
|
|
39
|
+
const labelWidth = Math.max(label.length, 3) * charWidth;
|
|
40
|
+
return /*#__PURE__*/_jsx(Skeleton, {
|
|
41
|
+
kind: "badge",
|
|
42
|
+
width: paddingHorizontal * 2 + labelWidth,
|
|
43
|
+
height: paddingVertical * 2 + lineHeight,
|
|
44
|
+
style: {
|
|
45
|
+
alignSelf: 'flex-start'
|
|
46
|
+
},
|
|
47
|
+
modes: modes
|
|
48
|
+
});
|
|
49
|
+
}
|
|
27
50
|
const Container = onPress ? Pressable : View;
|
|
28
51
|
const containerStyle = {
|
|
29
52
|
backgroundColor,
|
|
@@ -6,6 +6,8 @@ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
|
6
6
|
import { usePressableWebSupport } from '../../utils/web-platform-utils';
|
|
7
7
|
import { EMPTY_MODES } from '../../utils/react-utils';
|
|
8
8
|
import Icon from '../../icons/Icon';
|
|
9
|
+
import Skeleton from '../../skeleton/Skeleton';
|
|
10
|
+
import { useSkeleton } from '../../skeleton/SkeletonGroup';
|
|
9
11
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
10
12
|
// ---------------------------------------------------------------------------
|
|
11
13
|
// Module-scope constants — never re-allocated per render.
|
|
@@ -167,6 +169,7 @@ function ButtonImpl({
|
|
|
167
169
|
accessibilityHint,
|
|
168
170
|
accessibilityState,
|
|
169
171
|
webAccessibilityProps,
|
|
172
|
+
loading,
|
|
170
173
|
...rest
|
|
171
174
|
}) {
|
|
172
175
|
// Hover state is web-only in practice; the setter is gated so native taps
|
|
@@ -182,6 +185,14 @@ function ButtonImpl({
|
|
|
182
185
|
userHandlersRef.current.onHoverOut = rest?.onHoverOut;
|
|
183
186
|
const tokens = useMemo(() => resolveButtonTokens(modes, disabled), [modes, disabled]);
|
|
184
187
|
|
|
188
|
+
// Skeleton context — read unconditionally so React's hook order stays
|
|
189
|
+
// stable. The actual short-circuit return happens AFTER all remaining
|
|
190
|
+
// hooks have been called below.
|
|
191
|
+
const {
|
|
192
|
+
active: groupActive
|
|
193
|
+
} = useSkeleton();
|
|
194
|
+
const isLoading = loading ?? groupActive;
|
|
195
|
+
|
|
185
196
|
// Active label color: base by default; hover override (web-only) when hovered.
|
|
186
197
|
// Press color is intentionally NOT applied to the label on native — applying
|
|
187
198
|
// it would require a React render per touch and re-introduce the flicker.
|
|
@@ -259,6 +270,32 @@ function ButtonImpl({
|
|
|
259
270
|
console.warn('[Button] Custom content is used without an explicit `accessibilityLabel` or string `label`. ' + 'Screen readers may not announce this button correctly.');
|
|
260
271
|
}
|
|
261
272
|
}
|
|
273
|
+
if (isLoading) {
|
|
274
|
+
const {
|
|
275
|
+
container,
|
|
276
|
+
baseLabel,
|
|
277
|
+
iconSize,
|
|
278
|
+
accessoryOffset
|
|
279
|
+
} = tokens;
|
|
280
|
+
const paddingHorizontal = container.paddingHorizontal ?? 20;
|
|
281
|
+
const paddingVertical = container.paddingVertical ?? 12;
|
|
282
|
+
const lineHeight = baseLabel.lineHeight ?? 19;
|
|
283
|
+
const fontSize = baseLabel.fontSize ?? 16;
|
|
284
|
+
const labelText = typeof label === 'string' ? label : 'Button';
|
|
285
|
+
const charWidth = fontSize * 0.55;
|
|
286
|
+
const labelWidth = Math.max(labelText.length, 4) * charWidth;
|
|
287
|
+
const hasAccessory = !!(leading || trailing || icon);
|
|
288
|
+
const accessoryWidth = hasAccessory ? iconSize + accessoryOffset * 2 : 0;
|
|
289
|
+
const skeletonWidth = paddingHorizontal * 2 + labelWidth + accessoryWidth;
|
|
290
|
+
const skeletonHeight = paddingVertical * 2 + lineHeight;
|
|
291
|
+
return /*#__PURE__*/_jsx(Skeleton, {
|
|
292
|
+
kind: "other",
|
|
293
|
+
width: skeletonWidth,
|
|
294
|
+
height: skeletonHeight,
|
|
295
|
+
style: style,
|
|
296
|
+
modes: modes
|
|
297
|
+
});
|
|
298
|
+
}
|
|
262
299
|
return /*#__PURE__*/_jsxs(Pressable, {
|
|
263
300
|
accessibilityRole: "button",
|
|
264
301
|
accessibilityLabel: undefined,
|
|
@@ -6,6 +6,8 @@ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
|
6
6
|
import Icon from '../../icons/Icon';
|
|
7
7
|
import { usePressableWebSupport } from '../../utils/web-platform-utils';
|
|
8
8
|
import { EMPTY_MODES } from '../../utils/react-utils';
|
|
9
|
+
import Skeleton from '../../skeleton/Skeleton';
|
|
10
|
+
import { useSkeleton } from '../../skeleton/SkeletonGroup';
|
|
9
11
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
10
12
|
// ---------------------------------------------------------------------------
|
|
11
13
|
// Module-scope constants
|
|
@@ -93,6 +95,7 @@ function IconButton({
|
|
|
93
95
|
inactiveIcon,
|
|
94
96
|
inactiveSource,
|
|
95
97
|
isActive = false,
|
|
98
|
+
loading,
|
|
96
99
|
...rest
|
|
97
100
|
}) {
|
|
98
101
|
// Merge explicit props with modes for token resolution. Memoize the merged
|
|
@@ -104,6 +107,13 @@ function IconButton({
|
|
|
104
107
|
isActive
|
|
105
108
|
}), [modes, isToggle, isActive]);
|
|
106
109
|
const tokens = useMemo(() => resolveIconButtonTokens(componentModes, disabled), [componentModes, disabled]);
|
|
110
|
+
|
|
111
|
+
// Hook called unconditionally — short-circuit below comes AFTER all hooks
|
|
112
|
+
// to keep React's hook order stable across renders.
|
|
113
|
+
const {
|
|
114
|
+
active: groupActive
|
|
115
|
+
} = useSkeleton();
|
|
116
|
+
const isLoading = loading ?? groupActive;
|
|
107
117
|
const [isFocused, setIsFocused] = useState(false);
|
|
108
118
|
const [isHovered, setIsHovered] = useState(false);
|
|
109
119
|
const userHandlersRef = useRef({});
|
|
@@ -175,6 +185,16 @@ function IconButton({
|
|
|
175
185
|
const styleCallback = useCallback(({
|
|
176
186
|
pressed
|
|
177
187
|
}) => [tokens.baseContainerStyle, style, pressed && !disabled ? pressedOverlayStyle : null, isHovered && !disabled ? hoverOverlayStyle : null, isFocused && !disabled ? focusOverlayStyle : null], [tokens.baseContainerStyle, style, isHovered, isFocused, disabled]);
|
|
188
|
+
if (isLoading) {
|
|
189
|
+
const size = tokens.baseContainerStyle.width;
|
|
190
|
+
return /*#__PURE__*/_jsx(Skeleton, {
|
|
191
|
+
kind: "other",
|
|
192
|
+
width: size,
|
|
193
|
+
height: size,
|
|
194
|
+
style: style,
|
|
195
|
+
modes: componentModes
|
|
196
|
+
});
|
|
197
|
+
}
|
|
178
198
|
return /*#__PURE__*/_jsx(Pressable, {
|
|
179
199
|
accessibilityRole: "button",
|
|
180
200
|
accessibilityLabel: undefined,
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useMemo } from 'react';
|
|
4
4
|
import { Image as RNImage, View } from 'react-native';
|
|
5
|
+
import Skeleton from '../../skeleton/Skeleton';
|
|
6
|
+
import { useSkeleton } from '../../skeleton/SkeletonGroup';
|
|
5
7
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
6
8
|
function normalizeSource(imageSource) {
|
|
7
9
|
if (imageSource == null) return undefined;
|
|
@@ -41,7 +43,8 @@ function Image({
|
|
|
41
43
|
style,
|
|
42
44
|
accessibilityLabel,
|
|
43
45
|
accessibilityElementsHidden,
|
|
44
|
-
importantForAccessibility
|
|
46
|
+
importantForAccessibility,
|
|
47
|
+
loading
|
|
45
48
|
}) {
|
|
46
49
|
const source = useMemo(() => normalizeSource(imageSource), [imageSource]);
|
|
47
50
|
const layoutStyle = useMemo(() => {
|
|
@@ -63,6 +66,27 @@ function Image({
|
|
|
63
66
|
if (borderRadius != null) s.borderRadius = borderRadius;
|
|
64
67
|
return s;
|
|
65
68
|
}, [ratio, width, height, borderRadius]);
|
|
69
|
+
const {
|
|
70
|
+
active: groupActive
|
|
71
|
+
} = useSkeleton();
|
|
72
|
+
const isLoading = loading ?? groupActive;
|
|
73
|
+
if (isLoading) {
|
|
74
|
+
// Match the loaded image's exact box. If height is unknown but a ratio
|
|
75
|
+
// is set, the skeleton uses `aspectRatio` the same way the loaded image
|
|
76
|
+
// would, so layout never jumps when the load resolves.
|
|
77
|
+
const skeletonStyle = {
|
|
78
|
+
width: width ?? '100%',
|
|
79
|
+
...(height != null ? {
|
|
80
|
+
height: height
|
|
81
|
+
} : {
|
|
82
|
+
aspectRatio: ratio
|
|
83
|
+
})
|
|
84
|
+
};
|
|
85
|
+
return /*#__PURE__*/_jsx(Skeleton, {
|
|
86
|
+
kind: "image",
|
|
87
|
+
style: skeletonStyle
|
|
88
|
+
});
|
|
89
|
+
}
|
|
66
90
|
if (!source) {
|
|
67
91
|
return /*#__PURE__*/_jsx(View, {
|
|
68
92
|
style: [layoutStyle, style]
|