@retray-dev/ui-kit 7.0.1 → 9.1.0
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/COMPONENTS.md +567 -14
- package/EXAMPLES.md +21 -14
- package/README.md +14 -8
- package/dist/Accordion.js +57 -5
- package/dist/Accordion.mjs +4 -3
- package/dist/AlertBanner.js +4 -1
- package/dist/AlertBanner.mjs +3 -2
- package/dist/AppHeader.d.mts +40 -0
- package/dist/AppHeader.d.ts +40 -0
- package/dist/AppHeader.js +515 -0
- package/dist/AppHeader.mjs +10 -0
- package/dist/Avatar.js +39 -29
- package/dist/Avatar.mjs +2 -1
- package/dist/Badge.js +11 -1
- package/dist/Badge.mjs +2 -1
- package/dist/Button.d.mts +8 -3
- package/dist/Button.d.ts +8 -3
- package/dist/Button.js +126 -108
- package/dist/Button.mjs +6 -5
- package/dist/ButtonGroup.mjs +1 -0
- package/dist/Card.js +90 -70
- package/dist/Card.mjs +5 -4
- package/dist/CategoryStrip.js +79 -22
- package/dist/CategoryStrip.mjs +6 -6
- package/dist/Checkbox.js +118 -86
- package/dist/Checkbox.mjs +5 -5
- package/dist/Chip.js +113 -80
- package/dist/Chip.mjs +5 -5
- package/dist/ConfirmDialog.js +140 -110
- package/dist/ConfirmDialog.mjs +7 -6
- package/dist/CurrencyDisplay.mjs +1 -0
- package/dist/CurrencyInput.d.mts +1 -1
- package/dist/CurrencyInput.d.ts +1 -1
- package/dist/CurrencyInput.js +9 -5
- package/dist/CurrencyInput.mjs +5 -4
- package/dist/DetailRow.mjs +1 -0
- package/dist/EmptyState.js +131 -111
- package/dist/EmptyState.mjs +7 -6
- package/dist/ErrorBoundary.d.mts +42 -0
- package/dist/ErrorBoundary.d.ts +42 -0
- package/dist/ErrorBoundary.js +351 -0
- package/dist/ErrorBoundary.mjs +7 -0
- package/dist/Form.mjs +1 -0
- package/dist/HolographicCard.d.mts +55 -0
- package/dist/HolographicCard.d.ts +55 -0
- package/dist/HolographicCard.js +316 -0
- package/dist/HolographicCard.mjs +191 -0
- package/dist/IconButton.d.mts +8 -3
- package/dist/IconButton.d.ts +8 -3
- package/dist/IconButton.js +115 -98
- package/dist/IconButton.mjs +5 -4
- package/dist/ImageViewer.d.mts +23 -0
- package/dist/ImageViewer.d.ts +23 -0
- package/dist/ImageViewer.js +582 -0
- package/dist/ImageViewer.mjs +8 -0
- package/dist/Input.mjs +4 -3
- package/dist/LabelValue.mjs +1 -0
- package/dist/ListGroup.mjs +1 -0
- package/dist/ListItem.js +131 -117
- package/dist/ListItem.mjs +6 -5
- package/dist/MediaCard.js +54 -6
- package/dist/MediaCard.mjs +6 -5
- package/dist/MenuGroup.mjs +1 -0
- package/dist/MenuItem.js +91 -79
- package/dist/MenuItem.mjs +6 -5
- package/dist/MonthPicker.d.mts +10 -2
- package/dist/MonthPicker.d.ts +10 -2
- package/dist/MonthPicker.js +80 -17
- package/dist/MonthPicker.mjs +3 -2
- package/dist/PagerDots.d.mts +35 -0
- package/dist/PagerDots.d.ts +35 -0
- package/dist/PagerDots.js +392 -0
- package/dist/PagerDots.mjs +7 -0
- package/dist/Pressable.d.mts +5 -5
- package/dist/Pressable.d.ts +5 -5
- package/dist/Pressable.js +97 -86
- package/dist/Pressable.mjs +5 -4
- package/dist/PricingCard.d.mts +50 -0
- package/dist/PricingCard.d.ts +50 -0
- package/dist/PricingCard.js +636 -0
- package/dist/PricingCard.mjs +11 -0
- package/dist/Progress.mjs +3 -2
- package/dist/RadioGroup.js +81 -30
- package/dist/RadioGroup.mjs +5 -5
- package/dist/RetrayProvider.d.mts +2 -0
- package/dist/RetrayProvider.d.ts +2 -0
- package/dist/RetrayProvider.js +214 -0
- package/dist/RetrayProvider.mjs +5 -0
- package/dist/Select.js +51 -4
- package/dist/Select.mjs +5 -4
- package/dist/SelectableGrid.d.mts +44 -0
- package/dist/SelectableGrid.d.ts +44 -0
- package/dist/SelectableGrid.js +448 -0
- package/dist/SelectableGrid.mjs +9 -0
- package/dist/Separator.mjs +1 -0
- package/dist/Sheet.d.mts +13 -1
- package/dist/Sheet.d.ts +13 -1
- package/dist/Sheet.js +115 -5
- package/dist/Sheet.mjs +4 -2
- package/dist/Skeleton.d.mts +50 -0
- package/dist/Skeleton.d.ts +50 -0
- package/dist/Skeleton.js +61 -0
- package/dist/Skeleton.mjs +4 -2
- package/dist/Slider.js +51 -4
- package/dist/Slider.mjs +3 -2
- package/dist/Spinner.js +28 -7
- package/dist/Spinner.mjs +2 -1
- package/dist/Switch.js +98 -48
- package/dist/Switch.mjs +4 -3
- package/dist/TabBar.d.mts +42 -0
- package/dist/TabBar.d.ts +42 -0
- package/dist/TabBar.js +361 -0
- package/dist/TabBar.mjs +6 -0
- package/dist/Tabs.js +92 -62
- package/dist/Tabs.mjs +5 -4
- package/dist/Text.js +16 -0
- package/dist/Text.mjs +2 -1
- package/dist/Textarea.mjs +4 -3
- package/dist/Toast.d.mts +7 -7
- package/dist/Toast.d.ts +7 -7
- package/dist/Toast.mjs +1 -0
- package/dist/Toggle.d.mts +6 -3
- package/dist/Toggle.d.ts +6 -3
- package/dist/Toggle.js +135 -120
- package/dist/Toggle.mjs +5 -5
- package/dist/VirtualList.mjs +1 -0
- package/dist/{chunk-7H2OR44A.mjs → chunk-26BCI223.mjs} +1 -1
- package/dist/{chunk-CRYBX2CM.mjs → chunk-2TFTAWVJ.mjs} +44 -59
- package/dist/chunk-3DKJ2GIC.mjs +30 -0
- package/dist/{chunk-KWCPOM6W.mjs → chunk-3U4SSNWP.mjs} +32 -48
- package/dist/chunk-4I7D47FH.mjs +139 -0
- package/dist/chunk-4K625MVM.mjs +142 -0
- package/dist/{chunk-MN7OG7IY.mjs → chunk-6OAZJ577.mjs} +6 -4
- package/dist/{chunk-L7E7TVEZ.mjs → chunk-756RAKE4.mjs} +2 -2
- package/dist/{chunk-HSPSMN6U.mjs → chunk-7QHVVCB3.mjs} +2 -2
- package/dist/{chunk-URLL5JBR.mjs → chunk-A3A6KNQN.mjs} +3 -3
- package/dist/chunk-AJ7ZDNBT.mjs +120 -0
- package/dist/{chunk-FTLJOUOQ.mjs → chunk-AV4EMIRH.mjs} +25 -28
- package/dist/chunk-AZJF2BLK.mjs +115 -0
- package/dist/chunk-BNP626TY.mjs +159 -0
- package/dist/{chunk-5IKW3VNC.mjs → chunk-DVK4G2GT.mjs} +17 -1
- package/dist/{chunk-6LQYY7HC.mjs → chunk-EH745HE5.mjs} +2 -2
- package/dist/chunk-EJ7ZPXOH.mjs +163 -0
- package/dist/{chunk-RKLHUDZS.mjs → chunk-GD6KXMG5.mjs} +29 -15
- package/dist/{chunk-RR2VQLKE.mjs → chunk-GQYFLP3D.mjs} +14 -17
- package/dist/{chunk-Y6MXOREN.mjs → chunk-ID72TK46.mjs} +8 -17
- package/dist/{chunk-NQGVLMWG.mjs → chunk-JMOZEC77.mjs} +1 -1
- package/dist/{chunk-GCWOGZYL.mjs → chunk-JT7HKXRB.mjs} +39 -29
- package/dist/{chunk-LWG526VX.mjs → chunk-KIHCWCWL.mjs} +47 -62
- package/dist/chunk-LXJIIOYQ.mjs +104 -0
- package/dist/{chunk-SBZYEV4S.mjs → chunk-M6ZXVBTK.mjs} +5 -2
- package/dist/{chunk-XDMN67KV.mjs → chunk-MAC465BB.mjs} +10 -8
- package/dist/chunk-MBMXYJJV.mjs +36 -0
- package/dist/chunk-MLF3EZFW.mjs +119 -0
- package/dist/chunk-NA7PARID.mjs +147 -0
- package/dist/{chunk-QXGYKWI7.mjs → chunk-O3HA6TYM.mjs} +9 -4
- package/dist/{chunk-63357L2X.mjs → chunk-OB4JUQ3O.mjs} +1 -1
- package/dist/{chunk-AU2VDY4P.mjs → chunk-PFZTM6D5.mjs} +52 -4
- package/dist/chunk-QKH5ZOD5.mjs +97 -0
- package/dist/{chunk-KZJRQOIU.mjs → chunk-TERDKCLE.mjs} +11 -1
- package/dist/{chunk-U4N7WF4Z.mjs → chunk-UREA2GYY.mjs} +28 -23
- package/dist/{chunk-TAJ2PQ2O.mjs → chunk-VGTDN7SW.mjs} +7 -6
- package/dist/{chunk-URDE3EUU.mjs → chunk-VQ57HWPL.mjs} +27 -15
- package/dist/chunk-WBOOUHSS.mjs +62 -0
- package/dist/{chunk-GNGLDL6Z.mjs → chunk-WJLKJMKR.mjs} +18 -0
- package/dist/{chunk-YZJAFS4P.mjs → chunk-X4G6APW6.mjs} +22 -19
- package/dist/chunk-Y6FXYEAI.mjs +8 -0
- package/dist/chunk-YFZ3ELX5.mjs +16 -0
- package/dist/{chunk-QCNARS3X.mjs → chunk-YNROWHQJ.mjs} +1 -1
- package/dist/chunk-Z4BVUWW6.mjs +196 -0
- package/dist/{chunk-GPOUINK5.mjs → chunk-ZJKGQMYH.mjs} +10 -27
- package/dist/index-wt-orHUi.d.mts +85 -0
- package/dist/index-wt-orHUi.d.ts +85 -0
- package/dist/index.d.mts +59 -51
- package/dist/index.d.ts +59 -51
- package/dist/index.js +1940 -744
- package/dist/index.mjs +49 -39
- package/package.json +35 -5
- package/src/components/Accordion/Accordion.tsx +12 -1
- package/src/components/AlertBanner/AlertBanner.tsx +5 -0
- package/src/components/AppHeader/AppHeader.tsx +172 -0
- package/src/components/AppHeader/index.ts +1 -0
- package/src/components/Avatar/Avatar.tsx +10 -2
- package/src/components/Badge/Badge.tsx +8 -1
- package/src/components/Button/Button.tsx +20 -27
- package/src/components/Card/Card.tsx +12 -23
- package/src/components/CategoryStrip/CategoryStrip.tsx +17 -21
- package/src/components/Checkbox/Checkbox.tsx +26 -40
- package/src/components/Chip/Chip.tsx +24 -33
- package/src/components/CurrencyInput/CurrencyInput.tsx +10 -8
- package/src/components/EmptyState/EmptyState.tsx +2 -1
- package/src/components/ErrorBoundary/ErrorBoundary.tsx +153 -0
- package/src/components/ErrorBoundary/index.ts +1 -0
- package/src/components/HolographicCard/HolographicCard.tsx +315 -0
- package/src/components/HolographicCard/index.ts +1 -0
- package/src/components/IconButton/IconButton.tsx +19 -27
- package/src/components/ImageViewer/ImageViewer.tsx +290 -0
- package/src/components/ImageViewer/index.ts +1 -0
- package/src/components/ListItem/ListItem.tsx +70 -67
- package/src/components/MediaCard/MediaCard.tsx +8 -2
- package/src/components/MenuItem/MenuItem.tsx +10 -25
- package/src/components/MonthPicker/MonthPicker.tsx +39 -13
- package/src/components/MonthPicker/index.ts +1 -1
- package/src/components/PagerDots/PagerDots.tsx +200 -0
- package/src/components/PagerDots/index.ts +1 -0
- package/src/components/Pressable/Pressable.tsx +19 -35
- package/src/components/PricingCard/PricingCard.tsx +220 -0
- package/src/components/PricingCard/index.ts +1 -0
- package/src/components/RadioGroup/RadioGroup.tsx +14 -27
- package/src/components/RetrayProvider/RetrayProvider.tsx +59 -0
- package/src/components/RetrayProvider/index.ts +1 -0
- package/src/components/SelectableGrid/SelectableGrid.tsx +205 -0
- package/src/components/SelectableGrid/index.ts +1 -0
- package/src/components/Sheet/Sheet.tsx +65 -1
- package/src/components/Skeleton/Skeleton.tsx +142 -1
- package/src/components/Spinner/Spinner.tsx +17 -2
- package/src/components/Switch/Switch.tsx +30 -58
- package/src/components/TabBar/TabBar.tsx +169 -0
- package/src/components/TabBar/index.ts +1 -0
- package/src/components/Tabs/Tabs.tsx +23 -26
- package/src/components/Text/Text.tsx +2 -0
- package/src/components/Toggle/Toggle.tsx +35 -51
- package/src/fonts.ts +4 -1
- package/src/index.ts +23 -2
- package/src/utils/animations.ts +29 -1
- package/src/utils/fontGuard.ts +34 -0
- package/src/utils/haptics.ts +211 -9
- package/src/utils/pressable.ts +66 -0
- package/dist/chunk-76PFOSM2.mjs +0 -41
- package/dist/chunk-DITNP6PL.mjs +0 -106
- package/dist/chunk-JBLL7U3U.mjs +0 -64
- package/dist/chunk-LG4DO3DK.mjs +0 -174
- package/dist/chunk-RMMK64W5.mjs +0 -54
- package/dist/chunk-RTC3CFXF.mjs +0 -29
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import React, { useEffect, useMemo } from 'react'
|
|
2
|
+
import { StyleSheet, ViewStyle, TouchableOpacity, Platform } from 'react-native'
|
|
3
|
+
import {
|
|
4
|
+
Canvas,
|
|
5
|
+
Group,
|
|
6
|
+
Image as SkiaImage,
|
|
7
|
+
useImage,
|
|
8
|
+
RoundedRect,
|
|
9
|
+
LinearGradient,
|
|
10
|
+
vec,
|
|
11
|
+
} from '@shopify/react-native-skia'
|
|
12
|
+
import Animated, {
|
|
13
|
+
useSharedValue,
|
|
14
|
+
useDerivedValue,
|
|
15
|
+
useAnimatedStyle,
|
|
16
|
+
withTiming,
|
|
17
|
+
} from 'react-native-reanimated'
|
|
18
|
+
import { usePressScale } from '../../utils/usePressScale'
|
|
19
|
+
import { PRESS_SCALE } from '../../utils/animations'
|
|
20
|
+
import { impactLight } from '../../utils/haptics'
|
|
21
|
+
import { RADIUS } from '../../tokens'
|
|
22
|
+
|
|
23
|
+
// ─── Foil Color Presets ───────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/** Available foil color preset names */
|
|
26
|
+
export type FoilPreset =
|
|
27
|
+
| 'rainbow'
|
|
28
|
+
| 'gold'
|
|
29
|
+
| 'silver'
|
|
30
|
+
| 'cosmic'
|
|
31
|
+
| 'emerald'
|
|
32
|
+
| 'rose'
|
|
33
|
+
| 'ocean'
|
|
34
|
+
| 'fire'
|
|
35
|
+
| 'aurora'
|
|
36
|
+
| 'neon'
|
|
37
|
+
|
|
38
|
+
/** Foil color presets — each is an array of RGBA colors for the gradient */
|
|
39
|
+
export const FOIL_PRESETS: Record<FoilPreset, string[]> = {
|
|
40
|
+
// Classic holographic rainbow
|
|
41
|
+
rainbow: [
|
|
42
|
+
'rgba(255, 0, 128, 0.45)',
|
|
43
|
+
'rgba(255, 200, 0, 0.40)',
|
|
44
|
+
'rgba(0, 255, 170, 0.40)',
|
|
45
|
+
'rgba(0, 170, 255, 0.45)',
|
|
46
|
+
'rgba(180, 0, 255, 0.45)',
|
|
47
|
+
],
|
|
48
|
+
// Premium gold foil
|
|
49
|
+
gold: [
|
|
50
|
+
'rgba(255, 215, 0, 0.50)',
|
|
51
|
+
'rgba(255, 180, 0, 0.45)',
|
|
52
|
+
'rgba(218, 165, 32, 0.40)',
|
|
53
|
+
'rgba(255, 223, 128, 0.50)',
|
|
54
|
+
'rgba(184, 134, 11, 0.45)',
|
|
55
|
+
],
|
|
56
|
+
// Chrome silver foil
|
|
57
|
+
silver: [
|
|
58
|
+
'rgba(192, 192, 192, 0.50)',
|
|
59
|
+
'rgba(220, 220, 220, 0.45)',
|
|
60
|
+
'rgba(169, 169, 169, 0.40)',
|
|
61
|
+
'rgba(240, 240, 240, 0.50)',
|
|
62
|
+
'rgba(128, 128, 128, 0.45)',
|
|
63
|
+
],
|
|
64
|
+
// Deep space cosmic
|
|
65
|
+
cosmic: [
|
|
66
|
+
'rgba(75, 0, 130, 0.50)',
|
|
67
|
+
'rgba(138, 43, 226, 0.45)',
|
|
68
|
+
'rgba(255, 20, 147, 0.40)',
|
|
69
|
+
'rgba(0, 191, 255, 0.45)',
|
|
70
|
+
'rgba(148, 0, 211, 0.50)',
|
|
71
|
+
],
|
|
72
|
+
// Emerald green luxury
|
|
73
|
+
emerald: [
|
|
74
|
+
'rgba(0, 201, 87, 0.50)',
|
|
75
|
+
'rgba(46, 139, 87, 0.45)',
|
|
76
|
+
'rgba(0, 255, 127, 0.40)',
|
|
77
|
+
'rgba(60, 179, 113, 0.45)',
|
|
78
|
+
'rgba(0, 128, 0, 0.50)',
|
|
79
|
+
],
|
|
80
|
+
// Rose gold / pink
|
|
81
|
+
rose: [
|
|
82
|
+
'rgba(255, 105, 180, 0.50)',
|
|
83
|
+
'rgba(255, 182, 193, 0.45)',
|
|
84
|
+
'rgba(219, 112, 147, 0.40)',
|
|
85
|
+
'rgba(255, 20, 147, 0.45)',
|
|
86
|
+
'rgba(199, 21, 133, 0.50)',
|
|
87
|
+
],
|
|
88
|
+
// Deep ocean blue
|
|
89
|
+
ocean: [
|
|
90
|
+
'rgba(0, 119, 190, 0.50)',
|
|
91
|
+
'rgba(0, 180, 216, 0.45)',
|
|
92
|
+
'rgba(72, 202, 228, 0.40)',
|
|
93
|
+
'rgba(144, 224, 239, 0.45)',
|
|
94
|
+
'rgba(0, 150, 199, 0.50)',
|
|
95
|
+
],
|
|
96
|
+
// Hot fire gradient
|
|
97
|
+
fire: [
|
|
98
|
+
'rgba(255, 69, 0, 0.50)',
|
|
99
|
+
'rgba(255, 140, 0, 0.45)',
|
|
100
|
+
'rgba(255, 215, 0, 0.40)',
|
|
101
|
+
'rgba(255, 99, 71, 0.45)',
|
|
102
|
+
'rgba(220, 20, 60, 0.50)',
|
|
103
|
+
],
|
|
104
|
+
// Aurora borealis
|
|
105
|
+
aurora: [
|
|
106
|
+
'rgba(0, 255, 127, 0.45)',
|
|
107
|
+
'rgba(127, 255, 212, 0.40)',
|
|
108
|
+
'rgba(0, 206, 209, 0.45)',
|
|
109
|
+
'rgba(138, 43, 226, 0.40)',
|
|
110
|
+
'rgba(255, 20, 147, 0.45)',
|
|
111
|
+
],
|
|
112
|
+
// Neon cyberpunk
|
|
113
|
+
neon: [
|
|
114
|
+
'rgba(255, 0, 255, 0.55)',
|
|
115
|
+
'rgba(0, 255, 255, 0.50)',
|
|
116
|
+
'rgba(255, 255, 0, 0.45)',
|
|
117
|
+
'rgba(0, 255, 0, 0.50)',
|
|
118
|
+
'rgba(255, 0, 128, 0.55)',
|
|
119
|
+
],
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Default preset
|
|
123
|
+
const DEFAULT_FOIL_COLORS = FOIL_PRESETS.rainbow
|
|
124
|
+
|
|
125
|
+
// ─── Tilt Configuration ───────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
/** Default max tilt in degrees */
|
|
128
|
+
const DEFAULT_TILT_DEG = 10
|
|
129
|
+
/** Default normalization factor (~30° of device rotation = full tilt) */
|
|
130
|
+
const DEFAULT_NORM_FACTOR = Math.PI / 6
|
|
131
|
+
|
|
132
|
+
export interface HolographicCardProps {
|
|
133
|
+
/** Card art — `require()` asset or `{ uri }`. Omitted = gradient-only foil surface. */
|
|
134
|
+
source?: Parameters<typeof useImage>[0]
|
|
135
|
+
/** Card width (dp). Defaults to 300. */
|
|
136
|
+
width?: number
|
|
137
|
+
/** Card height (dp). Defaults to `width * 1.4` (trading-card ratio). */
|
|
138
|
+
height?: number
|
|
139
|
+
/** Corner radius (dp). Defaults to `RADIUS.md`. */
|
|
140
|
+
borderRadius?: number
|
|
141
|
+
/** React to device motion (gyroscope) for parallax tilt + sheen. Defaults to true. */
|
|
142
|
+
enableTilt?: boolean
|
|
143
|
+
/** Strength of the foil sheen, 0–1. Defaults to 1. */
|
|
144
|
+
intensity?: number
|
|
145
|
+
/** Called when the card is pressed. */
|
|
146
|
+
onPress?: () => void
|
|
147
|
+
style?: ViewStyle
|
|
148
|
+
|
|
149
|
+
// ─── New Customization Props ────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
/** Foil color preset. Defaults to 'rainbow'. Ignored if `foilColors` is provided. */
|
|
152
|
+
foilPreset?: FoilPreset
|
|
153
|
+
/** Custom foil gradient colors (array of RGBA strings). Overrides `foilPreset`. */
|
|
154
|
+
foilColors?: string[]
|
|
155
|
+
/** Maximum tilt angle in degrees (0–45). Defaults to 10. */
|
|
156
|
+
maxTiltDegrees?: number
|
|
157
|
+
/** Sensitivity of tilt to device motion (0.1–2). Higher = more responsive. Defaults to 1. */
|
|
158
|
+
tiltSensitivity?: number
|
|
159
|
+
/** How far the sheen moves relative to tilt (0–1). Defaults to 0.6. */
|
|
160
|
+
sheenSpread?: number
|
|
161
|
+
/** Animation duration for tilt smoothing in ms. Defaults to 80. */
|
|
162
|
+
tiltAnimationDuration?: number
|
|
163
|
+
/** Perspective depth for 3D effect (200–2000). Defaults to 800. */
|
|
164
|
+
perspective?: number
|
|
165
|
+
/** Blend mode for the foil overlay. Defaults to 'plus'. */
|
|
166
|
+
blendMode?: 'plus' | 'screen' | 'overlay' | 'softLight' | 'hardLight'
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Holographic / foil trading-card surface (Pokémon-TCG style). Renders the art
|
|
171
|
+
* on a Skia canvas with an animated rainbow sheen, and tilts in 3D toward device
|
|
172
|
+
* motion. The sheen position tracks the tilt so the foil "catches the light".
|
|
173
|
+
*
|
|
174
|
+
* Deep-import only — keeps Skia out of the main bundle:
|
|
175
|
+
* `import { HolographicCard } from '@retray-dev/ui-kit/HolographicCard'`
|
|
176
|
+
*
|
|
177
|
+
* Requires the optional peers `@shopify/react-native-skia` and (for tilt)
|
|
178
|
+
* `expo-sensors`. Without `expo-sensors` the card renders with a static sheen.
|
|
179
|
+
*/
|
|
180
|
+
export function HolographicCard({
|
|
181
|
+
source,
|
|
182
|
+
width = 300,
|
|
183
|
+
height,
|
|
184
|
+
borderRadius = RADIUS.md,
|
|
185
|
+
enableTilt = true,
|
|
186
|
+
intensity = 1,
|
|
187
|
+
onPress,
|
|
188
|
+
style,
|
|
189
|
+
// New customization props
|
|
190
|
+
foilPreset = 'rainbow',
|
|
191
|
+
foilColors,
|
|
192
|
+
maxTiltDegrees = DEFAULT_TILT_DEG,
|
|
193
|
+
tiltSensitivity = 1,
|
|
194
|
+
sheenSpread = 0.6,
|
|
195
|
+
tiltAnimationDuration = 80,
|
|
196
|
+
perspective = 800,
|
|
197
|
+
blendMode = 'plus',
|
|
198
|
+
}: HolographicCardProps) {
|
|
199
|
+
const h = height ?? width * 1.4
|
|
200
|
+
// Called unconditionally (rules of hooks); useImage returns null for a nullish source.
|
|
201
|
+
const image = useImage((source ?? null) as Parameters<typeof useImage>[0])
|
|
202
|
+
|
|
203
|
+
// Clamp and compute values
|
|
204
|
+
const clampedTilt = Math.max(0, Math.min(45, maxTiltDegrees))
|
|
205
|
+
const clampedSensitivity = Math.max(0.1, Math.min(2, tiltSensitivity))
|
|
206
|
+
const clampedSpread = Math.max(0, Math.min(1, sheenSpread))
|
|
207
|
+
const clampedPerspective = Math.max(200, Math.min(2000, perspective))
|
|
208
|
+
|
|
209
|
+
// Resolve foil colors: custom > preset > default
|
|
210
|
+
const resolvedFoilColors = useMemo(() => {
|
|
211
|
+
if (foilColors && foilColors.length >= 2) return foilColors
|
|
212
|
+
return FOIL_PRESETS[foilPreset] ?? DEFAULT_FOIL_COLORS
|
|
213
|
+
}, [foilColors, foilPreset])
|
|
214
|
+
|
|
215
|
+
// Normalization factor adjusted by sensitivity
|
|
216
|
+
const normFactor = DEFAULT_NORM_FACTOR / clampedSensitivity
|
|
217
|
+
|
|
218
|
+
// Tilt in normalized [-1, 1] on each axis.
|
|
219
|
+
const tiltX = useSharedValue(0)
|
|
220
|
+
const tiltY = useSharedValue(0)
|
|
221
|
+
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
if (!enableTilt || Platform.OS === 'web') return
|
|
224
|
+
let remove: (() => void) | undefined
|
|
225
|
+
let cancelled = false
|
|
226
|
+
// Dynamic import: expo-sensors is an optional peer. Absence = static sheen.
|
|
227
|
+
import('expo-sensors')
|
|
228
|
+
.then(({ DeviceMotion }) => {
|
|
229
|
+
if (cancelled) return
|
|
230
|
+
DeviceMotion.setUpdateInterval(16)
|
|
231
|
+
const sub = DeviceMotion.addListener(({ rotation }) => {
|
|
232
|
+
if (!rotation) return
|
|
233
|
+
const nx = Math.max(-1, Math.min(rotation.gamma / normFactor, 1))
|
|
234
|
+
const ny = Math.max(-1, Math.min(rotation.beta / normFactor, 1))
|
|
235
|
+
tiltX.value = withTiming(nx, { duration: tiltAnimationDuration })
|
|
236
|
+
tiltY.value = withTiming(ny, { duration: tiltAnimationDuration })
|
|
237
|
+
})
|
|
238
|
+
remove = () => sub.remove()
|
|
239
|
+
})
|
|
240
|
+
.catch(() => {
|
|
241
|
+
// expo-sensors not installed — leave tilt at rest.
|
|
242
|
+
})
|
|
243
|
+
return () => {
|
|
244
|
+
cancelled = true
|
|
245
|
+
remove?.()
|
|
246
|
+
}
|
|
247
|
+
}, [enableTilt, tiltX, tiltY, normFactor, tiltAnimationDuration])
|
|
248
|
+
|
|
249
|
+
// 3D parallax — perspective tilt of the whole card toward the device motion.
|
|
250
|
+
const { animatedStyle: pressStyle, onPressIn, onPressOut, hoverHandlers } = usePressScale({
|
|
251
|
+
pressScale: PRESS_SCALE.card,
|
|
252
|
+
disabled: !onPress,
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
const tiltStyle = useAnimatedStyle(() => ({
|
|
256
|
+
transform: [
|
|
257
|
+
{ perspective: clampedPerspective },
|
|
258
|
+
{ rotateX: `${-tiltY.value * clampedTilt}deg` },
|
|
259
|
+
{ rotateY: `${tiltX.value * clampedTilt}deg` },
|
|
260
|
+
],
|
|
261
|
+
}))
|
|
262
|
+
|
|
263
|
+
// Sheen sweeps across the card as it tilts.
|
|
264
|
+
const start = useDerivedValue(() => vec(width * (0.5 - tiltX.value * clampedSpread), h * (0.5 - tiltY.value * clampedSpread)))
|
|
265
|
+
const end = useDerivedValue(() => vec(width * (0.5 + tiltX.value * clampedSpread), h * (0.5 + tiltY.value * clampedSpread)))
|
|
266
|
+
|
|
267
|
+
const rrct = { x: 0, y: 0, width, height: h }
|
|
268
|
+
|
|
269
|
+
const canvas = (
|
|
270
|
+
<Canvas style={{ width, height: h }}>
|
|
271
|
+
{/* Art clipped to the rounded card. */}
|
|
272
|
+
{image ? (
|
|
273
|
+
<Group clip={{ rect: rrct, rx: borderRadius, ry: borderRadius }}>
|
|
274
|
+
<SkiaImage image={image} x={0} y={0} width={width} height={h} fit="cover" />
|
|
275
|
+
</Group>
|
|
276
|
+
) : null}
|
|
277
|
+
{/* Foil sheen — additive rainbow gradient that tracks the tilt. */}
|
|
278
|
+
<RoundedRect x={0} y={0} width={width} height={h} r={borderRadius} opacity={intensity} blendMode={blendMode}>
|
|
279
|
+
<LinearGradient start={start} end={end} colors={resolvedFoilColors} />
|
|
280
|
+
</RoundedRect>
|
|
281
|
+
</Canvas>
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
const card = (
|
|
285
|
+
<Animated.View style={[{ width, height: h }, tiltStyle, style]}>{canvas}</Animated.View>
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
if (!onPress) return card
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<Animated.View style={pressStyle}>
|
|
292
|
+
<TouchableOpacity
|
|
293
|
+
onPress={() => {
|
|
294
|
+
impactLight()
|
|
295
|
+
onPress()
|
|
296
|
+
}}
|
|
297
|
+
onPressIn={onPressIn}
|
|
298
|
+
onPressOut={onPressOut}
|
|
299
|
+
activeOpacity={1}
|
|
300
|
+
touchSoundDisabled={true}
|
|
301
|
+
accessibilityRole="imagebutton"
|
|
302
|
+
{...hoverHandlers}
|
|
303
|
+
style={styles.touch}
|
|
304
|
+
>
|
|
305
|
+
{card}
|
|
306
|
+
</TouchableOpacity>
|
|
307
|
+
</Animated.View>
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const styles = StyleSheet.create({
|
|
312
|
+
touch: {
|
|
313
|
+
alignSelf: 'flex-start',
|
|
314
|
+
},
|
|
315
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './HolographicCard'
|
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import {
|
|
3
|
-
TouchableOpacity,
|
|
4
3
|
ActivityIndicator,
|
|
5
4
|
StyleSheet,
|
|
6
5
|
View,
|
|
7
6
|
Text,
|
|
8
|
-
TouchableOpacityProps,
|
|
9
7
|
ViewStyle,
|
|
10
8
|
} from 'react-native'
|
|
11
|
-
import Animated from 'react-native-reanimated'
|
|
12
9
|
import { impactLight } from '../../utils/haptics'
|
|
13
10
|
import { useTheme } from '../../theme'
|
|
14
11
|
import { s, ms } from '../../utils/scaling'
|
|
15
12
|
import { renderIcon } from '../../utils/icons'
|
|
16
|
-
import {
|
|
17
|
-
import { PRESS_SCALE } from '../../utils/animations'
|
|
13
|
+
import { PressableButton } from '../../utils/pressable'
|
|
18
14
|
|
|
19
15
|
// primary: filled primary
|
|
20
16
|
// secondary: filled surface — icon on neutral bg (Airbnb icon-button-circle)
|
|
@@ -24,7 +20,7 @@ import { PRESS_SCALE } from '../../utils/animations'
|
|
|
24
20
|
export type IconButtonVariant = 'primary' | 'secondary' | 'outline' | 'text' | 'destructive'
|
|
25
21
|
export type IconButtonSize = 'sm' | 'md' | 'lg'
|
|
26
22
|
|
|
27
|
-
export interface IconButtonProps
|
|
23
|
+
export interface IconButtonProps {
|
|
28
24
|
iconName?: string
|
|
29
25
|
icon?: React.ReactNode
|
|
30
26
|
iconColor?: string
|
|
@@ -36,10 +32,16 @@ export interface IconButtonProps extends TouchableOpacityProps {
|
|
|
36
32
|
* The dot/count appears top-right of the button.
|
|
37
33
|
*/
|
|
38
34
|
badge?: boolean | number
|
|
35
|
+
disabled?: boolean
|
|
36
|
+
style?: ViewStyle
|
|
37
|
+
onPress?: () => void
|
|
38
|
+
accessibilityLabel?: string
|
|
39
|
+
accessibilityHint?: string
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
const sizeMap: Record<IconButtonSize, { container: number; icon: number }> = {
|
|
42
|
-
|
|
43
|
+
// AUDIT FIX: sm was 32pt — below Apple HIG 44pt minimum touch target.
|
|
44
|
+
sm: { container: s(44), icon: 18 },
|
|
43
45
|
md: { container: s(44), icon: 20 },
|
|
44
46
|
lg: { container: s(52), icon: 24 },
|
|
45
47
|
}
|
|
@@ -57,18 +59,13 @@ function IconButtonBase({
|
|
|
57
59
|
onPress,
|
|
58
60
|
accessibilityLabel,
|
|
59
61
|
accessibilityHint,
|
|
60
|
-
...props
|
|
61
62
|
}: IconButtonProps) {
|
|
62
63
|
const { colors } = useTheme()
|
|
63
64
|
const isDisabled = disabled || loading
|
|
64
|
-
const { animatedStyle, onPressIn, onPressOut, hoverHandlers } = usePressScale({
|
|
65
|
-
pressScale: PRESS_SCALE.button,
|
|
66
|
-
disabled: isDisabled,
|
|
67
|
-
})
|
|
68
65
|
|
|
69
|
-
const handlePress
|
|
66
|
+
const handlePress = () => {
|
|
70
67
|
impactLight()
|
|
71
|
-
onPress?.(
|
|
68
|
+
onPress?.()
|
|
72
69
|
}
|
|
73
70
|
|
|
74
71
|
const containerVariantStyle: ViewStyle = {
|
|
@@ -104,11 +101,8 @@ function IconButtonBase({
|
|
|
104
101
|
const showCount = typeof badge === 'number' && badge > 0
|
|
105
102
|
|
|
106
103
|
return (
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
{...hoverHandlers}
|
|
110
|
-
>
|
|
111
|
-
<TouchableOpacity
|
|
104
|
+
<View style={styles.wrapper}>
|
|
105
|
+
<PressableButton
|
|
112
106
|
style={[
|
|
113
107
|
styles.base,
|
|
114
108
|
containerVariantStyle,
|
|
@@ -116,24 +110,22 @@ function IconButtonBase({
|
|
|
116
110
|
isDisabled && styles.disabled,
|
|
117
111
|
style,
|
|
118
112
|
]}
|
|
119
|
-
|
|
120
|
-
activeOpacity={1}
|
|
121
|
-
touchSoundDisabled={true}
|
|
113
|
+
enabled={!isDisabled}
|
|
122
114
|
onPress={handlePress}
|
|
123
|
-
|
|
124
|
-
|
|
115
|
+
rippleColor="transparent"
|
|
116
|
+
touchSoundDisabled
|
|
117
|
+
activateOnHover
|
|
125
118
|
accessibilityRole="button"
|
|
126
119
|
accessibilityLabel={accessibilityLabel ?? iconName ?? 'icon button'}
|
|
127
120
|
accessibilityHint={accessibilityHint}
|
|
128
121
|
accessibilityState={{ disabled: isDisabled, busy: loading }}
|
|
129
|
-
{...props}
|
|
130
122
|
>
|
|
131
123
|
{loading ? (
|
|
132
124
|
<ActivityIndicator size="small" color={spinnerColor} />
|
|
133
125
|
) : (
|
|
134
126
|
resolvedIcon
|
|
135
127
|
)}
|
|
136
|
-
</
|
|
128
|
+
</PressableButton>
|
|
137
129
|
{showBadge && (
|
|
138
130
|
<View style={[
|
|
139
131
|
styles.badge,
|
|
@@ -147,7 +139,7 @@ function IconButtonBase({
|
|
|
147
139
|
)}
|
|
148
140
|
</View>
|
|
149
141
|
)}
|
|
150
|
-
</
|
|
142
|
+
</View>
|
|
151
143
|
)
|
|
152
144
|
}
|
|
153
145
|
|